[AZ-463] [AZ-469] [AZ-476] [AZ-477] Batch 6 - flight/responsive/upload/settings tests

- AZ-463 flight selection persistence (FT-P-16) + rehydration
  on boot (FT-P-17) PASS at the wire; 100-cycle leak guard
  (NFT-RES-LIM-07) and 1h SSE soak (NFT-RES-LIM-06)
  scaffolded as RUN_LONG_RUNNING-gated e2e companions.
- AZ-469 browser-support smoke (FT-P-34) runs in both
  Chromium and Firefox via the existing playwright config;
  responsive variants (FT-P-35 480px / FT-P-36 1024px) PASS
  in fast (Tailwind class shape) and e2e (visibility).
- AZ-476 upload 501 MB -> 413: AC-1 user-visible error is
  drift today (uploadFiles silently falls through to local
  mode); it.fails() + control + e2e test.fail. AC-2 no-alert
  PASS via dialog spy.
- AZ-477 settings save 500 / network drop: AC-1+AC-2+AC-3
  all drift today (no try/finally, no error region, deadline
  unmeasurable); 4 it.fails() + control pinning the stuck-
  disabled drift; e2e companions test.fail mirror it.
- LESSONS.md seeded: vi.stubGlobal('URL', {...URL,...})
  destroys the URL constructor and breaks new URL(...) in
  MSW; patch the methods directly instead.

Code review: PASS (0 findings). Fast: 22/22 files, 120
passed / 13 skipped. Static: 24/24 PASS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 05:19:35 +03:00
parent 6d03643c2c
commit bd2b718ddf
16 changed files with 1627 additions and 6 deletions
+86
View File
@@ -0,0 +1,86 @@
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 })
},
)
})