flows.spec.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import { expect, test, type Page } from '@playwright/test'
  2. import { readFileSync } from 'node:fs'
  3. const active = (page: Page) => page.locator('.taro_page').last()
  4. async function openEntry(page: Page, title: string) {
  5. await page.goto('/')
  6. await page.locator('.workbench-card').filter({ hasText: title }).click()
  7. }
  8. async function example(page: Page) {
  9. if (await page.locator('.mistake-capture-page, .paper-capture-page').count()) {
  10. const chooser = page.waitForEvent('filechooser')
  11. await page.getByText('从相册选择', { exact: true }).click()
  12. await (await chooser).setFiles('src/assets/sample-paper.png')
  13. return
  14. }
  15. if (await page.locator('.answer-capture-page').count()) {
  16. const chooser = page.waitForEvent('filechooser')
  17. await page.locator('.answer-album-button').click()
  18. await (await chooser).setFiles('src/assets/sample-answer.png')
  19. return
  20. }
  21. await page.getByText('添加示例照片', { exact: true }).click()
  22. }
  23. async function chooseStudents(page: Page) {
  24. await active(page).getByText('选择学生', { exact: true }).click()
  25. await page.locator('.student-class-trigger').click()
  26. await page.locator('.student-class-option').filter({ hasText: '初一' }).click()
  27. await page.locator('.student-names-trigger').click()
  28. await page.locator('.student-option').filter({ hasText: '李一' }).click()
  29. await page.locator('.student-class-trigger').click()
  30. await page.locator('.student-class-option').filter({ hasText: '初二' }).click()
  31. await page.locator('.student-names-trigger').click()
  32. await page.locator('.student-option').filter({ hasText: '李一' }).click()
  33. await page.locator('.student-confirm-button').click()
  34. }
  35. test('student associations belong to one upload and clear when reopening the page', async ({ page }) => {
  36. await openEntry(page, '上传错题')
  37. await chooseStudents(page)
  38. await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一')
  39. await page.getByText('查看示例', { exact: true }).click()
  40. await page.locator('.back-button').click()
  41. await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一')
  42. await page.locator('.back-button').click()
  43. await page.locator('.workbench-card').filter({ hasText: '上传错题' }).click()
  44. await expect(page.getByText('识别前请选择学生,支持跨班级多选', { exact: true })).toBeVisible()
  45. await active(page).getByText('选择学生', { exact: true }).click()
  46. await expect(page.locator('.student-selected-tag')).toHaveCount(0)
  47. await page.locator('.student-cancel-button').click()
  48. await chooseStudents(page)
  49. await page.reload()
  50. await expect(page.getByText('识别前请选择学生,支持跨班级多选', { exact: true })).toBeVisible()
  51. await page.screenshot({ path: `test-results/fresh-upload-${test.info().project.name}.png`, scale: 'css' })
  52. })
  53. test('photos and documents can be added before students are required at recognition', async ({ page }) => {
  54. await openEntry(page, '上传错题')
  55. const camera = page.waitForEvent('filechooser')
  56. await page.locator('.capture-zone').click()
  57. await (await camera).setFiles('src/assets/paper-capture-reference.png')
  58. await expect(page.locator('.student-dialog')).toHaveCount(0)
  59. const album = page.waitForEvent('filechooser')
  60. await page.getByText('从相册选择', { exact: true }).click()
  61. await (await album).setFiles('src/assets/sample-paper.png')
  62. await expect(page.locator('.student-dialog')).toHaveCount(0)
  63. const documents = page.waitForEvent('filechooser')
  64. await page.getByText('Word / PDF 文件', { exact: true }).click()
  65. await (await documents).setFiles({ name: '练习卷.pdf', mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4\n%%EOF') })
  66. await expect(page.locator('.student-dialog')).toHaveCount(0)
  67. await page.getByText('识别全部照片', { exact: true }).click()
  68. await expect(page.locator('.student-dialog')).toBeVisible()
  69. await expect(page.getByText('请先关联学生', { exact: true })).toHaveCount(0)
  70. await expect(page.locator('.question-card')).toHaveCount(0)
  71. await page.screenshot({ path: `test-results/recognition-students-${test.info().project.name}.png`, scale: 'css' })
  72. await page.locator('.student-cancel-button').click()
  73. await expect(page.getByText('已添加 3 张 · 查看 ›')).toBeVisible()
  74. await chooseStudents(page)
  75. await page.getByText('识别全部照片', { exact: true }).click()
  76. await expect(page.getByText('3 页 · 已识别 9 道题', { exact: false })).toBeVisible()
  77. })
  78. test('student dialog supports dropdowns, removable tags and cancelling draft selections', async ({ page }) => {
  79. await openEntry(page, '上传错题')
  80. await page.getByText('选择学生', { exact: true }).click()
  81. await page.locator('.student-confirm-button').click()
  82. await expect(page.locator('.student-dialog')).toBeVisible()
  83. await expect(page.locator('.student-class-trigger')).toHaveText('请选择班级▾')
  84. await page.locator('.student-names-trigger').click()
  85. await expect(page.locator('.student-class-option.selected')).toHaveCount(0)
  86. await expect(page.locator('.student-option')).toHaveCount(0)
  87. await page.locator('.student-class-option').filter({ hasText: '初一' }).click()
  88. await page.locator('.student-names-trigger').click()
  89. await page.locator('.student-option').filter({ hasText: '李一' }).click()
  90. await page.locator('.student-option').filter({ hasText: '李二' }).click()
  91. await page.locator('.student-names-trigger').click()
  92. await expect(page.locator('.student-names-trigger')).toHaveText('李一、李二▾')
  93. await expect(page.getByText('已选 2 名学生', { exact: true })).toBeVisible()
  94. await page.locator('.student-dialog').screenshot({ path: `test-results/student-dialog-${test.info().project.name}.png`, scale: 'css' })
  95. await page.getByLabel('移除初一 李二', { exact: true }).click()
  96. await expect(page.locator('.student-selected-tag')).toHaveCount(1)
  97. await page.locator('.student-cancel-button').click()
  98. await page.getByText('选择学生', { exact: true }).click()
  99. await expect(page.locator('.student-selected-tag')).toHaveCount(0)
  100. await page.locator('.student-cancel-button').click()
  101. await chooseStudents(page)
  102. await page.getByText('选择学生', { exact: true }).click()
  103. await expect(page.locator('.student-selected-tag')).toHaveCount(2)
  104. await expect(page.locator('.student-class-trigger')).toHaveText('初一▾')
  105. await expect(page.locator('.student-names-trigger')).toHaveText('李一▾')
  106. await page.locator('.student-class-trigger').click()
  107. await page.locator('.student-class-option').filter({ hasText: '初二' }).click()
  108. await expect(page.locator('.student-names-trigger')).toHaveText('李一▾')
  109. await page.locator('.student-cancel-button').click()
  110. await expect(page.locator('.student-link-copy')).toContainText('初一 · 李一、初二 · 李一')
  111. })
  112. test('reopening student dialog restores the selected class and students', async ({ page }) => {
  113. await openEntry(page, '上传错题')
  114. await page.getByText('选择学生', { exact: true }).click()
  115. await expect(page.locator('.student-class-trigger')).toHaveText('请选择班级▾')
  116. await page.locator('.student-class-trigger').click()
  117. await page.locator('.student-class-option').filter({ hasText: '初二' }).click()
  118. await page.locator('.student-names-trigger').click()
  119. await page.locator('.student-option').filter({ hasText: '李一' }).click()
  120. await page.locator('.student-option').filter({ hasText: '李二' }).click()
  121. await page.locator('.student-confirm-button').click()
  122. await page.getByText('选择学生', { exact: true }).click()
  123. await expect(page.locator('.student-class-trigger')).toHaveText('初二▾')
  124. await expect(page.locator('.student-names-trigger')).toHaveText('李一、李二▾')
  125. await expect(page.locator('.student-selected-tag')).toHaveCount(2)
  126. await page.locator('.student-dialog').screenshot({ path: `test-results/student-restored-${test.info().project.name}.png`, scale: 'css' })
  127. await page.locator('.student-names-trigger').click()
  128. await expect(page.locator('.student-option.selected')).toHaveCount(2)
  129. })
  130. test('mistakes retain edits across pages and retries, and persist linked students', async ({ page }) => {
  131. await openEntry(page, '上传错题')
  132. await chooseStudents(page)
  133. await example(page)
  134. await example(page)
  135. await page.getByText('已添加 2 张 · 查看 ›').click()
  136. await expect(page.locator('.gallery-row')).toHaveCount(2)
  137. await page.locator('.gallery-footer').getByText('完成', { exact: true }).click()
  138. await page.getByText('识别全部照片', { exact: true }).click()
  139. await expect(page.getByText('已识别 6 道题', { exact: false })).toBeVisible()
  140. await page.locator('.wrong-toggle').click()
  141. await page.locator('.question-card textarea').nth(0).fill('修改后的题干:求二次函数的交点。')
  142. await page.locator('.question-card .field-input input').first().fill('自定义选项 A')
  143. await page.getByText('下一题', { exact: true }).click()
  144. await page.getByText('上一题', { exact: true }).click()
  145. await expect(page.locator('.question-card textarea').nth(0)).toHaveValue('修改后的题干:求二次函数的交点。')
  146. await page.getByText('继续拍照', { exact: true }).click()
  147. await example(page)
  148. await page.getByText('识别全部照片', { exact: true }).click()
  149. await expect(page.locator('.question-card textarea').nth(0)).toHaveValue('修改后的题干:求二次函数的交点。')
  150. await page.getByText('体验上传失败与重试').click()
  151. await expect(page.getByText('本次模拟上传未完成,已填写和识别的内容已保留。')).toBeVisible()
  152. await page.getByText('返回重试', { exact: true }).click()
  153. await page.getByText('上传错题(1 道)').click()
  154. await expect(page.getByText('题目已存入题库')).toBeVisible()
  155. await expect(page.getByText('初一 · 李一、初二 · 李一')).toBeVisible()
  156. await page.getByText('查看上传记录', { exact: true }).click()
  157. await expect(page.getByText('关联学生:初一 李一、初二 李一')).toBeVisible()
  158. await page.reload()
  159. await expect(page.getByText('修改后的题干:求二次函数的交点。')).toBeVisible()
  160. await page.screenshot({ path: `test-results/mistakes-${test.info().project.name}.png`, fullPage: true })
  161. })
  162. test('mistake capture and photo guide match the reference layout and retain selected photos', async ({ page }) => {
  163. await openEntry(page, '上传错题')
  164. await expect(page.locator('.student-link-bar')).toBeVisible()
  165. expect((await page.locator('.student-link-copy').boundingBox())!.width).toBeGreaterThan(180)
  166. await expect(page.locator('.capture-import-card')).toHaveCount(2)
  167. await expect(page.getByText('识别全部照片', { exact: true })).toHaveCount(0)
  168. await page.locator('.mistake-capture-page').screenshot({ path: `test-results/capture-empty-${test.info().project.name}.png`, scale: 'css' })
  169. await chooseStudents(page)
  170. const chooser = page.waitForEvent('filechooser')
  171. await page.getByText('从相册选择', { exact: true }).click()
  172. await (await chooser).setFiles('src/assets/sample-paper.png')
  173. await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible()
  174. await page.locator('.mistake-capture-page').screenshot({ path: `test-results/capture-${test.info().project.name}.png`, scale: 'css' })
  175. await page.getByText('查看示例', { exact: true }).click()
  176. await expect(page.getByText('试卷拍摄规范', { exact: true })).toBeVisible()
  177. await expect(page.locator('.photo-guide-page .primary-button')).toHaveCount(0)
  178. await expect(page.locator('.photo-guide-page').getByText('添加示例照片', { exact: true })).toHaveCount(0)
  179. await expect(page.locator('.photo-guide-corner')).toHaveCount(4)
  180. await expect(page.locator('.photo-guide-rule')).toHaveCount(3)
  181. await expect(page.getByText('多页试卷怎么拍?', { exact: true })).toBeVisible()
  182. await expect.poll(() => page.locator('.photo-guide-reference img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBe(600)
  183. await expect.poll(() => active(page).evaluate((element) => element.scrollTop)).toBe(0)
  184. expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy()
  185. await page.locator('.photo-guide-page').screenshot({ path: `test-results/photo-guide-${test.info().project.name}.png`, scale: 'css' })
  186. await page.locator('.photo-guide-multipage').scrollIntoViewIfNeeded()
  187. await expect(page.locator('.photo-guide-multipage')).toBeInViewport()
  188. await page.screenshot({ path: `test-results/photo-guide-details-${test.info().project.name}.png`, scale: 'css' })
  189. await page.locator('.back-button').click()
  190. await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible()
  191. await page.getByText('已添加 1 张 · 查看 ›').click()
  192. await expect(page.locator('.gallery-row')).toHaveCount(1)
  193. })
  194. test('added photos use a mixed file list with preview, deletion, camera and done actions', async ({ page }) => {
  195. await openEntry(page, '上传错题')
  196. await chooseStudents(page)
  197. for (let i = 0; i < 2; i++) {
  198. const chooser = page.waitForEvent('filechooser')
  199. await page.locator('.capture-zone').click()
  200. await (await chooser).setFiles('src/assets/paper-capture-reference.png')
  201. }
  202. const album = page.waitForEvent('filechooser')
  203. await page.getByText('从相册选择', { exact: true }).click()
  204. await (await album).setFiles({ name: '初一中考.png', mimeType: 'image/png', buffer: readFileSync('src/assets/sample-paper.png') })
  205. const documents = page.waitForEvent('filechooser')
  206. await page.getByText('Word / PDF 文件', { exact: true }).click()
  207. await (await documents).setFiles({ name: '知了数学-阶段学案.docx', mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', buffer: Buffer.from('local demo document') })
  208. await page.getByText('已添加 4 张 · 查看 ›').click()
  209. await expect(page.locator('.gallery-row')).toHaveCount(4)
  210. await expect(page.locator('.gallery-file-name').nth(0)).toHaveText('1. 拍摄照片 1')
  211. await expect(page.locator('.gallery-file-name').nth(2)).toHaveText('3. 初一中考.png')
  212. await expect(page.locator('.gallery-row.is-document .gallery-view')).toHaveCount(0)
  213. await page.screenshot({ path: `test-results/photo-gallery-${test.info().project.name}.png`, scale: 'css' })
  214. await page.locator('.gallery-view').first().click()
  215. await expect(page.locator('.preview-image img')).toHaveCount(3)
  216. await page.locator('.preview-image').click({ position: { x: 5, y: 5 } })
  217. await page.locator('.gallery-row').nth(1).locator('.gallery-delete').click()
  218. await expect(page.locator('.gallery-file-name').nth(1)).toHaveText('2. 初一中考.png')
  219. const camera = page.waitForEvent('filechooser')
  220. await page.locator('.nav-action').getByText('继续拍照', { exact: true }).click()
  221. await (await camera).setFiles('src/assets/paper-capture-reference.png')
  222. await expect(page.locator('.gallery-row')).toHaveCount(4)
  223. await page.locator('.gallery-footer').getByText('完成', { exact: true }).click()
  224. await expect(page.getByText('已添加 4 张 · 查看 ›')).toBeVisible()
  225. })
  226. test('answer cards support multiple photos, partial failure, retake and successful submission', async ({ page }) => {
  227. await openEntry(page, '上传答题卡')
  228. await expect(page.locator('.answer-capture-page .demo-banner')).toHaveCount(0)
  229. await expect(page.locator('.nav-action .text-button')).toHaveCount(0)
  230. await expect(page.locator('.answer-rule')).toHaveCount(3)
  231. await expect.poll(() => page.locator('.answer-reference img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBe(606)
  232. await page.locator('.answer-capture-page').screenshot({ path: `test-results/answer-capture-${test.info().project.name}.png`, scale: 'css' })
  233. await page.locator('.answer-camera-button').scrollIntoViewIfNeeded()
  234. const albumButton = await page.locator('.answer-album-button').boundingBox()
  235. const cameraButton = await page.locator('.answer-camera-button').boundingBox()
  236. expect(Math.abs(albumButton!.y - cameraButton!.y)).toBeLessThan(1)
  237. await page.screenshot({ path: `test-results/answer-buttons-${test.info().project.name}.png`, scale: 'css' })
  238. expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy()
  239. await example(page)
  240. await page.getByText('查看示例', { exact: true }).click()
  241. await expect(page.locator('.answer-guide-page .answer-reference')).toBeVisible()
  242. await page.locator('.back-button').click()
  243. await page.locator('.back-button').click()
  244. const camera = page.waitForEvent('filechooser')
  245. await page.locator('.answer-camera-button').click()
  246. await (await camera).setFiles('src/assets/sample-answer.png')
  247. await page.getByText('体验上传失败与重试').click()
  248. await expect(page.getByText('1 张图片识别失败')).toBeVisible()
  249. await page.getByText('重新拍摄失败照片').click()
  250. await page.getByText('用示例重新拍摄').click()
  251. await expect(page.getByText('确认上传(2 张)')).toBeVisible()
  252. await page.getByText('确认上传(2 张)').click()
  253. await expect(page.getByText('解析结果请到客户端的【批量解析】中查看')).toBeVisible()
  254. await page.getByText('查看上传记录').click()
  255. await expect(page.getByText('已上传 2 张答题卡')).toBeVisible()
  256. })
  257. test('video playback, unread synchronization and library originals work offline', async ({ page }) => {
  258. const errors: string[] = []
  259. page.on('pageerror', (e) => errors.push(e.message))
  260. await openEntry(page, '题目讲解')
  261. await page.getByText('观看讲解 →', { exact: true }).first().click()
  262. const video = page.locator('video')
  263. await expect(video).toBeVisible()
  264. await video.evaluate(async (element) => {
  265. const player = element as HTMLVideoElement
  266. player.muted = true
  267. await player.play()
  268. })
  269. await expect.poll(() => video.evaluate((element) => (element as HTMLVideoElement).currentTime)).toBeGreaterThan(0.5)
  270. await video.evaluate((element) => (element as HTMLVideoElement).pause())
  271. await page.screenshot({ path: `test-results/explanation-${test.info().project.name}.png`, fullPage: true })
  272. await page.locator('.back-button').click()
  273. await expect(
  274. page
  275. .locator('.panel-card')
  276. .filter({ has: page.getByText('函数图像平移', { exact: true }) })
  277. .first()
  278. .getByText('● 未读')
  279. ).toHaveCount(0)
  280. await page.locator('.back-button').click()
  281. await page.goto('/#/pages/library/index')
  282. await page.getByText('数学练习卷(一)', { exact: true }).click()
  283. await page.getByText('查看原图', { exact: true }).click()
  284. await page.getByText('第 2 页', { exact: true }).click()
  285. await expect(page.locator('.original-image')).toBeVisible()
  286. await page.screenshot({ path: `test-results/originals-${test.info().project.name}.png`, fullPage: true })
  287. expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy()
  288. expect(errors).toEqual([])
  289. })
  290. test('paper metadata, local image and PDF imports, review approval and originals persist', async ({ page }) => {
  291. test.setTimeout(60000)
  292. await openEntry(page, '上传试卷')
  293. const imageChooser = page.waitForEvent('filechooser')
  294. await page.getByText('从相册选择', { exact: true }).click()
  295. await (await imageChooser).setFiles('src/assets/sample-paper.png')
  296. await page.getByText('识别全部照片', { exact: true }).click()
  297. await expect(page.getByText('请输入试卷名称', { exact: true })).toBeVisible()
  298. await page.locator('input[placeholder="请输入试卷名称"]').fill('回归测试卷')
  299. for (const label of ['年份', '试卷类型', '省份']) {
  300. await page
  301. .locator('.select-field')
  302. .filter({ has: page.getByText(label, { exact: true }) })
  303. .click()
  304. await page.locator('.weui-picker__action:visible').filter({ hasText: '确定' }).click()
  305. }
  306. const yearField = await page.locator('.paper-meta-field').filter({ hasText: '年份' }).boundingBox()
  307. const typeField = await page.locator('.paper-meta-field').filter({ hasText: '试卷类型' }).boundingBox()
  308. expect(Math.abs(yearField!.y - typeField!.y)).toBeLessThan(1)
  309. expect(typeField!.x).toBeGreaterThan(yearField!.x + yearField!.width)
  310. await expect(page.locator('.paper-capture-page .demo-banner')).toHaveCount(0)
  311. await expect(page.locator('.capture-zone').getByText('拍照', { exact: true })).toBeVisible()
  312. await page.getByText('查看示例', { exact: true }).click()
  313. await page.locator('.back-button').click()
  314. await expect(page.locator('input[placeholder="请输入试卷名称"]')).toHaveValue('回归测试卷')
  315. await page.getByText('已添加 1 张 · 查看 ›').click()
  316. await expect(page.locator('.gallery-row')).toHaveCount(1)
  317. await page.locator('.gallery-footer').getByText('完成', { exact: true }).click()
  318. await expect(page.getByText('已添加 1 张 · 查看 ›')).toBeVisible()
  319. await page.screenshot({ path: `test-results/paper-capture-${test.info().project.name}.png`, fullPage: true, scale: 'css' })
  320. expect(await active(page).evaluate((element) => element.scrollWidth <= window.innerWidth)).toBeTruthy()
  321. const documentChooser = page.waitForEvent('filechooser')
  322. await page.getByText('Word / PDF 文件', { exact: true }).click()
  323. await (
  324. await documentChooser
  325. ).setFiles({ name: '练习卷.pdf', mimeType: 'application/pdf', buffer: Buffer.from('%PDF-1.4\n%%EOF') })
  326. await page.getByText('识别全部照片', { exact: true }).click()
  327. await expect(page.getByText('2 页 · 已识别 6 道题', { exact: false })).toBeVisible()
  328. await page.screenshot({ path: `test-results/paper-review-${test.info().project.name}.png`, fullPage: true })
  329. await page.locator('.primary-button:visible').filter({ hasText: /^上传试卷$/ }).click()
  330. await expect(page.getByText('上传成功,待审核')).toBeVisible()
  331. await expect(page.getByText('试卷审核通过', { exact: true })).toBeVisible({ timeout: 25000 })
  332. await page.getByText('查看上传记录', { exact: true }).click()
  333. await page.reload()
  334. await expect(page.getByText('审核通过', { exact: true })).toBeVisible()
  335. await page.getByText('查看原图', { exact: true }).click()
  336. await expect(page.locator('.original-image')).toBeVisible()
  337. await expect.poll(() => page.locator('.original-image img').evaluate((img) => (img as HTMLImageElement).naturalWidth)).toBeGreaterThan(0)
  338. await page.getByText('第 2 页', { exact: true }).click()
  339. await expect(page.getByText('文档文件,暂不提供原图预览')).toBeVisible()
  340. })