import { expect, test, type Page } from '@playwright/test'; const identity = { student_no: '9000000001', name: '学生甲', topic: '机器人项目' }; const mixed = () => ({ relative_dir: 'test/student', student_no: '', name: '', topic: '', status: 'needs_confirmation', folder_identity: {}, detected_identity: {}, missing: [], warnings: ['不同材料封面的学号不一致,请核对后填写'], files: [ { relative_path: 'test/student/最终报告.pdf', category: 'final_report', status: 'recognized', sha256: 'a'.repeat(64), detected_identity: identity }, { relative_path: 'test/student/汇报PPT.pdf', category: 'presentation', status: 'needs_confirmation', sha256: 'b'.repeat(64), detected_identity: { student_no: '9000000002', name: '学生乙', topic: '另一项目' } }, ] }); async function setup(page: Page, project: any) { const imports: any[] = []; await page.route('**/api/**', async route => { const request = route.request(); const path = new URL(request.url()).pathname.replace(/^\/api/, ''); let data: any = []; if (path === '/courses') data = [{ id: 1, name: '课程甲' }]; if (path === '/courses/1/versions') data = [{ id: 1, published: true, version: 1, title: '初始版本' }]; if (path === '/import/scan') data = { projects: [project] }; if (path === '/import/confirm') { imports.push(request.postDataJSON()); data = { project_ids: [12] }; } await route.fulfill({ json: data }); }); await page.goto('/?page=import'); await page.getByRole('button', { name: '扫描此目录', exact: true }).click(); await choose(page, '导入课程', '课程甲'); await choose(page, '导入课程版本', /初始版本/); return imports; } async function choose(page: Page, label: string, option: string | RegExp) { await page.keyboard.press('Escape'); await expect(page.locator('.ant-select-dropdown:visible')).toHaveCount(0); const input = page.getByRole('combobox', { name: label, exact: true }); await input.focus(); await input.press('ArrowDown'); const listId = await input.getAttribute('aria-controls'); const dropdown = page.locator('.ant-select-dropdown').filter({ has: page.locator(`[id="${listId}"]`) }); await expect(dropdown).toBeVisible(); await dropdown.getByText(option, { exact: typeof option === 'string' }).click(); } test('invalid student number explains why confirm is disabled and recovers after editing', async ({ page }) => { const project = { ...mixed(), student_no: '1', name: '1', topic: '1' }; delete (project as any).folder_identity; for (const file of project.files) delete (file as any).detected_identity; await setup(page, project); await expect(page.getByText(/学号必须为5–20位数字/)).toBeVisible(); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeDisabled(); await page.getByRole('textbox', { name: '学号 test/student', exact: true }).fill('9000000001'); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeEnabled(); }); test('ignoring a foreign PPT recalculates identity and submits the intended project', async ({ page }) => { const imports = await setup(page, mixed()); await expect(page.getByText('学生乙(9000000002)', { exact: true })).toBeVisible(); await expect(page.getByText(/学号冲突:/)).toBeVisible(); await choose(page, '处理 test/student/汇报PPT.pdf', '忽略'); await expect(page.getByRole('textbox', { name: '学号 test/student', exact: true })).toHaveValue(identity.student_no); await expect(page.getByRole('textbox', { name: '姓名 test/student', exact: true })).toHaveValue(identity.name); await expect(page.getByText(/学号冲突:/)).toHaveCount(0); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeEnabled(); await page.getByRole('button', { name: '确认导入', exact: true }).click(); await expect.poll(() => imports.length).toBe(1); expect(imports[0].projects[0].student_no).toBe(identity.student_no); expect(imports[0].projects[0].files[1].status).toBe('ignored'); }); test('file selection preserves manual corrections until the user chooses cover values', async ({ page }) => { await setup(page, mixed()); const name = page.getByRole('textbox', { name: '姓名 test/student', exact: true }); await name.fill('人工修正姓名'); await choose(page, '处理 test/student/汇报PPT.pdf', '忽略'); await expect(name).toHaveValue('人工修正姓名'); await page.getByRole('button', { name: '使用所选资料的封面信息', exact: true }).click(); await expect(name).toHaveValue(identity.name); }); test('ignoring all files gives an explicit selected-material error', async ({ page }) => { await setup(page, mixed()); await choose(page, '处理 test/student/汇报PPT.pdf', '忽略'); await choose(page, '处理 test/student/最终报告.pdf', '忽略'); await expect(page.getByText(/至少选择1份资料/)).toBeVisible(); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeDisabled(); }); test('unknown and duplicate material types explain the block before sending a request', async ({ page }) => { const project = mixed(); project.files[1].category = 'unknown'; project.files[1].detected_identity = identity; await setup(page, project); await expect(page.getByText(/请选择材料类型/)).toBeVisible(); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeDisabled(); await choose(page, '材料类型 test/student/汇报PPT.pdf', '最终报告'); await expect(page.getByText(/最终报告选了2份/)).toBeVisible(); await choose(page, '处理 test/student/汇报PPT.pdf', '忽略'); await expect(page.getByRole('button', { name: '确认导入', exact: true })).toBeEnabled(); });