frontend/e2e/library-shortcuts.test.ts
Rama master · réplica de lectura del repositorio original.
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { after, before, describe, it } from 'node:test';
import type { Browser } from 'playwright-core';
import { launch, layout, settle, VIEWPORTS } from './harness.ts';
import { openItemPage } from './item-fixture.ts';
describe('我的收藏弹层', () => {
let browser: Browser;
before(async () => { browser = await launch() });
after(async () => { await browser.close() });
it('创作者头像依次尝试本地来源代理,全部失败显示身份字形', async () => {
const opened = await openItemPage(browser, '/', VIEWPORTS[0], { ready: '[data-catalog-frame]' });
const { page } = opened;
try {
await page.route(url => url.pathname === '/api/library-shortcuts', route => route.fulfill({ json: { favorites: [
{ key: 'entity:9567', entity_id: 9567, kind: 'creator', name: 'InitialA', image_url: '/test-avatar/official', image_fallbacks: ['/test-avatar/archive'] },
{ key: 'entity:9568', entity_id: 9568, kind: 'creator', name: '未取得头像', image_url: '/test-avatar/missing', image_fallbacks: ['/test-avatar/missing-last'] },
], filters: [] } }));
await page.route('**/test-avatar/*', route => route.request().url().endsWith('/archive')
? route.fulfill({ contentType: 'image/svg+xml', body: '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36"><circle cx="18" cy="18" r="18" fill="gray"/></svg>' })
: route.fulfill({ status: 404, body: '' }));
await page.getByRole('button', { name: '我的收藏', exact: true }).click();
const avatar = page.getByRole('link', { name: 'InitialA', exact: true }).locator('[data-shortcuts-avatar] img');
await page.waitForFunction(() => document.querySelector<HTMLImageElement>('img[src="/test-avatar/archive"]')?.naturalWidth);
assert.equal(await avatar.getAttribute('src'), '/test-avatar/archive');
const missing = page.getByRole('link', { name: '未取得头像', exact: true }).locator('[data-shortcuts-avatar]');
await missing.locator('svg').waitFor({ state: 'visible' });
assert.equal(await missing.locator('img').count(), 0);
} finally { await opened.close() }
});
for (const viewport of VIEWPORTS) {
it(`${viewport.name} 保存、应用、删除筛选及收藏分类`, { timeout: 60_000 }, async () => {
const opened = await openItemPage(browser, '/watch-later?loc=&tag=VR&jav=1', viewport, { ready: '[data-catalog-frame]' });
const page = opened.page;
let favorites = [{ key: 'entity:7', entity_id: 7, kind: 'studio', name: '厂牌长名称'.repeat(12), image_url: '/logo?studio=test' }];
let filters: { id: string; name: string; href: string }[] = [];
await page.route(url => url.pathname === '/api/library-shortcuts', async route => {
if (route.request().method() === 'POST') {
const body = route.request().postDataJSON();
if (body.action === 'save_filter') filters.push({ id: 'test', name: body.name, href: body.href });
if (body.action === 'delete_filter') filters = [];
if (body.action === 'favorite') favorites = [];
}
await route.fulfill({ json: { favorites, filters } });
});
try {
await page.locator('#settingsBtn').evaluate((button: HTMLElement) => button.click());
await page.locator('#settingsPanel:not([hidden])').waitFor();
// 光学玻璃的位移贴图在空闲时补画,两边都等它落定再比材质。
await page.locator('[data-settings-card] [data-settings-nav][data-optic-glass="true"]').waitFor();
const setting = await page.evaluate((mobile) => {
const card = document.querySelector('[data-settings-card]')!, nav = card.querySelector('[data-settings-nav]')!, heading = card.querySelector('h2')!;
const surface = getComputedStyle(card), glass = getComputedStyle(nav), title = getComputedStyle(heading);
const pick = getComputedStyle(nav.querySelector('[data-settings-glide]')!);
const action = getComputedStyle(nav.querySelector('[data-settings-service] button')!);
return { pick: { radius: pick.borderRadius, image: pick.backgroundImage, shadow: pick.boxShadow, backdrop: pick.backdropFilter },
action: mobile ? null : { padding: action.padding, radius: action.borderRadius, font: action.fontFamily, size: action.fontSize, weight: action.fontWeight, line: action.lineHeight, color: action.color, height: action.minHeight, gap: action.gap },
width: card.getBoundingClientRect().width, height: card.getBoundingClientRect().height,
radius: surface.borderRadius, background: surface.backgroundColor, shadow: surface.boxShadow,
image: glass.backgroundImage, backdrop: glass.backdropFilter.replace(/#peach-optic-\d+/g, '#peach-optic'), drift: glass.animationName,
font: title.fontSize, weight: title.fontWeight, line: title.lineHeight };
}, viewport.mobile);
await page.locator('#settingsClose').click();
await page.locator('#settingsPanel').waitFor({ state: 'hidden' });
await page.getByRole('button', { name: '我的收藏', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '我的收藏', exact: true });
await page.waitForFunction(() => {
const pane = document.querySelector<HTMLElement>('[data-shortcuts-glide]');
return pane && !pane.hidden && pane.getBoundingClientRect().width > 0
&& !!document.querySelector('[data-shortcuts-panel] [data-glass-pane][data-optic-glass="true"]');
});
const collection = await dialog.evaluate((node, mobile) => {
const card = node.closest('[data-modal-surface]')!, nav = node.querySelector(mobile ? '[data-shortcuts-categories]' : '[data-shortcuts-sidebar]')!, heading = node.querySelector('h2')!;
const surface = getComputedStyle(card), glass = getComputedStyle(nav), title = getComputedStyle(heading);
const pick = getComputedStyle(node.querySelector('[data-shortcuts-glide]')!);
const action = getComputedStyle(node.querySelector('[data-shortcuts-footer] a')!);
return { pick: { radius: pick.borderRadius, image: pick.backgroundImage, shadow: pick.boxShadow, backdrop: pick.backdropFilter },
action: mobile ? null : { padding: action.padding, radius: action.borderRadius, font: action.fontFamily, size: action.fontSize, weight: action.fontWeight, line: action.lineHeight, color: action.color, height: action.minHeight, gap: action.gap },
width: card.getBoundingClientRect().width, height: card.getBoundingClientRect().height,
radius: surface.borderRadius, background: surface.backgroundColor, shadow: surface.boxShadow,
image: glass.backgroundImage, backdrop: glass.backdropFilter.replace(/#peach-optic-\d+/g, '#peach-optic'), drift: glass.animationName,
font: title.fontSize, weight: title.fontWeight, line: title.lineHeight };
}, viewport.mobile);
assert.deepEqual(collection, setting, '收藏弹层与设置共用尺寸、材质、落影与标题排版');
const iconColors = await dialog.locator('[data-shortcuts-categories] button svg, [data-shortcuts-footer] a svg').evaluateAll(icons => icons.map(icon => getComputedStyle(icon).color));
const selectedColor = await dialog.locator('[data-shortcuts-categories] [aria-selected="true"]').evaluate(tab => getComputedStyle(tab).color);
assert.ok(iconColors.every(color => color === selectedColor), JSON.stringify(iconColors));
if (!viewport.mobile) {
const clipping = await dialog.evaluate(node => {
const sidebar = node.querySelector('[data-shortcuts-sidebar]')!.getBoundingClientRect();
const categories = node.querySelector('[data-shortcuts-categories]')!.getBoundingClientRect();
return [categories.left - sidebar.left, sidebar.right - categories.right];
});
assert.ok(clipping.every(edge => Math.abs(edge) < 1), JSON.stringify(clipping));
}
assert.equal(await dialog.getByRole('link', { name: '已标记', exact: true }).getAttribute('href'), '/flagged');
assert.equal(await dialog.getByRole('link', { name: '稍后看', exact: true }).getAttribute('href'), '/watch-later');
assert.equal(await dialog.getByRole('textbox', { name: '筛选名称' }).count(), 0);
await dialog.getByRole('tab', { name: '片商与厂牌', exact: true }).click();
await page.waitForFunction(() => {
const pane = document.querySelector('[data-shortcuts-glide]')!.getBoundingClientRect();
const tab = document.querySelector('[data-shortcuts-categories] [aria-selected="true"]')!.getBoundingClientRect();
return Math.abs(pane.left - tab.left) < 1 && Math.abs(pane.top - tab.top) < 1 && Math.abs(pane.width - tab.width) < 1 && Math.abs(pane.height - tab.height) < 1;
});
await dialog.getByRole('link', { name: favorites[0].name, exact: true }).waitFor();
await dialog.locator('[data-shortcuts-avatar] img').waitFor();
await page.waitForFunction(() => !!document.querySelector<HTMLImageElement>('[data-shortcuts-avatar] img')?.naturalWidth);
const size = await layout(page);
assert.equal(size.viewportWidth, viewport.width);
assert(size.scrollWidth <= size.viewportWidth + 1, JSON.stringify(size));
assert.deepEqual(size.offenders, []);
await page.screenshot({ path: resolve('../build', `shortcuts-${viewport.name}.png`) });
await dialog.getByRole('tab', { name: '保存的筛选', exact: true }).click();
await dialog.getByRole('tab', { name: '保存的筛选', exact: true }).press('Home');
assert.equal(await dialog.getByRole('tab', { name: '全部', exact: true }).getAttribute('aria-selected'), 'true');
await dialog.getByRole('tab', { name: '全部', exact: true }).press('End');
assert.equal(await dialog.getByRole('tab', { name: '事务所', exact: true }).getAttribute('aria-selected'), 'true');
await dialog.getByRole('tab', { name: '保存的筛选', exact: true }).click();
assert.equal(await dialog.getByRole('tab', { name: '保存的筛选', exact: true }).getAttribute('aria-selected'), 'true');
assert.equal(await dialog.getByRole('textbox', { name: '搜索收藏' }).count(), 0);
await dialog.getByRole('textbox', { name: '筛选名称' }).fill('VR 待看');
await dialog.getByRole('button', { name: '保存当前筛选', exact: true }).click();
const link = dialog.getByRole('link', { name: 'VR 待看', exact: true });
await link.waitFor();
const href = await link.getAttribute('href');
const address = new URL(href!, page.url());
assert.equal(address.pathname, '/watch-later');
assert.equal(address.searchParams.get('loc'), '');
assert.equal(address.searchParams.get('tag'), 'VR');
assert.equal(address.searchParams.get('jav'), '1');
await link.click();
await page.waitForURL(url => url.pathname === '/watch-later' && url.searchParams.get('tag') === 'VR');
await settle(page, '[data-catalog-frame]');
await page.getByRole('button', { name: '我的收藏', exact: true }).click();
await dialog.getByRole('tab', { name: '保存的筛选', exact: true }).click();
await dialog.getByRole('button', { name: '删除筛选VR 待看', exact: true }).click();
await link.waitFor({ state: 'detached' });
await dialog.getByRole('tab', { name: '全部', exact: true }).click();
await dialog.getByRole('button', { name: /^取消收藏/ }).click();
await dialog.getByRole('heading', { name: '还没有收藏', exact: true }).waitFor();
const close = dialog.getByRole('button', { name: '关闭我的收藏', exact: true });
assert.equal(await close.locator('use').getAttribute('href'), '#i-x');
assert.deepEqual(await close.evaluate(node => {
const box = node.getBoundingClientRect(); return [box.width, box.height];
}), [24, 24]);
await close.click();
await dialog.waitFor({ state: 'hidden' });
for (const [label, path] of [['已标记', '/flagged'], ['稍后看', '/watch-later']]) {
await settle(page, '[data-catalog-frame]');
await page.getByRole('button', { name: '我的收藏', exact: true }).click();
await dialog.getByRole('link', { name: label, exact: true }).click();
await page.waitForURL(url => url.pathname === path);
await dialog.waitFor({ state: 'hidden' });
}
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
}
});