Initial commit: ROA2WEB - FastAPI + Vue.js + Telegram Bot
Modern ERP Reports Application with microservices architecture Tech Stack: - Backend: FastAPI + python-oracledb (Oracle DB integration) - Frontend: Vue.js 3 + PrimeVue + Vite - Telegram Bot: python-telegram-bot + SQLite - Infrastructure: Shared database pool, JWT authentication, SSH tunnel Features: - FastAPI backend with async Oracle connection pool - Vue.js 3 responsive frontend with PrimeVue components - Telegram bot alternative interface - Microservices architecture with shared components - Complete deployment support (Linux Docker + Windows IIS) - Comprehensive testing (Playwright E2E + pytest) Repository Structure: - reports-app/ - Main application (backend, frontend, telegram-bot) - shared/ - Shared components (database pool, auth, utils) - deployment/ - Deployment scripts (Linux & Windows) - docs/ - Project documentation - security/ - Security scanning and git hooks
This commit is contained in:
908
reports-app/frontend/tests/ANDROID_TESTING_GUIDE.md
Normal file
908
reports-app/frontend/tests/ANDROID_TESTING_GUIDE.md
Normal file
@@ -0,0 +1,908 @@
|
||||
# Ghid Testare pe Telefon Android Real cu Chrome DevTools MCP
|
||||
|
||||
Ghid complet pentru configurarea testării aplicației ROA2WEB pe telefon Android real folosind ADB WiFi și Chrome DevTools MCP server.
|
||||
|
||||
## De ce Chrome DevTools MCP?
|
||||
|
||||
**Avantaje fata de emulare Playwright:**
|
||||
- [OK] Testezi pe hardware real (touch, senzori, performanta reala)
|
||||
- [OK] Vezi exact cum arata pe telefonul tau
|
||||
- [OK] Claude Code poate controla direct telefonul prin MCP
|
||||
- [OK] Performance profiling real
|
||||
- [OK] Network throttling real
|
||||
- [OK] Screenshot-uri de pe dispozitiv real
|
||||
|
||||
## Cerinte
|
||||
|
||||
- Telefon Android (versiune 10+) pentru ADB WiFi
|
||||
- Windows 10/11 cu PowerShell
|
||||
- WSL/Linux pentru development
|
||||
- Chrome instalat pe telefon
|
||||
- Telefon si calculator in aceeasi retea WiFi
|
||||
|
||||
## Arhitectura Retea
|
||||
|
||||
```
|
||||
Android Phone Windows Host WSL Environment
|
||||
(10.0.20.114) (10.0.20.144) (172.18.251.234)
|
||||
| | |
|
||||
|--- ADB WiFi ----------| |
|
||||
| | |
|
||||
| |--- Port Proxy --------|
|
||||
| | (9222, 3000, 8001) |
|
||||
| | |
|
||||
|<-- http://localhost:3000 (reverse proxy) --> App (Vite)
|
||||
| |
|
||||
|<-- http://localhost:8001 (reverse proxy) --> API (FastAPI)
|
||||
|
|
||||
Claude Code (WSL) --> MCP --> http://10.0.20.144:9222 --> ADB Forward --> Chrome on Phone
|
||||
```
|
||||
|
||||
**IP-uri importante:**
|
||||
- Phone WiFi IP: 10.0.20.114 (variaza)
|
||||
- Windows physical IP: 10.0.20.144 (variaza)
|
||||
- WSL IP: 172.18.251.234 (WSL internal network)
|
||||
- WSL gateway: 172.18.240.1
|
||||
|
||||
---
|
||||
|
||||
## Pas 1: Instalare ADB (Android Debug Bridge) pe Windows
|
||||
|
||||
**IMPORTANT:** ADB trebuie instalat pe Windows, NU in WSL! WSL2 nu poate vedea dispozitivele USB conectate la Windows, iar chiar si cu ADB WiFi exista probleme de networking intre WSL2 si dispozitivele Android.
|
||||
|
||||
### Metoda 1: Winget (Recomandat)
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Instalare ADB Platform Tools
|
||||
winget install Google.PlatformTools
|
||||
|
||||
# Verifica instalarea
|
||||
adb version
|
||||
```
|
||||
|
||||
**Daca adb version nu returneaza nimic:**
|
||||
|
||||
ADB poate fi instalat de winget dar nu adaugat automat in PATH. Locatie posibila:
|
||||
```
|
||||
C:\Users\[USERNAME]\AppData\Local\Microsoft\WinGet\Packages\Google.PlatformTools_Microsoft.Winget.Source_8wekyb3d8bbwe\platform-tools\
|
||||
```
|
||||
|
||||
**Adauga temporar in PATH (sesiunea curenta):**
|
||||
```powershell
|
||||
$env:Path += ";C:\Users\$env:USERNAME\AppData\Local\Microsoft\WinGet\Packages\Google.PlatformTools_Microsoft.Winget.Source_8wekyb3d8bbwe\platform-tools"
|
||||
|
||||
# Verifica
|
||||
adb version
|
||||
```
|
||||
|
||||
**Adauga permanent in PATH:**
|
||||
```powershell
|
||||
# Windows PowerShell (Administrator)
|
||||
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";C:\Users\$env:USERNAME\AppData\Local\Microsoft\WinGet\Packages\Google.PlatformTools_Microsoft.Winget.Source_8wekyb3d8bbwe\platform-tools", "Machine")
|
||||
```
|
||||
|
||||
### Metoda 2: Download Manual
|
||||
|
||||
Download de pe site oficial:
|
||||
```
|
||||
https://developer.android.com/tools/releases/platform-tools
|
||||
```
|
||||
|
||||
1. Download "SDK Platform-Tools for Windows"
|
||||
2. Extrage in `C:\platform-tools`
|
||||
3. Adauga `C:\platform-tools` in PATH (System Environment Variables)
|
||||
|
||||
---
|
||||
|
||||
## Pas 2: Configurare Telefon Android
|
||||
|
||||
### 1. Activeaza "Developer Options"
|
||||
|
||||
```
|
||||
Setari -> Despre telefon -> Apasa de 7 ori pe "Numar compilare" (Build number)
|
||||
```
|
||||
|
||||
Vei vedea un mesaj: "Esti acum developer!"
|
||||
|
||||
### 2. Activeaza USB Debugging si Wireless Debugging
|
||||
|
||||
```
|
||||
Setari -> System -> Optiuni pentru dezvoltatori
|
||||
-> Activeaza "USB debugging"
|
||||
-> Activeaza "Wireless debugging"
|
||||
```
|
||||
|
||||
**Pe unele telefoane:**
|
||||
- Samsung: Setari -> Optiuni dezvoltator -> USB debugging + Wireless debugging
|
||||
- Xiaomi: Setari -> Setari suplimentare -> Optiuni pentru dezvoltatori
|
||||
- Huawei: Setari -> System & updates -> Developer options
|
||||
|
||||
**IMPORTANT:** Android 10+ este necesar pentru Wireless debugging.
|
||||
|
||||
### 3. Conectare ADB WiFi
|
||||
|
||||
**Pe telefon:**
|
||||
```
|
||||
Setari -> Developer options -> Wireless debugging -> "Pair device with pairing code"
|
||||
```
|
||||
|
||||
Vei vedea un dialog cu:
|
||||
- **Pairing code:** 6 cifre (ex: 123456)
|
||||
- **IP address & Port:** ex: 10.0.20.114:37639
|
||||
|
||||
**NOTA IMPORTANTA:** Exista doua porturi diferite:
|
||||
- **Pairing port:** Pentru asociere initiala (ex: 37639)
|
||||
- **Wireless debugging port:** Pentru conexiune permanenta (diferit de pairing port!)
|
||||
|
||||
**In Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Pas 1: Pair (prima data, foloseste pairing port)
|
||||
adb pair 10.0.20.114:37639
|
||||
# Introdu codul de 6 cifre cand este cerut
|
||||
|
||||
# Pas 2: Verifica portul wireless debugging (MAIN PORT)
|
||||
# Pe telefon: Wireless debugging screen -> IP address & Port (diferit de pairing!)
|
||||
# Exemplu: 10.0.20.114:38261
|
||||
|
||||
# Pas 3: Connect (foloseste wireless debugging port, NU pairing port!)
|
||||
adb connect 10.0.20.114:38261
|
||||
|
||||
# Verifica conexiunea
|
||||
adb devices
|
||||
```
|
||||
|
||||
**Output asteptat:**
|
||||
```
|
||||
List of devices attached
|
||||
10.0.20.114:38261 device
|
||||
```
|
||||
|
||||
**Daca vezi `unauthorized`:**
|
||||
- Deblocheaza telefonul
|
||||
- Accepta prompt-ul "Allow wireless debugging"
|
||||
- Bifeaza "Always allow from this computer"
|
||||
|
||||
### 4. Troubleshooting Conexiune
|
||||
|
||||
**Error: "failed to connect"**
|
||||
```powershell
|
||||
# Restart ADB server
|
||||
adb kill-server
|
||||
adb start-server
|
||||
|
||||
# Reincearca pairing si connect
|
||||
```
|
||||
|
||||
**Pairing sau connect nu raspunde:**
|
||||
- Verifica telefonul si PC-ul sunt pe aceeasi retea WiFi
|
||||
- Dezactiveaza si reactiveaza "Wireless debugging" pe telefon
|
||||
- Verifica nu exista restrictii WiFi (guest network, isolation)
|
||||
|
||||
---
|
||||
|
||||
## Pas 3: Configurare Port Forwarding Complet
|
||||
|
||||
Port forwarding este necesar la TREI niveluri pentru ca totul sa functioneze:
|
||||
1. **ADB forward** - Phone Chrome -> Windows localhost
|
||||
2. **Windows port proxy** - Windows -> WSL (pentru MCP si acces aplicatie)
|
||||
3. **Firewall rules** - Permite conexiuni pe porturile necesare
|
||||
|
||||
### A) ADB Port Forwarding (Chrome DevTools)
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# Forward Chrome DevTools Protocol de pe telefon la Windows
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote
|
||||
|
||||
# Verifica port forwarding
|
||||
adb forward --list
|
||||
```
|
||||
|
||||
**Output asteptat:**
|
||||
```
|
||||
10.0.20.114:38261 tcp:9222 localabstract:chrome_devtools_remote
|
||||
```
|
||||
|
||||
**Testeaza conexiunea:**
|
||||
|
||||
Deschide in browser Windows desktop:
|
||||
```
|
||||
http://localhost:9222/json/version
|
||||
```
|
||||
|
||||
Ar trebui sa vezi informatii despre Chrome de pe telefon (versiune, WebSocket URL, etc.)
|
||||
|
||||
### B) Windows Port Proxy (pentru WSL/MCP Access)
|
||||
|
||||
**Windows PowerShell (Administrator):**
|
||||
|
||||
```powershell
|
||||
# Port proxy pentru Chrome DevTools (WSL -> Windows -> Phone)
|
||||
netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=127.0.0.1
|
||||
|
||||
# Port proxy pentru aplicatie (Phone -> Windows -> WSL)
|
||||
# Inlocuieste 172.18.251.234 cu IP-ul tau WSL!
|
||||
netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=172.18.251.234
|
||||
netsh interface portproxy add v4tov4 listenport=8001 listenaddress=0.0.0.0 connectport=8001 connectaddress=172.18.251.234
|
||||
|
||||
# Verifica configurarea
|
||||
netsh interface portproxy show all
|
||||
```
|
||||
|
||||
**Cum afli IP-ul WSL:**
|
||||
|
||||
```bash
|
||||
# In WSL bash
|
||||
ip route show | grep default | awk '{print $3}' # Gateway IP
|
||||
hostname -I | awk '{print $1}' # WSL IP direct
|
||||
```
|
||||
|
||||
### C) Configurare Firewall
|
||||
|
||||
**Windows PowerShell (Administrator):**
|
||||
|
||||
```powershell
|
||||
# Firewall rule pentru Chrome DevTools
|
||||
New-NetFirewallRule -DisplayName "Chrome-DevTools-Android" -Direction Inbound -LocalPort 9222 -Protocol TCP -Action Allow
|
||||
|
||||
# Firewall rules pentru aplicatie ROA2WEB
|
||||
New-NetFirewallRule -DisplayName "ROA2WEB-Frontend-WSL" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow
|
||||
New-NetFirewallRule -DisplayName "ROA2WEB-Backend-WSL" -Direction Inbound -LocalPort 8001 -Protocol TCP -Action Allow
|
||||
|
||||
# Verifica rules
|
||||
Get-NetFirewallRule -DisplayName "*ROA2WEB*" | Select-Object DisplayName, Enabled, Direction
|
||||
Get-NetFirewallRule -DisplayName "Chrome-DevTools-Android" | Select-Object DisplayName, Enabled, Direction
|
||||
```
|
||||
|
||||
### D) Script Automatizat pentru Setup Complet
|
||||
|
||||
In loc de comenzi manuale, foloseste scriptul automatizat:
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
Scriptul:
|
||||
- Verifica ADB si telefon conectat
|
||||
- Configureaza ADB forward pentru Chrome DevTools
|
||||
- Configureaza Windows port proxy (9222, 3000, 8001)
|
||||
- Testeaza conexiunea la Chrome pe telefon
|
||||
- Afiseaza informatii de retea si configurare MCP
|
||||
|
||||
### E) Testare Completa
|
||||
|
||||
**In WSL:**
|
||||
|
||||
```bash
|
||||
# Testeaza acces Chrome DevTools de la WSL
|
||||
# Inlocuieste 10.0.20.144 cu IP-ul tau Windows!
|
||||
curl http://10.0.20.144:9222/json/version
|
||||
```
|
||||
|
||||
**Pe telefon Chrome:**
|
||||
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
Daca aplicatia se incarca, port forwarding functioneaza corect!
|
||||
|
||||
### F) Verificare chrome://inspect (Optional)
|
||||
|
||||
Pe calculatorul tau, in Chrome desktop Windows:
|
||||
```
|
||||
chrome://inspect#devices
|
||||
```
|
||||
|
||||
Ar trebui sa vezi telefonul tau listat si tab-urile deschise in Chrome pe telefon.
|
||||
|
||||
---
|
||||
|
||||
## Pas 4: Instalare si Configurare Chrome DevTools MCP Server
|
||||
|
||||
### 1. Configurare MCP in Claude Code (WSL)
|
||||
|
||||
**IMPORTANT:** Pentru Claude Code care ruleaza in WSL, trebuie sa folosim IP-ul FIZIC al Windows, NU localhost!
|
||||
|
||||
**Editare fisier de configurare:**
|
||||
|
||||
```bash
|
||||
# In WSL
|
||||
nano ~/.claude.json
|
||||
```
|
||||
|
||||
**Gaseste sau adauga sectiunea chrome-devtools-android:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools-android": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browser-url",
|
||||
"http://10.0.20.144:9222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**NOTA CRITICA:** Inlocuieste `10.0.20.144` cu IP-ul FIZIC al calculatorului tau Windows!
|
||||
|
||||
**Cum afli IP-ul fizic Windows:**
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
ipconfig | findstr "IPv4"
|
||||
```
|
||||
|
||||
Output exemplu:
|
||||
```
|
||||
IPv4 Address. . . . . . . . . . . : 10.0.20.144
|
||||
```
|
||||
|
||||
**De ce NU localhost:9222?**
|
||||
|
||||
In WSL, `localhost` se refera la WSL intern, NU la Windows host. Deoarece ADB forward asculta pe Windows localhost, trebuie sa accesam prin IP-ul fizic Windows cu Windows port proxy configurat.
|
||||
|
||||
### 2. Alternativ: Chrome DevTools pentru Browser Desktop (Optional)
|
||||
|
||||
Daca vrei sa controlezi atat Chrome desktop CAT si Chrome Android:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browser-url",
|
||||
"http://localhost:9222"
|
||||
]
|
||||
},
|
||||
"chrome-devtools-android": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"chrome-devtools-mcp@latest",
|
||||
"--browser-url",
|
||||
"http://10.0.20.144:9222"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Reload Claude Code
|
||||
|
||||
Dupa salvarea configuratiei:
|
||||
|
||||
**VSCode (Claude Code):**
|
||||
```
|
||||
Ctrl+Shift+P -> "Developer: Reload Window"
|
||||
```
|
||||
|
||||
Sau restart complet VSCode.
|
||||
|
||||
### 4. Verificare MCP Functioneaza
|
||||
|
||||
**In WSL bash (pentru debugging):**
|
||||
|
||||
```bash
|
||||
# Testeaza ca MCP poate ajunge la Chrome pe telefon
|
||||
curl http://10.0.20.144:9222/json/version
|
||||
```
|
||||
|
||||
**In Claude Code:**
|
||||
|
||||
Cere Claude Code:
|
||||
```
|
||||
"Folosind chrome-devtools-android, fa un screenshot de pe telefonul Android"
|
||||
```
|
||||
|
||||
Daca totul este configurat corect, Claude Code va putea controla Chrome pe telefon!
|
||||
|
||||
---
|
||||
|
||||
## Pas 5: Testare pe Telefon Android
|
||||
|
||||
### Workflow Complet de Testare
|
||||
|
||||
#### 1. Setup Initial (Prima Data)
|
||||
|
||||
**Windows PowerShell (Administrator):**
|
||||
|
||||
```powershell
|
||||
# A) Conectare telefon WiFi
|
||||
adb pair 10.0.20.114:PAIRING_PORT # Cu pairing code
|
||||
adb connect 10.0.20.114:MAIN_PORT # Wireless debugging port
|
||||
|
||||
# B) Setup complet port forwarding si firewall
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
|
||||
# Scriptul configureaza automat:
|
||||
# - ADB forward pentru Chrome DevTools (9222)
|
||||
# - Windows port proxy pentru WSL access
|
||||
# - Firewall rules pentru porturile necesare
|
||||
# - Reverse port forwarding pentru acces localhost pe telefon
|
||||
```
|
||||
|
||||
#### 2. Porneste Aplicatia ROA2WEB
|
||||
|
||||
**In WSL:**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
Aplicatia va porni:
|
||||
- Backend (FastAPI): http://localhost:8001
|
||||
- Frontend (Vite): http://localhost:3000
|
||||
|
||||
#### 3. Acceseaza Aplicatia pe Telefon
|
||||
|
||||
**Pe telefon Chrome:**
|
||||
|
||||
Opțiunea 1 (Recomandat - cu reverse proxy):
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
Opțiunea 2 (Alternativ - IP Windows direct):
|
||||
```
|
||||
http://10.0.20.144:3000
|
||||
```
|
||||
|
||||
**Important:** Daca folosesti localhost pe telefon, trebuie sa fi rulat scriptul `android-test-setup.ps1` care configureaza reverse port forwarding automat!
|
||||
|
||||
#### 4. Control Chrome pe Telefon prin Claude Code
|
||||
|
||||
In Claude Code (WSL), poti cere:
|
||||
|
||||
```
|
||||
"Folosind chrome-devtools-android, fa un screenshot de pe telefonul Android"
|
||||
"Navigheaza la pagina de facturi pe telefon"
|
||||
"Verifica performanta dashboard-ului pe telefon"
|
||||
"Analizeaza console errors de pe Chrome Android"
|
||||
```
|
||||
|
||||
Claude Code poate:
|
||||
- [OK] Captura screenshot-uri de pe telefon
|
||||
- [OK] Naviga intre pagini
|
||||
- [OK] Analiza performance
|
||||
- [OK] Inspectie DOM
|
||||
- [OK] Citire console logs
|
||||
- [OK] Network request monitoring
|
||||
|
||||
---
|
||||
|
||||
## Comenzi Utile ADB (Windows PowerShell)
|
||||
|
||||
### Verificare conexiune:
|
||||
```powershell
|
||||
adb devices -l
|
||||
```
|
||||
|
||||
### Verificare Chrome este pornit pe telefon:
|
||||
```powershell
|
||||
adb shell dumpsys activity activities | Select-String -Pattern "chrome"
|
||||
```
|
||||
|
||||
### Restart ADB server:
|
||||
```powershell
|
||||
adb kill-server
|
||||
adb start-server
|
||||
adb devices
|
||||
```
|
||||
|
||||
### Port forwarding manual:
|
||||
```powershell
|
||||
# Forward Chrome DevTools
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote
|
||||
|
||||
# Reverse port forwarding (pentru acces localhost pe telefon)
|
||||
adb reverse tcp:3000 tcp:3000
|
||||
adb reverse tcp:8001 tcp:8001
|
||||
|
||||
# Verifica forwarding
|
||||
adb forward --list
|
||||
adb reverse --list
|
||||
```
|
||||
|
||||
### Logcat in timp real:
|
||||
```powershell
|
||||
adb logcat | Select-String -Pattern "chrome"
|
||||
```
|
||||
|
||||
### Informatii retea:
|
||||
```powershell
|
||||
# IP Windows fizic
|
||||
ipconfig | findstr "IPv4"
|
||||
|
||||
# Verifica port proxy Windows
|
||||
netsh interface portproxy show all
|
||||
|
||||
# Verifica firewall rules
|
||||
Get-NetFirewallRule -DisplayName "*ROA2WEB*" | Select-Object DisplayName, Enabled
|
||||
Get-NetFirewallRule -DisplayName "Chrome-DevTools-Android" | Select-Object DisplayName, Enabled
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow Zilnic de Testare
|
||||
|
||||
### Dimineata (Setup Rapid):
|
||||
|
||||
**Windows PowerShell:**
|
||||
|
||||
```powershell
|
||||
# 1. Conectare telefon WiFi (daca s-a deconectat)
|
||||
adb connect 10.0.20.114:38261 # Inlocuieste cu IP:PORT-ul tau
|
||||
|
||||
# 2. Verifica conexiune
|
||||
adb devices
|
||||
|
||||
# 3. Setup port forwarding complet
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
### Pornire Aplicatie:
|
||||
|
||||
**In WSL:**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web
|
||||
./start-dev.sh
|
||||
```
|
||||
|
||||
### Testare pe Telefon:
|
||||
|
||||
**Pe telefon Chrome:**
|
||||
```
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
**In Claude Code:**
|
||||
```
|
||||
"Folosind chrome-devtools-android, fa un screenshot de pe telefon"
|
||||
"Testeaza dashboard-ul pe telefonul Android"
|
||||
"Verifica performanta paginii de facturi pe telefon"
|
||||
```
|
||||
|
||||
### Seara (Cleanup):
|
||||
|
||||
**In WSL:**
|
||||
|
||||
```bash
|
||||
cd /mnt/e/proiecte/roa2web/roa2web/reports-app/frontend
|
||||
./scripts/android-disconnect.sh
|
||||
```
|
||||
|
||||
Script cleanup:
|
||||
- Sterge toate ADB forward rules
|
||||
- Sterge toate ADB reverse rules
|
||||
- Cleanup complet pentru deconectare sigura
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problema: `adb: no devices/emulators found`
|
||||
|
||||
**Cauze posibile:**
|
||||
1. Telefonul nu este conectat WiFi la aceeasi retea
|
||||
2. Wireless debugging nu este activat
|
||||
3. ADB server nu ruleaza
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
# 1. Verifica ADB server ruleaza
|
||||
adb devices
|
||||
|
||||
# 2. Restart ADB server
|
||||
adb kill-server
|
||||
adb start-server
|
||||
|
||||
# 3. Reconnect telefon WiFi
|
||||
adb connect 10.0.20.114:38261 # Inlocuieste cu IP:PORT-ul tau
|
||||
```
|
||||
|
||||
### Problema: `device unauthorized`
|
||||
|
||||
**Solutie:**
|
||||
1. Deblocheaza telefonul
|
||||
2. Ar trebui sa apara prompt-ul de autorizare
|
||||
3. Bifeaza "Always allow from this computer"
|
||||
4. Restart ADB:
|
||||
|
||||
```powershell
|
||||
adb kill-server
|
||||
adb start-server
|
||||
adb devices
|
||||
```
|
||||
|
||||
### Problema: Nu pot accesa http://localhost:9222 din Windows
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```powershell
|
||||
# Verifica port forwarding
|
||||
adb forward --list
|
||||
|
||||
# Re-forward portul
|
||||
adb forward --remove-all
|
||||
adb forward tcp:9222 localabstract:chrome_devtools_remote
|
||||
|
||||
# Deschide Chrome pe telefon si incearca din nou
|
||||
curl http://localhost:9222/json/version
|
||||
```
|
||||
|
||||
### Problema: Nu pot accesa http://10.0.20.144:9222 din WSL
|
||||
|
||||
**Cauza:** Windows port proxy nu este configurat sau firewall blocheaza.
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell (Administrator)
|
||||
|
||||
# 1. Configureaza port proxy
|
||||
netsh interface portproxy add v4tov4 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=127.0.0.1
|
||||
|
||||
# 2. Adauga firewall rule
|
||||
New-NetFirewallRule -DisplayName "Chrome-DevTools-Android" -Direction Inbound -LocalPort 9222 -Protocol TCP -Action Allow
|
||||
|
||||
# 3. Verifica din WSL
|
||||
```
|
||||
|
||||
```bash
|
||||
# WSL
|
||||
curl http://10.0.20.144:9222/json/version # Inlocuieste cu IP-ul tau Windows
|
||||
```
|
||||
|
||||
### Problema: Pe telefon nu se incarca aplicatia (http://localhost:3000)
|
||||
|
||||
**Cauza:** Reverse port forwarding nu este configurat.
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
# Ruleaza scriptul care configureaza automat reverse forwarding
|
||||
cd E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts
|
||||
.\android-test-setup.ps1
|
||||
```
|
||||
|
||||
Sau manual:
|
||||
```powershell
|
||||
adb reverse tcp:3000 tcp:3000
|
||||
adb reverse tcp:8001 tcp:8001
|
||||
|
||||
# Verifica
|
||||
adb reverse --list
|
||||
```
|
||||
|
||||
### Problema: Pe telefon "ERR_EMPTY_RESPONSE" sau "This site can't be reached"
|
||||
|
||||
**Verificari:**
|
||||
|
||||
1. **Telefonul si PC-ul sunt in aceeasi retea WiFi?**
|
||||
|
||||
2. **Windows port proxy este configurat?**
|
||||
```powershell
|
||||
netsh interface portproxy show all
|
||||
# Ar trebui sa vezi forwarding pentru 3000 si 8001
|
||||
```
|
||||
|
||||
3. **Firewall permite conexiuni?**
|
||||
```powershell
|
||||
Get-NetFirewallRule -DisplayName "*ROA2WEB*"
|
||||
```
|
||||
|
||||
4. **Backend ruleaza pe 0.0.0.0:8001 (nu doar 127.0.0.1)?**
|
||||
```bash
|
||||
# In WSL
|
||||
netstat -tulpn | grep 8001
|
||||
# Ar trebui: 0.0.0.0:8001
|
||||
```
|
||||
|
||||
### Problema: Chrome DevTools MCP nu se conecteaza din Claude Code
|
||||
|
||||
**Cauze posibile:**
|
||||
1. MCP configurat cu localhost in loc de IP fizic Windows
|
||||
2. Port proxy nu este configurat
|
||||
3. Chrome nu ruleaza pe telefon
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```bash
|
||||
# 1. Verifica configuratie MCP in WSL
|
||||
cat ~/.claude.json | grep browser-url
|
||||
# Ar trebui sa fie: http://10.0.20.144:9222 (IP fizic Windows, NU localhost!)
|
||||
|
||||
# 2. Testeaza manual din WSL
|
||||
curl http://10.0.20.144:9222/json/version
|
||||
|
||||
# 3. Daca nu functioneaza, verifica Windows port proxy
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell (Administrator)
|
||||
netsh interface portproxy show all
|
||||
# Ar trebui sa vezi: 9222 -> 127.0.0.1:9222
|
||||
|
||||
# 4. Reload Claude Code
|
||||
# VSCode: Ctrl+Shift+P -> "Developer: Reload Window"
|
||||
```
|
||||
|
||||
### Problema: "adb pair" nu raspunde sau timeout
|
||||
|
||||
**Solutie:**
|
||||
1. Verifica telefonul si PC sunt pe aceeasi retea WiFi (nu guest network!)
|
||||
2. Dezactiveaza si reactiveaza "Wireless debugging" pe telefon
|
||||
3. Incearca alt port (genereaza un nou pairing code)
|
||||
4. Verifica nu exista WiFi isolation (router settings)
|
||||
|
||||
### Problema: MCP screenshot deschide Chrome pe calculator, nu pe telefon
|
||||
|
||||
**Cauza:** MCP configurat cu `http://localhost:9222` in loc de IP fizic Windows.
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```bash
|
||||
# WSL - Editeaza configuratie MCP
|
||||
nano ~/.claude.json
|
||||
|
||||
# Schimba localhost cu IP fizic Windows:
|
||||
# "http://localhost:9222" -> "http://10.0.20.144:9222"
|
||||
|
||||
# Afla IP Windows:
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell
|
||||
ipconfig | findstr "IPv4"
|
||||
```
|
||||
|
||||
### Problema: WSL nu poate accesa serviciile (3000, 8001)
|
||||
|
||||
**Cauza:** Port proxy inversat (WSL -> Windows in loc de Windows -> WSL).
|
||||
|
||||
Port proxy corect:
|
||||
- **Pentru MCP (WSL -> Windows -> Phone):** Listen pe 0.0.0.0:9222, connect la 127.0.0.1:9222
|
||||
- **Pentru aplicatie (Phone -> Windows -> WSL):** Listen pe 0.0.0.0:3000, connect la 172.18.x.x:3000
|
||||
|
||||
**Solutie:**
|
||||
|
||||
```powershell
|
||||
# Windows PowerShell (Administrator)
|
||||
# Sterge port proxy gresit
|
||||
netsh interface portproxy delete v4tov4 listenport=3000 listenaddress=0.0.0.0
|
||||
|
||||
# Adauga port proxy corect (Phone -> WSL)
|
||||
# Inlocuieste 172.18.251.234 cu IP-ul tau WSL!
|
||||
netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=172.18.251.234
|
||||
netsh interface portproxy add v4tov4 listenport=8001 listenaddress=0.0.0.0 connectport=8001 connectaddress=172.18.251.234
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comparatie: Playwright Emulation vs Chrome DevTools MCP
|
||||
|
||||
| Aspect | Playwright Emulation | Chrome DevTools MCP (Real Device) |
|
||||
|--------|---------------------|----------------------------------|
|
||||
| **Setup** | [OK] Simplu, zero config | [WARN] Necesita ADB WiFi + port forwarding |
|
||||
| **Viteza** | [OK] Foarte rapid | [WARN] Mai lent (network latency) |
|
||||
| **Acuratete vizuala** | [WARN] Nu exact ca pe telefon | [OK] 100% real |
|
||||
| **Touch gestures** | [ERROR] Simulate | [OK] Touch real |
|
||||
| **Performance** | [ERROR] Hardware desktop | [OK] Performance reala telefon |
|
||||
| **CI/CD** | [OK] Perfect | [ERROR] Nu se poate (necesita device fizic) |
|
||||
| **Debug interactiv** | [WARN] Limitat | [OK] Excelent |
|
||||
| **Cost** | [OK] Gratis, instantaneu | [WARN] Timp + configurare |
|
||||
| **Platform** | [OK] Orice OS | [WARN] Necesita Windows + WSL |
|
||||
|
||||
**Recomandare:**
|
||||
- **Development zilnic:** Playwright emulation (rapid, suficient)
|
||||
- **Final testing:** Chrome DevTools MCP pe telefon real (asigura calitate)
|
||||
- **CI/CD:** Playwright emulation
|
||||
|
||||
---
|
||||
|
||||
## De ce WSL ADB nu functioneaza?
|
||||
|
||||
**Limitare tehnica:** WSL2 nu poate accesa dispozitive USB conectate la Windows host.
|
||||
|
||||
**Explicatie:**
|
||||
- WSL2 ruleaza intr-un lightweight VM (Hyper-V)
|
||||
- USB passthrough nu este suportat complet in WSL2
|
||||
- Chiar si cu ADB WiFi, exista probleme de networking intre WSL si Android device
|
||||
|
||||
**Solutia:** Foloseste ADB din Windows PowerShell direct, apoi Windows port proxy pentru a permite WSL (Claude Code) sa acceseze Chrome DevTools prin IP fizic Windows.
|
||||
|
||||
---
|
||||
|
||||
## Scripturi Disponibile
|
||||
|
||||
| Script | Platform | Descriere |
|
||||
|--------|----------|-----------|
|
||||
| **android-test-setup.ps1** | Windows PowerShell | Setup complet: ADB forward, port proxy, firewall |
|
||||
| **android-disconnect.sh** | Bash/WSL | Cleanup: sterge port forwarding |
|
||||
|
||||
**Screenshot-uri:** Nu mai este nevoie de script! Claude Code face screenshot-uri prin MCP (chrome-devtools-android) inline, fara salvare fisiere.
|
||||
|
||||
**Locatie scripturi:**
|
||||
```
|
||||
E:\proiecte\roa2web\roa2web\reports-app\frontend\scripts\
|
||||
```
|
||||
|
||||
**Documentatie scripturi:** Vedere `scripts/README_ANDROID.md`
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
Dupa configurare completa:
|
||||
|
||||
1. **Testeaza manual** aplicatia pe telefon (http://localhost:3000)
|
||||
2. **Cere Claude Code** sa faca screenshot-uri si sa analizeze layout-ul
|
||||
3. **Verifica performance** pe hardware real
|
||||
4. **Identifica probleme** care nu apar in emulare desktop
|
||||
5. **Optimizeaza** pentru experienta mobile reala
|
||||
6. **Documenteaza** probleme si solutii gasite
|
||||
|
||||
---
|
||||
|
||||
## Resurse Utile
|
||||
|
||||
- **Chrome DevTools MCP:** https://github.com/ChromeDevTools/chrome-devtools-mcp
|
||||
- **Chrome Remote Debugging:** https://developer.chrome.com/docs/devtools/remote-debugging/
|
||||
- **ADB Documentation:** https://developer.android.com/tools/adb
|
||||
- **ADB WiFi Debugging:** https://developer.android.com/studio/command-line/adb#wireless
|
||||
- **Playwright Mobile Testing:** https://playwright.dev/docs/emulation
|
||||
- **Windows Port Proxy:** https://docs.microsoft.com/en-us/windows-server/networking/technologies/netsh/netsh-interface-portproxy
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Arhitectura completa:**
|
||||
- **Phone (Android 10+):** ADB WiFi -> Chrome DevTools Protocol
|
||||
- **Windows Host:** ADB forward (9222) + Windows port proxy (9222, 3000, 8001) + Firewall rules
|
||||
- **WSL (Claude Code):** MCP client -> http://WINDOWS_IP:9222 -> Chrome on Phone
|
||||
|
||||
**Componente cheie:**
|
||||
1. ADB WiFi pairing and connection (Windows PowerShell)
|
||||
2. ADB forward pentru Chrome DevTools (9222)
|
||||
3. Windows port proxy pentru WSL access
|
||||
4. Firewall rules pentru porturile necesare
|
||||
5. MCP configurat cu IP fizic Windows (NU localhost!)
|
||||
6. Reverse port forwarding pentru acces localhost pe telefon
|
||||
|
||||
**Workflow zilnic:**
|
||||
1. Connect phone WiFi (adb connect)
|
||||
2. Run setup script (android-test-setup.ps1)
|
||||
3. Start app (./start-dev.sh in WSL)
|
||||
4. Test on phone (http://localhost:3000)
|
||||
5. Control via Claude Code (screenshots, testing)
|
||||
6. Cleanup (android-disconnect.sh)
|
||||
|
||||
---
|
||||
|
||||
**Autor:** ROA2WEB Development Team
|
||||
**Data:** 2025-10-20
|
||||
**Versiune:** 2.0 (ADB WiFi + Windows Port Forwarding)
|
||||
275
reports-app/frontend/tests/README.md
Normal file
275
reports-app/frontend/tests/README.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# ROA2WEB Frontend E2E Testing with Playwright
|
||||
|
||||
This directory contains end-to-end tests for the ROA2WEB frontend application using Playwright.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 16+ and npm 8+
|
||||
- Frontend development server running on `http://localhost:3001`
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
cd roa2web/reports-app/frontend/
|
||||
npm install
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Run all tests headlessly
|
||||
npm run test:e2e
|
||||
|
||||
# Run tests with browser UI visible
|
||||
npm run test:e2e:headed
|
||||
|
||||
# Run tests in debug mode
|
||||
npm run test:e2e:debug
|
||||
|
||||
# Run tests with Playwright UI mode
|
||||
npm run test:e2e:ui
|
||||
|
||||
# Show test report
|
||||
npm run test:e2e:report
|
||||
```
|
||||
|
||||
## 📁 Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── e2e/ # End-to-end test files
|
||||
│ ├── auth/ # Authentication flow tests
|
||||
│ │ └── login.spec.js
|
||||
│ ├── dashboard/ # Dashboard view tests
|
||||
│ │ └── dashboard.spec.js
|
||||
│ ├── invoices/ # Invoice management tests
|
||||
│ ├── payments/ # Payment tracking tests
|
||||
│ └── responsive/ # Responsive design tests
|
||||
│ └── breakpoints.spec.js
|
||||
├── fixtures/ # Test data and mock responses
|
||||
│ └── auth.js
|
||||
├── page-objects/ # Page Object Models
|
||||
│ ├── BasePage.js
|
||||
│ ├── LoginPage.js
|
||||
│ └── DashboardPage.js
|
||||
├── utils/ # Test utilities
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🎭 Test Categories
|
||||
|
||||
### 1. Authentication Tests (`auth/login.spec.js`)
|
||||
- ✅ Login page display and validation
|
||||
- ✅ Form validation for empty fields
|
||||
- ✅ Successful login flow with API mocking
|
||||
- ✅ Invalid credentials handling
|
||||
- ✅ Loading states and error handling
|
||||
- ✅ Focus management and UX
|
||||
|
||||
### 2. Dashboard Tests (`dashboard/dashboard.spec.js`)
|
||||
- ✅ Dashboard page rendering
|
||||
- ✅ Company selection workflow
|
||||
- ✅ Statistics display and data fetching
|
||||
- ✅ Navigation to invoices/payments views
|
||||
- ✅ API error handling
|
||||
- ✅ Company switching functionality
|
||||
|
||||
### 3. Invoice Management Tests (`invoices/invoices.spec.js`)
|
||||
- ✅ Invoice list display and table functionality
|
||||
- ✅ Search and filtering by invoice number/status
|
||||
- ✅ Sorting by different columns
|
||||
- ✅ Invoice details view
|
||||
- ✅ Data export functionality
|
||||
- ✅ Pagination handling
|
||||
- ✅ API error scenarios
|
||||
|
||||
### 4. Payment Tracking Tests (`payments/payments.spec.js`)
|
||||
- ✅ Payment list display and management
|
||||
- ✅ Filtering by payment method and date range
|
||||
- ✅ Payment totals and summary views
|
||||
- ✅ Export functionality
|
||||
- ✅ Payment details modal/panel
|
||||
- ✅ Method-based grouping and statistics
|
||||
|
||||
### 5. Responsive Design Tests (`responsive/breakpoints.spec.js`)
|
||||
- ✅ Mobile layout (320px) - form stacking, touch targets
|
||||
- ✅ Tablet layout (768px) - grid adjustments
|
||||
- ✅ Desktop layout (1024px+) - full feature layout
|
||||
- ✅ Wide screen (1920px) - content max-width
|
||||
- ✅ Orientation changes (portrait ↔ landscape)
|
||||
- ✅ Touch interaction testing
|
||||
|
||||
## 🏗️ Page Object Pattern
|
||||
|
||||
Tests use the Page Object Model pattern for maintainability:
|
||||
|
||||
### BasePage
|
||||
Base class with common functionality:
|
||||
- API response waiting
|
||||
- Loading state management
|
||||
- Error/success message checking
|
||||
- Navigation helpers
|
||||
|
||||
### LoginPage
|
||||
Encapsulates login page interactions:
|
||||
- Form filling and validation
|
||||
- Error message extraction
|
||||
- Loading state checking
|
||||
- Navigation to login page
|
||||
|
||||
### DashboardPage
|
||||
Handles dashboard-specific operations:
|
||||
- Company selection
|
||||
- Statistics reading
|
||||
- Action button clicks
|
||||
- Content visibility checks
|
||||
|
||||
### InvoicesPage
|
||||
Manages invoice-related interactions:
|
||||
- Invoice table navigation and filtering
|
||||
- Search functionality
|
||||
- Sorting and pagination
|
||||
- Export operations
|
||||
|
||||
### PaymentsPage
|
||||
Handles payment view operations:
|
||||
- Payment filtering and search
|
||||
- Method and date range filters
|
||||
- Summary view switching
|
||||
- Export and totals display
|
||||
|
||||
## 🎯 API Mocking Strategy
|
||||
|
||||
Tests use Playwright's route interception to mock API calls:
|
||||
|
||||
```javascript
|
||||
// Mock successful login
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ /* mock response */ })
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Mocked Endpoints
|
||||
- `POST /api/auth/login` - Authentication
|
||||
- `GET /api/companies` - Company list
|
||||
- `GET /api/invoices/{company}/summary` - Invoice statistics
|
||||
- `GET /api/payments/{company}/summary` - Payment statistics
|
||||
- `GET /api/invoices/{company}` - Invoice list with pagination
|
||||
- `GET /api/payments/{company}` - Payment list with filtering
|
||||
|
||||
## 📱 Responsive Testing
|
||||
|
||||
Tests verify application behavior across different viewport sizes:
|
||||
|
||||
| Breakpoint | Width | Focus Areas |
|
||||
|------------|-------|-------------|
|
||||
| Mobile | 320px | Touch targets, vertical stacking |
|
||||
| Tablet | 768px | Grid layouts, navigation |
|
||||
| Desktop | 1024px+ | Full feature set, horizontal layouts |
|
||||
| Wide | 1920px | Content max-width, spacing |
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Playwright Config (`playwright.config.js`)
|
||||
- **Base URL**: `http://localhost:3001`
|
||||
- **Browsers**: Chromium, Firefox, WebKit, Mobile Chrome, Mobile Safari
|
||||
- **Screenshots**: On failure only
|
||||
- **Videos**: Retained on failure
|
||||
- **Traces**: On first retry
|
||||
|
||||
### Test Environment
|
||||
Tests run against the local development server with mocked backend API calls to ensure consistent, fast, and reliable testing.
|
||||
|
||||
## 📊 Test Reports
|
||||
|
||||
After running tests, view the HTML report:
|
||||
```bash
|
||||
npm run test:e2e:report
|
||||
```
|
||||
|
||||
The report includes:
|
||||
- Test results with pass/fail status
|
||||
- Screenshots of failures
|
||||
- Video recordings of failed tests
|
||||
- Execution timing and performance metrics
|
||||
|
||||
## 🚨 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Tests timing out**
|
||||
- Ensure frontend server is running on port 3001
|
||||
- Check network connectivity
|
||||
- Increase timeout in test configuration
|
||||
|
||||
2. **Element not found errors**
|
||||
- Verify page object selectors match current DOM
|
||||
- Check for dynamic content loading
|
||||
- Add appropriate wait conditions
|
||||
|
||||
3. **API mock not working**
|
||||
- Verify route patterns match actual API calls
|
||||
- Check mock response format
|
||||
- Ensure mocks are set up before navigation
|
||||
|
||||
### Debug Mode
|
||||
Run tests in debug mode to step through execution:
|
||||
```bash
|
||||
npm run test:e2e:debug
|
||||
```
|
||||
|
||||
## 📝 Writing New Tests
|
||||
|
||||
### Test File Structure
|
||||
```javascript
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { YourPageObject } from '../../page-objects/YourPageObject.js';
|
||||
|
||||
test.describe('Feature Name', () => {
|
||||
let pageObject;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
pageObject = new YourPageObject(page);
|
||||
// Setup mocks, navigation, etc.
|
||||
});
|
||||
|
||||
test('should do something', async ({ page }) => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
1. **Use Page Objects** - Encapsulate page interactions
|
||||
2. **Mock API calls** - Avoid dependencies on backend state
|
||||
3. **Wait for elements** - Use proper wait strategies
|
||||
4. **Descriptive test names** - Clear test intent
|
||||
5. **Setup and teardown** - Clean test environment
|
||||
6. **Group related tests** - Use describe blocks effectively
|
||||
|
||||
## 🔄 CI/CD Integration
|
||||
|
||||
Tests are designed to run in continuous integration environments:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions workflow
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Page Object Model Pattern](https://playwright.dev/docs/test-pom)
|
||||
- [API Mocking with Playwright](https://playwright.dev/docs/mock)
|
||||
- [Visual Testing](https://playwright.dev/docs/test-screenshots)
|
||||
|
||||
---
|
||||
|
||||
*Tests implemented following the detailed plan in `PLAYWRIGHT_TESTING_PLAN.md`* 🎭
|
||||
192
reports-app/frontend/tests/e2e/button-fix-test.spec.js
Normal file
192
reports-app/frontend/tests/e2e/button-fix-test.spec.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
|
||||
test.describe('🔧 Button Fix Test - Identify Disabled State Issue', () => {
|
||||
let loginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('🐛 Debug Button Disabled State Logic', async ({ page }) => {
|
||||
console.log('\n=== DEBUGGING BUTTON DISABLED STATE ===');
|
||||
|
||||
// Helper function to get detailed button state
|
||||
const getButtonState = async () => {
|
||||
return await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
|
||||
// Get Vue component data if available
|
||||
const vueApp = document.querySelector('#app').__vue__;
|
||||
let vueData = null;
|
||||
|
||||
try {
|
||||
// Try to access Vue component state
|
||||
const loginComponent = document.querySelector('.login-container').__vueParentComponent;
|
||||
if (loginComponent && loginComponent.setupState) {
|
||||
vueData = {
|
||||
credentials: loginComponent.setupState.credentials?.value,
|
||||
formErrors: loginComponent.setupState.formErrors?.value,
|
||||
isFormValid: loginComponent.setupState.isFormValid?.value
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not access Vue state:', e.message);
|
||||
}
|
||||
|
||||
return {
|
||||
dom: {
|
||||
usernameValue: usernameInput?.value || '',
|
||||
passwordValue: passwordInput?.value || '',
|
||||
buttonDisabled: button?.disabled,
|
||||
buttonClasses: button?.className,
|
||||
usernameRequired: usernameInput?.required,
|
||||
passwordRequired: passwordInput?.required
|
||||
},
|
||||
vue: vueData
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Test 1: Initial state
|
||||
console.log('\n--- Test 1: Initial State ---');
|
||||
let state = await getButtonState();
|
||||
console.log('Initial state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 2: Fill only username
|
||||
console.log('\n--- Test 2: Username Only ---');
|
||||
await page.fill('#username', 'test_user');
|
||||
await page.waitForTimeout(500); // Wait for Vue reactivity
|
||||
state = await getButtonState();
|
||||
console.log('Username only state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 3: Fill both fields
|
||||
console.log('\n--- Test 3: Both Fields ---');
|
||||
await page.fill('#password input', 'test_password');
|
||||
await page.waitForTimeout(500); // Wait for Vue reactivity
|
||||
state = await getButtonState();
|
||||
console.log('Both fields state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 4: Check if validation triggers
|
||||
console.log('\n--- Test 4: Trigger Validation ---');
|
||||
await page.click('.login-card'); // Click outside to blur
|
||||
await page.waitForTimeout(500);
|
||||
state = await getButtonState();
|
||||
console.log('After blur state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 5: Manual button click attempt
|
||||
console.log('\n--- Test 5: Button Click Attempt ---');
|
||||
const isClickable = await page.evaluate(() => {
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
return !button.disabled;
|
||||
});
|
||||
console.log('Button is clickable:', isClickable);
|
||||
|
||||
if (isClickable) {
|
||||
console.log('✅ Button should be clickable');
|
||||
} else {
|
||||
console.log('❌ Button is still disabled - investigating why...');
|
||||
|
||||
// Check validation logic
|
||||
const validationState = await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
|
||||
return {
|
||||
usernameEmpty: !usernameInput.value.trim(),
|
||||
passwordEmpty: !passwordInput.value.trim(),
|
||||
usernameLength: usernameInput.value.length,
|
||||
passwordLength: passwordInput.value.length,
|
||||
formValidity: usernameInput.form?.checkValidity()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Validation details:', JSON.stringify(validationState, null, 2));
|
||||
}
|
||||
|
||||
// Take screenshot for analysis
|
||||
await page.screenshot({ path: 'button-debug.png', fullPage: true });
|
||||
});
|
||||
|
||||
test('🔄 Test Button Reactivity with Real Input', async ({ page }) => {
|
||||
console.log('\n=== TESTING BUTTON REACTIVITY ===');
|
||||
|
||||
// Monitor button state changes
|
||||
const buttonStates = [];
|
||||
|
||||
const checkButton = async (action) => {
|
||||
const disabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
buttonStates.push({ action, disabled });
|
||||
console.log(`After ${action}: disabled = ${disabled}`);
|
||||
};
|
||||
|
||||
await checkButton('initial load');
|
||||
|
||||
// Type character by character to see when button enables
|
||||
const username = 'test';
|
||||
const password = 'pass';
|
||||
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
await page.fill('#username', username.substring(0, i + 1));
|
||||
await page.waitForTimeout(100);
|
||||
await checkButton(`username: "${username.substring(0, i + 1)}"`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
await page.fill('#password input', password.substring(0, i + 1));
|
||||
await page.waitForTimeout(100);
|
||||
await checkButton(`password: "${password.substring(0, i + 1)}"`);
|
||||
}
|
||||
|
||||
console.log('\nButton state progression:');
|
||||
buttonStates.forEach((state, index) => {
|
||||
console.log(`${index + 1}. ${state.action}: ${state.disabled ? 'DISABLED' : 'ENABLED'}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('🎯 Force Button Enable Test', async ({ page }) => {
|
||||
console.log('\n=== TESTING FORCED BUTTON ENABLE ===');
|
||||
|
||||
// Fill valid data
|
||||
await page.fill('#username', 'valid_user');
|
||||
await page.fill('#password input', 'valid_password');
|
||||
|
||||
// Wait for Vue reactivity
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Force enable button via JavaScript if needed
|
||||
const buttonEnabled = await page.evaluate(() => {
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
const wasDisabled = button.disabled;
|
||||
|
||||
// Try to force enable for testing
|
||||
button.disabled = false;
|
||||
button.classList.remove('p-disabled');
|
||||
|
||||
return { wasDisabled, nowDisabled: button.disabled };
|
||||
});
|
||||
|
||||
console.log('Button enable attempt:', buttonEnabled);
|
||||
|
||||
if (!buttonEnabled.nowDisabled) {
|
||||
console.log('✅ Button was successfully enabled');
|
||||
|
||||
// Try to click it now
|
||||
await page.click('button[type="submit"]');
|
||||
console.log('✅ Button click succeeded');
|
||||
|
||||
// Wait for potential API call
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if login was attempted
|
||||
const currentUrl = page.url();
|
||||
console.log('Current URL after click:', currentUrl);
|
||||
|
||||
} else {
|
||||
console.log('❌ Could not enable button');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage.js';
|
||||
|
||||
test.describe('📊 Complete Reports Functionality Test', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
let networkRequests = [];
|
||||
let apiErrors = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset monitoring arrays
|
||||
networkRequests = [];
|
||||
apiErrors = [];
|
||||
|
||||
// Monitor network requests
|
||||
page.on('request', request => {
|
||||
networkRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400) {
|
||||
apiErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText()
|
||||
});
|
||||
console.log(`❌ API Error: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
});
|
||||
|
||||
test('🎯 Complete User Flow: Login → Dashboard → Reports', async ({ page }) => {
|
||||
console.log('\n🎯 === TESTING COMPLETE REPORTS FUNCTIONALITY ===');
|
||||
|
||||
// Phase 1: Authentication
|
||||
console.log('\n📍 Phase 1: Login with real credentials');
|
||||
await loginPage.navigate();
|
||||
|
||||
// Use real test credentials
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
// Submit login and wait for response
|
||||
const [authResponse] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
console.log(`📊 Auth Response: ${authResponse.status()}`);
|
||||
expect(authResponse.status()).toBe(200);
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
console.log('✅ Successfully redirected to dashboard');
|
||||
|
||||
// Phase 2: Test Dashboard Loading
|
||||
console.log('\n📍 Phase 2: Dashboard Data Loading');
|
||||
|
||||
// Wait for dashboard to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check for companies API call
|
||||
const companiesRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/companies') || req.url.includes('/api/companies')
|
||||
);
|
||||
|
||||
console.log(`📊 Companies API requests: ${companiesRequests.length}`);
|
||||
|
||||
if (companiesRequests.length > 0) {
|
||||
console.log('✅ Companies API was called');
|
||||
|
||||
// Check if there were CORS errors
|
||||
const corsErrors = apiErrors.filter(err =>
|
||||
err.url.includes('/companies')
|
||||
);
|
||||
|
||||
if (corsErrors.length > 0) {
|
||||
console.log('❌ CORS errors detected:');
|
||||
corsErrors.forEach(err => {
|
||||
console.log(` - ${err.status} ${err.url}`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ No CORS errors for companies API');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Companies API was not called - checking why...');
|
||||
}
|
||||
|
||||
// Look for company selector dropdown
|
||||
const companySelectors = [
|
||||
'.p-dropdown',
|
||||
'select',
|
||||
'[data-testid="company-select"]',
|
||||
'.company-selector'
|
||||
];
|
||||
|
||||
let companySelectorFound = false;
|
||||
for (const selector of companySelectors) {
|
||||
const element = page.locator(selector).first();
|
||||
if (await element.isVisible()) {
|
||||
console.log(`✅ Company selector found: ${selector}`);
|
||||
companySelectorFound = true;
|
||||
|
||||
// Try to interact with it
|
||||
await element.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Look for dropdown options
|
||||
const options = page.locator('.p-dropdown-item, option');
|
||||
const optionCount = await options.count();
|
||||
console.log(`📊 Company options available: ${optionCount}`);
|
||||
|
||||
if (optionCount > 0) {
|
||||
// Select first company
|
||||
await options.first().click();
|
||||
console.log('✅ Company selected');
|
||||
|
||||
// Wait for data to load after company selection
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check for additional API calls after company selection
|
||||
const invoicesRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/invoices')
|
||||
);
|
||||
const paymentsRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/payments')
|
||||
);
|
||||
|
||||
console.log(`📊 Invoices API requests: ${invoicesRequests.length}`);
|
||||
console.log(`📊 Payments API requests: ${paymentsRequests.length}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!companySelectorFound) {
|
||||
console.log('⚠️ Company selector not found - taking screenshot for analysis');
|
||||
await page.screenshot({ path: 'dashboard-no-company-selector.png', fullPage: true });
|
||||
}
|
||||
|
||||
// Phase 3: Test Navigation to Reports
|
||||
console.log('\n📍 Phase 3: Navigation to Reports');
|
||||
|
||||
// Look for navigation links
|
||||
const navLinks = [
|
||||
'text=/facturi/i',
|
||||
'text=/invoices/i',
|
||||
'text=/încasări/i',
|
||||
'text=/payments/i',
|
||||
'[href*="/invoices"]',
|
||||
'[href*="/payments"]'
|
||||
];
|
||||
|
||||
for (const linkSelector of navLinks) {
|
||||
const link = page.locator(linkSelector).first();
|
||||
if (await link.isVisible()) {
|
||||
const linkText = await link.textContent();
|
||||
console.log(`✅ Found navigation link: "${linkText}"`);
|
||||
|
||||
// Click the link
|
||||
await link.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if we navigated to the correct page
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 Navigated to: ${currentUrl}`);
|
||||
|
||||
// Wait for page content to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Take screenshot of the reports page
|
||||
await page.screenshot({
|
||||
path: `reports-${linkText.toLowerCase().replace(/[^a-z0-9]/g, '-')}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
// Look for data tables or report content
|
||||
const dataElements = [
|
||||
'table',
|
||||
'.p-datatable',
|
||||
'.data-table',
|
||||
'.report-table',
|
||||
'.invoice-list',
|
||||
'.payment-list'
|
||||
];
|
||||
|
||||
let dataFound = false;
|
||||
for (const selector of dataElements) {
|
||||
const element = page.locator(selector);
|
||||
if (await element.isVisible()) {
|
||||
const rowCount = await element.locator('tr, .row').count();
|
||||
console.log(`✅ Data table found with ${rowCount} rows`);
|
||||
dataFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataFound) {
|
||||
console.log('⚠️ No data tables found - checking for loading states or errors');
|
||||
|
||||
// Check for loading indicators
|
||||
const loadingElements = [
|
||||
'.loading',
|
||||
'.p-progress-spinner',
|
||||
'.spinner',
|
||||
'[data-testid="loading"]'
|
||||
];
|
||||
|
||||
for (const loadingSelector of loadingElements) {
|
||||
if (await page.locator(loadingSelector).isVisible()) {
|
||||
console.log('⏳ Loading indicator found - data may still be loading');
|
||||
await page.waitForTimeout(5000); // Wait longer for data
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to dashboard for next test
|
||||
await page.goto('/dashboard');
|
||||
await page.waitForTimeout(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Take final dashboard screenshot
|
||||
await page.screenshot({ path: 'final-dashboard-state.png', fullPage: true });
|
||||
});
|
||||
|
||||
test('🔍 API Endpoints Health Check', async ({ page }) => {
|
||||
console.log('\n🔍 === API ENDPOINTS HEALTH CHECK ===');
|
||||
|
||||
// First authenticate to get access token
|
||||
await loginPage.navigate();
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
const [authResponse] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
expect(authResponse.status()).toBe(200);
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
// Test individual API endpoints
|
||||
const endpoints = [
|
||||
'/api/companies',
|
||||
'/api/invoices',
|
||||
'/api/payments'
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
console.log(`\n--- Testing endpoint: ${endpoint} ---`);
|
||||
|
||||
// Navigate to trigger API call
|
||||
if (endpoint.includes('invoices')) {
|
||||
await page.goto('/invoices');
|
||||
} else if (endpoint.includes('payments')) {
|
||||
await page.goto('/payments');
|
||||
} else {
|
||||
await page.goto('/dashboard');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if API was called
|
||||
const apiCalls = networkRequests.filter(req => req.url.includes(endpoint));
|
||||
|
||||
if (apiCalls.length > 0) {
|
||||
console.log(`✅ ${endpoint} was called (${apiCalls.length} requests)`);
|
||||
|
||||
// Check for errors
|
||||
const errors = apiErrors.filter(err => err.url.includes(endpoint));
|
||||
if (errors.length > 0) {
|
||||
console.log(`❌ Errors found for ${endpoint}:`);
|
||||
errors.forEach(err => {
|
||||
console.log(` - ${err.status} ${err.statusText}`);
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ ${endpoint} returned successful responses`);
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ ${endpoint} was not called`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate test report
|
||||
console.log('\n📋 === TEST REPORT ===');
|
||||
console.log(`🌐 Total Network Requests: ${networkRequests.length}`);
|
||||
console.log(`❌ API Errors: ${apiErrors.length}`);
|
||||
|
||||
if (apiErrors.length > 0) {
|
||||
console.log('\n❌ API Errors Details:');
|
||||
apiErrors.forEach(error => {
|
||||
console.log(` - ${error.status} ${error.url} (${error.statusText})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for specific CORS errors
|
||||
const corsErrors = apiErrors.filter(err =>
|
||||
err.statusText.includes('CORS') || err.status === 0
|
||||
);
|
||||
|
||||
if (corsErrors.length > 0) {
|
||||
console.log('\n🚨 CORS Issues Detected:');
|
||||
corsErrors.forEach(error => {
|
||||
console.log(` - ${error.url}`);
|
||||
});
|
||||
console.log('\n💡 Recommendation: Check CORS configuration in backend');
|
||||
} else {
|
||||
console.log('\n✅ No CORS issues detected');
|
||||
}
|
||||
});
|
||||
});
|
||||
228
reports-app/frontend/tests/e2e/dashboard/dashboard.spec.js
Normal file
228
reports-app/frontend/tests/e2e/dashboard/dashboard.spec.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../../page-objects/DashboardPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
|
||||
test.describe('Dashboard View', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
|
||||
// Mock successful authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User'
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies endpoint
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' },
|
||||
{ code: 'COMP2', name: 'Compania Test 2' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock invoices summary endpoint
|
||||
await page.route('**/api/invoices/*/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 150,
|
||||
paid: 120,
|
||||
overdue: 30,
|
||||
amount: 850000.50
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock payments summary endpoint
|
||||
await page.route('**/api/payments/*/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 125,
|
||||
amount: 750000.25
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Login first
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
});
|
||||
|
||||
test('should display dashboard page correctly', async ({ page: _page }) => {
|
||||
// Check page elements
|
||||
expect(await dashboardPage.isOnDashboardPage()).toBe(true);
|
||||
|
||||
// Check page title contains "Dashboard"
|
||||
const title = await dashboardPage.getPageTitle();
|
||||
expect(title).toContain('Dashboard');
|
||||
|
||||
// Check welcome message includes username
|
||||
const welcomeMessage = await dashboardPage.getWelcomeMessage();
|
||||
expect(welcomeMessage).toContain('testuser');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
// Wait for dashboard to load
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
|
||||
// Should show company selection card
|
||||
expect(await dashboardPage.isCompanySelectionVisible()).toBe(true);
|
||||
|
||||
// Dashboard content should not be visible yet
|
||||
expect(await dashboardPage.isDashboardContentVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display dashboard content after company selection', async ({ page }) => {
|
||||
// Wait for dashboard to load
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
|
||||
// Select a company
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Wait for dashboard content to appear
|
||||
await page.waitForSelector(dashboardPage.dashboardContent, { timeout: 10000 });
|
||||
|
||||
// Dashboard content should now be visible
|
||||
expect(await dashboardPage.isDashboardContentVisible()).toBe(true);
|
||||
|
||||
// Stats cards should be visible
|
||||
expect(await dashboardPage.areStatsCardsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should display correct statistics after company selection', async ({ page }) => {
|
||||
// Wait for dashboard to load and select company
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Wait for stats to load
|
||||
await page.waitForSelector(dashboardPage.statsGrid, { timeout: 10000 });
|
||||
|
||||
// Check statistics values
|
||||
const stats = await dashboardPage.getStatsData();
|
||||
|
||||
expect(stats.invoices).toBe('150');
|
||||
expect(stats.payments).toBe('125');
|
||||
expect(stats.company).toContain('Compania Test 1');
|
||||
});
|
||||
|
||||
test('should navigate to invoices view when clicking invoices action', async ({ page }) => {
|
||||
// Setup dashboard with company selected
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.dashboardContent);
|
||||
|
||||
// Click invoices action button
|
||||
await dashboardPage.clickInvoicesAction();
|
||||
|
||||
// Should navigate to invoices page
|
||||
await page.waitForURL('/invoices');
|
||||
expect(page.url()).toContain('/invoices');
|
||||
});
|
||||
|
||||
test('should navigate to payments view when clicking payments action', async ({ page }) => {
|
||||
// Setup dashboard with company selected
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.dashboardContent);
|
||||
|
||||
// Click payments action button
|
||||
await dashboardPage.clickPaymentsAction();
|
||||
|
||||
// Should navigate to payments page
|
||||
await page.waitForURL('/payments');
|
||||
expect(page.url()).toContain('/payments');
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock companies API error
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
detail: 'Internal server error'
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate to dashboard
|
||||
await dashboardPage.navigate();
|
||||
|
||||
// Should still show the page but might show error messages
|
||||
expect(await dashboardPage.isOnDashboardPage()).toBe(true);
|
||||
|
||||
// Check for error toast messages
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should update stats when switching between companies', async ({ page }) => {
|
||||
// Mock different stats for second company
|
||||
await page.route('**/api/invoices/COMP2/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 200,
|
||||
paid: 180,
|
||||
overdue: 20,
|
||||
amount: 1200000.75
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/payments/COMP2/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 175,
|
||||
amount: 950000.50
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Select first company
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.statsGrid);
|
||||
|
||||
const stats1 = await dashboardPage.getStatsData();
|
||||
expect(stats1.invoices).toBe('150');
|
||||
|
||||
// Switch to second company
|
||||
await dashboardPage.selectCompany('Compania Test 2');
|
||||
await dashboardPage.waitForLoadingToFinish();
|
||||
|
||||
const stats2 = await dashboardPage.getStatsData();
|
||||
expect(stats2.invoices).toBe('200');
|
||||
expect(stats2.company).toContain('Compania Test 2');
|
||||
});
|
||||
});
|
||||
324
reports-app/frontend/tests/e2e/debugging-real-issues.spec.js
Normal file
324
reports-app/frontend/tests/e2e/debugging-real-issues.spec.js
Normal file
@@ -0,0 +1,324 @@
|
||||
//! 🔍 DEBUGGING COMPREHENSIVE TEST - ROA2WEB Real Issues Detection
|
||||
//! Created: 2025-08-04
|
||||
//! Purpose: Find and fix REAL problems, not just "passing tests"
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
|
||||
test.describe('🔍 ROA2WEB Real Issues Debugging Suite', () => {
|
||||
let loginPage;
|
||||
let networkRequests = [];
|
||||
let consoleErrors = [];
|
||||
let apiResponses = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset monitoring arrays
|
||||
networkRequests = [];
|
||||
consoleErrors = [];
|
||||
apiResponses = [];
|
||||
|
||||
// Setup comprehensive monitoring
|
||||
page.on('request', request => {
|
||||
networkRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
headers: request.headers(),
|
||||
postData: request.postData(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
apiResponses.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
headers: response.headers(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log failed requests immediately
|
||||
if (response.status() >= 400) {
|
||||
console.log(`❌ API Error: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
const error = {
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
consoleErrors.push(error);
|
||||
console.log(`🔥 Console Error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', err => {
|
||||
consoleErrors.push({
|
||||
type: 'pageerror',
|
||||
text: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
console.log(`💥 Page Error: ${err.message}`);
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('🧪 REAL AUTH FLOW - Find FormData vs JSON Issues', async ({ page }) => {
|
||||
console.log('\n🔍 === TESTING REAL AUTHENTICATION FLOW ===');
|
||||
|
||||
// Fill real credentials (update these with actual test credentials)
|
||||
const username = 'test_user';
|
||||
const password = 'test_password';
|
||||
|
||||
await page.fill('#username', username);
|
||||
await page.fill('#password input', password);
|
||||
|
||||
// Monitor the actual request being sent
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
// CRITICAL: Analyze the actual request format
|
||||
const request = response.request();
|
||||
const postData = request.postData();
|
||||
const contentType = request.headers()['content-type'];
|
||||
|
||||
console.log('\n📊 === REQUEST ANALYSIS ===');
|
||||
console.log('Content-Type:', contentType);
|
||||
console.log('Request Method:', request.method());
|
||||
console.log('Request Body:', postData);
|
||||
console.log('Response Status:', response.status());
|
||||
|
||||
// Check if FormData or JSON is being sent
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
console.log('✅ Sending JSON (correct)');
|
||||
try {
|
||||
const jsonData = JSON.parse(postData);
|
||||
expect(jsonData).toHaveProperty('username');
|
||||
expect(jsonData).toHaveProperty('password');
|
||||
} catch (e) {
|
||||
console.log('❌ Invalid JSON format');
|
||||
}
|
||||
} else if (contentType && contentType.includes('multipart/form-data')) {
|
||||
console.log('⚠️ Sending FormData (may cause issues)');
|
||||
} else {
|
||||
console.log('❓ Unknown content type:', contentType);
|
||||
}
|
||||
|
||||
// Check response
|
||||
if (response.status() === 422) {
|
||||
const responseBody = await response.text();
|
||||
console.log('🚨 422 Validation Error:', responseBody);
|
||||
}
|
||||
|
||||
// Generate comprehensive monitoring report
|
||||
console.log('\n📈 === MONITORING REPORT ===');
|
||||
console.log(`Network Requests: ${networkRequests.length}`);
|
||||
console.log(`API Responses: ${apiResponses.length}`);
|
||||
console.log(`Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
// Take screenshot for analysis
|
||||
await page.screenshot({
|
||||
path: 'debug-auth-flow.png',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('🔧 LOGIN BUTTON STATE - Debug Disabled Logic', async ({ page }) => {
|
||||
console.log('\n🔍 === DEBUGGING LOGIN BUTTON STATE ===');
|
||||
|
||||
// Test initial state
|
||||
const initialDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Initial button disabled:', initialDisabled);
|
||||
|
||||
// Test empty fields
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
|
||||
const emptyFieldsDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Empty fields - button disabled:', emptyFieldsDisabled);
|
||||
|
||||
// Test with only username
|
||||
await page.fill('#username', 'test');
|
||||
const usernameOnlyDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Username only - button disabled:', usernameOnlyDisabled);
|
||||
|
||||
// Test with both fields
|
||||
await page.fill('#password input', 'password');
|
||||
const bothFieldsDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Both fields - button disabled:', bothFieldsDisabled);
|
||||
|
||||
// Check form validation state
|
||||
const formValidation = await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
|
||||
return {
|
||||
usernameValue: usernameInput?.value,
|
||||
passwordValue: passwordInput?.value,
|
||||
buttonDisabled: button?.disabled,
|
||||
buttonClasses: button?.className,
|
||||
usernameValid: usernameInput?.checkValidity(),
|
||||
passwordValid: passwordInput?.checkValidity()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Form validation state:', JSON.stringify(formValidation, null, 2));
|
||||
|
||||
// Take screenshot of current state
|
||||
await page.screenshot({ path: 'debug-button-state.png' });
|
||||
});
|
||||
|
||||
test('🚨 ERROR MESSAGE FORMAT - Debug Toast Issues', async ({ page }) => {
|
||||
console.log('\n🔍 === DEBUGGING ERROR MESSAGE FORMAT ===');
|
||||
|
||||
// Force a network error by using wrong endpoint
|
||||
await page.route('**/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Server error for testing' })
|
||||
});
|
||||
});
|
||||
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for error message and analyze its content
|
||||
await page.waitForTimeout(2000); // Wait for toast to appear
|
||||
|
||||
// Check various possible error message selectors
|
||||
const errorSelectors = [
|
||||
'.error-message',
|
||||
'.p-toast-message-error',
|
||||
'.p-toast-message-text',
|
||||
'.p-toast-summary',
|
||||
'.p-toast-detail'
|
||||
];
|
||||
|
||||
for (const selector of errorSelectors) {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const text = await elements[i].textContent();
|
||||
if (text && text.trim()) {
|
||||
console.log(`Error message found in ${selector}[${i}]:`, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all visible text content that might contain error messages
|
||||
const allText = await page.evaluate(() => {
|
||||
const textNodes = [];
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
const text = node.textContent.trim();
|
||||
if (text.toLowerCase().includes('eroare') ||
|
||||
text.toLowerCase().includes('error') ||
|
||||
text.toLowerCase().includes('conectare')) {
|
||||
textNodes.push(text);
|
||||
}
|
||||
}
|
||||
return textNodes;
|
||||
});
|
||||
|
||||
console.log('All error-related text found:', allText);
|
||||
|
||||
// Take screenshot of error state
|
||||
await page.screenshot({ path: 'debug-error-messages.png' });
|
||||
});
|
||||
|
||||
test('🌐 NETWORK MONITORING - Real API Behavior', async ({ page }) => {
|
||||
console.log('\n🔍 === COMPREHENSIVE NETWORK MONITORING ===');
|
||||
|
||||
// Test various scenarios
|
||||
const testScenarios = [
|
||||
{ name: 'Valid Login', username: 'valid_user', password: 'valid_pass' },
|
||||
{ name: 'Invalid Credentials', username: 'invalid', password: 'invalid' },
|
||||
{ name: 'Empty Credentials', username: '', password: '' }
|
||||
];
|
||||
|
||||
for (const scenario of testScenarios) {
|
||||
console.log(`\n--- Testing: ${scenario.name} ---`);
|
||||
|
||||
// Clear form
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
|
||||
// Fill credentials if provided
|
||||
if (scenario.username) await page.fill('#username', scenario.username);
|
||||
if (scenario.password) await page.fill('#password input', scenario.password);
|
||||
|
||||
// Reset monitoring arrays for this scenario
|
||||
networkRequests.length = 0;
|
||||
apiResponses.length = 0;
|
||||
consoleErrors.length = 0;
|
||||
|
||||
// Try to submit (if button is enabled)
|
||||
const isDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
if (!isDisabled) {
|
||||
try {
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login', { timeout: 5000 }),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
console.log(`Response Status: ${response.status()}`);
|
||||
const responseBody = await response.text();
|
||||
console.log(`Response Body: ${responseBody.substring(0, 200)}...`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`No API call made: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('Button is disabled - no API call expected');
|
||||
}
|
||||
|
||||
// Wait a bit for any async operations
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
console.log(`Network requests: ${networkRequests.length}`);
|
||||
console.log(`Console errors: ${consoleErrors.length}`);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate final report
|
||||
console.log('\n📋 === FINAL TEST REPORT ===');
|
||||
console.log(`Total Network Requests: ${networkRequests.length}`);
|
||||
console.log(`Total API Responses: ${apiResponses.length}`);
|
||||
console.log(`Total Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('\n🚨 Console Errors Found:');
|
||||
consoleErrors.forEach((error, index) => {
|
||||
console.log(`${index + 1}. ${error.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (apiResponses.some(r => r.status >= 400)) {
|
||||
console.log('\n❌ Failed API Requests:');
|
||||
apiResponses
|
||||
.filter(r => r.status >= 400)
|
||||
.forEach(response => {
|
||||
console.log(`${response.status} ${response.url}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
214
reports-app/frontend/tests/e2e/invoices/invoices.spec.js
Normal file
214
reports-app/frontend/tests/e2e/invoices/invoices.spec.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { InvoicesPage } from '../../page-objects/InvoicesPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
import { mockInvoices } from '../../fixtures/invoices.js';
|
||||
|
||||
test.describe('Invoices View', () => {
|
||||
let loginPage;
|
||||
let invoicesPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
invoicesPage = new InvoicesPage(page);
|
||||
|
||||
// Mock authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock invoices endpoint
|
||||
await page.route('**/api/invoices/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockInvoices),
|
||||
});
|
||||
});
|
||||
|
||||
// Login and navigate to invoices
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
await invoicesPage.navigate();
|
||||
});
|
||||
|
||||
test('should display invoices page correctly', async ({ page: _page }) => {
|
||||
expect(await invoicesPage.isOnInvoicesPage()).toBe(true);
|
||||
|
||||
const title = await invoicesPage.getPageTitle();
|
||||
expect(title).toContain('Facturi');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
expect(await invoicesPage.isCompanySelectionVisible()).toBe(true);
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display invoices table after company selection', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
|
||||
await page.waitForSelector(invoicesPage.invoicesTable, { timeout: 10000 });
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter invoices by search term', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Search for specific invoice
|
||||
await invoicesPage.searchInvoices('INV001');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await invoicesPage.getVisibleInvoicesCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
|
||||
// Check that displayed invoices contain search term
|
||||
const firstRowData = await invoicesPage.getFirstInvoiceData();
|
||||
expect(firstRowData.number).toContain('INV001');
|
||||
});
|
||||
|
||||
test('should filter invoices by status', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Filter by paid status
|
||||
await invoicesPage.filterByStatus('paid');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await invoicesPage.getVisibleInvoicesCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should sort invoices by date', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click date column header to sort
|
||||
await invoicesPage.sortByColumn('date');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify sorting worked
|
||||
const firstRowDate = await invoicesPage.getFirstInvoiceData();
|
||||
expect(firstRowDate.date).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display invoice details when clicking on row', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click on first invoice row
|
||||
await invoicesPage.clickFirstInvoiceRow();
|
||||
|
||||
// Check if details panel or modal appears
|
||||
expect(await invoicesPage.isInvoiceDetailsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should export invoices data', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Set up download handler
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await invoicesPage.clickExportButton();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('facturi');
|
||||
});
|
||||
|
||||
test('should handle pagination correctly', async ({ page }) => {
|
||||
// Mock large dataset
|
||||
await page.route('**/api/invoices/COMP1*', async route => {
|
||||
const url = route.request().url();
|
||||
const urlParams = new URL(url).searchParams;
|
||||
const page_num = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: mockInvoices.slice((page_num - 1) * 10, page_num * 10),
|
||||
total: 25,
|
||||
page: page_num,
|
||||
size: 10,
|
||||
pages: 3
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Check pagination controls appear
|
||||
expect(await invoicesPage.isPaginationVisible()).toBe(true);
|
||||
|
||||
// Navigate to next page
|
||||
await invoicesPage.goToNextPage();
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify page changed
|
||||
const currentPage = await invoicesPage.getCurrentPage();
|
||||
expect(currentPage).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock API error
|
||||
await page.route('**/api/invoices/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Should show error message
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should refresh data when refresh button clicked', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click refresh button
|
||||
await invoicesPage.clickRefreshButton();
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Table should still be visible after refresh
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
254
reports-app/frontend/tests/e2e/payments/payments.spec.js
Normal file
254
reports-app/frontend/tests/e2e/payments/payments.spec.js
Normal file
@@ -0,0 +1,254 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { PaymentsPage } from '../../page-objects/PaymentsPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
import { mockPayments } from '../../fixtures/payments.js';
|
||||
|
||||
test.describe('Payments View', () => {
|
||||
let loginPage;
|
||||
let paymentsPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
paymentsPage = new PaymentsPage(page);
|
||||
|
||||
// Mock authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock payments endpoint
|
||||
await page.route('**/api/payments/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockPayments),
|
||||
});
|
||||
});
|
||||
|
||||
// Login and navigate to payments
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
await paymentsPage.navigate();
|
||||
});
|
||||
|
||||
test('should display payments page correctly', async ({ page: _page }) => {
|
||||
expect(await paymentsPage.isOnPaymentsPage()).toBe(true);
|
||||
|
||||
const title = await paymentsPage.getPageTitle();
|
||||
expect(title).toContain('Încasări');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
expect(await paymentsPage.isCompanySelectionVisible()).toBe(true);
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display payments table after company selection', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
|
||||
await page.waitForSelector(paymentsPage.paymentsTable, { timeout: 10000 });
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter payments by search term', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Search for specific payment
|
||||
await paymentsPage.searchPayments('PAY001');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
|
||||
// Check that displayed payments contain search term
|
||||
const firstRowData = await paymentsPage.getFirstPaymentData();
|
||||
expect(firstRowData.reference).toContain('PAY001');
|
||||
});
|
||||
|
||||
test('should filter payments by method', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Filter by bank transfer
|
||||
await paymentsPage.filterByMethod('bank_transfer');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should sort payments by date', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click date column header to sort
|
||||
await paymentsPage.sortByColumn('date');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify sorting worked
|
||||
const firstRowDate = await paymentsPage.getFirstPaymentData();
|
||||
expect(firstRowDate.date).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display payment details when clicking on row', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click on first payment row
|
||||
await paymentsPage.clickFirstPaymentRow();
|
||||
|
||||
// Check if details panel or modal appears
|
||||
expect(await paymentsPage.isPaymentDetailsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should export payments data', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Set up download handler
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await paymentsPage.clickExportButton();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('incasari');
|
||||
});
|
||||
|
||||
test('should display correct payment totals', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Check if totals card is visible and contains data
|
||||
if (await paymentsPage.isTotalsCardVisible()) {
|
||||
const totals = await paymentsPage.getTotalsData();
|
||||
expect(parseFloat(totals.totalAmount)).toBeGreaterThan(0);
|
||||
expect(parseInt(totals.totalCount)).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter payments by date range', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Apply this month filter
|
||||
await paymentsPage.filterByDateRange('thisMonth');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should handle pagination correctly', async ({ page }) => {
|
||||
// Mock large dataset
|
||||
await page.route('**/api/payments/COMP1*', async route => {
|
||||
const url = route.request().url();
|
||||
const urlParams = new URL(url).searchParams;
|
||||
const page_num = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: mockPayments.slice((page_num - 1) * 10, page_num * 10),
|
||||
total: 25,
|
||||
page: page_num,
|
||||
size: 10,
|
||||
pages: 3
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Check pagination controls appear
|
||||
expect(await paymentsPage.isPaginationVisible()).toBe(true);
|
||||
|
||||
// Navigate to next page
|
||||
await paymentsPage.goToNextPage();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify page changed
|
||||
const currentPage = await paymentsPage.getCurrentPage();
|
||||
expect(currentPage).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock API error
|
||||
await page.route('**/api/payments/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Should show error message
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should refresh data when refresh button clicked', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click refresh button
|
||||
await paymentsPage.clickRefreshButton();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Table should still be visible after refresh
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should group payments by method in summary view', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Switch to summary view if available
|
||||
if (await paymentsPage.isSummaryViewAvailable()) {
|
||||
await paymentsPage.switchToSummaryView();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
expect(await paymentsPage.isSummaryViewVisible()).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
364
reports-app/frontend/tests/e2e/real-world-comprehensive.spec.js
Normal file
364
reports-app/frontend/tests/e2e/real-world-comprehensive.spec.js
Normal file
@@ -0,0 +1,364 @@
|
||||
//! 🌍 COMPREHENSIVE REAL-WORLD TESTING SUITE
|
||||
//! Created: 2025-08-04
|
||||
//! Purpose: Test complete application flows with real data and interactions
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage.js';
|
||||
|
||||
test.describe('🌍 ROA2WEB Real-World Comprehensive Testing', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
let performanceMetrics = [];
|
||||
let networkErrors = [];
|
||||
let consoleErrors = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset metrics
|
||||
performanceMetrics = [];
|
||||
networkErrors = [];
|
||||
consoleErrors = [];
|
||||
|
||||
// Setup comprehensive monitoring
|
||||
page.on('response', response => {
|
||||
const timing = {
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
timing: response.request().timing(),
|
||||
size: response.headers()['content-length'] || 0,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
performanceMetrics.push(timing);
|
||||
|
||||
if (response.status() >= 400) {
|
||||
networkErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push({
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
});
|
||||
|
||||
test('🎯 COMPLETE USER JOURNEY - Login to Dashboard to Reports', async ({ page }) => {
|
||||
console.log('\n🌍 === COMPLETE USER JOURNEY TEST ===');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Phase 1: Navigate to Login
|
||||
console.log('\n📍 Phase 1: Navigate to Login');
|
||||
await loginPage.navigate();
|
||||
await page.screenshot({ path: 'journey-01-login-page.png' });
|
||||
|
||||
// Verify login page loads correctly
|
||||
await expect(page).toHaveTitle(/ROA Reports/);
|
||||
console.log('✅ Login page loaded');
|
||||
|
||||
// Phase 2: Attempt Authentication with Real Credentials
|
||||
console.log('\n📍 Phase 2: Authentication Flow');
|
||||
|
||||
// Test with test credentials first
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
// Verify button becomes enabled
|
||||
await page.waitForTimeout(200);
|
||||
const buttonEnabled = !(await page.locator('button[type="submit"]').isDisabled());
|
||||
expect(buttonEnabled).toBe(true);
|
||||
console.log('✅ Login button enabled with credentials');
|
||||
|
||||
// Monitor the authentication request
|
||||
const authPromise = page.waitForResponse('**/auth/login').catch(() => null);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const authResponse = await authPromise;
|
||||
if (authResponse) {
|
||||
console.log(`📊 Auth Response: ${authResponse.status()}`);
|
||||
|
||||
if (authResponse.status() === 200) {
|
||||
console.log('✅ Authentication successful');
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 }).catch(() => {
|
||||
console.log('⚠️ No redirect to dashboard - checking current state');
|
||||
});
|
||||
|
||||
} else if (authResponse.status() === 422) {
|
||||
console.log('❌ Validation error - checking response');
|
||||
const responseBody = await authResponse.text();
|
||||
console.log('Response body:', responseBody);
|
||||
|
||||
} else if (authResponse.status() === 401) {
|
||||
console.log('❌ Authentication failed - invalid credentials');
|
||||
|
||||
} else {
|
||||
console.log(`❌ Unexpected response: ${authResponse.status()}`);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ No authentication response received');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'journey-02-after-auth.png' });
|
||||
|
||||
// Phase 3: Dashboard Interaction (if successful)
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 Current URL: ${currentUrl}`);
|
||||
|
||||
if (currentUrl.includes('/dashboard')) {
|
||||
console.log('\n📍 Phase 3: Dashboard Interaction');
|
||||
|
||||
// Wait for dashboard to load
|
||||
await page.waitForSelector('.dashboard-container', { timeout: 5000 }).catch(() => {
|
||||
console.log('⚠️ Dashboard container not found');
|
||||
});
|
||||
|
||||
// Test dashboard functionality
|
||||
const companySelector = page.locator('select, .p-dropdown');
|
||||
if (await companySelector.first().isVisible()) {
|
||||
console.log('✅ Company selector visible');
|
||||
|
||||
// Try to select a company
|
||||
await companySelector.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const options = page.locator('.p-dropdown-item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
if (optionCount > 0) {
|
||||
await options.first().click();
|
||||
console.log('✅ Company selected');
|
||||
|
||||
// Wait for data to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if statistics are displayed
|
||||
const statsCards = page.locator('.stat-card, .dashboard-stat, .metric-card');
|
||||
const statsCount = await statsCards.count();
|
||||
console.log(`📊 Statistics cards found: ${statsCount}`);
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'journey-03-dashboard.png' });
|
||||
|
||||
// Phase 4: Navigation Test
|
||||
console.log('\n📍 Phase 4: Navigation Test');
|
||||
|
||||
const navLinks = page.locator('nav a, .nav-link, .menu-item');
|
||||
const navCount = await navLinks.count();
|
||||
console.log(`🧭 Navigation links found: ${navCount}`);
|
||||
|
||||
if (navCount > 0) {
|
||||
// Try to navigate to invoices
|
||||
const invoicesLink = page.locator('text=/facturi|invoice/i').first();
|
||||
if (await invoicesLink.isVisible()) {
|
||||
await invoicesLink.click();
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('✅ Navigated to invoices');
|
||||
await page.screenshot({ path: 'journey-04-invoices.png' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`\n⏱️ Total journey time: ${totalTime}ms`);
|
||||
});
|
||||
|
||||
test('🔍 NETWORK PERFORMANCE ANALYSIS', async ({ page }) => {
|
||||
console.log('\n🔍 === NETWORK PERFORMANCE ANALYSIS ===');
|
||||
|
||||
// Navigate and monitor performance
|
||||
await loginPage.navigate();
|
||||
|
||||
// Wait for all initial requests to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Analyze performance metrics
|
||||
const slowRequests = performanceMetrics.filter(metric => {
|
||||
const timing = metric.timing;
|
||||
return timing && (timing.responseEnd - timing.requestStart) > 2000;
|
||||
});
|
||||
|
||||
const failedRequests = performanceMetrics.filter(metric => metric.status >= 400);
|
||||
|
||||
console.log(`📊 Total requests: ${performanceMetrics.length}`);
|
||||
console.log(`🐌 Slow requests (>2s): ${slowRequests.length}`);
|
||||
console.log(`❌ Failed requests: ${failedRequests.length}`);
|
||||
|
||||
if (slowRequests.length > 0) {
|
||||
console.log('\n🐌 Slow requests:');
|
||||
slowRequests.forEach(request => {
|
||||
const duration = request.timing ?
|
||||
(request.timing.responseEnd - request.timing.requestStart) : 'unknown';
|
||||
console.log(` - ${request.url}: ${duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
if (failedRequests.length > 0) {
|
||||
console.log('\n❌ Failed requests:');
|
||||
failedRequests.forEach(request => {
|
||||
console.log(` - ${request.status} ${request.url}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance assertions
|
||||
expect(slowRequests.length).toBeLessThan(5); // Max 5 slow requests
|
||||
expect(failedRequests.length).toBeLessThan(3); // Max 3 failed requests
|
||||
});
|
||||
|
||||
test('🧪 ERROR HANDLING STRESS TEST', async ({ page }) => {
|
||||
console.log('\n🧪 === ERROR HANDLING STRESS TEST ===');
|
||||
|
||||
await loginPage.navigate();
|
||||
|
||||
// Test various error scenarios
|
||||
const errorScenarios = [
|
||||
{
|
||||
name: 'Server Error 500',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 500, body: '{"detail": "Internal server error"}' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Network Timeout',
|
||||
setup: () => page.route('**/auth/login', route => route.abort('timeout'))
|
||||
},
|
||||
{
|
||||
name: 'Invalid JSON Response',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 200, body: 'invalid json' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Rate Limiting 429',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 429, body: '{"detail": "Too many requests"}' })
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of errorScenarios) {
|
||||
console.log(`\n--- Testing: ${scenario.name} ---`);
|
||||
|
||||
// Setup error scenario
|
||||
await scenario.setup();
|
||||
|
||||
// Fill credentials and submit
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for error handling
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check if error is handled gracefully
|
||||
const errorMessage = await page.locator('.error-message, .p-toast-error').isVisible();
|
||||
console.log(`Error message shown: ${errorMessage}`);
|
||||
|
||||
// Verify user is still on login page
|
||||
const isOnLogin = await loginPage.isOnLoginPage();
|
||||
console.log(`Still on login page: ${isOnLogin}`);
|
||||
|
||||
expect(isOnLogin).toBe(true);
|
||||
|
||||
// Clear the route override
|
||||
await page.unroute('**/auth/login');
|
||||
|
||||
// Clear form for next test
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
test('📱 RESPONSIVE DESIGN VALIDATION', async ({ page }) => {
|
||||
console.log('\n📱 === RESPONSIVE DESIGN VALIDATION ===');
|
||||
|
||||
const viewports = [
|
||||
{ name: 'Mobile Portrait', width: 375, height: 667 },
|
||||
{ name: 'Mobile Landscape', width: 667, height: 375 },
|
||||
{ name: 'Tablet', width: 768, height: 1024 },
|
||||
{ name: 'Desktop', width: 1920, height: 1080 }
|
||||
];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
console.log(`\n--- Testing: ${viewport.name} (${viewport.width}x${viewport.height}) ---`);
|
||||
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||
await loginPage.navigate();
|
||||
|
||||
// Wait for layout to adjust
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check if login form is visible and accessible
|
||||
const formVisible = await page.locator('.login-form').isVisible();
|
||||
const buttonVisible = await page.locator('button[type="submit"]').isVisible();
|
||||
|
||||
console.log(`Form visible: ${formVisible}`);
|
||||
console.log(`Button visible: ${buttonVisible}`);
|
||||
|
||||
expect(formVisible).toBe(true);
|
||||
expect(buttonVisible).toBe(true);
|
||||
|
||||
// Test form interaction
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
|
||||
const buttonEnabled = !(await page.locator('button[type="submit"]').isDisabled());
|
||||
expect(buttonEnabled).toBe(true);
|
||||
|
||||
// Take screenshot for visual verification
|
||||
await page.screenshot({
|
||||
path: `responsive-${viewport.name.toLowerCase().replace(' ', '-')}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate comprehensive test report
|
||||
console.log('\n📋 === COMPREHENSIVE TEST REPORT ===');
|
||||
console.log(`🌐 Total Network Requests: ${performanceMetrics.length}`);
|
||||
console.log(`❌ Network Errors: ${networkErrors.length}`);
|
||||
console.log(`🔥 Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
if (networkErrors.length > 0) {
|
||||
console.log('\n❌ Network Errors:');
|
||||
networkErrors.forEach(error => {
|
||||
console.log(` - ${error.status} ${error.url}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('\n🔥 Console Errors:');
|
||||
consoleErrors.forEach(error => {
|
||||
console.log(` - ${error.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance summary
|
||||
const avgResponseTime = performanceMetrics.length > 0 ?
|
||||
performanceMetrics.reduce((sum, metric) => {
|
||||
const timing = metric.timing;
|
||||
return sum + (timing ? (timing.responseEnd - timing.requestStart) : 0);
|
||||
}, 0) / performanceMetrics.length : 0;
|
||||
|
||||
console.log(`⚡ Average Response Time: ${Math.round(avgResponseTime)}ms`);
|
||||
|
||||
if (avgResponseTime > 1000) {
|
||||
console.log('⚠️ Performance Warning: Average response time is high');
|
||||
}
|
||||
});
|
||||
});
|
||||
237
reports-app/frontend/tests/e2e/responsive/breakpoints.spec.js
Normal file
237
reports-app/frontend/tests/e2e/responsive/breakpoints.spec.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../../page-objects/DashboardPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
|
||||
test.describe('Responsive Design Tests', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
|
||||
// Common setup for all responsive tests
|
||||
const setupMockAuth = async (page) => {
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Test Company' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('Mobile Layout (320px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 568 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display login form correctly on mobile', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
// Take screenshot for visual verification
|
||||
await page.screenshot({ path: 'mobile-login.png', fullPage: true });
|
||||
|
||||
// Login form should be visible and properly sized
|
||||
await expect(page.locator(loginPage.loginCard)).toBeVisible();
|
||||
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
|
||||
await expect(page.locator(loginPage.passwordInput)).toBeVisible();
|
||||
|
||||
// Check that form takes appropriate width
|
||||
const cardWidth = await page.locator(loginPage.loginCard).boundingBox();
|
||||
expect(cardWidth.width).toBeLessThan(320); // Should fit in viewport
|
||||
});
|
||||
|
||||
test('should adapt dashboard layout for mobile', async ({ page }) => {
|
||||
// Login and navigate to dashboard
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: 'mobile-dashboard.png', fullPage: true });
|
||||
|
||||
// Dashboard should be responsive
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
|
||||
// Stats grid should stack vertically on mobile
|
||||
const statsGrid = page.locator(dashboardPage.statsGrid);
|
||||
if (await statsGrid.isVisible()) {
|
||||
const gridBox = await statsGrid.boundingBox();
|
||||
expect(gridBox.width).toBeLessThan(320);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tablet Layout (768px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display login form appropriately on tablet', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
await page.screenshot({ path: 'tablet-login.png', fullPage: true });
|
||||
|
||||
// Login card should be centered and well-proportioned
|
||||
const loginCard = page.locator(loginPage.loginCard);
|
||||
await expect(loginCard).toBeVisible();
|
||||
|
||||
const cardBox = await loginCard.boundingBox();
|
||||
expect(cardBox.width).toBeGreaterThan(300);
|
||||
expect(cardBox.width).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('should show proper tablet dashboard layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'tablet-dashboard.png', fullPage: true });
|
||||
|
||||
// Dashboard elements should be properly spaced
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
|
||||
// Stats should be arranged in appropriate grid
|
||||
const statsCards = page.locator('.stat-card');
|
||||
const cardCount = await statsCards.count();
|
||||
if (cardCount > 0) {
|
||||
// Cards should be visible and properly sized
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
await expect(statsCards.nth(i)).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Desktop Layout (1024px+)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 768 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display full desktop login layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
await page.screenshot({ path: 'desktop-login.png', fullPage: true });
|
||||
|
||||
// Login should be centered with appropriate sizing
|
||||
const loginCard = page.locator(loginPage.loginCard);
|
||||
await expect(loginCard).toBeVisible();
|
||||
|
||||
// Card should not take full width on desktop
|
||||
const cardBox = await loginCard.boundingBox();
|
||||
expect(cardBox.width).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('should show complete desktop dashboard layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'desktop-dashboard.png', fullPage: true });
|
||||
|
||||
// All dashboard elements should be visible
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
await expect(page.locator(dashboardPage.pageSubtitle)).toBeVisible();
|
||||
|
||||
// Stats grid should use horizontal layout
|
||||
const statsGrid = page.locator(dashboardPage.statsGrid);
|
||||
if (await statsGrid.isVisible()) {
|
||||
const gridBox = await statsGrid.boundingBox();
|
||||
expect(gridBox.width).toBeGreaterThan(600);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Wide Screen Layout (1920px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should handle wide screen layouts appropriately', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'widescreen-dashboard.png', fullPage: true });
|
||||
|
||||
// Content should not stretch too wide
|
||||
const mainContent = page.locator('.dashboard-content');
|
||||
if (await mainContent.isVisible()) {
|
||||
const contentBox = await mainContent.boundingBox();
|
||||
// Content should have reasonable max-width
|
||||
expect(contentBox.width).toBeLessThan(1600);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Orientation Changes', () => {
|
||||
test('should handle portrait to landscape orientation', async ({ page }) => {
|
||||
// Start in mobile portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
loginPage = new LoginPage(page);
|
||||
await setupMockAuth(page);
|
||||
|
||||
await loginPage.navigate();
|
||||
await page.screenshot({ path: 'mobile-portrait.png' });
|
||||
|
||||
// Rotate to landscape
|
||||
await page.setViewportSize({ width: 667, height: 375 });
|
||||
await page.waitForTimeout(500); // Allow for reflow
|
||||
|
||||
await page.screenshot({ path: 'mobile-landscape.png' });
|
||||
|
||||
// Login form should still be usable
|
||||
await expect(page.locator(loginPage.loginCard)).toBeVisible();
|
||||
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Touch Interactions', () => {
|
||||
test('should handle touch interactions on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
loginPage = new LoginPage(page);
|
||||
await setupMockAuth(page);
|
||||
|
||||
await loginPage.navigate();
|
||||
|
||||
// Test touch interactions
|
||||
await page.tap(loginPage.usernameInput);
|
||||
await page.fill(loginPage.usernameInput, 'testuser');
|
||||
|
||||
await page.tap(loginPage.passwordInput);
|
||||
await page.fill(loginPage.passwordInput, 'testpass');
|
||||
|
||||
// Login button should be tappable
|
||||
const loginButton = page.locator(loginPage.loginButton);
|
||||
await expect(loginButton).toBeEnabled();
|
||||
|
||||
// Button should have appropriate touch target size (minimum 44px)
|
||||
const buttonBox = await loginButton.boundingBox();
|
||||
expect(buttonBox.height).toBeGreaterThanOrEqual(44);
|
||||
});
|
||||
});
|
||||
});
|
||||
142
reports-app/frontend/tests/fixtures/invoices.js
vendored
Normal file
142
reports-app/frontend/tests/fixtures/invoices.js
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
export const mockInvoices = [
|
||||
{
|
||||
id: 1,
|
||||
number: 'INV001',
|
||||
date: '2024-01-15',
|
||||
client_name: 'SC CLIENT TEST SRL',
|
||||
amount: 25000.50,
|
||||
currency: 'RON',
|
||||
status: 'paid',
|
||||
due_date: '2024-02-15',
|
||||
payment_date: '2024-02-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
number: 'INV002',
|
||||
date: '2024-01-20',
|
||||
client_name: 'CLIENT EXEMPLU SA',
|
||||
amount: 18500.75,
|
||||
currency: 'RON',
|
||||
status: 'unpaid',
|
||||
due_date: '2024-02-20',
|
||||
payment_date: null
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
number: 'INV003',
|
||||
date: '2024-01-25',
|
||||
client_name: 'FIRMA TEST SRL',
|
||||
amount: 12750.00,
|
||||
currency: 'RON',
|
||||
status: 'overdue',
|
||||
due_date: '2024-02-25',
|
||||
payment_date: null
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
number: 'INV004',
|
||||
date: '2024-02-01',
|
||||
client_name: 'COMPANIA ABC SRL',
|
||||
amount: 35000.00,
|
||||
currency: 'RON',
|
||||
status: 'paid',
|
||||
due_date: '2024-03-01',
|
||||
payment_date: '2024-02-28'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
number: 'INV005',
|
||||
date: '2024-02-05',
|
||||
client_name: 'BUSINESS PARTNER SA',
|
||||
amount: 8900.25,
|
||||
currency: 'RON',
|
||||
status: 'unpaid',
|
||||
due_date: '2024-03-05',
|
||||
payment_date: null
|
||||
}
|
||||
];
|
||||
|
||||
export const mockInvoiceDetails = {
|
||||
id: 1,
|
||||
number: 'INV001',
|
||||
date: '2024-01-15',
|
||||
due_date: '2024-02-15',
|
||||
client: {
|
||||
name: 'SC CLIENT TEST SRL',
|
||||
tax_code: 'RO12345678',
|
||||
address: 'Strada Exemplu, Nr. 123, București',
|
||||
phone: '+40 21 123 4567',
|
||||
email: 'contact@clienttest.ro'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
description: 'Servicii consultanță',
|
||||
quantity: 10,
|
||||
unit_price: 2000.00,
|
||||
amount: 20000.00
|
||||
},
|
||||
{
|
||||
description: 'Servicii implementare',
|
||||
quantity: 5,
|
||||
unit_price: 1000.10,
|
||||
amount: 5000.50
|
||||
}
|
||||
],
|
||||
subtotal: 25000.50,
|
||||
tax_rate: 19,
|
||||
tax_amount: 4750.10,
|
||||
total: 29750.60,
|
||||
currency: 'RON',
|
||||
status: 'paid',
|
||||
payment_date: '2024-02-10',
|
||||
payment_method: 'Transfer bancar',
|
||||
notes: 'Factura plătită la termen'
|
||||
};
|
||||
|
||||
export const invoiceStatuses = {
|
||||
paid: 'Plătit',
|
||||
unpaid: 'Neplătit',
|
||||
overdue: 'Întârziat',
|
||||
draft: 'Proiect',
|
||||
cancelled: 'Anulat'
|
||||
};
|
||||
|
||||
export const invoiceFilters = {
|
||||
status: ['all', 'paid', 'unpaid', 'overdue'],
|
||||
dateRange: {
|
||||
thisMonth: 'Această lună',
|
||||
lastMonth: 'Luna trecută',
|
||||
thisYear: 'Acest an',
|
||||
custom: 'Personalizat'
|
||||
},
|
||||
sortBy: {
|
||||
date: 'Data',
|
||||
number: 'Număr',
|
||||
client: 'Client',
|
||||
amount: 'Sumă',
|
||||
status: 'Status'
|
||||
}
|
||||
};
|
||||
|
||||
export const mockApiResponses = {
|
||||
invoicesSuccess: {
|
||||
status: 200,
|
||||
body: {
|
||||
items: mockInvoices,
|
||||
total: mockInvoices.length,
|
||||
page: 1,
|
||||
size: 10,
|
||||
pages: 1
|
||||
}
|
||||
},
|
||||
invoicesError: {
|
||||
status: 500,
|
||||
body: {
|
||||
detail: 'Error fetching invoices'
|
||||
}
|
||||
},
|
||||
invoiceDetailsSuccess: {
|
||||
status: 200,
|
||||
body: mockInvoiceDetails
|
||||
}
|
||||
};
|
||||
176
reports-app/frontend/tests/fixtures/payments.js
vendored
Normal file
176
reports-app/frontend/tests/fixtures/payments.js
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
export const mockPayments = [
|
||||
{
|
||||
id: 1,
|
||||
reference: 'PAY001',
|
||||
date: '2024-02-10',
|
||||
client_name: 'SC CLIENT TEST SRL',
|
||||
amount: 25000.50,
|
||||
currency: 'RON',
|
||||
method: 'bank_transfer',
|
||||
invoice_number: 'INV001',
|
||||
bank_reference: 'TRF240210001',
|
||||
description: 'Plată factură INV001'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
reference: 'PAY002',
|
||||
date: '2024-02-28',
|
||||
client_name: 'COMPANIA ABC SRL',
|
||||
amount: 35000.00,
|
||||
currency: 'RON',
|
||||
method: 'bank_transfer',
|
||||
invoice_number: 'INV004',
|
||||
bank_reference: 'TRF240228002',
|
||||
description: 'Plată factură INV004'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
reference: 'PAY003',
|
||||
date: '2024-02-15',
|
||||
client_name: 'BUSINESS CASH SRL',
|
||||
amount: 5000.00,
|
||||
currency: 'RON',
|
||||
method: 'cash',
|
||||
invoice_number: 'INV010',
|
||||
bank_reference: null,
|
||||
description: 'Plată cash factură INV010'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
reference: 'PAY004',
|
||||
date: '2024-02-20',
|
||||
client_name: 'CARD PAYMENT SA',
|
||||
amount: 12500.75,
|
||||
currency: 'RON',
|
||||
method: 'card',
|
||||
invoice_number: 'INV015',
|
||||
bank_reference: 'CARD240220003',
|
||||
description: 'Plată cu cardul factură INV015'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
reference: 'PAY005',
|
||||
date: '2024-02-25',
|
||||
client_name: 'CHECK COMPANY SRL',
|
||||
amount: 8900.25,
|
||||
currency: 'RON',
|
||||
method: 'check',
|
||||
invoice_number: 'INV020',
|
||||
bank_reference: 'CHECK001234',
|
||||
description: 'Plată cu cec factură INV020'
|
||||
}
|
||||
];
|
||||
|
||||
export const mockPaymentDetails = {
|
||||
id: 1,
|
||||
reference: 'PAY001',
|
||||
date: '2024-02-10',
|
||||
client: {
|
||||
name: 'SC CLIENT TEST SRL',
|
||||
tax_code: 'RO12345678',
|
||||
account_number: 'RO49AAAA1B31007593840000'
|
||||
},
|
||||
amount: 25000.50,
|
||||
currency: 'RON',
|
||||
method: 'bank_transfer',
|
||||
invoice: {
|
||||
number: 'INV001',
|
||||
date: '2024-01-15',
|
||||
amount: 25000.50
|
||||
},
|
||||
bank_details: {
|
||||
reference: 'TRF240210001',
|
||||
bank_name: 'Banca Transilvania',
|
||||
transaction_id: 'BT202402100001',
|
||||
fees: 5.00
|
||||
},
|
||||
description: 'Plată factură INV001',
|
||||
created_at: '2024-02-10T10:30:00Z',
|
||||
created_by: 'admin',
|
||||
notes: 'Plată procesată automat'
|
||||
};
|
||||
|
||||
export const paymentMethods = {
|
||||
bank_transfer: 'Transfer bancar',
|
||||
cash: 'Numerar',
|
||||
card: 'Card',
|
||||
check: 'Cec',
|
||||
other: 'Altă metodă'
|
||||
};
|
||||
|
||||
export const paymentFilters = {
|
||||
method: ['all', 'bank_transfer', 'cash', 'card', 'check'],
|
||||
dateRange: {
|
||||
thisMonth: 'Această lună',
|
||||
lastMonth: 'Luna trecută',
|
||||
thisYear: 'Acest an',
|
||||
custom: 'Personalizat'
|
||||
},
|
||||
sortBy: {
|
||||
date: 'Data',
|
||||
reference: 'Referință',
|
||||
client: 'Client',
|
||||
amount: 'Sumă',
|
||||
method: 'Metodă'
|
||||
}
|
||||
};
|
||||
|
||||
export const mockPaymentSummary = {
|
||||
total_amount: 86400.50,
|
||||
total_count: 5,
|
||||
by_method: {
|
||||
bank_transfer: {
|
||||
amount: 60000.50,
|
||||
count: 2,
|
||||
percentage: 69.5
|
||||
},
|
||||
cash: {
|
||||
amount: 5000.00,
|
||||
count: 1,
|
||||
percentage: 5.8
|
||||
},
|
||||
card: {
|
||||
amount: 12500.75,
|
||||
count: 1,
|
||||
percentage: 14.5
|
||||
},
|
||||
check: {
|
||||
amount: 8900.25,
|
||||
count: 1,
|
||||
percentage: 10.3
|
||||
}
|
||||
},
|
||||
by_month: {
|
||||
'2024-02': {
|
||||
amount: 86400.50,
|
||||
count: 5
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const mockApiResponses = {
|
||||
paymentsSuccess: {
|
||||
status: 200,
|
||||
body: {
|
||||
items: mockPayments,
|
||||
total: mockPayments.length,
|
||||
page: 1,
|
||||
size: 10,
|
||||
pages: 1
|
||||
}
|
||||
},
|
||||
paymentsError: {
|
||||
status: 500,
|
||||
body: {
|
||||
detail: 'Error fetching payments'
|
||||
}
|
||||
},
|
||||
paymentDetailsSuccess: {
|
||||
status: 200,
|
||||
body: mockPaymentDetails
|
||||
},
|
||||
paymentSummarySuccess: {
|
||||
status: 200,
|
||||
body: mockPaymentSummary
|
||||
}
|
||||
};
|
||||
364
reports-app/frontend/tests/integration/README.md
Normal file
364
reports-app/frontend/tests/integration/README.md
Normal file
@@ -0,0 +1,364 @@
|
||||
# ROA2WEB Integration Tests with Console Error Monitoring
|
||||
|
||||
This directory contains comprehensive integration tests for the ROA2WEB application, implementing the full Playwright testing plan with console error monitoring and real Oracle data validation.
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
The integration test suite provides:
|
||||
|
||||
- **Console Error Monitoring** - Comprehensive tracking and classification of frontend errors
|
||||
- **Real Oracle Data Testing** - Integration tests using actual CONTAFIN_ORACLE credentials
|
||||
- **Performance Regression Testing** - Automated baseline validation and monitoring
|
||||
- **Cross-Schema Validation** - Testing data flow between Oracle schemas
|
||||
- **Health Monitoring** - Backend service and database connectivity validation
|
||||
|
||||
## 🏗️ Test Architecture
|
||||
|
||||
```
|
||||
tests/integration/
|
||||
├── real-auth/ # Real authentication tests
|
||||
│ └── oracle-login.spec.js # CONTAFIN_ORACLE credential testing
|
||||
├── real-data/ # Real data integration tests
|
||||
│ └── romfast-reports.spec.js # ROMFAST company data validation
|
||||
├── api-endpoints/ # Backend API validation
|
||||
│ ├── health-check.spec.js # Service health monitoring
|
||||
│ └── data-consistency.spec.js # Cross-schema data validation
|
||||
├── console-monitoring/ # Console error analysis
|
||||
│ ├── error-tracking.spec.js # Error pattern detection
|
||||
│ └── performance-monitoring.spec.js # Performance regression testing
|
||||
├── global-setup.js # Test environment setup
|
||||
├── global-teardown.js # Test cleanup
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🔧 Setup and Configuration
|
||||
|
||||
### 1. Environment Configuration
|
||||
|
||||
Copy the example environment file:
|
||||
```bash
|
||||
cp .env.test.example .env.test
|
||||
```
|
||||
|
||||
Edit `.env.test` with your database configuration:
|
||||
```bash
|
||||
# Oracle Database (through SSH tunnel)
|
||||
ORACLE_USER=CONTAFIN_ORACLE
|
||||
ORACLE_PASSWORD=your_password_here
|
||||
ORACLE_HOST=localhost
|
||||
ORACLE_PORT=1521
|
||||
ORACLE_SID=ROA
|
||||
|
||||
# Test Credentials
|
||||
TEST_USERNAME=MARIUS M
|
||||
TEST_PASSWORD=PAROLA9911
|
||||
TEST_COMPANY=ROMFAST
|
||||
```
|
||||
|
||||
### 2. Prerequisites
|
||||
|
||||
- **SSH Tunnel Active**: Oracle database accessible via SSH tunnel
|
||||
- **Backend Running**: FastAPI backend on port 8000
|
||||
- **Frontend Running**: Vue.js frontend on port 3001
|
||||
- **Node.js**: v16+ with npm
|
||||
- **Python**: v3.8+ with backend dependencies
|
||||
|
||||
### 3. Service Dependencies
|
||||
|
||||
Ensure all services are running:
|
||||
```bash
|
||||
# SSH Tunnel
|
||||
cd /path/to/roa2web
|
||||
./ssh_tunnel.sh start
|
||||
|
||||
# Backend
|
||||
cd reports-app/backend
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# Frontend
|
||||
cd reports-app/frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 🚀 Running Tests
|
||||
|
||||
### Quick Start - Comprehensive Test Suite
|
||||
|
||||
Run all tests with service management:
|
||||
```bash
|
||||
./run-comprehensive-tests.sh
|
||||
```
|
||||
|
||||
### Individual Test Categories
|
||||
|
||||
**Real Authentication Tests:**
|
||||
```bash
|
||||
npx playwright test tests/integration/real-auth/ --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
**ROMFAST Data Integration:**
|
||||
```bash
|
||||
npx playwright test tests/integration/real-data/ --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
**Console Error Monitoring:**
|
||||
```bash
|
||||
npx playwright test tests/integration/console-monitoring/ --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
**Backend Health Monitoring:**
|
||||
```bash
|
||||
npx playwright test tests/integration/api-endpoints/ --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
### Test Runner Options
|
||||
|
||||
```bash
|
||||
# Skip mock tests, run integration only
|
||||
./run-comprehensive-tests.sh --no-mock
|
||||
|
||||
# Skip integration tests, run mock only
|
||||
./run-comprehensive-tests.sh --no-integration
|
||||
|
||||
# Don't cleanup services (for debugging)
|
||||
./run-comprehensive-tests.sh --no-cleanup
|
||||
|
||||
# Skip report generation
|
||||
./run-comprehensive-tests.sh --no-reports
|
||||
```
|
||||
|
||||
## 📊 Console Error Monitoring
|
||||
|
||||
### Error Classification System
|
||||
|
||||
The test suite automatically classifies console messages:
|
||||
|
||||
- **CRITICAL**: Authentication failures, database errors, uncaught exceptions
|
||||
- **WARNING**: Network failures, 404 errors, component warnings
|
||||
- **INFO**: Development messages, HMR notifications, DevTools
|
||||
- **UNKNOWN**: Unclassified error patterns
|
||||
|
||||
### Performance Baselines
|
||||
|
||||
Automated validation against performance baselines:
|
||||
|
||||
```javascript
|
||||
PerformanceBaselines = {
|
||||
loginTime: 2000, // Max 2s for login
|
||||
dashboardLoad: 3000, // Max 3s for dashboard
|
||||
reportGeneration: 5000, // Max 5s for reports
|
||||
apiResponse: 1500, // Max 1.5s for API calls
|
||||
pageLoad: 4000 // Max 4s for page loads
|
||||
}
|
||||
```
|
||||
|
||||
### Error Pattern Detection
|
||||
|
||||
Automatic detection of recurring error patterns:
|
||||
- Failed fetch requests
|
||||
- 404 Not Found errors
|
||||
- JavaScript TypeErrors
|
||||
- Vue component warnings
|
||||
- Oracle connection issues
|
||||
|
||||
## 🔍 Test Categories
|
||||
|
||||
### Real Authentication Tests (`real-auth/`)
|
||||
|
||||
- **Oracle Login Testing**: Validates CONTAFIN_ORACLE authentication
|
||||
- **JWT Token Management**: Tests token storage and expiration
|
||||
- **Session Persistence**: Validates session across page reloads
|
||||
- **Invalid Credentials**: Tests error handling for bad credentials
|
||||
- **Performance Monitoring**: Measures auth response times
|
||||
|
||||
### Real Data Integration (`real-data/`)
|
||||
|
||||
- **ROMFAST Data Loading**: Tests real company data integration
|
||||
- **Invoice Schema Validation**: Validates Oracle invoice data fields
|
||||
- **Payment Data Testing**: Tests payment data structure and loading
|
||||
- **Data Filtering**: Tests search and filter functionality
|
||||
- **Dashboard Metrics**: Validates dashboard accuracy with real data
|
||||
- **Performance Under Load**: Tests data loading performance
|
||||
|
||||
### API Endpoint Validation (`api-endpoints/`)
|
||||
|
||||
- **Health Check Monitoring**: Validates backend and database health
|
||||
- **SSH Tunnel Dependency**: Tests tunnel connectivity requirements
|
||||
- **Error Rate Monitoring**: Tracks API error patterns
|
||||
- **Concurrent Load Testing**: Tests backend under concurrent requests
|
||||
- **Resource Usage Monitoring**: Detects memory leaks and resource issues
|
||||
- **Cross-Schema Data Validation**: Tests Oracle schema relationships
|
||||
|
||||
### Console Error Analysis (`console-monitoring/`)
|
||||
|
||||
- **Error Pattern Detection**: Identifies recurring error patterns
|
||||
- **Performance Warning Detection**: Monitors performance-related warnings
|
||||
- **Error Context Analysis**: Provides debugging context for errors
|
||||
- **Comprehensive Error Reporting**: Generates detailed error reports
|
||||
- **Memory Leak Detection**: Monitors JavaScript memory usage
|
||||
- **Performance Regression Testing**: Validates performance consistency
|
||||
|
||||
## 📈 Performance Monitoring
|
||||
|
||||
### Metrics Collected
|
||||
|
||||
- **Page Load Times**: DOM content loaded, first paint, interactive
|
||||
- **API Response Times**: Individual and average response times
|
||||
- **Network Resource Timing**: Resource loading performance
|
||||
- **Memory Usage**: JavaScript heap size monitoring
|
||||
- **Error Frequencies**: Console error occurrence rates
|
||||
|
||||
### Regression Detection
|
||||
|
||||
- **Baseline Validation**: Automatic comparison against performance baselines
|
||||
- **Consistency Analysis**: Validates performance across multiple runs
|
||||
- **Outlier Detection**: Identifies abnormal performance spikes
|
||||
- **Trend Analysis**: Monitors performance degradation over time
|
||||
|
||||
## 🏥 Health Monitoring
|
||||
|
||||
### Service Health Checks
|
||||
|
||||
- **Backend Health**: `/health` endpoint validation
|
||||
- **Database Connectivity**: Oracle connection through SSH tunnel
|
||||
- **Frontend Availability**: Vue.js application responsiveness
|
||||
- **SSH Tunnel Status**: Tunnel connectivity validation
|
||||
|
||||
### Error Handling Validation
|
||||
|
||||
- **Graceful Degradation**: Tests behavior during service failures
|
||||
- **Error Message Clarity**: Validates user-facing error messages
|
||||
- **Recovery Mechanisms**: Tests automatic recovery from failures
|
||||
- **Fallback Behavior**: Validates fallback when services unavailable
|
||||
|
||||
## 📋 Test Reports
|
||||
|
||||
### Report Types Generated
|
||||
|
||||
- **HTML Reports**: Interactive test results with screenshots
|
||||
- **JSON Reports**: Machine-readable test data
|
||||
- **JUnit Reports**: CI/CD integration format
|
||||
- **Console Error Reports**: Detailed error analysis
|
||||
- **Performance Reports**: Performance metrics and trends
|
||||
|
||||
### Report Locations
|
||||
|
||||
```
|
||||
test-results/
|
||||
├── playwright-report-integration/ # HTML reports
|
||||
├── integration-results.json # JSON results
|
||||
├── integration-junit.xml # JUnit format
|
||||
├── integration-summary.json # Combined summary
|
||||
└── reports/ # Additional reports
|
||||
└── comprehensive-test-report-*.json
|
||||
```
|
||||
|
||||
## 🔧 Debugging and Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**SSH Tunnel Not Running:**
|
||||
```bash
|
||||
cd /path/to/roa2web
|
||||
./ssh_tunnel.sh status
|
||||
./ssh_tunnel.sh start
|
||||
```
|
||||
|
||||
**Backend Not Accessible:**
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
# Should return: {"database":"connected","api":"healthy"}
|
||||
```
|
||||
|
||||
**Frontend Not Running:**
|
||||
```bash
|
||||
curl http://localhost:3001
|
||||
# Should return HTML content
|
||||
```
|
||||
|
||||
**Oracle Connection Issues:**
|
||||
- Verify SSH tunnel is active
|
||||
- Check Oracle credentials in environment
|
||||
- Validate database accessibility through tunnel
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Run tests with detailed logging:
|
||||
```bash
|
||||
DEBUG=1 npx playwright test --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
View console messages during test execution:
|
||||
```bash
|
||||
npx playwright test --headed --config=playwright.real-api.config.js
|
||||
```
|
||||
|
||||
### Log Files
|
||||
|
||||
Test execution logs are available in:
|
||||
- `frontend.log` - Frontend service logs
|
||||
- `backend.log` - Backend service logs
|
||||
- `test-results/*.log` - Individual test logs
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
### Passing Integration Tests
|
||||
|
||||
- ✅ Zero critical console errors with real data
|
||||
- ✅ Response times under baseline thresholds
|
||||
- ✅ 100% coverage for ROMFAST company flows
|
||||
- ✅ Automatic error pattern detection working
|
||||
- ✅ Performance baselines consistently met
|
||||
- ✅ Cross-schema data validation passing
|
||||
- ✅ Health monitoring detecting issues correctly
|
||||
|
||||
### Performance Requirements
|
||||
|
||||
- Login: < 2 seconds
|
||||
- Dashboard load: < 3 seconds
|
||||
- Report generation: < 5 seconds
|
||||
- API responses: < 1.5 seconds
|
||||
- Page loads: < 4 seconds
|
||||
|
||||
### Error Tolerance
|
||||
|
||||
- Critical errors: 0 allowed
|
||||
- Warning errors: < 10 per test run
|
||||
- Performance degradation: < 50% variance
|
||||
- Memory leaks: < 100% memory growth
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
### Adding New Integration Tests
|
||||
|
||||
1. Create test file in appropriate category directory
|
||||
2. Import required utilities from `../../utils/`
|
||||
3. Use `setupConsoleCapture(page)` in `beforeEach`
|
||||
4. Use `assertNoCriticalErrors(page, expect)` for validation
|
||||
5. Generate error reports in `afterEach`
|
||||
|
||||
### Test Utilities Available
|
||||
|
||||
- `setupConsoleCapture()` - Console monitoring setup
|
||||
- `authenticateWithRealCredentials()` - Real Oracle authentication
|
||||
- `selectCompany()` - Company selection helper
|
||||
- `assertNoCriticalErrors()` - Error validation
|
||||
- `generateErrorReport()` - Comprehensive error reporting
|
||||
- `PerformanceMonitor` - Performance measurement utilities
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Always use real credentials for integration tests
|
||||
- Monitor console errors in all tests
|
||||
- Validate performance against baselines
|
||||
- Generate comprehensive error reports
|
||||
- Test with actual Oracle data when possible
|
||||
- Clean up test state between runs
|
||||
|
||||
---
|
||||
|
||||
**Integration Test Status**: ✅ Fully Implemented
|
||||
**Console Monitoring**: ✅ Active
|
||||
**Real Data Testing**: ✅ ROMFAST Validated
|
||||
**Performance Monitoring**: ✅ Baseline Tracking
|
||||
**Error Analysis**: ✅ Pattern Detection Active
|
||||
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Cross-Schema Data Validation Tests
|
||||
* Validates data consistency between CONTAFIN_ORACLE authentication schema
|
||||
* and ROMFAST company data, ensuring proper Oracle data flow
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
authenticateWithRealCredentials,
|
||||
getRealCompanies,
|
||||
selectCompany,
|
||||
REAL_CREDENTIALS,
|
||||
API_ENDPOINTS
|
||||
} from '../../utils/real-auth.js';
|
||||
import {
|
||||
setupConsoleCapture,
|
||||
assertNoCriticalErrors,
|
||||
generateErrorReport
|
||||
} from '../../utils/console-monitor.js';
|
||||
|
||||
test.describe('Oracle Cross-Schema Data Consistency', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
setupConsoleCapture(page);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
const report = generateErrorReport(page, test.info().title);
|
||||
if (report.summary.classifications.critical > 0) {
|
||||
console.warn('🚨 Critical errors in data consistency test:', report.details.criticalErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate CONTAFIN_ORACLE → ROMFAST data flow', async ({ page }) => {
|
||||
console.log('🔄 Testing Oracle cross-schema data flow...');
|
||||
|
||||
// Authenticate with CONTAFIN_ORACLE schema credentials
|
||||
const authResult = await authenticateWithRealCredentials(page);
|
||||
expect(authResult.success, 'CONTAFIN_ORACLE authentication failed').toBe(true);
|
||||
|
||||
console.log('✅ CONTAFIN_ORACLE authentication successful');
|
||||
|
||||
// Test companies endpoint returns ROMFAST from NOM_FIRME table
|
||||
console.log('🏢 Validating companies data from NOM_FIRME...');
|
||||
const companiesResponse = await page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.companies}`);
|
||||
expect(companiesResponse.status()).toBe(200);
|
||||
|
||||
const companies = await companiesResponse.json();
|
||||
expect(companies).toBeInstanceOf(Array);
|
||||
expect(companies.length).toBeGreaterThan(0);
|
||||
|
||||
console.log(`📊 Found ${companies.length} companies in NOM_FIRME table`);
|
||||
|
||||
// Validate ROMFAST company exists
|
||||
const romfast = companies.find(c => c.id_firma === 'ROMFAST');
|
||||
expect(romfast, 'ROMFAST company not found in NOM_FIRME table').toBeDefined();
|
||||
expect(romfast.name).toContain('ROMFAST');
|
||||
|
||||
console.log('✅ ROMFAST company validated in NOM_FIRME table:', romfast);
|
||||
|
||||
// Validate company data structure matches Oracle schema
|
||||
expect(romfast).toHaveProperty('id_firma');
|
||||
expect(romfast).toHaveProperty('name');
|
||||
|
||||
// Additional Oracle-specific fields that might be present
|
||||
const oracleFields = ['cui', 'reg_com', 'adresa', 'telefon', 'email'];
|
||||
oracleFields.forEach(field => {
|
||||
if (romfast.hasOwnProperty(field)) {
|
||||
console.log(`ℹ️ Oracle field '${field}' present:`, romfast[field]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Cross-schema authentication and company data flow validated');
|
||||
});
|
||||
|
||||
test('should validate invoice schema consistency', async ({ page }) => {
|
||||
console.log('📋 Validating invoice data schema consistency...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Get invoices data for ROMFAST
|
||||
console.log('📥 Fetching ROMFAST invoice data...');
|
||||
const invoicesResponse = await page.request.get(`${API_ENDPOINTS.backend}/api/invoices/ROMFAST`);
|
||||
expect(invoicesResponse.status()).toBe(200);
|
||||
|
||||
const invoicesData = await invoicesResponse.json();
|
||||
expect(invoicesData).toHaveProperty('data');
|
||||
expect(invoicesData.data).toBeInstanceOf(Array);
|
||||
|
||||
if (invoicesData.data.length > 0) {
|
||||
const sampleInvoice = invoicesData.data[0];
|
||||
console.log('📋 Sample invoice structure:', Object.keys(sampleInvoice));
|
||||
|
||||
// Validate Oracle-specific invoice fields are present
|
||||
const requiredOracleFields = [
|
||||
'numar_factura', // Invoice number
|
||||
'data_scadenta', // Due date
|
||||
'suma_totala' // Total amount
|
||||
];
|
||||
|
||||
requiredOracleFields.forEach(field => {
|
||||
expect(sampleInvoice, `Missing Oracle field: ${field}`).toHaveProperty(field);
|
||||
console.log(`✅ Oracle field '${field}':`, sampleInvoice[field]);
|
||||
});
|
||||
|
||||
// Validate data types
|
||||
expect(typeof sampleInvoice.numar_factura).toBe('string');
|
||||
expect(sampleInvoice.suma_totala).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Validate date format (should be ISO string or valid date)
|
||||
if (sampleInvoice.data_scadenta) {
|
||||
const date = new Date(sampleInvoice.data_scadenta);
|
||||
expect(date.toString()).not.toBe('Invalid Date');
|
||||
console.log(`✅ Date validation passed: ${sampleInvoice.data_scadenta}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Invoice schema validation passed (${invoicesData.data.length} invoices)`);
|
||||
} else {
|
||||
console.log('ℹ️ No invoice data found for ROMFAST - schema validation skipped');
|
||||
}
|
||||
|
||||
// Check for console errors during data retrieval
|
||||
assertNoCriticalErrors(page, expect);
|
||||
});
|
||||
|
||||
test('should validate payment schema consistency', async ({ page }) => {
|
||||
console.log('💰 Validating payment data schema consistency...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Get payments data for ROMFAST
|
||||
console.log('💳 Fetching ROMFAST payment data...');
|
||||
const paymentsResponse = await page.request.get(`${API_ENDPOINTS.backend}/api/payments/ROMFAST`);
|
||||
expect(paymentsResponse.status()).toBe(200);
|
||||
|
||||
const paymentsData = await paymentsResponse.json();
|
||||
expect(paymentsData).toHaveProperty('data');
|
||||
expect(paymentsData.data).toBeInstanceOf(Array);
|
||||
|
||||
if (paymentsData.data.length > 0) {
|
||||
const samplePayment = paymentsData.data[0];
|
||||
console.log('💳 Sample payment structure:', Object.keys(samplePayment));
|
||||
|
||||
// Validate Oracle-specific payment fields
|
||||
const requiredOracleFields = [
|
||||
'numar_plata', // Payment number
|
||||
'data_plata', // Payment date
|
||||
'suma_plata' // Payment amount
|
||||
];
|
||||
|
||||
requiredOracleFields.forEach(field => {
|
||||
expect(samplePayment, `Missing Oracle payment field: ${field}`).toHaveProperty(field);
|
||||
console.log(`✅ Oracle payment field '${field}':`, samplePayment[field]);
|
||||
});
|
||||
|
||||
// Validate payment data types
|
||||
expect(typeof samplePayment.numar_plata).toBe('string');
|
||||
expect(samplePayment.suma_plata).toBeGreaterThanOrEqual(0);
|
||||
|
||||
// Validate payment date
|
||||
if (samplePayment.data_plata) {
|
||||
const date = new Date(samplePayment.data_plata);
|
||||
expect(date.toString()).not.toBe('Invalid Date');
|
||||
console.log(`✅ Payment date validation passed: ${samplePayment.data_plata}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Payment schema validation passed (${paymentsData.data.length} payments)`);
|
||||
} else {
|
||||
console.log('ℹ️ No payment data found for ROMFAST - schema validation skipped');
|
||||
}
|
||||
|
||||
// Check for console errors during data retrieval
|
||||
assertNoCriticalErrors(page, expect);
|
||||
});
|
||||
|
||||
test('should validate user permissions across schemas', async ({ page }) => {
|
||||
console.log('🔐 Validating user permissions across Oracle schemas...');
|
||||
|
||||
const authResult = await authenticateWithRealCredentials(page);
|
||||
expect(authResult.success).toBe(true);
|
||||
|
||||
// Test access to different endpoints with authenticated user
|
||||
const endpointsToTest = [
|
||||
{ url: '/api/companies', name: 'Companies List', expectAccess: true },
|
||||
{ url: '/api/invoices/ROMFAST', name: 'ROMFAST Invoices', expectAccess: true },
|
||||
{ url: '/api/payments/ROMFAST', name: 'ROMFAST Payments', expectAccess: true },
|
||||
{ url: '/api/user/profile', name: 'User Profile', expectAccess: true },
|
||||
{ url: '/api/admin/users', name: 'Admin Users', expectAccess: false } // Should fail
|
||||
];
|
||||
|
||||
const accessResults = [];
|
||||
|
||||
for (const endpoint of endpointsToTest) {
|
||||
console.log(`🔍 Testing access to ${endpoint.name}...`);
|
||||
|
||||
try {
|
||||
const response = await page.request.get(`${API_ENDPOINTS.backend}${endpoint.url}`);
|
||||
const hasAccess = response.status() < 400;
|
||||
|
||||
accessResults.push({
|
||||
endpoint: endpoint.name,
|
||||
url: endpoint.url,
|
||||
status: response.status(),
|
||||
hasAccess: hasAccess,
|
||||
expectAccess: endpoint.expectAccess
|
||||
});
|
||||
|
||||
if (endpoint.expectAccess) {
|
||||
expect(hasAccess, `Expected access to ${endpoint.name} but got ${response.status()}`).toBe(true);
|
||||
console.log(`✅ ${endpoint.name}: Access granted (${response.status()})`);
|
||||
} else {
|
||||
expect(hasAccess, `Expected no access to ${endpoint.name} but got ${response.status()}`).toBe(false);
|
||||
console.log(`✅ ${endpoint.name}: Access denied as expected (${response.status()})`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`⚠️ ${endpoint.name}: Request failed - ${error.message}`);
|
||||
accessResults.push({
|
||||
endpoint: endpoint.name,
|
||||
url: endpoint.url,
|
||||
error: error.message,
|
||||
hasAccess: false,
|
||||
expectAccess: endpoint.expectAccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Summary of access results
|
||||
console.log('📊 Permission Validation Summary:');
|
||||
accessResults.forEach(result => {
|
||||
const status = result.hasAccess === result.expectAccess ? '✅' : '❌';
|
||||
console.log(` ${status} ${result.endpoint}: ${result.hasAccess ? 'Access' : 'No Access'}`);
|
||||
});
|
||||
|
||||
console.log('✅ User permission validation completed');
|
||||
});
|
||||
|
||||
test('should validate data relationships between tables', async ({ page }) => {
|
||||
console.log('🔗 Validating data relationships between Oracle tables...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
// Get company data
|
||||
const companiesResponse = await page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.companies}`);
|
||||
const companies = await companiesResponse.json();
|
||||
const romfast = companies.find(c => c.id_firma === 'ROMFAST');
|
||||
|
||||
expect(romfast).toBeDefined();
|
||||
|
||||
// Get invoices for ROMFAST
|
||||
const invoicesResponse = await page.request.get(`${API_ENDPOINTS.backend}/api/invoices/ROMFAST`);
|
||||
const invoicesData = await invoicesResponse.json();
|
||||
|
||||
// Get payments for ROMFAST
|
||||
const paymentsResponse = await page.request.get(`${API_ENDPOINTS.backend}/api/payments/ROMFAST`);
|
||||
const paymentsData = await paymentsResponse.json();
|
||||
|
||||
console.log('📊 Data relationship summary:');
|
||||
console.log(` Company: ${romfast.name} (${romfast.id_firma})`);
|
||||
console.log(` Invoices: ${invoicesData.data?.length || 0}`);
|
||||
console.log(` Payments: ${paymentsData.data?.length || 0}`);
|
||||
|
||||
// Validate referential integrity
|
||||
if (invoicesData.data && invoicesData.data.length > 0) {
|
||||
const sampleInvoice = invoicesData.data[0];
|
||||
|
||||
// Invoice should reference the company
|
||||
if (sampleInvoice.cod_firma) {
|
||||
expect(sampleInvoice.cod_firma).toBe(romfast.id_firma);
|
||||
console.log('✅ Invoice-Company relationship validated');
|
||||
}
|
||||
|
||||
// Check if there are related payments
|
||||
if (paymentsData.data && paymentsData.data.length > 0) {
|
||||
console.log('✅ Payment data exists for company with invoices');
|
||||
|
||||
// Look for potential invoice-payment relationships
|
||||
const samplePayment = paymentsData.data[0];
|
||||
if (samplePayment.cod_firma) {
|
||||
expect(samplePayment.cod_firma).toBe(romfast.id_firma);
|
||||
console.log('✅ Payment-Company relationship validated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate data consistency
|
||||
const totalInvoicesFromApi = invoicesData.data?.length || 0;
|
||||
const totalPaymentsFromApi = paymentsData.data?.length || 0;
|
||||
|
||||
// These should be reasonable numbers for a real company
|
||||
if (totalInvoicesFromApi > 0) {
|
||||
console.log(`✅ Company has ${totalInvoicesFromApi} invoices`);
|
||||
}
|
||||
|
||||
if (totalPaymentsFromApi > 0) {
|
||||
console.log(`✅ Company has ${totalPaymentsFromApi} payments`);
|
||||
}
|
||||
|
||||
// Check for console errors during relationship validation
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log('✅ Data relationship validation completed');
|
||||
});
|
||||
|
||||
test('should validate Oracle connection persistence during operations', async ({ page }) => {
|
||||
console.log('🔄 Testing Oracle connection persistence...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
// Perform multiple operations to test connection persistence
|
||||
const operations = [
|
||||
{ name: 'Companies Load', action: () => page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.companies}`) },
|
||||
{ name: 'Invoice Load', action: () => page.request.get(`${API_ENDPOINTS.backend}/api/invoices/ROMFAST`) },
|
||||
{ name: 'Payment Load', action: () => page.request.get(`${API_ENDPOINTS.backend}/api/payments/ROMFAST`) },
|
||||
{ name: 'Health Check', action: () => page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`) }
|
||||
];
|
||||
|
||||
const connectionResults = [];
|
||||
|
||||
for (let cycle = 1; cycle <= 3; cycle++) {
|
||||
console.log(`🔄 Connection persistence test cycle ${cycle}/3`);
|
||||
|
||||
for (const operation of operations) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await operation.action();
|
||||
const responseTime = Date.now() - startTime;
|
||||
const success = response.status() < 400;
|
||||
|
||||
connectionResults.push({
|
||||
cycle,
|
||||
operation: operation.name,
|
||||
success,
|
||||
status: response.status(),
|
||||
responseTime
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log(` ✅ ${operation.name}: ${response.status()} (${responseTime}ms)`);
|
||||
} else {
|
||||
console.log(` ❌ ${operation.name}: ${response.status()} (${responseTime}ms)`);
|
||||
}
|
||||
|
||||
// Brief delay between operations
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
} catch (error) {
|
||||
console.log(` ❌ ${operation.name}: ${error.message}`);
|
||||
connectionResults.push({
|
||||
cycle,
|
||||
operation: operation.name,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Delay between cycles
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
// Analyze connection persistence
|
||||
const totalOperations = connectionResults.length;
|
||||
const successfulOperations = connectionResults.filter(r => r.success).length;
|
||||
const successRate = (successfulOperations / totalOperations) * 100;
|
||||
|
||||
console.log('📊 Connection Persistence Analysis:');
|
||||
console.log(` Total Operations: ${totalOperations}`);
|
||||
console.log(` Successful: ${successfulOperations}`);
|
||||
console.log(` Success Rate: ${successRate.toFixed(1)}%`);
|
||||
|
||||
// Connection should be persistent (>90% success rate)
|
||||
expect(successRate, 'Oracle connection not persistent enough').toBeGreaterThan(90);
|
||||
|
||||
// No connection should fail in the same cycle
|
||||
const cycleFailures = {};
|
||||
connectionResults.filter(r => !r.success).forEach(r => {
|
||||
cycleFailures[r.cycle] = (cycleFailures[r.cycle] || 0) + 1;
|
||||
});
|
||||
|
||||
Object.entries(cycleFailures).forEach(([cycle, failures]) => {
|
||||
if (failures === operations.length) {
|
||||
throw new Error(`Complete connection failure in cycle ${cycle}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Oracle connection persistence validated');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* Backend Health Monitoring Integration Tests
|
||||
* Validates backend services, database connectivity, and error handling
|
||||
* Monitors system health through comprehensive endpoint testing
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { API_ENDPOINTS } from '../../utils/real-auth.js';
|
||||
import {
|
||||
setupConsoleCapture,
|
||||
assertNoCriticalErrors,
|
||||
generateErrorReport,
|
||||
PerformanceBaselines,
|
||||
assertPerformanceBaseline
|
||||
} from '../../utils/console-monitor.js';
|
||||
|
||||
test.describe('Backend Health Monitoring', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
setupConsoleCapture(page);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
const report = generateErrorReport(page, test.info().title);
|
||||
if (report.summary.classifications.critical > 0) {
|
||||
console.warn('⚠️ Critical errors in health monitoring:', report.details.criticalErrors);
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate database connectivity through health endpoint', async ({ page }) => {
|
||||
console.log('🏥 Testing backend health endpoint...');
|
||||
|
||||
const healthStartTime = Date.now();
|
||||
const response = await page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`);
|
||||
const healthResponseTime = Date.now() - healthStartTime;
|
||||
|
||||
// Validate response
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const health = await response.json();
|
||||
console.log('📊 Health response:', health);
|
||||
|
||||
// Validate health data structure
|
||||
expect(health).toHaveProperty('database');
|
||||
expect(health).toHaveProperty('api');
|
||||
expect(health).toHaveProperty('timestamp');
|
||||
|
||||
// Database should be connected (Oracle via SSH tunnel)
|
||||
expect(health.database).toBe('connected');
|
||||
expect(health.api).toBe('healthy');
|
||||
|
||||
// Validate response time
|
||||
assertPerformanceBaseline(
|
||||
healthResponseTime,
|
||||
1000, // Max 1s for health check
|
||||
'Health endpoint response',
|
||||
expect
|
||||
);
|
||||
|
||||
console.log(`✅ Backend health validated in ${healthResponseTime}ms`);
|
||||
});
|
||||
|
||||
test('should validate SSH tunnel dependency in health check', async ({ page }) => {
|
||||
console.log('🔐 Testing SSH tunnel dependency validation...');
|
||||
|
||||
const response = await page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
const health = await response.json();
|
||||
|
||||
// Should indicate SSH tunnel status
|
||||
if (health.ssh_tunnel !== undefined) {
|
||||
expect(health.ssh_tunnel).toBe('active');
|
||||
console.log('✅ SSH tunnel status confirmed in health check');
|
||||
}
|
||||
|
||||
// Database connection implies SSH tunnel is working
|
||||
expect(health.database).toBe('connected');
|
||||
|
||||
console.log('✅ SSH tunnel dependency validated through database connectivity');
|
||||
});
|
||||
|
||||
test('should handle Oracle connection failures gracefully', async ({ page }) => {
|
||||
console.log('💥 Testing Oracle connection failure handling...');
|
||||
|
||||
// First verify normal operation
|
||||
const normalResponse = await page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`);
|
||||
expect(normalResponse.status()).toBe(200);
|
||||
|
||||
// Navigate to app to test error handling in UI
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Mock database connection failure
|
||||
await page.route('**/api/**', async (route) => {
|
||||
if (route.request().url().includes('/health')) {
|
||||
await route.fulfill({
|
||||
status: 503,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
database: 'disconnected',
|
||||
api: 'degraded',
|
||||
error: 'Oracle connection failed'
|
||||
})
|
||||
});
|
||||
} else {
|
||||
await route.continue();
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger health check from frontend
|
||||
await page.reload();
|
||||
|
||||
// Should show appropriate error handling
|
||||
const errorElements = await page.locator('[data-testid*="error"], [data-testid*="warning"]').count();
|
||||
|
||||
if (errorElements > 0) {
|
||||
console.log(`🚨 Found ${errorElements} error indicators in UI`);
|
||||
}
|
||||
|
||||
// Check console for appropriate error messages (not critical failures)
|
||||
const consoleMessages = page.consoleMessages || [];
|
||||
const dbErrors = consoleMessages.filter(msg =>
|
||||
msg.text.includes('database') || msg.text.includes('Oracle') || msg.text.includes('503')
|
||||
);
|
||||
|
||||
expect(dbErrors.length).toBeGreaterThan(0);
|
||||
console.log(`📝 Found ${dbErrors.length} database-related console messages`);
|
||||
|
||||
// Should not crash the application
|
||||
const currentUrl = page.url();
|
||||
expect(currentUrl).not.toContain('/error');
|
||||
|
||||
console.log('✅ Oracle connection failure handled gracefully');
|
||||
});
|
||||
|
||||
test('should validate API endpoint availability and response times', async ({ page }) => {
|
||||
console.log('📡 Testing API endpoint availability...');
|
||||
|
||||
const endpoints = [
|
||||
{ path: API_ENDPOINTS.health, name: 'Health Check', maxTime: 1000 },
|
||||
{ path: API_ENDPOINTS.companies, name: 'Companies', maxTime: 2000 },
|
||||
{ path: `${API_ENDPOINTS.invoices}/ROMFAST`, name: 'ROMFAST Invoices', maxTime: 3000 },
|
||||
{ path: `${API_ENDPOINTS.payments}/ROMFAST`, name: 'ROMFAST Payments', maxTime: 3000 }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
console.log(`🔍 Testing ${endpoint.name} endpoint...`);
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const response = await page.request.get(`${API_ENDPOINTS.backend}${endpoint.path}`);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
const result = {
|
||||
name: endpoint.name,
|
||||
path: endpoint.path,
|
||||
status: response.status(),
|
||||
responseTime,
|
||||
success: response.ok(),
|
||||
maxTime: endpoint.maxTime
|
||||
};
|
||||
|
||||
results.push(result);
|
||||
|
||||
if (response.ok()) {
|
||||
console.log(`✅ ${endpoint.name}: ${result.status} (${responseTime}ms)`);
|
||||
|
||||
// Validate response time
|
||||
assertPerformanceBaseline(
|
||||
responseTime,
|
||||
endpoint.maxTime,
|
||||
`${endpoint.name} response time`,
|
||||
expect
|
||||
);
|
||||
} else {
|
||||
console.warn(`⚠️ ${endpoint.name}: ${result.status} (${responseTime}ms)`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ ${endpoint.name} failed:`, error.message);
|
||||
results.push({
|
||||
name: endpoint.name,
|
||||
path: endpoint.path,
|
||||
error: error.message,
|
||||
success: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const totalEndpoints = endpoints.length;
|
||||
|
||||
console.log(`📊 API Endpoint Summary: ${successCount}/${totalEndpoints} successful`);
|
||||
|
||||
// At least health and companies endpoints should work
|
||||
const criticalEndpoints = results.filter(r =>
|
||||
r.name === 'Health Check' || r.name === 'Companies'
|
||||
);
|
||||
const criticalSuccess = criticalEndpoints.filter(r => r.success).length;
|
||||
|
||||
expect(criticalSuccess).toBe(2); // Both critical endpoints must work
|
||||
|
||||
console.log('✅ Critical API endpoints validated');
|
||||
});
|
||||
|
||||
test('should monitor backend error rates and patterns', async ({ page }) => {
|
||||
console.log('📈 Monitoring backend error rates...');
|
||||
|
||||
const testRequests = [
|
||||
{ url: `${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`, expected: 200 },
|
||||
{ url: `${API_ENDPOINTS.backend}${API_ENDPOINTS.companies}`, expected: 200 },
|
||||
{ url: `${API_ENDPOINTS.backend}/api/nonexistent`, expected: 404 },
|
||||
{ url: `${API_ENDPOINTS.backend}/api/invoices/INVALID_COMPANY`, expected: 404 }
|
||||
];
|
||||
|
||||
const errorPatterns = {};
|
||||
let totalRequests = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const request of testRequests) {
|
||||
totalRequests++;
|
||||
console.log(`📤 Testing: ${request.url}`);
|
||||
|
||||
try {
|
||||
const response = await page.request.get(request.url);
|
||||
const status = response.status();
|
||||
|
||||
if (status !== request.expected) {
|
||||
errorCount++;
|
||||
const pattern = `${Math.floor(status / 100)}xx`;
|
||||
errorPatterns[pattern] = (errorPatterns[pattern] || 0) + 1;
|
||||
|
||||
console.warn(`⚠️ Unexpected status: ${status} (expected ${request.expected})`);
|
||||
} else {
|
||||
console.log(`✅ Expected status: ${status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
errorPatterns['network'] = (errorPatterns['network'] || 0) + 1;
|
||||
console.error(`❌ Network error:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const errorRate = (errorCount / totalRequests) * 100;
|
||||
|
||||
console.log(`📊 Error Analysis:`);
|
||||
console.log(` Total Requests: ${totalRequests}`);
|
||||
console.log(` Errors: ${errorCount}`);
|
||||
console.log(` Error Rate: ${errorRate.toFixed(1)}%`);
|
||||
console.log(` Error Patterns:`, errorPatterns);
|
||||
|
||||
// Error rate should be reasonable (some 404s expected)
|
||||
expect(errorRate).toBeLessThan(75); // Allow some expected errors
|
||||
|
||||
// Should not have network errors
|
||||
expect(errorPatterns.network || 0).toBe(0);
|
||||
|
||||
console.log('✅ Backend error monitoring completed');
|
||||
});
|
||||
|
||||
test('should validate backend performance under concurrent requests', async ({ page }) => {
|
||||
console.log('⚡ Testing backend performance under load...');
|
||||
|
||||
const concurrentRequests = 5;
|
||||
const requestsPerBatch = 3;
|
||||
const totalBatches = concurrentRequests;
|
||||
|
||||
const performanceResults = [];
|
||||
|
||||
for (let batch = 0; batch < totalBatches; batch++) {
|
||||
console.log(`🔄 Batch ${batch + 1}/${totalBatches}`);
|
||||
|
||||
const batchStartTime = Date.now();
|
||||
const batchPromises = [];
|
||||
|
||||
// Create concurrent requests
|
||||
for (let req = 0; req < requestsPerBatch; req++) {
|
||||
const requestPromise = page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`)
|
||||
.then(response => ({
|
||||
status: response.status(),
|
||||
timing: Date.now() - batchStartTime
|
||||
}))
|
||||
.catch(error => ({
|
||||
error: error.message,
|
||||
timing: Date.now() - batchStartTime
|
||||
}));
|
||||
|
||||
batchPromises.push(requestPromise);
|
||||
}
|
||||
|
||||
// Wait for all requests in batch
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
const batchTime = Date.now() - batchStartTime;
|
||||
|
||||
performanceResults.push({
|
||||
batch: batch + 1,
|
||||
results: batchResults,
|
||||
totalTime: batchTime,
|
||||
successful: batchResults.filter(r => r.status === 200).length
|
||||
});
|
||||
|
||||
console.log(`⏱️ Batch ${batch + 1}: ${batchTime}ms (${batchResults.filter(r => r.status === 200).length}/${requestsPerBatch} successful)`);
|
||||
|
||||
// Small delay between batches
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// Analyze results
|
||||
const totalRequests = totalBatches * requestsPerBatch;
|
||||
const successfulRequests = performanceResults.reduce((sum, batch) => sum + batch.successful, 0);
|
||||
const averageBatchTime = performanceResults.reduce((sum, batch) => sum + batch.totalTime, 0) / totalBatches;
|
||||
const successRate = (successfulRequests / totalRequests) * 100;
|
||||
|
||||
console.log(`📊 Concurrent Load Test Results:`);
|
||||
console.log(` Total Requests: ${totalRequests}`);
|
||||
console.log(` Successful: ${successfulRequests}`);
|
||||
console.log(` Success Rate: ${successRate.toFixed(1)}%`);
|
||||
console.log(` Average Batch Time: ${averageBatchTime.toFixed(0)}ms`);
|
||||
|
||||
// Validate performance under load
|
||||
expect(successRate).toBeGreaterThan(90); // 90%+ success rate
|
||||
expect(averageBatchTime).toBeLessThan(5000); // Max 5s per batch
|
||||
|
||||
// Individual requests should not be extremely slow
|
||||
const slowRequests = performanceResults
|
||||
.flatMap(batch => batch.results)
|
||||
.filter(result => result.timing > 10000); // > 10s
|
||||
|
||||
expect(slowRequests.length).toBe(0);
|
||||
|
||||
console.log('✅ Backend performance under concurrent load validated');
|
||||
});
|
||||
|
||||
test('should validate backend memory and resource monitoring', async ({ page }) => {
|
||||
console.log('💾 Testing backend resource monitoring...');
|
||||
|
||||
// Test multiple heavy operations to check for memory leaks
|
||||
const operations = [
|
||||
() => page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.companies}`),
|
||||
() => page.request.get(`${API_ENDPOINTS.backend}/api/invoices/ROMFAST`),
|
||||
() => page.request.get(`${API_ENDPOINTS.backend}/api/payments/ROMFAST`),
|
||||
() => page.request.get(`${API_ENDPOINTS.backend}${API_ENDPOINTS.health}`)
|
||||
];
|
||||
|
||||
const resourceMetrics = [];
|
||||
|
||||
// Perform operations multiple times
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
console.log(`🔄 Resource test cycle ${cycle + 1}/3`);
|
||||
|
||||
const cycleStartTime = Date.now();
|
||||
const cycleResults = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
const opStartTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await operation();
|
||||
const responseTime = Date.now() - opStartTime;
|
||||
|
||||
cycleResults.push({
|
||||
success: response.ok(),
|
||||
status: response.status(),
|
||||
responseTime
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
cycleResults.push({
|
||||
success: false,
|
||||
error: error.message,
|
||||
responseTime: Date.now() - opStartTime
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cycleTime = Date.now() - cycleStartTime;
|
||||
const avgResponseTime = cycleResults.reduce((sum, r) => sum + r.responseTime, 0) / cycleResults.length;
|
||||
|
||||
resourceMetrics.push({
|
||||
cycle: cycle + 1,
|
||||
totalTime: cycleTime,
|
||||
averageResponseTime: avgResponseTime,
|
||||
successCount: cycleResults.filter(r => r.success).length
|
||||
});
|
||||
|
||||
console.log(`📊 Cycle ${cycle + 1}: ${cycleTime}ms avg, ${avgResponseTime.toFixed(0)}ms response`);
|
||||
}
|
||||
|
||||
// Check for performance degradation over cycles (indicating resource leaks)
|
||||
const firstCycleAvg = resourceMetrics[0].averageResponseTime;
|
||||
const lastCycleAvg = resourceMetrics[resourceMetrics.length - 1].averageResponseTime;
|
||||
const degradationRatio = lastCycleAvg / firstCycleAvg;
|
||||
|
||||
console.log(`📈 Performance degradation ratio: ${degradationRatio.toFixed(2)}`);
|
||||
|
||||
// Should not degrade significantly (< 50% increase)
|
||||
expect(degradationRatio).toBeLessThan(1.5);
|
||||
|
||||
// All cycles should maintain good success rates
|
||||
resourceMetrics.forEach((metric, index) => {
|
||||
expect(metric.successCount).toBeGreaterThan(2); // At least 3/4 operations successful
|
||||
});
|
||||
|
||||
console.log('✅ Backend resource monitoring validated - no significant degradation detected');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,494 @@
|
||||
/**
|
||||
* Console Error Pattern Analysis Tests
|
||||
* Analyzes frontend console errors, categorizes patterns, and monitors error frequencies
|
||||
* Provides insights into application stability and potential issues
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
authenticateWithRealCredentials,
|
||||
selectCompany,
|
||||
REAL_CREDENTIALS
|
||||
} from '../../utils/real-auth.js';
|
||||
import {
|
||||
setupConsoleCapture,
|
||||
ErrorClassifier,
|
||||
generateErrorReport,
|
||||
assertNoCriticalErrors
|
||||
} from '../../utils/console-monitor.js';
|
||||
|
||||
test.describe('Console Error Pattern Analysis', () => {
|
||||
const commonErrorPatterns = [
|
||||
{ pattern: /Failed to fetch/i, category: 'Network Error', severity: 'WARNING' },
|
||||
{ pattern: /Network request failed/i, category: 'Network Error', severity: 'WARNING' },
|
||||
{ pattern: /404.*not found/i, category: '404 Error', severity: 'WARNING' },
|
||||
{ pattern: /Uncaught TypeError/i, category: 'JavaScript Error', severity: 'CRITICAL' },
|
||||
{ pattern: /Uncaught ReferenceError/i, category: 'JavaScript Error', severity: 'CRITICAL' },
|
||||
{ pattern: /Vue warn/i, category: 'Vue Warning', severity: 'WARNING' },
|
||||
{ pattern: /Component.*not found/i, category: 'Component Error', severity: 'WARNING' },
|
||||
{ pattern: /Oracle.*connection/i, category: 'Database Error', severity: 'CRITICAL' },
|
||||
{ pattern: /Authentication.*failed/i, category: 'Auth Error', severity: 'CRITICAL' },
|
||||
{ pattern: /Cannot read property/i, category: 'Property Error', severity: 'CRITICAL' },
|
||||
{ pattern: /Cannot access before initialization/i, category: 'Initialization Error', severity: 'CRITICAL' }
|
||||
];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
setupConsoleCapture(page);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
const report = generateErrorReport(page, test.info().title);
|
||||
|
||||
if (report.summary.classifications.critical > 0) {
|
||||
console.warn('🚨 Critical console errors detected:', report.details.criticalErrors);
|
||||
}
|
||||
|
||||
// Log error pattern summary
|
||||
if (Object.keys(report.details.errorPatterns).length > 0) {
|
||||
console.log('📊 Error patterns detected:', report.details.errorPatterns);
|
||||
}
|
||||
});
|
||||
|
||||
test('should detect and categorize frontend errors during navigation', async ({ page }) => {
|
||||
console.log('🔍 Analyzing console errors during complete navigation flow...');
|
||||
|
||||
// Navigate through all main application views
|
||||
const navigationFlow = [
|
||||
{ action: () => page.goto('/login'), name: 'Login Page' },
|
||||
{ action: () => authenticateWithRealCredentials(page), name: 'Authentication' },
|
||||
{ action: () => page.goto('/dashboard'), name: 'Dashboard' },
|
||||
{ action: () => selectCompany(page, REAL_CREDENTIALS.company), name: 'Company Selection' },
|
||||
{ action: () => page.goto('/invoices'), name: 'Invoices Page' },
|
||||
{ action: () => page.goto('/payments'), name: 'Payments Page' },
|
||||
{ action: () => page.goto('/dashboard'), name: 'Return to Dashboard' }
|
||||
];
|
||||
|
||||
const errorsByStep = {};
|
||||
|
||||
for (const step of navigationFlow) {
|
||||
console.log(`📍 Navigating to: ${step.name}`);
|
||||
|
||||
const initialErrorCount = (page.consoleMessages || []).length;
|
||||
|
||||
try {
|
||||
await step.action();
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Navigation warning for ${step.name}:`, error.message);
|
||||
}
|
||||
|
||||
const newErrors = (page.consoleMessages || []).slice(initialErrorCount);
|
||||
errorsByStep[step.name] = newErrors;
|
||||
|
||||
console.log(`📊 ${step.name}: ${newErrors.length} new console messages`);
|
||||
}
|
||||
|
||||
// Analyze error patterns across all steps
|
||||
const allErrors = Object.values(errorsByStep).flat();
|
||||
const errorsByPattern = {};
|
||||
const errorsBySeverity = { CRITICAL: 0, WARNING: 0, INFO: 0, UNKNOWN: 0 };
|
||||
|
||||
allErrors.forEach(error => {
|
||||
// Classify by severity
|
||||
const severity = ErrorClassifier.classify(error);
|
||||
errorsBySeverity[severity]++;
|
||||
|
||||
// Classify by pattern
|
||||
const pattern = commonErrorPatterns.find(p => p.pattern.test(error.text || error.error || ''));
|
||||
if (pattern) {
|
||||
errorsByPattern[pattern.category] = (errorsByPattern[pattern.category] || 0) + 1;
|
||||
} else if (error.type === 'error') {
|
||||
errorsByPattern['Unclassified Error'] = (errorsByPattern['Unclassified Error'] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📈 Error Analysis Summary:');
|
||||
console.log(` Total Console Messages: ${allErrors.length}`);
|
||||
console.log(` By Severity:`, errorsBySeverity);
|
||||
console.log(` By Pattern:`, errorsByPattern);
|
||||
|
||||
// Validate error thresholds
|
||||
expect(errorsBySeverity.CRITICAL, 'Critical errors detected during navigation').toBe(0);
|
||||
expect(errorsBySeverity.WARNING, 'Excessive warnings during navigation').toBeLessThan(10);
|
||||
|
||||
// Check for high-frequency patterns
|
||||
Object.entries(errorsByPattern).forEach(([pattern, count]) => {
|
||||
if (count > 3) {
|
||||
console.warn(`⚠️ High frequency error pattern: ${pattern} (${count} occurrences)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ Console error pattern analysis completed');
|
||||
});
|
||||
|
||||
test('should monitor error frequencies and identify recurring issues', async ({ page }) => {
|
||||
console.log('📊 Monitoring error frequencies across multiple operations...');
|
||||
|
||||
// Authenticate first
|
||||
await authenticateWithRealCredentials(page);
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
const operations = [
|
||||
{ name: 'Dashboard Refresh', action: () => page.reload() },
|
||||
{ name: 'Invoices Navigation', action: () => page.goto('/invoices') },
|
||||
{ name: 'Payments Navigation', action: () => page.goto('/payments') },
|
||||
{ name: 'Dashboard Return', action: () => page.goto('/dashboard') },
|
||||
{ name: 'Company Re-selection', action: () => selectCompany(page, REAL_CREDENTIALS.company) }
|
||||
];
|
||||
|
||||
const errorFrequencies = {};
|
||||
const operationErrors = {};
|
||||
|
||||
for (let cycle = 0; cycle < 2; cycle++) {
|
||||
console.log(`🔄 Error monitoring cycle ${cycle + 1}/2`);
|
||||
|
||||
for (const operation of operations) {
|
||||
const initialMessageCount = (page.consoleMessages || []).length;
|
||||
|
||||
try {
|
||||
await operation.action();
|
||||
await page.waitForLoadState('networkidle', { timeout: 8000 });
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Operation ${operation.name} encountered issue:`, error.message);
|
||||
}
|
||||
|
||||
const newMessages = (page.consoleMessages || []).slice(initialMessageCount);
|
||||
const errorMessages = newMessages.filter(msg => msg.type === 'error' || msg.type === 'pageerror');
|
||||
|
||||
operationErrors[`${operation.name}_Cycle${cycle + 1}`] = errorMessages;
|
||||
|
||||
// Track error frequencies
|
||||
errorMessages.forEach(error => {
|
||||
const errorText = error.text || error.error || '';
|
||||
const pattern = commonErrorPatterns.find(p => p.pattern.test(errorText));
|
||||
const key = pattern ? pattern.category : 'Unclassified';
|
||||
|
||||
errorFrequencies[key] = (errorFrequencies[key] || 0) + 1;
|
||||
});
|
||||
|
||||
console.log(` ${operation.name}: ${errorMessages.length} errors`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze recurring patterns
|
||||
console.log('🔍 Error Frequency Analysis:');
|
||||
const recurringIssues = Object.entries(errorFrequencies)
|
||||
.filter(([pattern, count]) => count > 2)
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
if (recurringIssues.length > 0) {
|
||||
console.log('🚨 Recurring Error Patterns:');
|
||||
recurringIssues.forEach(([pattern, count]) => {
|
||||
console.log(` ${pattern}: ${count} occurrences`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ No recurring error patterns detected');
|
||||
}
|
||||
|
||||
// Validate error thresholds
|
||||
const totalErrors = Object.values(errorFrequencies).reduce((sum, count) => sum + count, 0);
|
||||
expect(totalErrors, 'Excessive total errors across operations').toBeLessThan(20);
|
||||
|
||||
// Critical patterns should not recur
|
||||
const criticalRecurring = recurringIssues.filter(([pattern]) =>
|
||||
commonErrorPatterns.find(p => p.category === pattern && p.severity === 'CRITICAL')
|
||||
);
|
||||
|
||||
expect(criticalRecurring.length, `Critical recurring errors: ${JSON.stringify(criticalRecurring)}`).toBe(0);
|
||||
|
||||
console.log('✅ Error frequency monitoring completed');
|
||||
});
|
||||
|
||||
test('should detect performance-related console warnings', async ({ page }) => {
|
||||
console.log('⚡ Detecting performance-related console warnings...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
const performanceKeywords = [
|
||||
'slow', 'performance', 'memory', 'leak', 'timeout',
|
||||
'blocking', 'lag', 'delay', 'optimization', 'cache'
|
||||
];
|
||||
|
||||
// Perform operations that might trigger performance warnings
|
||||
const heavyOperations = [
|
||||
{ name: 'Large Data Load', action: () => selectCompany(page, REAL_CREDENTIALS.company) },
|
||||
{ name: 'Invoices with Filtering', action: async () => {
|
||||
await page.goto('/invoices');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 });
|
||||
|
||||
// Trigger filtering operations
|
||||
if (await page.locator('[data-testid="search-input"]').isVisible()) {
|
||||
await page.fill('[data-testid="search-input"]', 'test');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
}},
|
||||
{ name: 'Multiple Page Navigation', action: async () => {
|
||||
const pages = ['/dashboard', '/invoices', '/payments', '/dashboard'];
|
||||
for (const pagePath of pages) {
|
||||
await page.goto(pagePath);
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
||||
}
|
||||
}}
|
||||
];
|
||||
|
||||
const performanceWarnings = [];
|
||||
|
||||
for (const operation of heavyOperations) {
|
||||
console.log(`🔧 Executing: ${operation.name}`);
|
||||
|
||||
const initialMessageCount = (page.consoleMessages || []).length;
|
||||
|
||||
const startTime = Date.now();
|
||||
await operation.action();
|
||||
const operationTime = Date.now() - startTime;
|
||||
|
||||
const newMessages = (page.consoleMessages || []).slice(initialMessageCount);
|
||||
const perfMessages = newMessages.filter(msg => {
|
||||
const text = msg.text || msg.error || '';
|
||||
return performanceKeywords.some(keyword =>
|
||||
text.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
if (perfMessages.length > 0) {
|
||||
performanceWarnings.push({
|
||||
operation: operation.name,
|
||||
operationTime,
|
||||
warnings: perfMessages,
|
||||
count: perfMessages.length
|
||||
});
|
||||
|
||||
console.log(`⚠️ ${operation.name}: ${perfMessages.length} performance warnings (${operationTime}ms)`);
|
||||
} else {
|
||||
console.log(`✅ ${operation.name}: No performance warnings (${operationTime}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze performance warnings
|
||||
if (performanceWarnings.length > 0) {
|
||||
console.log('📊 Performance Warning Analysis:');
|
||||
performanceWarnings.forEach(warning => {
|
||||
console.log(` ${warning.operation}: ${warning.count} warnings, ${warning.operationTime}ms`);
|
||||
warning.warnings.forEach(w => {
|
||||
console.log(` - ${w.text || w.error}`);
|
||||
});
|
||||
});
|
||||
|
||||
// Performance warnings should be investigated but not fail tests
|
||||
const totalPerfWarnings = performanceWarnings.reduce((sum, w) => sum + w.count, 0);
|
||||
if (totalPerfWarnings > 5) {
|
||||
console.warn(`⚠️ High number of performance warnings: ${totalPerfWarnings}`);
|
||||
}
|
||||
} else {
|
||||
console.log('✅ No performance-related console warnings detected');
|
||||
}
|
||||
|
||||
// Critical performance issues should not be present
|
||||
const criticalPerfIssues = (page.consoleMessages || []).filter(msg => {
|
||||
const text = msg.text || msg.error || '';
|
||||
return msg.type === 'error' && performanceKeywords.some(keyword =>
|
||||
text.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
expect(criticalPerfIssues.length, `Critical performance errors: ${JSON.stringify(criticalPerfIssues)}`).toBe(0);
|
||||
|
||||
console.log('✅ Performance warning detection completed');
|
||||
});
|
||||
|
||||
test('should analyze error context and provide debugging information', async ({ page }) => {
|
||||
console.log('🔬 Analyzing error context for debugging insights...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
// Collect errors with context
|
||||
const contextualErrors = [];
|
||||
|
||||
// Navigate through application collecting error context
|
||||
const testScenarios = [
|
||||
{
|
||||
name: 'Invalid Route Access',
|
||||
action: () => page.goto('/nonexistent-route'),
|
||||
expectErrors: true
|
||||
},
|
||||
{
|
||||
name: 'Rapid Navigation',
|
||||
action: async () => {
|
||||
await page.goto('/dashboard');
|
||||
await page.goto('/invoices');
|
||||
await page.goto('/payments');
|
||||
await page.goto('/dashboard');
|
||||
},
|
||||
expectErrors: false
|
||||
},
|
||||
{
|
||||
name: 'Form Interaction',
|
||||
action: async () => {
|
||||
await page.goto('/invoices');
|
||||
if (await page.locator('[data-testid="search-input"]').isVisible()) {
|
||||
await page.fill('[data-testid="search-input"]', 'test search');
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
},
|
||||
expectErrors: false
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of testScenarios) {
|
||||
console.log(`🎭 Testing scenario: ${scenario.name}`);
|
||||
|
||||
const initialMessageCount = (page.consoleMessages || []).length;
|
||||
|
||||
try {
|
||||
await scenario.action();
|
||||
await page.waitForLoadState('networkidle', { timeout: 8000 });
|
||||
} catch (error) {
|
||||
console.log(`ℹ️ Expected error in ${scenario.name}:`, error.message);
|
||||
}
|
||||
|
||||
const newMessages = (page.consoleMessages || []).slice(initialMessageCount);
|
||||
const errors = newMessages.filter(msg => msg.type === 'error' || msg.type === 'pageerror');
|
||||
|
||||
if (errors.length > 0) {
|
||||
errors.forEach(error => {
|
||||
contextualErrors.push({
|
||||
scenario: scenario.name,
|
||||
error: error,
|
||||
url: page.url(),
|
||||
timestamp: error.timestamp,
|
||||
expected: scenario.expectErrors
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`📍 ${scenario.name}: ${errors.length} errors (expected: ${scenario.expectErrors})`);
|
||||
} else {
|
||||
console.log(`✅ ${scenario.name}: No errors detected`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze contextual errors
|
||||
if (contextualErrors.length > 0) {
|
||||
console.log('🔍 Contextual Error Analysis:');
|
||||
|
||||
// Group errors by type and scenario
|
||||
const errorsByScenario = {};
|
||||
const errorsByType = {};
|
||||
|
||||
contextualErrors.forEach(error => {
|
||||
// Group by scenario
|
||||
if (!errorsByScenario[error.scenario]) {
|
||||
errorsByScenario[error.scenario] = [];
|
||||
}
|
||||
errorsByScenario[error.scenario].push(error);
|
||||
|
||||
// Group by error type
|
||||
const errorText = error.error.text || error.error.error || '';
|
||||
const pattern = commonErrorPatterns.find(p => p.pattern.test(errorText));
|
||||
const category = pattern ? pattern.category : 'Unclassified';
|
||||
|
||||
errorsByType[category] = (errorsByType[category] || 0) + 1;
|
||||
});
|
||||
|
||||
console.log('📊 Errors by Scenario:');
|
||||
Object.entries(errorsByScenario).forEach(([scenario, errors]) => {
|
||||
console.log(` ${scenario}: ${errors.length} errors`);
|
||||
});
|
||||
|
||||
console.log('📊 Errors by Type:');
|
||||
Object.entries(errorsByType).forEach(([type, count]) => {
|
||||
console.log(` ${type}: ${count} occurrences`);
|
||||
});
|
||||
|
||||
// Identify unexpected errors (those in scenarios that shouldn't have errors)
|
||||
const unexpectedErrors = contextualErrors.filter(error => !error.expected);
|
||||
|
||||
if (unexpectedErrors.length > 0) {
|
||||
console.warn('🚨 Unexpected errors detected:');
|
||||
unexpectedErrors.forEach(error => {
|
||||
console.warn(` ${error.scenario}: ${error.error.text || error.error.error}`);
|
||||
});
|
||||
|
||||
// Unexpected critical errors should fail the test
|
||||
const criticalUnexpected = unexpectedErrors.filter(error =>
|
||||
ErrorClassifier.classify(error.error) === 'CRITICAL'
|
||||
);
|
||||
|
||||
expect(criticalUnexpected.length, `Unexpected critical errors: ${JSON.stringify(criticalUnexpected.map(e => e.error.text))}`).toBe(0);
|
||||
}
|
||||
} else {
|
||||
console.log('✅ No contextual errors to analyze');
|
||||
}
|
||||
|
||||
console.log('✅ Error context analysis completed');
|
||||
});
|
||||
|
||||
test('should generate comprehensive error report for debugging', async ({ page }) => {
|
||||
console.log('📋 Generating comprehensive error report...');
|
||||
|
||||
// Perform full application workflow
|
||||
await authenticateWithRealCredentials(page);
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
const workflow = [
|
||||
() => page.goto('/dashboard'),
|
||||
() => page.goto('/invoices'),
|
||||
() => page.goto('/payments'),
|
||||
() => page.goto('/dashboard')
|
||||
];
|
||||
|
||||
for (const step of workflow) {
|
||||
await step();
|
||||
await page.waitForLoadState('networkidle', { timeout: 8000 });
|
||||
}
|
||||
|
||||
// Generate final error report
|
||||
const finalReport = generateErrorReport(page, 'Complete Application Workflow');
|
||||
|
||||
console.log('📊 Final Error Report:');
|
||||
console.log(' Test:', finalReport.testName);
|
||||
console.log(' Timestamp:', finalReport.timestamp);
|
||||
console.log(' Summary:', finalReport.summary);
|
||||
|
||||
if (finalReport.details.criticalErrors.length > 0) {
|
||||
console.log('🚨 Critical Errors:');
|
||||
finalReport.details.criticalErrors.forEach(error => {
|
||||
console.log(` - ${error.text || error.error} (${error.location?.url || 'unknown'})`);
|
||||
});
|
||||
}
|
||||
|
||||
if (finalReport.details.warnings.length > 0) {
|
||||
console.log('⚠️ Warnings:');
|
||||
finalReport.details.warnings.slice(0, 5).forEach(warning => {
|
||||
console.log(` - ${warning.text || warning.error}`);
|
||||
});
|
||||
|
||||
if (finalReport.details.warnings.length > 5) {
|
||||
console.log(` ... and ${finalReport.details.warnings.length - 5} more warnings`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(finalReport.details.errorPatterns).length > 0) {
|
||||
console.log('📈 Error Patterns:');
|
||||
Object.entries(finalReport.details.errorPatterns).forEach(([pattern, count]) => {
|
||||
console.log(` ${pattern}: ${count} occurrences`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance metrics
|
||||
if (finalReport.performance && finalReport.performance.apiCalls) {
|
||||
const slowApiCalls = finalReport.performance.apiCalls.filter(call => call.timing > 2000);
|
||||
if (slowApiCalls.length > 0) {
|
||||
console.log('⚡ Slow API Calls:');
|
||||
slowApiCalls.forEach(call => {
|
||||
console.log(` ${call.url}: ${call.timing}ms`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Final validation
|
||||
expect(finalReport.summary.classifications.critical, 'Critical errors in comprehensive workflow').toBe(0);
|
||||
|
||||
console.log('✅ Comprehensive error report generated successfully');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* Performance Regression Testing Suite
|
||||
* Monitors application performance baselines and detects regressions
|
||||
* Tests real Oracle data loading performance with comprehensive metrics
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
authenticateWithRealCredentials,
|
||||
selectCompany,
|
||||
REAL_CREDENTIALS
|
||||
} from '../../utils/real-auth.js';
|
||||
import {
|
||||
setupConsoleCapture,
|
||||
PerformanceMonitor,
|
||||
PerformanceBaselines,
|
||||
assertPerformanceBaseline,
|
||||
generateErrorReport
|
||||
} from '../../utils/console-monitor.js';
|
||||
|
||||
test.describe('Performance Regression Testing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
setupConsoleCapture(page);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
const report = generateErrorReport(page, test.info().title);
|
||||
|
||||
// Log performance metrics from the test
|
||||
if (page.performanceMetrics?.apiCalls?.length > 0) {
|
||||
const avgApiTime = page.performanceMetrics.apiCalls
|
||||
.reduce((sum, call) => sum + call.timing, 0) / page.performanceMetrics.apiCalls.length;
|
||||
|
||||
console.log(`📊 Average API response time: ${avgApiTime.toFixed(0)}ms`);
|
||||
|
||||
const slowCalls = page.performanceMetrics.apiCalls.filter(call => call.timing > 3000);
|
||||
if (slowCalls.length > 0) {
|
||||
console.warn('⚠️ Slow API calls detected:', slowCalls.map(c => `${c.url}: ${c.timing}ms`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should meet performance baselines with real data', async ({ page }) => {
|
||||
console.log('📈 Testing performance baselines with ROMFAST real data...');
|
||||
|
||||
// Measure login performance
|
||||
console.log('🔐 Measuring login performance...');
|
||||
const loginStart = Date.now();
|
||||
const authResult = await authenticateWithRealCredentials(page);
|
||||
const loginTime = Date.now() - loginStart;
|
||||
|
||||
expect(authResult.success, 'Authentication must succeed for performance test').toBe(true);
|
||||
assertPerformanceBaseline(loginTime, PerformanceBaselines.loginTime, 'Login process', expect);
|
||||
|
||||
console.log(`✅ Login completed in ${loginTime}ms (baseline: ${PerformanceBaselines.loginTime}ms)`);
|
||||
|
||||
// Measure dashboard load with ROMFAST data
|
||||
console.log('📊 Measuring dashboard load performance...');
|
||||
const dashboardStart = Date.now();
|
||||
const selectSuccess = await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
await page.waitForSelector('[data-testid="dashboard-stats"]', { timeout: 15000 });
|
||||
const dashboardTime = Date.now() - dashboardStart;
|
||||
|
||||
expect(selectSuccess, 'Company selection must succeed').toBe(true);
|
||||
assertPerformanceBaseline(dashboardTime, PerformanceBaselines.dashboardLoad, 'Dashboard load', expect);
|
||||
|
||||
console.log(`✅ Dashboard loaded in ${dashboardTime}ms (baseline: ${PerformanceBaselines.dashboardLoad}ms)`);
|
||||
|
||||
// Measure report generation performance
|
||||
console.log('📋 Measuring report generation performance...');
|
||||
const reportStart = Date.now();
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 20000 });
|
||||
const reportTime = Date.now() - reportStart;
|
||||
|
||||
assertPerformanceBaseline(reportTime, PerformanceBaselines.reportGeneration, 'Invoice report generation', expect);
|
||||
|
||||
console.log(`✅ Report generated in ${reportTime}ms (baseline: ${PerformanceBaselines.reportGeneration}ms)`);
|
||||
|
||||
// Check for performance-related console warnings
|
||||
const performanceWarnings = (page.consoleMessages || [])
|
||||
.filter(msg => msg.text && (
|
||||
msg.text.includes('slow') ||
|
||||
msg.text.includes('performance') ||
|
||||
msg.text.includes('timeout')
|
||||
));
|
||||
|
||||
if (performanceWarnings.length > 0) {
|
||||
console.warn('⚠️ Performance warnings detected:', performanceWarnings.map(w => w.text));
|
||||
}
|
||||
|
||||
// Overall workflow should be reasonably fast
|
||||
const totalWorkflowTime = loginTime + dashboardTime + reportTime;
|
||||
expect(totalWorkflowTime).toBeLessThan(12000); // Max 12s for complete workflow
|
||||
|
||||
console.log(`✅ Complete workflow: ${totalWorkflowTime}ms`);
|
||||
});
|
||||
|
||||
test('should detect performance regressions across multiple runs', async ({ page }) => {
|
||||
console.log('🔄 Testing performance consistency across multiple runs...');
|
||||
|
||||
const performanceRuns = [];
|
||||
const numberOfRuns = 3;
|
||||
|
||||
for (let run = 1; run <= numberOfRuns; run++) {
|
||||
console.log(`🏃 Performance run ${run}/${numberOfRuns}`);
|
||||
|
||||
// Clear state for clean run
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
const runMetrics = {
|
||||
run: run,
|
||||
login: 0,
|
||||
dashboard: 0,
|
||||
invoices: 0,
|
||||
payments: 0,
|
||||
navigation: 0
|
||||
};
|
||||
|
||||
// Login timing
|
||||
const loginStart = Date.now();
|
||||
const authResult = await authenticateWithRealCredentials(page);
|
||||
runMetrics.login = Date.now() - loginStart;
|
||||
|
||||
expect(authResult.success).toBe(true);
|
||||
|
||||
// Dashboard timing
|
||||
const dashboardStart = Date.now();
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
await page.waitForSelector('[data-testid="dashboard-stats"]', { timeout: 15000 });
|
||||
runMetrics.dashboard = Date.now() - dashboardStart;
|
||||
|
||||
// Invoices timing
|
||||
const invoicesStart = Date.now();
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 });
|
||||
runMetrics.invoices = Date.now() - invoicesStart;
|
||||
|
||||
// Payments timing
|
||||
const paymentsStart = Date.now();
|
||||
await page.click('[data-testid="nav-payments"]');
|
||||
await page.waitForSelector('[data-testid="payments-table"]', { timeout: 15000 });
|
||||
runMetrics.payments = Date.now() - paymentsStart;
|
||||
|
||||
// Navigation timing (return to dashboard)
|
||||
const navStart = Date.now();
|
||||
await page.click('[data-testid="nav-dashboard"]');
|
||||
await page.waitForSelector('[data-testid="dashboard-stats"]', { timeout: 10000 });
|
||||
runMetrics.navigation = Date.now() - navStart;
|
||||
|
||||
performanceRuns.push(runMetrics);
|
||||
|
||||
console.log(`📊 Run ${run} metrics:`, {
|
||||
login: `${runMetrics.login}ms`,
|
||||
dashboard: `${runMetrics.dashboard}ms`,
|
||||
invoices: `${runMetrics.invoices}ms`,
|
||||
payments: `${runMetrics.payments}ms`,
|
||||
navigation: `${runMetrics.navigation}ms`
|
||||
});
|
||||
|
||||
// Short delay between runs
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
// Analyze performance consistency
|
||||
const metrics = ['login', 'dashboard', 'invoices', 'payments', 'navigation'];
|
||||
const performanceAnalysis = {};
|
||||
|
||||
metrics.forEach(metric => {
|
||||
const values = performanceRuns.map(run => run[metric]);
|
||||
const avg = values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
performanceAnalysis[metric] = {
|
||||
average: avg,
|
||||
min: min,
|
||||
max: max,
|
||||
standardDeviation: stdDev,
|
||||
variationCoeff: (stdDev / avg) * 100 // Coefficient of variation as percentage
|
||||
};
|
||||
});
|
||||
|
||||
console.log('📈 Performance Consistency Analysis:');
|
||||
Object.entries(performanceAnalysis).forEach(([metric, stats]) => {
|
||||
console.log(` ${metric}:`);
|
||||
console.log(` Average: ${stats.average.toFixed(0)}ms`);
|
||||
console.log(` Range: ${stats.min}ms - ${stats.max}ms`);
|
||||
console.log(` Variation: ${stats.variationCoeff.toFixed(1)}%`);
|
||||
});
|
||||
|
||||
// Validate performance consistency
|
||||
metrics.forEach(metric => {
|
||||
const stats = performanceAnalysis[metric];
|
||||
|
||||
// Average should meet baseline
|
||||
const baseline = PerformanceBaselines[metric] || PerformanceBaselines.apiResponse;
|
||||
expect(stats.average, `${metric} average performance regression`).toBeLessThan(baseline);
|
||||
|
||||
// Variation should be reasonable (< 50% coefficient of variation)
|
||||
expect(stats.variationCoeff, `${metric} performance too inconsistent`).toBeLessThan(50);
|
||||
|
||||
// Max time should not be extremely higher than average (< 2x)
|
||||
const maxRatio = stats.max / stats.average;
|
||||
expect(maxRatio, `${metric} has outlier performance`).toBeLessThan(2.5);
|
||||
});
|
||||
|
||||
console.log('✅ Performance consistency validated across all runs');
|
||||
});
|
||||
|
||||
test('should measure page load performance metrics', async ({ page }) => {
|
||||
console.log('📄 Measuring comprehensive page load performance...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
const pages = [
|
||||
{ path: '/dashboard', name: 'Dashboard', selector: '[data-testid="dashboard-stats"]' },
|
||||
{ path: '/invoices', name: 'Invoices', selector: '[data-testid="invoices-table"]' },
|
||||
{ path: '/payments', name: 'Payments', selector: '[data-testid="payments-table"]' }
|
||||
];
|
||||
|
||||
const pageMetrics = [];
|
||||
|
||||
for (const pageInfo of pages) {
|
||||
console.log(`📊 Measuring ${pageInfo.name} page performance...`);
|
||||
|
||||
const navigationStart = Date.now();
|
||||
await page.goto(pageInfo.path);
|
||||
|
||||
// Wait for page to be interactive
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
const domContentLoadTime = Date.now() - navigationStart;
|
||||
|
||||
// Wait for main content
|
||||
await page.waitForSelector(pageInfo.selector, { timeout: 15000 });
|
||||
const contentLoadTime = Date.now() - navigationStart;
|
||||
|
||||
// Get detailed performance metrics
|
||||
const perfMetrics = await PerformanceMonitor.measurePageLoad(page);
|
||||
const networkMetrics = await PerformanceMonitor.getNetworkMetrics(page);
|
||||
|
||||
const pageMetric = {
|
||||
page: pageInfo.name,
|
||||
path: pageInfo.path,
|
||||
navigationTime: contentLoadTime,
|
||||
domContentLoaded: domContentLoadTime,
|
||||
performanceApi: perfMetrics,
|
||||
network: networkMetrics
|
||||
};
|
||||
|
||||
pageMetrics.push(pageMetric);
|
||||
|
||||
console.log(` ${pageInfo.name} Performance:`);
|
||||
console.log(` Navigation: ${contentLoadTime}ms`);
|
||||
console.log(` DOM Content Loaded: ${domContentLoadTime}ms`);
|
||||
console.log(` First Paint: ${perfMetrics.firstPaint.toFixed(0)}ms`);
|
||||
console.log(` Network Resources: ${networkMetrics.totalResources}`);
|
||||
console.log(` Average Resource Time: ${networkMetrics.averageResponseTime.toFixed(0)}ms`);
|
||||
|
||||
if (networkMetrics.slowResources.length > 0) {
|
||||
console.log(` Slow Resources: ${networkMetrics.slowResources.length}`);
|
||||
networkMetrics.slowResources.slice(0, 3).forEach(resource => {
|
||||
console.log(` ${resource.name}: ${resource.duration.toFixed(0)}ms`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate page performance
|
||||
pageMetrics.forEach(metric => {
|
||||
// Navigation time should meet baseline
|
||||
assertPerformanceBaseline(
|
||||
metric.navigationTime,
|
||||
PerformanceBaselines.pageLoad,
|
||||
`${metric.page} navigation`,
|
||||
expect
|
||||
);
|
||||
|
||||
// DOM content should load quickly
|
||||
expect(metric.domContentLoaded, `${metric.page} DOM content load too slow`)
|
||||
.toBeLessThan(3000);
|
||||
|
||||
// First paint should be reasonable
|
||||
if (metric.performanceApi.firstPaint > 0) {
|
||||
expect(metric.performanceApi.firstPaint, `${metric.page} first paint too slow`)
|
||||
.toBeLessThan(2000);
|
||||
}
|
||||
|
||||
// Should not have excessive slow resources
|
||||
expect(metric.network.slowResources.length, `${metric.page} has too many slow resources`)
|
||||
.toBeLessThan(5);
|
||||
});
|
||||
|
||||
console.log('✅ Page load performance metrics validated');
|
||||
});
|
||||
|
||||
test('should monitor API response times and detect slow endpoints', async ({ page }) => {
|
||||
console.log('🌐 Monitoring API response times...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
const apiEndpoints = [
|
||||
{ name: 'Companies', trigger: () => page.reload() },
|
||||
{ name: 'Dashboard Stats', trigger: () => page.goto('/dashboard') },
|
||||
{ name: 'Invoices', trigger: () => page.goto('/invoices') },
|
||||
{ name: 'Payments', trigger: () => page.goto('/payments') }
|
||||
];
|
||||
|
||||
const apiMetrics = [];
|
||||
|
||||
for (const endpoint of apiEndpoints) {
|
||||
console.log(`📡 Testing ${endpoint.name} API performance...`);
|
||||
|
||||
// Clear previous metrics
|
||||
if (page.performanceMetrics) {
|
||||
page.performanceMetrics.apiCalls = [];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
await endpoint.trigger();
|
||||
|
||||
// Wait for API calls to complete
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
// Analyze API calls made during this operation
|
||||
const apiCalls = page.performanceMetrics?.apiCalls || [];
|
||||
|
||||
if (apiCalls.length > 0) {
|
||||
const avgResponseTime = apiCalls.reduce((sum, call) => sum + call.timing, 0) / apiCalls.length;
|
||||
const maxResponseTime = Math.max(...apiCalls.map(call => call.timing));
|
||||
const slowCalls = apiCalls.filter(call => call.timing > PerformanceBaselines.apiResponse);
|
||||
|
||||
const metric = {
|
||||
endpoint: endpoint.name,
|
||||
totalTime: totalTime,
|
||||
apiCallCount: apiCalls.length,
|
||||
averageApiTime: avgResponseTime,
|
||||
maxApiTime: maxResponseTime,
|
||||
slowCallCount: slowCalls.length,
|
||||
slowCalls: slowCalls
|
||||
};
|
||||
|
||||
apiMetrics.push(metric);
|
||||
|
||||
console.log(` ${endpoint.name} API Metrics:`);
|
||||
console.log(` Total Operation: ${totalTime}ms`);
|
||||
console.log(` API Calls: ${apiCalls.length}`);
|
||||
console.log(` Average API Time: ${avgResponseTime.toFixed(0)}ms`);
|
||||
console.log(` Max API Time: ${maxResponseTime}ms`);
|
||||
|
||||
if (slowCalls.length > 0) {
|
||||
console.log(` Slow Calls: ${slowCalls.length}`);
|
||||
slowCalls.forEach(call => {
|
||||
console.log(` ${call.url}: ${call.timing}ms (${call.status})`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(` ${endpoint.name}: No API calls detected`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate API performance
|
||||
apiMetrics.forEach(metric => {
|
||||
// Average API response time should meet baseline
|
||||
if (metric.averageApiTime > 0) {
|
||||
assertPerformanceBaseline(
|
||||
metric.averageApiTime,
|
||||
PerformanceBaselines.apiResponse,
|
||||
`${metric.endpoint} average API response`,
|
||||
expect
|
||||
);
|
||||
}
|
||||
|
||||
// Should not have many slow calls
|
||||
const slowCallRatio = metric.slowCallCount / metric.apiCallCount;
|
||||
expect(slowCallRatio, `${metric.endpoint} has too many slow API calls`)
|
||||
.toBeLessThan(0.3); // Max 30% slow calls
|
||||
|
||||
// No API call should be extremely slow
|
||||
expect(metric.maxApiTime, `${metric.endpoint} has extremely slow API call`)
|
||||
.toBeLessThan(10000); // Max 10s
|
||||
});
|
||||
|
||||
// Overall API performance summary
|
||||
const totalApiCalls = apiMetrics.reduce((sum, m) => sum + m.apiCallCount, 0);
|
||||
const totalSlowCalls = apiMetrics.reduce((sum, m) => sum + m.slowCallCount, 0);
|
||||
const overallSlowRatio = totalSlowCalls / totalApiCalls;
|
||||
|
||||
console.log('📊 Overall API Performance Summary:');
|
||||
console.log(` Total API Calls: ${totalApiCalls}`);
|
||||
console.log(` Slow Calls: ${totalSlowCalls}`);
|
||||
console.log(` Slow Call Rate: ${(overallSlowRatio * 100).toFixed(1)}%`);
|
||||
|
||||
expect(overallSlowRatio, 'Overall API performance degraded').toBeLessThan(0.25);
|
||||
|
||||
console.log('✅ API response time monitoring completed');
|
||||
});
|
||||
|
||||
test('should detect memory leaks and resource usage patterns', async ({ page }) => {
|
||||
console.log('🧠 Monitoring memory usage and detecting potential leaks...');
|
||||
|
||||
await authenticateWithRealCredentials(page);
|
||||
|
||||
const memorySnapshots = [];
|
||||
const operations = [
|
||||
{ name: 'Initial State', action: () => Promise.resolve() },
|
||||
{ name: 'Company Selection', action: () => selectCompany(page, REAL_CREDENTIALS.company) },
|
||||
{ name: 'Invoices Load', action: () => {
|
||||
return page.goto('/invoices').then(() =>
|
||||
page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 })
|
||||
);
|
||||
}},
|
||||
{ name: 'Payments Load', action: () => {
|
||||
return page.goto('/payments').then(() =>
|
||||
page.waitForSelector('[data-testid="payments-table"]', { timeout: 15000 })
|
||||
);
|
||||
}},
|
||||
{ name: 'Dashboard Return', action: () => page.goto('/dashboard') },
|
||||
{ name: 'Multiple Navigation Cycles', action: async () => {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.goto('/invoices');
|
||||
await page.goto('/payments');
|
||||
await page.goto('/dashboard');
|
||||
}
|
||||
}}
|
||||
];
|
||||
|
||||
for (const operation of operations) {
|
||||
console.log(`📊 Memory snapshot: ${operation.name}`);
|
||||
|
||||
await operation.action();
|
||||
await page.waitForLoadState('networkidle', { timeout: 10000 });
|
||||
|
||||
// Get memory metrics
|
||||
const memoryMetrics = await page.evaluate(() => {
|
||||
// Force garbage collection if available (in dev environments)
|
||||
if (window.gc && typeof window.gc === 'function') {
|
||||
window.gc();
|
||||
}
|
||||
|
||||
const performance = window.performance;
|
||||
const memory = performance.memory || {};
|
||||
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
usedJSHeapSize: memory.usedJSHeapSize || 0,
|
||||
totalJSHeapSize: memory.totalJSHeapSize || 0,
|
||||
jsHeapSizeLimit: memory.jsHeapSizeLimit || 0,
|
||||
// Additional performance metrics
|
||||
navigation: performance.getEntriesByType('navigation')[0] || {},
|
||||
resources: performance.getEntriesByType('resource').length
|
||||
};
|
||||
});
|
||||
|
||||
memorySnapshots.push({
|
||||
operation: operation.name,
|
||||
...memoryMetrics
|
||||
});
|
||||
|
||||
if (memoryMetrics.usedJSHeapSize > 0) {
|
||||
const memoryMB = (memoryMetrics.usedJSHeapSize / 1024 / 1024).toFixed(1);
|
||||
console.log(` Used Memory: ${memoryMB}MB`);
|
||||
console.log(` Resources: ${memoryMetrics.resources}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze memory usage patterns
|
||||
if (memorySnapshots.length > 1 && memorySnapshots[0].usedJSHeapSize > 0) {
|
||||
console.log('🔍 Memory Usage Analysis:');
|
||||
|
||||
// Check for significant memory increases
|
||||
const initialMemory = memorySnapshots[0].usedJSHeapSize;
|
||||
const finalMemory = memorySnapshots[memorySnapshots.length - 1].usedJSHeapSize;
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
const increaseRatio = memoryIncrease / initialMemory;
|
||||
|
||||
console.log(` Initial Memory: ${(initialMemory / 1024 / 1024).toFixed(1)}MB`);
|
||||
console.log(` Final Memory: ${(finalMemory / 1024 / 1024).toFixed(1)}MB`);
|
||||
console.log(` Memory Increase: ${(memoryIncrease / 1024 / 1024).toFixed(1)}MB`);
|
||||
console.log(` Increase Ratio: ${(increaseRatio * 100).toFixed(1)}%`);
|
||||
|
||||
// Memory increase should be reasonable (< 100% growth)
|
||||
expect(increaseRatio, 'Potential memory leak detected').toBeLessThan(1.0);
|
||||
|
||||
// Final memory should not be excessive (< 100MB for typical usage)
|
||||
const finalMemoryMB = finalMemory / 1024 / 1024;
|
||||
expect(finalMemoryMB, 'Excessive memory usage').toBeLessThan(100);
|
||||
|
||||
// Check for memory leaks by comparing before/after cycles
|
||||
if (memorySnapshots.length >= 4) {
|
||||
const beforeCycles = memorySnapshots[memorySnapshots.length - 3].usedJSHeapSize;
|
||||
const afterCycles = memorySnapshots[memorySnapshots.length - 1].usedJSHeapSize;
|
||||
const cycleIncrease = (afterCycles - beforeCycles) / beforeCycles;
|
||||
|
||||
if (cycleIncrease > 0.5) { // > 50% increase from cycles
|
||||
console.warn(`⚠️ Potential memory leak from repeated operations: ${(cycleIncrease * 100).toFixed(1)}% increase`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ Memory metrics not available in this environment');
|
||||
}
|
||||
|
||||
console.log('✅ Memory usage monitoring completed');
|
||||
});
|
||||
});
|
||||
138
reports-app/frontend/tests/integration/global-setup.js
Normal file
138
reports-app/frontend/tests/integration/global-setup.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Global setup for real API integration tests
|
||||
* Ensures SSH tunnel and backend services are running
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default async function globalSetup() {
|
||||
console.log('🔧 Setting up real API integration test environment...');
|
||||
|
||||
// Root directory for reference if needed later
|
||||
// const rootDir = path.resolve(__dirname, '../../../../../..');
|
||||
|
||||
try {
|
||||
// Check if SSH tunnel is running by testing Oracle port
|
||||
console.log('📡 Checking SSH tunnel status...');
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/health', { timeout: 5000 });
|
||||
const health = await response.json();
|
||||
if (health.database === 'connected') {
|
||||
console.log('✅ SSH tunnel appears to be working (database connected)');
|
||||
} else {
|
||||
console.log('⚠️ Database not connected - SSH tunnel may need to be started manually');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Could not check tunnel status - continuing anyway');
|
||||
}
|
||||
|
||||
// Check backend health
|
||||
console.log('🏥 Checking backend health...');
|
||||
try {
|
||||
const healthResponse = await fetch('http://localhost:8000/health', {
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!healthResponse.ok) {
|
||||
throw new Error(`Backend health check failed: ${healthResponse.status}`);
|
||||
}
|
||||
|
||||
const healthData = await healthResponse.json();
|
||||
console.log('✅ Backend health check passed:', healthData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Backend health check failed:', error.message);
|
||||
throw new Error('Backend is not available for integration tests');
|
||||
}
|
||||
|
||||
// Check frontend availability
|
||||
console.log('🌐 Checking frontend availability...');
|
||||
try {
|
||||
const frontendResponse = await fetch('http://localhost:3001', {
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!frontendResponse.ok) {
|
||||
throw new Error(`Frontend not available: ${frontendResponse.status}`);
|
||||
}
|
||||
|
||||
console.log('✅ Frontend is available');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Frontend availability check failed:', error.message);
|
||||
throw new Error('Frontend is not available for integration tests');
|
||||
}
|
||||
|
||||
// Validate environment variables
|
||||
console.log('🔐 Validating environment configuration...');
|
||||
const requiredEnvVars = [
|
||||
'ORACLE_USER',
|
||||
'ORACLE_PASSWORD',
|
||||
'ORACLE_HOST',
|
||||
'ORACLE_PORT',
|
||||
'ORACLE_SID'
|
||||
];
|
||||
|
||||
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
|
||||
if (missingVars.length > 0) {
|
||||
console.warn('⚠️ Missing environment variables:', missingVars.join(', '));
|
||||
console.log('ℹ️ Some tests may use default values');
|
||||
} else {
|
||||
console.log('✅ All required environment variables are set');
|
||||
}
|
||||
|
||||
// Test database connectivity through backend
|
||||
console.log('🗄️ Testing database connectivity...');
|
||||
try {
|
||||
const dbTestResponse = await fetch('http://localhost:8000/api/companies', {
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
if (dbTestResponse.ok) {
|
||||
const companies = await dbTestResponse.json();
|
||||
console.log(`✅ Database connectivity verified (${companies.length} companies found)`);
|
||||
|
||||
// Check if ROMFAST is available
|
||||
const romfast = companies.find(c => c.id_firma === 'ROMFAST');
|
||||
if (romfast) {
|
||||
console.log('✅ ROMFAST company data available for testing');
|
||||
} else {
|
||||
console.warn('⚠️ ROMFAST company not found in test data');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Database connectivity test returned:', dbTestResponse.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Database connectivity test failed:', error.message);
|
||||
console.log('ℹ️ Tests will proceed but may fail if database is not accessible');
|
||||
}
|
||||
|
||||
console.log('🎯 Global setup completed successfully');
|
||||
|
||||
// Store setup metadata for tests
|
||||
global.__INTEGRATION_SETUP__ = {
|
||||
timestamp: new Date().toISOString(),
|
||||
backend: 'http://localhost:8000',
|
||||
frontend: 'http://localhost:3001',
|
||||
sshTunnelActive: true,
|
||||
environmentValidated: true
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Global setup encountered error:', error.message);
|
||||
console.log('ℹ️ Continuing with tests - they may fail if services are not available');
|
||||
|
||||
// Don't fail setup - let individual tests handle service unavailability
|
||||
global.__INTEGRATION_SETUP__ = {
|
||||
timestamp: new Date().toISOString(),
|
||||
backend: 'http://localhost:8000',
|
||||
frontend: 'http://localhost:3001',
|
||||
sshTunnelActive: false,
|
||||
environmentValidated: false,
|
||||
setupError: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
51
reports-app/frontend/tests/integration/global-teardown.js
Normal file
51
reports-app/frontend/tests/integration/global-teardown.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Global teardown for real API integration tests
|
||||
* Cleanup resources and generate final reports
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default async function globalTeardown() {
|
||||
console.log('🧹 Starting global teardown for integration tests...');
|
||||
|
||||
try {
|
||||
// Generate final test report
|
||||
const testReport = {
|
||||
testRun: {
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'integration',
|
||||
environment: 'development'
|
||||
},
|
||||
setup: global.__INTEGRATION_SETUP__ || {},
|
||||
summary: {
|
||||
message: 'Integration test run completed',
|
||||
backend: 'http://localhost:8000',
|
||||
frontend: 'http://localhost:3001',
|
||||
sshTunnel: 'managed externally'
|
||||
}
|
||||
};
|
||||
|
||||
// Write final report
|
||||
const reportPath = path.join(__dirname, '../../test-results/integration-summary.json');
|
||||
try {
|
||||
writeFileSync(reportPath, JSON.stringify(testReport, null, 2));
|
||||
console.log(`📊 Integration test summary written to: ${reportPath}`);
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not write integration test summary:', error.message);
|
||||
}
|
||||
|
||||
// Log cleanup completion
|
||||
console.log('✅ Global teardown completed');
|
||||
console.log('ℹ️ SSH tunnel and services left running for continued development');
|
||||
console.log('ℹ️ Use ./ssh_tunnel.sh stop to manually stop the SSH tunnel if needed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Global teardown encountered errors:', error.message);
|
||||
// Don't fail teardown on non-critical errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* ROMFAST Company Data Integration Tests
|
||||
* Tests real Oracle data loading and validation for ROMFAST company
|
||||
* Monitors console errors during data operations
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import {
|
||||
authenticateWithRealCredentials,
|
||||
selectCompany,
|
||||
getRealCompanies,
|
||||
REAL_CREDENTIALS,
|
||||
API_ENDPOINTS
|
||||
} from '../../utils/real-auth.js';
|
||||
import {
|
||||
setupConsoleCapture,
|
||||
assertNoCriticalErrors,
|
||||
generateErrorReport,
|
||||
PerformanceMonitor,
|
||||
PerformanceBaselines,
|
||||
assertPerformanceBaseline
|
||||
} from '../../utils/console-monitor.js';
|
||||
|
||||
test.describe('ROMFAST Company Data Integration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Setup console monitoring
|
||||
setupConsoleCapture(page);
|
||||
|
||||
// Authenticate with real credentials
|
||||
const authResult = await authenticateWithRealCredentials(page);
|
||||
expect(authResult.success, `Authentication failed: ${authResult.error}`).toBe(true);
|
||||
|
||||
console.log('🔐 Authenticated successfully for ROMFAST data tests');
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate comprehensive error report
|
||||
const report = generateErrorReport(page, test.info().title);
|
||||
|
||||
if (report.summary.classifications.critical > 0) {
|
||||
console.warn('❌ Critical errors in ROMFAST data test:', report.details.criticalErrors);
|
||||
}
|
||||
|
||||
if (report.summary.classifications.warning > 3) {
|
||||
console.warn('⚠️ High number of warnings:', report.summary.classifications.warning);
|
||||
}
|
||||
});
|
||||
|
||||
test('should load ROMFAST company data correctly', async ({ page }) => {
|
||||
console.log('🏢 Testing ROMFAST company data loading...');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Select ROMFAST from real companies list
|
||||
const selectSuccess = await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
expect(selectSuccess, 'Failed to select ROMFAST company').toBe(true);
|
||||
|
||||
const selectionTime = Date.now() - startTime;
|
||||
|
||||
// Verify company stats loaded
|
||||
await page.waitForSelector('[data-testid="company-stats"]', { timeout: 15000 });
|
||||
|
||||
// Verify company name display
|
||||
const companyName = await page.locator('[data-testid="company-name"]').textContent();
|
||||
expect(companyName).toContain('ROMFAST');
|
||||
|
||||
// Check for console errors during data load
|
||||
const criticalErrors = (page.consoleMessages || [])
|
||||
.filter(msg => msg.type === 'error' && !msg.text.includes('404'));
|
||||
|
||||
expect(criticalErrors, `Critical errors during ROMFAST data load: ${JSON.stringify(criticalErrors)}`).toHaveLength(0);
|
||||
|
||||
// Validate performance
|
||||
assertPerformanceBaseline(
|
||||
selectionTime,
|
||||
PerformanceBaselines.dashboardLoad,
|
||||
'ROMFAST company selection',
|
||||
expect
|
||||
);
|
||||
|
||||
console.log(`✅ ROMFAST company data loaded successfully in ${selectionTime}ms`);
|
||||
});
|
||||
|
||||
test('should validate ROMFAST invoice data structure', async ({ page }) => {
|
||||
console.log('📋 Testing ROMFAST invoice data structure...');
|
||||
|
||||
// Select ROMFAST company
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Navigate to invoices
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForURL('/invoices');
|
||||
|
||||
// Measure API response time
|
||||
const apiStartTime = Date.now();
|
||||
await page.waitForResponse(response =>
|
||||
response.url().includes('/api/invoices') && response.status() === 200,
|
||||
{ timeout: 10000 }
|
||||
);
|
||||
const apiResponseTime = Date.now() - apiStartTime;
|
||||
|
||||
// Wait for invoice data to load
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 });
|
||||
|
||||
// Verify Oracle-specific data fields are present
|
||||
const invoiceRows = await page.locator('[data-testid="invoice-row"]').count();
|
||||
expect(invoiceRows).toBeGreaterThan(0);
|
||||
|
||||
if (invoiceRows > 0) {
|
||||
// Check first invoice for Oracle schema fields
|
||||
const firstInvoice = page.locator('[data-testid="invoice-row"]').first();
|
||||
|
||||
// These should match Oracle CONTAFIN schema
|
||||
await expect(firstInvoice.locator('[data-testid="numar-factura"]')).toBeVisible();
|
||||
await expect(firstInvoice.locator('[data-testid="data-scadenta"]')).toBeVisible();
|
||||
await expect(firstInvoice.locator('[data-testid="suma-totala"]')).toBeVisible();
|
||||
|
||||
console.log(`📊 Found ${invoiceRows} ROMFAST invoices with Oracle schema fields`);
|
||||
}
|
||||
|
||||
// Validate API performance
|
||||
assertPerformanceBaseline(
|
||||
apiResponseTime,
|
||||
PerformanceBaselines.apiResponse,
|
||||
'ROMFAST invoices API',
|
||||
expect
|
||||
);
|
||||
|
||||
// Check for no critical console errors
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log(`✅ ROMFAST invoice data structure validated (API: ${apiResponseTime}ms)`);
|
||||
});
|
||||
|
||||
test('should validate ROMFAST payment data integration', async ({ page }) => {
|
||||
console.log('💰 Testing ROMFAST payment data integration...');
|
||||
|
||||
// Select ROMFAST company
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Navigate to payments
|
||||
await page.click('[data-testid="nav-payments"]');
|
||||
await page.waitForURL('/payments');
|
||||
|
||||
// Measure payment data loading
|
||||
const loadStartTime = Date.now();
|
||||
await page.waitForSelector('[data-testid="payments-table"]', { timeout: 15000 });
|
||||
const loadTime = Date.now() - loadStartTime;
|
||||
|
||||
// Verify payment data structure
|
||||
const paymentRows = await page.locator('[data-testid="payment-row"]').count();
|
||||
console.log(`💳 Found ${paymentRows} ROMFAST payments`);
|
||||
|
||||
if (paymentRows > 0) {
|
||||
// Verify Oracle payment schema fields
|
||||
const firstPayment = page.locator('[data-testid="payment-row"]').first();
|
||||
|
||||
await expect(firstPayment.locator('[data-testid="numar-plata"]')).toBeVisible();
|
||||
await expect(firstPayment.locator('[data-testid="data-plata"]')).toBeVisible();
|
||||
await expect(firstPayment.locator('[data-testid="suma-plata"]')).toBeVisible();
|
||||
}
|
||||
|
||||
// Validate performance
|
||||
assertPerformanceBaseline(
|
||||
loadTime,
|
||||
PerformanceBaselines.reportGeneration,
|
||||
'ROMFAST payments loading',
|
||||
expect
|
||||
);
|
||||
|
||||
// Check console for errors
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log(`✅ ROMFAST payment data validated (Load: ${loadTime}ms)`);
|
||||
});
|
||||
|
||||
test('should handle ROMFAST data filtering and search', async ({ page }) => {
|
||||
console.log('🔍 Testing ROMFAST data filtering capabilities...');
|
||||
|
||||
// Select ROMFAST company
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Go to invoices for filtering test
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 });
|
||||
|
||||
// Get initial row count
|
||||
const initialCount = await page.locator('[data-testid="invoice-row"]').count();
|
||||
console.log(`📊 Initial ROMFAST invoices: ${initialCount}`);
|
||||
|
||||
if (initialCount > 0) {
|
||||
// Test date range filtering
|
||||
if (await page.locator('[data-testid="date-filter-from"]').isVisible()) {
|
||||
const filterStartTime = Date.now();
|
||||
|
||||
// Set date filter for last 30 days
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
await page.fill('[data-testid="date-filter-from"]', thirtyDaysAgo.toISOString().split('T')[0]);
|
||||
await page.click('[data-testid="apply-filter"]');
|
||||
|
||||
// Wait for filtered results
|
||||
await page.waitForTimeout(2000); // Allow for filtering
|
||||
|
||||
const filteredCount = await page.locator('[data-testid="invoice-row"]').count();
|
||||
const filterTime = Date.now() - filterStartTime;
|
||||
|
||||
console.log(`🗓️ Filtered to ${filteredCount} invoices in ${filterTime}ms`);
|
||||
|
||||
// Performance check for filtering
|
||||
expect(filterTime).toBeLessThan(3000); // Max 3s for filtering
|
||||
}
|
||||
|
||||
// Test search functionality
|
||||
if (await page.locator('[data-testid="search-input"]').isVisible()) {
|
||||
const searchStartTime = Date.now();
|
||||
|
||||
// Search for specific criteria
|
||||
await page.fill('[data-testid="search-input"]', 'ROMFAST');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await page.waitForTimeout(1500); // Allow for search
|
||||
|
||||
const searchResults = await page.locator('[data-testid="invoice-row"]').count();
|
||||
const searchTime = Date.now() - searchStartTime;
|
||||
|
||||
console.log(`🔎 Search returned ${searchResults} results in ${searchTime}ms`);
|
||||
|
||||
// Performance check for search
|
||||
expect(searchTime).toBeLessThan(2000); // Max 2s for search
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no critical errors during filtering operations
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log('✅ ROMFAST data filtering and search validated');
|
||||
});
|
||||
|
||||
test('should validate ROMFAST dashboard metrics accuracy', async ({ page }) => {
|
||||
console.log('📈 Testing ROMFAST dashboard metrics accuracy...');
|
||||
|
||||
// Select ROMFAST company
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
|
||||
// Wait for dashboard stats to load
|
||||
await page.waitForSelector('[data-testid="company-stats"]', { timeout: 15000 });
|
||||
|
||||
// Capture dashboard metrics
|
||||
const dashboardMetrics = await page.evaluate(() => {
|
||||
const getMetric = (selector) => {
|
||||
const element = document.querySelector(selector);
|
||||
return element ? element.textContent.trim() : null;
|
||||
};
|
||||
|
||||
return {
|
||||
totalInvoices: getMetric('[data-testid="total-invoices"]'),
|
||||
totalPayments: getMetric('[data-testid="total-payments"]'),
|
||||
pendingAmount: getMetric('[data-testid="pending-amount"]'),
|
||||
overdueCount: getMetric('[data-testid="overdue-count"]')
|
||||
};
|
||||
});
|
||||
|
||||
console.log('📊 ROMFAST Dashboard Metrics:', dashboardMetrics);
|
||||
|
||||
// Validate metrics are present and reasonable
|
||||
if (dashboardMetrics.totalInvoices) {
|
||||
const invoiceCount = parseInt(dashboardMetrics.totalInvoices.replace(/\D/g, ''));
|
||||
expect(invoiceCount).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
|
||||
if (dashboardMetrics.totalPayments) {
|
||||
const paymentCount = parseInt(dashboardMetrics.totalPayments.replace(/\D/g, ''));
|
||||
expect(paymentCount).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
|
||||
// Cross-validate with individual pages
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 10000 });
|
||||
|
||||
const actualInvoiceCount = await page.locator('[data-testid="invoice-row"]').count();
|
||||
console.log(`🔄 Cross-validation: Dashboard vs Invoices page (${actualInvoiceCount})`);
|
||||
|
||||
// Return to dashboard
|
||||
await page.click('[data-testid="nav-dashboard"]');
|
||||
|
||||
// Check for console errors during metric calculations
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log('✅ ROMFAST dashboard metrics accuracy validated');
|
||||
});
|
||||
|
||||
test('should measure ROMFAST data loading performance under load', async ({ page }) => {
|
||||
console.log('⚡ Testing ROMFAST data performance under simulated load...');
|
||||
|
||||
const performanceMetrics = [];
|
||||
|
||||
// Perform multiple data loading operations
|
||||
for (let i = 0; i < 3; i++) {
|
||||
console.log(`🔄 Performance test iteration ${i + 1}/3`);
|
||||
|
||||
const iterationStart = Date.now();
|
||||
|
||||
// Select company
|
||||
await selectCompany(page, REAL_CREDENTIALS.company);
|
||||
const companySelectTime = Date.now() - iterationStart;
|
||||
|
||||
// Load invoices
|
||||
const invoicesStart = Date.now();
|
||||
await page.click('[data-testid="nav-invoices"]');
|
||||
await page.waitForSelector('[data-testid="invoices-table"]', { timeout: 15000 });
|
||||
const invoicesLoadTime = Date.now() - invoicesStart;
|
||||
|
||||
// Load payments
|
||||
const paymentsStart = Date.now();
|
||||
await page.click('[data-testid="nav-payments"]');
|
||||
await page.waitForSelector('[data-testid="payments-table"]', { timeout: 15000 });
|
||||
const paymentsLoadTime = Date.now() - paymentsStart;
|
||||
|
||||
// Return to dashboard
|
||||
const dashboardStart = Date.now();
|
||||
await page.click('[data-testid="nav-dashboard"]');
|
||||
await page.waitForSelector('[data-testid="company-stats"]', { timeout: 10000 });
|
||||
const dashboardLoadTime = Date.now() - dashboardStart;
|
||||
|
||||
const totalIterationTime = Date.now() - iterationStart;
|
||||
|
||||
performanceMetrics.push({
|
||||
iteration: i + 1,
|
||||
companySelect: companySelectTime,
|
||||
invoicesLoad: invoicesLoadTime,
|
||||
paymentsLoad: paymentsLoadTime,
|
||||
dashboardLoad: dashboardLoadTime,
|
||||
total: totalIterationTime
|
||||
});
|
||||
|
||||
console.log(`📊 Iteration ${i + 1} - Total: ${totalIterationTime}ms`);
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
const averages = {
|
||||
companySelect: performanceMetrics.reduce((sum, m) => sum + m.companySelect, 0) / performanceMetrics.length,
|
||||
invoicesLoad: performanceMetrics.reduce((sum, m) => sum + m.invoicesLoad, 0) / performanceMetrics.length,
|
||||
paymentsLoad: performanceMetrics.reduce((sum, m) => sum + m.paymentsLoad, 0) / performanceMetrics.length,
|
||||
dashboardLoad: performanceMetrics.reduce((sum, m) => sum + m.dashboardLoad, 0) / performanceMetrics.length,
|
||||
total: performanceMetrics.reduce((sum, m) => sum + m.total, 0) / performanceMetrics.length
|
||||
};
|
||||
|
||||
console.log('📈 Average Performance Metrics:', averages);
|
||||
|
||||
// Validate against baselines
|
||||
assertPerformanceBaseline(averages.companySelect, PerformanceBaselines.dashboardLoad, 'Company selection', expect);
|
||||
assertPerformanceBaseline(averages.invoicesLoad, PerformanceBaselines.reportGeneration, 'Invoices loading', expect);
|
||||
assertPerformanceBaseline(averages.paymentsLoad, PerformanceBaselines.reportGeneration, 'Payments loading', expect);
|
||||
assertPerformanceBaseline(averages.dashboardLoad, PerformanceBaselines.dashboardLoad, 'Dashboard loading', expect);
|
||||
|
||||
// Total workflow should complete reasonably quickly
|
||||
expect(averages.total).toBeLessThan(15000); // Max 15s for full workflow
|
||||
|
||||
// Check for no critical errors across all iterations
|
||||
assertNoCriticalErrors(page, expect);
|
||||
|
||||
console.log('✅ ROMFAST performance under load validated');
|
||||
});
|
||||
});
|
||||
37
reports-app/frontend/tests/page-objects/BasePage.js
Normal file
37
reports-app/frontend/tests/page-objects/BasePage.js
Normal file
@@ -0,0 +1,37 @@
|
||||
export class BasePage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async waitForApiResponse(url, status = 200) {
|
||||
return await this.page.waitForResponse(response =>
|
||||
response.url().includes(url) && response.status() === status
|
||||
);
|
||||
}
|
||||
|
||||
async waitForLoadingToFinish() {
|
||||
// Wait for any loading spinners to disappear
|
||||
await this.page.waitForFunction(() => {
|
||||
const loadingElements = document.querySelectorAll('[data-testid="loading"], .p-progress-spinner');
|
||||
return loadingElements.length === 0;
|
||||
}, { timeout: 10000 });
|
||||
}
|
||||
|
||||
async checkErrorMessage(expectedMessage) {
|
||||
const errorElement = this.page.locator('.p-message-error, [data-testid="error"]');
|
||||
await errorElement.waitFor({ state: 'visible', timeout: 5000 });
|
||||
const actualMessage = await errorElement.textContent();
|
||||
return actualMessage.includes(expectedMessage);
|
||||
}
|
||||
|
||||
async checkSuccessMessage(expectedMessage) {
|
||||
const successElement = this.page.locator('.p-message-success, [data-testid="success"]');
|
||||
await successElement.waitFor({ state: 'visible', timeout: 5000 });
|
||||
const actualMessage = await successElement.textContent();
|
||||
return actualMessage.includes(expectedMessage);
|
||||
}
|
||||
|
||||
async waitForNavigation() {
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
}
|
||||
}
|
||||
129
reports-app/frontend/tests/page-objects/DashboardPage.js
Normal file
129
reports-app/frontend/tests/page-objects/DashboardPage.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { BasePage } from './BasePage.js';
|
||||
|
||||
export class DashboardPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
// Header selectors
|
||||
this.pageTitle = '.page-title';
|
||||
this.pageSubtitle = '.page-subtitle';
|
||||
this.userWelcome = '.page-subtitle';
|
||||
|
||||
// Company selection selectors
|
||||
this.companySelectionCard = '.company-selection-card';
|
||||
this.companyDropdown = '.company-selection .p-dropdown';
|
||||
this.companyDropdownTrigger = '.company-selection .p-dropdown-trigger';
|
||||
this.companyOptions = '.p-dropdown-item';
|
||||
|
||||
// Stats cards selectors
|
||||
this.statsGrid = '.stats-grid';
|
||||
this.invoicesStatCard = '.stat-card.stat-invoices';
|
||||
this.paymentsStatCard = '.stat-card.stat-payments';
|
||||
this.companyStatCard = '.stat-card.stat-company';
|
||||
|
||||
// Stat values
|
||||
this.invoicesTotal = '.stat-invoices .stat-value';
|
||||
this.paymentsTotal = '.stat-payments .stat-value';
|
||||
this.companyName = '.stat-company .stat-value';
|
||||
|
||||
// Quick actions
|
||||
this.quickActionsCard = '.quick-actions-card';
|
||||
this.invoicesActionButton = 'button:has-text("Facturi")';
|
||||
this.paymentsActionButton = 'button:has-text("Încasări")';
|
||||
|
||||
// Navigation
|
||||
this.dashboardContent = '.dashboard-content';
|
||||
}
|
||||
|
||||
async navigate() {
|
||||
await this.page.goto('/dashboard');
|
||||
await this.page.waitForSelector(this.pageTitle);
|
||||
}
|
||||
|
||||
async isOnDashboardPage() {
|
||||
return await this.page.locator(this.pageTitle).isVisible();
|
||||
}
|
||||
|
||||
async getPageTitle() {
|
||||
return await this.page.locator(this.pageTitle).textContent();
|
||||
}
|
||||
|
||||
async getWelcomeMessage() {
|
||||
return await this.page.locator(this.userWelcome).textContent();
|
||||
}
|
||||
|
||||
async isCompanySelectionVisible() {
|
||||
return await this.page.locator(this.companySelectionCard).isVisible();
|
||||
}
|
||||
|
||||
async isDashboardContentVisible() {
|
||||
return await this.page.locator(this.dashboardContent).isVisible();
|
||||
}
|
||||
|
||||
async selectCompany(companyName) {
|
||||
// Click dropdown to open options
|
||||
await this.page.click(this.companyDropdownTrigger);
|
||||
|
||||
// Wait for options to appear and select the company
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
await this.page.click(`${this.companyOptions}:has-text("${companyName}")`);
|
||||
|
||||
// Wait for selection to be processed
|
||||
await this.waitForLoadingToFinish();
|
||||
}
|
||||
|
||||
async getSelectedCompanyName() {
|
||||
if (await this.page.locator(this.companyName).isVisible()) {
|
||||
return await this.page.locator(this.companyName).textContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getInvoicesCount() {
|
||||
if (await this.page.locator(this.invoicesTotal).isVisible()) {
|
||||
return await this.page.locator(this.invoicesTotal).textContent();
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
async getPaymentsCount() {
|
||||
if (await this.page.locator(this.paymentsTotal).isVisible()) {
|
||||
return await this.page.locator(this.paymentsTotal).textContent();
|
||||
}
|
||||
return '0';
|
||||
}
|
||||
|
||||
async clickInvoicesAction() {
|
||||
await this.page.click(this.invoicesActionButton);
|
||||
await this.waitForNavigation();
|
||||
}
|
||||
|
||||
async clickPaymentsAction() {
|
||||
await this.page.click(this.paymentsActionButton);
|
||||
await this.waitForNavigation();
|
||||
}
|
||||
|
||||
async areStatsCardsVisible() {
|
||||
const invoicesVisible = await this.page.locator(this.invoicesStatCard).isVisible();
|
||||
const paymentsVisible = await this.page.locator(this.paymentsStatCard).isVisible();
|
||||
const companyVisible = await this.page.locator(this.companyStatCard).isVisible();
|
||||
|
||||
return invoicesVisible && paymentsVisible && companyVisible;
|
||||
}
|
||||
|
||||
async getStatsData() {
|
||||
return {
|
||||
invoices: await this.getInvoicesCount(),
|
||||
payments: await this.getPaymentsCount(),
|
||||
company: await this.getSelectedCompanyName()
|
||||
};
|
||||
}
|
||||
|
||||
async waitForDashboardLoad() {
|
||||
// Wait for either company selection or dashboard content to appear
|
||||
await Promise.race([
|
||||
this.page.waitForSelector(this.companySelectionCard, { timeout: 10000 }),
|
||||
this.page.waitForSelector(this.dashboardContent, { timeout: 10000 })
|
||||
]);
|
||||
}
|
||||
}
|
||||
195
reports-app/frontend/tests/page-objects/InvoicesPage.js
Normal file
195
reports-app/frontend/tests/page-objects/InvoicesPage.js
Normal file
@@ -0,0 +1,195 @@
|
||||
import { BasePage } from './BasePage.js';
|
||||
|
||||
export class InvoicesPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
// Page selectors
|
||||
this.pageTitle = '.page-title';
|
||||
this.pageSubtitle = '.page-subtitle';
|
||||
|
||||
// Company selection
|
||||
this.companySelectionCard = '.company-selection-card';
|
||||
this.companyDropdown = '.company-selection .p-dropdown';
|
||||
this.companyDropdownTrigger = '.company-selection .p-dropdown-trigger';
|
||||
this.companyOptions = '.p-dropdown-item';
|
||||
|
||||
// Search and filters
|
||||
this.searchInput = '.search-input input';
|
||||
this.statusFilter = '.status-filter .p-dropdown';
|
||||
this.statusFilterTrigger = '.status-filter .p-dropdown-trigger';
|
||||
this.refreshButton = '.refresh-button';
|
||||
this.exportButton = '.export-button';
|
||||
|
||||
// Table selectors
|
||||
this.invoicesTable = '.invoices-table';
|
||||
this.tableRows = '.invoices-table tbody tr';
|
||||
this.tableHeaders = '.invoices-table thead th';
|
||||
this.loadingSpinner = '.p-datatable-loading';
|
||||
|
||||
// Pagination
|
||||
this.pagination = '.p-paginator';
|
||||
this.nextPageButton = '.p-paginator-next';
|
||||
this.prevPageButton = '.p-paginator-prev';
|
||||
this.currentPageSpan = '.p-paginator-current';
|
||||
|
||||
// Invoice details
|
||||
this.invoiceDetailsModal = '.invoice-details-modal';
|
||||
this.invoiceDetailsPanel = '.invoice-details-panel';
|
||||
|
||||
// Specific table columns (adjust based on actual implementation)
|
||||
this.numberColumn = 'td:nth-child(1)';
|
||||
this.dateColumn = 'td:nth-child(2)';
|
||||
this.clientColumn = 'td:nth-child(3)';
|
||||
this.amountColumn = 'td:nth-child(4)';
|
||||
this.statusColumn = 'td:nth-child(5)';
|
||||
}
|
||||
|
||||
async navigate() {
|
||||
await this.page.goto('/invoices');
|
||||
await this.page.waitForSelector(this.pageTitle);
|
||||
}
|
||||
|
||||
async isOnInvoicesPage() {
|
||||
return await this.page.locator(this.pageTitle).isVisible();
|
||||
}
|
||||
|
||||
async getPageTitle() {
|
||||
return await this.page.locator(this.pageTitle).textContent();
|
||||
}
|
||||
|
||||
async isCompanySelectionVisible() {
|
||||
return await this.page.locator(this.companySelectionCard).isVisible();
|
||||
}
|
||||
|
||||
async isInvoicesTableVisible() {
|
||||
return await this.page.locator(this.invoicesTable).isVisible();
|
||||
}
|
||||
|
||||
async selectCompany(companyName) {
|
||||
await this.page.click(this.companyDropdownTrigger);
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
await this.page.click(`${this.companyOptions}:has-text("${companyName}")`);
|
||||
await this.waitForLoadingToFinish();
|
||||
}
|
||||
|
||||
async searchInvoices(searchTerm) {
|
||||
await this.page.fill(this.searchInput, searchTerm);
|
||||
await this.page.press(this.searchInput, 'Enter');
|
||||
}
|
||||
|
||||
async filterByStatus(status) {
|
||||
await this.page.click(this.statusFilterTrigger);
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
|
||||
// Map status to Romanian text (adjust based on actual implementation)
|
||||
const statusMap = {
|
||||
'paid': 'Plătit',
|
||||
'unpaid': 'Neplătit',
|
||||
'overdue': 'Întârziat'
|
||||
};
|
||||
|
||||
const statusText = statusMap[status] || status;
|
||||
await this.page.click(`${this.companyOptions}:has-text("${statusText}")`);
|
||||
}
|
||||
|
||||
async sortByColumn(columnName) {
|
||||
// Map column names to actual header text
|
||||
const columnMap = {
|
||||
'number': 'Număr',
|
||||
'date': 'Data',
|
||||
'client': 'Client',
|
||||
'amount': 'Sumă',
|
||||
'status': 'Status'
|
||||
};
|
||||
|
||||
const headerText = columnMap[columnName] || columnName;
|
||||
await this.page.click(`${this.tableHeaders}:has-text("${headerText}")`);
|
||||
}
|
||||
|
||||
async getVisibleInvoicesCount() {
|
||||
return await this.page.locator(this.tableRows).count();
|
||||
}
|
||||
|
||||
async getFirstInvoiceData() {
|
||||
const firstRow = this.page.locator(this.tableRows).first();
|
||||
|
||||
return {
|
||||
number: await firstRow.locator(this.numberColumn).textContent(),
|
||||
date: await firstRow.locator(this.dateColumn).textContent(),
|
||||
client: await firstRow.locator(this.clientColumn).textContent(),
|
||||
amount: await firstRow.locator(this.amountColumn).textContent(),
|
||||
status: await firstRow.locator(this.statusColumn).textContent()
|
||||
};
|
||||
}
|
||||
|
||||
async clickFirstInvoiceRow() {
|
||||
await this.page.locator(this.tableRows).first().click();
|
||||
}
|
||||
|
||||
async isInvoiceDetailsVisible() {
|
||||
const modalVisible = await this.page.locator(this.invoiceDetailsModal).isVisible();
|
||||
const panelVisible = await this.page.locator(this.invoiceDetailsPanel).isVisible();
|
||||
return modalVisible || panelVisible;
|
||||
}
|
||||
|
||||
async clickExportButton() {
|
||||
await this.page.click(this.exportButton);
|
||||
}
|
||||
|
||||
async clickRefreshButton() {
|
||||
await this.page.click(this.refreshButton);
|
||||
}
|
||||
|
||||
async isPaginationVisible() {
|
||||
return await this.page.locator(this.pagination).isVisible();
|
||||
}
|
||||
|
||||
async goToNextPage() {
|
||||
await this.page.click(this.nextPageButton);
|
||||
}
|
||||
|
||||
async goToPrevPage() {
|
||||
await this.page.click(this.prevPageButton);
|
||||
}
|
||||
|
||||
async getCurrentPage() {
|
||||
const pageText = await this.page.locator(this.currentPageSpan).textContent();
|
||||
// Extract page number from text like "Page 2 of 5"
|
||||
const match = pageText.match(/(\d+)/);
|
||||
return match ? parseInt(match[1]) : 1;
|
||||
}
|
||||
|
||||
async waitForPageLoad() {
|
||||
await Promise.race([
|
||||
this.page.waitForSelector(this.companySelectionCard, { timeout: 10000 }),
|
||||
this.page.waitForSelector(this.invoicesTable, { timeout: 10000 })
|
||||
]);
|
||||
}
|
||||
|
||||
async waitForTableLoad() {
|
||||
await this.page.waitForSelector(this.invoicesTable, { timeout: 10000 });
|
||||
await this.waitForLoadingToFinish();
|
||||
}
|
||||
|
||||
async getInvoiceByNumber(invoiceNumber) {
|
||||
const rows = this.page.locator(this.tableRows);
|
||||
const rowCount = await rows.count();
|
||||
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const row = rows.nth(i);
|
||||
const number = await row.locator(this.numberColumn).textContent();
|
||||
if (number.trim() === invoiceNumber) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async clickInvoiceByNumber(invoiceNumber) {
|
||||
const row = await this.getInvoiceByNumber(invoiceNumber);
|
||||
if (row) {
|
||||
await row.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
99
reports-app/frontend/tests/page-objects/LoginPage.js
Normal file
99
reports-app/frontend/tests/page-objects/LoginPage.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { BasePage } from './BasePage.js';
|
||||
|
||||
export class LoginPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
// Selectors
|
||||
this.usernameInput = '#username';
|
||||
this.passwordInput = '#password input';
|
||||
this.loginButton = 'button[type="submit"]';
|
||||
this.errorMessage = '.error-message';
|
||||
this.loadingSpinner = '.p-button-loading';
|
||||
this.loginTitle = '.login-title';
|
||||
this.loginCard = '.login-card';
|
||||
|
||||
// Form validation selectors
|
||||
this.usernameError = '.field:has(#username) .p-error';
|
||||
this.passwordError = '.field:has(#password) .p-error';
|
||||
this.invalidField = '.p-invalid';
|
||||
}
|
||||
|
||||
async navigate() {
|
||||
await this.page.goto('/');
|
||||
await this.page.waitForSelector(this.loginCard);
|
||||
}
|
||||
|
||||
async fillCredentials(username, password) {
|
||||
await this.page.fill(this.usernameInput, username);
|
||||
await this.page.fill(this.passwordInput, password);
|
||||
}
|
||||
|
||||
async clickLogin() {
|
||||
await this.page.click(this.loginButton);
|
||||
}
|
||||
|
||||
async login(username, password) {
|
||||
await this.fillCredentials(username, password);
|
||||
await this.clickLogin();
|
||||
}
|
||||
|
||||
async waitForLoginResult() {
|
||||
// Wait for either redirect to dashboard or error message
|
||||
try {
|
||||
await Promise.race([
|
||||
this.page.waitForURL('/dashboard', { timeout: 5000 }),
|
||||
this.page.waitForSelector(this.errorMessage, { timeout: 5000 })
|
||||
]);
|
||||
} catch (error) {
|
||||
// Continue - we'll check the state separately
|
||||
}
|
||||
}
|
||||
|
||||
async isOnLoginPage() {
|
||||
return await this.page.locator(this.loginTitle).isVisible();
|
||||
}
|
||||
|
||||
async isLoginButtonDisabled() {
|
||||
return await this.page.locator(this.loginButton).isDisabled();
|
||||
}
|
||||
|
||||
async isLoading() {
|
||||
return await this.page.locator(this.loadingSpinner).isVisible();
|
||||
}
|
||||
|
||||
async getErrorMessage() {
|
||||
const errorElement = this.page.locator(this.errorMessage);
|
||||
if (await errorElement.isVisible()) {
|
||||
return await errorElement.textContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async getFieldError(field) {
|
||||
const selector = field === 'username' ? this.usernameError : this.passwordError;
|
||||
const errorElement = this.page.locator(selector);
|
||||
if (await errorElement.isVisible()) {
|
||||
return await errorElement.textContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async hasInvalidField() {
|
||||
return await this.page.locator(this.invalidField).count() > 0;
|
||||
}
|
||||
|
||||
async clearForm() {
|
||||
await this.page.fill(this.usernameInput, '');
|
||||
await this.page.fill(this.passwordInput, '');
|
||||
}
|
||||
|
||||
async validateFormFields() {
|
||||
// Trigger validation by clicking outside fields
|
||||
await this.page.click(this.loginCard);
|
||||
}
|
||||
|
||||
async getPageTitle() {
|
||||
return await this.page.locator(this.loginTitle).textContent();
|
||||
}
|
||||
}
|
||||
271
reports-app/frontend/tests/page-objects/PaymentsPage.js
Normal file
271
reports-app/frontend/tests/page-objects/PaymentsPage.js
Normal file
@@ -0,0 +1,271 @@
|
||||
import { BasePage } from './BasePage.js';
|
||||
|
||||
export class PaymentsPage extends BasePage {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
|
||||
// Page selectors
|
||||
this.pageTitle = '.page-title';
|
||||
this.pageSubtitle = '.page-subtitle';
|
||||
|
||||
// Company selection
|
||||
this.companySelectionCard = '.company-selection-card';
|
||||
this.companyDropdown = '.company-selection .p-dropdown';
|
||||
this.companyDropdownTrigger = '.company-selection .p-dropdown-trigger';
|
||||
this.companyOptions = '.p-dropdown-item';
|
||||
|
||||
// Search and filters
|
||||
this.searchInput = '.search-input input';
|
||||
this.methodFilter = '.method-filter .p-dropdown';
|
||||
this.methodFilterTrigger = '.method-filter .p-dropdown-trigger';
|
||||
this.dateRangeFilter = '.date-range-filter .p-dropdown';
|
||||
this.dateRangeFilterTrigger = '.date-range-filter .p-dropdown-trigger';
|
||||
this.refreshButton = '.refresh-button';
|
||||
this.exportButton = '.export-button';
|
||||
|
||||
// View toggles
|
||||
this.tableViewButton = '.table-view-button';
|
||||
this.summaryViewButton = '.summary-view-button';
|
||||
|
||||
// Table selectors
|
||||
this.paymentsTable = '.payments-table';
|
||||
this.tableRows = '.payments-table tbody tr';
|
||||
this.tableHeaders = '.payments-table thead th';
|
||||
this.loadingSpinner = '.p-datatable-loading';
|
||||
|
||||
// Summary view
|
||||
this.summaryView = '.payments-summary-view';
|
||||
this.methodSummaryCards = '.method-summary-card';
|
||||
|
||||
// Totals card
|
||||
this.totalsCard = '.payments-totals-card';
|
||||
this.totalAmount = '.total-amount .amount-value';
|
||||
this.totalCount = '.total-count .count-value';
|
||||
|
||||
// Pagination
|
||||
this.pagination = '.p-paginator';
|
||||
this.nextPageButton = '.p-paginator-next';
|
||||
this.prevPageButton = '.p-paginator-prev';
|
||||
this.currentPageSpan = '.p-paginator-current';
|
||||
|
||||
// Payment details
|
||||
this.paymentDetailsModal = '.payment-details-modal';
|
||||
this.paymentDetailsPanel = '.payment-details-panel';
|
||||
|
||||
// Specific table columns (adjust based on actual implementation)
|
||||
this.referenceColumn = 'td:nth-child(1)';
|
||||
this.dateColumn = 'td:nth-child(2)';
|
||||
this.clientColumn = 'td:nth-child(3)';
|
||||
this.amountColumn = 'td:nth-child(4)';
|
||||
this.methodColumn = 'td:nth-child(5)';
|
||||
}
|
||||
|
||||
async navigate() {
|
||||
await this.page.goto('/payments');
|
||||
await this.page.waitForSelector(this.pageTitle);
|
||||
}
|
||||
|
||||
async isOnPaymentsPage() {
|
||||
return await this.page.locator(this.pageTitle).isVisible();
|
||||
}
|
||||
|
||||
async getPageTitle() {
|
||||
return await this.page.locator(this.pageTitle).textContent();
|
||||
}
|
||||
|
||||
async isCompanySelectionVisible() {
|
||||
return await this.page.locator(this.companySelectionCard).isVisible();
|
||||
}
|
||||
|
||||
async isPaymentsTableVisible() {
|
||||
return await this.page.locator(this.paymentsTable).isVisible();
|
||||
}
|
||||
|
||||
async selectCompany(companyName) {
|
||||
await this.page.click(this.companyDropdownTrigger);
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
await this.page.click(`${this.companyOptions}:has-text("${companyName}")`);
|
||||
await this.waitForLoadingToFinish();
|
||||
}
|
||||
|
||||
async searchPayments(searchTerm) {
|
||||
await this.page.fill(this.searchInput, searchTerm);
|
||||
await this.page.press(this.searchInput, 'Enter');
|
||||
}
|
||||
|
||||
async filterByMethod(method) {
|
||||
await this.page.click(this.methodFilterTrigger);
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
|
||||
// Map method to Romanian text (adjust based on actual implementation)
|
||||
const methodMap = {
|
||||
'bank_transfer': 'Transfer bancar',
|
||||
'cash': 'Numerar',
|
||||
'card': 'Card',
|
||||
'check': 'Cec'
|
||||
};
|
||||
|
||||
const methodText = methodMap[method] || method;
|
||||
await this.page.click(`${this.companyOptions}:has-text("${methodText}")`);
|
||||
}
|
||||
|
||||
async filterByDateRange(range) {
|
||||
await this.page.click(this.dateRangeFilterTrigger);
|
||||
await this.page.waitForSelector(this.companyOptions);
|
||||
|
||||
// Map range to Romanian text
|
||||
const rangeMap = {
|
||||
'thisMonth': 'Această lună',
|
||||
'lastMonth': 'Luna trecută',
|
||||
'thisYear': 'Acest an',
|
||||
'custom': 'Personalizat'
|
||||
};
|
||||
|
||||
const rangeText = rangeMap[range] || range;
|
||||
await this.page.click(`${this.companyOptions}:has-text("${rangeText}")`);
|
||||
}
|
||||
|
||||
async sortByColumn(columnName) {
|
||||
// Map column names to actual header text
|
||||
const columnMap = {
|
||||
'reference': 'Referință',
|
||||
'date': 'Data',
|
||||
'client': 'Client',
|
||||
'amount': 'Sumă',
|
||||
'method': 'Metodă'
|
||||
};
|
||||
|
||||
const headerText = columnMap[columnName] || columnName;
|
||||
await this.page.click(`${this.tableHeaders}:has-text("${headerText}")`);
|
||||
}
|
||||
|
||||
async getVisiblePaymentsCount() {
|
||||
return await this.page.locator(this.tableRows).count();
|
||||
}
|
||||
|
||||
async getFirstPaymentData() {
|
||||
const firstRow = this.page.locator(this.tableRows).first();
|
||||
|
||||
return {
|
||||
reference: await firstRow.locator(this.referenceColumn).textContent(),
|
||||
date: await firstRow.locator(this.dateColumn).textContent(),
|
||||
client: await firstRow.locator(this.clientColumn).textContent(),
|
||||
amount: await firstRow.locator(this.amountColumn).textContent(),
|
||||
method: await firstRow.locator(this.methodColumn).textContent()
|
||||
};
|
||||
}
|
||||
|
||||
async clickFirstPaymentRow() {
|
||||
await this.page.locator(this.tableRows).first().click();
|
||||
}
|
||||
|
||||
async isPaymentDetailsVisible() {
|
||||
const modalVisible = await this.page.locator(this.paymentDetailsModal).isVisible();
|
||||
const panelVisible = await this.page.locator(this.paymentDetailsPanel).isVisible();
|
||||
return modalVisible || panelVisible;
|
||||
}
|
||||
|
||||
async clickExportButton() {
|
||||
await this.page.click(this.exportButton);
|
||||
}
|
||||
|
||||
async clickRefreshButton() {
|
||||
await this.page.click(this.refreshButton);
|
||||
}
|
||||
|
||||
async isTotalsCardVisible() {
|
||||
return await this.page.locator(this.totalsCard).isVisible();
|
||||
}
|
||||
|
||||
async getTotalsData() {
|
||||
return {
|
||||
totalAmount: await this.page.locator(this.totalAmount).textContent(),
|
||||
totalCount: await this.page.locator(this.totalCount).textContent()
|
||||
};
|
||||
}
|
||||
|
||||
async isSummaryViewAvailable() {
|
||||
return await this.page.locator(this.summaryViewButton).isVisible();
|
||||
}
|
||||
|
||||
async switchToSummaryView() {
|
||||
await this.page.click(this.summaryViewButton);
|
||||
}
|
||||
|
||||
async switchToTableView() {
|
||||
await this.page.click(this.tableViewButton);
|
||||
}
|
||||
|
||||
async isSummaryViewVisible() {
|
||||
return await this.page.locator(this.summaryView).isVisible();
|
||||
}
|
||||
|
||||
async isPaginationVisible() {
|
||||
return await this.page.locator(this.pagination).isVisible();
|
||||
}
|
||||
|
||||
async goToNextPage() {
|
||||
await this.page.click(this.nextPageButton);
|
||||
}
|
||||
|
||||
async goToPrevPage() {
|
||||
await this.page.click(this.prevPageButton);
|
||||
}
|
||||
|
||||
async getCurrentPage() {
|
||||
const pageText = await this.page.locator(this.currentPageSpan).textContent();
|
||||
// Extract page number from text like "Page 2 of 5"
|
||||
const match = pageText.match(/(\d+)/);
|
||||
return match ? parseInt(match[1]) : 1;
|
||||
}
|
||||
|
||||
async waitForPageLoad() {
|
||||
await Promise.race([
|
||||
this.page.waitForSelector(this.companySelectionCard, { timeout: 10000 }),
|
||||
this.page.waitForSelector(this.paymentsTable, { timeout: 10000 })
|
||||
]);
|
||||
}
|
||||
|
||||
async waitForTableLoad() {
|
||||
await this.page.waitForSelector(this.paymentsTable, { timeout: 10000 });
|
||||
await this.waitForLoadingToFinish();
|
||||
}
|
||||
|
||||
async getPaymentByReference(reference) {
|
||||
const rows = this.page.locator(this.tableRows);
|
||||
const rowCount = await rows.count();
|
||||
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
const row = rows.nth(i);
|
||||
const ref = await row.locator(this.referenceColumn).textContent();
|
||||
if (ref.trim() === reference) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async clickPaymentByReference(reference) {
|
||||
const row = await this.getPaymentByReference(reference);
|
||||
if (row) {
|
||||
await row.click();
|
||||
}
|
||||
}
|
||||
|
||||
async getMethodSummaryData() {
|
||||
const summaryCards = this.page.locator(this.methodSummaryCards);
|
||||
const cardCount = await summaryCards.count();
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
const card = summaryCards.nth(i);
|
||||
const method = await card.locator('.method-name').textContent();
|
||||
const amount = await card.locator('.method-amount').textContent();
|
||||
const count = await card.locator('.method-count').textContent();
|
||||
|
||||
data.push({ method, amount, count });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
317
reports-app/frontend/tests/utils/console-monitor.js
Normal file
317
reports-app/frontend/tests/utils/console-monitor.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Console Error Monitoring Infrastructure for ROA2WEB Testing
|
||||
*
|
||||
* Provides comprehensive console error tracking, classification, and performance monitoring
|
||||
* for Playwright tests with real Oracle data integration.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error classification system for console messages
|
||||
*/
|
||||
export const ErrorClassifier = {
|
||||
CRITICAL: [
|
||||
'Authentication failed',
|
||||
'Database connection',
|
||||
'Uncaught TypeError',
|
||||
'Uncaught ReferenceError',
|
||||
'Oracle connection error',
|
||||
'SSH tunnel failed',
|
||||
'Failed to authenticate',
|
||||
'Cannot read property',
|
||||
'Cannot access before initialization'
|
||||
],
|
||||
WARNING: [
|
||||
'404 Not Found',
|
||||
'Failed to fetch',
|
||||
'Network request failed',
|
||||
'Component warning',
|
||||
'Vue warn',
|
||||
'Resource loading error',
|
||||
'Timeout exceeded',
|
||||
'Connection refused'
|
||||
],
|
||||
INFO: [
|
||||
'Development build',
|
||||
'Vue devtools',
|
||||
'[HMR]',
|
||||
'Hot reload',
|
||||
'DevTools',
|
||||
'webpack',
|
||||
'vite'
|
||||
],
|
||||
|
||||
/**
|
||||
* Classify a console message based on its content
|
||||
* @param {Object} message - Console message object
|
||||
* @returns {string} Classification level
|
||||
*/
|
||||
classify(message) {
|
||||
const text = message.text || message.error || '';
|
||||
if (this.CRITICAL.some(pattern => text.includes(pattern))) return 'CRITICAL';
|
||||
if (this.WARNING.some(pattern => text.includes(pattern))) return 'WARNING';
|
||||
if (this.INFO.some(pattern => text.includes(pattern))) return 'INFO';
|
||||
return 'UNKNOWN';
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if message should be ignored in tests
|
||||
* @param {Object} message - Console message object
|
||||
* @returns {boolean} True if message should be ignored
|
||||
*/
|
||||
shouldIgnore(message) {
|
||||
const ignoredPatterns = [
|
||||
'DevTools listening',
|
||||
'Debugging information',
|
||||
'Chrome extension',
|
||||
'webpack-dev-server',
|
||||
'Live reload enabled'
|
||||
];
|
||||
|
||||
const text = message.text || message.error || '';
|
||||
return ignoredPatterns.some(pattern => text.includes(pattern));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performance monitoring utilities
|
||||
*/
|
||||
export const PerformanceMonitor = {
|
||||
/**
|
||||
* Measure page load performance metrics
|
||||
* @param {Page} page - Playwright page object
|
||||
* @returns {Object} Performance metrics
|
||||
*/
|
||||
async measurePageLoad(page) {
|
||||
return await page.evaluate(() => {
|
||||
const timing = performance.timing;
|
||||
const navigation = performance.getEntriesByType('navigation')[0];
|
||||
|
||||
return {
|
||||
domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
|
||||
loadComplete: timing.loadEventEnd - timing.navigationStart,
|
||||
firstPaint: performance.getEntriesByType('paint').find(p => p.name === 'first-paint')?.startTime || 0,
|
||||
firstContentfulPaint: performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint')?.startTime || 0,
|
||||
timeToInteractive: navigation?.domInteractive - navigation?.fetchStart || 0
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Measure API response time
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} apiPattern - API endpoint pattern to monitor
|
||||
* @returns {Promise<number>} Response time in milliseconds
|
||||
*/
|
||||
async measureApiResponse(page, apiPattern) {
|
||||
const startTime = Date.now();
|
||||
await page.waitForResponse(response => response.url().includes(apiPattern), { timeout: 10000 });
|
||||
return Date.now() - startTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Monitor network performance during test execution
|
||||
* @param {Page} page - Playwright page object
|
||||
* @returns {Object} Network performance data
|
||||
*/
|
||||
async getNetworkMetrics(page) {
|
||||
const resourceTiming = await page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map(entry => ({
|
||||
name: entry.name,
|
||||
duration: entry.duration,
|
||||
transferSize: entry.transferSize,
|
||||
type: entry.initiatorType
|
||||
}));
|
||||
});
|
||||
|
||||
const slowResources = resourceTiming
|
||||
.filter(resource => resource.duration > 1000)
|
||||
.sort((a, b) => b.duration - a.duration);
|
||||
|
||||
return {
|
||||
totalResources: resourceTiming.length,
|
||||
slowResources: slowResources.slice(0, 5),
|
||||
averageResponseTime: resourceTiming.reduce((sum, r) => sum + r.duration, 0) / resourceTiming.length
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Console monitoring setup function for test beforeEach hooks
|
||||
* @param {Page} page - Playwright page object
|
||||
* @returns {Object} Monitoring data collectors
|
||||
*/
|
||||
export function setupConsoleCapture(page) {
|
||||
const consoleMessages = [];
|
||||
const networkErrors = [];
|
||||
const performanceMetrics = {
|
||||
startTime: Date.now(),
|
||||
apiCalls: []
|
||||
};
|
||||
|
||||
// Capture console messages
|
||||
page.on('console', msg => {
|
||||
const message = {
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: new Date().toISOString(),
|
||||
args: msg.args()
|
||||
};
|
||||
|
||||
if (!ErrorClassifier.shouldIgnore(message)) {
|
||||
consoleMessages.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
// Capture JavaScript errors
|
||||
page.on('pageerror', error => {
|
||||
const errorMessage = {
|
||||
type: 'pageerror',
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
consoleMessages.push(errorMessage);
|
||||
});
|
||||
|
||||
// Capture network failures
|
||||
page.on('requestfailed', request => {
|
||||
const networkError = {
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
failure: request.failure(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
networkErrors.push(networkError);
|
||||
});
|
||||
|
||||
// Monitor API responses
|
||||
page.on('response', response => {
|
||||
if (response.url().includes('/api/')) {
|
||||
performanceMetrics.apiCalls.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
timing: Date.now() - performanceMetrics.startTime
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Store collectors on page object for test access
|
||||
page.consoleMessages = consoleMessages;
|
||||
page.networkErrors = networkErrors;
|
||||
page.performanceMetrics = performanceMetrics;
|
||||
|
||||
return { consoleMessages, networkErrors, performanceMetrics };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comprehensive error report
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} testName - Name of the test
|
||||
* @returns {Object} Error report
|
||||
*/
|
||||
export function generateErrorReport(page, testName) {
|
||||
const consoleMessages = page.consoleMessages || [];
|
||||
const networkErrors = page.networkErrors || [];
|
||||
|
||||
// Classify console messages
|
||||
const classified = {
|
||||
critical: consoleMessages.filter(msg => ErrorClassifier.classify(msg) === 'CRITICAL'),
|
||||
warning: consoleMessages.filter(msg => ErrorClassifier.classify(msg) === 'WARNING'),
|
||||
info: consoleMessages.filter(msg => ErrorClassifier.classify(msg) === 'INFO'),
|
||||
unknown: consoleMessages.filter(msg => ErrorClassifier.classify(msg) === 'UNKNOWN')
|
||||
};
|
||||
|
||||
// Find error patterns
|
||||
const errorPatterns = {};
|
||||
consoleMessages.forEach(msg => {
|
||||
if (msg.type === 'error') {
|
||||
const pattern = findErrorPattern(msg.text);
|
||||
errorPatterns[pattern] = (errorPatterns[pattern] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
testName,
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
totalConsoleMessages: consoleMessages.length,
|
||||
totalNetworkErrors: networkErrors.length,
|
||||
classifications: {
|
||||
critical: classified.critical.length,
|
||||
warning: classified.warning.length,
|
||||
info: classified.info.length,
|
||||
unknown: classified.unknown.length
|
||||
}
|
||||
},
|
||||
details: {
|
||||
criticalErrors: classified.critical,
|
||||
warnings: classified.warning,
|
||||
networkErrors,
|
||||
errorPatterns
|
||||
},
|
||||
performance: page.performanceMetrics
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find common error patterns in console messages
|
||||
* @param {string} errorText - Error message text
|
||||
* @returns {string} Error pattern category
|
||||
*/
|
||||
function findErrorPattern(errorText) {
|
||||
const patterns = [
|
||||
{ pattern: /Failed to fetch/, category: 'Network Error' },
|
||||
{ pattern: /404.*not found/i, category: '404 Error' },
|
||||
{ pattern: /Uncaught TypeError/, category: 'JavaScript TypeError' },
|
||||
{ pattern: /Vue warn/, category: 'Vue Warning' },
|
||||
{ pattern: /Component.*not found/, category: 'Component Error' },
|
||||
{ pattern: /Oracle.*connection/, category: 'Database Error' },
|
||||
{ pattern: /Authentication.*failed/, category: 'Auth Error' }
|
||||
];
|
||||
|
||||
const match = patterns.find(p => p.pattern.test(errorText));
|
||||
return match ? match.category : 'Unknown Error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert no critical console errors in test
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {Object} expect - Playwright expect object
|
||||
*/
|
||||
export function assertNoCriticalErrors(page, expect) {
|
||||
const consoleMessages = page.consoleMessages || [];
|
||||
const criticalErrors = consoleMessages.filter(msg =>
|
||||
ErrorClassifier.classify(msg) === 'CRITICAL'
|
||||
);
|
||||
|
||||
if (criticalErrors.length > 0) {
|
||||
const errorDetails = criticalErrors.map(err =>
|
||||
`${err.type}: ${err.text || err.error} at ${err.location?.url || 'unknown'}:${err.location?.lineNumber || 0}`
|
||||
).join('\n');
|
||||
|
||||
expect(criticalErrors, `Critical console errors found:\n${errorDetails}`).toHaveLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performance baselines for ROA2WEB application
|
||||
*/
|
||||
export const PerformanceBaselines = {
|
||||
loginTime: 2000, // Max 2s for login
|
||||
dashboardLoad: 3000, // Max 3s for dashboard
|
||||
reportGeneration: 5000, // Max 5s for reports
|
||||
apiResponse: 1500, // Max 1.5s for API calls
|
||||
pageLoad: 4000 // Max 4s for page loads
|
||||
};
|
||||
|
||||
/**
|
||||
* Assert performance meets baselines
|
||||
* @param {number} actualTime - Actual measured time
|
||||
* @param {number} baseline - Performance baseline
|
||||
* @param {string} operation - Operation name
|
||||
* @param {Object} expect - Playwright expect object
|
||||
*/
|
||||
export function assertPerformanceBaseline(actualTime, baseline, operation, expect) {
|
||||
expect(actualTime, `${operation} took ${actualTime}ms, expected < ${baseline}ms`).toBeLessThan(baseline);
|
||||
}
|
||||
194
reports-app/frontend/tests/utils/helpers.js
Normal file
194
reports-app/frontend/tests/utils/helpers.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Test utility functions for ROA2WEB frontend testing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wait for element to be visible with custom timeout
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} selector - CSS selector
|
||||
* @param {number} timeout - Timeout in milliseconds
|
||||
*/
|
||||
export async function waitForVisible(page, selector, timeout = 10000) {
|
||||
await page.waitForSelector(selector, {
|
||||
state: 'visible',
|
||||
timeout
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all API calls to complete
|
||||
* @param {Page} page - Playwright page object
|
||||
*/
|
||||
export async function waitForApiCalls(page) {
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Take screenshot with timestamp
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} name - Screenshot name
|
||||
*/
|
||||
export async function takeTimestampedScreenshot(page, name) {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
await page.screenshot({
|
||||
path: `test-results/${name}-${timestamp}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if element exists in DOM (without waiting)
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {boolean} True if element exists
|
||||
*/
|
||||
export async function elementExists(page, selector) {
|
||||
const element = await page.$(selector);
|
||||
return element !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get element text content safely
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} selector - CSS selector
|
||||
* @returns {string|null} Text content or null if element not found
|
||||
*/
|
||||
export async function getTextContent(page, selector) {
|
||||
try {
|
||||
const element = await page.locator(selector);
|
||||
if (await element.isVisible()) {
|
||||
return await element.textContent();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Element not found: ${selector}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for toast message and return its content
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} type - Toast type: 'error', 'success', 'info', 'warn'
|
||||
* @param {number} timeout - Timeout in milliseconds
|
||||
* @returns {string|null} Toast message content
|
||||
*/
|
||||
export async function waitForToast(page, type = 'error', timeout = 5000) {
|
||||
try {
|
||||
const toastSelector = `.p-toast-message-${type}`;
|
||||
await page.waitForSelector(toastSelector, { timeout });
|
||||
return await page.locator(toastSelector).textContent();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill form fields from object
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {Object} fields - Object with selector: value pairs
|
||||
*/
|
||||
export async function fillForm(page, fields) {
|
||||
for (const [selector, value] of Object.entries(fields)) {
|
||||
await page.fill(selector, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current URL matches pattern
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} pattern - URL pattern to match
|
||||
* @returns {boolean} True if URL matches
|
||||
*/
|
||||
export function urlMatches(page, pattern) {
|
||||
return page.url().includes(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock successful authentication for tests
|
||||
* @param {Page} page - Playwright page object
|
||||
*/
|
||||
export async function mockSuccessfulAuth(page) {
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User'
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock companies API
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {Array} companies - Array of company objects
|
||||
*/
|
||||
export async function mockCompanies(page, companies = []) {
|
||||
const defaultCompanies = [
|
||||
{ code: 'COMP1', name: 'Test Company 1' },
|
||||
{ code: 'COMP2', name: 'Test Company 2' }
|
||||
];
|
||||
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(companies.length > 0 ? companies : defaultCompanies),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock API error response
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} endpoint - API endpoint pattern
|
||||
* @param {number} status - HTTP status code
|
||||
* @param {string} message - Error message
|
||||
*/
|
||||
export async function mockApiError(page, endpoint, status = 500, message = 'Internal server error') {
|
||||
await page.route(endpoint, async route => {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
detail: message
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all localStorage data
|
||||
* @param {Page} page - Playwright page object
|
||||
*/
|
||||
export async function clearStorage(page) {
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set viewport to common device size
|
||||
* @param {Page} page - Playwright page object
|
||||
* @param {string} device - Device name: 'mobile', 'tablet', 'desktop'
|
||||
*/
|
||||
export async function setDeviceViewport(page, device) {
|
||||
const viewports = {
|
||||
mobile: { width: 375, height: 667 },
|
||||
tablet: { width: 768, height: 1024 },
|
||||
desktop: { width: 1024, height: 768 },
|
||||
wide: { width: 1920, height: 1080 }
|
||||
};
|
||||
|
||||
if (viewports[device]) {
|
||||
await page.setViewportSize(viewports[device]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user