VSEARCH / scripts /test-viewer-ui.mjs
ML Intern
Restore viewer CSS, ring only the newest ten, and clear the search pill
f134575
Raw History Blame Contribute Delete
7.79 kB
/**
* 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 <dialog> 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);