[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
+126
View File
@@ -0,0 +1,126 @@
import { test, expect } from '@playwright/test'
// AZ-476 — e2e companion for the 500 MB upload cap.
//
// AC-1 (FT-N-06 / NFT-RES-07): nginx returns 413 on a 501 MB upload; SPA
// renders an in-DOM error region carrying an
// i18n-keyed message. Today production silently
// swallows the failure and falls through to
// local-mode (drift) — `test.fail()` until the
// error region is wired.
// AC-2 (no alert): The 413 path does NOT invoke `alert()`. Today
// this passes vacuously (no error path runs at
// all). The fast-profile test pins both contracts
// against MSW; this e2e companion exercises the
// real nginx body-size limit set in the suite.
//
// Requires the suite docker-compose stack (`e2e/docker-compose.suite-e2e.yml`).
// Skips with a clear reason on developer hosts without the stack.
const BIG_BYTES = 501 * 1024 * 1024
function buildOversizedBuffer(): Buffer {
// Spec requires a 501 MB sparse zero-filled payload. Buffer.alloc is the
// cheapest in-memory way to build it; 501 MB sits well below the 1 GB
// Node default heap on CI runners. If a future runner downsizes its heap,
// switch this fixture to a temp file produced by `dd`.
return Buffer.alloc(BIG_BYTES)
}
test.describe('AZ-476 — upload 501 MB → 413 (e2e companion)', () => {
test.fail(
'AC-1 (FT-N-06 / NFT-RES-07) — 501 MB upload surfaces in-DOM error',
async ({ page }) => {
// Capture every batch upload response so we can verify nginx really
// returned 413 (and not the SPA short-circuiting on the client side).
const batchResponses: { url: string; status: number }[] = []
page.on('response', async (resp) => {
const u = resp.url()
if (/\/api\/annotations\/media\/batch(\?|$)/.test(u)) {
batchResponses.push({ url: u, status: resp.status() })
}
})
await page.goto('/annotations')
// The "Open File" input is hidden behind a label; Playwright's
// setInputFiles works directly on the input element regardless of CSS
// visibility.
const fileInput = page.locator('input[type="file"]').nth(1)
if (!(await fileInput.count())) {
test.skip(true, 'Suite UI did not render the upload input')
}
await fileInput.setInputFiles({
name: 'huge_recon_video.mp4',
mimeType: 'video/mp4',
buffer: buildOversizedBuffer(),
})
// Wait for the 413 to come back. If nginx in this stack is configured
// with a different cap, the test reports the configuration mismatch
// explicitly rather than masking the contract.
await page
.waitForFunction(
(checkUrl) => {
type Win = Window & { __batchStatuses?: number[] }
const w = window as Win
void checkUrl
return Array.isArray(w.__batchStatuses) && w.__batchStatuses.includes(413)
},
'noop',
{ timeout: 30_000 },
)
.catch(() => null)
// Fallback assertion — page-side wait may not see the response. Use
// the response listener accumulator we set up above.
const sawThirteen = batchResponses.some((r) => r.status === 413)
if (!sawThirteen) {
test.skip(
true,
`Suite nginx did not return 413 for a 501 MB upload (saw: ${
batchResponses.map((r) => r.status).join(',') || 'no /batch responses'
})`,
)
}
// Contract assertion — drift today. Will pass once production wires the
// toast + i18n key for the 413 path.
const alertEl = page.getByRole('alert').first()
await expect(alertEl).toBeVisible({ timeout: 5000 })
await expect(alertEl).toContainText(/too large|exceeds|413/i)
},
)
test('AC-2 — the 413 path does NOT invoke window.alert()', async ({ page }) => {
// Track every dialog. Playwright auto-dismisses dialogs after listeners
// are attached, so a stray `alert()` shows up here as a "dialog" event.
const dialogs: string[] = []
page.on('dialog', async (dialog) => {
dialogs.push(`${dialog.type()}:${dialog.message()}`)
await dialog.dismiss().catch(() => null)
})
await page.goto('/annotations')
const fileInput = page.locator('input[type="file"]').nth(1)
if (!(await fileInput.count())) {
test.skip(true, 'Suite UI did not render the upload input')
}
await fileInput.setInputFiles({
name: 'huge_recon_video.mp4',
mimeType: 'video/mp4',
buffer: buildOversizedBuffer(),
})
// Allow the 413 round-trip + any error-handling React render to settle.
await page.waitForTimeout(2000)
// Filter for alert() specifically — confirm() and prompt() are out of
// scope for this AC, but we still want to know if either fires.
const alertDialogs = dialogs.filter((d) => d.startsWith('alert:'))
expect(alertDialogs).toEqual([])
})
})