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:
192
reports-app/frontend/tests/e2e/button-fix-test.spec.js
Normal file
192
reports-app/frontend/tests/e2e/button-fix-test.spec.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
|
||||
test.describe('🔧 Button Fix Test - Identify Disabled State Issue', () => {
|
||||
let loginPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('🐛 Debug Button Disabled State Logic', async ({ page }) => {
|
||||
console.log('\n=== DEBUGGING BUTTON DISABLED STATE ===');
|
||||
|
||||
// Helper function to get detailed button state
|
||||
const getButtonState = async () => {
|
||||
return await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
|
||||
// Get Vue component data if available
|
||||
const vueApp = document.querySelector('#app').__vue__;
|
||||
let vueData = null;
|
||||
|
||||
try {
|
||||
// Try to access Vue component state
|
||||
const loginComponent = document.querySelector('.login-container').__vueParentComponent;
|
||||
if (loginComponent && loginComponent.setupState) {
|
||||
vueData = {
|
||||
credentials: loginComponent.setupState.credentials?.value,
|
||||
formErrors: loginComponent.setupState.formErrors?.value,
|
||||
isFormValid: loginComponent.setupState.isFormValid?.value
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not access Vue state:', e.message);
|
||||
}
|
||||
|
||||
return {
|
||||
dom: {
|
||||
usernameValue: usernameInput?.value || '',
|
||||
passwordValue: passwordInput?.value || '',
|
||||
buttonDisabled: button?.disabled,
|
||||
buttonClasses: button?.className,
|
||||
usernameRequired: usernameInput?.required,
|
||||
passwordRequired: passwordInput?.required
|
||||
},
|
||||
vue: vueData
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Test 1: Initial state
|
||||
console.log('\n--- Test 1: Initial State ---');
|
||||
let state = await getButtonState();
|
||||
console.log('Initial state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 2: Fill only username
|
||||
console.log('\n--- Test 2: Username Only ---');
|
||||
await page.fill('#username', 'test_user');
|
||||
await page.waitForTimeout(500); // Wait for Vue reactivity
|
||||
state = await getButtonState();
|
||||
console.log('Username only state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 3: Fill both fields
|
||||
console.log('\n--- Test 3: Both Fields ---');
|
||||
await page.fill('#password input', 'test_password');
|
||||
await page.waitForTimeout(500); // Wait for Vue reactivity
|
||||
state = await getButtonState();
|
||||
console.log('Both fields state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 4: Check if validation triggers
|
||||
console.log('\n--- Test 4: Trigger Validation ---');
|
||||
await page.click('.login-card'); // Click outside to blur
|
||||
await page.waitForTimeout(500);
|
||||
state = await getButtonState();
|
||||
console.log('After blur state:', JSON.stringify(state, null, 2));
|
||||
|
||||
// Test 5: Manual button click attempt
|
||||
console.log('\n--- Test 5: Button Click Attempt ---');
|
||||
const isClickable = await page.evaluate(() => {
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
return !button.disabled;
|
||||
});
|
||||
console.log('Button is clickable:', isClickable);
|
||||
|
||||
if (isClickable) {
|
||||
console.log('✅ Button should be clickable');
|
||||
} else {
|
||||
console.log('❌ Button is still disabled - investigating why...');
|
||||
|
||||
// Check validation logic
|
||||
const validationState = await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
|
||||
return {
|
||||
usernameEmpty: !usernameInput.value.trim(),
|
||||
passwordEmpty: !passwordInput.value.trim(),
|
||||
usernameLength: usernameInput.value.length,
|
||||
passwordLength: passwordInput.value.length,
|
||||
formValidity: usernameInput.form?.checkValidity()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Validation details:', JSON.stringify(validationState, null, 2));
|
||||
}
|
||||
|
||||
// Take screenshot for analysis
|
||||
await page.screenshot({ path: 'button-debug.png', fullPage: true });
|
||||
});
|
||||
|
||||
test('🔄 Test Button Reactivity with Real Input', async ({ page }) => {
|
||||
console.log('\n=== TESTING BUTTON REACTIVITY ===');
|
||||
|
||||
// Monitor button state changes
|
||||
const buttonStates = [];
|
||||
|
||||
const checkButton = async (action) => {
|
||||
const disabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
buttonStates.push({ action, disabled });
|
||||
console.log(`After ${action}: disabled = ${disabled}`);
|
||||
};
|
||||
|
||||
await checkButton('initial load');
|
||||
|
||||
// Type character by character to see when button enables
|
||||
const username = 'test';
|
||||
const password = 'pass';
|
||||
|
||||
for (let i = 0; i < username.length; i++) {
|
||||
await page.fill('#username', username.substring(0, i + 1));
|
||||
await page.waitForTimeout(100);
|
||||
await checkButton(`username: "${username.substring(0, i + 1)}"`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
await page.fill('#password input', password.substring(0, i + 1));
|
||||
await page.waitForTimeout(100);
|
||||
await checkButton(`password: "${password.substring(0, i + 1)}"`);
|
||||
}
|
||||
|
||||
console.log('\nButton state progression:');
|
||||
buttonStates.forEach((state, index) => {
|
||||
console.log(`${index + 1}. ${state.action}: ${state.disabled ? 'DISABLED' : 'ENABLED'}`);
|
||||
});
|
||||
});
|
||||
|
||||
test('🎯 Force Button Enable Test', async ({ page }) => {
|
||||
console.log('\n=== TESTING FORCED BUTTON ENABLE ===');
|
||||
|
||||
// Fill valid data
|
||||
await page.fill('#username', 'valid_user');
|
||||
await page.fill('#password input', 'valid_password');
|
||||
|
||||
// Wait for Vue reactivity
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Force enable button via JavaScript if needed
|
||||
const buttonEnabled = await page.evaluate(() => {
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
const wasDisabled = button.disabled;
|
||||
|
||||
// Try to force enable for testing
|
||||
button.disabled = false;
|
||||
button.classList.remove('p-disabled');
|
||||
|
||||
return { wasDisabled, nowDisabled: button.disabled };
|
||||
});
|
||||
|
||||
console.log('Button enable attempt:', buttonEnabled);
|
||||
|
||||
if (!buttonEnabled.nowDisabled) {
|
||||
console.log('✅ Button was successfully enabled');
|
||||
|
||||
// Try to click it now
|
||||
await page.click('button[type="submit"]');
|
||||
console.log('✅ Button click succeeded');
|
||||
|
||||
// Wait for potential API call
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if login was attempted
|
||||
const currentUrl = page.url();
|
||||
console.log('Current URL after click:', currentUrl);
|
||||
|
||||
} else {
|
||||
console.log('❌ Could not enable button');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,324 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage.js';
|
||||
|
||||
test.describe('📊 Complete Reports Functionality Test', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
let networkRequests = [];
|
||||
let apiErrors = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset monitoring arrays
|
||||
networkRequests = [];
|
||||
apiErrors = [];
|
||||
|
||||
// Monitor network requests
|
||||
page.on('request', request => {
|
||||
networkRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400) {
|
||||
apiErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText()
|
||||
});
|
||||
console.log(`❌ API Error: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
});
|
||||
|
||||
test('🎯 Complete User Flow: Login → Dashboard → Reports', async ({ page }) => {
|
||||
console.log('\n🎯 === TESTING COMPLETE REPORTS FUNCTIONALITY ===');
|
||||
|
||||
// Phase 1: Authentication
|
||||
console.log('\n📍 Phase 1: Login with real credentials');
|
||||
await loginPage.navigate();
|
||||
|
||||
// Use real test credentials
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
// Submit login and wait for response
|
||||
const [authResponse] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
console.log(`📊 Auth Response: ${authResponse.status()}`);
|
||||
expect(authResponse.status()).toBe(200);
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
console.log('✅ Successfully redirected to dashboard');
|
||||
|
||||
// Phase 2: Test Dashboard Loading
|
||||
console.log('\n📍 Phase 2: Dashboard Data Loading');
|
||||
|
||||
// Wait for dashboard to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check for companies API call
|
||||
const companiesRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/companies') || req.url.includes('/api/companies')
|
||||
);
|
||||
|
||||
console.log(`📊 Companies API requests: ${companiesRequests.length}`);
|
||||
|
||||
if (companiesRequests.length > 0) {
|
||||
console.log('✅ Companies API was called');
|
||||
|
||||
// Check if there were CORS errors
|
||||
const corsErrors = apiErrors.filter(err =>
|
||||
err.url.includes('/companies')
|
||||
);
|
||||
|
||||
if (corsErrors.length > 0) {
|
||||
console.log('❌ CORS errors detected:');
|
||||
corsErrors.forEach(err => {
|
||||
console.log(` - ${err.status} ${err.url}`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ No CORS errors for companies API');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Companies API was not called - checking why...');
|
||||
}
|
||||
|
||||
// Look for company selector dropdown
|
||||
const companySelectors = [
|
||||
'.p-dropdown',
|
||||
'select',
|
||||
'[data-testid="company-select"]',
|
||||
'.company-selector'
|
||||
];
|
||||
|
||||
let companySelectorFound = false;
|
||||
for (const selector of companySelectors) {
|
||||
const element = page.locator(selector).first();
|
||||
if (await element.isVisible()) {
|
||||
console.log(`✅ Company selector found: ${selector}`);
|
||||
companySelectorFound = true;
|
||||
|
||||
// Try to interact with it
|
||||
await element.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Look for dropdown options
|
||||
const options = page.locator('.p-dropdown-item, option');
|
||||
const optionCount = await options.count();
|
||||
console.log(`📊 Company options available: ${optionCount}`);
|
||||
|
||||
if (optionCount > 0) {
|
||||
// Select first company
|
||||
await options.first().click();
|
||||
console.log('✅ Company selected');
|
||||
|
||||
// Wait for data to load after company selection
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check for additional API calls after company selection
|
||||
const invoicesRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/invoices')
|
||||
);
|
||||
const paymentsRequests = networkRequests.filter(req =>
|
||||
req.url.includes('/payments')
|
||||
);
|
||||
|
||||
console.log(`📊 Invoices API requests: ${invoicesRequests.length}`);
|
||||
console.log(`📊 Payments API requests: ${paymentsRequests.length}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!companySelectorFound) {
|
||||
console.log('⚠️ Company selector not found - taking screenshot for analysis');
|
||||
await page.screenshot({ path: 'dashboard-no-company-selector.png', fullPage: true });
|
||||
}
|
||||
|
||||
// Phase 3: Test Navigation to Reports
|
||||
console.log('\n📍 Phase 3: Navigation to Reports');
|
||||
|
||||
// Look for navigation links
|
||||
const navLinks = [
|
||||
'text=/facturi/i',
|
||||
'text=/invoices/i',
|
||||
'text=/încasări/i',
|
||||
'text=/payments/i',
|
||||
'[href*="/invoices"]',
|
||||
'[href*="/payments"]'
|
||||
];
|
||||
|
||||
for (const linkSelector of navLinks) {
|
||||
const link = page.locator(linkSelector).first();
|
||||
if (await link.isVisible()) {
|
||||
const linkText = await link.textContent();
|
||||
console.log(`✅ Found navigation link: "${linkText}"`);
|
||||
|
||||
// Click the link
|
||||
await link.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if we navigated to the correct page
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 Navigated to: ${currentUrl}`);
|
||||
|
||||
// Wait for page content to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Take screenshot of the reports page
|
||||
await page.screenshot({
|
||||
path: `reports-${linkText.toLowerCase().replace(/[^a-z0-9]/g, '-')}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
// Look for data tables or report content
|
||||
const dataElements = [
|
||||
'table',
|
||||
'.p-datatable',
|
||||
'.data-table',
|
||||
'.report-table',
|
||||
'.invoice-list',
|
||||
'.payment-list'
|
||||
];
|
||||
|
||||
let dataFound = false;
|
||||
for (const selector of dataElements) {
|
||||
const element = page.locator(selector);
|
||||
if (await element.isVisible()) {
|
||||
const rowCount = await element.locator('tr, .row').count();
|
||||
console.log(`✅ Data table found with ${rowCount} rows`);
|
||||
dataFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataFound) {
|
||||
console.log('⚠️ No data tables found - checking for loading states or errors');
|
||||
|
||||
// Check for loading indicators
|
||||
const loadingElements = [
|
||||
'.loading',
|
||||
'.p-progress-spinner',
|
||||
'.spinner',
|
||||
'[data-testid="loading"]'
|
||||
];
|
||||
|
||||
for (const loadingSelector of loadingElements) {
|
||||
if (await page.locator(loadingSelector).isVisible()) {
|
||||
console.log('⏳ Loading indicator found - data may still be loading');
|
||||
await page.waitForTimeout(5000); // Wait longer for data
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go back to dashboard for next test
|
||||
await page.goto('/dashboard');
|
||||
await page.waitForTimeout(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Take final dashboard screenshot
|
||||
await page.screenshot({ path: 'final-dashboard-state.png', fullPage: true });
|
||||
});
|
||||
|
||||
test('🔍 API Endpoints Health Check', async ({ page }) => {
|
||||
console.log('\n🔍 === API ENDPOINTS HEALTH CHECK ===');
|
||||
|
||||
// First authenticate to get access token
|
||||
await loginPage.navigate();
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
const [authResponse] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
expect(authResponse.status()).toBe(200);
|
||||
await page.waitForURL(/.*dashboard/);
|
||||
|
||||
// Test individual API endpoints
|
||||
const endpoints = [
|
||||
'/api/companies',
|
||||
'/api/invoices',
|
||||
'/api/payments'
|
||||
];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
console.log(`\n--- Testing endpoint: ${endpoint} ---`);
|
||||
|
||||
// Navigate to trigger API call
|
||||
if (endpoint.includes('invoices')) {
|
||||
await page.goto('/invoices');
|
||||
} else if (endpoint.includes('payments')) {
|
||||
await page.goto('/payments');
|
||||
} else {
|
||||
await page.goto('/dashboard');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if API was called
|
||||
const apiCalls = networkRequests.filter(req => req.url.includes(endpoint));
|
||||
|
||||
if (apiCalls.length > 0) {
|
||||
console.log(`✅ ${endpoint} was called (${apiCalls.length} requests)`);
|
||||
|
||||
// Check for errors
|
||||
const errors = apiErrors.filter(err => err.url.includes(endpoint));
|
||||
if (errors.length > 0) {
|
||||
console.log(`❌ Errors found for ${endpoint}:`);
|
||||
errors.forEach(err => {
|
||||
console.log(` - ${err.status} ${err.statusText}`);
|
||||
});
|
||||
} else {
|
||||
console.log(`✅ ${endpoint} returned successful responses`);
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ ${endpoint} was not called`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate test report
|
||||
console.log('\n📋 === TEST REPORT ===');
|
||||
console.log(`🌐 Total Network Requests: ${networkRequests.length}`);
|
||||
console.log(`❌ API Errors: ${apiErrors.length}`);
|
||||
|
||||
if (apiErrors.length > 0) {
|
||||
console.log('\n❌ API Errors Details:');
|
||||
apiErrors.forEach(error => {
|
||||
console.log(` - ${error.status} ${error.url} (${error.statusText})`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for specific CORS errors
|
||||
const corsErrors = apiErrors.filter(err =>
|
||||
err.statusText.includes('CORS') || err.status === 0
|
||||
);
|
||||
|
||||
if (corsErrors.length > 0) {
|
||||
console.log('\n🚨 CORS Issues Detected:');
|
||||
corsErrors.forEach(error => {
|
||||
console.log(` - ${error.url}`);
|
||||
});
|
||||
console.log('\n💡 Recommendation: Check CORS configuration in backend');
|
||||
} else {
|
||||
console.log('\n✅ No CORS issues detected');
|
||||
}
|
||||
});
|
||||
});
|
||||
228
reports-app/frontend/tests/e2e/dashboard/dashboard.spec.js
Normal file
228
reports-app/frontend/tests/e2e/dashboard/dashboard.spec.js
Normal file
@@ -0,0 +1,228 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../../page-objects/DashboardPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
|
||||
test.describe('Dashboard View', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
|
||||
// Mock successful authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'testuser',
|
||||
full_name: 'Test User'
|
||||
}
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies endpoint
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' },
|
||||
{ code: 'COMP2', name: 'Compania Test 2' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock invoices summary endpoint
|
||||
await page.route('**/api/invoices/*/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 150,
|
||||
paid: 120,
|
||||
overdue: 30,
|
||||
amount: 850000.50
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock payments summary endpoint
|
||||
await page.route('**/api/payments/*/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 125,
|
||||
amount: 750000.25
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Login first
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
});
|
||||
|
||||
test('should display dashboard page correctly', async ({ page: _page }) => {
|
||||
// Check page elements
|
||||
expect(await dashboardPage.isOnDashboardPage()).toBe(true);
|
||||
|
||||
// Check page title contains "Dashboard"
|
||||
const title = await dashboardPage.getPageTitle();
|
||||
expect(title).toContain('Dashboard');
|
||||
|
||||
// Check welcome message includes username
|
||||
const welcomeMessage = await dashboardPage.getWelcomeMessage();
|
||||
expect(welcomeMessage).toContain('testuser');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
// Wait for dashboard to load
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
|
||||
// Should show company selection card
|
||||
expect(await dashboardPage.isCompanySelectionVisible()).toBe(true);
|
||||
|
||||
// Dashboard content should not be visible yet
|
||||
expect(await dashboardPage.isDashboardContentVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display dashboard content after company selection', async ({ page }) => {
|
||||
// Wait for dashboard to load
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
|
||||
// Select a company
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Wait for dashboard content to appear
|
||||
await page.waitForSelector(dashboardPage.dashboardContent, { timeout: 10000 });
|
||||
|
||||
// Dashboard content should now be visible
|
||||
expect(await dashboardPage.isDashboardContentVisible()).toBe(true);
|
||||
|
||||
// Stats cards should be visible
|
||||
expect(await dashboardPage.areStatsCardsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should display correct statistics after company selection', async ({ page }) => {
|
||||
// Wait for dashboard to load and select company
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Wait for stats to load
|
||||
await page.waitForSelector(dashboardPage.statsGrid, { timeout: 10000 });
|
||||
|
||||
// Check statistics values
|
||||
const stats = await dashboardPage.getStatsData();
|
||||
|
||||
expect(stats.invoices).toBe('150');
|
||||
expect(stats.payments).toBe('125');
|
||||
expect(stats.company).toContain('Compania Test 1');
|
||||
});
|
||||
|
||||
test('should navigate to invoices view when clicking invoices action', async ({ page }) => {
|
||||
// Setup dashboard with company selected
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.dashboardContent);
|
||||
|
||||
// Click invoices action button
|
||||
await dashboardPage.clickInvoicesAction();
|
||||
|
||||
// Should navigate to invoices page
|
||||
await page.waitForURL('/invoices');
|
||||
expect(page.url()).toContain('/invoices');
|
||||
});
|
||||
|
||||
test('should navigate to payments view when clicking payments action', async ({ page }) => {
|
||||
// Setup dashboard with company selected
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.dashboardContent);
|
||||
|
||||
// Click payments action button
|
||||
await dashboardPage.clickPaymentsAction();
|
||||
|
||||
// Should navigate to payments page
|
||||
await page.waitForURL('/payments');
|
||||
expect(page.url()).toContain('/payments');
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock companies API error
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
detail: 'Internal server error'
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate to dashboard
|
||||
await dashboardPage.navigate();
|
||||
|
||||
// Should still show the page but might show error messages
|
||||
expect(await dashboardPage.isOnDashboardPage()).toBe(true);
|
||||
|
||||
// Check for error toast messages
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should update stats when switching between companies', async ({ page }) => {
|
||||
// Mock different stats for second company
|
||||
await page.route('**/api/invoices/COMP2/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 200,
|
||||
paid: 180,
|
||||
overdue: 20,
|
||||
amount: 1200000.75
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/payments/COMP2/summary', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
total: 175,
|
||||
amount: 950000.50
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Select first company
|
||||
await dashboardPage.waitForDashboardLoad();
|
||||
await dashboardPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(dashboardPage.statsGrid);
|
||||
|
||||
const stats1 = await dashboardPage.getStatsData();
|
||||
expect(stats1.invoices).toBe('150');
|
||||
|
||||
// Switch to second company
|
||||
await dashboardPage.selectCompany('Compania Test 2');
|
||||
await dashboardPage.waitForLoadingToFinish();
|
||||
|
||||
const stats2 = await dashboardPage.getStatsData();
|
||||
expect(stats2.invoices).toBe('200');
|
||||
expect(stats2.company).toContain('Compania Test 2');
|
||||
});
|
||||
});
|
||||
324
reports-app/frontend/tests/e2e/debugging-real-issues.spec.js
Normal file
324
reports-app/frontend/tests/e2e/debugging-real-issues.spec.js
Normal file
@@ -0,0 +1,324 @@
|
||||
//! 🔍 DEBUGGING COMPREHENSIVE TEST - ROA2WEB Real Issues Detection
|
||||
//! Created: 2025-08-04
|
||||
//! Purpose: Find and fix REAL problems, not just "passing tests"
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
|
||||
test.describe('🔍 ROA2WEB Real Issues Debugging Suite', () => {
|
||||
let loginPage;
|
||||
let networkRequests = [];
|
||||
let consoleErrors = [];
|
||||
let apiResponses = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset monitoring arrays
|
||||
networkRequests = [];
|
||||
consoleErrors = [];
|
||||
apiResponses = [];
|
||||
|
||||
// Setup comprehensive monitoring
|
||||
page.on('request', request => {
|
||||
networkRequests.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
headers: request.headers(),
|
||||
postData: request.postData(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
apiResponses.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText(),
|
||||
headers: response.headers(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log failed requests immediately
|
||||
if (response.status() >= 400) {
|
||||
console.log(`❌ API Error: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
const error = {
|
||||
type: msg.type(),
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
consoleErrors.push(error);
|
||||
console.log(`🔥 Console Error: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', err => {
|
||||
consoleErrors.push({
|
||||
type: 'pageerror',
|
||||
text: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
console.log(`💥 Page Error: ${err.message}`);
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
await loginPage.navigate();
|
||||
});
|
||||
|
||||
test('🧪 REAL AUTH FLOW - Find FormData vs JSON Issues', async ({ page }) => {
|
||||
console.log('\n🔍 === TESTING REAL AUTHENTICATION FLOW ===');
|
||||
|
||||
// Fill real credentials (update these with actual test credentials)
|
||||
const username = 'test_user';
|
||||
const password = 'test_password';
|
||||
|
||||
await page.fill('#username', username);
|
||||
await page.fill('#password input', password);
|
||||
|
||||
// Monitor the actual request being sent
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login'),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
// CRITICAL: Analyze the actual request format
|
||||
const request = response.request();
|
||||
const postData = request.postData();
|
||||
const contentType = request.headers()['content-type'];
|
||||
|
||||
console.log('\n📊 === REQUEST ANALYSIS ===');
|
||||
console.log('Content-Type:', contentType);
|
||||
console.log('Request Method:', request.method());
|
||||
console.log('Request Body:', postData);
|
||||
console.log('Response Status:', response.status());
|
||||
|
||||
// Check if FormData or JSON is being sent
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
console.log('✅ Sending JSON (correct)');
|
||||
try {
|
||||
const jsonData = JSON.parse(postData);
|
||||
expect(jsonData).toHaveProperty('username');
|
||||
expect(jsonData).toHaveProperty('password');
|
||||
} catch (e) {
|
||||
console.log('❌ Invalid JSON format');
|
||||
}
|
||||
} else if (contentType && contentType.includes('multipart/form-data')) {
|
||||
console.log('⚠️ Sending FormData (may cause issues)');
|
||||
} else {
|
||||
console.log('❓ Unknown content type:', contentType);
|
||||
}
|
||||
|
||||
// Check response
|
||||
if (response.status() === 422) {
|
||||
const responseBody = await response.text();
|
||||
console.log('🚨 422 Validation Error:', responseBody);
|
||||
}
|
||||
|
||||
// Generate comprehensive monitoring report
|
||||
console.log('\n📈 === MONITORING REPORT ===');
|
||||
console.log(`Network Requests: ${networkRequests.length}`);
|
||||
console.log(`API Responses: ${apiResponses.length}`);
|
||||
console.log(`Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
// Take screenshot for analysis
|
||||
await page.screenshot({
|
||||
path: 'debug-auth-flow.png',
|
||||
fullPage: true
|
||||
});
|
||||
});
|
||||
|
||||
test('🔧 LOGIN BUTTON STATE - Debug Disabled Logic', async ({ page }) => {
|
||||
console.log('\n🔍 === DEBUGGING LOGIN BUTTON STATE ===');
|
||||
|
||||
// Test initial state
|
||||
const initialDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Initial button disabled:', initialDisabled);
|
||||
|
||||
// Test empty fields
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
|
||||
const emptyFieldsDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Empty fields - button disabled:', emptyFieldsDisabled);
|
||||
|
||||
// Test with only username
|
||||
await page.fill('#username', 'test');
|
||||
const usernameOnlyDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Username only - button disabled:', usernameOnlyDisabled);
|
||||
|
||||
// Test with both fields
|
||||
await page.fill('#password input', 'password');
|
||||
const bothFieldsDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
console.log('Both fields - button disabled:', bothFieldsDisabled);
|
||||
|
||||
// Check form validation state
|
||||
const formValidation = await page.evaluate(() => {
|
||||
const usernameInput = document.getElementById('username');
|
||||
const passwordInput = document.querySelector('#password input');
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
|
||||
return {
|
||||
usernameValue: usernameInput?.value,
|
||||
passwordValue: passwordInput?.value,
|
||||
buttonDisabled: button?.disabled,
|
||||
buttonClasses: button?.className,
|
||||
usernameValid: usernameInput?.checkValidity(),
|
||||
passwordValid: passwordInput?.checkValidity()
|
||||
};
|
||||
});
|
||||
|
||||
console.log('Form validation state:', JSON.stringify(formValidation, null, 2));
|
||||
|
||||
// Take screenshot of current state
|
||||
await page.screenshot({ path: 'debug-button-state.png' });
|
||||
});
|
||||
|
||||
test('🚨 ERROR MESSAGE FORMAT - Debug Toast Issues', async ({ page }) => {
|
||||
console.log('\n🔍 === DEBUGGING ERROR MESSAGE FORMAT ===');
|
||||
|
||||
// Force a network error by using wrong endpoint
|
||||
await page.route('**/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Server error for testing' })
|
||||
});
|
||||
});
|
||||
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for error message and analyze its content
|
||||
await page.waitForTimeout(2000); // Wait for toast to appear
|
||||
|
||||
// Check various possible error message selectors
|
||||
const errorSelectors = [
|
||||
'.error-message',
|
||||
'.p-toast-message-error',
|
||||
'.p-toast-message-text',
|
||||
'.p-toast-summary',
|
||||
'.p-toast-detail'
|
||||
];
|
||||
|
||||
for (const selector of errorSelectors) {
|
||||
const elements = await page.locator(selector).all();
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const text = await elements[i].textContent();
|
||||
if (text && text.trim()) {
|
||||
console.log(`Error message found in ${selector}[${i}]:`, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check all visible text content that might contain error messages
|
||||
const allText = await page.evaluate(() => {
|
||||
const textNodes = [];
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
const text = node.textContent.trim();
|
||||
if (text.toLowerCase().includes('eroare') ||
|
||||
text.toLowerCase().includes('error') ||
|
||||
text.toLowerCase().includes('conectare')) {
|
||||
textNodes.push(text);
|
||||
}
|
||||
}
|
||||
return textNodes;
|
||||
});
|
||||
|
||||
console.log('All error-related text found:', allText);
|
||||
|
||||
// Take screenshot of error state
|
||||
await page.screenshot({ path: 'debug-error-messages.png' });
|
||||
});
|
||||
|
||||
test('🌐 NETWORK MONITORING - Real API Behavior', async ({ page }) => {
|
||||
console.log('\n🔍 === COMPREHENSIVE NETWORK MONITORING ===');
|
||||
|
||||
// Test various scenarios
|
||||
const testScenarios = [
|
||||
{ name: 'Valid Login', username: 'valid_user', password: 'valid_pass' },
|
||||
{ name: 'Invalid Credentials', username: 'invalid', password: 'invalid' },
|
||||
{ name: 'Empty Credentials', username: '', password: '' }
|
||||
];
|
||||
|
||||
for (const scenario of testScenarios) {
|
||||
console.log(`\n--- Testing: ${scenario.name} ---`);
|
||||
|
||||
// Clear form
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
|
||||
// Fill credentials if provided
|
||||
if (scenario.username) await page.fill('#username', scenario.username);
|
||||
if (scenario.password) await page.fill('#password input', scenario.password);
|
||||
|
||||
// Reset monitoring arrays for this scenario
|
||||
networkRequests.length = 0;
|
||||
apiResponses.length = 0;
|
||||
consoleErrors.length = 0;
|
||||
|
||||
// Try to submit (if button is enabled)
|
||||
const isDisabled = await page.locator('button[type="submit"]').isDisabled();
|
||||
if (!isDisabled) {
|
||||
try {
|
||||
const [response] = await Promise.all([
|
||||
page.waitForResponse('**/auth/login', { timeout: 5000 }),
|
||||
page.click('button[type="submit"]')
|
||||
]);
|
||||
|
||||
console.log(`Response Status: ${response.status()}`);
|
||||
const responseBody = await response.text();
|
||||
console.log(`Response Body: ${responseBody.substring(0, 200)}...`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`No API call made: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('Button is disabled - no API call expected');
|
||||
}
|
||||
|
||||
// Wait a bit for any async operations
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
console.log(`Network requests: ${networkRequests.length}`);
|
||||
console.log(`Console errors: ${consoleErrors.length}`);
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate final report
|
||||
console.log('\n📋 === FINAL TEST REPORT ===');
|
||||
console.log(`Total Network Requests: ${networkRequests.length}`);
|
||||
console.log(`Total API Responses: ${apiResponses.length}`);
|
||||
console.log(`Total Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('\n🚨 Console Errors Found:');
|
||||
consoleErrors.forEach((error, index) => {
|
||||
console.log(`${index + 1}. ${error.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (apiResponses.some(r => r.status >= 400)) {
|
||||
console.log('\n❌ Failed API Requests:');
|
||||
apiResponses
|
||||
.filter(r => r.status >= 400)
|
||||
.forEach(response => {
|
||||
console.log(`${response.status} ${response.url}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
214
reports-app/frontend/tests/e2e/invoices/invoices.spec.js
Normal file
214
reports-app/frontend/tests/e2e/invoices/invoices.spec.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { InvoicesPage } from '../../page-objects/InvoicesPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
import { mockInvoices } from '../../fixtures/invoices.js';
|
||||
|
||||
test.describe('Invoices View', () => {
|
||||
let loginPage;
|
||||
let invoicesPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
invoicesPage = new InvoicesPage(page);
|
||||
|
||||
// Mock authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock invoices endpoint
|
||||
await page.route('**/api/invoices/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockInvoices),
|
||||
});
|
||||
});
|
||||
|
||||
// Login and navigate to invoices
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
await invoicesPage.navigate();
|
||||
});
|
||||
|
||||
test('should display invoices page correctly', async ({ page: _page }) => {
|
||||
expect(await invoicesPage.isOnInvoicesPage()).toBe(true);
|
||||
|
||||
const title = await invoicesPage.getPageTitle();
|
||||
expect(title).toContain('Facturi');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
expect(await invoicesPage.isCompanySelectionVisible()).toBe(true);
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display invoices table after company selection', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
|
||||
await page.waitForSelector(invoicesPage.invoicesTable, { timeout: 10000 });
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter invoices by search term', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Search for specific invoice
|
||||
await invoicesPage.searchInvoices('INV001');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await invoicesPage.getVisibleInvoicesCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
|
||||
// Check that displayed invoices contain search term
|
||||
const firstRowData = await invoicesPage.getFirstInvoiceData();
|
||||
expect(firstRowData.number).toContain('INV001');
|
||||
});
|
||||
|
||||
test('should filter invoices by status', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Filter by paid status
|
||||
await invoicesPage.filterByStatus('paid');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await invoicesPage.getVisibleInvoicesCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should sort invoices by date', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click date column header to sort
|
||||
await invoicesPage.sortByColumn('date');
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify sorting worked
|
||||
const firstRowDate = await invoicesPage.getFirstInvoiceData();
|
||||
expect(firstRowDate.date).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display invoice details when clicking on row', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click on first invoice row
|
||||
await invoicesPage.clickFirstInvoiceRow();
|
||||
|
||||
// Check if details panel or modal appears
|
||||
expect(await invoicesPage.isInvoiceDetailsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should export invoices data', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Set up download handler
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await invoicesPage.clickExportButton();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('facturi');
|
||||
});
|
||||
|
||||
test('should handle pagination correctly', async ({ page }) => {
|
||||
// Mock large dataset
|
||||
await page.route('**/api/invoices/COMP1*', async route => {
|
||||
const url = route.request().url();
|
||||
const urlParams = new URL(url).searchParams;
|
||||
const page_num = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: mockInvoices.slice((page_num - 1) * 10, page_num * 10),
|
||||
total: 25,
|
||||
page: page_num,
|
||||
size: 10,
|
||||
pages: 3
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Check pagination controls appear
|
||||
expect(await invoicesPage.isPaginationVisible()).toBe(true);
|
||||
|
||||
// Navigate to next page
|
||||
await invoicesPage.goToNextPage();
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify page changed
|
||||
const currentPage = await invoicesPage.getCurrentPage();
|
||||
expect(currentPage).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock API error
|
||||
await page.route('**/api/invoices/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Should show error message
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should refresh data when refresh button clicked', async ({ page }) => {
|
||||
await invoicesPage.waitForPageLoad();
|
||||
await invoicesPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(invoicesPage.invoicesTable);
|
||||
|
||||
// Click refresh button
|
||||
await invoicesPage.clickRefreshButton();
|
||||
await invoicesPage.waitForLoadingToFinish();
|
||||
|
||||
// Table should still be visible after refresh
|
||||
expect(await invoicesPage.isInvoicesTableVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
254
reports-app/frontend/tests/e2e/payments/payments.spec.js
Normal file
254
reports-app/frontend/tests/e2e/payments/payments.spec.js
Normal file
@@ -0,0 +1,254 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { PaymentsPage } from '../../page-objects/PaymentsPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
import { mockPayments } from '../../fixtures/payments.js';
|
||||
|
||||
test.describe('Payments View', () => {
|
||||
let loginPage;
|
||||
let paymentsPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
loginPage = new LoginPage(page);
|
||||
paymentsPage = new PaymentsPage(page);
|
||||
|
||||
// Mock authentication
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock companies
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Compania Test 1' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock payments endpoint
|
||||
await page.route('**/api/payments/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockPayments),
|
||||
});
|
||||
});
|
||||
|
||||
// Login and navigate to payments
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
await paymentsPage.navigate();
|
||||
});
|
||||
|
||||
test('should display payments page correctly', async ({ page: _page }) => {
|
||||
expect(await paymentsPage.isOnPaymentsPage()).toBe(true);
|
||||
|
||||
const title = await paymentsPage.getPageTitle();
|
||||
expect(title).toContain('Încasări');
|
||||
});
|
||||
|
||||
test('should show company selection when no company selected', async ({ page: _page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
expect(await paymentsPage.isCompanySelectionVisible()).toBe(true);
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test('should display payments table after company selection', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
|
||||
await page.waitForSelector(paymentsPage.paymentsTable, { timeout: 10000 });
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter payments by search term', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Search for specific payment
|
||||
await paymentsPage.searchPayments('PAY001');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
|
||||
// Check that displayed payments contain search term
|
||||
const firstRowData = await paymentsPage.getFirstPaymentData();
|
||||
expect(firstRowData.reference).toContain('PAY001');
|
||||
});
|
||||
|
||||
test('should filter payments by method', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Filter by bank transfer
|
||||
await paymentsPage.filterByMethod('bank_transfer');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should sort payments by date', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click date column header to sort
|
||||
await paymentsPage.sortByColumn('date');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify sorting worked
|
||||
const firstRowDate = await paymentsPage.getFirstPaymentData();
|
||||
expect(firstRowDate.date).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display payment details when clicking on row', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click on first payment row
|
||||
await paymentsPage.clickFirstPaymentRow();
|
||||
|
||||
// Check if details panel or modal appears
|
||||
expect(await paymentsPage.isPaymentDetailsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should export payments data', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Set up download handler
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await paymentsPage.clickExportButton();
|
||||
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toContain('incasari');
|
||||
});
|
||||
|
||||
test('should display correct payment totals', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Check if totals card is visible and contains data
|
||||
if (await paymentsPage.isTotalsCardVisible()) {
|
||||
const totals = await paymentsPage.getTotalsData();
|
||||
expect(parseFloat(totals.totalAmount)).toBeGreaterThan(0);
|
||||
expect(parseInt(totals.totalCount)).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter payments by date range', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Apply this month filter
|
||||
await paymentsPage.filterByDateRange('thisMonth');
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
const visibleRows = await paymentsPage.getVisiblePaymentsCount();
|
||||
expect(visibleRows).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('should handle pagination correctly', async ({ page }) => {
|
||||
// Mock large dataset
|
||||
await page.route('**/api/payments/COMP1*', async route => {
|
||||
const url = route.request().url();
|
||||
const urlParams = new URL(url).searchParams;
|
||||
const page_num = parseInt(urlParams.get('page') || '1');
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
items: mockPayments.slice((page_num - 1) * 10, page_num * 10),
|
||||
total: 25,
|
||||
page: page_num,
|
||||
size: 10,
|
||||
pages: 3
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Check pagination controls appear
|
||||
expect(await paymentsPage.isPaginationVisible()).toBe(true);
|
||||
|
||||
// Navigate to next page
|
||||
await paymentsPage.goToNextPage();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Verify page changed
|
||||
const currentPage = await paymentsPage.getCurrentPage();
|
||||
expect(currentPage).toBe(2);
|
||||
});
|
||||
|
||||
test('should handle API errors gracefully', async ({ page }) => {
|
||||
// Mock API error
|
||||
await page.route('**/api/payments/COMP1', async route => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ detail: 'Internal server error' }),
|
||||
});
|
||||
});
|
||||
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
|
||||
// Should show error message
|
||||
const errorToast = page.locator('.p-toast-message-error');
|
||||
if (await errorToast.isVisible()) {
|
||||
const errorText = await errorToast.textContent();
|
||||
expect(errorText.toLowerCase()).toContain('eroare');
|
||||
}
|
||||
});
|
||||
|
||||
test('should refresh data when refresh button clicked', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Click refresh button
|
||||
await paymentsPage.clickRefreshButton();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
// Table should still be visible after refresh
|
||||
expect(await paymentsPage.isPaymentsTableVisible()).toBe(true);
|
||||
});
|
||||
|
||||
test('should group payments by method in summary view', async ({ page }) => {
|
||||
await paymentsPage.waitForPageLoad();
|
||||
await paymentsPage.selectCompany('Compania Test 1');
|
||||
await page.waitForSelector(paymentsPage.paymentsTable);
|
||||
|
||||
// Switch to summary view if available
|
||||
if (await paymentsPage.isSummaryViewAvailable()) {
|
||||
await paymentsPage.switchToSummaryView();
|
||||
await paymentsPage.waitForLoadingToFinish();
|
||||
|
||||
expect(await paymentsPage.isSummaryViewVisible()).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
364
reports-app/frontend/tests/e2e/real-world-comprehensive.spec.js
Normal file
364
reports-app/frontend/tests/e2e/real-world-comprehensive.spec.js
Normal file
@@ -0,0 +1,364 @@
|
||||
//! 🌍 COMPREHENSIVE REAL-WORLD TESTING SUITE
|
||||
//! Created: 2025-08-04
|
||||
//! Purpose: Test complete application flows with real data and interactions
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../page-objects/DashboardPage.js';
|
||||
|
||||
test.describe('🌍 ROA2WEB Real-World Comprehensive Testing', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
let performanceMetrics = [];
|
||||
let networkErrors = [];
|
||||
let consoleErrors = [];
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Reset metrics
|
||||
performanceMetrics = [];
|
||||
networkErrors = [];
|
||||
consoleErrors = [];
|
||||
|
||||
// Setup comprehensive monitoring
|
||||
page.on('response', response => {
|
||||
const timing = {
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
timing: response.request().timing(),
|
||||
size: response.headers()['content-length'] || 0,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
performanceMetrics.push(timing);
|
||||
|
||||
if (response.status() >= 400) {
|
||||
networkErrors.push({
|
||||
url: response.url(),
|
||||
status: response.status(),
|
||||
statusText: response.statusText()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push({
|
||||
text: msg.text(),
|
||||
location: msg.location(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
});
|
||||
|
||||
test('🎯 COMPLETE USER JOURNEY - Login to Dashboard to Reports', async ({ page }) => {
|
||||
console.log('\n🌍 === COMPLETE USER JOURNEY TEST ===');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Phase 1: Navigate to Login
|
||||
console.log('\n📍 Phase 1: Navigate to Login');
|
||||
await loginPage.navigate();
|
||||
await page.screenshot({ path: 'journey-01-login-page.png' });
|
||||
|
||||
// Verify login page loads correctly
|
||||
await expect(page).toHaveTitle(/ROA Reports/);
|
||||
console.log('✅ Login page loaded');
|
||||
|
||||
// Phase 2: Attempt Authentication with Real Credentials
|
||||
console.log('\n📍 Phase 2: Authentication Flow');
|
||||
|
||||
// Test with test credentials first
|
||||
await page.fill('#username', 'MARIUS M');
|
||||
await page.fill('#password input', 'PAROLA9911');
|
||||
|
||||
// Verify button becomes enabled
|
||||
await page.waitForTimeout(200);
|
||||
const buttonEnabled = !(await page.locator('button[type="submit"]').isDisabled());
|
||||
expect(buttonEnabled).toBe(true);
|
||||
console.log('✅ Login button enabled with credentials');
|
||||
|
||||
// Monitor the authentication request
|
||||
const authPromise = page.waitForResponse('**/auth/login').catch(() => null);
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
const authResponse = await authPromise;
|
||||
if (authResponse) {
|
||||
console.log(`📊 Auth Response: ${authResponse.status()}`);
|
||||
|
||||
if (authResponse.status() === 200) {
|
||||
console.log('✅ Authentication successful');
|
||||
|
||||
// Wait for redirect to dashboard
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 }).catch(() => {
|
||||
console.log('⚠️ No redirect to dashboard - checking current state');
|
||||
});
|
||||
|
||||
} else if (authResponse.status() === 422) {
|
||||
console.log('❌ Validation error - checking response');
|
||||
const responseBody = await authResponse.text();
|
||||
console.log('Response body:', responseBody);
|
||||
|
||||
} else if (authResponse.status() === 401) {
|
||||
console.log('❌ Authentication failed - invalid credentials');
|
||||
|
||||
} else {
|
||||
console.log(`❌ Unexpected response: ${authResponse.status()}`);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ No authentication response received');
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'journey-02-after-auth.png' });
|
||||
|
||||
// Phase 3: Dashboard Interaction (if successful)
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 Current URL: ${currentUrl}`);
|
||||
|
||||
if (currentUrl.includes('/dashboard')) {
|
||||
console.log('\n📍 Phase 3: Dashboard Interaction');
|
||||
|
||||
// Wait for dashboard to load
|
||||
await page.waitForSelector('.dashboard-container', { timeout: 5000 }).catch(() => {
|
||||
console.log('⚠️ Dashboard container not found');
|
||||
});
|
||||
|
||||
// Test dashboard functionality
|
||||
const companySelector = page.locator('select, .p-dropdown');
|
||||
if (await companySelector.first().isVisible()) {
|
||||
console.log('✅ Company selector visible');
|
||||
|
||||
// Try to select a company
|
||||
await companySelector.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const options = page.locator('.p-dropdown-item');
|
||||
const optionCount = await options.count();
|
||||
|
||||
if (optionCount > 0) {
|
||||
await options.first().click();
|
||||
console.log('✅ Company selected');
|
||||
|
||||
// Wait for data to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check if statistics are displayed
|
||||
const statsCards = page.locator('.stat-card, .dashboard-stat, .metric-card');
|
||||
const statsCount = await statsCards.count();
|
||||
console.log(`📊 Statistics cards found: ${statsCount}`);
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'journey-03-dashboard.png' });
|
||||
|
||||
// Phase 4: Navigation Test
|
||||
console.log('\n📍 Phase 4: Navigation Test');
|
||||
|
||||
const navLinks = page.locator('nav a, .nav-link, .menu-item');
|
||||
const navCount = await navLinks.count();
|
||||
console.log(`🧭 Navigation links found: ${navCount}`);
|
||||
|
||||
if (navCount > 0) {
|
||||
// Try to navigate to invoices
|
||||
const invoicesLink = page.locator('text=/facturi|invoice/i').first();
|
||||
if (await invoicesLink.isVisible()) {
|
||||
await invoicesLink.click();
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('✅ Navigated to invoices');
|
||||
await page.screenshot({ path: 'journey-04-invoices.png' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
console.log(`\n⏱️ Total journey time: ${totalTime}ms`);
|
||||
});
|
||||
|
||||
test('🔍 NETWORK PERFORMANCE ANALYSIS', async ({ page }) => {
|
||||
console.log('\n🔍 === NETWORK PERFORMANCE ANALYSIS ===');
|
||||
|
||||
// Navigate and monitor performance
|
||||
await loginPage.navigate();
|
||||
|
||||
// Wait for all initial requests to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Analyze performance metrics
|
||||
const slowRequests = performanceMetrics.filter(metric => {
|
||||
const timing = metric.timing;
|
||||
return timing && (timing.responseEnd - timing.requestStart) > 2000;
|
||||
});
|
||||
|
||||
const failedRequests = performanceMetrics.filter(metric => metric.status >= 400);
|
||||
|
||||
console.log(`📊 Total requests: ${performanceMetrics.length}`);
|
||||
console.log(`🐌 Slow requests (>2s): ${slowRequests.length}`);
|
||||
console.log(`❌ Failed requests: ${failedRequests.length}`);
|
||||
|
||||
if (slowRequests.length > 0) {
|
||||
console.log('\n🐌 Slow requests:');
|
||||
slowRequests.forEach(request => {
|
||||
const duration = request.timing ?
|
||||
(request.timing.responseEnd - request.timing.requestStart) : 'unknown';
|
||||
console.log(` - ${request.url}: ${duration}ms`);
|
||||
});
|
||||
}
|
||||
|
||||
if (failedRequests.length > 0) {
|
||||
console.log('\n❌ Failed requests:');
|
||||
failedRequests.forEach(request => {
|
||||
console.log(` - ${request.status} ${request.url}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance assertions
|
||||
expect(slowRequests.length).toBeLessThan(5); // Max 5 slow requests
|
||||
expect(failedRequests.length).toBeLessThan(3); // Max 3 failed requests
|
||||
});
|
||||
|
||||
test('🧪 ERROR HANDLING STRESS TEST', async ({ page }) => {
|
||||
console.log('\n🧪 === ERROR HANDLING STRESS TEST ===');
|
||||
|
||||
await loginPage.navigate();
|
||||
|
||||
// Test various error scenarios
|
||||
const errorScenarios = [
|
||||
{
|
||||
name: 'Server Error 500',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 500, body: '{"detail": "Internal server error"}' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Network Timeout',
|
||||
setup: () => page.route('**/auth/login', route => route.abort('timeout'))
|
||||
},
|
||||
{
|
||||
name: 'Invalid JSON Response',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 200, body: 'invalid json' })
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'Rate Limiting 429',
|
||||
setup: () => page.route('**/auth/login', route =>
|
||||
route.fulfill({ status: 429, body: '{"detail": "Too many requests"}' })
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
for (const scenario of errorScenarios) {
|
||||
console.log(`\n--- Testing: ${scenario.name} ---`);
|
||||
|
||||
// Setup error scenario
|
||||
await scenario.setup();
|
||||
|
||||
// Fill credentials and submit
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
// Wait for error handling
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Check if error is handled gracefully
|
||||
const errorMessage = await page.locator('.error-message, .p-toast-error').isVisible();
|
||||
console.log(`Error message shown: ${errorMessage}`);
|
||||
|
||||
// Verify user is still on login page
|
||||
const isOnLogin = await loginPage.isOnLoginPage();
|
||||
console.log(`Still on login page: ${isOnLogin}`);
|
||||
|
||||
expect(isOnLogin).toBe(true);
|
||||
|
||||
// Clear the route override
|
||||
await page.unroute('**/auth/login');
|
||||
|
||||
// Clear form for next test
|
||||
await page.fill('#username', '');
|
||||
await page.fill('#password input', '');
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
});
|
||||
|
||||
test('📱 RESPONSIVE DESIGN VALIDATION', async ({ page }) => {
|
||||
console.log('\n📱 === RESPONSIVE DESIGN VALIDATION ===');
|
||||
|
||||
const viewports = [
|
||||
{ name: 'Mobile Portrait', width: 375, height: 667 },
|
||||
{ name: 'Mobile Landscape', width: 667, height: 375 },
|
||||
{ name: 'Tablet', width: 768, height: 1024 },
|
||||
{ name: 'Desktop', width: 1920, height: 1080 }
|
||||
];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
console.log(`\n--- Testing: ${viewport.name} (${viewport.width}x${viewport.height}) ---`);
|
||||
|
||||
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
||||
await loginPage.navigate();
|
||||
|
||||
// Wait for layout to adjust
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check if login form is visible and accessible
|
||||
const formVisible = await page.locator('.login-form').isVisible();
|
||||
const buttonVisible = await page.locator('button[type="submit"]').isVisible();
|
||||
|
||||
console.log(`Form visible: ${formVisible}`);
|
||||
console.log(`Button visible: ${buttonVisible}`);
|
||||
|
||||
expect(formVisible).toBe(true);
|
||||
expect(buttonVisible).toBe(true);
|
||||
|
||||
// Test form interaction
|
||||
await page.fill('#username', 'test');
|
||||
await page.fill('#password input', 'test');
|
||||
|
||||
const buttonEnabled = !(await page.locator('button[type="submit"]').isDisabled());
|
||||
expect(buttonEnabled).toBe(true);
|
||||
|
||||
// Take screenshot for visual verification
|
||||
await page.screenshot({
|
||||
path: `responsive-${viewport.name.toLowerCase().replace(' ', '-')}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
// Generate comprehensive test report
|
||||
console.log('\n📋 === COMPREHENSIVE TEST REPORT ===');
|
||||
console.log(`🌐 Total Network Requests: ${performanceMetrics.length}`);
|
||||
console.log(`❌ Network Errors: ${networkErrors.length}`);
|
||||
console.log(`🔥 Console Errors: ${consoleErrors.length}`);
|
||||
|
||||
if (networkErrors.length > 0) {
|
||||
console.log('\n❌ Network Errors:');
|
||||
networkErrors.forEach(error => {
|
||||
console.log(` - ${error.status} ${error.url}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('\n🔥 Console Errors:');
|
||||
consoleErrors.forEach(error => {
|
||||
console.log(` - ${error.text}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance summary
|
||||
const avgResponseTime = performanceMetrics.length > 0 ?
|
||||
performanceMetrics.reduce((sum, metric) => {
|
||||
const timing = metric.timing;
|
||||
return sum + (timing ? (timing.responseEnd - timing.requestStart) : 0);
|
||||
}, 0) / performanceMetrics.length : 0;
|
||||
|
||||
console.log(`⚡ Average Response Time: ${Math.round(avgResponseTime)}ms`);
|
||||
|
||||
if (avgResponseTime > 1000) {
|
||||
console.log('⚠️ Performance Warning: Average response time is high');
|
||||
}
|
||||
});
|
||||
});
|
||||
237
reports-app/frontend/tests/e2e/responsive/breakpoints.spec.js
Normal file
237
reports-app/frontend/tests/e2e/responsive/breakpoints.spec.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from '../../page-objects/LoginPage.js';
|
||||
import { DashboardPage } from '../../page-objects/DashboardPage.js';
|
||||
import { testCredentials } from '../../fixtures/auth.js';
|
||||
|
||||
test.describe('Responsive Design Tests', () => {
|
||||
let loginPage;
|
||||
let dashboardPage;
|
||||
|
||||
// Common setup for all responsive tests
|
||||
const setupMockAuth = async (page) => {
|
||||
await page.route('**/api/auth/login', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
access_token: 'mock_access_token',
|
||||
refresh_token: 'mock_refresh_token',
|
||||
user: { id: 1, username: 'testuser', full_name: 'Test User' }
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/companies', async route => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify([
|
||||
{ code: 'COMP1', name: 'Test Company' }
|
||||
]),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test.describe('Mobile Layout (320px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 568 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display login form correctly on mobile', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
// Take screenshot for visual verification
|
||||
await page.screenshot({ path: 'mobile-login.png', fullPage: true });
|
||||
|
||||
// Login form should be visible and properly sized
|
||||
await expect(page.locator(loginPage.loginCard)).toBeVisible();
|
||||
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
|
||||
await expect(page.locator(loginPage.passwordInput)).toBeVisible();
|
||||
|
||||
// Check that form takes appropriate width
|
||||
const cardWidth = await page.locator(loginPage.loginCard).boundingBox();
|
||||
expect(cardWidth.width).toBeLessThan(320); // Should fit in viewport
|
||||
});
|
||||
|
||||
test('should adapt dashboard layout for mobile', async ({ page }) => {
|
||||
// Login and navigate to dashboard
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
// Take screenshot
|
||||
await page.screenshot({ path: 'mobile-dashboard.png', fullPage: true });
|
||||
|
||||
// Dashboard should be responsive
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
|
||||
// Stats grid should stack vertically on mobile
|
||||
const statsGrid = page.locator(dashboardPage.statsGrid);
|
||||
if (await statsGrid.isVisible()) {
|
||||
const gridBox = await statsGrid.boundingBox();
|
||||
expect(gridBox.width).toBeLessThan(320);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tablet Layout (768px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display login form appropriately on tablet', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
await page.screenshot({ path: 'tablet-login.png', fullPage: true });
|
||||
|
||||
// Login card should be centered and well-proportioned
|
||||
const loginCard = page.locator(loginPage.loginCard);
|
||||
await expect(loginCard).toBeVisible();
|
||||
|
||||
const cardBox = await loginCard.boundingBox();
|
||||
expect(cardBox.width).toBeGreaterThan(300);
|
||||
expect(cardBox.width).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('should show proper tablet dashboard layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'tablet-dashboard.png', fullPage: true });
|
||||
|
||||
// Dashboard elements should be properly spaced
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
|
||||
// Stats should be arranged in appropriate grid
|
||||
const statsCards = page.locator('.stat-card');
|
||||
const cardCount = await statsCards.count();
|
||||
if (cardCount > 0) {
|
||||
// Cards should be visible and properly sized
|
||||
for (let i = 0; i < cardCount; i++) {
|
||||
await expect(statsCards.nth(i)).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Desktop Layout (1024px+)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 768 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should display full desktop login layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
|
||||
await page.screenshot({ path: 'desktop-login.png', fullPage: true });
|
||||
|
||||
// Login should be centered with appropriate sizing
|
||||
const loginCard = page.locator(loginPage.loginCard);
|
||||
await expect(loginCard).toBeVisible();
|
||||
|
||||
// Card should not take full width on desktop
|
||||
const cardBox = await loginCard.boundingBox();
|
||||
expect(cardBox.width).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('should show complete desktop dashboard layout', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'desktop-dashboard.png', fullPage: true });
|
||||
|
||||
// All dashboard elements should be visible
|
||||
await expect(page.locator(dashboardPage.pageTitle)).toBeVisible();
|
||||
await expect(page.locator(dashboardPage.pageSubtitle)).toBeVisible();
|
||||
|
||||
// Stats grid should use horizontal layout
|
||||
const statsGrid = page.locator(dashboardPage.statsGrid);
|
||||
if (await statsGrid.isVisible()) {
|
||||
const gridBox = await statsGrid.boundingBox();
|
||||
expect(gridBox.width).toBeGreaterThan(600);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Wide Screen Layout (1920px)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
loginPage = new LoginPage(page);
|
||||
dashboardPage = new DashboardPage(page);
|
||||
await setupMockAuth(page);
|
||||
});
|
||||
|
||||
test('should handle wide screen layouts appropriately', async ({ page }) => {
|
||||
await loginPage.navigate();
|
||||
await loginPage.login(testCredentials.valid.username, testCredentials.valid.password);
|
||||
await page.waitForURL('/dashboard');
|
||||
|
||||
await page.screenshot({ path: 'widescreen-dashboard.png', fullPage: true });
|
||||
|
||||
// Content should not stretch too wide
|
||||
const mainContent = page.locator('.dashboard-content');
|
||||
if (await mainContent.isVisible()) {
|
||||
const contentBox = await mainContent.boundingBox();
|
||||
// Content should have reasonable max-width
|
||||
expect(contentBox.width).toBeLessThan(1600);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Orientation Changes', () => {
|
||||
test('should handle portrait to landscape orientation', async ({ page }) => {
|
||||
// Start in mobile portrait
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
loginPage = new LoginPage(page);
|
||||
await setupMockAuth(page);
|
||||
|
||||
await loginPage.navigate();
|
||||
await page.screenshot({ path: 'mobile-portrait.png' });
|
||||
|
||||
// Rotate to landscape
|
||||
await page.setViewportSize({ width: 667, height: 375 });
|
||||
await page.waitForTimeout(500); // Allow for reflow
|
||||
|
||||
await page.screenshot({ path: 'mobile-landscape.png' });
|
||||
|
||||
// Login form should still be usable
|
||||
await expect(page.locator(loginPage.loginCard)).toBeVisible();
|
||||
await expect(page.locator(loginPage.usernameInput)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Touch Interactions', () => {
|
||||
test('should handle touch interactions on mobile', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
loginPage = new LoginPage(page);
|
||||
await setupMockAuth(page);
|
||||
|
||||
await loginPage.navigate();
|
||||
|
||||
// Test touch interactions
|
||||
await page.tap(loginPage.usernameInput);
|
||||
await page.fill(loginPage.usernameInput, 'testuser');
|
||||
|
||||
await page.tap(loginPage.passwordInput);
|
||||
await page.fill(loginPage.passwordInput, 'testpass');
|
||||
|
||||
// Login button should be tappable
|
||||
const loginButton = page.locator(loginPage.loginButton);
|
||||
await expect(loginButton).toBeEnabled();
|
||||
|
||||
// Button should have appropriate touch target size (minimum 44px)
|
||||
const buttonBox = await loginButton.boundingBox();
|
||||
expect(buttonBox.height).toBeGreaterThanOrEqual(44);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user