* fix(inspector): give image tags their own labelled section (#449) The inspector drew a file's tags as a bare chip row right under the negative prompt, so they read as part of it. The side panel, the viewer details and History's current file now draw them in labelled sections in the prompt sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags · N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are removed within a source only. Recorded as D154. * feat(inspect): choosing a file opens the viewer with its details (#448) On Inspect a file, choosing or dropping a file now opens the shared viewer on it at once, details showing, instead of previewing it and waiting for "Open file metadata". Details show on this page even when "Collapse viewer details by default" is on. An unreadable file or an AVI/WMV note stays on the page; the heading action reopens any selection. A new selection that starts while the viewer shows the previous file closes it first, so its blob URL is never revoked under an open element. Recorded as D155. * feat(viewer): full-screen toggle beside the close button, and a System default (#450) Every viewer gets a button before the close button that switches between Centered (today's framed dialog) and Full screen (the whole browser window, edge to edge); it names the view it switches to. In full screen the details stay toggleable. System > Appearance gains a Viewer surface with "Open viewer in: Centered / Full screen" and the moved "Collapse viewer details by default"; the choice is a browser display preference (viewerOpenMode). The button changes only the current view and hides at 700px and below, where the viewer is already full-bleed. Recorded as D156. * fix(inspect): open a video's viewer only once its preview settles (#448) Replacing a video while both the inline preview and the auto-opened viewer's video were still starting deadlocked WebKit (D62). The viewer now opens for a video once its inline preview reports metadata or an error (or after 15 s). * test(viewer): pin the D97 view-switch spec to the desktop width (#450) * test(inspect): cover the AVI note and an open dialog holding the auto-open back (#448) Frontend coverage for inspect.mjs fell below its floor (branches 91.04 < 92, functions 92.85 < 93): no spec reached the AVI toast or the open-dialog guard, and the video wait had a page-end callback no spec reached. The wait now ends through its own callback at page end, and two specs cover the AVI note and a selection landing under another dialog.
143 lines
8.9 KiB
JavaScript
143 lines
8.9 KiB
JavaScript
// #450 (TESTING.md D156): every viewer has a full-screen toggle beside ✕. Full screen fills the browser window edge to
|
||
// edge, Centered is the concept's framed dialog; the button names the view it switches to. System › Appearance ›
|
||
// Viewer › "Open viewer in" chooses how the viewer opens; the button changes the current view only.
|
||
import { test, expect } from './fixtures.mjs';
|
||
import { seedConcept, openProduction } from './concept-inventory.mjs';
|
||
import { parityPng } from './local-inspect-files.mjs';
|
||
|
||
test.use({ locale: 'en-US', timezoneId: 'UTC', reducedMotion: 'reduce' });
|
||
|
||
const CARDS = '.gallery-body > .image-grid > .media-card';
|
||
const viewer = page => page.locator('dialog#viewer-dialog[open]');
|
||
const modeButton = page => viewer(page).locator('.dialog-header > button#viewer-mode');
|
||
const details = page => viewer(page).locator('#viewer-details');
|
||
|
||
test.beforeEach(async ({ request }, testInfo) => { testInfo.manifest = await seedConcept(request); });
|
||
|
||
async function openImages(page, { width = 1600, height = 1100, theme = 'dark' } = {}) {
|
||
await page.setViewportSize({ width, height });
|
||
await openProduction(page, 'images', theme);
|
||
await page.locator(CARDS).first().locator('.image-open').click();
|
||
await expect(viewer(page).locator('#viewer-details .panel-heading')).toBeVisible();
|
||
}
|
||
|
||
/** The dialog's box and edges against the window. */
|
||
const frame = page => viewer(page).evaluate(node => {
|
||
const box = node.getBoundingClientRect(), style = getComputedStyle(node);
|
||
return { left: Math.round(box.left), top: Math.round(box.top), right: Math.round(innerWidth - box.right),
|
||
bottom: Math.round(document.documentElement.clientHeight - box.bottom), radius: style.borderTopLeftRadius,
|
||
border: style.borderTopWidth, fills: Math.abs(box.width - document.documentElement.clientWidth) <= 1 };
|
||
});
|
||
|
||
test('the button beside ✕ switches between Centered and Full screen and names the view it switches to', async ({ page }) => {
|
||
await openImages(page);
|
||
const header = await viewer(page).locator('.dialog-header').evaluate(node => [...node.children].map(child => child.id || child.localName));
|
||
expect(header).toEqual(['div', 'viewer-mode', 'viewer-close']);
|
||
await expect(modeButton(page)).toHaveAccessibleName('Full screen view');
|
||
await expect(modeButton(page)).toHaveAttribute('title', 'Full screen view');
|
||
await expect(modeButton(page).locator('svg.icon')).toHaveCount(1);
|
||
const centered = await frame(page);
|
||
expect(centered.left).toBeGreaterThan(0);
|
||
expect(centered.fills).toBe(false);
|
||
// Keyboard: the button is reachable and keeps focus after a switch.
|
||
await viewer(page).locator('#viewer-close').focus();
|
||
await page.keyboard.press('Shift+Tab');
|
||
await expect(modeButton(page)).toBeFocused();
|
||
await page.keyboard.press('Enter');
|
||
await expect(viewer(page)).toHaveClass(/\bviewer-fullscreen\b/);
|
||
await expect(modeButton(page)).toBeFocused();
|
||
await expect(modeButton(page)).toHaveAccessibleName('Centered view');
|
||
expect(await frame(page)).toEqual({ left: 0, top: 0, right: 0, bottom: 0, radius: '0px', border: '0px', fills: true });
|
||
// The details stay toggleable in full screen.
|
||
const toggle = viewer(page).getByRole('button', { name: 'Hide details', exact: true });
|
||
await expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
||
await toggle.click();
|
||
await expect(details(page)).toBeHidden();
|
||
await viewer(page).getByRole('button', { name: 'Show details', exact: true }).click();
|
||
await expect(details(page)).toBeVisible();
|
||
// Back to Centered: today's dialog, with no toggle while the collapse setting is off.
|
||
await modeButton(page).click();
|
||
await expect(viewer(page)).not.toHaveClass(/\bviewer-fullscreen\b/);
|
||
expect(await frame(page)).toEqual(centered);
|
||
await expect(viewer(page).getByRole('button', { name: /^(Show|Hide) details$/ })).toHaveCount(0);
|
||
// Navigation keeps the current view; Esc closes from full screen; the next open is Centered again.
|
||
await modeButton(page).click();
|
||
await viewer(page).getByRole('button', { name: 'Next media', exact: true }).click();
|
||
await expect(viewer(page)).toHaveClass(/\bviewer-fullscreen\b/);
|
||
await viewer(page).getByRole('button', { name: 'Hide details', exact: true }).focus();
|
||
await page.keyboard.press('Escape');
|
||
await expect(viewer(page)).toHaveCount(0);
|
||
await page.locator(CARDS).first().locator('.image-open').click();
|
||
await expect(viewer(page)).not.toHaveClass(/\bviewer-fullscreen\b/);
|
||
});
|
||
|
||
test('switching back to Centered keeps the collapse setting\'s toggle and hidden details (D97)', async ({ page }) => {
|
||
await page.addInitScript(() => localStorage.setItem('gallerySettings', JSON.stringify({ sidebarCollapsedByDefault: true })));
|
||
await page.setViewportSize({ width: 1600, height: 1100 });
|
||
await openProduction(page, 'images', 'dark');
|
||
await page.locator(CARDS).first().locator('.image-open').click();
|
||
await expect(viewer(page)).toBeVisible();
|
||
await expect(details(page)).toBeHidden();
|
||
await modeButton(page).click();
|
||
await expect(viewer(page).getByRole('button', { name: 'Show details', exact: true })).toBeVisible();
|
||
await modeButton(page).click();
|
||
await expect(details(page)).toBeHidden();
|
||
await expect(viewer(page).getByRole('button', { name: 'Show details', exact: true })).toHaveAttribute('aria-expanded', 'false');
|
||
});
|
||
|
||
test('System › Appearance › Viewer chooses how the viewer opens; the button changes only the current view', async ({ page }) => {
|
||
await page.setViewportSize({ width: 1600, height: 1100 });
|
||
await openProduction(page, 'system/appearance', 'dark');
|
||
const surface = page.locator('#workspace-main section.surface').filter({ has: page.locator('h2', { hasText: /^Viewer$/ }) });
|
||
await expect(surface.locator('.setting-row > div > :is(label, h3)')).toHaveText(['Open viewer in', 'Collapse viewer details by default']);
|
||
const select = surface.getByRole('combobox', { name: 'Open viewer in', exact: true });
|
||
await expect(select).toHaveValue('centered');
|
||
await select.selectOption('fullscreen');
|
||
await page.getByRole('button', { name: 'Save preferences', exact: true }).click();
|
||
await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem('gallerySettings') || '{}').viewerOpenMode)).toBe('fullscreen');
|
||
await page.evaluate(() => { location.hash = '#/images'; });
|
||
await page.locator(CARDS).first().locator('.image-open').click();
|
||
await expect(viewer(page)).toHaveClass(/\bviewer-fullscreen\b/);
|
||
await expect(modeButton(page)).toHaveAccessibleName('Centered view');
|
||
await modeButton(page).click();
|
||
await expect(viewer(page)).not.toHaveClass(/\bviewer-fullscreen\b/);
|
||
expect(await page.evaluate(() => JSON.parse(localStorage.getItem('gallerySettings')).viewerOpenMode)).toBe('fullscreen');
|
||
});
|
||
|
||
test('Inspect opens full screen with its details when that is the default (D155)', async ({ page }) => {
|
||
await page.addInitScript(() => localStorage.setItem('gallerySettings', JSON.stringify({ viewerOpenMode: 'fullscreen', sidebarCollapsedByDefault: true })));
|
||
await page.setViewportSize({ width: 1600, height: 1100 });
|
||
await openProduction(page, 'inspect', 'dark');
|
||
await page.locator('#local-file').setInputFiles(parityPng('f02.png'));
|
||
await expect(viewer(page)).toHaveClass(/\bviewer-fullscreen\b/);
|
||
await expect(details(page)).toBeVisible();
|
||
await expect(viewer(page).getByRole('button', { name: 'Hide details', exact: true })).toBeVisible();
|
||
});
|
||
|
||
test('the Library gallery viewer has the button too', async ({ page }, testInfo) => {
|
||
await page.setViewportSize({ width: 1600, height: 1100 });
|
||
await openProduction(page, `library/${testInfo.manifest.prompts[0].positive_id}`, 'dark');
|
||
await page.locator('.media-card .image-open').first().click();
|
||
await expect(modeButton(page)).toHaveAccessibleName('Full screen view');
|
||
});
|
||
|
||
test('at 390px the viewer is already full-bleed and the button hides', async ({ page }) => {
|
||
await openImages(page, { width: 390, height: 844 });
|
||
await expect(viewer(page).locator('#viewer-mode')).toBeHidden();
|
||
await expect(viewer(page).locator('#viewer-close')).toBeVisible();
|
||
});
|
||
|
||
test('full screen holds at 200% zoom and coarse pointers get a 40px target', async ({ page, browserName }) => {
|
||
await openImages(page);
|
||
await modeButton(page).click();
|
||
await page.evaluate(() => { document.documentElement.style.zoom = '2'; });
|
||
const zoomed = await frame(page);
|
||
expect(zoomed).toMatchObject({ left: 0, top: 0, right: 0, bottom: 0 });
|
||
await page.evaluate(() => { document.documentElement.style.zoom = '1'; });
|
||
if (await page.evaluate(() => matchMedia('(pointer: coarse)').matches)) {
|
||
const size = await modeButton(page).evaluate(node => [node.getBoundingClientRect().width, node.getBoundingClientRect().height]);
|
||
expect(Math.min(...size)).toBeGreaterThanOrEqual(40);
|
||
}
|
||
test.info().annotations.push({ type: 'engine', description: browserName });
|
||
});
|