- schema v8:lexgo_book_attachments(按 book_id+kind 唯一,bytes MEDIUMBLOB)与 lexgo_playback_positions(owner_id+book_id 主键),只用可重放 DDL,外键级联删书 - 后端:按文件头 magic bytes 判定类型(MP3 / JPG / PNG / WebP,含 WebP 维度解析), 音频 ≤20 MiB、封面 ≤2 MiB 且 ≤4096²;上传/替换/移除;binaryResponse 交给 http.ServeContent 提供 Range 206、416、ETag 与 304;播放位置 upsert,替换音频时重置 - 学习端:带凭据的 fetch + 对象 URL(令牌不进 URL),书库封面、书籍页「音频与封面」 区块、阅读页常驻播放器(播放/暂停、进度、0.75–1.5 倍速、错误重试、位置上报) - 测试:Go 86 项(新增附件单测/集成与 v7→v8 迁移)、学习端 157 单测与 26 项 E2E - 顺带修掉 e2e/phrase.spec.ts 的偶发失败:重试条件改为断言期望词数、重读坐标、关面板后重试 - 文档:Architecture / Business-Rules / Local-Development / Requirements / Home / Deployment-and-Operations(备份范围含附件与体积提示)
78 lines
4.5 KiB
TypeScript
78 lines
4.5 KiB
TypeScript
import { expect, test } from '@playwright/test'
|
|
import { login, mockLearnerApi, touchSwipe } from './mobile-fixtures'
|
|
|
|
// Touch must scroll the chapter instead of being swallowed by the selection handling, and a tap
|
|
// must still open the word panel. This is emulated touch input through Chromium, not a real phone.
|
|
test('a touch swipe scrolls the chapter and does not open the word panel', async ({ page }) => {
|
|
await mockLearnerApi(page)
|
|
await login(page)
|
|
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()
|
|
// The words arrive with the token response, so wait for them instead of sampling once.
|
|
await expect.poll(async () => page.locator('.reader-word').count()).toBeGreaterThan(3)
|
|
|
|
// A swipe upwards scrolls the page down and leaves the panel closed.
|
|
await touchSwipe(page, 640, 240)
|
|
await expect.poll(async () => page.evaluate(() => window.scrollY)).toBeGreaterThan(0)
|
|
expect(await page.locator('.lookup-panel').count()).toBe(0)
|
|
await page.screenshot({ path: '../.local/evidence/issue14-mobile-scrolled.png' })
|
|
|
|
// A tap on a word still opens the panel, which sits above the text without covering it.
|
|
await page.locator('.reader-word').nth(1).tap()
|
|
const panel = page.locator('.lookup-panel')
|
|
await expect(panel).toBeVisible()
|
|
const panelBox = (await panel.boundingBox())!
|
|
expect(panelBox.x).toBeGreaterThanOrEqual(0)
|
|
expect(panelBox.x + panelBox.width).toBeLessThanOrEqual(391)
|
|
// The page reserves room for the fixed sheet, so the last paragraph can still be read.
|
|
const padding = await page.evaluate(() => getComputedStyle(document.querySelector('.reader-page') as Element).paddingBottom)
|
|
expect(parseFloat(padding)).toBeGreaterThan(100)
|
|
|
|
// Closing the panel keeps the reader where they were: the tapped word stays on screen. The page
|
|
// releases the space it reserved for the sheet, so the browser may clamp the scroll by a little
|
|
// when the chapter end is already in view; what must not happen is losing sight of the word.
|
|
const word = page.locator('.reader-word').nth(1)
|
|
await page.getByRole('button', { name: '关闭释义' }).tap()
|
|
await expect(panel).toHaveCount(0)
|
|
await expect(word).toBeInViewport()
|
|
})
|
|
|
|
test('the display choice survives a reload and belongs to the account', async ({ page }) => {
|
|
await mockLearnerApi(page)
|
|
await login(page)
|
|
await page.getByTestId('display-trigger').tap()
|
|
await page.getByRole('menu').getByText('深色', { exact: true }).tap()
|
|
await page.getByTestId('display-trigger').tap()
|
|
await page.getByRole('menu').getByText('特大', { exact: true }).tap()
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
|
|
|
|
const readerScale = await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--reader-font-scale').trim())
|
|
expect(readerScale).toBe('1.3')
|
|
|
|
// Reloading keeps both choices: they are stored, not just applied.
|
|
await page.reload()
|
|
await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible()
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
|
|
expect(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--reader-font-scale').trim())).toBe('1.3')
|
|
|
|
// The reading text really is larger, which is what the choice promises.
|
|
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()
|
|
const size = await page.locator('.reader-text').evaluate(element => parseFloat(getComputedStyle(element).fontSize))
|
|
expect(size).toBeGreaterThan(16)
|
|
await page.screenshot({ path: '../.local/evidence/issue14-mobile-dark-large.png' })
|
|
|
|
// Signing out and back in as another account must not inherit the first account's choice.
|
|
await page.getByRole('button', { name: '退出登录' }).tap()
|
|
await expect(page.getByRole('heading', { name: '欢迎回来' })).toBeVisible()
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light')
|
|
await page.getByLabel('账号').fill('fictional-mobile')
|
|
await page.getByLabel('密码', { exact: true }).fill('fictional-password')
|
|
await page.getByRole('button', { name: '登录', exact: true }).click()
|
|
await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible()
|
|
// The same account keeps its own choice.
|
|
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
|
|
})
|