|
|
@@ -10,7 +10,7 @@
|
|
|
// open llm seam.
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
-import type { Browser, Page } from 'playwright'
|
|
|
+import type { Browser, Locator, Page } from 'playwright'
|
|
|
import { chromium } from 'playwright'
|
|
|
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
|
|
import { join } from 'node:path'
|
|
|
@@ -190,13 +190,33 @@ describe('web e2e: settings modal and General preferences', () => {
|
|
|
expect(tripwire.pageErrors).toEqual([])
|
|
|
}, 60_000)
|
|
|
|
|
|
+ async function selectTheme(cube: Locator, preference: 'light' | 'dark' | 'system'): Promise<void> {
|
|
|
+ // Optimistic UI and a file value from an earlier gesture do not prove this write finished.
|
|
|
+ const [response] = await Promise.all([
|
|
|
+ page.waitForResponse((candidate) => {
|
|
|
+ if (candidate.request().method() !== 'POST'
|
|
|
+ || new URL(candidate.url()).pathname !== '/api/settings/mutate') return false
|
|
|
+ const { payload: { args } } = candidate.request().postDataJSON() as {
|
|
|
+ payload: { args: { ns: string; ops: { op: string; path: string[]; value?: unknown }[] } }
|
|
|
+ }
|
|
|
+ return args.ns === 'ui-theme' && args.ops.some(op => op.op === 'set'
|
|
|
+ && op.path.length === 1 && op.path[0] === 'preference' && op.value === preference)
|
|
|
+ }, { timeout: 5_000 }),
|
|
|
+ cube.click(),
|
|
|
+ ])
|
|
|
+ expect(response.ok()).toBe(true)
|
|
|
+ expect(await response.json()).toMatchObject({
|
|
|
+ result: { ok: true, value: { ns: 'ui-theme', value: { preference } } },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
it('uses the persisted dark preference while plugins are still loading', async () => {
|
|
|
onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-boot-theme'))
|
|
|
await page.emulateMedia({ colorScheme: 'light' })
|
|
|
await page.getByRole('button', { name: '设置', exact: true }).click()
|
|
|
const initialDialog = page.getByRole('dialog', { name: '设置' })
|
|
|
const darkCube = initialDialog.getByRole('button', { name: '深色' })
|
|
|
- await darkCube.click()
|
|
|
+ await selectTheme(darkCube, 'dark')
|
|
|
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
|
|
await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
|
|
|
.toMatch(/ui-theme:\n\s+preference: dark/)
|
|
|
@@ -242,7 +262,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
|
|
await page.getByRole('button', { name: '设置', exact: true }).click()
|
|
|
const restoredDialog = page.getByRole('dialog', { name: '设置' })
|
|
|
const systemCube = restoredDialog.getByRole('button', { name: '跟随系统' })
|
|
|
- await systemCube.click()
|
|
|
+ await selectTheme(systemCube, 'system')
|
|
|
await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
|
|
await expect.poll(() => page.evaluate(() => document.body.hasAttribute('data-ds-dark-theme')), {
|
|
|
timeout: 5_000,
|
|
|
@@ -291,7 +311,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
|
|
await dialog.waitFor({ timeout: 10_000 })
|
|
|
const darkCube = dialog.getByRole('button', { name: '深色' })
|
|
|
expect(await darkCube.getAttribute('aria-pressed')).toBe('false')
|
|
|
- await darkCube.click()
|
|
|
+ await selectTheme(darkCube, 'dark')
|
|
|
// The full cascade: pressed state, Host-backed preference, body attribute,
|
|
|
// alias token flip — all from one real user gesture.
|
|
|
await expect.poll(() => darkCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
|
|
@@ -340,7 +360,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
|
|
// `system` follows the emulated OS scheme (dark stays dark, light clears).
|
|
|
await page.getByRole('button', { name: '设置', exact: true }).click()
|
|
|
const systemCube = page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '跟随系统' })
|
|
|
- await systemCube.click()
|
|
|
+ await selectTheme(systemCube, 'system')
|
|
|
await expect.poll(() => systemCube.getAttribute('aria-pressed'), { timeout: 5_000 }).toBe('true')
|
|
|
await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(false)
|
|
|
expectThemeColorSynchronized(await readState())
|
|
|
@@ -349,7 +369,7 @@ describe('web e2e: settings modal and General preferences', () => {
|
|
|
expectThemeColorSynchronized(await readState())
|
|
|
// Restore for the specs that follow: light preference beats the emulated
|
|
|
// dark OS scheme, leaving the shared page in the light default.
|
|
|
- await page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '浅色' }).click()
|
|
|
+ await selectTheme(page.getByRole('dialog', { name: '设置' }).getByRole('button', { name: '浅色' }), 'light')
|
|
|
await expect.poll(async () => (await readState()).attr, { timeout: 5_000 }).toBe(false)
|
|
|
expectThemeColorSynchronized(await readState())
|
|
|
await page.keyboard.press('Escape')
|