mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 23:51:10 +00:00
cdebfccada
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>
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
|
|
// AZ-473 — e2e companion for FT-P-50 (yoloId on the wire).
|
|
//
|
|
// Task spec marks FT-P-48 / FT-P-49 fast-only. Only FT-P-50 — the wire
|
|
// offset arithmetic `classNum == classId + photoModeOffset` — is `fast +
|
|
// e2e`, because the contract is observable on the network and the bug
|
|
// shape (wrong offset on save) is invisible to fast unit tests once the
|
|
// SPA's PhotoModeContext is exercised through the real DetectionClasses
|
|
// fetch + AnnotationsPage save.
|
|
//
|
|
// The companion runs the wire assertion for ALL three modes (P=0, 20, 40)
|
|
// against the suite stack so a regression in any mode-offset path lights
|
|
// up immediately.
|
|
|
|
const ALICE_EMAIL = 'op_alice@test.local'
|
|
const ALICE_PASSWORD = 'TestPassword!23'
|
|
|
|
async function login(page: import('@playwright/test').Page): Promise<void> {
|
|
await page.goto('/login')
|
|
await page.getByLabel(/email/i).fill(ALICE_EMAIL)
|
|
await page.getByLabel(/password/i).fill(ALICE_PASSWORD)
|
|
await Promise.all([
|
|
page.waitForResponse(
|
|
(r) => r.url().includes('/api/admin/auth/login') && r.request().method() === 'POST',
|
|
),
|
|
page.getByRole('button', { name: /sign in/i }).click(),
|
|
])
|
|
}
|
|
|
|
async function selectPhotoMode(
|
|
page: import('@playwright/test').Page,
|
|
modeOffset: number,
|
|
): Promise<void> {
|
|
// PhotoMode UI surfaces the three offsets as toggle buttons (mode 0,
|
|
// 20, 40). The button label uses the offset directly; if the suite seed
|
|
// localizes them, fall back to a position-based selector.
|
|
const byLabel = page.getByRole('button', { name: new RegExp(`mode\\s*${modeOffset}`, 'i') })
|
|
if (await byLabel.first().isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await byLabel.first().click()
|
|
return
|
|
}
|
|
// Fallback — the buttons render in a fixed order [0, 20, 40].
|
|
const idx = modeOffset === 0 ? 0 : modeOffset === 20 ? 1 : 2
|
|
await page.locator('[data-testid="photo-mode-button"]').nth(idx).click()
|
|
}
|
|
|
|
async function drawAndSave(
|
|
page: import('@playwright/test').Page,
|
|
): Promise<void> {
|
|
const canvas = page.locator('canvas').first()
|
|
const box = await canvas.boundingBox()
|
|
if (!box) throw new Error('canvas missing bounding box')
|
|
|
|
// Draw a small bbox so the page has something to save.
|
|
await page.mouse.move(box.x + box.width * 0.4, box.y + box.height * 0.4)
|
|
await page.mouse.down()
|
|
await page.mouse.move(box.x + box.width * 0.6, box.y + box.height * 0.6, { steps: 8 })
|
|
await page.mouse.up()
|
|
|
|
const saveBtn = page.getByRole('button', { name: /^Save$/i }).first()
|
|
if (await saveBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
await saveBtn.click().catch(() => null)
|
|
}
|
|
}
|
|
|
|
test.describe('AZ-473 — yoloId on the wire (e2e companion)', () => {
|
|
for (const modeOffset of [0, 20, 40]) {
|
|
test(`FT-P-50 — mode ${modeOffset}: classNum == classId + ${modeOffset}`, async ({
|
|
page,
|
|
browserName,
|
|
}) => {
|
|
test.skip(
|
|
browserName !== 'chromium',
|
|
'Pointer-event canvas drawing reliable on Chromium only; the wire contract is the same on every browser',
|
|
)
|
|
test.setTimeout(30_000)
|
|
|
|
const captured: Array<Record<string, unknown>> = []
|
|
await page.route('**/api/annotations/annotations**', async (route) => {
|
|
const req = route.request()
|
|
if (req.method() === 'POST') {
|
|
const body = req.postData()
|
|
if (body) captured.push(JSON.parse(body) as Record<string, unknown>)
|
|
}
|
|
await route.continue()
|
|
})
|
|
|
|
await login(page)
|
|
await page.goto('/annotations')
|
|
|
|
const canvas = page.locator('canvas').first()
|
|
if (!(await canvas.isVisible({ timeout: 5000 }).catch(() => false))) {
|
|
test.skip(true, 'Suite seed has no media for annotation')
|
|
}
|
|
|
|
await selectPhotoMode(page, modeOffset).catch(() => null)
|
|
|
|
// Pick the first valid class for this mode. The DetectionClasses panel
|
|
// renders the filtered list; clicking the first item selects it.
|
|
const firstClass = page.locator('[data-testid^="class-row"], button[data-testid*="class"]').first()
|
|
if (await firstClass.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
await firstClass.click().catch(() => null)
|
|
}
|
|
|
|
await drawAndSave(page)
|
|
await page.waitForTimeout(750)
|
|
|
|
expect(captured.length, 'expected at least one save POST').toBeGreaterThan(0)
|
|
|
|
type Detection = { classNum?: number; classId?: number }
|
|
const lastBody = captured[captured.length - 1]
|
|
const detections = (lastBody.detections as Detection[] | undefined) ?? []
|
|
expect(detections.length, 'last save must include the drawn detection').toBeGreaterThan(0)
|
|
|
|
for (const d of detections) {
|
|
// Wire contract per AC-3: classNum == classId + photoModeOffset.
|
|
// Some service variants drop classId from the wire and only emit
|
|
// classNum — in that case we assert classNum is in the [P, P+20)
|
|
// window for this mode rather than failing on a missing field.
|
|
if (typeof d.classId === 'number' && typeof d.classNum === 'number') {
|
|
expect(d.classNum).toBe(d.classId + modeOffset)
|
|
} else if (typeof d.classNum === 'number') {
|
|
expect(d.classNum).toBeGreaterThanOrEqual(modeOffset)
|
|
expect(d.classNum).toBeLessThan(modeOffset + 20)
|
|
} else {
|
|
throw new Error('Detection has neither classNum nor classId — wire contract broken')
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|