Optimize PDF export layout with compact columns and more space for partner names. Add accounting period display to invoices matching Trial Balance format. Fix date filtering to use local timezone instead of UTC. Update invoice ordering to chronological sequence (DATAACT, NRACT, NUME). **Backend changes:** - Add accounting period query from calendar table - Add currency (valuta) and cont filter support - Change invoice ordering to chronological (DATAACT ASC, NRACT ASC, NUME) - Add accounting_period field to InvoiceListResponse model **Frontend changes:** - Optimize PDF column widths (37% for partner names, compact numeric columns) - Add custom column width support in exportUtils - Fix date conversion from UTC to local timezone (prevents day shift) - Add accounting period display in PDF exports - Enhance E2E test coverage **Cleanup:** - Remove obsolete Trial Balance feature documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
444 lines
15 KiB
JavaScript
444 lines
15 KiB
JavaScript
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 - FIX: Use query parameters instead of path parameter
|
|
await page.route('**/api/invoices**', async route => {
|
|
const url = route.request().url();
|
|
const urlParams = new URL(url).searchParams;
|
|
const partnerType = urlParams.get('partner_type') || 'CLIENTI';
|
|
|
|
// Return different data based on partner_type
|
|
const invoicesData = partnerType === 'CLIENTI'
|
|
? mockInvoices.filter(inv => inv.type === 'client')
|
|
: mockInvoices.filter(inv => inv.type === 'supplier');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
invoices: invoicesData,
|
|
total_count: invoicesData.length,
|
|
filtered_count: invoicesData.length,
|
|
total_amount: invoicesData.reduce((sum, inv) => sum + inv.totctva, 0),
|
|
page: parseInt(urlParams.get('page') || '1'),
|
|
page_size: parseInt(urlParams.get('page_size') || '50'),
|
|
has_more: false
|
|
}),
|
|
});
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
|
|
// NEW TESTS for fixed issues
|
|
|
|
test('should filter by invoice type (CLIENTI/FURNIZORI)', async ({ page }) => {
|
|
let capturedPartnerType = null;
|
|
|
|
// Intercept API requests to verify partner_type parameter
|
|
await page.route('**/api/invoices**', async route => {
|
|
const url = route.request().url();
|
|
const urlParams = new URL(url).searchParams;
|
|
capturedPartnerType = urlParams.get('partner_type');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
invoices: [],
|
|
total_count: 0,
|
|
filtered_count: 0,
|
|
total_amount: 0,
|
|
page: 1,
|
|
page_size: 50,
|
|
has_more: false
|
|
}),
|
|
});
|
|
});
|
|
|
|
await invoicesPage.waitForPageLoad();
|
|
await invoicesPage.selectCompany('Compania Test 1');
|
|
await page.waitForSelector(invoicesPage.invoicesTable);
|
|
|
|
// Select FURNIZORI from dropdown
|
|
await page.locator('[placeholder="Tip factură"]').click();
|
|
await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click();
|
|
await page.waitForTimeout(1000); // Wait for API call
|
|
|
|
// Verify partner_type parameter was sent correctly
|
|
expect(capturedPartnerType).toBe('FURNIZORI');
|
|
});
|
|
|
|
test('should filter by cont (account number)', async ({ page }) => {
|
|
let capturedCont = null;
|
|
|
|
// Intercept API requests to verify cont parameter
|
|
await page.route('**/api/invoices**', async route => {
|
|
const url = route.request().url();
|
|
const urlParams = new URL(url).searchParams;
|
|
capturedCont = urlParams.get('cont');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
invoices: [],
|
|
total_count: 0,
|
|
filtered_count: 0,
|
|
total_amount: 0,
|
|
page: 1,
|
|
page_size: 50,
|
|
has_more: false
|
|
}),
|
|
});
|
|
});
|
|
|
|
await invoicesPage.waitForPageLoad();
|
|
await invoicesPage.selectCompany('Compania Test 1');
|
|
await page.waitForSelector(invoicesPage.invoicesTable);
|
|
|
|
// Enter cont filter
|
|
await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111');
|
|
await page.waitForTimeout(1000); // Wait for debounced API call
|
|
|
|
// Verify cont parameter was sent correctly
|
|
expect(capturedCont).toBe('4111');
|
|
});
|
|
|
|
test('should use partner_name parameter for search', async ({ page }) => {
|
|
let capturedPartnerName = null;
|
|
let capturedSearchParam = null;
|
|
|
|
// Intercept API requests to verify correct parameter name
|
|
await page.route('**/api/invoices**', async route => {
|
|
const url = route.request().url();
|
|
const urlParams = new URL(url).searchParams;
|
|
capturedPartnerName = urlParams.get('partner_name');
|
|
capturedSearchParam = urlParams.get('search');
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
invoices: [],
|
|
total_count: 0,
|
|
filtered_count: 0,
|
|
total_amount: 0,
|
|
page: 1,
|
|
page_size: 50,
|
|
has_more: false
|
|
}),
|
|
});
|
|
});
|
|
|
|
await invoicesPage.waitForPageLoad();
|
|
await invoicesPage.selectCompany('Compania Test 1');
|
|
await page.waitForSelector(invoicesPage.invoicesTable);
|
|
|
|
// Search for partner name
|
|
await page.locator('[placeholder="Căutați după număr, partener..."]').fill('Test Partner');
|
|
await page.waitForTimeout(1000); // Wait for debounced API call
|
|
|
|
// Verify partner_name parameter was sent (not search)
|
|
expect(capturedPartnerName).toBe('Test Partner');
|
|
expect(capturedSearchParam).toBeNull();
|
|
});
|
|
|
|
test('should export XLSX with all filters applied', async ({ page }) => {
|
|
let exportRequestParams = null;
|
|
|
|
// Intercept export API request
|
|
await page.route('**/api/invoices**', async route => {
|
|
const url = route.request().url();
|
|
const urlParams = new URL(url).searchParams;
|
|
|
|
// Capture params if it's the export request (page_size = 999999)
|
|
if (urlParams.get('page_size') === '999999') {
|
|
exportRequestParams = {
|
|
partner_type: urlParams.get('partner_type'),
|
|
partner_name: urlParams.get('partner_name'),
|
|
cont: urlParams.get('cont'),
|
|
page_size: urlParams.get('page_size')
|
|
};
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
invoices: [
|
|
{
|
|
cont: '4111',
|
|
nract: 'INV001',
|
|
dataact: '2024-01-01',
|
|
datascad: '2024-02-01',
|
|
nume: 'Test Client',
|
|
totctva: 1000,
|
|
achitat: 500,
|
|
soldfinal: 500
|
|
}
|
|
],
|
|
total_count: 1,
|
|
filtered_count: 1,
|
|
total_amount: 1000,
|
|
page: 1,
|
|
page_size: 999999,
|
|
has_more: false
|
|
}),
|
|
});
|
|
});
|
|
|
|
await invoicesPage.waitForPageLoad();
|
|
await invoicesPage.selectCompany('Compania Test 1');
|
|
await page.waitForSelector(invoicesPage.invoicesTable);
|
|
|
|
// Apply filters before export
|
|
await page.locator('[placeholder="Tip factură"]').click();
|
|
await page.locator('.p-dropdown-item').filter({ hasText: 'Furnizori' }).click();
|
|
await page.locator('[placeholder="Filtru cont (ex: 4111)"]').fill('4111');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Click Excel export
|
|
const downloadPromise = page.waitForEvent('download', { timeout: 10000 }).catch(() => null);
|
|
await page.locator('button:has-text("Export Excel")').click();
|
|
await page.waitForTimeout(2000); // Wait for export to complete
|
|
|
|
// Verify export request included all filters
|
|
expect(exportRequestParams).toBeTruthy();
|
|
expect(exportRequestParams.partner_type).toBe('FURNIZORI');
|
|
expect(exportRequestParams.cont).toBe('4111');
|
|
expect(exportRequestParams.page_size).toBe('999999');
|
|
|
|
// Download may or may not occur due to mock, but we verified the API call
|
|
await downloadPromise;
|
|
});
|
|
|
|
test('should have hover effect on table rows', async ({ page }) => {
|
|
await invoicesPage.waitForPageLoad();
|
|
await invoicesPage.selectCompany('Compania Test 1');
|
|
await page.waitForSelector(invoicesPage.invoicesTable);
|
|
|
|
// Wait for table rows to load
|
|
const firstRow = page.locator('.p-datatable-tbody tr').first();
|
|
await firstRow.waitFor();
|
|
|
|
// Get initial background color
|
|
const initialBgColor = await firstRow.evaluate(el =>
|
|
window.getComputedStyle(el).backgroundColor
|
|
);
|
|
|
|
// Hover over the row
|
|
await firstRow.hover();
|
|
await page.waitForTimeout(300); // Wait for transition
|
|
|
|
// Get background color after hover
|
|
const hoverBgColor = await firstRow.evaluate(el =>
|
|
window.getComputedStyle(el).backgroundColor
|
|
);
|
|
|
|
// Background color should change on hover
|
|
expect(hoverBgColor).not.toBe(initialBgColor);
|
|
|
|
// Verify hover color is the expected blue (#e3f2fd = rgb(227, 242, 253))
|
|
expect(hoverBgColor).toBe('rgb(227, 242, 253)');
|
|
});
|
|
}); |