- 主题(浅色/深色/跟随系统)与正文字号(标准/大/特大)按账号存在本机, 账号切换不串、退出回默认;深色走 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 与需求更新
80 lines
3.7 KiB
TypeScript
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)
|
|
})
|