/** * Browser checks for the restored viewer CSS, the ten-ring limit, and pill * clearance. * * The CSS regression this file exists for was silent: opening a photograph * produced an unstyled that still worked, still showed the image, and * just looked nothing like the design. Nothing throws, so only a computed-style * assertion catches it. */ import { chromium } from 'playwright'; const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173/'; let passed = 0, failed = 0; const test = async (name, fn) => { try { await fn(); console.log(' ok ' + name); passed++; } catch (error) { console.log(' FAIL ' + name + '\n ' + error.message.split('\n')[0]); failed++; } }; const assert = (cond, message) => { if (!cond) throw new Error(message); }; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1600, height: 950 } }); const errors = []; page.on('pageerror', error => errors.push(error.message)); await page.goto(BASE, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('.image-tile', { timeout: 120000 }); await page.waitForTimeout(6000); console.log('\nrestored viewer CSS'); // ────────────────────────────────────────────────────────────────────────── await test('the viewer opens full-screen with its own background', async () => { // force: true because the field is deliberately alive -- every tile drifts a // few px per frame, so a click aimed at a position sampled a moment ago can // land beside the tile. That is the field working, not a bug, and it is why // the shot here is dispatched at the element rather than at coordinates. await page.locator('.image-tile').first().click({ force: true, timeout: 15000 }); await page.waitForSelector('.photo-viewer[open]', { timeout: 30000 }); const box = await page.locator('.photo-viewer').boundingBox(); const style = await page.evaluate(() => { const v = document.querySelector('.photo-viewer'); const s = getComputedStyle(v); return { position: s.position, bg: s.backgroundColor, w: s.width, h: s.height }; }); assert(style.position === 'fixed', 'viewer is ' + style.position + ', not fixed'); assert(box.width >= 1500 && box.height >= 900, 'viewer is ' + Math.round(box.width) + 'x' + Math.round(box.height)); assert(style.bg !== 'rgba(0, 0, 0, 0)', 'viewer has no background'); }); await test('the toolbar is a bar, not stacked plain text', async () => { // The regression rendered this as inline-flow divs at full width. A real // toolbar is absolutely positioned over the image, which is what stops the // share button sitting on top of the photograph. const style = await page.evaluate(() => { const t = document.querySelector('.photo-toolbar'); return t ? { position: getComputedStyle(t).position, display: getComputedStyle(t).display } : null; }); assert(style, 'no .photo-toolbar found'); assert(style.position === 'absolute', 'toolbar is ' + style.position); }); await test('the close button is sized and round, not a bare glyph', async () => { const box = await page.locator('.photo-toolbar button').first().boundingBox(); assert(box && box.width >= 28 && box.height >= 28, 'close button is ' + (box ? Math.round(box.width) + 'x' + Math.round(box.height) : 'missing')); }); await test('the frame sits inside the viewport, not flush to an edge', async () => { const box = await page.locator('.photo-frame').boundingBox(); assert(box && box.width > 100 && box.height > 100, 'frame is ' + JSON.stringify(box)); assert(box.x >= 0 && box.y >= 0 && box.x + box.width <= 1601 && box.y + box.height <= 951, 'frame escapes the viewport'); }); await test('the detail panel is styled when the photograph has metadata', async () => { const n = await page.locator('.photo-detail').count(); if (!n) { console.log(' (skipped: this photograph has no metadata panel)'); return; } const style = await page.evaluate(() => { const d = document.querySelector('.photo-detail'); const s = getComputedStyle(d); return { position: s.position, z: s.zIndex }; }); assert(['absolute', 'fixed'].includes(style.position), 'panel is ' + style.position); }); console.log('\nten rings, not sixty'); // ────────────────────────────────────────────────────────────────────────── await test('at most ten tiles carry the neon', async () => { await page.keyboard.press('Escape'); await page.waitForTimeout(1500); const ringed = await page.locator('.image-tile.featured').count(); assert(ringed <= 10, ringed + ' tiles are ringed, expected at most 10'); }); await test('more photographs are shown than before', async () => { const total = await page.locator('.image-tile').count(); assert(total > 96, 'only ' + total + ' tiles; the field should now hold 150 on desktop'); }); await test('un-ringed new photographs are still visible', async () => { // "còn lại hiển thị bình thường" -- the rest must render as ordinary tiles. const plain = await page.evaluate(() => { const tiles = [...document.querySelectorAll('.image-tile')]; return tiles.filter(t => !t.classList.contains('featured')).length; }); assert(plain > 10, 'no un-ringed tiles found'); }); console.log('\npill clearance'); // ────────────────────────────────────────────────────────────────────────── await test('no ringed tile overlaps the search pill', async () => { // The brief is explicit: big, near the search box, but NOT covering it. // Measured against every ringed tile's real box, not its centre point. const worst = await page.evaluate(() => { const pill = document.querySelector('.search-pill').getBoundingClientRect(); let overlap = 0, who = null; for (const tile of document.querySelectorAll('.image-tile.featured')) { const r = tile.getBoundingClientRect(); const w = Math.max(0, Math.min(r.right, pill.right) - Math.max(r.left, pill.left)); const h = Math.max(0, Math.min(r.bottom, pill.bottom) - Math.max(r.top, pill.top)); const area = w * h; if (area > overlap) { overlap = area; who = tile.dataset.imageId; } } return { overlap, who, pill: pill.width * pill.height }; }); assert(worst.overlap === 0, `tile ${worst.who} covers ${Math.round(worst.overlap)}px² of the pill (${Math.round(worst.pill)}px² total)`); }); await test('a ringed tile is still larger than an ordinary one', async () => { const r = await page.evaluate(() => { const area = el => { const b = el.getBoundingClientRect(); return b.width * b.height; }; const f = document.querySelector('.image-tile.featured'); const o = [...document.querySelectorAll('.image-tile')].find(t => !t.classList.contains('featured')); return { f: area(f), o: area(o) }; }); assert(r.f > r.o * 1.15, `ringed ${Math.round(r.f)}px² vs ordinary ${Math.round(r.o)}px²`); }); console.log('\nno regressions'); // ────────────────────────────────────────────────────────────────────────── await test('no uncaught page errors', async () => { assert(errors.length === 0, errors.slice(0, 3).join(' | ')); }); await browser.close(); console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed ? 1 : 0);