Files
reader/web/tools/ui-review/desktop-reader.mjs
hectorqin fbb6dc65cf feat(ui): one Sheet for every overlay; drop 来源与刮削
- ui/sheet.tsx Sheet/ConfirmSheet replace the centred Modal, SweetAlert confirms, DialogView overlays, the reader Panel, the shelf settings floating card, the two media dialogs and Mantine Modal.
- Phones: bottom sheet. Wide windows: right-edge drawer; compact sheets (confirm, prompt, pick) open next to the tap or focused control.
- One set of exits (close, Escape, backdrop), 关闭 label, focus starts on 取消, danger confirms.
- Remove sweetalert2 and the unused @mantine/notifications.
- Remove the 来源与刮削 media settings panel and its entries in 我的 and 影音设置; TMDB and MusicBrainz stay configurable in 系统设置 > 服务配置.
- docs/ui.md 3.7 describes the rule; tests and review scripts follow the new markup.
2026-10-21 07:30:27 +08:00

104 lines
6.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { chromium } from 'playwright';
import { BOOK_ID, createReviewServer } from './server.mjs';
const server = createReviewServer({ port: 0 });
await server.listen();
const base = 'http://127.0.0.1:' + server.server.address().port;
const browser = await chromium.launch({ headless: true, ...(process.env.CHROME_PATH ? { executablePath: process.env.CHROME_PATH } : {}) });
const shots = process.env.UI_REVIEW_DIR ? resolve(process.env.UI_REVIEW_DIR) : join(resolve(import.meta.dirname, '../../..'), 'artifacts/ui-review/desktop-reader');
try {
await mkdir(shots, { recursive: true });
const page = await browser.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.goto(base);
await page.locator('input[autocomplete=username]').fill('review');
await page.locator('input[type=password]').fill('password12');
await page.locator('button[type=submit]').click();
await page.locator('.shelf-screen').waitFor();
const surface = () => page.locator('book-content').evaluate(el => {
const rect = el.getBoundingClientRect();
const flow = el.shadowRoot?.querySelector('.book-flow') ?? el.querySelector('.book-flow');
const scroller = el.dataset.paginated === 'true' ? flow : el;
return { width: rect.width, height: rect.height, top: rect.top, x: scroller.scrollLeft, y: scroller.scrollTop,
page: document.querySelector('.progress-scrubber').value };
});
async function shot(name) {
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, name + ' horizontal overflow');
await page.screenshot({ path: join(shots, 'desktop-reader-' + name + '.png'), animations: 'disabled' });
}
for (const [width, height] of [[1024, 768], [1366, 768], [1920, 1080]]) {
await page.setViewportSize({ width, height });
await page.goto(base + '/#/shelf');
await page.goto(base + '/#/book/' + BOOK_ID);
await page.waitForFunction(() => Number(document.querySelector('.progress-scrubber')?.max) > 1);
await shot('toolbar-' + width);
assert.ok(await page.locator('.footer').evaluate(el => el.getBoundingClientRect().width <= 800), 'desktop controls stay within the reading column');
assert.equal(await page.locator('.topbar').evaluate(el => {
const bar=el.getBoundingClientRect(), book=document.querySelector('book-content').getBoundingClientRect();
return Math.abs(bar.left-book.left)<1 && Math.abs(bar.right-book.right)<1;
}), true, 'book header aligns with the paper');
for (const label of ['目录', '外观', '设置', '朗读']) {
const before = await surface();
const trigger = page.locator('.reader-actions').getByRole('button', { name: label, exact: true });
await trigger.click();
const panel = page.locator('.reader-screen .sheet');
await panel.waitFor();
await shot(label + '-' + width);
const bounds = await panel.evaluate(el => {
const r = el.getBoundingClientRect();
return { left: r.left, rightGap: innerWidth - r.right, width: r.width, top: r.top, bottomGap: innerHeight - r.bottom,
overflow: el.scrollWidth > el.clientWidth };
});
assert.ok(bounds.width <= 500 && bounds.width >= 440, 'bounded panel width');
assert.ok(bounds.top >= 10 && bounds.bottomGap >= 10 && !bounds.overflow, 'panel fits viewport');
assert.ok(bounds.rightGap <= 20, 'every reader sheet is the right-edge drawer');
assert.deepEqual(await surface(), before, 'opening ' + label + ' preserves the reading position and geometry');
await panel.getByRole('button', { name: '关闭', exact: true }).press('Escape');
await panel.waitFor({ state: 'detached' });
assert.equal(await trigger.evaluate(el => document.activeElement === el), true, 'Escape returns focus to the opener');
assert.deepEqual(await surface(), before, 'closing ' + label + ' preserves the reading position');
}
const before = await surface();
await page.mouse.click(width / 2, height / 2);
await page.locator('.reader-screen[data-chrome=hidden]').waitFor();
await shot('immersive-' + width);
assert.deepEqual(await surface(), before, 'hiding chrome preserves the page');
await page.mouse.click(width / 2, height / 2);
await page.locator('.reader-screen[data-chrome=visible]').waitFor();
}
await page.locator('.reader-actions').getByRole('button', {name:'设置',exact:true}).click();
await page.locator('.sheet').getByRole('button',{name:'翻页',exact:true}).click();
await page.locator('.sheet button[aria-label="关闭"]').click();
await page.locator('.progress-scrubber').fill('2');
await page.waitForFunction(() => document.querySelector('.progress-page')?.textContent?.includes('第 2/'));
await shot('paged');
const paged = await surface();
await page.locator('.reader-actions').getByRole('button',{name:'目录',exact:true}).click();
await page.locator('.sheet button[aria-label="关闭"]').press('Escape');
assert.deepEqual(await surface(),paged,'side panels preserve the selected page in paged mode');
await page.getByRole('button',{name:'夜间',exact:true}).click();
await shot('dark');
await page.locator('.reader-actions').getByRole('button',{name:'外观',exact:true}).click();
await shot('dark-settings');
await page.locator('.sheet button[aria-label="关闭"]').click();
await page.getByRole('button',{name:'日间',exact:true}).click();
await page.setViewportSize({ width: 1024, height: 420 });
await page.locator('.reader-actions').getByRole('button', { name: '外观', exact: true }).click();
const body = page.locator('.sheet-body');
await body.hover(); await page.mouse.wheel(0, 300);
await page.waitForFunction(() => document.querySelector('.sheet-body')?.scrollTop > 0);
await shot('short-window-scroll');
await page.locator('.sheet button[aria-label="关闭"]').click();
assert.deepEqual(errors, []);
console.log('PASS desktop reader: 1024/1366/1920px, four panels, Escape/focus, immersive mode, stable page geometry and short-window scrolling');
} finally {
await browser.close();
await server.close();
}