frontend/e2e/configuration-layout.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 { launch, layout, settle, visit, VIEWPORTS } from './harness.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, '/settings/library?section=' + encodeURIComponent('下载渠道'), viewport);
const { page } = opened;
try {
await page.route('**/api/configuration', async route => {
const data = await (await route.fetch()).json();
await route.fulfill({ json: { ...data, downloads: { ...data.downloads, media_sources: ['guangya'], token_set: true,
config: { ...data.downloads.config, clouddrive_address: 'http://127.0.0.1:19798' } } } });
});
await page.route('**/api/configuration/downloads/check', route => route.fulfill({ json: {
ok: true, address: 'http://127.0.0.1:19798', permissions: [], missing: [], root: '/', folder: null,
quota: null, suggested_target: null, suggested_pikpak_root: '', problems: [],
} }));
await page.reload();
const form = page.getByRole('form', { name: '云下载', exact: true });
await form.getByRole('button', { name: '检查连接', exact: true }).click();
await settle(page);
assert.equal(await form.locator('dl').count(), 0);
assert.equal(await form.locator('.border-t').count(), 1);
const empty = await form.locator('div').evaluateAll(nodes => nodes.filter(node =>
!node.childNodes.length && node.getBoundingClientRect().height > 0).map(node => node.className));
assert.deepEqual(empty, []);
assert.ok((await layout(page)).scrollWidth <= viewport.width + 1);
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:来源站标本地提供,帮助链接紧接文字`, async () => {
const opened = await visit(browser, '/settings/subscriptions', viewport);
const { page } = opened;
try {
const section = page.getByRole('form', { name: '资源搜索', exact: true });
await section.getByText('索引器连接帮助', { exact: true }).click();
const link = section.getByRole('link', { name: '详细配置说明', exact: true });
await link.waitFor({ state: 'visible' });
assert.equal(await link.evaluate(node => node.parentElement?.tagName), 'P');
const head = page.locator('b').filter({ hasText: /^F95zone$/ }).locator('..');
const icon = head.locator('img');
await icon.scrollIntoViewIfNeeded();
await page.waitForFunction(() => [...document.querySelectorAll<HTMLImageElement>('img[src*="provider=f95zone"]')].some(image => image.complete && image.naturalWidth > 0));
assert.equal(await icon.getAttribute('src'), '/source-icon?provider=f95zone');
await icon.evaluate(image => image.dispatchEvent(new Event('error')));
await head.locator('svg').waitFor();
assert.equal(await head.locator('img').count(), 0);
await section.screenshot({ path: `../build/indexer-help-${viewport.name}.png` });
assert.deepEqual((await layout(page)).offenders, []);
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:应用更新包说明和体积不越出视口`, async () => {
const opened = await visit(browser, '/settings/library?section=' + encodeURIComponent('应用与维护'), viewport);
const { page } = opened;
try {
await page.route('**/api/configuration', async route => {
const data = await (await route.fetch()).json();
await route.fulfill({ json: { ...data, updates: { ...data.updates, installation: '独立测试包',
state: 'available', latest_version: '1.0.0', message: '有新版本可下载。', package_kind: 'update',
asset_size: 12 * 1048576, release_url: 'https://github.com/peach-mitao/peach/releases/tag/v1.0.0' },
update_job: { state: 'idle', progress: 0 } } });
});
await page.reload();
const region = page.getByRole('region', { name: '检查更新', exact: true });
await region.getByText('应用更新包 · 12.0 MB', { exact: true }).waitFor();
assert.ok((await region.innerText()).includes('工具缺失、损坏或版本变化时会另行下载'));
assert.equal(await region.getByRole('link', { name: '完整安装包', exact: true }).count(), 0);
await settle(page);
assert.ok((await layout(page)).scrollWidth <= viewport.width + 1);
await region.screenshot({ path: `../build/update-download-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:挂载目录的流量保护可切换且不越出视口`, async () => {
const opened = await visit(browser, '/settings/library?section=' + encodeURIComponent('这台电脑'), viewport);
const { page } = opened;
try {
await page.route('**/api/configuration/cloud-proxies', route => route.fulfill({ json: { accounts: [] } }));
await page.route('**/api/configuration', async route => {
const data = await (await route.fetch()).json();
await route.fulfill({ json: { ...data, editable: true, windows: true, media_sources: [
{ location: 'onedrive', root: 'O:\\Movies', path: 'O:\\Movies', cost: 'metered' },
{ location: 'mounted', root: 'M:\\Movies', path: 'M:\\Movies', cost: 'free' },
{ location: 'guangya', root: 'G:\\Movies', path: 'G:\\Movies', cost: 'free' },
{ location: '115', root: 'B:\\Movies', path: 'B:\\Movies', cost: 'free' },
{ location: 'pikpak', root: 'A:\\Movies', path: 'A:\\Movies', cost: 'metered' },
] } });
});
await page.reload();
const form = page.getByRole('form', { name: '媒体库与目录', exact: true });
const protectedFolder = form.locator('[data-media-read-policy="0"] [aria-haspopup="listbox"]');
await protectedFolder.waitFor();
// 带品牌图的四个媒体库,标题行的图标、文件夹行的来源与所属媒体库各显示一枚站标。
await page.waitForFunction(() => {
const marks = [...document.querySelectorAll<HTMLImageElement>('[data-library-block] img')];
return marks.length === 12 &&marks.every(image => image.complete && image.naturalWidth > 0);
});
const marks = await form.locator('[data-library-block] img').evaluateAll(images => images.map(image => {
// 站标是行内字形,画框取 `--icon-inline`(所在文字字号的 1.2 倍)。
const box = image.getBoundingClientRect();
const glyph = parseFloat(getComputedStyle(image).fontSize) * 1.2;
return { inline: image.getAttribute('src')?.startsWith('data:image/png;base64,'),
sized: Math.abs(box.width - glyph) < 0.5 && Math.abs(box.height - glyph) < 0.5 };
}));
assert.deepEqual(marks, Array.from({ length: 12 }, () => ({ inline: true, sized: true })));
assert.ok(!(await form.innerText()).includes('(只读)'));
assert.ok((await protectedFolder.textContent())?.includes('流量保护'));
assert.ok((await form.locator('[data-media-read-policy="1"]').textContent())?.includes('正常读取'));
if (!viewport.mobile) {
const spread = (boxes: { y: number; height: number }[]) => {
const centers = boxes.map(box => box.y + box.height / 2);
return Math.max(...centers) - Math.min(...centers);
};
for (let index = 0; index < 5; index++) {
const block = form.locator('[data-library-block]').nth(index);
const head = block.locator('[data-library-head]');
const icon = await head.getByRole('button').first().boundingBox();
const name = await head.getByRole('textbox').boundingBox();
const preset = await head.locator('[aria-haspopup="listbox"]').last().boundingBox();
const row = block.locator('[data-folder-row]');
const source = await row.locator('[aria-haspopup="listbox"]').first().boundingBox();
const policy = await row.locator('[data-media-read-policy] [aria-haspopup="listbox"]').boundingBox();
const library = await row.getByRole('button', { name: new RegExp(`目录所属媒体库 ${index + 1}`) }).boundingBox();
assert.ok(icon && name && preset && source && policy && library);
// 媒体库的图标、名称与处理方式一排写在卡头;文件夹行的来源、读取策略与所属媒体库一排,同排控件中线对齐。
assert.ok(spread([icon, name, preset]) <= 3, JSON.stringify({ index, icon, name, preset }));
assert.ok(spread([source, policy, library]) <= 3, JSON.stringify({ index, source, policy, library }));
assert.ok(source.y >= name.y + name.height, JSON.stringify({ index, name, source }));
assert.ok(policy.x > source.x && library.x > policy.x);
assert.ok(policy.width <= 132);
}
}
await form.locator('[data-library-head]').first().getByRole('button').first().click();
const icons = page.getByRole('radiogroup', { name: '可选图标', exact: true });
await icons.waitFor();
const inputBounds = await icons.locator('input[type="radio"]').evaluateAll(inputs => inputs.map(input => {
const native = input.getBoundingClientRect();
const label = input.closest('label')!.getBoundingClientRect();
return native.top >= label.top - 1 && native.bottom <= label.bottom + 1
&& native.left >= label.left - 1 && native.right <= label.right + 1;
}));
assert.ok(inputBounds.length > 7 && inputBounds.every(Boolean), '图标网格的隐藏单选输入框越出所属格子');
await icons.getByRole('radio', { name: '磁盘', exact: true }).focus();
const iconScroll = await page.evaluate(() => window.scrollY);
await page.keyboard.press('ArrowRight');
assert.equal(await icons.getByRole('radio', { name: '资料库', exact: true }).isChecked(), true);
assert.equal(await page.evaluate(() => window.scrollY), iconScroll, '选择图标移动了页面');
await page.getByRole('dialog').filter({ has: icons }).getByRole('button', { name: '取消', exact: true }).click();
await protectedFolder.click();
await page.getByRole('option', { name: '正常读取', exact: true }).click();
await settle(page);
assert.ok((await protectedFolder.textContent())?.includes('正常读取'));
assert.deepEqual((await layout(page)).offenders, []);
await form.screenshot({ path: `../build/cloud-source-icons-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:PikPak 表单直接显示,登录按钮同排`, async () => {
const opened = await visit(browser, '/settings/library?section=' + encodeURIComponent('下载渠道'), viewport);
const { page } = opened;
try {
await page.route('**/api/configuration', async route => {
const data = await (await route.fetch()).json();
const body = data.downloads;
await route.fulfill({ json: { ...data, downloads: { ...body, available: true, media_sources: ['pikpak'],
pikpak: { ...body.pikpak, logged_in: false },
pikpak_browser: { available: true, state: 'idle', message: '' } } } });
});
await page.reload();
const form = page.getByRole('form', { name: 'PikPak 账号', exact: true });
await form.getByLabel('账号', { exact: true }).waitFor();
assert.equal(await form.locator('details').count(), 0);
const password = await form.getByRole('button', { name: '登录', exact: true }).boundingBox();
const browserLogin = await form.getByRole('button', { name: '用浏览器登录', exact: true }).boundingBox();
assert.ok(password && browserLogin && Math.abs(password.y - browserLogin.y) <= 1);
assert.ok((await layout(page)).scrollWidth <= viewport.width + 1);
await form.screenshot({ path: `../build/pikpak-form-${viewport.name}.png` });
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
it(`${viewport.name}:完全卸载行与设置卡片正文对齐`, async () => {
const opened = await visit(browser, '/settings/library?section=' + encodeURIComponent('应用与维护'), viewport);
const { page } = opened;
try {
await page.route('**/api/configuration', async route => {
const data = await (await route.fetch()).json();
await route.fulfill({ json: { ...data, uninstall: { ...data.uninstall, available: false } } });
});
await page.reload();
const region = page.getByRole('region', { name: '卸载 Peach', exact: true });
await region.getByText('完全卸载', { exact: true }).waitFor();
await settle(page);
const geometry = await region.evaluate(node => {
const card = node.lastElementChild!.getBoundingClientRect();
const label = [...node.querySelectorAll('p')].find(item => item.textContent === '完全卸载')!.getBoundingClientRect();
return { top: label.top - card.top, left: label.left - card.left };
});
assert.ok(geometry.top <= 12 && geometry.top >= 9, JSON.stringify(geometry));
assert.ok(Math.abs(geometry.left - 12) <= 1, JSON.stringify(geometry));
assert.deepEqual(opened.problems, []);
} finally { await opened.close() }
});
}
});