Files
lexgo/learner/e2e/theme.spec.ts
T
ila e069894c4e feat: 补齐桌面与手机体验、显示偏好与键盘操作 (#14)
- 主题(浅色/深色/跟随系统)与正文字号(标准/大/特大)按账号存在本机,
  账号切换不串、退出回默认;深色走 html[data-theme] 与 Element Plus 的 html.dark
- style.css 收敛为语义调色板::root 的 53 个变量是文件内仅有的颜色字面量,
  其余规则全部走 var(),暗色只覆盖变量
- 字号经 --reader-font-scale 只作用于阅读面,不做全局缩放
- 阅读位置按账号+章节保存滚动比例与该章 content_sha256,正文换版本后不恢复
- 复习页键盘:空格/Enter 显示答案、1/2/3 评分;输入控件与聚焦按钮的按键不被劫持
- 站点头部新增「显示」控件,七个学习页面共用
- Playwright 新增 390×844 hasTouch 的 mobile 项目与移动用例(无溢出、可返回、
  触摸滑动不误开面板、深色与字号持久化、账号隔离);新增主题变量回归用例
- Wiki 记录 Architecture、Business-Rules、Local-Development 与需求更新
2026-09-15 15:11:30 +08:00

80 lines
3.7 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test'
const user = { id: 42, username: 'fictional-theme', role: 'learner' }
// The palette is one indirection deep: a CSS variable that references itself silently resolves to
// nothing and every surface loses its colour. These checks read the values back from the browser.
const palette = ['--bg', '--surface', '--surface-input', '--text', '--text-muted', '--border', '--accent',
'--accent-marker', '--notice-bg', '--status-ready-bg', '--status-read-bg', '--status-failed-bg', '--word-learning-bg',
'--saved', '--sense-bg', '--el-color-primary']
async function mockApi(page: Page): Promise<void> {
await page.route('**/api/v1/**', async route => {
const path = new URL(route.request().url()).pathname
let data: unknown = null
if (path === '/api/v1/login') data = { token: 'fictional-session', user }
else if (path === '/api/v1/me') data = user
else if (path === '/api/v1/space') data = { ownerId: user.id, language: 'en' }
else if (path === '/api/v1/books') data = { items: [] }
await route.fulfill({ json: { code: 200, data } })
})
}
async function login(page: Page): Promise<void> {
await page.goto('/')
await page.getByLabel('账号').fill(user.username)
await page.getByLabel('密码', { exact: true }).fill('fictional-password')
await page.getByRole('button', { name: '登录', exact: true }).click()
await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible()
}
test('every palette colour resolves, in both themes', async ({ page }) => {
await mockApi(page)
await login(page)
const read = () => page.evaluate(names => {
const root = getComputedStyle(document.documentElement)
const values: Record<string, string> = {}
for (const name of names) values[name] = root.getPropertyValue(name).trim()
// The palette is declared on :root, so that is where the painted surface is read.
const html = getComputedStyle(document.documentElement)
return {
values,
body: html.backgroundColor,
text: getComputedStyle(document.body).color,
}
}, palette)
const light = await read()
for (const name of palette) {
expect(light.values[name], `${name} must resolve in the light theme`).not.toBe('')
}
// The page really paints with them: an unresolved background would be transparent.
expect(light.body).not.toBe('rgba(0, 0, 0, 0)')
expect(light.body).not.toBe('transparent')
expect(light.text).not.toBe(light.body)
await page.screenshot({ path: '../.local/evidence/issue14-theme-light.png' })
await page.getByTestId('display-trigger').click()
await page.getByRole('menu').getByText('深色', { exact: true }).click()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
const dark = await read()
for (const name of palette) {
expect(dark.values[name], `${name} must resolve in the dark theme`).not.toBe('')
}
// The dark theme is a different surface, not the same one relabelled.
expect(dark.values['--bg']).not.toBe(light.values['--bg'])
expect(dark.body).not.toBe(light.body)
expect(dark.text).not.toBe(dark.body)
// Element Plus follows the same choice, so its components do not stay on a white card.
const epBackground = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--el-bg-color').trim())
expect(epBackground).not.toBe('')
await page.screenshot({ path: '../.local/evidence/issue14-theme-dark.png' })
// Back to light by hand, and the surface returns to the original value.
await page.getByTestId('display-trigger').click()
await page.getByRole('menu').getByText('浅色', { exact: true }).click()
expect((await read()).body).toBe(light.body)
})