('#native-host');
+ if (host) {
+ host.style.width = args.expanded ? '760px' : '480px';
+ host.style.height = args.expanded ? '416px' : '80px';
+ host.style.bottom = args.expanded ? '48px' : '8px';
+ }
+ return;
+ }
+ if (command === 'qa_window_dismiss') bridge.emit('chat-panel:closing', {});
+ return null;
+ },
+};
+Object.assign(window, { __TAURI_EVENT_PLUGIN_INTERNALS__: { unregisterListener() {} } });
+const { i18nReady } = await import('../../src/i18n');
+await i18nReady;
+const { OpenLessPill } = await import('../../src/components/OpenLessCapsule');
+const { CapsuleTranslationBadge } = await import('../../src/components/LiveTranscriptPill');
+const { QaPanel } = await import('../../src/pages/QaPanel');
+const { Capsule } = await import('../../src/components/Capsule');
+function CapsuleFixture() {
+ const [state, set] = useState({
+ state: 'recording' as CapsuleState,
+ level: 0.2,
+ warming: false,
+ translation: false,
+ leaving: false,
+ operating: false,
+ });
+ bridge.setCapsule = (patch) => set((previous) => ({ ...previous, ...patch }));
+ return (
+
+
+ bridge.setCapsule({ leaving: true })}
+ onConfirm={() => bridge.setCapsule({ state: 'polishing' })}
+ />
+
+ );
+}
+document.body.style.background = '#0b1215';
+createRoot(document.getElementById('root')!).render(
+ new URLSearchParams(location.search).has('full') ? (
+
+
+
+ ) : qa ? (
+
+
+
+ ) : (
+
+ ),
+);
diff --git a/openless-all/app/e2e/motion.spec.ts b/openless-all/app/e2e/motion.spec.ts
index 26aca0ef2..a59cfbe1f 100644
--- a/openless-all/app/e2e/motion.spec.ts
+++ b/openless-all/app/e2e/motion.spec.ts
@@ -266,6 +266,9 @@ test('changing reduced motion stops the decorative WebGL loop', async ({ page })
});
await page.emulateMedia({ reducedMotion: 'no-preference' });
await page.getByRole('button', { name: '设置', exact: true }).click();
+ // OpenLess is now the default; opt into the optional WebGL style for this probe.
+ await page.getByRole('combobox', { name: '胶囊样式' }).click();
+ await page.getByRole('option', { name: /Siri/ }).click();
await expect
.poll(() => page.evaluate(() => (window as Window & { motionDraws: number }).motionDraws))
.toBeGreaterThan(5);
diff --git a/openless-all/app/e2e/openless-default.spec.ts b/openless-all/app/e2e/openless-default.spec.ts
new file mode 100644
index 000000000..a099f89e4
--- /dev/null
+++ b/openless-all/app/e2e/openless-default.spec.ts
@@ -0,0 +1,108 @@
+import { expect, test } from '@playwright/test';
+
+test('default capsule is white and blue with eleven bars, including legacy style links', async ({
+ page,
+}, info) => {
+ for (const style of ['classic', 'typeless']) {
+ await page.goto(`/?window=capsule&style=${style}&state=recording&level=0.2`);
+ const pill = page.locator('.ol-default-capsule');
+ await expect(pill).toBeVisible();
+ await expect(pill).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
+ await expect(pill.locator('.ol-default-waveform > span')).toHaveCount(11);
+ expect(await pill.evaluate((el) => getComputedStyle(el, '::before').backgroundColor)).toBe(
+ 'rgb(255, 255, 255)',
+ );
+ await expect(pill.locator('.ol-default-waveform > span').first()).toHaveCSS(
+ 'background-color',
+ 'rgb(37, 99, 235)',
+ );
+ await expect(page.locator('.ol-typeless-capsule')).toHaveCount(0);
+ }
+ await page.screenshot({ path: info.outputPath('capsule-recording.png'), animations: 'disabled' });
+});
+
+test('processing stays cancellable, hides confirm and shows a compact status', async ({
+ page,
+}, info) => {
+ await page.goto('/?window=capsule&style=classic&state=polishing&translation=1');
+ const pill = page.locator('.ol-default-capsule');
+ await expect(pill).toHaveAttribute('data-processing', 'true');
+ await expect(pill.locator('.ol-default-status')).toHaveCSS('opacity', '1');
+ await expect(pill.locator('.ol-default-processing-cancel')).toBeEnabled();
+ await expect(pill.locator('.ol-default-confirm')).toBeDisabled();
+ await expect(page.locator('.ol-classic-translation')).toHaveCSS(
+ 'background-color',
+ 'rgb(255, 255, 255)',
+ );
+ await expect(pill).toHaveCSS('width', '80px');
+ await expect(page.locator('.ol-classic-translation')).toHaveCSS('opacity', '0');
+ await page.screenshot({
+ path: info.outputPath('capsule-processing.png'),
+ animations: 'disabled',
+ });
+});
+
+test('sidebar distinguishes hover from selection without navigating on hover', async ({
+ page,
+}, info) => {
+ await page.emulateMedia({ colorScheme: 'light' });
+ await page.goto('/');
+ await expect(page.locator('html')).not.toHaveAttribute('data-ol-theme', 'dark');
+ const nav = page.locator('.ol-sidebar-surface nav');
+ const history = nav.getByRole('button', { name: '历史', exact: true });
+ const overview = nav.getByRole('button', { name: '概览', exact: true });
+ // Selected: black on the darker wash. Idle: light gray, no wash. Hover: near-black on a
+ // lighter wash than the selection, with a pointer.
+ await expect(overview).toHaveCSS('color', 'rgb(0, 0, 0)');
+ await expect(overview).toHaveCSS('background-color', 'rgba(0, 0, 0, 0.075)');
+ await expect(history).toHaveCSS('color', 'rgb(133, 133, 142)');
+ await expect(history).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
+ await expect(history).toHaveCSS('cursor', 'pointer');
+ await history.hover();
+ await expect(history).toHaveCSS('color', 'rgb(24, 24, 27)');
+ await expect(history).toHaveCSS('background-color', 'rgba(0, 0, 0, 0.04)');
+ await expect(page.locator('[data-ol-page="overview"]')).toBeVisible();
+ await history.click();
+ await expect(history).toHaveAttribute('aria-current', 'page');
+ await expect(history).toHaveCSS('color', 'rgb(0, 0, 0)');
+ await expect(history).toHaveCSS('background-color', 'rgba(0, 0, 0, 0.075)');
+ await expect(page.locator('[data-ol-page="history"]')).toHaveCSS('opacity', '1');
+ await page.screenshot({ path: info.outputPath('navigation.png'), animations: 'disabled' });
+});
+
+test('rapid page choices settle immediately and reduced motion leaves no animation', async ({
+ page,
+}) => {
+ await page.goto('/');
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ for (const name of ['历史', '词典', '概览']) {
+ await page
+ .locator('.ol-sidebar-surface nav')
+ .getByRole('button', { name, exact: true })
+ .click();
+ }
+ const content = page.locator('[data-ol-page="overview"]');
+ await expect(content).toBeVisible();
+ await expect(content).toHaveCSS('opacity', '1');
+ expect(
+ await content.evaluate((el) => ({
+ inert: (el as HTMLElement).inert,
+ animations: el.getAnimations().length,
+ })),
+ ).toEqual({ inert: false, animations: 0 });
+});
+
+test('quick follow-up uses white surfaces and blue accents even in dark mode', async ({
+ page,
+}, info) => {
+ await page.goto('/?window=qa');
+ const shell = page.locator('.qa-capsule-shell');
+ await expect(shell).toBeVisible();
+ expect(
+ await shell.evaluate((el) => ({
+ paper: getComputedStyle(el).getPropertyValue('--qa-paper').trim(),
+ accent: getComputedStyle(el).getPropertyValue('--qa-accent').trim(),
+ })),
+ ).toEqual({ paper: '#fff', accent: '#2563eb' });
+ await page.screenshot({ path: info.outputPath('quick-followup.png'), animations: 'disabled' });
+});
diff --git a/openless-all/app/e2e/refinements.spec.ts b/openless-all/app/e2e/refinements.spec.ts
index ec3ebe434..ec6456e2a 100644
--- a/openless-all/app/e2e/refinements.spec.ts
+++ b/openless-all/app/e2e/refinements.spec.ts
@@ -38,6 +38,8 @@ test('shortcut search and categories live in the flat settings pane; keyboard cl
test('native scrolling has no JS wheel interception and is stable when stopped', async ({
page,
}) => {
+ // The wheel distance is asserted in CSS pixels, which match device pixels only at 100%.
+ await page.addInitScript(() => localStorage.setItem('ol-font-scale', 'medium'));
await openShortcuts(page);
const content = page.locator('.ol-settings-scroll');
await expect(content).toHaveCSS('scroll-behavior', 'auto');
@@ -162,7 +164,7 @@ test('every main destination and settings category stays readable on desktop and
await expect(page.locator('main h1')).toBeVisible();
}
await page.getByRole('button', { name: '工具', exact: true }).click();
- for (const name of ['翻译', '划词追问', '速记', '纠正规则']) {
+ for (const name of ['翻译', '问任何问题', '速记', '纠正规则']) {
await page.getByRole('button', { name, exact: true }).click();
await expect(page.locator('main h1')).toBeVisible();
}
diff --git a/openless-all/app/e2e/resource-lifecycle.spec.ts b/openless-all/app/e2e/resource-lifecycle.spec.ts
index 4033fb5ab..258dd9749 100644
--- a/openless-all/app/e2e/resource-lifecycle.spec.ts
+++ b/openless-all/app/e2e/resource-lifecycle.spec.ts
@@ -20,6 +20,9 @@ test('offscreen WebGL preview releases its animation clock and resumes only when
};
});
await page.getByRole('button', { name: '设置', exact: true }).click();
+ // The default OpenLess preview is CSS-only; opt into the WebGL style under test.
+ await page.getByRole('combobox', { name: '胶囊样式' }).click();
+ await page.getByRole('option', { name: /Siri/ }).click();
const draws = () => page.evaluate(() => (window as Window & { draws: number }).draws);
await expect.poll(draws).toBeGreaterThan(5);
await page.locator('.ol-settings-scroll').evaluate((element) => {
diff --git a/openless-all/app/e2e/settings-regressions.spec.ts b/openless-all/app/e2e/settings-regressions.spec.ts
index 1664d4229..d0bf5de97 100644
--- a/openless-all/app/e2e/settings-regressions.spec.ts
+++ b/openless-all/app/e2e/settings-regressions.spec.ts
@@ -8,11 +8,11 @@ async function openSettings(page: Page) {
return page.getByRole('dialog', { name: '设置', exact: true });
}
-test('100% is the fresh default; explicit 110% survives reload and the settings titlebar stays draggable', async ({
+test('110% is the fresh default; explicit 100% survives reload and the settings titlebar stays draggable', async ({
page,
}) => {
const dialog = await openSettings(page);
- await expect(page.locator('html')).toHaveCSS('zoom', '1');
+ await expect(page.locator('html')).toHaveCSS('zoom', '1.1');
await expect(dialog.getByRole('button', { name: '返回应用', exact: true })).toHaveCount(1);
await expect(dialog.getByRole('button', { name: '关闭', exact: true })).toHaveCount(0);
expect(
@@ -21,10 +21,13 @@ test('100% is the fresh default; explicit 110% survives reload and the settings
),
).toBe(true);
await dialog.getByRole('button', { name: '外观与语言', exact: true }).click();
- await page.getByRole('combobox', { name: '界面大小', exact: true }).click();
- await page.getByRole('option', { name: '110%', exact: true }).click();
+ const interfaceSize = page.getByRole('combobox', { name: '界面大小', exact: true });
+ await expect(interfaceSize).toContainText('110%');
+ await interfaceSize.click();
+ await page.getByRole('option', { name: '100%', exact: true }).click();
+ await expect(page.locator('html')).toHaveCSS('zoom', '1');
await page.reload();
- await expect(page.locator('html')).toHaveCSS('zoom', '1.1');
+ await expect(page.locator('html')).toHaveCSS('zoom', '1');
await page.getByRole('button', { name: '设置', exact: true }).click();
expect(
await page.evaluate(
diff --git a/openless-all/app/e2e/switch-motion.spec.ts b/openless-all/app/e2e/switch-motion.spec.ts
new file mode 100644
index 000000000..035899065
--- /dev/null
+++ b/openless-all/app/e2e/switch-motion.spec.ts
@@ -0,0 +1,285 @@
+import { expect, test, type ElementHandle, type Locator, type Page } from '@playwright/test';
+
+// Every switch the user can reach in Settings (plus the services channel lists and the
+// Selection Ask page) must slide its knob between on and off, never jump. Each switch is
+// flipped twice so both directions are sampled and the mock preferences end unchanged.
+
+const errors = new WeakMap();
+test.beforeEach(async ({ page }) => {
+ const pageErrors: string[] = [];
+ errors.set(page, pageErrors);
+ page.on('pageerror', (error) => pageErrors.push(error.message));
+ await page.emulateMedia({ reducedMotion: 'no-preference' });
+ await page.goto('/?os=mac');
+ await expect(page.getByRole('button', { name: '设置', exact: true })).toBeVisible();
+});
+test.afterEach(async ({ page }) => {
+ expect(errors.get(page)).toEqual([]);
+});
+
+type Sample = {
+ name: string;
+ checked: [string, string];
+ start: number;
+ end: number;
+ xs: number[];
+ /** Knob positions at controlled points of the real CSS transition. */
+ controlledXs: number[];
+ /** elapsedTime (s) of every completed knob transform transition. */
+ finished: number[];
+};
+
+/** Clicks a switch and records its knob's left edge on every animation frame. Works on the
+ * element itself: a channel row may move within its list once its switch flips. */
+async function flip(page: Page, control: ElementHandle): Promise {
+ await control.scrollIntoViewIfNeeded();
+ // Pointer input: keyboard mode deliberately disables the knob transition.
+ const box = (await control.boundingBox())!;
+ await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
+ const before = (await control.getAttribute('aria-checked')) ?? '';
+ const name = await control.evaluate((element) => {
+ const labelledBy = element.getAttribute('aria-labelledby');
+ return (
+ element.getAttribute('aria-label') ??
+ (labelledBy ? document.getElementById(labelledBy)?.textContent : null) ??
+ ''
+ ).trim();
+ });
+ const start = await control.evaluate((element) => {
+ const knob = element.querySelector('.ol-switch-thumb')!;
+ return knob.getBoundingClientRect().left;
+ });
+ const sampling = control.evaluate(async (element) => {
+ const knob = element.querySelector('.ol-switch-thumb')!;
+ const finished: number[] = [];
+ const controlledXs: number[] = [];
+ const onRun = (event: TransitionEvent) => {
+ if (event.target !== knob || event.propertyName !== 'transform') return;
+ const transition = knob
+ .getAnimations()
+ .find(
+ (animation) =>
+ animation instanceof CSSTransition && animation.transitionProperty === 'transform',
+ );
+ if (!transition?.effect) return;
+ const duration = transition.effect.getComputedTiming().duration;
+ if (typeof duration !== 'number' || duration <= 0) return;
+ // CI can paint only every 80–100ms, missing the useful part of a 180ms ease-out.
+ // Seek within the real transition without pausing it. Pausing and playing can defer
+ // WebKit's restart until another paint, extending the slide into the row reorder.
+ // Restore the original timeline so sampling cannot delay normal completion.
+ const startTime = transition.startTime;
+ const currentTime = transition.currentTime;
+ for (const fraction of [0.25, 0.5, 0.75]) {
+ transition.currentTime = duration * fraction;
+ controlledXs.push(knob.getBoundingClientRect().left);
+ }
+ if (startTime !== null) transition.startTime = startTime;
+ else transition.currentTime = currentTime;
+ };
+ const onEnd = (event: TransitionEvent) => {
+ if (event.target === knob && event.propertyName === 'transform') {
+ finished.push(event.elapsedTime);
+ }
+ };
+ knob.addEventListener('transitionrun', onRun);
+ knob.addEventListener('transitionend', onEnd);
+ const xs: number[] = [];
+ for (let frame = 0; frame < 45; frame++) {
+ await new Promise(requestAnimationFrame);
+ // A remount would leave this node detached and frozen: count it as a jump.
+ xs.push(element.isConnected ? knob.getBoundingClientRect().left : Number.NaN);
+ }
+ knob.removeEventListener('transitionrun', onRun);
+ knob.removeEventListener('transitionend', onEnd);
+ return { xs, controlledXs, finished };
+ });
+ await page.mouse.down();
+ await page.mouse.up();
+ const { xs, controlledXs, finished } = await sampling;
+ const after = (await control.getAttribute('aria-checked')) ?? '';
+ return { name, checked: [before, after], start, end: xs.at(-1)!, xs, controlledXs, finished };
+}
+
+function expectAnimated(sample: Sample) {
+ const { name, checked, start, end, controlledXs, finished } = sample;
+ expect(checked[1], `${name} changed state`).not.toBe(checked[0]);
+ expect(Math.abs(end - start), `${name} knob travelled`).toBeGreaterThan(4);
+ // The knob's transform transition ran to completion (a jump or remount has none).
+ expect(
+ finished.some((elapsed) => elapsed >= 0.1),
+ `${name} knob finished a transform transition`,
+ ).toBe(true);
+ const low = Math.min(start, end) + 0.5;
+ const high = Math.max(start, end) - 0.5;
+ expect(controlledXs, `${name} exposed its transform transition`).toHaveLength(3);
+ expect(
+ controlledXs.some((x) => x > low && x < high),
+ `${name} knob passed through controlled intermediate positions (${start} → ${controlledXs.join(' ')} → ${end})`,
+ ).toBe(true);
+}
+
+async function exerciseVisibleSwitches(scope: Locator, label: string) {
+ const page = scope.page();
+ // Handles, not positions: flipping a channel can move its row within the list.
+ const switches = await scope.locator('[role="switch"]').elementHandles();
+ let exercised = 0;
+ for (const control of switches as ElementHandle[]) {
+ if (!(await control.isVisible())) continue;
+ const unavailable = await control.evaluate(
+ (element) =>
+ element.hasAttribute('disabled') ||
+ element.hasAttribute('data-disabled') ||
+ element.getAttribute('aria-disabled') === 'true' ||
+ // Collapsed groups keep their rows laid out but inert.
+ element.closest('[inert]') !== null,
+ );
+ if (unavailable) continue;
+ expectAnimated(await flip(page, control));
+ expectAnimated(await flip(page, control));
+ exercised++;
+ }
+ test.info().annotations.push({ type: 'switches', description: `${label}: ${exercised}` });
+ return exercised;
+}
+
+async function openSettings(page: Page) {
+ await page.getByRole('button', { name: '设置', exact: true }).click();
+ const dialog = page.getByRole('dialog', { name: '设置', exact: true });
+ await expect(dialog).toHaveCSS('opacity', '1');
+ return dialog;
+}
+
+async function openSection(dialog: Locator, name: string) {
+ await dialog
+ .getByRole('navigation', { name: '设置分类' })
+ .getByRole('button', { name, exact: true })
+ .click();
+}
+
+test('every settings switch slides between on and off', async ({ page }) => {
+ test.setTimeout(180_000);
+ const dialog = await openSettings(page);
+ const sections = await dialog
+ .getByRole('navigation', { name: '设置分类' })
+ .getByRole('button')
+ .allTextContents();
+ expect(sections.length).toBeGreaterThan(4);
+ let total = 0;
+ for (const section of sections.map((name) => name.trim())) {
+ await openSection(dialog, section);
+ // Open every collapsible group so its switches are reachable.
+ const headers = dialog.locator('[data-ol-toggle-group] > button[aria-expanded]');
+ for (let index = 0; index < (await headers.count()); index++) {
+ const header = headers.nth(index);
+ if ((await header.getAttribute('aria-expanded')) !== 'false') continue;
+ await header.click();
+ await expect(header).toHaveAttribute('aria-expanded', 'true');
+ }
+ if (section === '实验与扩展') {
+ const entries = dialog.locator('[data-ol-advanced-entry]');
+ const visibleEntries = () =>
+ entries.evaluateAll((items) =>
+ items
+ .filter((item) => item.getClientRects().length > 0)
+ .map((item) => item.getAttribute('data-ol-advanced-entry')!),
+ );
+ const visit = async (id: string, inside?: () => Promise) => {
+ await dialog.locator(`[data-ol-advanced-entry="${id}"]`).click();
+ await expect(dialog.locator('.ol-settings-back')).toBeVisible();
+ // Pages visited earlier stay mounted but hidden; only the open page is visible.
+ total += await exerciseVisibleSwitches(dialog, `advanced:${id}`);
+ await inside?.();
+ await dialog.locator('.ol-settings-back').click();
+ await expect(entries.first()).toBeVisible();
+ };
+ for (const id of await visibleEntries()) {
+ await visit(id, async () => {
+ if (id !== 'testingFeatures') return;
+ // Testing pages are only listed while their switch is on.
+ const enable = dialog.getByRole('switch', { name: '启用正在测试中的功能' });
+ await enable.click();
+ await expect(enable).toHaveAttribute('aria-checked', 'true');
+ await expect(entries.first()).toBeVisible();
+ for (const testingId of await visibleEntries()) await visit(testingId);
+ await enable.click();
+ await expect(enable).toHaveAttribute('aria-checked', 'false');
+ });
+ }
+ continue;
+ }
+ total += await exerciseVisibleSwitches(dialog, section);
+ }
+ expect(total).toBeGreaterThan(15);
+});
+
+test('channel enable switches slide in every services list', async ({ page }) => {
+ const dialog = await openSettings(page);
+ await openSection(dialog, 'AI 服务与模型');
+ let total = 0;
+ for (const view of ['语言模型', '语音识别']) {
+ await dialog
+ .locator('.ol-service-views')
+ .getByRole('button', { name: new RegExp(`^${view}`) })
+ .click();
+ const list = dialog.locator('.ol-reorder-list');
+ await expect(list.locator('[role="switch"]').first()).toBeVisible();
+ total += await exerciseVisibleSwitches(list, view);
+ }
+ expect(total).toBeGreaterThanOrEqual(2);
+});
+
+test('channel sorting waits for a slow knob transition to finish', async ({ page }) => {
+ const dialog = await openSettings(page);
+ await openSection(dialog, 'AI 服务与模型');
+ await page.addStyleTag({
+ content: '.ol-switch-thumb { transition-duration: 700ms !important; }',
+ });
+ const first = dialog.locator('.ol-reorder-list [role="switch"][aria-checked="true"]').first();
+ await expect(first).toBeVisible();
+ const name = (await first.getAttribute('aria-label'))!;
+ const control = dialog.getByRole('switch', { name, exact: true });
+ await control.evaluate((element) => {
+ const knob = element.querySelector('.ol-switch-thumb')!;
+ element.setAttribute('data-test-slide', 'pending');
+ for (const type of ['transitionend', 'transitioncancel']) {
+ knob.addEventListener(type, (event) => {
+ if ((event as TransitionEvent).propertyName === 'transform') {
+ element.setAttribute('data-test-slide', type);
+ }
+ });
+ }
+ });
+ await control.click();
+ await expect(control).toHaveAttribute('aria-checked', 'false');
+ await expect(control).toHaveAttribute('data-test-slide', 'transitionend');
+});
+
+test('the Selection Ask history switch slides', async ({ page }) => {
+ await page.getByRole('button', { name: '工具', exact: true }).click();
+ await page.getByRole('button', { name: '问任何问题', exact: true }).click();
+ const main = page.locator('main');
+ await expect(main.getByRole('switch').first()).toBeVisible();
+ expect(await exerciseVisibleSwitches(main, 'selectionAsk')).toBeGreaterThan(0);
+});
+
+test('stacked switches ripple their neighbours in collapsible groups and channel lists', async ({
+ page,
+}) => {
+ const dialog = await openSettings(page);
+ const neighbourMoves = async (group: Locator) => {
+ const switches = group.locator('[role="switch"]');
+ await expect(switches.nth(1)).toBeVisible();
+ await switches.nth(0).click();
+ return switches.nth(1).evaluate((element) => element.getAnimations().length);
+ };
+ const startup = dialog.getByRole('button', { name: '启动', exact: true });
+ if ((await startup.getAttribute('aria-expanded')) === 'false') await startup.click();
+ // The collapsible's own root is its group: switches ripple only within that list.
+ const startupGroup = startup.locator('xpath=..');
+ await expect(startupGroup).toHaveAttribute('data-ol-toggle-group', '');
+ expect(await neighbourMoves(startupGroup)).toBeGreaterThan(0);
+ await openSection(dialog, 'AI 服务与模型');
+ const channels = dialog.locator('.ol-reorder-list[data-ol-toggle-group]');
+ expect(await neighbourMoves(channels)).toBeGreaterThan(0);
+});
diff --git a/openless-all/app/e2e/visual-refinements.spec.ts b/openless-all/app/e2e/visual-refinements.spec.ts
index 66e7e2eec..cf3d00041 100644
--- a/openless-all/app/e2e/visual-refinements.spec.ts
+++ b/openless-all/app/e2e/visual-refinements.spec.ts
@@ -1,28 +1,76 @@
-import { test, expect } from '@playwright/test';
+import { test, expect, type Locator } from '@playwright/test';
-// Playwright selects reviewed references per browser and runner OS: system
-// font rasterization is intentionally not shared between macOS and Ubuntu.
+async function expectReadableLayout(dialog: Locator) {
+ const scroll = dialog.locator('.ol-settings-scroll');
+ expect(
+ await scroll.evaluate((element) => element.scrollWidth - element.clientWidth),
+ ).toBeLessThanOrEqual(1);
+ const heading = dialog.getByRole('heading', { level: 2 }).first();
+ await expect(heading).toBeVisible();
+ expect(
+ await heading.evaluate((element) => element.scrollWidth - element.clientWidth),
+ ).toBeLessThanOrEqual(1);
+}
for (const theme of ['dark', 'light'] as const) {
- test(`settings visual baseline · ${theme}`, async ({ page }) => {
+ test(`settings color and layout contracts · ${theme}`, async ({ page }) => {
await page.emulateMedia({ colorScheme: theme, reducedMotion: 'reduce' });
await page.goto('/');
await page.getByRole('button', { name: '设置', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '设置', exact: true });
- await page.getByRole('button', { name: '快捷键', exact: true }).click();
- await expect(page.locator('.ol-shortcut-row').first()).toBeVisible();
- // Collect both independent screens on failure; soft assertions still fail CI.
- await expect.soft(dialog).toHaveScreenshot(`shortcuts-${theme}.png`, {
- animations: 'disabled',
- maxDiffPixels: 100,
- threshold: 0.1,
- });
- await page.getByRole('button', { name: '外观与语言', exact: true }).click();
- await expect(page.locator('.ol-typography')).toBeVisible();
- await expect.soft(dialog).toHaveScreenshot(`typography-${theme}.png`, {
- animations: 'disabled',
- maxDiffPixels: 100,
- threshold: 0.1,
- });
+ const navigation = dialog.getByRole('navigation', { name: '设置分类' });
+ await expect(page.locator('html')).toHaveCSS('zoom', '1.1');
+ await expect(page.locator('html')).toHaveCSS('color-scheme', theme);
+
+ for (const section of ['快捷键', '外观与语言']) {
+ const button = navigation.getByRole('button', { name: section, exact: true });
+ await button.click();
+ await expect(button).toHaveAttribute('aria-current', 'page');
+ await expect(button).toHaveCSS('cursor', 'pointer');
+ await expect(button).toHaveCSS(
+ 'color',
+ theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(0, 0, 0)',
+ );
+ const heading = dialog.getByRole('heading', { name: section, exact: true });
+ await expect(heading).toBeVisible();
+ await expectReadableLayout(dialog);
+ const navigationBounds = (await navigation.boundingBox())!;
+ const headingBounds = (await heading.boundingBox())!;
+ expect(headingBounds.x).toBeGreaterThan(navigationBounds.x + navigationBounds.width);
+
+ if (section === '快捷键') {
+ const rows = dialog.locator('.ol-shortcut-row');
+ await expect(rows.first()).toBeVisible();
+ expect(await rows.count()).toBeGreaterThanOrEqual(9);
+ await expect(dialog.getByText('问任何问题', { exact: true })).toBeVisible();
+ const bounds = await rows.evaluateAll((elements) =>
+ elements.map((element) => {
+ const rect = element.getBoundingClientRect();
+ return { top: rect.top, bottom: rect.bottom, height: rect.height };
+ }),
+ );
+ for (let index = 0; index < bounds.length; index++) {
+ expect(bounds[index].height).toBeGreaterThanOrEqual(40);
+ if (index) expect(bounds[index].top).toBeGreaterThanOrEqual(bounds[index - 1].bottom - 1);
+ }
+ } else {
+ const controls = ['字体系列', '界面大小', '阅读宽度'].map((name) =>
+ dialog.getByRole('combobox', { name, exact: true }),
+ );
+ await expect(controls[1]).toContainText('110%');
+ const bounds = [];
+ for (const control of controls) {
+ await expect(control).toBeVisible();
+ bounds.push((await control.boundingBox())!);
+ }
+ for (let index = 0; index < bounds.length; index++) {
+ expect(Math.abs(bounds[index].x - bounds[0].x)).toBeLessThanOrEqual(1);
+ expect(bounds[index].height).toBeGreaterThanOrEqual(28);
+ expect(bounds[index].width).toBeGreaterThanOrEqual(200);
+ if (index)
+ expect(bounds[index].y).toBeGreaterThan(bounds[index - 1].y + bounds[index - 1].height);
+ }
+ }
+ }
});
}
diff --git a/openless-all/app/e2e/visual-refinements.spec.ts-snapshots/shortcuts-dark-chromium-darwin.png b/openless-all/app/e2e/visual-refinements.spec.ts-snapshots/shortcuts-dark-chromium-darwin.png
deleted file mode 100644
index fbba61f910d3ef0fbb0c6ca068da7cddb27a32c5..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 90221
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