fix: explain import blockers and recompute selected identities

This commit is contained in:
2026-10-14 14:57:02 +08:00
parent 0d6dc4b342
commit 1776b1b50c
3 changed files with 233 additions and 10 deletions
+98
View File
@@ -0,0 +1,98 @@
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();
});