import { chromium } from 'playwright'; import { mkdir, writeFile, readdir } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import assert from 'node:assert/strict'; const root = dirname(fileURLToPath(import.meta.url)); const out = resolve(root, 'test-results', 'layout'); await mkdir(out, { recursive: true }); const cache = resolve(process.env.LOCALAPPDATA, 'ms-playwright'); const installed = (await readdir(cache)).filter(x => /^chromium-\d+$/.test(x)).sort().reverse(); const browser = await chromium.launch({ executablePath: process.env.DEMO_CHROME || resolve(cache, installed[0], 'chrome-win64', 'chrome.exe') }); const page = await browser.newPage({ reducedMotion: 'reduce' }); page.setDefaultTimeout(6000); const results = []; const errors = []; page.on('pageerror', error => errors.push(error.message)); const routes = ['chat/zhixia', 'contacts', 'projects', 'projects/autumn', 'events', 'workbench', 'tasks', 'tasks/T-106', 'deliverables', 'agent/zhixia', 'mine']; async function test(name, action) { try { await action(); results.push({ name, passed: true }); } catch (error) { results.push({ name, passed: false, error: error.message }); process.stdout.write(`FAIL ${name}: ${error.message}\n`); } } async function geometry() { return page.evaluate(() => { const main = document.querySelector('#main'); const root = document.documentElement; const overlaps = []; for (const selector of ['.settings-content .setting-row', '.settings-appearance', '.settings-layout', '.account-panel']) { for (const group of document.querySelectorAll(selector)) { const children = [...group.children].map(el => ({ name: el.className || el.tagName, r: el.getBoundingClientRect() })).filter(({ r }) => r.width && r.height); for (let i = 0; i < children.length; i++) { for (let j = i + 1; j < children.length; j++) { const a = children[i], b = children[j]; const width = Math.min(a.r.right, b.r.right) - Math.max(a.r.left, b.r.left); const height = Math.min(a.r.bottom, b.r.bottom) - Math.max(a.r.top, b.r.top); if (width > 1 && height > 1) overlaps.push({ selector, a: a.name, b: b.name, width, height }); } } } } const overflows = [...document.querySelectorAll('.settings-section, .account-panel, .page, .task-detail, .project-detail, .agent-card')].filter(el => el.scrollWidth > el.clientWidth + 1).map(el => ({ element: el.className, width: el.clientWidth, scroll: el.scrollWidth })); const content = main?.querySelector(':scope > .page'); let margins = null; if (content && document.body.dataset.mode === 'desktop') { // Compare against the usable workspace, excluding the navigation and scrollbar. const viewportLeft = main.getBoundingClientRect().left + main.clientLeft; const rect = content.getBoundingClientRect(); margins = { left: rect.left - viewportLeft, right: viewportLeft + main.clientWidth - rect.right }; } return { width: innerWidth, docWidth: root.scrollWidth, mainWidth: main?.clientWidth, mainScroll: main?.scrollWidth, overlaps, overflows, margins }; }); } for (const mode of ['desktop', 'mobile']) { for (const width of mode === 'desktop' ? [2560, 1920, 1440, 1280, 1150, 1024, 940, 768] : [360, 390, 430]) { await page.setViewportSize({ width, height: mode === 'desktop' ? 900 : 844 }); for (const route of mode === 'desktop' ? routes : ['home', 'messages', ...routes]) { await test(`${mode} ${width}px ${route}`, async () => { await page.goto(`http://localhost:4300/${mode}/${route}`); const layout = await geometry(); assert.ok(layout.docWidth <= width + 1, JSON.stringify(layout)); assert.ok(layout.mainScroll <= layout.mainWidth + 1, JSON.stringify(layout)); assert.deepEqual(layout.overlaps, []); assert.deepEqual(layout.overflows, []); if (layout.margins) { assert.ok(Math.abs(layout.margins.left - layout.margins.right) <= 1, `Content is not centered: ${JSON.stringify(layout.margins)}`); } if (width === 1440 || width === 390) { await page.screenshot({ path: resolve(out, `${mode}-${route.replaceAll('/', '-')}.png`), scale: 'css' }); } if (width === 1920 && ['contacts', 'events', 'workbench', 'mine'].includes(route)) { await page.screenshot({ path: resolve(out, `desktop-wide-${route}.png`), scale: 'css' }); } }); } } } await test('desktop settings fit at 1440x900', async () => { await page.setViewportSize({ width: 1440, height: 900 }); await page.goto('http://localhost:4300/desktop/mine'); const geometry = await page.evaluate(() => { const account = document.querySelector('.account-panel').getBoundingClientRect(); const content = document.querySelector('.settings-content').getBoundingClientRect(); return { gap: content.left - account.right, topDifference: content.top - account.top, bottom: content.bottom }; }); assert.ok(geometry.gap >= 20); assert.equal(geometry.topDifference, 0); assert.ok(geometry.bottom <= 900, JSON.stringify(geometry)); }); await test('dark settings contrast surfaces remain distinct', async () => { await page.locator('[data-action="theme"][data-value="dark"]').click(); const surfaces = await page.evaluate(() => ['.sidebar', '.workspace', '.settings-content'].map(s => getComputedStyle(document.querySelector(s)).backgroundColor)); assert.equal(new Set(surfaces).size, 3); await page.screenshot({ path: resolve(out, 'desktop-mine-dark.png') }); }); await test('desktop sidebar remains reachable at 768x600', async () => { await page.setViewportSize({ width: 768, height: 600 }); await page.goto('http://localhost:4300/desktop/mine'); await page.locator('.profile-button').scrollIntoViewIfNeeded(); const box = await page.locator('.profile-button').boundingBox(); assert.ok(box.y + box.height <= 600); }); await test('no runtime errors', async () => assert.deepEqual(errors, [])); await writeFile(resolve(out, 'report.json'), JSON.stringify({ passed: results.filter(r => r.passed).length, total: results.length, results, errors }, null, 2)); await browser.close(); process.stdout.write(`${results.filter(r => r.passed).length}/${results.length} layout checks passed.\n`); if (results.some(r => !r.passed)) process.exitCode = 1;