| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105 |
- import { chromium } from 'playwright';
- import { writeFile, readdir } from 'node:fs/promises';
- import { resolve } from 'node:path';
- import assert from 'node:assert/strict';
- 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({ viewport: { width: 1440, height: 900 }, reducedMotion: 'no-preference' });
- page.setDefaultTimeout(6000);
- const results = [];
- const errors = [];
- page.on('pageerror', error => errors.push(error.message));
- 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 clickFrames(selector, target = '.page') {
- return page.evaluate(async ({ selector, target }) => {
- document.querySelector(selector).click();
- await new Promise(requestAnimationFrame);
- return document.querySelector(target)?.getAnimations().map(a => ({ frames: a.effect.getKeyframes(), duration: a.effect.getTiming().duration })) || [];
- }, { selector, target });
- }
- await test('navigation preserves sidebar, desktop fade has no displacement', async () => {
- await page.goto('http://localhost:4300/desktop/contacts');
- await page.evaluate(() => { window.testSidebar = document.querySelector('.sidebar'); });
- const animation = await clickFrames('.nav-link[href="/desktop/projects"]');
- assert.equal(animation.length, 1);
- assert.equal(animation[0].duration, 160);
- assert.ok(animation[0].frames.every(f => !f.transform));
- assert.ok(await page.evaluate(() => window.testSidebar === document.querySelector('.sidebar')));
- });
- await test('search preserves input node, focus and does not replay route animation', async () => {
- await page.goto('http://localhost:4300/desktop/contacts');
- await page.evaluate(() => { window.testSearch = document.querySelector('#search-contacts'); });
- await page.locator('#search-contacts').fill('知');
- assert.ok(await page.evaluate(() => window.testSearch === document.activeElement && window.testSearch === document.querySelector('#search-contacts')));
- assert.equal(await page.locator('.page').evaluate(el => el.getAnimations().length), 0);
- assert.equal(await page.locator('.contact-row').count(), 1);
- });
- await test('rapid route changes do not stack animations', async () => {
- const animation = await page.evaluate(async () => {
- for (const name of ['projects', 'events', 'workbench']) document.querySelector(`.nav-link[href="/desktop/${name}"]`).click();
- await new Promise(requestAnimationFrame);
- return document.querySelector('.page').getAnimations().length;
- });
- assert.ok(animation <= 1, `${animation} animations overlap`);
- assert.ok(page.url().endsWith('/workbench'));
- });
- await test('mobile detail enters forward and returns backward', async () => {
- await page.setViewportSize({ width: 390, height: 844 });
- await page.goto('http://localhost:4300/mobile/projects');
- const forward = await clickFrames('.project-row');
- assert.equal(forward.length, 1);
- assert.match(forward[0].frames[0].transform, /translateX\(16px\)/);
- assert.equal(forward[0].duration, 200);
- const back = await clickFrames('.breadcrumbs [data-action="navigate"]');
- assert.equal(back.length, 1);
- assert.match(back[0].frames[0].transform, /translateX\(-16px\)/);
- });
- await test('bottom drawer enters vertically and completes its exit', async () => {
- await page.goto('http://localhost:4300/mobile/chat/zhixia');
- const frames = await clickFrames('[data-action="results-drawer"]', '.drawer');
- assert.match(frames[0].frames[0].transform, /translateY\(100%\)/);
- assert.ok(await page.locator('#app').evaluate(el => el.inert));
- const closing = await page.evaluate(() => {
- document.querySelector('.dialog [data-action="close"]').click();
- return document.querySelector('.dialog')?.getAnimations().map(a => a.effect.getTiming().duration);
- });
- assert.deepEqual(closing, [160]);
- await page.locator('.dialog').waitFor({ state: 'detached' });
- assert.equal(await page.locator('#app').evaluate(el => el.inert), false);
- assert.equal(await page.evaluate(() => document.activeElement.dataset.action), 'results-drawer');
- });
- await test('desktop drawer enters horizontally', async () => {
- await page.setViewportSize({ width: 1440, height: 900 });
- await page.goto('http://localhost:4300/desktop/chat');
- const frames = await clickFrames('[data-action="lab"]', '.drawer');
- assert.match(frames[0].frames[0].transform, /translateX\(100%\)/);
- await page.keyboard.press('Escape');
- await page.locator('.dialog').waitFor({ state: 'detached' });
- });
- await test('modal keyboard focus never reaches background navigation', async () => {
- await page.goto('http://localhost:4300/desktop/mine');
- await page.locator('[data-action="logout"]').click();
- for (let i = 0; i < 8; i++) await page.keyboard.press('Tab');
- assert.ok(await page.evaluate(() => !!document.activeElement.closest('.dialog')));
- await page.keyboard.press('Escape');
- await page.locator('.dialog').waitFor({ state: 'detached' });
- });
- await test('reduced motion disables route and drawer transitions', async () => {
- await page.emulateMedia({ reducedMotion: 'reduce' });
- const frames = await clickFrames('.nav-link[href="/desktop/contacts"]');
- assert.equal(frames.length, 0);
- const drawerFrames = await clickFrames('[data-action="lab"]', '.drawer');
- assert.equal(drawerFrames.length, 0);
- await page.locator('.dialog [data-action="close"]').click();
- assert.equal(await page.locator('.dialog').count(), 0);
- });
- await test('no motion runtime errors', async () => assert.deepEqual(errors, []));
- await writeFile('test-results/motion-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} motion checks passed.\n`);
- if (results.some(r => !r.passed)) process.exitCode = 1;
|