// Needs review against the approved concept (#249 PR F, plan Task F3, Mock inventory M4: app.js renderReview // a749–760, style.css s407–415, s796–814 and their breakpoints, screenshot needs-review-tagging.png). Production runs // on the review fixture's concept-shaped queue (workspace_review_fixture.py `concept: true`: the concept's first three // image names and sizes, and the concept's first prompt on the open item), so every remaining difference is a recorded // deviation applied by mapConceptReview (D3, D29, D119) or a test failure. Paired captures of every state are attached // as artifacts; no screenshot baseline is added. import { test, expect } from './fixtures.mjs'; import { openConcept, openProduction, inventoryOf, mutate, saveImageTags, diffInventory, SIZES, THEMES } from './concept-inventory.mjs'; import { reviewInventory, mapConceptReview, taggingInventory, mapConceptTagging, diffTaggingInventory } from './tagging-canonical-comparison.mjs'; const CONCEPT_FILE = 'alpine_00001.png'; const CONCEPT_TAGS = ['cinematic', 'blue_hour', 'architecture']; const SURFACE = ['h2', 'p.small', 'div.row-actions.spaced-15', 'div.field', 'section.positive.prompt-section', 'section.negative.prompt-section', 'div.dialog-actions']; // The fake model's server default instructions (tests/browser/server.py TestModel.default_prompt), D84. const INSTRUCTIONS = 'Describe the image'; const panel = page => page.locator('#workspace-main section.review-panel'); const tagDialog = page => page.getByRole('dialog', { name: 'Tag image', exact: true }); async function seed(request, data = {}) { await expect(await request.post('/__test__/review/reset', { data: { items: 3, concept: true, ...data } })).toBeOK(); } /** Production's Needs review page with its first item's sampler paths loaded. */ async function openReview(page, theme) { await openProduction(page, 'review', theme); await expect(panel(page).getByRole('combobox', { name: 'Sampler context', exact: true })).toBeEnabled(); await settle(page); } async function settle(page) { await page.mouse.move(0, 0); await page.evaluate(async () => { await document.fonts.ready; await Promise.all([...document.images].map(image => image.decode().catch(() => {}))); }); } const conceptInventory = page => page.evaluate(reviewInventory, '#main'); const productionInventory = page => page.evaluate(reviewInventory, '#workspace-main'); const node = (tree, prefix) => tree.tag?.startsWith(prefix) ? tree : tree.children?.map(child => node(child, prefix)).find(Boolean) ?? null; /** Computed values of CSS tokens, read through a probe element in the open page. */ const tokens = (page, declarations) => page.evaluate(text => { const probe = document.createElement('i'); probe.style.cssText = text; document.body.append(probe); const style = getComputedStyle(probe), values = [style.borderTopColor, style.backgroundColor]; probe.remove(); return values; }, declarations); async function compareAt(page, request, width, height, theme) { await seed(request); await page.setViewportSize({ width, height }); await openConcept(page, 'review', theme); await settle(page); const concept = await conceptInventory(page); await openReview(page, theme); const actual = await productionInventory(page); return { concept, actual, reference: mapConceptReview(concept, actual) }; } test.beforeEach(async ({ page }) => { page.errors = []; page.on('pageerror', error => page.errors.push(error.message)); }); test.afterEach(async ({ page, request }) => { expect(page.errors).toEqual([]); await request.post('/__test__/jobs/restart'); }); for (const [width, height] of SIZES) for (const theme of THEMES) { test(`concept and production Metadata review match at ${width}x${height} ${theme}`, async ({ page, request }) => { const { actual, reference } = await compareAt(page, request, width, height, theme); expect(diffInventory(reference, actual), `${width}x${height} ${theme}`).toEqual([]); // Pinned concept values (M4), so a mapping mistake on both sides cannot pass unseen. const main = actual.main; expect(main.children.map(child => child.tag)).toEqual(['div.heading', 'div.notice', 'div.gallery-layout']); const notice = node(main, 'div.notice'); const [accent, soft] = await tokens(page, 'border-top:1px solid var(--ca-accent);background:var(--soft-blue)'); expect([notice.borderTopColor, notice.borderLeftColor, notice.backgroundColor]).toEqual([accent, accent, soft]); expect([notice.borderTopWidth, notice.borderTopLeftRadius, notice.paddingTop, notice.paddingLeft, notice.marginBottom]) .toEqual(['1px', '6px', '15px', '18px', '22px']); expect(notice.flexDirection).toBe(width <= 700 ? 'column' : 'row'); expect(notice.children.map(child => child.children.map(item => [item.tag, item.text]))) .toEqual([[['h2', '3 images need a decision'], ['p', 'Multiple sampler paths were found. The existing image links were preserved. Choose the correct context or leave them unresolved.']]]); const layout = node(main, 'div.gallery-layout'); expect(layout.gridTemplateColumns).toBe(width <= 700 ? `${layout.width}px` : `${layout.width - 22 - 310}px 310px`); const surface = node(main, 'section.review-panel'); expect(surface.children.map(child => child.tag)).toEqual(SURFACE); expect(surface.children[0].text).toBe('Resolve the association'); expect(surface.children[1].text).toBe(CONCEPT_FILE); const actions = surface.children.at(-1); expect(actions.children.map(button => [button.text, button.primary])).toEqual([['Leave for later', false], ['Apply association', true]]); expect(node(surface, 'select').options.at(-1)).toEqual(['Leave unresolved', false]); expect(node(main, 'div.image-grid').children.map(card => node(card, 'h3').text)) .toEqual(['alpine_00001.png', 'dunes_00001.png', 'forest_00001.png']); expect(actual.noPageScroll).toBe(true); }); } test('the gallery layout follows the concept at every breakpoint', async ({ page, request }) => { await seed(request); // 310px beside the queue, 275px and a 15px gap at ≤1250px, 280px at ≤950px, one column at ≤700px. for (const [width, columns, gap] of [[1600, '310px', '22px'], [1200, '275px', '15px'], [900, '280px', '15px'], [390, null, '15px']]) { await page.setViewportSize({ width, height: 1000 }); await openConcept(page, 'review', 'dark'); const concept = node((await conceptInventory(page)).main, 'div.gallery-layout'); await openReview(page, 'dark'); const actual = node((await productionInventory(page)).main, 'div.gallery-layout'); expect([actual.gridTemplateColumns, actual.columnGap], `${width}px`).toEqual([concept.gridTemplateColumns, concept.columnGap]); expect([actual.gridTemplateColumns.split(' ').at(-1), actual.columnGap], `${width}px`).toEqual([columns ?? `${actual.width}px`, gap]); expect(actual.gridTemplateColumns.split(' ').length, `${width}px`).toBe(width <= 700 ? 1 : 2); } }); test('SAVED VIEWS rows match the concept with Needs review after Favorites', async ({ page, request }) => { await seed(request); await page.setViewportSize({ width: 1600, height: 1100 }); await openConcept(page, 'review', 'dark'); const concept = (await inventoryOf(page, '#main')).chrome.navs.find(nav => nav.id === 'saved-nav'); await openReview(page, 'dark'); const actual = (await inventoryOf(page, '#workspace-main')).chrome.navs.find(nav => nav.id === 'saved-nav'); // Sample data: the concept stars three images; this queue's fixture stars none. Needs review counts the queue (3). const reference = { ...concept, rows: concept.rows.map(row => row.text === 'Favorites' ? { ...row, count: { ...row.count, text: '0' } } : row) }; expect(diffInventory(reference, actual)).toEqual([]); expect(actual.rows.map(row => [row.text, row.count?.text, row.current])).toEqual([['Favorites', '0', null], ['Needs review', '3', 'page']]); // The count's auto start margin follows the row's own text; every other count token is shared. const { text: favoriteCount, marginInlineStart: favoriteMargin, ...favoriteStyle } = actual.rows[0].count; const { text: reviewCount, marginInlineStart: reviewMargin, ...reviewStyle } = actual.rows[1].count; expect(reviewStyle).toEqual(favoriteStyle); expect(actual.rows[1].icon.d).toBe('M4 3h16v18H4zM8 7h8M8 12h8M8 17h5'); }); test('review Tag dialog matches the needs-review-tagging reference', async ({ page, request }) => { await seed(request); await saveImageTags(request, `review/${CONCEPT_FILE}`, CONCEPT_TAGS); await page.setViewportSize({ width: 1600, height: 1100 }); await openConcept(page, 'review', 'dark'); await page.locator('#main section.surface').getByRole('button', { name: 'Tag image', exact: true }).click(); await expect(tagDialog(page)).toBeVisible(); await settle(page); const opened = await page.locator('dialog#modal').evaluate(taggingInventory); await page.keyboard.press('Escape'); await openReview(page, 'dark'); await panel(page).getByRole('button', { name: 'Tag image', exact: true }).click(); await expect(tagDialog(page).locator('img.tag-image-preview')).toHaveAttribute('src', /\/image-annotations\/capture\//); await expect(tagDialog(page).locator('#manual-image-tags')).toHaveValue(CONCEPT_TAGS.join(', ')); await settle(page); const actual = await page.locator('dialog#modal').evaluate(taggingInventory); const numberField = opened.modelPanel.fields.find(field => field.control.id === 'image-general-threshold'); const mapped = mapConceptTagging(opened, { instructions: INSTRUCTIONS, chip: opened.heading.size, numberField }); // D119: an undecided queue item names no prompt, so the heading and the preview's alt are the file name. const reference = { ...mapped, heading: { ...mapped.heading, h3: { ...mapped.heading.h3, text: CONCEPT_FILE }, preview: { ...mapped.heading.preview, alt: CONCEPT_FILE } } }; expect(actual.scopeNote.text).toBe('Tags belong to this image. Resolve its prompt association separately in Needs review.'); expect(diffTaggingInventory(reference, actual)).toEqual([]); await page.keyboard.press('Escape'); await expect(tagDialog(page)).toHaveCount(0); }); test('the comparison notices a dropped notice border and reordered actions', async ({ page, request }) => { const { actual, reference } = await compareAt(page, request, 1600, 1100, 'dark'); expect(diffInventory(reference, actual)).toEqual([]); const undoBorder = await mutate(page, { css: '#workspace-main .notice { border: 0 !important; }' }); const borderless = await productionInventory(page); expect(diffInventory(reference, borderless).map(item => item.path)).toEqual(expect.arrayContaining([ 'main.children.1.borderTopWidth', 'main.children.1.borderTopStyle'])); await undoBorder(); await panel(page).locator('.dialog-actions').evaluate(row => row.prepend(row.lastElementChild)); const reordered = await productionInventory(page); expect(diffInventory(reference, reordered).map(item => item.path)).toEqual(expect.arrayContaining([ 'main.children.2.children.1.children.6.children.0.text', 'main.children.2.children.1.children.6.children.0.primary'])); await panel(page).locator('.dialog-actions').evaluate(row => row.prepend(row.lastElementChild)); expect(diffInventory(reference, await productionInventory(page))).toEqual([]); }); // Paired captures for the PR (no baselines): the populated queue beside the concept in both themes and widths, then // the states the concept lacks (conflict, limited item, unavailable file, empty queue) as production captures. test('paired Needs review captures', async ({ page, request }, testInfo) => { test.setTimeout(120_000); const shot = async name => { await settle(page); const file = testInfo.outputPath(`${name}.png`); await page.screenshot({ path: file, fullPage: true }); await testInfo.attach(name, { path: file, contentType: 'image/png' }); }; await seed(request); for (const [width, height] of SIZES) for (const theme of THEMES) { await page.setViewportSize({ width, height }); await openConcept(page, 'review', theme); await shot(`concept-review-${width}-${theme}`); await openReview(page, theme); await shot(`production-review-${width}-${theme}`); } await page.setViewportSize({ width: 1600, height: 1100 }); await seed(request, { items: 0, conflict: true, limited: true, unavailable: true }); await openReview(page, 'dark'); await panel(page).getByRole('combobox', { name: 'Sampler context', exact: true }).selectOption({ index: 0 }); const decided = page.waitForResponse(item => /\/decision$/.test(item.url())); await panel(page).getByRole('button', { name: 'Apply association', exact: true }).click(); expect((await decided).status()).toBe(409); await expect(panel(page).locator('.warning-note')).toBeVisible(); await shot('production-review-conflict'); for (const [index, name] of [[1, 'limited'], [2, 'unavailable']]) { await page.locator('#workspace-main .review-queue article').nth(index).getByRole('button', { name: /^Inspect / }).click(); await expect(panel(page).locator('.warning-note')).toBeVisible(); await shot(`production-review-${name}`); } await seed(request, { items: 0 }); await openProduction(page, 'review', 'dark'); await expect(page.locator('#workspace-main .notice h2')).toHaveText('No images need a decision'); await shot('production-review-empty'); });