[AZ-461] [AZ-464] [AZ-470] [AZ-472] Batch 5 - detection/bulk-validate/panel-width/classes tests
ci/woodpecker/push/build-arm Pipeline was successful

- AZ-461 sync image detect URL canary (FT-P-11) PASS;
  async-video QUARANTINE (FT-P-12) + X-Refresh-Token drift
  (FT-P-13) recorded as it.fails() with controls.
- AZ-464 bulk-validate URL + UI sync (≤2 s) PASS;
  body shape drift {annotationIds,status} vs contract
  {ids,targetStatus:30} captured as it.fails().
- AZ-470 panel-width debounce + rehydration: entire task
  is Phase-B target (useResizablePanel has no PUT writer
  / no rehydration); 3 ACs as it.fails() with controls.
- AZ-472 DetectionClasses load + click + fallback PASS;
  hotkey arithmetic P=0 PASS, P=20/P=40 it.fails() for
  classes[idx+P]-against-dense-array drift.

Code review: PASS (0 findings). Fast: 18/18 files,
102 passed / 13 skipped. Static: 21/21 PASS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:38:22 +03:00
parent 1dd25edee3
commit 6d03643c2c
15 changed files with 1644 additions and 4 deletions
+104
View File
@@ -0,0 +1,104 @@
import { test, expect } from '@playwright/test'
// AZ-464 — e2e companion for bulk-validate URL + body + UI sync.
//
// AC-1 (FT-P-20 URL): outbound POST URL is `/api/annotations/dataset/bulk-status`.
// AC-2 (FT-P-20 body): drift today — production sends `{annotationIds, status}`,
// contract wants `{ids, targetStatus: 30}`. `test.fail()`.
// AC-3 (FT-P-21): UI rows show `Validated` within 2 s of the 200 response.
//
// Requires the suite docker-compose stack with seeded dataset items. The seed
// must include at least 3 items in Created status so the bulk-validate UI
// path is exercised end-to-end.
test.describe('AZ-464 — bulk-validate (e2e companion)', () => {
test('AC-1 (FT-P-20) — outbound URL is /api/annotations/dataset/bulk-status', async ({ page }) => {
const posts: { url: string; body: string | null }[] = []
await page.route('**/api/annotations/dataset/bulk-status', async (route) => {
const req = route.request()
if (req.method() === 'POST') {
posts.push({ url: req.url(), body: req.postData() })
}
await route.continue()
})
await page.goto('/dataset')
// Suite seed must surface at least 3 selectable rows; otherwise skip.
const rows = page.locator('div.cursor-pointer')
const visibleCount = await rows.count().catch(() => 0)
if (visibleCount < 3) {
test.skip(true, 'Suite seed has fewer than 3 dataset rows for bulk-validate')
}
for (let i = 0; i < 3; i++) {
await rows.nth(i).click({ modifiers: ['Control'] })
}
const validateBtn = page.getByRole('button', { name: /Validate \(\d+\)/i })
if (!(await validateBtn.isVisible({ timeout: 5000 }).catch(() => false))) {
test.skip(true, 'Validate button not visible — selection not applied?')
}
await validateBtn.click()
await page.waitForFunction(() => true, undefined, { timeout: 3000 }).catch(() => null)
expect(posts.length).toBe(1)
const path = new URL(posts[0].url).pathname
expect(path).toBe('/api/annotations/dataset/bulk-status')
})
test.fail('AC-2 (FT-P-20) — body shape `{ids, targetStatus: 30}` (drift)', async ({ page }) => {
const captured: Record<string, unknown>[] = []
await page.route('**/api/annotations/dataset/bulk-status', async (route) => {
const req = route.request()
if (req.method() === 'POST') {
const text = req.postData()
if (text) captured.push(JSON.parse(text))
}
await route.continue()
})
await page.goto('/dataset')
const rows = page.locator('div.cursor-pointer')
if ((await rows.count().catch(() => 0)) < 3) {
test.skip(true, 'Seed gap')
}
for (let i = 0; i < 3; i++) {
await rows.nth(i).click({ modifiers: ['Control'] })
}
const validateBtn = page.getByRole('button', { name: /Validate \(\d+\)/i })
await validateBtn.click()
await page.waitForTimeout(1000)
expect(captured.length).toBeGreaterThan(0)
for (const body of captured) {
expect(body).toHaveProperty('ids')
expect(body).toHaveProperty('targetStatus', 30)
}
})
test('AC-3 (FT-P-21) — UI shows Validated badge ≤ 2 000 ms after success', async ({ page }) => {
await page.goto('/dataset')
const rows = page.locator('div.cursor-pointer')
if ((await rows.count().catch(() => 0)) < 3) {
test.skip(true, 'Seed gap — need 3 rows in Created status')
}
for (let i = 0; i < 3; i++) {
await rows.nth(i).click({ modifiers: ['Control'] })
}
const validateBtn = page.getByRole('button', { name: /Validate \(\d+\)/i })
if (!(await validateBtn.isVisible({ timeout: 5000 }).catch(() => false))) {
test.skip(true, 'Validate button not visible')
}
const t0 = Date.now()
await validateBtn.click()
// Wait for at least one row to flip to the Validated badge.
await page.waitForFunction(
() => {
const badges = Array.from(
document.querySelectorAll('span'),
).filter((el) => /Validated/i.test(el.textContent ?? ''))
return badges.length > 0
},
undefined,
{ timeout: 2000 },
)
expect(Date.now() - t0).toBeLessThanOrEqual(2000)
})
})