import { startCoverage, saveCoverage } from '../../tools/vendor/browser-coverage.mjs'; import { test, expect } from './fixtures.mjs'; /** * #249: cards and strips load previews, never the full file. The approved mock has no repeated thumbnail prompt * (docs/concepts/workspace/README.md), so a preview that was not made yet is made by the server on first view: * the listing still names the `_thumb` URL. Only the media viewer's main image (and downloads) load the original. */ const images = '/prompt_manager/workspace#/images'; const library = '/prompt_manager/workspace#/library'; const cards = page => page.locator('article[data-image-key]'); const details = page => page.getByRole('complementary', { name: 'Selected image details', exact: true }); const SERVE = '/prompt_manager/images/serve/'; /** Every media file the page asks the server for, as `{path, preview}` (path relative to its root). */ function recordMedia(page) { const seen = []; page.on('request', request => { const url = new URL(request.url()); if (!url.pathname.startsWith(SERVE)) return; const path = decodeURIComponent(url.pathname.slice(SERVE.length)); seen.push({ path, preview: /^thumbnails\/.+_thumb\.[a-z0-9]+$/.test(path) }); }); return seen; } /** Every card image has loaded (or failed): what it asked for is all in `seen`. */ const settled = page => page.evaluate(() => Promise.all([...document.querySelectorAll('article img')].map(image => image.decode().catch(() => {})))); const originals = seen => seen.filter(item => !item.preview).map(item => item.path); test.beforeEach(async ({ page, browserName }) => { await startCoverage(page, browserName); page.thumbErrors = []; page.on('pageerror', error => page.thumbErrors.push(error.message)); }); test.afterEach(async ({ page, browserName }) => { await saveCoverage(page, browserName); expect(page.thumbErrors).toEqual([]); }); test('a page of the Images grid requests previews, never originals, in every layout and size', async ({ page, request }) => { await expect(await request.post('/__test__/reset')).toBeOK(); // Newest outputs with no preview yet: the live finding (844 such files listed with a null thumbnail). await expect(await request.post('/__test__/system/unthumbnailed', { data: { count: 6 } })).toBeOK(); const listing = await (await request.get('/prompt_manager/images/output?limit=40')).json(); const fresh = listing.images.filter(image => image.path.startsWith('unthumbnailed/')); expect(fresh).toHaveLength(6); for (const image of fresh) expect(image.thumbnail_url).toMatch(/\/thumbnails\/unthumbnailed\/plain_\d{3}_thumb\.webp\?root=0/); const seen = recordMedia(page); await page.goto(images); await expect(cards(page)).toHaveCount(8); await settled(page); expect(originals(seen)).toEqual([]); expect(seen.filter(item => item.path.startsWith('thumbnails/unthumbnailed/'))).toHaveLength(6); // The server made them on that first view. await expect.poll(async () => (await (await request.get('/prompt_manager/images/thumbnails/status')).json()).pending).toBe(0); 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'); for (const density of ['large', 'compact', 'comfortable']) { await page.getByRole('combobox', { name: 'Thumbnail size', exact: true }).selectOption(density); await expect(cards(page)).toHaveCount(8); await settled(page); } } expect(originals(seen)).toEqual([]); }); test('the inspector cover is the preview and only the media viewer loads the original', async ({ page, request }) => { await expect(await request.post('/__test__/reset')).toBeOK(); await expect(await request.post('/__test__/system/unthumbnailed', { data: { count: 1 } })).toBeOK(); const seen = recordMedia(page); await page.goto(images); const card = cards(page).filter({ hasText: 'plain_001.png' }); await card.getByRole('button', { name: 'Inspect plain_001.png', exact: true }).click(); const cover = details(page).locator('img.detail-cover'); await expect(cover).toHaveAttribute('src', /\/thumbnails\/unthumbnailed\/plain_001_thumb\.webp\?/); await settled(page); await cover.evaluate(image => image.decode().catch(() => {})); expect(originals(seen)).toEqual([]); await card.getByRole('button', { name: 'Open plain_001.png', exact: true }).click(); await expect(page.locator('dialog#viewer-dialog')).toBeVisible(); await expect.poll(() => originals(seen)).toEqual(['unthumbnailed/plain_001.png']); await page.getByRole('button', { name: 'Close image viewer', exact: true }).click(); }); test('a video card shows its preview frame instead of loading the video', async ({ page, request }) => { await expect(await request.post('/__test__/reset', { data: { include_video: true } })).toBeOK(); const seen = recordMedia(page); await page.goto(images); const card = cards(page).filter({ hasText: 'clip.webm' }); await expect(card.locator('.media-art > .image-open > img')).toHaveAttribute('src', /\/thumbnails\/clip_thumb\.webp\?/); await expect(card.locator('video')).toHaveCount(0); await settled(page); expect(originals(seen)).toEqual([]); }); test('Library cards and the shared gallery request previews only', async ({ page, request }) => { await expect(await request.post('/__test__/reset', { data: { native_library: true } })).toBeOK(); const seen = recordMedia(page); await page.goto(library); const cat = page.locator('article[data-prompt-id]').filter({ has: page.getByText('Cat portrait', { exact: true }) }); await expect(cat.locator('.card-art img')).not.toHaveCount(0); await settled(page); expect(seen.length).toBeGreaterThan(0); expect(originals(seen)).toEqual([]); }); test('a video whose preview cannot be loaded swaps back to its own element', async ({ page, request }) => { await expect(await request.post('/__test__/reset', { data: { include_video: true } })).toBeOK(); await page.route(url => url.pathname.endsWith('/thumbnails/clip_thumb.webp'), route => route.fulfill({ status: 404, body: '' })); await page.goto(images); const card = cards(page).filter({ hasText: 'clip.webm' }); await expect(card.locator('.media-art > .image-open > video')).toHaveCount(1); await expect(card.locator('.media-art > .image-open > img')).toHaveCount(0); await card.getByRole('button', { name: 'Inspect clip.webm', exact: true }).click(); await expect(details(page).locator('video.detail-cover')).toHaveCount(1); await expect(details(page).locator('img.detail-cover')).toHaveCount(0); }); test('an image listed without a preview (a nested thumbnails folder) shows its original', async ({ page, request }) => { await expect(await request.post('/__test__/reset')).toBeOK(); // M10: such files are listed with a null thumbnail_url; the card falls back to the original as before. await page.route(url => url.pathname === '/prompt_manager/images/output', async route => { const response = await route.fetch(); const body = await response.json(); if (Array.isArray(body.images)) body.images = body.images.map(image => ({ ...image, thumbnail_url: null })); await route.fulfill({ response, json: body }); }); const seen = recordMedia(page); await page.goto(images); await expect(cards(page)).toHaveCount(2); await settled(page); expect(seen.length).toBeGreaterThan(0); expect(seen.every(item => !item.preview)).toBe(true); }); test('a JPEG XL thumbnail this browser cannot show loads the original; one it can show is used (#317)', async ({ page, request }) => { await expect(await request.post('/__test__/reset')).toBeOK(); await expect(await request.post('/__test__/system/reset')).toBeOK(); try { await expect(await request.post('/__test__/system/jxl-encoder', { data: { available: true } })).toBeOK(); const saved = await request.post('/prompt_manager/settings', { data: { thumbnail_format: 'jxl' } }); await expect(saved).toBeOK(); await expect(await request.post('/__test__/system/unthumbnailed', { data: { count: 1 } })).toBeOK(); const listing = await (await request.get('/prompt_manager/images/output?limit=40')).json(); const fresh = listing.images.find(image => image.path === 'unthumbnailed/plain_001.png'); expect(fresh.thumbnail_url).toMatch(/\/thumbnails\/unthumbnailed\/plain_001_thumb\.jxl\?root=0/); await page.goto(images); // What this browser does with JPEG XL, decided the way the workspace decides it (an embedded probe). const decodes = await page.evaluate(() => new Promise(resolve => { const probe = new Image(); probe.onload = () => resolve(true); probe.onerror = () => resolve(false); probe.src = 'data:image/jxl;base64,/woAkAEAE4gCALQAtZ8gAAAVKqOMG7yc6/nyQ4fFtI3rDG21bWEJY7O9MEhIOGwY4s3xwRCTShIA'; })); const card = cards(page).filter({ hasText: 'plain_001.png' }); const media = card.locator('.media-art > .image-open > img'); // A browser without JPEG XL shows the original; one with it shows the preview, or the original when the // preview cannot be made here (no real encoder in the test server): never a broken image. if (!decodes) await expect(media).toHaveAttribute('src', /\/unthumbnailed\/plain_001\.png\?root=0/); await expect.poll(() => media.evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true); // The inspector cover follows the same rule. await card.getByRole('button', { name: 'Inspect plain_001.png', exact: true }).click(); const cover = details(page).locator('img.detail-cover'); if (!decodes) await expect(cover).toHaveAttribute('src', /\/unthumbnailed\/plain_001\.png\?/); await expect.poll(() => cover.evaluate(image => image.complete && image.naturalWidth > 0)).toBe(true); } finally { await request.post('/__test__/system/reset'); } });