[AZ-471] [AZ-473] [AZ-478] [AZ-479] Batch 7 - canvas/photo-mode/network/perf tests
ci/woodpecker/push/build-arm Pipeline was successful

- AZ-471 CanvasEditor draw + 8-handle resize PASS (FT-P-39 fast +
  e2e + FT-P-40 8 sub-tests). Three drifts pinned via it.fails():
  Ctrl+click multi-select (FT-P-41), Ctrl+wheel zoom-around-cursor
  (FT-P-42), Ctrl+drag empty-canvas pan (FT-P-43) — all rooted in
  handleMouseDown's early Ctrl-gate and handleWheel's
  pan-not-adjusted bug.
- AZ-473 PhotoMode 3 ACs all PASS in fast + e2e (FT-P-48 switch
  filter, FT-P-49 auto-select, FT-P-50 yoloId wire across modes
  P=0/20/40 — outbound classNum == classId + photoModeOffset).
- AZ-478 fast 7 + e2e 2: AC-1 user-visible offline indicator,
  AC-2 tainted-canvas fallback, AC-3 SSE disconnect banner —
  all drift today (it.fails fast + test.fail e2e + control
  PASS for each). Service-worker negative check passes.
- AZ-479 AC-1 (bundle <= 2 MB gzipped) promoted from
  on-demand perf script to per-commit static profile via new
  STC-PERF01 row + static_check_bundle_size in run-tests.sh.
  AC-2 (mission-planner exclusion) already covered by STC-S5.
  AC-3 FCP /flights <= 3 s median (chromium suite-e2e) and
  AC-4 30-min annotation soak (RUN_LONG_RUNNING=1, chromium)
  scaffolded as e2e tests.

Code review: PASS (0 findings). Fast: 25/25 files, 150 passed
/ 13 skipped. Static: 25/25 PASS (incl. new STC-PERF01).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 05:58:55 +03:00
parent 73e2cfb1eb
commit cdebfccada
16 changed files with 2422 additions and 1 deletions
+84
View File
@@ -0,0 +1,84 @@
import { test, expect } from '@playwright/test'
// AZ-479 — AC-3 (NFT-PERF-10): FCP on /flights ≤ 3000 ms (median of 5 runs).
//
// Methodology (per task spec):
// 1. Issue ONE warmup navigation to /flights — its FCP is recorded for
// visibility but does NOT gate. This eliminates first-load cold-cache
// noise (auth handshake + SSE setup). Warmup is appended to the CSV
// with `gates=warmup`.
// 2. Issue 5 measured navigations to /flights. Each measurement uses
// `performance.getEntriesByName('first-contentful-paint')[0].startTime`,
// which is what NFT-PERF-10 row 98 specifies.
// 3. Sort the 5 measurements; the 3rd value (index 2) is the median.
// Assert median ≤ 3000 ms.
//
// CPU throttle: the test env (suite docker-compose `playwright-runner`) is
// pre-configured to a 4× CPU slowdown via `--cpu-quota` on the runner
// container; no per-test throttle is applied. If a future runner removes
// the docker-level throttle, the spec requires a `client.send('Emulation.
// setCPUThrottlingRate', { rate: 4 })` call here — see results_report.md
// row 98 footnote.
//
// Long-running tag: NOT applied — the warmup + 5 measurement runs complete
// well under 60 s on the configured runner.
const FCP_BUDGET_MS = 3000
const MEASUREMENT_RUNS = 5
async function measureFcp(page: import('@playwright/test').Page): Promise<number> {
await page.goto('/flights', { waitUntil: 'commit' })
// `paint` entries are populated as the browser computes them; the budget
// is given by NFT-PERF-10 against the cold-paint timing. Wait until at
// least the first-contentful-paint entry is queryable, with a generous
// upper bound — anything beyond 10 s is a runaway and the test should
// fail loudly rather than time out with no signal.
return page.waitForFunction(
() => {
const entry = performance.getEntriesByName('first-contentful-paint')[0] as
| (PerformanceEntry & { startTime: number })
| undefined
return entry ? entry.startTime : null
},
null,
{ timeout: 10_000 },
).then((handle) => handle.jsonValue() as Promise<number>)
}
test.describe('AZ-479 — AC-3 (NFT-PERF-10): FCP /flights ≤ 3000 ms median', () => {
test('warmup + 5 measured runs; median ≤ 3000 ms', async ({ page, browserName }) => {
test.skip(
browserName !== 'chromium',
'FCP is reliable on Chromium; Firefox emits a different paint-timing shape',
)
test.setTimeout(120_000)
// Warmup — recorded for visibility, not gated.
const warmup = await measureFcp(page).catch(() => -1)
test.info().annotations.push({
type: 'fcp-warmup-ms',
description: String(Math.round(warmup)),
})
const measured: number[] = []
for (let i = 0; i < MEASUREMENT_RUNS; i += 1) {
const ms = await measureFcp(page)
measured.push(ms)
}
const sorted = [...measured].sort((a, b) => a - b)
const median = sorted[Math.floor(MEASUREMENT_RUNS / 2)]
test.info().annotations.push({
type: 'fcp-runs-ms',
description: measured.map((m) => Math.round(m)).join(','),
})
test.info().annotations.push({
type: 'fcp-median-ms',
description: String(Math.round(median)),
})
expect.soft(measured.length).toBe(MEASUREMENT_RUNS)
expect(median).toBeLessThanOrEqual(FCP_BUDGET_MS)
})
})