import { expect, test } from '@playwright/test' // The TXT upload path against a mocked API: pre-checks, multipart body and the hand-off to // the same processing screen the paste path uses. test('upload a UTF-8 TXT file and open the created book', async ({ page }) => { const user = { id: 42, username: 'fictional-uploader', role: 'learner' } const book = { id: 1, title: 'Studio Notes', language: 'en' } const timestamps = { createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' } const pasted = 'Mira opened the workshop.\r\n\r\n\tThe sign read “A small step…”\n' let uploaded = false 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 status = 200 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' && method === 'GET') data = { items: uploaded ? [{ ...book, chapterCount: 1, pendingCount: 0, processingCount: 0, readyCount: 1, failedCount: 0, ...timestamps }] : [] } else if (path === '/api/v1/books/upload') { // The upload must arrive as multipart: the fields and the file are inspected directly. expect(route.request().headers()['content-type']).toContain('multipart/form-data') const raw = route.request().postData() ?? '' expect(raw).toContain('name="requestId"') expect(raw).toContain('name="language"') expect(raw).toContain('name="title"') expect(raw).toContain('Studio Notes') expect(raw).toContain('filename="notes.txt"') expect(raw).toContain('Mira opened the workshop.') uploaded = true status = 201 data = { book, chapter: { id: 9, bookId: 1, ordinal: 1, title: 'Studio Notes', status: 'pending', charCount: [...pasted].length, errorReason: '', errorMessage: '', jobId: 5, ...timestamps }, job: { id: 5, bookId: 1, chapterId: 9, status: 'pending', attempts: 0, errorReason: '', errorMessage: '', ...timestamps }, duplicate: false, } } else if (path === '/api/v1/books/1') data = { book, chapters: [{ id: 9, bookId: 1, ordinal: 1, title: 'Studio Notes', status: 'ready', charCount: [...pasted].length, errorReason: '', errorMessage: '', jobId: 5, ...timestamps }] } else if (path === '/api/v1/chapters/9') data = { book, chapter: { id: 9, bookId: 1, ordinal: 1, title: 'Studio Notes', status: 'ready', charCount: [...pasted].length, errorReason: '', errorMessage: '', jobId: 5, contentSha256: 'fictional-sha', originalText: pasted, ...timestamps }, navigation: { previousChapterId: null, nextChapterId: null } } await route.fulfill({ status, 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() await page.getByRole('button', { name: '导入内容' }).click() // Element Plus hides the native radio behind a styled span, so the label is what a person // clicks; the input still carries the checked state. await page.locator('label.el-radio', { hasText: 'TXT 文件' }).click() await expect(page.getByRole('radio', { name: 'TXT 文件' })).toBeChecked() await expect(page.locator('textarea#text')).toHaveCount(0) await expect(page.getByText('仅支持 UTF-8')).toBeVisible() // An unusable file is refused in the browser, before any request is made. await page.setInputFiles('[data-testid="file-input"]', { name: 'notes.md', mimeType: 'text/markdown', buffer: Buffer.from('# heading\n') }) await expect(page.getByText('请选择 .txt 文件。')).toBeVisible() // A UTF-8 file is accepted and its metadata is shown; the title comes from the file name. await page.setInputFiles('[data-testid="file-input"]', { name: 'notes.txt', mimeType: 'text/plain', buffer: Buffer.from(pasted, 'utf8') }) await expect(page.getByTestId('file-info')).toContainText('notes.txt · UTF-8') await expect(page.getByLabel('标题')).toHaveValue('notes') await page.getByLabel('标题').fill('Studio Notes') await page.getByRole('button', { name: '上传并处理' }).click() await expect(page).toHaveURL(/\/books\/1$/) await expect(page.getByText('已就绪')).toBeVisible() await page.getByRole('link', { name: 'Studio Notes' }).click() const readerText = page.locator('.reader-text') await expect(readerText).toBeVisible() // The uploaded bytes reached the reader unchanged. expect(await readerText.evaluate(element => element.textContent)).toBe(pasted) })