/** * Translations module tests for cPad. * * Routes under /cp/translation/{file} * The most common translation file is "app". * * Coverage: * • Main translation index page loads * • Translation list page for "app" file loads * • Add translation entry page loads * • Translation grid page loads * • No console/server errors on any page * • (Optional) inline edit via CRUD helper * * Run: * npx playwright test tests/cpad/modules/translations.spec.ts --project=cpad */ import { test, expect } from '@playwright/test'; import { CP_PATH, attachErrorListeners } from '../../helpers/auth'; /** Translation file slugs to probe — add more as needed */ const TRANSLATION_FILES = ['app'] as const; /** Reusable page-health assertion */ async function assertPageHealthy(page: import('@playwright/test').Page, path: string) { const consoleErrors: string[] = []; const networkErrors: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') consoleErrors.push(msg.text()); }); page.on('pageerror', (err) => consoleErrors.push(err.message)); page.on('response', (res) => { if (res.status() >= 500) networkErrors.push(`HTTP ${res.status()}: ${res.url()}`); }); await page.goto(path); await page.waitForLoadState('networkidle', { timeout: 20_000 }); expect(page.url(), `${path} must not redirect to login`).not.toContain('/login'); await expect(page.locator('body')).toBeVisible(); const bodyText = await page.locator('body').textContent() ?? ''; const hasError = /Whoops|Server Error|SQLSTATE|Call to undefined/.test(bodyText); expect(hasError, `Server error at ${path}`).toBe(false); expect(consoleErrors.length, `Console errors at ${path}: ${consoleErrors.join(' | ')}`).toBe(0); expect(networkErrors.length, `HTTP 5xx at ${path}: ${networkErrors.join(' | ')}`).toBe(0); } test.describe('cPad Translations Module', () => { for (const file of TRANSLATION_FILES) { const base = `${CP_PATH}/translation/${file}`; test(`translation main page (${file}) loads`, async ({ page }) => { await assertPageHealthy(page, base); }); test(`translation list page (${file}) loads`, async ({ page }) => { await assertPageHealthy(page, `${base}/list`); }); test(`translation add page (${file}) loads`, async ({ page }) => { await assertPageHealthy(page, `${base}/add`); }); test(`translation grid page (${file}) loads`, async ({ page }) => { await assertPageHealthy(page, `${base}/grid`); }); test(`translation list (${file}) renders content`, async ({ page }) => { await page.goto(`${base}/list`); await page.waitForLoadState('networkidle'); if (page.url().includes('/login')) { test.skip(true, 'Not authenticated'); return; } // A table, a grid element, or an empty-state message should be present const tableCount = await page.locator('table, .grid, [class*="grid"]').count(); const emptyMsgCount = await page.locator( ':has-text("No translations"), :has-text("No records"), :has-text("Empty")', ).count(); const inputCount = await page.locator('input[type=text], textarea').count(); expect( tableCount + emptyMsgCount + inputCount, 'Translation list should contain table, grid, empty state, or editable fields', ).toBeGreaterThan(0); }); test(`translation add page (${file}) contains a form`, async ({ page }) => { await page.goto(`${base}/add`); await page.waitForLoadState('networkidle'); if (page.url().includes('/login')) { test.skip(true, 'Not authenticated'); return; } const formCount = await page.locator('form').count(); const inputCount = await page.locator('input:not([type=hidden]), textarea').count(); expect( formCount + inputCount, 'Add translation page should contain a form or input fields', ).toBeGreaterThan(0); }); } });