import { writeFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { captureCanonicalState, historyGeometry, compareCanonicalHistory, nativeDialogState, exerciseCurrentPairSend, assertRecordedHistory } from './history-canonical-comparison.mjs'; import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; import { test, expect } from '../../tools/vendor/node_modules/@playwright/test/index.mjs'; test.use({ locale: 'en-US', timezoneId: 'UTC', reducedMotion: 'reduce' }); const workspace = '/prompt_manager/workspace'; const primary = ['images', 'library', 'tags', 'inspect']; const tools = ['overview', 'system']; async function noOverflow(page) { expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBeLessThanOrEqual(1); } async function groupedNavigation(page) { for (const [group, routes] of [['primary', primary], ['tools', tools]]) { const region = page.locator(`.workspace-nav-${group}`); await expect(region).toHaveCount(1); expect(await region.locator('[data-route]').evaluateAll(nodes => nodes.map(node => node.dataset.route))).toEqual(routes); } } async function attachEvidence(page, testInfo, name, fullPage = true) { await page.evaluate(async () => { await document.fonts.ready; for (const image of document.images) image.loading = 'eager'; await Promise.all([...document.images].map(image => image.decode().catch(() => {}))); }); const screenshotPath = testInfo.outputPath(`${name}.png`); await page.screenshot({ path: screenshotPath, fullPage }); await testInfo.attach(name, { path: screenshotPath, contentType: 'image/png' }); const geometry = await page.evaluate(() => Object.fromEntries(['.sidebar', '.topbar', '#workspace-main', '.workspace-heading', 'h1', '.image-workspace', '.image-grid', '.image-card', '.image-details', '.prompt-block-heading', '.library-toolbar', '.library-grid', '.library-card', '.library-card-art', '.library-card-content'].map(selector => { const node = document.querySelector(selector); if (!node) return [selector, null]; const box = node.getBoundingClientRect(), style = getComputedStyle(node); return [selector, { x: box.x, y: box.y, width: box.width, height: box.height, padding: style.padding, fontSize: style.fontSize }]; }))); const geometryPath = testInfo.outputPath(`${name}-geometry.json`); await writeFile(geometryPath, JSON.stringify(geometry, null, 2)); await testInfo.attach(`${name}-geometry`, { path: geometryPath, contentType: 'application/json' }); } test.beforeEach(async ({ page, request, browserName }) => { await startCoverage(page, browserName); await expect(await request.post('/__test__/reset')).toBeOK(); }); test.afterEach(async ({ page, browserName }) => { await saveCoverage(page, browserName); }); test('canonical history assets are the exact immutable approved source', async ({ request }) => { const assets = [ ['/__test__/canonical/workspace', 'docs/concepts/workspace/index.html', 'd84712c7284bb232919a1ed9b154575c8ac574f63b2404a4ac4bc06cbf323c03'], ['/workspace/app.js', 'docs/concepts/workspace/app.js', 'e33e7652da12a68cd3fe31e94d77d680aa439b6434ceee94f21cc4ce8883263e'], ['/workspace/style.css', 'docs/concepts/workspace/style.css', '2c479ca1a2ddbbd14a550eae27dd731c326ccdb91fe9ddc87e2b81cd5298fce8'], ]; for (const [url, source, sha256] of assets) { const response = await request.get(url);await expect(response).toBeOK(); const bytes = await response.body(); expect(bytes).toEqual(await readFile(new URL(`../../${source}`, import.meta.url))); expect(createHash('sha256').update(bytes).digest('hex')).toBe(sha256); expect(response.headers()['cache-control']).toBe('no-store'); expect(response.headers()['x-content-type-options']).toBe('nosniff'); } for (const url of ['/workspace/missing.js', '/__test__/canonical/server.py', '/toolkit/missing.js', '/vendor/missing.js']) { expect((await request.get(url)).status()).toBe(404); } }); for (const [width, height] of [[1600, 1100], [390, 844]]) for (const theme of ['dark', 'light']) { test(`canonical chronological history ${width} ${theme}`, async ({ page, request }, testInfo) => { await page.setViewportSize({ width, height }); await page.goto('/__test__/canonical/workspace#prompt/alpine'); await page.getByRole('combobox', { name: 'Theme', exact: true }).selectOption(theme); await expect(page.locator('.timeline')).toBeVisible(); const referenceDot = page.getByRole('button', { name: 'Select negative Quality baseline', exact: true }); await referenceDot.focus();await page.keyboard.press('Enter'); await expect(page.getByRole('button', { name: 'Select negative Quality baseline', exact: true })).toHaveAttribute('aria-pressed', 'true'); const reference = await historyGeometry(page, false); for (const [name, selector] of [['saved', '.surface'], ['branches', '.timeline-panel'], ['gallery', '.history-gallery']]) { await captureCanonicalState(page, testInfo, `concept-${name}-${width}-${theme}`, selector); } await page.getByRole('button', { name: 'Show repeat run for Quality baseline', exact: true }).click(); await expect(page.getByRole('dialog', { name: 'Repeat run', exact: true })).toBeVisible(); expect(await page.locator('#modal').evaluate(node => node.matches(':modal'))).toBe(true); reference.runDialog = await nativeDialogState(page.locator('#modal')); await captureCanonicalState(page, testInfo, `concept-run-${width}-${theme}`, '#modal'); await page.keyboard.press('Escape');await expect(page.locator('#modal')).not.toBeVisible(); await page.locator('.history-gallery .image-open').first().click(); await expect(page.locator('#viewer-dialog')).toBeVisible(); await expect(page.locator('#viewer-dialog')).toHaveAccessibleName('Alpine observatory · alpine_00001.png'); await captureCanonicalState(page, testInfo, `concept-viewer-${width}-${theme}`, '#viewer-dialog'); await page.keyboard.press('Escape'); const reset = await request.post('/__test__/reset', { data: { chronological_history: true } });await expect(reset).toBeOK();const fixture = await reset.json(); await page.goto(`${workspace}#/library/${fixture.canonical_id}`); await page.getByRole('combobox', { name: 'Appearance', exact: true }).selectOption(theme); const details = await (await request.get(`/prompt_manager/library/${fixture.canonical_id}`)).json(); const index = details.variants.findIndex(variant => variant.id === fixture.canonical_pair); const choice = page.getByRole('button', { name: `Select negative variant ${index + 1}`, exact: true }); await choice.focus();await page.keyboard.press('Enter');await expect(choice).toHaveAttribute('aria-pressed', 'true'); const actual = await historyGeometry(page, true); const roles = page.getByRole('region', { name: 'Saved prompt texts', exact: true }); const positive = roles.getByRole('region', { name: 'Positive prompt', exact: true }); expect(await positive.evaluate(node => node.nextElementSibling?.getAttribute('aria-label'))).toBe('Negative prompt'); for (const name of ['Copy positive prompt', 'Send positive prompt', 'Copy negative prompt', 'Send negative prompt']) await expect(roles.getByRole('button', { name, exact: true })).toBeVisible(); // As in the concept, the selected pair's send sits in the history heading, not among the saved texts. await expect(roles.getByRole('button', { name: 'Send prompt pair', exact: true })).toHaveCount(0); await expect(page.locator('.workspace-heading button.send')).toBeEnabled(); await expect(page.locator('.library-shared-gallery article[data-image-key]')).toHaveCount(2); await page.locator('.library-shared-gallery .image-open').first().click(); await expect(page.getByRole('region', { name: 'Image details', exact: true })).toContainText('canonical-alt.png'); await page.getByRole('button', { name: 'Open full preview', exact: true }).click(); await expect(page.getByRole('dialog', { name: 'Image preview', exact: true })).toBeVisible();await page.keyboard.press('Escape'); for (const [name, selector] of [['saved', '.library-surface'], ['branches', '.library-variants'], ['gallery', '.library-shared-gallery'], ['chronology', '.recorded-history']]) { await captureCanonicalState(page, testInfo, `production-${name}-${width}-${theme}`, selector); } const run = page.locator(`button[data-run-id="${fixture.canonical_run}"][data-anchor-id="10"]`); await run.focus();await page.keyboard.press('Enter'); const runDialog = page.getByRole('dialog', { name: 'Recorded run', exact: true }); await expect(runDialog.locator('[data-receipt]')).toHaveCount(2); expect(await runDialog.evaluate(node => node.matches(':modal') && node.contains(document.activeElement))).toBe(true); actual.runDialog = await nativeDialogState(runDialog); for (const name of ['Raw', 'Effective', 'Close recorded run', 'Send prompt pair']) await expect(runDialog.getByRole('button', { name, exact: true })).toBeVisible(); await captureCanonicalState(page, testInfo, `production-run-${width}-${theme}`, '.recorded-run-dialog'); const view = runDialog.getByRole('button', { name: 'View current file', exact: true }).first();await view.click(); const viewer = page.getByRole('dialog', { name: 'Image preview', exact: true });await expect(viewer).toBeVisible(); await expect(viewer.getByRole('region', { name: 'Positive prompt', exact: true })).toContainText(fixture.canonical_positive); await expect(viewer.getByRole('region', { name: 'Negative prompt', exact: true })).toContainText(fixture.canonical_negative); for (const name of ['Copy positive prompt', 'Send positive prompt', 'Copy negative prompt', 'Send negative prompt', 'Send prompt pair']) await expect(viewer.getByRole('button', { name, exact: true })).toBeVisible(); await expect(viewer).toContainText('42001');await expect(viewer).toContainText('720');await expect(viewer).toContainText('480'); const currentPositive = viewer.getByRole('region', { name: 'Positive prompt', exact: true }); await currentPositive.getByRole('button', { name: 'Copy positive prompt', exact: true }).click(); await expect(currentPositive.getByRole('status')).toContainText(/Copied\.|Copy unavailable/); await exerciseCurrentPairSend(page, { positive: fixture.canonical_positive, negative: fixture.canonical_negative }); await captureCanonicalState(page, testInfo, `production-viewer-${width}-${theme}`, 'dialog.image-preview'); await page.keyboard.press('Escape');await expect(view).toBeFocused();await page.keyboard.press('Escape');await expect(run).toBeFocused(); await compareCanonicalHistory(reference, actual, testInfo); const history = await request.get(`/prompt_manager/library/${fixture.positive_id}/history`);await expect(history).toBeOK(); const window = await history.json();expect(window.items.map(item => item.run_id)).toEqual(fixture.aab); expect(window.context_summaries).toHaveLength(2); const contextIds = [...new Set(window.items.flatMap(item => item.matching_anchors.map(anchor => anchor.context_id)))]; expect(contextIds).toHaveLength(2); await page.goto(`${workspace}#/library/${fixture.positive_id}`); await expect(page.locator('.history-run-marker')).toHaveCount(3); await page.locator('.history-timeline').evaluate(node => { node.scrollLeft = 0;node.scrollTop = 0; }); const anchor = page.locator('.history-context-anchor').first();await anchor.focus();await expect(anchor).toBeFocused(); const multiple = await historyGeometry(page, true); expect(multiple.recordedBranch.map(branch => branch.contextId)).toEqual(contextIds); expect(multiple.recordedMarker.map(marker => marker.runId)).toEqual(fixture.aab); await captureCanonicalState(page, testInfo, `production-chronology-multiple-${width}-${theme}`, '.recorded-history'); const geometryPath = testInfo.outputPath('canonical-recorded-geometry.json'); await writeFile(geometryPath, JSON.stringify({ reference, actual: multiple, expected: { contextIds, runIds: fixture.aab, branchCount: 2, anchorCount: 2, repeatCount: 1 }, differences: 'Two real frozen contexts / three runs versus three illustrative saved variants; chronological columns retain 64px run spacing, with the same role colors, curves and 57px row rhythm.' }, null, 2)); await testInfo.attach('canonical-recorded-geometry', { path: geometryPath, contentType: 'application/json' }); assertRecordedHistory(reference, multiple, { branchCount: 2, anchorCount: 2, repeatCount: 1 }); }); } test('visual fixture exposes real stable media and library records', async ({ request }) => { expect((await request.post('/__test__/reset', { data: { workspace_visual: 'true' } })).status()).toBe(400); for (const option of ['extra_images', 'include_video', 'prompt_inspector', 'native_library', 'extra_prompts', 'extra_variants']) { expect((await request.post('/__test__/reset', { data: { workspace_visual: true, [option]: option.startsWith('extra') ? 1 : true } })).status()).toBe(400); } await expect(await request.post('/__test__/reset', { data: { workspace_visual: true } })).toBeOK(); const images = await (await request.get('/prompt_manager/images/output?limit=40')).json(); expect(images.total).toBe(24); expect(new Set(images.images.map(image => image.root_index))).toEqual(new Set([0, 1])); const image = images.images[0]; await expect(await request.get(image.url)).toBeOK(); const thumbnail = await request.get(image.thumbnail_url); await expect(thumbnail).toBeOK(); expect(thumbnail.headers()['content-type']).toBe('image/png'); const inspection = await (await request.get(`/prompt_manager/images/inspect/${encodeURIComponent(image.path)}?root=${image.root_index}`)).json(); expect(inspection.success).toBe(true); expect(inspection.image.path).toBe(image.path); expect(inspection.prompts.positive.text).toContain('Amber geometry'); expect(inspection.prompts.negative.text).toBe('blurred edges, watermark'); expect(new Set(images.images.map(image => image.modified_time)).size).toBe(24); const library = await (await request.get('/prompt_manager/library')).json(); expect(library.total).toBe(6); expect(library.items.reduce((sum, item) => sum + item.variant_count, 0)).toBe(10); const recipes = await Promise.all(library.items.map(async item => (await request.get(`/prompt_manager/library/${item.id}`)).json())); expect(recipes.flatMap(recipe => recipe.variants).some(variant => variant.negative_text === '')).toBe(true); const options = await (await request.get('/prompt_manager/library/options')).json(); expect(options.success).toBe(true); await request.post('/__test__/reset', { data: { workspace_visual: true } }); const repeated = await (await request.get('/prompt_manager/images/output?limit=40')).json(); const identity = items => items.map(({root_index, path, modified_time}) => ({root_index, path, modified_time})); expect(identity(repeated.images)).toEqual(identity(images.images)); await expect(await request.post('/__test__/reset')).toBeOK(); expect((await (await request.get('/prompt_manager/stats')).json()).stats.total_prompts).toBe(2); }); test('shell matches approved stable geometry in both themes', async ({ page, request }, testInfo) => { await request.post('/__test__/reset', { data: { workspace_visual: true } }); for (const [width, height] of [[1600, 1100], [390, 844]]) { await page.setViewportSize({ width, height }); for (const route of ['images', 'library']) { await page.goto(`${workspace}#/${route}`); if (width === 1600) await expect(page.locator('.sidebar')).toHaveCSS('width', '218px'); await expect(page.locator('.workspace-heading > .workspace-heading-copy > h1')).toBeVisible(); await groupedNavigation(page); await expect(page.locator(route === 'images' ? 'article[data-image-key]' : 'article[data-prompt-id]').first()).toBeVisible(); await expect(page.locator('.brand')).toHaveAttribute('href', '#/images'); await expect(page.locator('#workspace-main > .eyebrow')).toHaveCount(0); for (const [theme, active] of [['dark', 'rgb(20, 54, 77)'], ['light', 'rgb(228, 241, 251)']]) { await page.getByRole('combobox', { name: 'Appearance', exact: true }).selectOption(theme); await expect(page.locator('nav [aria-current="page"]')).toHaveCSS('background-color', active); await expect(page.locator('h1')).toHaveCSS('font-weight', '620'); await expect(page.locator('h1')).toHaveCSS('letter-spacing', '-0.7px'); await expect(page.locator('.workspace-heading')).toHaveCSS('margin-block-end', '22px'); await expect(page.locator('h1')).toHaveCSS('font-size', width === 1600 ? '28px' : '25px'); await expect(page.locator('.workspace-heading-copy > .intro')).toHaveCSS('line-height', '18px'); await expect(page.locator('#workspace-main')).toHaveCSS('padding-inline-start', width === 1600 ? '30px' : '14px'); if (width === 1600) { await expect(page.locator('.sidebar')).toHaveCSS('width', '218px'); await expect(page.locator('.topbar')).toHaveCSS('height', '68px'); } else { await expect(page.locator('.sidebar')).toHaveCSS('position', 'static'); expect(await page.locator('.workspace-shell').evaluate(node => getComputedStyle(node).gridTemplateColumns.split(' ').length)).toBe(1); } await noOverflow(page); await attachEvidence(page, testInfo, `${route}-${width}-${theme}`); } } } }); test('long content and zoom keep grouped navigation reachable', async ({ page, request }) => { await request.post('/__test__/reset', { data: { workspace_visual: true } }); await page.setViewportSize({ width: 1600, height: 1100 }); await page.goto(`${workspace}#/library`); await expect(page.locator('article[data-prompt-id]')).toHaveCount(6); await page.evaluate(() => { document.documentElement.style.zoom = '2'; }); await groupedNavigation(page); for (const route of [...primary, ...tools]) { const link = page.locator(`nav [data-route="${route}"]`); await link.focus(); await expect(link).toBeFocused(); } await page.getByRole('combobox', { name: 'Appearance', exact: true }).focus(); await expect(page.getByRole('combobox', { name: 'Appearance', exact: true })).toBeFocused(); const text = page.locator('.prompt-text').filter({ hasText: 'A study in very long creative context' }); await text.scrollIntoViewIfNeeded(); expect((await text.textContent()).length).toBeGreaterThan(1500); await text.focus(); await expect(text).toBeFocused(); expect(await text.evaluate(node => { const scroller = node.closest('.library-card-prompts'); return scroller.scrollHeight <= scroller.clientHeight + 1 && node.scrollHeight <= node.clientHeight + 1; })).toBe(true); await noOverflow(page); await page.evaluate(() => { document.documentElement.style.zoom = ''; }); for (const width of [1251, 1250, 951, 950, 780, 700]) { await page.setViewportSize({ width, height: 1100 }); await expect(page.locator('.sidebar')).toHaveCSS('position', width > 950 ? 'sticky' : 'static'); if (width > 950) await expect(page.locator('.sidebar')).toHaveCSS('width', width > 1250 ? '218px' : '185px'); await expect(page.locator('#workspace-main')).toHaveCSS('padding-inline-start', width > 1250 ? '30px' : width > 700 ? '24px' : '14px'); await noOverflow(page); } }); test('Appearance label and control fit the narrow shell at 200 percent zoom', async ({ page, request }, testInfo) => { await request.post('/__test__/reset', { data: { workspace_visual: true } }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto(`${workspace}#/images`); await expect(page.locator('article[data-image-key]')).toHaveCount(24); await page.evaluate(() => { document.documentElement.style.zoom = '2'; }); const preference = page.getByRole('combobox', { name: 'Appearance', exact: true }); const bounds = await preference.evaluate(select => { const field = select.closest('label'); return { intendedWrapper: field.classList.contains('workspace-appearance'), boxes: [field, field.querySelector('.ca-field-label'), select].map(node => { const box = node.getBoundingClientRect(); return { left: box.left, right: box.right, width: box.width }; }), width: document.documentElement.clientWidth }; }); for (const box of bounds.boxes) { expect(box.width).toBeGreaterThan(0); expect(box.left).toBeGreaterThanOrEqual(-1); expect(box.right).toBeLessThanOrEqual(bounds.width + 1); } expect(bounds.intendedWrapper).toBe(true); for (const theme of ['follow', 'light', 'dark']) { await preference.selectOption(theme); await expect(preference).toHaveValue(theme); } await preference.focus(); await expect(preference).toBeFocused(); await noOverflow(page); await attachEvidence(page, testInfo, 'zoom-shell-390', false); }); async function containedImageContent(page) { return page.locator('article[data-image-key]').evaluateAll(nodes => { const failures = []; for (const card of nodes) { const outer = card.getBoundingClientRect(); for (const selector of ['.image-card-footer', 'strong', 'small', '.image-card-footer button']) { const node = card.querySelector(selector), box = node.getBoundingClientRect(); if (box.width <= 0 || box.height <= 0 || box.left < outer.left - 1 || box.right > outer.right + 1 || box.top < outer.top - 1 || box.bottom > outer.bottom + 1 || (selector === 'small' && node.scrollHeight > node.clientHeight + 1)) { failures.push({ key: card.dataset.imageKey, selector, card: { left: outer.left, right: outer.right, top: outer.top, bottom: outer.bottom }, content: { left: box.left, right: box.right, top: box.top, bottom: box.bottom } }); } } const action = card.querySelector('.image-card-footer button').getBoundingClientRect(); const range = document.createRange(); range.selectNodeContents(card.querySelector('small')); for (const text of range.getClientRects()) { if (Math.min(text.right, action.right) - Math.max(text.left, action.left) > .5 && Math.min(text.bottom, action.bottom) - Math.max(text.top, action.top) > .5) { failures.push({ key: card.dataset.imageKey, selector: 'facts/action overlap', text: { left: text.left, right: text.right, top: text.top, bottom: text.bottom }, action: { left: action.left, right: action.right, top: action.top, bottom: action.bottom } }); } } } return failures; }); } for (const [width, height] of [[390, 844], [1600, 1100]]) for (const layout of ['Grid', 'List', 'Masonry']) { test(`${layout} contains real card text and keyboard actions at ${width}px with 200 percent zoom`, async ({ page, request }, testInfo) => { await request.post('/__test__/reset', { data: { workspace_visual: true } }); await page.setViewportSize({ width, height }); await page.goto(`${workspace}#/images`); const cards = page.locator('article[data-image-key]'); await expect(cards).toHaveCount(24); await page.getByRole('button', { name: layout, exact: true }).click(); const firstKey = await cards.first().getAttribute('data-image-key'); for (const zoom of [1, 2]) { await page.evaluate(zoom => { document.documentElement.style.zoom = String(zoom); }, zoom); for (const density of ['comfortable', 'large', 'compact']) { await page.getByRole('combobox', { name: 'Thumbnail size', exact: true }).selectOption(density); await expect.poll(() => containedImageContent(page)).toEqual([]); await orderedMedia(page); const columns = await cards.evaluateAll(nodes => { const top = nodes[0].getBoundingClientRect().top; return nodes.filter(node => Math.abs(node.getBoundingClientRect().top - top) < 1).length; }); if (zoom === 1) expect(columns).toBe(layout === 'List' ? 1 : width === 390 || density === 'large' ? 2 : density === 'compact' ? 4 : 3); else expect(columns).toBe(layout === 'List' || width === 390 ? 1 : density === 'large' ? 2 : density === 'compact' ? 4 : 3); if (zoom === 1 && layout === 'List') await expect(cards.first()).toHaveCSS('height', '97px'); await cards.first().locator('.image-open').focus(); await page.keyboard.press('ArrowDown'); await expect(cards.nth(columns).locator('.image-open')).toBeFocused(); await page.keyboard.press('ArrowUp'); await expect(cards.first().locator('.image-open')).toBeFocused(); expect(await cards.first().getAttribute('data-image-key')).toBe(firstKey); } const longCard = cards.filter({ hasText: 'a-very-long-geometric-composition-' }); const filename = await longCard.locator('strong').textContent(); expect(filename).toMatch(/layered-context-.*\.png/); await longCard.locator('.image-open').focus(); await page.keyboard.press('Enter'); await expect(page.locator('.image-details h2')).toHaveText(filename); const fullPreview = page.getByRole('button', { name: 'Open full preview', exact: true }); await fullPreview.focus(); await expect(fullPreview).toBeFocused(); await page.keyboard.press('Enter'); await expect(page.getByRole('dialog', { name: 'Image preview', exact: true })).toBeVisible(); await expect(page.locator('.image-preview > header h2')).toHaveText(filename); await page.keyboard.press('Escape'); await longCard.locator('.image-open').focus(); await page.keyboard.press('Tab'); await expect(longCard.getByRole('button', { name: `Preview ${filename}`, exact: true })).toBeFocused(); await page.keyboard.press('Enter'); await expect(page.locator('dialog[open]')).toHaveCount(1); await page.keyboard.press('Escape'); await expect.poll(() => containedImageContent(page)).toEqual([]); await orderedMedia(page); await page.evaluate(() => scrollTo(0, 0)); await attachEvidence(page, testInfo, `zoom-${layout.toLowerCase()}-${width}-${zoom}`, false); } }); } test('zoomed virtual Images keeps the visible real media mounted during native scrolling', async ({ page, request }) => { await page.addInitScript(() => { window.IntersectionObserver = undefined; }); await request.post('/__test__/reset', { data: { extra_images: 200 } }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto(`${workspace}#/images`); const cards = page.locator('article[data-image-key]'); await expect(cards).toHaveCount(40); await page.evaluate(() => { document.documentElement.style.zoom = '2'; }); await expect.poll(() => cards.nth(1).evaluate(node => node.offsetLeft)).toBe(0); const stride = await cards.first().evaluate(node => node.getBoundingClientRect().height + 26); const more = page.getByRole('button', { name: 'Load more images', exact: true }); for (const loaded of [80, 120, 160, 200, 202]) { await more.click(); await expect(page.locator('.image-toolbar')).toContainText(`${loaded} of 202 images`); } await expect.poll(() => cards.count()).toBeLessThan(120); await page.evaluate(stride => { const grid = document.querySelector('.image-grid'); scrollTo(0, grid.getBoundingClientRect().top + scrollY + stride * 100 - 120); }, stride); await expect(page.locator('article[data-index="100"]')).toBeVisible(); await expect(page.locator('article[data-index="101"]')).toHaveCount(1); const visible = page.locator('article[data-index="100"]'); await visible.locator('.image-open').focus(); await page.keyboard.press('ArrowDown'); await expect(page.locator('article[data-index="101"] .image-open')).toBeFocused(); await page.keyboard.press('ArrowUp'); await expect(visible.locator('.image-open')).toBeFocused(); await expect(visible).toBeInViewport(); await expect.poll(() => containedImageContent(page)).toEqual([]); await orderedMedia(page); await expect.poll(() => cards.count()).toBeLessThan(120); }); test('supplemental extreme zoom grows real footers preserves the visible anchor and resets reservations', async ({ page, request }) => { await request.post('/__test__/reset', { data: { workspace_visual: true } }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto(`${workspace}#/images`); const cards = page.locator('article[data-image-key]'), grid = page.locator('.image-grid'); await expect(cards).toHaveCount(24); await page.evaluate(() => { document.documentElement.style.zoom = '2'; }); await expect.poll(() => cards.nth(1).evaluate(node => node.offsetLeft)).toBe(0); const selected = cards.nth(16), key = await selected.getAttribute('data-image-key'); await selected.locator('.image-open').click(); await selected.locator('.image-open').focus(); const filename = await selected.locator('strong').textContent(); await selected.evaluate(node => scrollBy(0, node.getBoundingClientRect().top - 120)); await page.evaluate(() => { const grid = document.querySelector('.image-grid'), footers = [...grid.querySelectorAll('.image-card-footer')]; const nativeHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight').get; const descriptors = footers.map(node => [node, Object.getOwnPropertyDescriptor(node, 'offsetHeight')]); const restore = () => { for (const [node, descriptor] of descriptors) { if (descriptor) Object.defineProperty(node, 'offsetHeight', descriptor); else delete node.offsetHeight; } }; window.footerMeasurementProbe = { before: null, restore }; for (const footer of footers) Object.defineProperty(footer, 'offsetHeight', { configurable: true, get() { const height = nativeHeight.call(this); if (document.documentElement.style.zoom === '4' && grid.style.getPropertyValue('--image-footer-height') === '54px' && Math.abs(Number.parseFloat(this.closest('.image-card').style.width) - Number.parseFloat(getComputedStyle(grid).width)) < .01) { const top = visualViewport?.offsetTop || 0, bottom = top + (visualViewport?.height || innerHeight); const first = [...grid.children].find(node => { const box = node.getBoundingClientRect(); return box.bottom > top && box.top < bottom; }); window.footerMeasurementProbe.before = first ? { index: Number(first.dataset.index), key: first.dataset.imageKey, top: first.getBoundingClientRect().top - top } : null; restore(); } return height; } }); document.documentElement.style.zoom = '4'; const box = grid.getBoundingClientRect(); // The approved initial reservation is 54px with 4:3 media and a 13px row gap. const stride = (box.width / 4 - 2) * .75 + 54 + 2 + 13; // Place row eight inside the 52px scaled gap so the preceding row is not visible. const index = 8; scrollTo(0, box.top + scrollY + index * stride * 4 - 16); }); try { await expect.poll(() => page.evaluate(() => window.footerMeasurementProbe.before?.index)).toBe(8); const anchor = await page.evaluate(() => window.footerMeasurementProbe.before); await expect.poll(() => grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBeGreaterThan(54); await expect.poll(() => containedImageContent(page)).toEqual([]); const visibleAnchor = page.locator(`article[data-index="${anchor.index}"]`); const anchorTop = await visibleAnchor.evaluate(node => node.getBoundingClientRect().top - (visualViewport?.offsetTop || 0)); expect(Math.abs(anchorTop - anchor.top)).toBeLessThanOrEqual(2); await expect(page.locator(`article[data-image-key="${key}"]`)).toHaveClass(/selected/); await expect(selected.locator('.image-open')).toBeFocused(); await expect(page.locator('.image-details h2')).toHaveText(filename); const mutations = await grid.evaluate(async node => { let changes = 0; const observer = new MutationObserver(records => { changes += records.length; }); observer.observe(node, { subtree: true, attributes: true, attributeFilter: ['style'] }); for (let frame = 0; frame < 6; frame++) await new Promise(requestAnimationFrame); observer.disconnect(); return changes; }); expect(mutations).toBe(0); await page.getByRole('button', { name: 'List', exact: true }).click(); await expect.poll(() => containedImageContent(page)).toEqual([]); expect(await cards.first().evaluate(node => node.offsetHeight)).toBeGreaterThan(97); await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('no-real-media-match'); await expect(cards).toHaveCount(0); expect(await grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBe(54); await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill(''); await expect(cards).toHaveCount(24); await expect.poll(() => grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBeGreaterThan(54); const extremeFooter = await grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height'))); await page.evaluate(() => { document.documentElement.style.zoom = '2'; }); await expect.poll(() => grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBeLessThan(extremeFooter); await expect(cards.first()).toHaveCSS('height', '97px'); await expect.poll(() => containedImageContent(page)).toEqual([]); await page.getByRole('button', { name: 'Grid', exact: true }).click(); await expect.poll(() => grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBe(54); await orderedMedia(page); } finally { await page.evaluate(() => { window.footerMeasurementProbe?.restore(); delete window.footerMeasurementProbe; }); } }); test('supplemental wrapped real footer reservation survives append without moving the visible card', async ({ page, request }) => { await page.addInitScript(() => { window.IntersectionObserver = undefined; }); await request.post('/__test__/reset', { data: { extra_images: 200 } }); await page.setViewportSize({ width: 390, height: 844 }); await page.goto(`${workspace}#/images`); const cards = page.locator('article[data-image-key]'), grid = page.locator('.image-grid'); await expect(cards).toHaveCount(40); await cards.first().locator('.image-open').click(); const selectedKey = await cards.first().getAttribute('data-image-key'); await page.evaluate(() => { document.documentElement.style.zoom = '4'; }); await expect.poll(() => grid.evaluate(node => Number.parseFloat(node.style.getPropertyValue('--image-footer-height')))).toBeGreaterThan(54); let release, pending = false; const held = new Promise(resolve => { release = resolve; }); await page.route('**/images/output?**', async route => { if (new URL(route.request().url()).searchParams.has('cursor')) { pending = true; await held; } await route.continue(); }); try { const more = page.getByRole('button', { name: 'Load more images', exact: true }); await more.click(); await expect.poll(() => pending).toBe(true); const anchor = await cards.evaluateAll(nodes => { const top = visualViewport?.offsetTop || 0, bottom = top + (visualViewport?.height || innerHeight); const node = nodes.find(node => { const box = node.getBoundingClientRect(); return box.bottom > top && box.top < bottom; }); return node ? { key: node.dataset.imageKey, top: node.getBoundingClientRect().top - top } : null; }); expect(anchor).not.toBeNull(); release(); await expect(page.locator('.image-toolbar')).toContainText('80 of 202 images'); await expect(cards).toHaveCount(80); await expect.poll(async () => Math.abs(await page.locator(`article[data-image-key="${anchor.key}"]`).evaluate(node => node.getBoundingClientRect().top - (visualViewport?.offsetTop || 0)) - anchor.top)).toBeLessThanOrEqual(1); await expect(page.locator(`article[data-image-key="${selectedKey}"]`)).toHaveClass(/selected/); await expect(more).toBeFocused(); await expect.poll(() => containedImageContent(page)).toEqual([]); } finally { release(); } }); test('Library visual evidence keeps natural cards and returns from real history in both themes',async({browser,browserName,request},testInfo)=>{ await request.post('/__test__/reset',{data:{workspace_visual:true}}); for(const [width,height] of [[1600,1100],[390,844]]) for(const theme of ['dark','light']) { const context=await browser.newContext({viewport:{width,height},locale:'en-US',timezoneId:'UTC',reducedMotion:'reduce'}); const page=await context.newPage();const errors=[];page.on('pageerror',error=>errors.push(error.message));await startCoverage(page,browserName); try { await page.goto(`${workspace}#/library`);await expect(page.locator('article[data-prompt-id]')).toHaveCount(6); await page.getByRole('combobox',{name:'Appearance',exact:true}).selectOption(theme); await attachEvidence(page,testInfo,`after-library-${width}-${theme}`,false); await noOverflow(page); await page.getByRole('link',{name:'Explore history',exact:true}).first().click(); await expect(page.getByRole('region',{name:'Prompt history',exact:true})).toBeVisible(); await expect(page.locator('.library-history > div > .intro')).toHaveCount(1); await page.getByRole('button',{name:'← Prompt library',exact:true}).click(); await expect(page.locator('article[data-prompt-id]')).toHaveCount(6); await expect(page.locator('.workspace-heading-copy > .intro')).toHaveCount(1); expect(errors).toEqual([]); } finally {await saveCoverage(page,browserName);await context.close();} } }); test('approved icons are decorative safe SVGs and reject unknown vocabulary', async ({ page }) => { await page.goto(`${workspace}#/images`); const icons = await page.evaluate(async () => { const { createWorkspaceIcon } = await import('/prompt_manager/js/workspace/icons.mjs'); const names = ['images', 'library', 'tags', 'upload', 'overview', 'settings', 'grid', 'list', 'masonry', 'search', 'info', 'copy', 'branch']; const otherDocument = document.implementation.createHTMLDocument(); const rendered = names.map(name => { const icon = createWorkspaceIcon(name, otherDocument); return { name, svg: icon instanceof SVGSVGElement, owner: icon.ownerDocument === otherDocument, viewBox: icon.getAttribute('viewBox'), hidden: icon.getAttribute('aria-hidden'), focusable: icon.getAttribute('focusable'), fill: icon.getAttribute('fill'), stroke: icon.getAttribute('stroke'), width: icon.getAttribute('stroke-width'), path: icon.querySelector('path').getAttribute('d'), children: icon.childElementCount }; }); const unknown = ['missing', '__proto__', '', null, 1].map(name => { try { createWorkspaceIcon(name); return false; } catch (error) { return error instanceof TypeError; } }); return { rendered, unknown }; }); expect(icons.unknown).toEqual([true, true, true, true, true]); for (const icon of icons.rendered) { expect(icon).toMatchObject({ svg: true, owner: true, viewBox: '0 0 24 24', hidden: 'true', focusable: 'false', fill: 'none', stroke: 'currentColor', width: '1.65', children: 1 }); expect(icon.path.length).toBeGreaterThan(10); } expect(icons.rendered.find(icon => icon.name === 'overview').path).toBe('M3 12h4l3-8 4 16 3-8h4'); }); async function orderedMedia(page) { const boxes=await page.locator('article[data-image-key]').evaluateAll(nodes=>nodes.map(node=>{ const box=node.getBoundingClientRect();return {x:box.x,y:box.y,right:box.right,bottom:box.bottom}; })); for (let index=0;index{ await request.post('/__test__/reset',{data:{workspace_visual:true}}); let release, mediaRequests=0;const held=new Promise(resolve=>{release=resolve;}); await page.route('**/images/serve/**',async route=>{mediaRequests++;await held;await route.continue().catch(()=>{});}); try { await page.setViewportSize({width:1600,height:1100});await page.goto(`${workspace}#/images`,{waitUntil:'domcontentloaded'}); await expect(page.locator('article[data-image-key]')).toHaveCount(24); await expect.poll(()=>mediaRequests).toBeGreaterThan(0); expect(await page.locator('.image-card img').first().evaluate(image=>image.complete)).toBe(false); await expect(page.locator('.image-card').first()).toHaveCSS('border-radius','7px'); await orderedMedia(page); } finally {release();} for (const [width,height] of [[1600,1100],[390,844]]) { await page.setViewportSize({width,height}); for (const theme of ['dark','light']) { await page.getByRole('combobox',{name:'Appearance',exact:true}).selectOption(theme); await page.locator('.image-card .image-open').first().click(); await expect(page.locator('.image-details h2')).toHaveText('study-01.png'); for (const layout of ['Grid','List','Masonry']) { await page.getByRole('button',{name:layout,exact:true}).click();await page.evaluate(()=>scrollTo(0,0)); if(layout==='List') await expect(page.locator('.image-card').first()).toHaveCSS('height','97px'); await orderedMedia(page); await attachEvidence(page,testInfo,`after-images-${layout.toLowerCase()}-${width}-${theme}`,false); } await page.locator('article[data-image-key]').last().scrollIntoViewIfNeeded(); const longCard=page.locator('article[data-image-key]').filter({hasText:'a-very-long-geometric-composition-'}); await expect(longCard.locator('strong')).toHaveText(/layered-context-.*\.png/); await longCard.locator('.image-open').click(); await expect(page.locator('.image-details h2')).toHaveText(await longCard.locator('strong').textContent()); await orderedMedia(page); await attachEvidence(page,testInfo,`after-images-loaded-scroll-${width}-${theme}`,false); } } await page.evaluate(()=>{location.hash='#/overview';}); await expect(page.locator('.image-card,.image-details,.viewer-container,dialog[open]')).toHaveCount(0); }); test('embedded history gallery keeps the history heading and disposes its layouts',async({page,request},testInfo)=>{ await request.post('/__test__/reset',{data:{workspace_visual:true}}); for (const width of [1600,390]) { await page.setViewportSize({width,height:width===1600?1100:844});await page.goto(`${workspace}#/library`); await page.getByRole('link',{name:'Explore history',exact:true}).first().click(); await expect(page.getByRole('region',{name:'Prompt history',exact:true})).toBeVisible(); await expect(page.locator('h1')).not.toHaveText('Prompt library'); const title=await page.locator('h1').textContent();expect(title).not.toBe('Images'); // History opens on its first negative variant (concept navigate: state.variant = 0); the layout contract uses the whole gallery. await page.getByRole('button',{name:'All images for this positive',exact:true}).click(); await expect(page.locator('article[data-image-key]').first()).toBeVisible(); for (const layout of ['List','Masonry','Grid']) { await page.getByRole('button',{name:layout,exact:true}).click();await orderedMedia(page); await expect(page.locator('h1')).toHaveText(title); await expect(page.locator('.workspace-heading-copy > .intro')).toHaveCount(0); } await attachEvidence(page,testInfo,`after-images-embedded-history-${width}`,false); await page.getByRole('button',{name:'← Prompt library',exact:true}).click(); await expect(page.locator('.image-card,.image-details,dialog[open]')).toHaveCount(0); } });