Files
lexgo/learner/e2e/mobile-layout.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

88 lines
4.7 KiB
TypeScript

import { expect, test } from '@playwright/test'
import { login, mockLearnerApi, overflowOffenders } from './mobile-fixtures'
// Every learner surface on a phone viewport: nothing sticks out sideways and every page can be
// left again. The measured truth in #14 was that the layouts already fit; this locks that in.
test('every page fits the phone width and can be left again', async ({ page }) => {
await mockLearnerApi(page)
await login(page)
const surfaces: { name: string; open: () => Promise<void>; back: () => Promise<void> }[] = [
{
name: '书库',
open: async () => { await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible() },
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '导入',
open: async () => { await page.getByRole('button', { name: '导入内容' }).click(); await expect(page.getByRole('heading', { name: '导入英文内容' })).toBeVisible() },
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '书籍',
open: async () => { await page.getByRole('link', { name: 'Fictional mobile book' }).click(); await expect(page.locator('.chapter-list')).toBeVisible() },
back: async () => { await page.getByRole('link', { name: '我的书库' }).first().click() },
},
{
name: '阅读',
open: async () => {
await page.getByRole('link', { name: 'Fictional mobile book' }).click()
await page.locator('.chapter-list a.chapter-name').first().click()
await expect(page.locator('.reader-text')).toBeVisible()
},
back: async () => { await page.locator('.breadcrumb').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '查词面板',
open: async () => {
await page.getByRole('link', { name: 'Fictional mobile book' }).click()
await page.locator('.chapter-list a.chapter-name').first().click()
await expect(page.locator('.reader-text')).toBeVisible()
await page.locator('.reader-word').first().click()
await expect(page.locator('.lookup-panel')).toBeVisible()
},
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '生词本',
open: async () => { await page.locator('.site-header nav').getByRole('link', { name: '生词本' }).click(); await expect(page.getByRole('heading', { name: '生词本' })).toBeVisible() },
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '复习',
open: async () => { await page.locator('.site-header nav').getByRole('link', { name: '到期复习' }).click(); await expect(page.getByTestId('review-card')).toBeVisible() },
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
{
name: '进度',
open: async () => { await page.locator('.site-header nav').getByRole('link', { name: '进度' }).click(); await expect(page.getByRole('heading', { name: '我的进度' })).toBeVisible() },
back: async () => { await page.locator('.site-header nav').getByRole('link', { name: '我的书库' }).click() },
},
]
for (const surface of surfaces) {
await surface.open()
expect(await overflowOffenders(page), `${surface.name} must not overflow sideways`).toEqual([])
const widths = await page.evaluate(() => ({ scroll: document.documentElement.scrollWidth, inner: window.innerWidth }))
expect(widths.scroll, `${surface.name} must not scroll sideways`).toBeLessThanOrEqual(widths.inner)
await page.screenshot({ path: `../.local/evidence/issue14-mobile-${surface.name}.png`, fullPage: true })
await surface.back()
await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible()
}
})
test('the display control is reachable on a phone', async ({ page }) => {
await mockLearnerApi(page)
await login(page)
const trigger = page.getByTestId('display-trigger')
await expect(trigger).toBeVisible()
const box = (await trigger.boundingBox())!
// A finger needs a target of at least 38 pixels; the control is in the header on every page.
expect(box.height).toBeGreaterThanOrEqual(36)
await trigger.tap()
await expect(page.getByRole('menu').getByText('主题')).toBeVisible()
await page.getByRole('menu').getByText('深色', { exact: true }).tap()
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
await page.screenshot({ path: '../.local/evidence/issue14-mobile-dark.png' })
})