frontend/e2e/designed-motion.test.ts
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { Browser } from 'playwright-core';
import { openFollowManage } from './design-fixture.ts';
import { launch, settle, visit, VIEWPORTS } from './harness.ts';
import { mockJavDetail } from './jav-detail-fixture.ts';
describe('已设计动效的实际触发', () => {
let browser: Browser;
before(async () => { browser = await launch() });
after(async () => { await browser.close() });
for (const viewport of VIEWPORTS) {
it(`${viewport.name}:换批等待保留箭头并传递描线周期`, async () => {
const opened = await visit(browser, '/', viewport);
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve });
try {
const { page } = opened;
await settle(page);
await page.emulateMedia({ reducedMotion: 'no-preference' });
await page.route(/\/api\/items\?/, async route => { await gate; await route.fallback() });
const button = page.getByRole('button', { name: '换一批', exact: true });
const icon = await button.locator('svg').elementHandle();
await button.click();
await page.waitForFunction(() => document.querySelector('[data-entity-batch]')?.getAttribute('aria-busy') === 'true');
const busy = await button.evaluate((node, original) => {
const svg = node.querySelector('svg')!;
return { stable: svg === original, shuffle: !!svg?.querySelector('use[href="#i-shuffle"]'),
cycle: svg ? getComputedStyle(svg).getPropertyValue('--strand-cycle').trim() : '' };
}, icon);
assert.deepEqual(busy, { stable: true, shuffle: true, cycle: '2.2s' });
await page.emulateMedia({ reducedMotion: 'reduce' });
assert.equal(await page.locator('#i-shuffle .strand').first().evaluate(node => getComputedStyle(node).animationName), 'none');
release();
await page.waitForFunction(() => document.querySelector('[data-entity-batch]')?.getAttribute('aria-busy') !== 'true');
assert.equal(await button.evaluate((node, original) => node.querySelector('svg') === original, icon), true);
assert.deepEqual(opened.problems, []);
} finally { release(); await opened.close() }
});
it(`${viewport.name}:排序立即改变方向且两枚箭头原位交接`, async () => {
const opened = await openFollowManage(browser, viewport);
try {
const { page } = opened;
await page.emulateMedia({ reducedMotion: 'no-preference' });
const button = page.getByRole('button', { name: /^按.+排序$/ });
const icon = button.locator('[data-icon-swap]');
assert.equal(await icon.count(), 1);
const handle = await icon.elementHandle();
const initial = await button.getAttribute('data-icon-state');
const label = await button.getAttribute('aria-label');
await button.click();
assert.notEqual(await button.getAttribute('aria-label'), label);
assert.notEqual(await button.getAttribute('data-icon-state'), initial);
const state = await icon.evaluate((node, original) => ({ stable: node === original,
duration: getComputedStyle(node.querySelector('[data-icon]')!).transitionDuration }), handle);
assert.equal(state.stable, true); assert.notEqual(state.duration, '0s');
await button.click(); await button.click();
await page.waitForFunction(marker => {
if (!marker) return false;
const opacity = [...marker.querySelectorAll('[data-icon]')].map(node => getComputedStyle(node).opacity);
return opacity.filter(value => value === '1').length === 1
&& opacity.every(value => value === '0' || value === '1')
&& marker.getAnimations({ subtree: true }).length === 0;
}, handle, { timeout: 5000 });
assert.equal(await icon.locator('[data-icon]').evaluateAll(nodes => nodes.filter(node => getComputedStyle(node).opacity === '1').length), 1);
await page.emulateMedia({ reducedMotion: 'reduce' });
assert.equal(await icon.locator('[data-icon]').first().evaluate(node => getComputedStyle(node).transitionDuration), '0s');
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:JAV 详情标题与元信息实际播放揭示并清理`, async () => {
const opened = await visit(browser, '/works/EXM-001', viewport, async page => {
await mockJavDetail(page);
await page.emulateMedia({ reducedMotion: 'no-preference' });
await page.addInitScript(() => {
const state = window as unknown as { javReveals: string[] };
state.javReveals = [];
document.addEventListener('transitionrun', event => {
const node = event.target;
if (node instanceof Element && node.matches('[data-reveal-line]') && node.closest('[data-jav-detail]'))
state.javReveals.push(node.hasAttribute('data-stage-title') ? 'title' : 'meta');
}, true);
});
});
try {
await opened.page.waitForFunction(() => {
const state = window as unknown as { javReveals: string[] };
return state.javReveals.includes('title') && state.javReveals.includes('meta');
}, undefined, { timeout: 5000 });
await opened.page.locator('[data-jav-detail] .revealline').first().waitFor({ state: 'detached' });
assert.equal(await opened.page.locator('[data-jav-detail] [data-reveal-line]').count(), 2);
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
}
});