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[] = [] 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) }) })