import { expect, test } from '@playwright/test' test('paste English text, watch a chapter finish processing, then read it verbatim', async ({ page }) => { const user = { id: 42, username: 'fictional-reader', role: 'learner' } const book = { id: 1, title: '虚构样例书', language: 'en' } const chapterTitle = '虚构样例第一章' // Line breaks, a tab and repeated spaces must survive the whole round trip. const pasted = 'First line of the chapter.\n\tIndented line.\nTwo spaces kept.\n\nLast line.\n' const timestamps = { createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' } // The worker reports the fresh chapter as processing until the worker settles it. let status: 'processing' | 'ready' = 'processing' const chapterPayload = () => ({ id: 55, bookId: book.id, ordinal: 1, title: chapterTitle, status, charCount: [...pasted].length, errorReason: '', errorMessage: '', jobId: 7, ...timestamps, }) await page.route('**/api/v1/**', async route => { const path = new URL(route.request().url()).pathname const method = route.request().method() let data: unknown = null let statusCode = 200 if (path === '/api/v1/login') { expect(route.request().postDataJSON()).toEqual({ username: user.username, password: 'fictional-password' }) 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' && method === 'GET') { data = { items: [{ ...book, chapterCount: 0, pendingCount: 0, processingCount: 0, readyCount: 0, failedCount: 0, ...timestamps }] } } else if (path === '/api/v1/books' && method === 'POST') { const body = route.request().postDataJSON() as { requestId: string; title: string; text: string; language: string } expect(body.requestId).toMatch(/^[0-9a-f-]{36}$/) expect(body).toMatchObject({ title: chapterTitle, text: pasted, language: 'en' }) statusCode = 201 data = { book, chapter: chapterPayload(), job: { id: 7, bookId: book.id, chapterId: 55, status, attempts: 0, errorReason: '', errorMessage: '', ...timestamps }, duplicate: false, } } else if (path === '/api/v1/books/1') data = { book, chapters: [chapterPayload()] } else if (path === '/api/v1/chapters/55') { data = { book, chapter: { ...chapterPayload(), contentSha256: 'fictional-sha256', ...(status === 'ready' ? { originalText: pasted } : {}) }, navigation: { previousChapterId: null, nextChapterId: null }, } } await route.fulfill({ status: statusCode, 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() // The library lists the caller's book. await expect(page.getByRole('heading', { name: '我的书库' })).toBeVisible() await expect(page.getByRole('link', { name: book.title })).toBeVisible() // Paste text through the import form. await page.getByRole('button', { name: '导入内容' }).click() await expect(page.getByRole('heading', { name: '导入英文内容' })).toBeVisible() await page.getByLabel('标题').fill(chapterTitle) await page.getByLabel('正文').fill(pasted) await page.getByRole('button', { name: '开始处理' }).click() // The new book opens with the chapter still processing… await expect(page).toHaveURL(/\/books\/1$/) await expect(page.getByText('处理中')).toBeVisible() // …and the browser poll turns it ready without a page reload. status = 'ready' await expect(page.getByText('已就绪')).toBeVisible({ timeout: 15000 }) // Open the chapter and check the pasted text survived verbatim. await page.getByRole('link', { name: chapterTitle }).click() await expect(page).toHaveURL(/\/chapters\/55$/) const readerText = page.locator('.reader-text') await expect(readerText).toBeVisible() expect(await readerText.evaluate(element => element.textContent)).toBe(pasted) expect(await readerText.evaluate(element => getComputedStyle(element).whiteSpace)).toBe('pre-wrap') await expect(page.getByRole('button', { name: '上一章' })).toBeDisabled() await expect(page.getByRole('button', { name: '下一章' })).toBeDisabled() })