| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- 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;
|