mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 21:41:10 +00:00
[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:
@@ -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([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user