import { test, expect } from '@playwright/test' // AZ-477 — e2e companion for Settings save resilience + 2 s deadline. // // AC-1 (FT-N-13 / NFT-RES-05): real backend returns 500 on settings PUT; // SPA renders an error region AND clears the // `saving` flag (button enabled again) within // 2 s. Today both contracts are drift — // `test.fail()` until try/finally + alert lands. // AC-2 (FT-N-14 / NFT-RES-06): same on a network drop. The fast-profile // test pins both contracts against MSW; this // companion exercises the real wire boundary // against the suite stack. // AC-3 (NFT-PERF-09): ≤ 2 s deadline for error visibility — pinned // in the fast suite via `performance.now()`; // the e2e companion just asserts visibility // within Playwright's 2 s timeout. // // Requires the suite docker-compose stack (`e2e/docker-compose.suite-e2e.yml`). // Uses `page.route` to inject the failure mode without depending on a real // crashed backend in CI. test.describe('AZ-477 — Settings save resilience (e2e companion)', () => { test.fail( 'AC-1 (500) — Save button re-enables AND error region visible within 2 s', async ({ page }) => { // Force the system-settings PUT to fail with a 500. Other endpoints // pass through so the page mounts normally. await page.route('**/api/annotations/settings/system', async (route) => { if (route.request().method() === 'PUT') { await route.fulfill({ status: 500, body: 'upstream failure' }) return } await route.continue() }) await page.goto('/settings') // Tenant Configuration heading + scoped Save button — same anchor as // the fast suite. If the suite seed has no tenant config, the test // reports the gap rather than masking the UI. const tenantHeading = page.getByRole('heading', { name: /Tenant Configuration/i }) if (!(await tenantHeading.isVisible({ timeout: 5000 }).catch(() => false))) { test.skip(true, 'Suite UI did not render Settings → Tenant Configuration') } const tenantPanel = tenantHeading.locator('xpath=..') const saveBtn = tenantPanel.getByRole('button', { name: /^Save$/i }) await saveBtn.click() // Both assertions race the 2 s deadline. await expect(saveBtn).toBeEnabled({ timeout: 2000 }) const alertEl = page.getByRole('alert').first() await expect(alertEl).toBeVisible({ timeout: 2000 }) await expect(alertEl).toContainText(/error|failed|try again|500/i) }, ) test.fail( 'AC-2 (network drop) — Save button re-enables AND error region visible within 2 s', async ({ page }) => { await page.route('**/api/annotations/settings/system', async (route) => { if (route.request().method() === 'PUT') { await route.abort('connectionfailed') return } await route.continue() }) await page.goto('/settings') const tenantHeading = page.getByRole('heading', { name: /Tenant Configuration/i }) if (!(await tenantHeading.isVisible({ timeout: 5000 }).catch(() => false))) { test.skip(true, 'Suite UI did not render Settings → Tenant Configuration') } const tenantPanel = tenantHeading.locator('xpath=..') const saveBtn = tenantPanel.getByRole('button', { name: /^Save$/i }) await saveBtn.click() await expect(saveBtn).toBeEnabled({ timeout: 2000 }) const alertEl = page.getByRole('alert').first() await expect(alertEl).toBeVisible({ timeout: 2000 }) }, ) })