import { expect, test, type Page } from '@playwright/test' // Completing a chapter and reading the progress page, against a mocked API. test('marking a chapter read shows up in the book and on the progress page once', async ({ page }) => { const user = { id: 42, username: 'fictional-progress', role: 'learner' } const chapter = { id: 3, bookId: 7, ordinal: 1, title: 'Fictional chapter', status: 'ready', charCount: 31, errorReason: '', errorMessage: '', jobId: 9, readAt: null as string | null, createdAt: '', updatedAt: '', } let completions = 0 await page.route('**/api/v1/**', async route => { const url = new URL(route.request().url()) const path = url.pathname const method = route.request().method() 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: [{ id: 7, title: 'Fictional reader', language: 'en', chapterCount: 1, pendingCount: 0, processingCount: 0, readyCount: 1, failedCount: 0, createdAt: '', updatedAt: '' }] } else if (path === '/api/v1/books/7') data = { book: { id: 7, title: 'Fictional reader', language: 'en' }, chapters: [chapter] } else if (path === '/api/v1/chapters/3/tokens') data = { tokens: [{ start: 0, end: 9, text: 'Curiosity', kind: 'word' }] } else if (path === '/api/v1/chapters/3' && method === 'GET') { data = { book: { id: 7, title: 'Fictional reader', language: 'en' }, chapter: { ...chapter, contentSha256: 'sha', originalText: 'Curiosity opens the first door.\n' }, navigation: { previousChapterId: null, nextChapterId: null } } } else if (path === '/api/v1/chapters/3/complete') { completions += 1 const readAt = '2026-09-15T03:04:05Z' chapter.readAt = readAt data = { progress: { chapterId: 3, bookId: 7, read: true, readAt, duplicate: completions > 1 } } } else if (path === '/api/v1/progress') { data = { readChapters: chapter.readAt ? 1 : 0, totalChapters: 1, knownTerms: 2, learningTerms: 1, newTerms: 3, ignoredTerms: 1, savedTerms: 7, dueNow: chapter.readAt ? 1 : 4, books: [{ id: 7, title: 'Fictional reader', readChapters: chapter.readAt ? 1 : 0, totalChapters: 1 }], } } else if (path === '/api/v1/reviews/queue') data = { items: [], total: 0 } await route.fulfill({ json: { code: 200, data } }) }) 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() // The reader offers the explicit completion action and says what it does and does not do. await page.goto('/chapters/3') await expect(page.getByRole('heading', { name: 'Fictional chapter' })).toBeVisible() await expect(page.getByTestId('mark-read')).toHaveText('标记本章已读') await expect(page.locator('.chapter-complete')).toContainText('只记录已读,不改变词语状态') await page.screenshot({ path: '../.local/evidence/issue13-reader-unread.png' }) // Completing shows the reading time, and repeating is answered without counting twice. await page.getByTestId('mark-read').click() await expect(page.getByTestId('chapter-read-state')).toContainText('本章已读') await expect(page.getByTestId('complete-notice')).toContainText('已记为读完本章') await page.screenshot({ path: '../.local/evidence/issue13-reader-read.png' }) await page.getByTestId('mark-read').click() await expect(page.getByTestId('complete-notice')).toContainText('没有重复计数') // The book list shows which chapters are read. await page.goto('/books/7') await expect(page.getByTestId('chapter-read-chip').first()).toHaveText('已读') // The progress page reports the reading total and keeps the four word statuses apart. await page.getByRole('link', { name: '进度' }).click() await expect(page).toHaveURL(/\/progress/) await expect(page.getByTestId('stat-read')).toContainText('1 / 1') await expect(page.getByTestId('stat-known')).toContainText('2') await expect(page.getByTestId('stat-ignored')).toContainText('1') await expect(page.getByTestId('stat-due')).toContainText('1') await expect(page.getByTestId('progress-books')).toContainText('Fictional reader') await page.screenshot({ path: '../.local/evidence/issue13-progress.png' }) // Reloading keeps the same numbers: they come from the server, not from the page. await page.reload() await expect(page.getByTestId('stat-read')).toContainText('1 / 1') expect(completions).toBe(2) }) test('a fresh space explains the empty progress instead of showing zeros alone', async ({ page }) => { const user = { id: 42, username: 'fictional-progress', role: 'learner' } 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: [] } else if (path === '/api/v1/progress') { data = { readChapters: 0, totalChapters: 0, knownTerms: 0, learningTerms: 0, newTerms: 0, ignoredTerms: 0, savedTerms: 0, dueNow: 0, books: [] } } await route.fulfill({ json: { code: 200, data } }) }) 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 page.goto('/progress') await expect(page.getByTestId('stat-read')).toContainText('0 / 0') await expect(page.getByTestId('progress-no-books')).toContainText('还没有书') await expect(page.getByTestId('stat-ignored')).toContainText('0') })