* feat(inspector): expose bounded physical file evidence * fix(inspector): distinguish ambiguous saved associations * feat(inspector): derive conservative generation facts * fix(inspector): guard fact ancestry and inspection state * feat(inspector): share complete file details and exports * fix(inspector): preserve fact states and touch targets * feat(viewer): navigate the loaded media collection Refs #216 * fix(viewer): refit after current stage geometry changes Ref #216. Observe the current media stage after readiness and wrapped title changes without rebuilding focused controls or resetting transforms at unchanged dimensions. * feat(viewer): add lifecycle-safe slideshow and media preferences * fix(viewer): resume gallery continuation after close (#216) * fix(images): guard source recovery and wrap viewer actions Clear stale root and folder intent, cancel old pages, and wait for current source discovery before loading a refreshed collection. Preserve safe retry through failed, superseded, and disposed discovery. Wrap narrow viewer footer actions and wait for measured gallery viewport settling in responsive checks. Refs #216; addresses PR #224 review. * test(viewer): await gallery layout after viewport changes Require actual inner, root client, and document widths to agree after each responsive-case viewport and CSS zoom transition. Keep the existing exact frame, overflow, focus, and coarse target assertions unchanged. Observed PR #224 CI initial mobile resize at537px before pending card relayout settled to356px. Refs #216.
744 lines
51 KiB
JavaScript
744 lines
51 KiB
JavaScript
import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs';
|
|
import { test, expect } from '../../tools/vendor/node_modules/@playwright/test/index.mjs';
|
|
|
|
const workspace = '/prompt_manager/workspace#/images';
|
|
const cards = page => page.locator('article[data-image-key]');
|
|
const details = page => page.getByRole('region', { name: 'Image details', exact: true });
|
|
|
|
test.beforeEach(async ({ page, request, browserName }) => {
|
|
await startCoverage(page, browserName);
|
|
await expect(await request.post('/__test__/reset')).toBeOK();
|
|
page.imageErrors = [];
|
|
page.on('pageerror', error => page.imageErrors.push(error.message));
|
|
});
|
|
|
|
test.afterEach(async ({ page, browserName }) => {
|
|
await saveCoverage(page, browserName);
|
|
expect(page.imageErrors).toEqual([]);
|
|
});
|
|
|
|
test('native Images keeps colliding filenames distinct and inspects the selected source', async ({ page, request }, testInfo) => {
|
|
await page.setViewportSize({width:1600,height:1100});
|
|
async function capture(name) {
|
|
await page.evaluate(async()=>{await document.fonts.ready;await Promise.all([...document.images].map(image=>image.decode().catch(()=>{})));});
|
|
const path=testInfo.outputPath(`${name}.png`);await page.screenshot({path});
|
|
await testInfo.attach(name,{path,contentType:'image/png'});
|
|
}
|
|
const snapshot = await (await request.get('/prompt_manager/stats')).json();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
expect(await cards(page).evaluateAll(elements => elements.map(element => element.dataset.imageKey)))
|
|
.toEqual(['0:shared/same.png', '1:shared/same.png']);
|
|
const originals = await cards(page).locator('img').evaluateAll(elements => elements.map(element => element.src));
|
|
expect(new Set(originals).size).toBe(2);
|
|
await cards(page).nth(1).getByRole('button', { name: 'Open same.png', exact: true }).click();
|
|
await expect(details(page)).toContainText('same.png');
|
|
await expect(details(page)).toContainText('shared/same.png');
|
|
await expect(details(page)).toContainText(/input/i);
|
|
await capture('after-images-source-collision-input-1600');
|
|
await cards(page).first().getByRole('button', { name: 'Open same.png', exact: true }).click();
|
|
await expect(details(page)).toContainText(/output/i);
|
|
await capture('after-images-source-collision-output-1600');
|
|
await page.setViewportSize({width:390,height:844});
|
|
for (const [index,source] of [[1,'input'],[0,'output']]) {
|
|
await cards(page).nth(index).getByRole('button',{name:'Open same.png',exact:true}).click();
|
|
await expect(details(page)).toContainText(new RegExp(source,'i'));
|
|
await details(page).scrollIntoViewIfNeeded();
|
|
await capture(`after-images-source-collision-${source}-390`);
|
|
}
|
|
expect((await (await request.get('/prompt_manager/stats')).json()).stats).toEqual(snapshot.stats);
|
|
});
|
|
|
|
test('workspace defaults to the native Images collection without a hash route', async ({ page }) => {
|
|
await page.goto('/prompt_manager/workspace');
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Images');
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await expect(page.getByRole('navigation', { name: 'Workspace', exact: true })
|
|
.getByRole('link', { name: 'Images', exact: true })).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
test('image selection is announced on the selected image button', async ({ page }) => {
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
const selected = cards(page).last().getByRole('button', { name: 'Open same.png', exact: true });
|
|
await selected.click();
|
|
await expect(selected).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(cards(page).first().getByRole('button', { name: 'Open same.png', exact: true }))
|
|
.toHaveAttribute('aria-pressed', 'false');
|
|
});
|
|
|
|
test('closing details preview restores its trigger focus', async ({ page }) => {
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
const selected = cards(page).last().getByRole('button', { name: 'Open same.png', exact: true });
|
|
await selected.click();
|
|
const preview = details(page).getByRole('button', { name: 'Open full preview', exact: true });
|
|
await preview.click();
|
|
await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
|
|
await page.keyboard.press('Escape');
|
|
await expect(page.locator('dialog[open]')).toHaveCount(0);
|
|
await expect(preview).toBeFocused();
|
|
});
|
|
|
|
test('a short visible batch automatically continues until the sentinel is filled or exhausted', async ({ page, request }) => {
|
|
const real = await (await request.get('/prompt_manager/images/output?limit=40')).json();
|
|
let reads = 0;
|
|
await page.route('**/prompt_manager/images/output?*', async route => {
|
|
const current = reads++;
|
|
await route.fulfill({ json: {
|
|
success: true, images: [real.images[current]], total: 2,
|
|
has_more: current === 0, next_cursor: current === 0 ? 'next-short-batch' : null,
|
|
} });
|
|
});
|
|
await page.setViewportSize({ width: 1440, height: 1200 });
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
expect(reads).toBe(2);
|
|
await expect(page.getByRole('button', { name: 'Load more images', exact: true })).toBeHidden();
|
|
});
|
|
|
|
for (const embedded of [false, true]) {
|
|
for (const close of ['Close', 'Escape', 'parent abort']) {
|
|
test(`viewer closure resumes native continuation after a short pending page: ${embedded ? 'embedded' : 'Images'} ${close}`, async ({ page, request }, testInfo) => {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 80 } })).toBeOK();
|
|
const data = await (await request.get('/prompt_manager/images/output?limit=200')).json();
|
|
await page.setViewportSize({ width: 1600, height: 1100 });
|
|
await page.route('**/prompt_manager/workspace', async route => {
|
|
const response = await route.fetch();
|
|
const html = (await response.text()).replace('<script type="module" src="js/workspace/index.mjs"></script>', '');
|
|
await route.fulfill({ response, body: html });
|
|
});
|
|
await page.goto('/prompt_manager/workspace');
|
|
await page.evaluate(async ({ images, embedded }) => {
|
|
const { mountImages } = await import('/prompt_manager/js/workspace/images.mjs');
|
|
const { createScope } = await import('/prompt_manager/js/shared/toolkit/core.js');
|
|
window.ApiClient = { resolveUrl: value => value };
|
|
const fixture = window.continuationFixture = { reads: [], scope: createScope() };
|
|
const api = { get: async (url, options) => (await fetch(url, options)).json() };
|
|
const fetchPage = ({ parameters }) => {
|
|
fixture.reads.push(parameters.get('cursor'));
|
|
if (fixture.reads.length === 1) return { images: images.slice(0, 40), total: 80, has_more: true, next_cursor: 'next-0' };
|
|
if (fixture.reads.length === 2) return new Promise(resolve => {
|
|
fixture.finishPage = () => resolve({ images: [], total: 80, has_more: true, next_cursor: 'next-1' });
|
|
});
|
|
return { images: images.slice(40, 80), total: 80, has_more: false, next_cursor: null };
|
|
};
|
|
mountImages(document.querySelector('#workspace-main'), api, fixture.scope, { embedded, fetchPage });
|
|
}, { images: data.images, embedded });
|
|
await expect(cards(page)).toHaveCount(40);
|
|
expect(await page.evaluate(() => window.continuationFixture.reads)).toEqual([null]);
|
|
await page.locator('.image-continuation').scrollIntoViewIfNeeded();
|
|
await expect.poll(() => page.evaluate(() => window.continuationFixture.reads)).toEqual([null, 'next-0']);
|
|
const key = await cards(page).last().getAttribute('data-image-key');
|
|
const preview = page.locator(`article[data-image-key="${key}"]`).getByRole('button', { name: /^Preview / });
|
|
await preview.click();
|
|
const viewer = page.getByRole('dialog', { name: 'Image preview', exact: true });
|
|
await expect(viewer.locator('.viewer-canvas img')).toBeVisible();
|
|
await page.evaluate(() => window.continuationFixture.finishPage());
|
|
await expect(page.getByRole('button', { name: 'Load more images', exact: true })).toHaveAttribute('aria-disabled', 'false');
|
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
expect(await page.evaluate(() => ({ reads: window.continuationFixture.reads, nativeObserver: typeof IntersectionObserver, visible: document.querySelector('.image-continuation').getBoundingClientRect().top <= innerHeight })))
|
|
.toEqual({ reads: [null, 'next-0'], nativeObserver: 'function', visible: true });
|
|
if (close === 'parent abort') await page.evaluate(() => window.continuationFixture.scope.dispose());
|
|
else if (close === 'Close') await viewer.getByRole('button', { name: 'Close preview', exact: true }).click();
|
|
else await page.keyboard.press('Escape');
|
|
await expect(viewer).toHaveCount(0);
|
|
if (close !== 'parent abort') {
|
|
await expect(preview).toBeFocused();
|
|
await expect.poll(() => page.evaluate(() => window.continuationFixture.reads)).toEqual([null, 'next-0', 'next-1']);
|
|
await expect(page.locator('.image-toolbar')).toContainText(embedded ? '80 loaded' : '80 of 80');
|
|
}
|
|
await page.evaluate(() => window.continuationFixture.scope.dispose());
|
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
const reads = await page.evaluate(() => window.continuationFixture.reads);
|
|
expect(reads).toEqual(close === 'parent abort' ? [null, 'next-0'] : [null, 'next-0', 'next-1']);
|
|
await testInfo.attach('native-continuation-reads', { body: JSON.stringify({ embedded, close, reads }), contentType: 'application/json' });
|
|
});
|
|
}
|
|
}
|
|
|
|
test('source, folder and media filters operate on real backend records', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 3 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(5);
|
|
await page.getByRole('combobox', { name: 'Source', exact: true }).selectOption('1');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page).first()).toHaveAttribute('data-image-key', '1:shared/same.png');
|
|
await page.getByRole('combobox', { name: 'Source', exact: true }).selectOption('all');
|
|
await page.getByRole('combobox', { name: 'Folder', exact: true }).selectOption('bulk');
|
|
await expect(cards(page)).toHaveCount(3);
|
|
await expect(cards(page).first()).toHaveAttribute('data-image-key', /^0:bulk\//);
|
|
await page.getByRole('combobox', { name: 'Media', exact: true }).selectOption('video');
|
|
await expect(cards(page)).toHaveCount(0);
|
|
await expect(page.locator('#workspace-main')).toContainText(/no (matching )?(images|media)/i);
|
|
await page.getByRole('combobox', { name: 'Media', exact: true }).selectOption('image');
|
|
await expect(cards(page)).toHaveCount(3);
|
|
await page.getByRole('combobox', { name: 'Folder', exact: true }).selectOption('');
|
|
await expect(cards(page)).toHaveCount(5);
|
|
});
|
|
|
|
test('filename search finds records beyond the initial batch and sort orders them', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 100 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(40);
|
|
await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('same.png');
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('0099');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page)).toHaveAttribute('data-image-key', '0:bulk/0099.png');
|
|
await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('same.png');
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await page.getByRole('combobox', { name: 'Sort', exact: true }).selectOption('oldest');
|
|
await expect(cards(page).first()).toHaveAttribute('data-image-key', '1:shared/same.png');
|
|
await page.getByRole('combobox', { name: 'Sort', exact: true }).selectOption('newest');
|
|
await expect(cards(page).first()).toHaveAttribute('data-image-key', '0:shared/same.png');
|
|
});
|
|
|
|
test('layout changes retain selection and use one collection in Grid, List and Masonry', async ({ page }) => {
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await cards(page).nth(1).getByRole('button', { name: 'Open same.png', exact: true }).click();
|
|
for (const layout of ['List', 'Masonry', 'Grid']) {
|
|
await page.getByRole('button', { name: layout, exact: true }).click();
|
|
await expect(page.getByRole('button', { name: layout, exact: true })).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await expect(details(page)).toContainText(/input/i);
|
|
const boxes = await cards(page).evaluateAll(elements => elements.map(element => {
|
|
const box = element.getBoundingClientRect();
|
|
return { x: box.x, y: box.y, right: box.right, bottom: box.bottom };
|
|
}));
|
|
expect(boxes[0].right <= boxes[1].x || boxes[1].right <= boxes[0].x ||
|
|
boxes[0].bottom <= boxes[1].y || boxes[1].bottom <= boxes[0].y).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('returning to Images restores the filtered stream, layout, selection and scroll context', async ({ page, request }) => {
|
|
await page.addInitScript(() => { window.IntersectionObserver = undefined; });
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 100 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
const search = page.getByRole('searchbox', { name: 'Search filenames', exact: true });
|
|
await page.getByRole('combobox', { name: 'Source', exact: true }).selectOption('0');
|
|
await search.fill('00');
|
|
await expect(page.locator('.image-toolbar')).toContainText('40 of 100 images');
|
|
await page.getByRole('button', { name: 'List', exact: true }).click();
|
|
await page.getByRole('button', { name: 'Load more images', exact: true }).click();
|
|
await expect(page.locator('.image-toolbar')).toContainText('80 of 100 images');
|
|
await cards(page).first().getByRole('button', { name: /^Open / }).click();
|
|
const selectedTitle = await details(page).getByRole('heading', { level: 2 }).textContent();
|
|
await cards(page).nth(30).scrollIntoViewIfNeeded();
|
|
const previousScroll = await page.evaluate(() => window.scrollY);
|
|
expect(previousScroll).toBeGreaterThan(500);
|
|
// Hash navigation preserves the scroll position being tested; clicking the fixed
|
|
// navigation can otherwise scroll a non-sticky mobile sidebar into view first.
|
|
await page.evaluate(() => { window.location.hash = '/overview'; });
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
await page.evaluate(() => { window.location.hash = '/images'; });
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Images');
|
|
await expect(search).toHaveValue('00');
|
|
await expect(page.getByRole('combobox', { name: 'Source', exact: true })).toHaveValue('0');
|
|
await expect(page.getByRole('button', { name: 'List', exact: true })).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(page.locator('.image-toolbar')).toContainText('80 of 100 images');
|
|
await expect(cards(page)).toHaveCount(80);
|
|
await expect(details(page).getByRole('heading', { level: 2 })).toHaveText(selectedTitle);
|
|
await expect.poll(async () => Math.abs((await page.evaluate(() => window.scrollY)) - previousScroll)).toBeLessThan(120);
|
|
});
|
|
|
|
test('continuous loading appends without replacing cards or losing selected details', async ({ page, request }) => {
|
|
// Exercise the keyboard fallback independently from IntersectionObserver scrolling.
|
|
await page.addInitScript(() => { window.IntersectionObserver = undefined; });
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 100 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(40);
|
|
await cards(page).first().evaluate(element => { element.dataset.preserved = 'yes'; });
|
|
await cards(page).first().getByRole('button', { name: /^Open / }).click();
|
|
const selected = await cards(page).first().getAttribute('data-image-key');
|
|
const more = page.getByRole('button', { name: 'Load more images', exact: true });
|
|
await more.focus();
|
|
await page.keyboard.press('Enter');
|
|
await expect(cards(page)).toHaveCount(80);
|
|
await expect(cards(page).first()).toHaveAttribute('data-preserved', 'yes');
|
|
await expect(cards(page).first()).toHaveAttribute('data-image-key', selected);
|
|
await expect(details(page)).toBeVisible();
|
|
await expect(more).toBeFocused();
|
|
await page.keyboard.press('Enter');
|
|
await expect(cards(page)).toHaveCount(102);
|
|
expect(new Set(await cards(page).evaluateAll(elements => elements.map(element => element.dataset.imageKey))).size).toBe(102);
|
|
await expect(more).toBeHidden();
|
|
await expect(page.getByRole('button', { name: /next page|previous page/i })).toHaveCount(0);
|
|
});
|
|
|
|
test('scrolling to the sentinel automatically appends the next backend batch', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 100 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(40);
|
|
const first = cards(page).first();
|
|
await first.evaluate(element => { element.dataset.preserved = 'scroll'; });
|
|
await page.getByRole('button', { name: 'Load more images', exact: true }).scrollIntoViewIfNeeded();
|
|
await expect(cards(page)).toHaveCount(80);
|
|
await expect(first).toHaveAttribute('data-preserved', 'scroll');
|
|
});
|
|
|
|
test('large continuous collections bound the mounted cards and restore earlier images on backscroll', async ({ page, request }) => {
|
|
await page.addInitScript(() => { window.IntersectionObserver = undefined; });
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 200 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(40);
|
|
const first = cards(page).first();
|
|
const firstKey = await first.getAttribute('data-image-key');
|
|
await first.getByRole('button', { name: /^Open / }).click();
|
|
const selectedTitle = await details(page).getByRole('heading', { level: 2 }).textContent();
|
|
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(more).toBeHidden();
|
|
await expect.poll(() => cards(page).count()).toBeLessThan(120);
|
|
await expect(details(page).getByRole('heading', { level: 2 })).toHaveText(selectedTitle);
|
|
expect(new Set(await cards(page).evaluateAll(elements => elements.map(element => element.dataset.imageKey))).size)
|
|
.toBe(await cards(page).count());
|
|
await page.evaluate(() => window.scrollTo(0, 0));
|
|
await expect(page.locator(`article[data-image-key="${firstKey}"]`)).toBeVisible();
|
|
await expect.poll(() => cards(page).count()).toBeLessThan(120);
|
|
await expect(details(page).getByRole('heading', { level: 2 })).toHaveText(selectedTitle);
|
|
await page.locator(`article[data-image-key="${firstKey}"]`)
|
|
.getByRole('button', { name: /^Preview / }).click();
|
|
await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
|
|
await page.getByRole('button', { name: 'Close preview', exact: true }).click();
|
|
});
|
|
|
|
test('virtual gallery keyboard navigation reaches loaded endpoints and announces collection positions', async ({ page, request }) => {
|
|
await page.addInitScript(() => { window.IntersectionObserver = undefined; });
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 200 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(40);
|
|
const firstKey = await cards(page).first().getAttribute('data-image-key');
|
|
const firstName = await cards(page).first().getByRole('button', { name: /^Open / }).getAttribute('aria-label');
|
|
await cards(page).first().getByRole('button', { name: /^Open / }).click();
|
|
const selectedTitle = await details(page).getByRole('heading', { level: 2 }).textContent();
|
|
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 page.evaluate(() => window.scrollTo(0, 0));
|
|
const first = page.locator(`article[data-image-key="${firstKey}"]`);
|
|
await expect(first).toBeVisible();
|
|
await first.getByRole('button', { name: firstName, exact: true }).focus();
|
|
await page.keyboard.press('End');
|
|
await expect(page.locator('article[data-image-key="1:shared/same.png"]')
|
|
.getByRole('button', { name: 'Open same.png', exact: true })).toBeFocused();
|
|
await expect(page.locator('article[data-image-key="1:shared/same.png"]')).toHaveAttribute('aria-posinset', '202');
|
|
await expect(page.locator('article[data-image-key="1:shared/same.png"]')).toHaveAttribute('aria-setsize', '202');
|
|
await expect(page.getByRole('list', { name: 'Image collection', exact: true })).toBeVisible();
|
|
await expect(page.locator('article[data-image-key="1:shared/same.png"]')).toHaveAttribute('role', 'listitem');
|
|
await page.keyboard.press('Home');
|
|
await expect(first.getByRole('button', { name: firstName, exact: true })).toBeFocused();
|
|
await expect(first).toHaveAttribute('aria-posinset', '1');
|
|
await page.keyboard.press('ArrowRight');
|
|
await expect(page.locator('article[aria-posinset="2"]').getByRole('button', { name: /^Open / })).toBeFocused();
|
|
await expect(details(page).getByRole('heading', { level: 2 })).toHaveText(selectedTitle);
|
|
await expect.poll(() => cards(page).count()).toBeLessThan(120);
|
|
});
|
|
|
|
test('preview uses actual Viewer.js and cleans up when closing or leaving Images', async ({ page }) => {
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await cards(page).last().getByRole('button', { name: 'Preview same.png', exact: true }).click();
|
|
await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
|
|
await expect(page.locator('dialog[open] .viewer-canvas img')).toHaveAttribute('src', /root=1/);
|
|
await page.getByRole('button', { name: 'Close preview', exact: true }).click();
|
|
await expect(page.locator('dialog[open]')).toHaveCount(0);
|
|
await cards(page).first().getByRole('button', { name: 'Preview same.png', exact: true }).click();
|
|
await expect(page.locator('dialog[open] .viewer-canvas img')).toBeVisible();
|
|
await page.evaluate(() => { window.location.hash = '/overview'; });
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
await expect(page.locator('.viewer-container')).toHaveCount(0);
|
|
await expect(page.locator('dialog[open]')).toHaveCount(0);
|
|
});
|
|
|
|
test('real video media has a usable card and native playback cleans up on close and navigation', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { include_video: true } })).toBeOK();
|
|
const media = await (await request.get('/prompt_manager/images/output?limit=40')).json();
|
|
expect(media.images.find(image => image.filename === 'clip.webm').thumbnail_url).toBeNull();
|
|
await page.goto(workspace);
|
|
await page.getByRole('combobox', { name: 'Media', exact: true }).selectOption('video');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
const card = cards(page).first();
|
|
await expect(card).toHaveAttribute('data-image-key', '0:clip.webm');
|
|
await expect(card.locator('img')).toHaveCount(0);
|
|
await expect(card.getByRole('button', { name: 'Open clip.webm', exact: true })).toBeVisible();
|
|
await card.getByRole('button', { name: 'Preview clip.webm', exact: true }).click();
|
|
const video = page.locator('dialog[open] video');
|
|
await expect(video).toBeVisible();
|
|
await expect(video).toHaveJSProperty('controls', true);
|
|
await expect.poll(() => video.evaluate(element => element.readyState)).toBeGreaterThanOrEqual(1);
|
|
const playing = await video.evaluate(async element => {
|
|
window.__previewVideo = element;
|
|
element.muted = true;
|
|
await element.play();
|
|
return !element.paused;
|
|
});
|
|
expect(playing).toBe(true);
|
|
await page.getByRole('button', { name: 'Close preview', exact: true }).click();
|
|
await expect(page.locator('dialog video')).toHaveCount(0);
|
|
expect(await page.evaluate(() => window.__previewVideo.paused && !window.__previewVideo.isConnected)).toBe(true);
|
|
await card.getByRole('button', { name: 'Preview clip.webm', exact: true }).click();
|
|
await expect(video).toBeVisible();
|
|
await video.evaluate(async element => {
|
|
window.__previewVideo = element;
|
|
element.muted = true;
|
|
await element.play();
|
|
});
|
|
await page.evaluate(() => { window.location.hash = '/overview'; });
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
await expect(page.locator('video')).toHaveCount(0);
|
|
expect(await page.evaluate(() => window.__previewVideo.paused && !window.__previewVideo.isConnected)).toBe(true);
|
|
});
|
|
|
|
test('collection viewer ignores delayed media callbacks and keeps inspection retries independent',async({page})=>{
|
|
await page.goto(workspace);await expect(cards(page)).toHaveCount(2);
|
|
await page.evaluate(()=>{const Native=window.Viewer;window.liveViewers=[];window.Viewer=class extends Native {constructor(...args){super(...args);window.liveViewers.push(this);}};});
|
|
let release,held;const waiting=new Promise(resolve=>{held=resolve;});const pending=new Promise(resolve=>{release=resolve;});
|
|
await page.route('**/images/serve/shared/same.png?*',async route=>{
|
|
const url=new URL(route.request().url());
|
|
if(url.searchParams.get('root')==='0'&&url.searchParams.has('_pm_viewer')) {held();await pending;}
|
|
await route.continue().catch(()=>{});
|
|
});
|
|
await cards(page).first().getByRole('button',{name:'Preview same.png',exact:true}).click();await waiting;
|
|
const preview=page.getByRole('dialog',{name:'Image preview',exact:true});
|
|
await expect(preview.getByRole('button',{name:'Zoom in',exact:true})).toHaveAttribute('aria-disabled','true');
|
|
await page.route('**/images/inspect/shared/same.png?*',route=>route.fulfill({status:503,json:{success:false,error:'Unavailable'}}),{times:1});
|
|
await preview.getByRole('button',{name:'Next media',exact:true}).click();
|
|
await expect(preview).toContainText('Could not read prompts from this file.');await expect(preview.getByRole('button',{name:'Zoom in',exact:true})).toHaveAttribute('aria-disabled','false');
|
|
await expect(preview.getByRole('group',{name:'Media controls',exact:true}).getByRole('link',{name:'Download original',exact:true})).toHaveAttribute('href',/root=1&root_scope=/);
|
|
await preview.getByRole('button',{name:'Rotate image 90 degrees',exact:true}).click();
|
|
await page.evaluate(()=>{window.latestCanvas=document.querySelector('.viewer-canvas img');window.liveViewers[0].options.viewed();});release();
|
|
expect(await page.evaluate(()=>window.liveViewers.at(-1).imageData.rotate)).toBe(90);
|
|
await preview.getByRole('button',{name:'Retry prompt details',exact:true}).click();
|
|
await expect(preview).toContainText('same.png');await expect(preview.getByRole('button',{name:'Retry prompt details',exact:true})).toHaveCount(0);
|
|
expect(await page.evaluate(()=>window.latestCanvas===document.querySelector('.viewer-canvas img'))).toBe(true);
|
|
await page.evaluate(()=>{location.hash='/overview';});await expect(page.locator('.viewer-container,.image-preview')).toHaveCount(0);
|
|
});
|
|
|
|
test('collection viewer describes video transforms and releases native playback on navigation',async({page,request})=>{
|
|
await expect(await request.post('/__test__/reset',{data:{include_video:true}})).toBeOK();await page.goto(workspace);
|
|
await page.locator('article[data-image-key="0:clip.webm"] .image-card-footer button').click();
|
|
const preview=page.getByRole('dialog',{name:'Image preview',exact:true}),video=preview.locator('video');
|
|
await expect(video).toHaveJSProperty('controls',true);await expect(video).toHaveJSProperty('playsInline',true);
|
|
const zoom=preview.getByRole('button',{name:'Zoom in',exact:true});await expect(zoom).toHaveAttribute('aria-disabled','true');await expect(zoom).toHaveAccessibleDescription('Image transforms are unavailable for video.');
|
|
await video.focus();const position=await preview.locator('.image-preview-position').textContent();await page.keyboard.press('ArrowRight');await expect(preview.locator('.image-preview-position')).toHaveText(position);
|
|
await video.evaluate(async node=>{window.previousClip=node;node.muted=true;await node.play();});
|
|
await preview.getByRole('button',{name:'Next media',exact:true}).click();await expect(preview.locator('video')).toHaveCount(0);await expect(zoom).toHaveAttribute('aria-disabled','false');
|
|
expect(await page.evaluate(()=>window.previousClip.paused&&!window.previousClip.isConnected&&!window.previousClip.hasAttribute('src'))).toBe(true);
|
|
await preview.getByRole('button',{name:'Close preview',exact:true}).click();await expect(page.locator('.image-preview')).toHaveCount(0);
|
|
});
|
|
|
|
test('failed image requests offer retry while stale results cannot replace a newer route', async ({ page }) => {
|
|
await page.route('**/prompt_manager/images/output?*', route => route.fulfill({
|
|
status: 503, json: { success: false, error: 'Images unavailable' },
|
|
}), { times: 1 });
|
|
await page.goto(workspace);
|
|
await expect(page.locator('#workspace-main').getByRole('alert')).toBeVisible();
|
|
await page.getByRole('button', { name: 'Retry', exact: true }).click();
|
|
await expect(cards(page)).toHaveCount(2);
|
|
let release;
|
|
const pending = new Promise(resolve => { release = resolve; });
|
|
let arrived;
|
|
const received = new Promise(resolve => { arrived = resolve; });
|
|
await page.route('**/prompt_manager/images/output?*', async route => {
|
|
const response = await route.fetch();
|
|
arrived();
|
|
await pending;
|
|
await route.fulfill({ response }).catch(() => {});
|
|
});
|
|
await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('nothing');
|
|
await received;
|
|
await page.getByRole('navigation', { name: 'Workspace', exact: true })
|
|
.getByRole('link', { name: 'Overview', exact: true }).click();
|
|
release();
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
await expect(cards(page)).toHaveCount(0);
|
|
await expect(details(page)).toHaveCount(0);
|
|
});
|
|
|
|
test('source and folder discovery failure stays visible and Refresh restores usable filters', async ({ page }) => {
|
|
await page.route('**/prompt_manager/gallery/subfolders?*', route => route.fulfill({
|
|
status: 503, json: { success: false, error: 'Sources unavailable' },
|
|
}), { times: 1 });
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await expect(page.getByRole('alert')).toHaveText('Sources and folders could not be loaded. Refresh to retry.');
|
|
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
|
|
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
await page.getByRole('combobox', { name: 'Source', exact: true }).selectOption('1');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page)).toHaveAttribute('data-image-key', '1:shared/same.png');
|
|
await page.getByRole('combobox', { name: 'Folder', exact: true }).selectOption('shared');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
});
|
|
|
|
async function staleSourceRecovery(page, request, roots) {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 80 } })).toBeOK();
|
|
await page.setViewportSize({ width: 1600, height: 1100 });
|
|
await page.goto(workspace);
|
|
const source = page.getByRole('combobox', { name: 'Source', exact: true });
|
|
const folder = page.getByRole('combobox', { name: 'Folder', exact: true });
|
|
await source.selectOption('0');await expect(folder.locator('option[value="bulk"]')).toHaveCount(1);await folder.selectOption('bulk');
|
|
const configured = page.waitForResponse(response => {
|
|
const url = new URL(response.url());return url.pathname.endsWith('/images/output') && url.searchParams.get('root') === '0' && url.searchParams.get('subfolder') === 'bulk' && url.searchParams.get('q') === 'png' && url.searchParams.get('media') === 'image' && url.searchParams.get('sort') === 'name';
|
|
});
|
|
await page.getByRole('searchbox', { name: 'Search filenames', exact: true }).fill('png');
|
|
await page.getByRole('combobox', { name: 'Media', exact: true }).selectOption('image');await page.getByRole('combobox', { name: 'Sort', exact: true }).selectOption('name');
|
|
const initial = await (await configured).json();await expect(page.locator('.image-toolbar')).toContainText('40 of 80');
|
|
let oldPage;
|
|
await page.route('**/prompt_manager/images/output?*', async route => {
|
|
if (new URL(route.request().url()).searchParams.has('cursor') && !oldPage) {
|
|
const response = await route.fetch();oldPage = { route, data: await response.json() };
|
|
} else await route.continue();
|
|
});
|
|
await page.getByRole('button', { name: 'Load more images', exact: true }).scrollIntoViewIfNeeded();await expect.poll(() => !!oldPage).toBe(true);
|
|
await expect(await request.post('/prompt_manager/settings', { data: { gallery_root_paths: roots } })).toBeOK();
|
|
const selected = initial.images.at(-1), key = `${selected.root_index}:${selected.path}`;
|
|
const stale = page.waitForResponse(response => response.url().includes('/images/inspect/') && response.status() === 409);
|
|
await page.locator(`article[data-image-key="${key}"] .image-open`).click();expect((await stale).status()).toBe(409);
|
|
await expect(details(page)).toContainText('The source changed. Refresh the image collection before inspecting this file.');
|
|
await page.locator(`article[data-image-key="${key}"] .image-card-footer button`).click();
|
|
const viewer = page.getByRole('dialog', { name: 'Image preview', exact: true });await expect(viewer).toContainText('The source changed.');
|
|
const pages = [], inspections = [], options = [], mutations = [];
|
|
page.on('request', request => {
|
|
const url = new URL(request.url());
|
|
if (url.pathname.endsWith('/images/output')) pages.push(url);
|
|
if (url.pathname.includes('/images/inspect/')) inspections.push(url);
|
|
if (request.method() !== 'GET') mutations.push(url.pathname);
|
|
});
|
|
await page.route('**/prompt_manager/gallery/subfolders?*', route => { options.push(route); });
|
|
await viewer.getByRole('button', { name: 'Refresh image collection', exact: true }).click();
|
|
await expect(viewer).toHaveCount(0);await expect.poll(() => options.length).toBe(1);
|
|
await oldPage.route.fulfill({ json: oldPage.data });
|
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
return { source, folder, pages, inspections, options, mutations };
|
|
}
|
|
|
|
for (const [name, roots] of [['reordered', ['input', 'output']], ['removed', ['input']]]) {
|
|
test(`source-change recovery clears a ${name} root before discovery and safe retry`, async ({ page, request }, testInfo) => {
|
|
const recovery = await staleSourceRecovery(page, request, roots);
|
|
try {
|
|
expect(recovery.pages.map(url => url.search)).toEqual([]);expect(recovery.inspections).toEqual([]);
|
|
await expect(recovery.source).toHaveValue('all');await expect(recovery.source.locator('option')).toHaveText(['All sources']);await expect(recovery.folder).toHaveValue('');
|
|
for (const name of ['Source', 'Folder', 'Media', 'Sort']) await expect(page.getByRole('combobox', { name, exact: true })).toBeDisabled();
|
|
await expect(page.getByRole('searchbox', { name: 'Search filenames', exact: true })).toBeDisabled();
|
|
await expect(page.getByRole('button', { name: 'Load more images', exact: true })).toBeDisabled();
|
|
await page.getByRole('button', { name: 'Load more images', exact: true }).evaluate(node => node.click());await recovery.source.dispatchEvent('change');
|
|
await recovery.options[0].fulfill({ status: 503, json: { success: false, error: 'Discovery unavailable' } });
|
|
await expect(page.getByRole('alert').filter({ hasText: 'Sources and folders could not be loaded.' })).toBeVisible();
|
|
expect(recovery.pages).toEqual([]);expect(recovery.inspections).toEqual([]);await expect(cards(page)).toHaveCount(0);
|
|
const retry = details(page).getByRole('button', { name: 'Refresh image collection', exact: true });await expect(retry).toBeEnabled();
|
|
if (testInfo.project.name === 'chromium' && name === 'reordered') await page.screenshot({ path: testInfo.outputPath('safe-source-discovery-failure.png') });
|
|
await retry.click();await expect.poll(() => recovery.options.length).toBe(2);
|
|
expect(new URL(recovery.options[1].request().url()).searchParams.has('root')).toBe(false);
|
|
const response = await recovery.options[1].fetch();await recovery.options[1].fulfill({ response });
|
|
await expect.poll(() => recovery.pages.length).toBe(1);
|
|
const query = recovery.pages[0].searchParams;expect(query.has('root')).toBe(false);expect(query.has('subfolder')).toBe(false);
|
|
expect(query.get('q')).toBe('png');expect(query.get('media')).toBe('image');expect(query.get('sort')).toBe('name');
|
|
await expect(recovery.source).toBeEnabled();await expect(recovery.source).toHaveValue('all');
|
|
await expect(recovery.source.locator('option')).toHaveText(['All sources', ...roots]);
|
|
await expect.poll(() => recovery.inspections.length).toBe(1);expect(recovery.inspections[0].searchParams.get('root')).toBe(name === 'reordered' ? '1' : '0');
|
|
expect(recovery.mutations).toEqual([]);
|
|
if (testInfo.project.name === 'chromium' && name === 'reordered') await page.screenshot({ path: testInfo.outputPath('safe-source-recovery-success.png') });
|
|
} finally { await expect(await request.post('/prompt_manager/settings', { data: { gallery_root_paths: ['output', 'input'] } })).toBeOK(); }
|
|
});
|
|
}
|
|
|
|
test('source-change recovery ignores superseded discovery and route disposal', async ({ page, request }) => {
|
|
const recovery = await staleSourceRecovery(page, request, ['input', 'output']);
|
|
try {
|
|
expect(recovery.pages).toEqual([]);await details(page).getByRole('button', { name: 'Refresh image collection', exact: true }).click();await expect.poll(() => recovery.options.length).toBe(2);
|
|
await recovery.options[0].fulfill({ json: { roots: [{ index: 0, label: 'Obsolete source label' }], subfolders: ['obsolete'] } });
|
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
expect(recovery.pages).toEqual([]);await expect(recovery.source.locator('option')).toHaveText(['All sources']);
|
|
await page.getByRole('navigation', { name: 'Workspace', exact: true }).getByRole('link', { name: 'Overview', exact: true }).click();
|
|
await recovery.options[1].fulfill({ json: { roots: [{ index: 0, label: 'Late source label' }], subfolders: ['late'] } });
|
|
await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');expect(recovery.pages).toEqual([]);expect(recovery.inspections).toEqual([]);expect(recovery.mutations).toEqual([]);
|
|
} finally { await expect(await request.post('/prompt_manager/settings', { data: { gallery_root_paths: ['output', 'input'] } })).toBeOK(); }
|
|
});
|
|
|
|
test('mobile Images keeps filters, layouts and selected details within the viewport', async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
for (const layout of ['List', 'Masonry', 'Grid']) {
|
|
await page.getByRole('button', { name: layout, exact: true }).click();
|
|
await cards(page).last().getByRole('button', { name: 'Open same.png', exact: true }).click();
|
|
await expect(details(page)).toContainText(/input/i);
|
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
|
|
}
|
|
await expect(page.getByRole('searchbox', { name: 'Search filenames', exact: true })).toBeVisible();
|
|
});
|
|
|
|
test('a delayed search cannot replace results for a newer filename query', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { extra_images: 3 } })).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(5);
|
|
let release;
|
|
let arrived;
|
|
let complete;
|
|
const pending = new Promise(resolve => { release = resolve; });
|
|
const received = new Promise(resolve => { arrived = resolve; });
|
|
const completed = new Promise(resolve => { complete = resolve; });
|
|
let delayed = false;
|
|
await page.route('**/prompt_manager/images/output?*', async route => {
|
|
if (delayed) return route.continue();
|
|
delayed = true;
|
|
const response = await route.fetch();
|
|
arrived();
|
|
await pending;
|
|
await route.fulfill({ response }).catch(() => {});
|
|
complete();
|
|
});
|
|
const search = page.getByRole('searchbox', { name: 'Search filenames', exact: true });
|
|
await search.fill('same.png');
|
|
await received;
|
|
await search.fill('0001');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page)).toHaveAttribute('data-image-key', '0:bulk/0001.png');
|
|
release();
|
|
await completed;
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page)).toHaveAttribute('data-image-key', '0:bulk/0001.png');
|
|
});
|
|
|
|
test('selected layout stays blue with readable text on hover in either theme', async ({ page }) => {
|
|
await page.goto(workspace);
|
|
const layout = page.getByRole('group', { name: 'Image layout' }).getByRole('button', { name: 'Masonry', exact: true });
|
|
for (const theme of ['light', 'dark']) {
|
|
await page.getByRole('combobox', { name: 'Appearance', exact: true }).selectOption(theme);
|
|
await layout.click();
|
|
await layout.hover();
|
|
await expect(layout).toHaveCSS('background-color', 'rgb(8, 119, 187)');
|
|
await expect(layout).toHaveCSS('color', 'rgb(255, 255, 255)');
|
|
}
|
|
});
|
|
|
|
test('refresh releases the previous inspector video source', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/reset', { data: { include_video: true } })).toBeOK();
|
|
await page.goto(workspace);
|
|
const card = cards(page).filter({ hasText: 'clip.webm' });
|
|
await card.getByRole('button', { name: 'Open clip.webm', exact: true }).click();
|
|
await expect(details(page).locator('video')).toBeVisible();
|
|
await details(page).locator('video').evaluate(element => { window.__oldInspectorVideo = element; });
|
|
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
|
|
await expect.poll(() => page.evaluate(() => !window.__oldInspectorVideo.isConnected)).toBe(true);
|
|
expect(await page.evaluate(() => window.__oldInspectorVideo.getAttribute('src'))).toBeNull();
|
|
});
|
|
|
|
test('Refresh rescans cached files and preserves the collection when rescan fails', async ({ page, request }) => {
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await expect(await request.post('/__test__/add-nested-image')).toBeOK();
|
|
await page.route('**/prompt_manager/gallery/rescan', route => route.fulfill({
|
|
status: 503, json: { success: false, error: 'Rescan unavailable' },
|
|
}), { times: 1 });
|
|
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
|
|
await expect(page.getByRole('alert')).toContainText('could not be refreshed');
|
|
await expect(cards(page)).toHaveCount(2);
|
|
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
|
|
await expect(cards(page)).toHaveCount(3);
|
|
await expect(cards(page).filter({ hasText: 'new-output.png' })).toHaveCount(1);
|
|
await expect(page.getByRole('alert')).toBeHidden();
|
|
});
|
|
|
|
test('folder discovery offers ancestors that filter nested images', async ({ page, request }) => {
|
|
await expect(await request.post('/__test__/add-nested-image')).toBeOK();
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(3);
|
|
const folder = page.getByRole('combobox', { name: 'Folder', exact: true });
|
|
await expect(folder.locator('option[value="project"]')).toHaveCount(1);
|
|
await folder.selectOption('project');
|
|
await expect(cards(page)).toHaveCount(1);
|
|
await expect(cards(page)).toContainText('new-output.png');
|
|
});
|
|
|
|
test('leaving during Refresh cancels it without replacing the next workspace', async ({ page }) => {
|
|
let release;
|
|
const pending = new Promise(resolve => { release = resolve; });
|
|
await page.route('**/prompt_manager/gallery/rescan', async route => {
|
|
await pending;
|
|
await route.fulfill({ json: { success: true } }).catch(() => {});
|
|
});
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(2);
|
|
const requested = page.waitForRequest('**/prompt_manager/gallery/rescan');
|
|
await page.getByRole('button', { name: 'Refresh', exact: true }).click();
|
|
await requested;
|
|
await page.getByRole('navigation', { name: 'Workspace', exact: true })
|
|
.getByRole('link', { name: 'Overview', exact: true }).click();
|
|
await expect(page.locator('#workspace-main h1')).toHaveText('Overview');
|
|
release();
|
|
await expect(page.getByRole('heading', { name: 'Continue creating', exact: true })).toBeVisible();
|
|
await expect(page.getByText('Images could not be refreshed. Try Refresh again.', { exact: true })).toHaveCount(0);
|
|
});
|
|
|
|
|
|
test('Images uses approved geometry while keeping real controls', async ({page,request}) => {
|
|
await request.post('/__test__/reset',{data:{workspace_visual:true}});
|
|
await page.setViewportSize({width:1600,height:1100});
|
|
await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(24);
|
|
await expect(page.locator('.workspace-heading-copy > .intro')).toHaveCount(1);
|
|
await expect(details(page)).toHaveCSS('width','310px');
|
|
await expect(page.locator('.image-workspace')).toHaveCSS('column-gap','22px');
|
|
for (const label of ['Source','Folder','Media','Sort','Thumbnail size']) await expect(page.getByRole('combobox',{name:label,exact:true})).toBeVisible();
|
|
await expect(page.getByRole('searchbox',{name:'Search filenames',exact:true})).toHaveAttribute('placeholder',/filename or folder/);
|
|
await expect(page.getByRole('link',{name:'Group by positive prompt',exact:true})).toHaveAttribute('href','#/library');
|
|
const geometry = await cards(page).evaluateAll(nodes=>nodes.slice(0,4).map(node=>{
|
|
const box=node.getBoundingClientRect(),media=node.querySelector('.image-open').getBoundingClientRect();
|
|
return {x:box.x,y:box.y,right:box.right,bottom:box.bottom,width:box.width,mediaWidth:media.width,mediaHeight:media.height};
|
|
}));
|
|
expect(geometry[1].x-geometry[0].right).toBeCloseTo(13,0);
|
|
expect(geometry[3].y-geometry[0].bottom).toBeCloseTo(13,0);
|
|
expect(geometry[3].x).toBeCloseTo(geometry[0].x,0);
|
|
expect(geometry[0].mediaWidth/geometry[0].mediaHeight).toBeCloseTo(4/3,2);
|
|
expect((await page.locator('.image-toolbar').boundingBox()).y).toBeLessThan(geometry[0].y);
|
|
await expect(cards(page).first()).toHaveCSS('outline-style','solid');
|
|
const cover=details(page).getByRole('button',{name:'Open full preview',exact:true});
|
|
await expect(cover).toHaveCSS('height','150px');
|
|
await expect(cover.locator('img')).toBeVisible();await expect(cover.locator('img')).toHaveCSS('height','150px');
|
|
expect(await cover.evaluate(node=>node===node.parentElement.firstElementChild&&node.tagName==='BUTTON')).toBe(true);
|
|
await page.setViewportSize({width:390,height:844});
|
|
await expect.poll(()=>cards(page).nth(1).evaluate(node=>node.offsetTop)).toBe(0);
|
|
await expect.poll(()=>cards(page).nth(2).evaluate(node=>node.offsetTop)).toBeGreaterThan(0);
|
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)).toBeLessThanOrEqual(1);
|
|
});
|
|
|
|
test('density changes and resize preserve identity focus and selection', async ({page,request}) => {
|
|
await request.post('/__test__/reset',{data:{workspace_visual:true}});
|
|
await page.setViewportSize({width:1600,height:1100});await page.goto(workspace);
|
|
await expect(cards(page)).toHaveCount(24);
|
|
const first=cards(page).first();await first.evaluate(node=>{window.originalImageCard=node;});
|
|
await first.getByRole('button',{name:/^Open /}).click();
|
|
const key=await first.getAttribute('data-image-key');
|
|
let imageReads=0;page.on('request',request=>{if(request.url().includes('/images/output?')) imageReads++;});
|
|
for (const layout of ['Grid','List','Masonry']) for (const [density,columns] of [['comfortable',3],['large',2],['compact',4]]) {
|
|
await page.getByRole('button',{name:layout,exact:true}).click();
|
|
await page.getByRole('combobox',{name:'Thumbnail size',exact:true}).selectOption(density);
|
|
await first.getByRole('button',{name:/^Open /}).focus();await page.keyboard.press('ArrowDown');
|
|
await expect(cards(page).nth(layout==='List'?1:columns).getByRole('button',{name:/^Open /})).toBeFocused();
|
|
await expect(first).toHaveClass(/selected/);
|
|
expect(await first.evaluate(node=>node===window.originalImageCard)).toBe(true);
|
|
}
|
|
expect(imageReads).toBe(0);
|
|
await page.evaluate(()=>{location.hash='#/overview';});await expect(page.locator('h1')).toHaveText('Overview');
|
|
await page.evaluate(()=>{location.hash='#/images';});await expect(page.getByRole('combobox',{name:'Thumbnail size',exact:true})).toHaveValue('compact');
|
|
await expect(page.locator(`article[data-image-key="${key}"]`)).toHaveClass(/selected/);
|
|
await page.getByRole('button',{name:'Grid',exact:true}).click();
|
|
for (const [width,columns] of [[951,3],[950,3],[700,2],[390,2]]) {
|
|
await page.setViewportSize({width,height:844});
|
|
await expect.poll(()=>cards(page).nth(columns).evaluate(node=>node.offsetLeft)).toBe(0);
|
|
await first.getByRole('button',{name:/^Open /}).focus();await page.keyboard.press('ArrowDown');
|
|
await expect(cards(page).nth(columns).getByRole('button',{name:/^Open /})).toBeFocused();
|
|
expect(await page.evaluate(()=>document.documentElement.scrollWidth-document.documentElement.clientWidth)).toBeLessThanOrEqual(1);
|
|
}
|
|
});
|