import { expect, test, type Page } from '@playwright/test' import { readFileSync } from 'node:fs' const active = (page: Page) => page.locator('.taro_page').last() async function openEntry(page: Page, title: string) { await page.goto('/') await page.locator('.workbench-card').filter({ hasText: title }).click() } async function example(page: Page) { if (await page.locator('.mistake-capture-page, .paper-capture-page').count()) { const chooser = page.waitForEvent('filechooser') await page.getByText('从相册选择', { exact: true }).click() await (await chooser).setFiles('src/assets/sample-paper.png') return } if (await page.locator('.answer-capture-page').count()) { const chooser = page.waitForEvent('filechooser') await page.locator('.answer-album-button').click() await (await chooser).setFiles('src/assets/sample-answer.png') return } await page.getByText('添加示例照片', { exact: true }).click() } async function chooseStudents(page: Page) { await active(page).getByText('选择学生', { exact: true }).click() await page.locator('.student-class-trigger').click() await page.locator('.student-class-option').filter({ hasText: '初一' }).click() await page.locator('.student-names-trigger').click() await page.locator('.student-option').filter({ hasText: '李一' }).click() await page.locator('.student-class-trigger').click() await page.locator('.student-class-option').filter({ hasText: '初二' }).click() await page.locator('.student-names-trigger').click() await page.locator('.student-option').filter({ hasText: '李一' }).click() await page.locator('.student-confirm-button').click() } test('student associations belong to one upload and clear when reopening the page', async ({ page }) => { await openEntry(page, '上传错题') await chooseStudents(page) await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一') await page.getByText('查看示例', { exact: true }).click() await page.locator('.back-button').click() await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一') await page.locator('.back-button').click() await page.locator('.workbench-card').filter({ hasText: '上传错题' }).click() await expect(page.getByText('识别前请选择学生,支持跨班级多选', { exact: true })).toBeVisible() await active(page).getByText('选择学生', { exact: true }).click() await expect(page.locator('.student-selected-tag')).toHaveCount(0) await page.locator('.student-cancel-button').click() await chooseStudents(page) await page.reload() await expect(page.getByText('识别前请选择学生,支持跨班级多选', { exact: true })).toBeVisible() await page.screenshot({ path: `test-results/fresh-upload-${test.info().project.name}.png`, scale: 'css' }) }) test('photos and documents can be added before students are required at recognition', async ({ page }) => { await openEntry(page, '上传错题') const camera = page.waitForEvent('filechooser') await page.locator('.capture-zone').click() await (await camera).setFiles('src/assets/paper-capture-reference.png') await expect(page.locator('.student-dialog')).toHaveCount(0) const album = page.waitForEvent('filechooser') await page.getByText('从相册选择', { exact: true }).click() await (await album).setFiles('src/assets/sample-paper.png') await expect(page.locator('.student-dialog')).toHaveCount(0) const documents = page.waitForEvent('filechooser') await page.getByText('Word / PDF 文件', { exact: true }).click() await (await documents).setFiles({ name: '练习卷.pdf', mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4\n%%EOF') }) await expect(page.locator('.student-dialog')).toHaveCount(0) await page.getByText('识别全部照片', { exact: true }).click() await expect(page.locator('.student-dialog')).toBeVisible() await expect(page.getByText('请先关联学生', { exact: true })).toHaveCount(0) await expect(page.locator('.question-card')).toHaveCount(0) await page.screenshot({ path: `test-results/recognition-students-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.student-cancel-button').click() await expect(page.getByText('已添加 3 张 · 查看 ›')).toBeVisible() await chooseStudents(page) await page.getByText('识别全部照片', { exact: true }).click() await expect(page.getByText('3 页 · 已识别 9 道题', { exact: false })).toBeVisible() }) test('student dialog supports dropdowns, removable tags and cancelling draft selections', async ({ page }) => { await openEntry(page, '上传错题') await page.getByText('选择学生', { exact: true }).click() await page.locator('.student-confirm-button').click() await expect(page.locator('.student-dialog')).toBeVisible() await expect(page.locator('.student-class-trigger')).toHaveText('请选择班级▾') await page.locator('.student-names-trigger').click() await expect(page.locator('.student-class-option.selected')).toHaveCount(0) await expect(page.locator('.student-option')).toHaveCount(0) await page.locator('.student-class-option').filter({ hasText: '初一' }).click() await page.locator('.student-names-trigger').click() await page.locator('.student-option').filter({ hasText: '李一' }).click() await page.locator('.student-option').filter({ hasText: '李二' }).click() await page.locator('.student-names-trigger').click() await expect(page.locator('.student-names-trigger')).toHaveText('李一、李二▾') await expect(page.getByText('已选 2 名学生', { exact: true })).toBeVisible() await page.locator('.student-dialog').screenshot({ path: `test-results/student-dialog-${test.info().project.name}.png`, scale: 'css' }) await page.getByLabel('移除初一 李二', { exact: true }).click() await expect(page.locator('.student-selected-tag')).toHaveCount(1) await page.locator('.student-cancel-button').click() await page.getByText('选择学生', { exact: true }).click() await expect(page.locator('.student-selected-tag')).toHaveCount(0) await page.locator('.student-cancel-button').click() await chooseStudents(page) await page.getByText('选择学生', { exact: true }).click() await expect(page.locator('.student-selected-tag')).toHaveCount(2) await expect(page.locator('.student-class-trigger')).toHaveText('初一▾') await expect(page.locator('.student-names-trigger')).toHaveText('李一▾') await page.locator('.student-class-trigger').click() await page.locator('.student-class-option').filter({ hasText: '初二' }).click() await expect(page.locator('.student-names-trigger')).toHaveText('李一▾') await page.locator('.student-cancel-button').click() await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一') }) test('reopening student dialog restores the selected class and students', async ({ page }) => { await openEntry(page, '上传错题') await page.getByText('选择学生', { exact: true }).click() await expect(page.locator('.student-class-trigger')).toHaveText('请选择班级▾') await page.locator('.student-class-trigger').click() await page.locator('.student-class-option').filter({ hasText: '初二' }).click() await page.locator('.student-names-trigger').click() await page.locator('.student-option').filter({ hasText: '李一' }).click() await page.locator('.student-option').filter({ hasText: '李二' }).click() await page.locator('.student-confirm-button').click() await page.getByText('选择学生', { exact: true }).click() await expect(page.locator('.student-class-trigger')).toHaveText('初二▾') await expect(page.locator('.student-names-trigger')).toHaveText('李一、李二▾') await expect(page.locator('.student-selected-tag')).toHaveCount(2) await page.locator('.student-dialog').screenshot({ path: `test-results/student-restored-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.student-names-trigger').click() await expect(page.locator('.student-option.selected')).toHaveCount(2) }) test('mistakes retain edits across pages and retries, and persist linked students', async ({ page }) => { await openEntry(page, '上传错题') await chooseStudents(page) await example(page) await example(page) await page.getByText('已添加 2 张 · 查看 ›').click() await expect(page.locator('.gallery-row')).toHaveCount(2) await page.locator('.gallery-footer').getByText('完成', { exact: true }).click() await page.getByText('识别全部照片', { exact: true }).click() await expect(page.getByText('已识别 6 道题', { exact: false })).toBeVisible() await page.locator('.wrong-toggle').click() await page.locator('.question-card textarea').nth(0).fill('修改后的题干:求二次函数的交点。') await page.locator('.question-card .field-input input').first().fill('自定义选项 A') await page.getByText('下一题', { exact: true }).click() await page.getByText('上一题', { exact: true }).click() await expect(page.locator('.question-card textarea').nth(0)).toHaveValue('修改后的题干:求二次函数的交点。') await page.getByText('继续拍照', { exact: true }).click() await example(page) await page.getByText('识别全部照片', { exact: true }).click() await expect(page.locator('.question-card textarea').nth(0)).toHaveValue('修改后的题干:求二次函数的交点。') await page.getByText('体验上传失败与重试').click() await expect(page.getByText('本次模拟上传未完成,已填写和识别的内容已保留。')).toBeVisible() await page.getByText('返回重试', { exact: true }).click() await page.getByText('上传错题(1 道)').click() await expect(page.getByText('题目已存入题库')).toBeVisible() await expect(page.getByText('初一 · 李一、初二 · 李一')).toBeVisible() await page.getByText('查看上传记录', { exact: true }).click() await expect(page.getByText('关联学生:初一 李一、初二 李一')).toBeVisible() await page.reload() await expect(page.getByText('修改后的题干:求二次函数的交点。')).toBeVisible() await page.screenshot({ path: `test-results/mistakes-${test.info().project.name}.png`, fullPage: true }) }) test('mistake capture and photo guide match the reference layout and retain selected photos', async ({ page }) => { await openEntry(page, '上传错题') await expect(page.locator('.student-link-bar')).toBeVisible() expect((await page.locator('.student-link-copy').boundingBox())!.width).toBeGreaterThan(180) await expect(page.locator('.capture-import-card')).toHaveCount(2) await expect(page.getByText('识别全部照片', { exact: true })).toHaveCount(0) await page.locator('.mistake-capture-page').screenshot({ path: `test-results/capture-empty-${test.info().project.name}.png`, scale: 'css' }) await chooseStudents(page) const chooser = page.waitForEvent('filechooser') await page.getByText('从相册选择', { exact: true }).click() await (await chooser).setFiles('src/assets/sample-paper.png') await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible() await page.locator('.mistake-capture-page').screenshot({ path: `test-results/capture-${test.info().project.name}.png`, scale: 'css' }) await page.getByText('查看示例', { exact: true }).click() await expect(page.getByText('试卷拍摄规范', { exact: true })).toBeVisible() await expect(page.locator('.photo-guide-page .primary-button')).toHaveCount(0) await expect(page.locator('.photo-guide-page').getByText('添加示例照片', { exact: true })).toHaveCount(0) await expect(page.locator('.photo-guide-corner')).toHaveCount(4) await expect(page.locator('.photo-guide-rule')).toHaveCount(3) await expect(page.getByText('多页试卷怎么拍?', { exact: true })).toBeVisible() await expect.poll(() => page.locator('.photo-guide-reference img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBe(600) await expect.poll(() => active(page).evaluate((element) => element.scrollTop)).toBe(0) expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy() await page.locator('.photo-guide-page').screenshot({ path: `test-results/photo-guide-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.photo-guide-multipage').scrollIntoViewIfNeeded() await expect(page.locator('.photo-guide-multipage')).toBeInViewport() await page.screenshot({ path: `test-results/photo-guide-details-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.back-button').click() await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible() await page.getByText('已添加 1 张 · 查看 ›').click() await expect(page.locator('.gallery-row')).toHaveCount(1) }) test('added photos use a mixed file list with preview, deletion, camera and done actions', async ({ page }) => { await openEntry(page, '上传错题') await chooseStudents(page) for (let i = 0; i < 2; i++) { const chooser = page.waitForEvent('filechooser') await page.locator('.capture-zone').click() await (await chooser).setFiles('src/assets/paper-capture-reference.png') } const album = page.waitForEvent('filechooser') await page.getByText('从相册选择', { exact: true }).click() await (await album).setFiles({ name: '初一中考.png', mimeType: 'image/png', buffer: readFileSync('src/assets/sample-paper.png') }) const documents = page.waitForEvent('filechooser') await page.getByText('Word / PDF 文件', { exact: true }).click() await (await documents).setFiles({ name: '知了数学-阶段学案.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', buffer: Buffer.from('local demo document') }) await page.getByText('已添加 4 张 · 查看 ›').click() await expect(page.locator('.gallery-row')).toHaveCount(4) await expect(page.locator('.gallery-file-name').nth(0)).toHaveText('1. 拍摄照片 1') await expect(page.locator('.gallery-file-name').nth(2)).toHaveText('3. 初一中考.png') await expect(page.locator('.gallery-row.is-document .gallery-view')).toHaveCount(0) await page.screenshot({ path: `test-results/photo-gallery-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.gallery-view').first().click() await expect(page.locator('.preview-image img')).toHaveCount(3) await page.locator('.preview-image').click({ position: { x: 5, y: 5 } }) await page.locator('.gallery-row').nth(1).locator('.gallery-delete').click() await expect(page.locator('.gallery-file-name').nth(1)).toHaveText('2. 初一中考.png') const camera = page.waitForEvent('filechooser') await page.locator('.nav-action').getByText('继续拍照', { exact: true }).click() await (await camera).setFiles('src/assets/paper-capture-reference.png') await expect(page.locator('.gallery-row')).toHaveCount(4) await page.locator('.gallery-footer').getByText('完成', { exact: true }).click() await expect(page.getByText('已添加 4 张 · 查看 ›')).toBeVisible() }) test('answer cards support multiple photos, partial failure, retake and successful submission', async ({ page }) => { await openEntry(page, '上传答题卡') await expect(page.locator('.answer-capture-page .demo-banner')).toHaveCount(0) await expect(page.locator('.nav-action .text-button')).toHaveCount(0) await expect(page.locator('.answer-rule')).toHaveCount(3) await expect.poll(() => page.locator('.answer-reference img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBe(606) await page.locator('.answer-capture-page').screenshot({ path: `test-results/answer-capture-${test.info().project.name}.png`, scale: 'css' }) await page.locator('.answer-camera-button').scrollIntoViewIfNeeded() const albumButton = await page.locator('.answer-album-button').boundingBox() const cameraButton = await page.locator('.answer-camera-button').boundingBox() expect(Math.abs(albumButton!.y - cameraButton!.y)).toBeLessThan(1) await page.screenshot({ path: `test-results/answer-buttons-${test.info().project.name}.png`, scale: 'css' }) expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy() await example(page) await page.getByText('查看示例', { exact: true }).click() await expect(page.locator('.answer-guide-page .answer-reference')).toBeVisible() await page.locator('.back-button').click() await page.locator('.back-button').click() const camera = page.waitForEvent('filechooser') await page.locator('.answer-camera-button').click() await (await camera).setFiles('src/assets/sample-answer.png') await page.getByText('体验上传失败与重试').click() await expect(page.getByText('1 张图片识别失败')).toBeVisible() await page.getByText('重新拍摄失败照片').click() await page.getByText('用示例重新拍摄').click() await expect(page.getByText('确认上传(2 张)')).toBeVisible() await page.getByText('确认上传(2 张)').click() await expect(page.getByText('解析结果请到客户端的【批量解析】中查看')).toBeVisible() await page.getByText('查看上传记录').click() await expect(page.getByText('已上传 2 张答题卡')).toBeVisible() }) test('video playback, unread synchronization and library originals work offline', async ({ page }) => { const errors: string[] = [] page.on('pageerror', (e) => errors.push(e.message)) await openEntry(page, '题目讲解') await page.getByText('观看讲解 →', { exact: true }).first().click() const video = page.locator('video') await expect(video).toBeVisible() await video.evaluate(async (element) => { const player = element as HTMLVideoElement player.muted = true await player.play() }) await expect.poll(() => video.evaluate((element) => (element as HTMLVideoElement).currentTime)).toBeGreaterThan(0.5) await video.evaluate((element) => (element as HTMLVideoElement).pause()) await page.screenshot({ path: `test-results/explanation-${test.info().project.name}.png`, fullPage: true }) await page.locator('.back-button').click() await expect( page .locator('.panel-card') .filter({ has: page.getByText('函数图像平移', { exact: true }) }) .first() .getByText('● 未读') ).toHaveCount(0) await page.locator('.back-button').click() await page.goto('/#/pages/library/index') await page.getByText('数学练习卷(一)', { exact: true }).click() await page.getByText('查看原图', { exact: true }).click() await page.getByText('第 2 页', { exact: true }).click() await expect(page.locator('.original-image')).toBeVisible() await page.screenshot({ path: `test-results/originals-${test.info().project.name}.png`, fullPage: true }) expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy() expect(errors).toEqual([]) }) test('paper metadata, local image and PDF imports, review approval and originals persist', async ({ page }) => { test.setTimeout(60000) await openEntry(page, '上传试卷') const imageChooser = page.waitForEvent('filechooser') await page.getByText('从相册选择', { exact: true }).click() await (await imageChooser).setFiles('src/assets/sample-paper.png') await page.getByText('识别全部照片', { exact: true }).click() await expect(page.getByText('请输入试卷名称', { exact: true })).toBeVisible() await page.locator('input[placeholder="请输入试卷名称"]').fill('回归测试卷') for (const label of ['年份', '试卷类型', '省份']) { await page .locator('.select-field') .filter({ has: page.getByText(label, { exact: true }) }) .click() await page.locator('.weui-picker__action:visible').filter({ hasText: '确定' }).click() } const yearField = await page.locator('.paper-meta-field').filter({ hasText: '年份' }).boundingBox() const typeField = await page.locator('.paper-meta-field').filter({ hasText: '试卷类型' }).boundingBox() expect(Math.abs(yearField!.y - typeField!.y)).toBeLessThan(1) expect(typeField!.x).toBeGreaterThan(yearField!.x + yearField!.width) await expect(page.locator('.paper-capture-page .demo-banner')).toHaveCount(0) await expect(page.locator('.capture-zone').getByText('拍照', { exact: true })).toBeVisible() await page.getByText('查看示例', { exact: true }).click() await page.locator('.back-button').click() await expect(page.locator('input[placeholder="请输入试卷名称"]')).toHaveValue('回归测试卷') await page.getByText('已添加 1 张 · 查看 ›').click() await expect(page.locator('.gallery-row')).toHaveCount(1) await page.locator('.gallery-footer').getByText('完成', { exact: true }).click() await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible() await page.screenshot({ path: `test-results/paper-capture-${test.info().project.name}.png`, fullPage: true, scale: 'css' }) expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy() const documentChooser = page.waitForEvent('filechooser') await page.getByText('Word / PDF 文件', { exact: true }).click() await ( await documentChooser ).setFiles({ name: '练习卷.pdf', mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4\n%%EOF') }) await page.getByText('识别全部照片', { exact: true }).click() await expect(page.getByText('2 页 · 已识别 6 道题', { exact: false })).toBeVisible() await page.screenshot({ path: `test-results/paper-review-${test.info().project.name}.png`, fullPage: true }) await page.locator('.primary-button:visible').filter({ hasText: /^上传试卷$/ }).click() await expect(page.getByText('上传成功,待审核')).toBeVisible() await expect(page.getByText('试卷审核通过', { exact: true })).toBeVisible({ timeout: 25000 }) await page.getByText('查看上传记录', { exact: true }).click() await page.reload() await expect(page.getByText('审核通过', { exact: true })).toBeVisible() await page.getByText('查看原图', { exact: true }).click() await expect(page.locator('.original-image')).toBeVisible() await expect.poll(() => page.locator('.original-image img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBeGreaterThan(0) await page.getByText('第 2 页', { exact: true }).click() await expect(page.getByText('文档文件,暂不提供原图预览')).toBeVisible() })