frontend/e2e/source-credentials-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})`, { timeout: 60_000 }, async () => {
const opened = await visit(browser, '/sources', viewport);
const page = opened.page;
const items: unknown[] = [];
await page.route('**/api/custom-metadata', async route => {
const body = route.request().method() === 'POST' ? route.request().postDataJSON() : null;
if (body?.action === 'preview') {
assert.equal(body.code, 'DEMO-001');
await route.fulfill({ json: { record: { code: 'DEMO-001', title: '接口试查结果' } } });
return;
}
if (body) items.push({ ...body, id: 'custom-demo' });
await route.fulfill({ json: { items } });
});
try {
await page.reload();
const region = page.getByRole('region', { name: '自定义元数据来源', exact: true });
const add = region.getByRole('button', { name: '添加来源', exact: true });
const assertClosedCard = async () => {
await add.waitFor(); await settle(page);
const geometry = await add.evaluate(button => {
const row = button.parentElement!;
const card = button.closest('section')!.lastElementChild!;
return { divider: parseFloat(getComputedStyle(row).borderTopWidth),
radius: parseFloat(getComputedStyle(card).borderTopLeftRadius),
top: button.getBoundingClientRect().top - card.getBoundingClientRect().top };
});
assert.equal(geometry.divider, 0, JSON.stringify(geometry));
assert.ok(geometry.radius > 0 && geometry.top >= 12 && geometry.top <= 18, JSON.stringify(geometry));
};
await assertClosedCard();
await page.getByRole('button', { name: '添加来源', exact: true }).click();
const form = page.getByRole('form', { name: '元数据来源配置', exact: true });
await form.getByRole('button', { name: '取消', exact: true }).click();
await form.waitFor({ state: 'detached' });
await assertClosedCard();
await region.screenshot({ path: `../build/agent-verification/browser/custom-source-closed-${viewport.name}.png` });
await add.click();
const editorGeometry = await region.evaluate(node => {
const card = node.lastElementChild!.getBoundingClientRect();
const input = node.querySelector('form label')!.getBoundingClientRect();
const form = node.querySelector('form')!.getBoundingClientRect();
const footer = node.querySelector('form button[type="submit"]')!.parentElement!.getBoundingClientRect();
return { top: input.top - card.top, left: footer.left - card.left, right: card.right - footer.right, bottom: form.bottom - footer.bottom };
});
assert.ok(editorGeometry.top >= 14 && editorGeometry.top <= 18, JSON.stringify(editorGeometry));
assert.ok(Math.abs(editorGeometry.left) <= 1 && Math.abs(editorGeometry.right) <= 1 && Math.abs(editorGeometry.bottom) <= 1, JSON.stringify(editorGeometry));
await form.getByRole('textbox', { name: '来源名称', exact: true }).fill('公开演示元数据');
await form.getByRole('textbox', { name: 'JSON 接口地址', exact: true }).fill('https://example.com/api/{code}');
const code = form.getByRole('textbox', { name: '试查番号(回车查询)', exact: true });
await code.fill('DEMO-001');
await code.press('Enter');
await form.locator('pre').filter({ hasText: '接口试查结果' }).waitFor();
const dimensions = await layout(page);
assert.ok(dimensions.scrollWidth <= dimensions.viewportWidth + 1, JSON.stringify(dimensions));
assert.deepEqual(dimensions.offenders, []);
await page.getByRole('region', { name: '自定义元数据来源', exact: true }).screenshot({
path: `../build/agent-verification/browser/custom-source-${viewport.name}.png`,
});
await form.getByRole('button', { name: '保存来源', exact: true }).click();
await form.waitFor({ state: 'detached' });
const savedSource = region.getByText('公开演示元数据', { exact: true }).locator('..');
await savedSource.locator('[data-status-tone="neutral"]').filter({ hasText: '已停用' }).waitFor();
assert.equal(items.length, 1);
assert.equal(await add.evaluate(button => parseFloat(getComputedStyle(button.parentElement!).borderTopWidth)), 1);
assert.deepEqual(opened.problems, []);
} finally { await opened.close(); }
});
it(`GitHub 标识、网址和外链箭头居中对齐(${viewport.name})`, async () => {
const opened = await visit(browser, '/sources', viewport);
try {
await opened.page.route('**/api/scraping/amane-bridge', route => route.fulfill({ json: {
installed: true, installed_version: '0.17.0', version: '0.17.0',
repository: 'https://github.com/sgzw-x/amane', license: 'GPL-3.0', sites: [], job: { status: 'idle' },
} }));
await opened.page.reload();
const link = opened.page.locator('section[aria-label="amane"] a');
await link.waitFor(); await settle(opened.page);
const positions = await link.evaluate(node => {
const boxes = [node.querySelector('img')!, node.querySelector('span')!, node.querySelector('svg')!].map(el => {
const r = el.getBoundingClientRect(); return { center: r.y + r.height / 2, width: r.width };
});
return boxes;
});
assert.equal(positions.length, 3);
assert.ok(positions.every(p => p.width > 0));
assert.ok(Math.max(...positions.map(p => p.center)) - Math.min(...positions.map(p => p.center)) <= 1);
const dimensions = await layout(opened.page);
assert.ok(dimensions.scrollWidth <= dimensions.viewportWidth);
} finally { await opened.close(); }
});
it(`凭据的每个输入字段占满正文宽度(${viewport.name})`, async () => {
const opened = await visit(browser, '/settings/library?section='+encodeURIComponent('来源与订阅'), viewport);
try {
await opened.page.route('**/api/follow/credentials', route => route.fulfill({ json: {
root: '', providers: [{ provider: 'simpcity', provider_label: 'SimpCity', followable: true,
requirement: 'required', needs: ['cookie', 'user_agent'], fields: [], missing: ['cookie', 'user_agent'], why: '来源登录说明' }],
} }));
await opened.page.reload();
const field = opened.page.getByLabel('cookie', { exact: true });
await field.waitFor(); await settle(opened.page);
const labels = await opened.page.evaluate(() => ['来源凭据', '作品资料站点'].map(name => {
const section = document.querySelector(`[aria-label="${name}"]`)!;
const label = section.querySelector('p')!;
return { x: label.getBoundingClientRect().x, size: getComputedStyle(label).fontSize, weight: getComputedStyle(label).fontWeight };
}));
assert.deepEqual(labels[0], labels[1], '配置分区标题对齐且使用相同排版');
for (const name of ['cookie', 'user_agent']) {
const geometry = await opened.page.getByLabel(name, { exact: true }).evaluate(node => {
const wrapper = node.closest('[data-input-size]')!;
return { width: wrapper.getBoundingClientRect().width, available: wrapper.parentElement!.getBoundingClientRect().width };
});
assert.ok(Math.abs(geometry.width - geometry.available) <= 1, JSON.stringify(geometry));
}
const dimensions = await layout(opened.page);
assert.ok(dimensions.scrollWidth <= dimensions.viewportWidth);
} finally { await opened.close(); }
});
}
});