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:
2025-10-25 14:55:08 +03:00
commit 6b13ffa183
237 changed files with 70035 additions and 0 deletions

View 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)

View 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`* 🎭

View 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');
}
});
});

View File

@@ -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');
}
});
});

View 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');
});
});

View 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}`);
});
}
});
});

View 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);
});
});

View 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);
}
});
});

View 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');
}
});
});

View 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);
});
});
});

View 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
}
};

View 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
}
};

View 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

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View 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
};
}
}

View 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
}
}

View File

@@ -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');
});
});

View 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');
}
}

View 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 })
]);
}
}

View 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();
}
}
}

View 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();
}
}

View 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;
}
}

View 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);
}

View 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]);
}
}