/* 实体资料页资料卡下面那一整块:交集条与两排的玻璃浮层(`entity-filter` 岛,ADR-0031)。
*
* 演示库没有人物实体:资料、作品与照片都照接口的形状在这里给,页面自己的路由、渲染和动作照常跑。
* 验的是行为:骨架换成真浮层时不跳、按下去的反馈不等请求、视图之间切换时哪些东西该收起来、
* 吸顶与窄屏横滚。外观对照另有截图,不在这里比像素。 */
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { Browser, Page } from 'playwright-core';
import { launch, settle, visit, VIEWPORTS, type Visit } from './harness.ts';
const DESKTOP = VIEWPORTS.find((viewport) => !viewport.mobile)!;
const PHONE = VIEWPORTS.find((viewport) => viewport.mobile)!;
const NAME = '篠田ゆう';
const PATH = `/performers/${encodeURIComponent(NAME)}`;
const THUMB = '';
const TAGS = ['巨乳', '美乳', '中出', '单体作品', '高画质', '痴女', '口交', '姐姐', '苗条', '制服', '潮吹', '美腿'];
const ENTITY = {
id: 90_101, kind: 'performer', canonical_name: NAME, aliases: [], display_aliases: [], user_aliases: [],
asset_count: 24, tags: TAGS.map((k, at) => ({ k, n: 40 - at * 2 })), related_performers: [], links: [],
metadata: {}, has_image: false, has_avatar: false, avatar_focus: null, representative_asset_id: null,
entry_links: [], feed: null,
};
const costar = (id: number, k: string) => ({ id, k, n: 3, rep: null, has_image: false, has_avatar: false, avatar_focus: null });
const AGENCY = {
...ENTITY, id: 90_701, kind: 'agency', canonical_name: 'New Actor eXperience', member_count: 3, feed: undefined,
tags: ENTITY.tags.slice(0, 6),
related_performers: [costar(90_710, NAME), costar(90_711, '河北彩伽'), costar(90_712, '三上悠亜')],
};
type Entity = typeof ENTITY | typeof AGENCY;
const items = (jav: boolean) => Array.from({ length: 24 }, (_, at) => ({
id: 91_000 + at, name: jav ? `SSIS-${100 + at} 演示作品 ${at + 1}` : `演示作品 ${at + 1}`, code: jav ? `SSIS-${100 + at}` : '',
is_jav: jav, has_thumb: true, duration: 7200, studio: 'S1 NO.1 STYLE', tags: [],
}));
const PHOTOS = { total: 18, sample_total: 0, sets: [], has_more: false, seed: '',
items: Array.from({ length: 18 }, (_, at) => ({ id: 92_000 + at, name: `${String(at + 1).padStart(3, '0')}.jpg` })) };
const NO_PHOTOS = { total: 0, sample_total: 0, sets: [], items: [], has_more: false, seed: '' };
/** 资料、作品、照片与图都在这里给;事务所旗下不是 JAV 语境,只有女优有照片。
* `/api/items` 可以挂住:`hold()` 之后的那一趟等 `release()` 才回。 */
async function serve(page: Page, entity: Entity = ENTITY) {
let gate: Promise | null = null;
let open = () => {};
const itemQueries: string[] = [];
const list = items(entity.kind !== 'agency');
await page.route(/\/api\/entity\?/, (route) => route.fulfill({ json: entity }));
await page.route((url) => url.pathname === '/api/items', async (route) => {
const url = new URL(route.request().url());
itemQueries.push(url.search);
if (gate) await gate;
const offset = Number(url.searchParams.get('offset') || 0);
await route.fulfill({ json: { items: offset ? [] : list, total: list.length, has_more: false } });
});
await page.route((url) => url.pathname === '/api/photos', (route) =>
route.fulfill({ json: entity.kind === 'performer' ? PHOTOS : NO_PHOTOS }));
await page.route((url) => url.pathname === '/api/feeds/discoveries', (route) =>
route.fulfill({ json: { ok: true, more: false, items: [] } }));
await page.route(/\/(?:thumb|poster|photo-thumb|cover)\?/, (route) =>
route.fulfill({ contentType: 'image/svg+xml', body: THUMB }));
return {
itemQueries,
hold() { gate = new Promise((resolve) => { open = resolve; }); },
release() { open(); gate = null; },
};
}
async function openPerformer(browser: Browser, viewport = DESKTOP, search = '', entity: Entity = ENTITY) {
const opened = await visit(browser, '/', viewport);
const stubs = await serve(opened.page, entity);
const path = entity.kind === 'agency' ? `/agencies/${encodeURIComponent(entity.canonical_name)}` : PATH;
await opened.page.goto(new URL(path + search, opened.page.url()).href, { waitUntil: 'load' });
await opened.page.locator('[data-entity-filter-glass] [data-entity-readout]', { hasText: /\S/ })
.waitFor({ timeout: 15_000 });
await settle(opened.page);
assert.equal(await opened.page.locator('[data-entity-filter-glass]').getByRole('button', { name: '我的收藏', exact: true }).count(), 1);
return { ...opened, stubs };
}
/** 滑动玻璃与它该垫着的那一枚键:两者的外框,玻璃收起时为 null。 */
const glideOver = (page: Page, glide: string, key: string) => page.evaluate(([glideSelector, keySelector]) => {
const rect = (node: Element | null) => {
if (!node || (node as HTMLElement).hidden) return null;
const box = node.getBoundingClientRect();
return { left: box.left, top: box.top, width: box.width, height: box.height };
};
return { glide: rect(document.querySelector(glideSelector)), key: rect(document.querySelector(keySelector)) };
}, [glide, key] as const);
function assertCovers(pair: Awaited>, what: string) {
assert.ok(pair.glide && pair.key, `${what}:玻璃或键不在屏幕上 ${JSON.stringify(pair)}`);
for (const side of ['left', 'top', 'width', 'height'] as const) {
assert.ok(Math.abs(pair.glide![side] - pair.key![side]) <= 1,
`${what}:玻璃的 ${side} 是 ${pair.glide![side]},键是 ${pair.key![side]}`);
}
}
const unexpected = (opened: Visit) => opened.problems;
/** 分段开关里的一格:点得到的是整格 label,单选框本身是藏起来的。 */
const segment = (page: Page, group: string, value: string) =>
page.locator(`[data-entity-layout][aria-label="${group}"] label:has(input[value="${value}"])`);
describe('实体页筛选浮层', () => {
let browser: Browser;
before(async () => { browser = await launch(); });
after(async () => { await browser?.close(); });
for (const viewport of [DESKTOP, PHONE]) {
it(`${viewport.mobile ? '手机' : '桌面'}别名地址归一保留参数与片段,后退回到入口`, { timeout: 60_000 }, async () => {
const opened = await visit(browser, '/', viewport);
try {
const { page } = opened;
await serve(page);
const suffix = '?sort=release&dir=asc&tag=a%2Fb&offset=48#works';
await page.goto(new URL('/performers/OldName' + suffix, page.url()).href, { waitUntil: 'load' });
await page.waitForURL(url => url.pathname === '/performers/90101' && url.search + url.hash === suffix);
await page.locator('[data-entity-filter-glass] [data-entity-readout]').waitFor();
await page.goBack({ waitUntil: 'load' });
assert.equal(new URL(page.url()).pathname, '/');
} finally { await opened.close(); }
});
}
it('骨架里那块浮层换成真浮层时不跳:大小、离资料卡的距离与作品网格的上沿都留在原位', { timeout: 60_000 }, async () => {
const opened = await visit(browser, '/', DESKTOP);
try {
const { page } = opened;
await serve(page);
let release = () => {};
const held = new Promise((resolve) => { release = resolve; });
await page.route(/\/api\/entity\?/, async (route) => { await held; await route.fallback(); });
await page.goto(new URL(PATH, page.url()).href, { waitUntil: 'load' });
await page.locator('#index [data-skeleton="entity/performer"] .board-filter-frame').waitFor({ timeout: 15_000 });
const material = await page.locator('#index [data-skeleton] .board-filter-frame').evaluate(node => {
const style = getComputedStyle(node);
return { image: style.backgroundImage, backdrop: style.backdropFilter, background: style.backgroundColor };
});
assert.notEqual(material.backdrop, 'none');
assert.notEqual(material.background, 'rgb(0, 0, 0)');
/* 资料卡是另一座岛,骨架与真卡的高度差不归这一块管:浮层的位置按它离资料卡下沿的距离量。 */
const measure = (hero: string, glass: string, grid: string) => page.evaluate(([heroSelector, glassSelector, gridSelector]) => {
const heroBox = document.querySelector(heroSelector)!.getBoundingClientRect();
const glassBox = document.querySelector(glassSelector)!.getBoundingClientRect();
const gridBox = document.querySelector(gridSelector)!.getBoundingClientRect();
return { gap: glassBox.top - heroBox.bottom, left: glassBox.left, width: glassBox.width,
height: glassBox.height, grid: gridBox.top - glassBox.bottom };
}, [hero, glass, grid] as const);
const skeleton = await measure('#index [data-skeleton] .entityhero', '#index [data-skeleton] .board-filter-frame',
'#index [data-skeleton] .entitysection');
release();
await page.locator('[data-entity-filter-glass] [data-entity-readout]', { hasText: '视频' }).waitFor({ timeout: 15_000 });
await settle(page);
const live = await measure('#index [data-entity-card]', '#index [data-entity-filter-glass]', '#index [data-entity-body]');
for (const key of ['gap', 'left', 'width', 'height', 'grid'] as const) {
assert.ok(Math.abs(skeleton[key] - live[key]) <= 1,
`浮层的 ${key} 接管时跳了:骨架 ${skeleton[key]},接管后 ${live[key]}`);
}
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('进页时两块滑动玻璃就垫在当前视图与观看状态下面,换观看状态时玻璃当场滑过去、读数等这一趟', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser);
try {
const { page, stubs } = opened;
assertCovers(await glideOver(page, '[data-view-glide="round"]', '[data-media-view][aria-pressed="true"]'), '视图键');
assertCovers(await glideOver(page, '[data-view-glide=""]', '[data-entity-state][aria-pressed="true"]'), '观看状态');
stubs.hold();
await page.locator('[data-entity-state="fresh"]').click();
const pressed = await page.evaluate(() => ({
state: document.querySelector('[data-entity-state="fresh"]')?.getAttribute('aria-pressed'),
busy: document.querySelector('[data-entity-filter-glass] [data-filter-row="bottom"]')?.getAttribute('aria-busy'),
shimmer: !!document.querySelector('[data-entity-readout] [data-async-text][aria-busy="true"] > [data-async-text-skeleton]'),
retained: document.querySelector('[data-entity-readout] [data-async-text-value]')?.getAttribute('aria-hidden'),
}));
assert.deepEqual(pressed, { state: 'true', busy: 'true', shimmer: true, retained: 'true' }, '按下态和读数等待态当场到位');
await page.waitForFunction(() => getComputedStyle(document.querySelector('[data-entity-readout] [data-async-text-skeleton]')!).opacity === '1');
assertCovers(await glideOver(page, '[data-view-glide=""]', '[data-entity-state="fresh"]'), '换过去的观看状态');
stubs.release();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24' }).waitFor({ timeout: 15_000 });
await page.waitForFunction(() => !document.querySelector('[data-entity-readout] [data-async-text][aria-busy="true"]')
&& getComputedStyle(document.querySelector('[data-entity-readout] [data-async-text-skeleton]')!).opacity === '0');
assert.match(page.url(), /[?&]state=fresh/);
assert.match(stubs.itemQueries.at(-1)!, /state=fresh/);
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('换排序当场按下并画出方向箭头,再点一次翻方向,JAV 版式切换不发请求', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser);
try {
const { page, stubs } = opened;
stubs.hold();
await page.locator('[data-entity-sort="rating"]').click();
assert.equal(await page.locator('[data-entity-sort="rating"]').getAttribute('aria-pressed'), 'true');
assert.equal(await page.locator('[data-entity-sort="new"]').getAttribute('aria-pressed'), 'false');
const visibleArrows = await page.locator('[data-entity-sort="rating"] svg').evaluateAll(nodes =>
nodes.filter(node => getComputedStyle(node.closest('[data-icon]') || node).opacity === '1').length);
assert.equal(visibleArrows, 1, '选中的排序键应显示一个方向');
stubs.release();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24' }).waitFor({ timeout: 15_000 });
assert.equal(await page.locator('[data-entity-sort="rating"]').getAttribute('aria-label'), '按评分从低到高排序');
await page.locator('[data-entity-sort="rating"]').click();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24' }).waitFor({ timeout: 15_000 });
assert.match(page.url(), /[?&]dir=asc/);
const before = stubs.itemQueries.length;
const small = segment(page, 'JAV 卡片版式', 'small');
assert.equal(await small.getAttribute('data-selected'), null);
await small.click();
await page.waitForFunction(() => document.querySelector(
'[data-entity-layout][aria-label="JAV 卡片版式"] label:has(input[value="small"])')?.hasAttribute('data-selected'));
await segment(page, 'JAV 卡片版式', 'big').click();
assert.equal(stubs.itemQueries.length, before, '切版式重新取了一次作品');
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('点标签加进交集条,交集条上撤掉它、标签跟着弹起;地址栏同步', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser);
try {
const { page } = opened;
assert.equal(await page.locator('[data-entity-combo] [data-combo-chip]').count(), 0);
// 标签跟首页筛选条上那枚同一张脸:`--control-h-compact` 那一档 32px 高、`--tag-radius` 那一档 8px 圆角、一圈 1px 实线。
const face = await page.locator('[data-entity-tag="中出"]').evaluate((el) => {
const css = getComputedStyle(el);
return [el.getBoundingClientRect().height, css.borderTopLeftRadius, css.borderTopWidth, css.borderTopStyle];
});
assert.deepEqual(face, [32, '8px', '1px', 'solid']);
await page.locator('[data-entity-tag="中出"]').click();
assert.equal(await page.locator('[data-entity-tag="中出"]').getAttribute('aria-pressed'), 'true');
await page.locator('[data-entity-combo] [data-combo-chip]', { hasText: '中出' }).waitFor();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24 · 已选 1 个标签' }).waitFor({ timeout: 15_000 });
assert.match(decodeURIComponent(page.url()), /[?&]tag=中出/);
await page.getByRole('button', { name: '撤掉 中出' }).click();
await page.locator('[data-entity-readout]', { hasText: /^视频 · 24$/ }).waitFor({ timeout: 15_000 });
assert.equal(await page.locator('[data-entity-tag="中出"]').getAttribute('aria-pressed'), 'false');
assert.equal(await page.locator('[data-entity-combo] [data-combo-chip]').count(), 0);
assert.doesNotMatch(page.url(), /[?&]tag=/);
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('切到照片:视图键当场换过去,观看状态、标签与交集条收起,下排换成照片那一排;切回视频都回来', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser, DESKTOP, `?tag=${encodeURIComponent('中出')}`);
try {
const { page } = opened;
await page.locator('[data-media-view="photos"]').click();
await page.locator('[data-entity-readout]', { hasText: /^照片/ }).waitFor({ timeout: 15_000 });
const photos = await page.evaluate(() => ({
pressed: document.querySelector('[data-media-view="photos"]')?.getAttribute('aria-pressed'),
states: (document.querySelector('[data-entity-states]') as HTMLElement | null)?.hidden,
tags: document.querySelectorAll('[data-entity-tag]').length,
combo: (document.querySelector('[data-entity-combo]') as HTMLElement | null)?.hidden,
sorts: document.querySelectorAll('[data-entity-sort]').length,
batch: document.querySelectorAll('[data-entity-batch]').length,
}));
assert.deepEqual(photos, { pressed: 'true', states: true, tags: 0, combo: true, sorts: 0, batch: 1 });
assertCovers(await glideOver(page, '[data-view-glide="round"]', '[data-media-view="photos"]'), '照片键');
assert.equal(await page.locator('[data-view-glide=""]').evaluate((node) => (node as HTMLElement).hidden), true,
'观看状态收起了,那块玻璃还亮着');
await segment(page, '图片布局', 'fixed').click();
await page.waitForFunction(() => document.querySelector('[data-local-wall]')?.getAttribute('data-layout') === 'fixed');
await segment(page, '图片布局', 'masonry').click();
await page.locator('[data-media-view="videos"]').click();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24 · 已选 1 个标签' }).waitFor({ timeout: 15_000 });
assert.equal(await page.locator('[data-entity-tag]').count(), TAGS.length);
assert.equal(await page.locator('[data-entity-combo] [data-combo-chip]').count(), 1);
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('浮层吸在顶栏下沿,吸住时标 data-stuck,玻璃跟着键不走位', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser);
try {
const { page } = opened;
await page.evaluate(() => window.scrollTo(0, 700));
await page.waitForFunction(() => document.querySelector('[data-entity-filter-glass]')!.hasAttribute('data-stuck'),
undefined, { timeout: 5_000 });
const pinned = await page.evaluate(() => {
const glass = document.querySelector('[data-entity-filter-glass]')!;
return { top: glass.getBoundingClientRect().top, sticky: parseFloat(getComputedStyle(glass).top) };
});
assert.ok(Math.abs(pinned.top - pinned.sticky) <= 1, `浮层没有吸住:上沿 ${pinned.top},吸顶位置 ${pinned.sticky}`);
assertCovers(await glideOver(page, '[data-view-glide="round"]', '[data-media-view][aria-pressed="true"]'), '吸顶时的视图键');
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForFunction(() => !document.querySelector('[data-entity-filter-glass]')!.hasAttribute('data-stuck'),
undefined, { timeout: 5_000 });
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('事务所进页落在旗下艺人名册,下排只有读数;切到视频出排序与标签,没有 JAV 版式', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser, DESKTOP, '', AGENCY);
try {
const { page } = opened;
const roster = await page.evaluate(() => ({
readout: document.querySelector('[data-entity-readout]')?.textContent,
pressed: document.querySelector('[data-media-view][aria-pressed="true"]')?.getAttribute('data-media-view'),
views: [...document.querySelectorAll('[data-media-view]')].map((node) => node.getAttribute('data-media-view')),
sorts: document.querySelectorAll('[data-entity-sort]').length,
tags: document.querySelectorAll('[data-entity-tag]').length,
cards: document.querySelectorAll('#index [data-entity-body] [data-index-grid] > [data-index-cell]').length,
}));
assert.deepEqual(roster, { readout: '艺人 · 3', pressed: 'people', views: ['people', 'videos'], sorts: 0, tags: 0, cards: 3 });
await page.locator('[data-media-view="videos"]').click();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24' }).waitFor({ timeout: 15_000 });
assert.equal(await page.locator('[data-entity-tag]').count(), AGENCY.tags.length);
assert.ok(await page.locator('[data-entity-sort]').count() > 0);
assert.equal(await page.locator('[data-entity-layout][aria-label="JAV 卡片版式"]').count(), 0);
assertCovers(await glideOver(page, '[data-view-glide="round"]', '[data-media-view="videos"]'), '视频键');
await page.locator('[data-media-view="people"]').click();
await page.locator('[data-entity-readout]', { hasText: '艺人 · 3' }).waitFor({ timeout: 15_000 });
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('手机宽度下观看状态与标签整段横滚、视图键钉住;往下滚浮层让出顶栏,往回滚落下来', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser, PHONE);
try {
const { page } = opened;
const scroller = page.locator('[data-entity-scroll]');
const overflow = await scroller.evaluate((node) => node.scrollWidth - node.clientWidth);
assert.ok(overflow > 100, `窄屏下这一段没有横向溢出(${overflow}px),横滚的判据没生效`);
const keyBefore = await page.locator('[data-media-view="videos"]').boundingBox();
await scroller.evaluate((node) => { node.scrollLeft = 220; node.dispatchEvent(new Event('scroll')); });
await page.waitForFunction(() => document.querySelector('[data-entity-scroll]')!.scrollLeft > 0);
const keyAfter = await page.locator('[data-media-view="videos"]').boundingBox();
assert.equal(keyAfter?.x, keyBefore?.x, '视图键跟着横滚走了');
await page.waitForFunction(() => (document.querySelector('[data-view-glide=""]') as HTMLElement).hidden,
undefined, { timeout: 5_000 });
const glass = '[data-entity-filter-glass]';
/* 方向判据按「上一个 y」算:壳在 scroll 事件里排一帧 rAF 才读 scrollY,两次滚动落进同一帧
的话它只读到后一次,往下 300 再到 700 会被看成一步、700 回 500 会被看成从 300 往下滚。
所以每滚一次都等到这个位置的 scroll 事件真的发出来,再过两帧:壳那一帧排在这一帧前面,
两帧过去它一定已经读过这个位置,下一次滚动才发。 */
const scrollSettled = (y: number) => page.evaluate((target) => new Promise((done) => {
addEventListener('scroll', function seen() {
if (scrollY !== target) return;
removeEventListener('scroll', seen);
requestAnimationFrame(() => requestAnimationFrame(() => done()));
});
scrollTo(0, target);
}), y);
await scrollSettled(300);
await scrollSettled(700);
await page.waitForFunction((selector) => document.querySelector(selector)!.hasAttribute('data-filter-free'),
glass, { timeout: 5_000 });
const bottom = (selector: string) => page.evaluate((s) => document.querySelector(s)!.getBoundingClientRect().bottom, selector);
assert.ok(await bottom(glass) <= 0, `让位后浮层还压在屏幕上沿:下沿 ${await bottom(glass)}`);
await scrollSettled(500);
await page.waitForFunction((selector) => !document.querySelector(selector)!.hasAttribute('data-filter-free'),
glass, { timeout: 5_000 });
const top = await page.evaluate((s) => {
const node = document.querySelector(s)!;
return { at: node.getBoundingClientRect().top, sticky: parseFloat(getComputedStyle(node).top) };
}, glass);
assert.ok(Math.abs(top.at - top.sticky) <= 1, `往回滚后浮层没有落回顶栏下沿:${JSON.stringify(top)}`);
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('深链带 media=photos 直接落在照片墙;回到视频后加一枚标签只重取一次,切照片再切回不重取', { timeout: 60_000 }, async () => {
const opened = await openPerformer(browser, DESKTOP, '?media=photos');
try {
const { page, stubs } = opened;
await page.locator('[data-entity-readout]', { hasText: /^照片/ }).waitFor({ timeout: 15_000 });
assert.equal(await page.locator('[data-media-view="photos"]').getAttribute('aria-pressed'), 'true');
assert.equal(await page.locator('[data-local-wall] [data-photo-index]').count(), PHOTOS.items.length);
await page.locator('[data-media-view="videos"]').click();
await page.locator('[data-entity-readout]', { hasText: /^视频 · 24$/ }).waitFor({ timeout: 15_000 });
assert.doesNotMatch(page.url(), /[?&]media=/);
const before = stubs.itemQueries.length;
await page.locator('[data-entity-tag="中出"]').click();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24 · 已选 1 个标签' }).waitFor({ timeout: 15_000 });
await settle(page);
assert.equal(stubs.itemQueries.length - before, 1, `加一枚标签发了 ${stubs.itemQueries.length - before} 次作品请求`);
await page.locator('[data-media-view="photos"]').click();
await page.locator('[data-entity-readout]', { hasText: /^照片/ }).waitFor({ timeout: 15_000 });
await page.locator('[data-media-view="videos"]').click();
await page.locator('[data-entity-readout]', { hasText: '视频 · 24 · 已选 1 个标签' }).waitFor({ timeout: 15_000 });
await settle(page);
assert.equal(stubs.itemQueries.length - before, 1, '切照片再切回视频又取了一遍作品');
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
});
it('选了六枚长标签,读数只报个数,两种宽度下页面都不被撑宽', { timeout: 60_000 }, async () => {
const long = Array.from({ length: 6 }, (_, at) => `とても長い日本語のタグ名その${at + 1}・途中で切れない`);
for (const viewport of [DESKTOP, PHONE]) {
const opened = await openPerformer(browser, viewport, `?tag=${encodeURIComponent(long.join(','))}`);
try {
const { page } = opened;
await page.locator('[data-entity-readout]', { hasText: '视频 · 24 · 已选 6 个标签' }).waitFor({ timeout: 15_000 });
const width = await page.evaluate(() => ({
page: document.documentElement.scrollWidth, view: window.innerWidth,
readout: document.querySelector('[data-entity-filter-glass] [data-entity-readout]')!.textContent,
}));
assert.ok(width.page <= width.view, `${viewport.name}:页面被撑到 ${width.page}px(视口 ${width.view})`);
assert.ok(!long.some((tag) => width.readout?.includes(tag)), `${viewport.name}:读数里又列了标签名`);
assert.deepEqual(unexpected(opened), []);
} finally {
await opened.close();
}
}
});
});