|
- import { chromium, expect } from '@playwright/test';
- import crypto from 'node:crypto';
- import fs from 'node:fs';
- import assert from 'node:assert/strict';
- import { login, call } from './role-permissions-live.mjs';
- const dir = 'reports/bridge-scripted-tasks-20260913';
- const admin = await login('root'), checks = [], fixtures = [], errors = [];
- const adminApi = async (url, data, method) => { const r = await call(url, admin.accessToken, data, method); assert.equal(r.status, 200, `${url}: ${r.message}`); return r.data; };
- const roles = await adminApi('auth/v1/roles/all'), role = (roles.records || roles).find(item => item.code === 'student');
- const suffix = Date.now().toString(36), password = 'Br!' + crypto.randomBytes(8).toString('hex');
- const created = await adminApi('auth/v1/users', {username: 'bridge_' + suffix, password, displayName: '桥梁脚本闭环验收', departmentId: '5', roleIds: [role.id], defaultRoleId: role.id, enabled: true, remark: '临时验收账号'});
- const userId = (created.user || created).id;
- const browser = await chromium.launch({channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader']});
- let page;
- try {
- const signed = await call('auth/v1/auth/login', null, {userId, password, roleCode: 'student', rememberMe: false});
- assert.equal(signed.status, 200);
- const api = async (url, data) => { const r = await call(`tran/v1/${url}`, signed.data.accessToken, data); assert.equal(r.status, 200, `${url}: ${r.message}`); return r.data; };
- const registered = JSON.parse(fs.readFileSync(`${dir}/registered.json`, 'utf8'));
- const context = await browser.newContext({viewport: {width: 1600, height: 1000}});
- await context.addInitScript(s => {
- sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken);
- sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken);
- localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({themeMode: 'military'}));
- }, signed.data);
- page = await context.newPage(); page.on('pageerror', error => errors.push(error.message));
- page.on('response', async response => {
- if (response.status() >= 400 && response.url().includes('/api/tran/')) {
- const body = await response.json().catch(() => ({}));
- console.log(JSON.stringify({path: new URL(response.url()).pathname, status: response.status(), message: body.message}));
- }
- });
- await page.goto('http://127.0.0.1:6180/teaching/virtual-training');
- await page.getByPlaceholder('请输入任务名称或编码').fill('TASK-VIRTUAL-00');
- await page.getByPlaceholder('请输入任务名称或编码').press('Enter');
- for (const item of registered) {
- const card = page.getByTestId(`teaching-assignment-${item.code}`);
- await expect(card).toBeVisible({timeout:30000});
- await expect(card.getByRole('button',{name:'领取任务',exact:true})).toBeEnabled();
- }
- await expect(page.getByText('静态流程 · 不计成绩',{exact:true})).toHaveCount(0);
- await page.screenshot({path:`${dir}/normal-task-list.png`});
- checks.push('原四条任务保留正常领取入口,不显示静态演示或禁评分标签');
- // Test copies keep the four delivery tasks free of test runs and test grades.
- for (const item of [...registered.slice(1), registered[0]].filter(item => !process.env.BRIDGE_CODE || item.code === process.env.BRIDGE_CODE)) {
- const original = await api(`teaching/assignments/${item.id}`);
- assert.equal(original.executionMode, 'PRACTICE'); assert.equal(original.definitionSnapshot.runtime.definitionSource, 'SCRIPT');
- let task = await adminApi('tran/v1/teaching/assignments', {code: `CHECK-${suffix}-${item.code}`, name: `验收·${item.name}`,
- description: '脚本训练闭环自动化验证,完成后归档', channel: 'VIRTUAL', assignmentKind: 'TRAINING', executionMode: 'PRACTICE',
- audienceType: 'COMMON', collaborationMode: 'INDIVIDUAL', digitalHumanAllowed: false, contentProjectId: item.projectId, contentVersionId: item.versionId, members: []});
- const fixture = {taskId: task.id, code: item.code}; fixtures.push(fixture);
- task = await adminApi(`tran/v1/teaching/assignments/${task.id}/publish`, {version: task.version});
- let run = await api(`teaching/assignments/${task.id}/accept`, {assignmentVersion: task.version, teamCode: 'NEUTRAL', positionCode: 'LEARNER'});
- fixture.runId = run.id;
- await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/runs/${run.id}`);
- const procedure = item.presentation === 'procedure';
- if (procedure) await expect(page.locator('.bridge-procedure h2')).toContainText(item.name);
- else await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout: 120000});
- run = await api(`teaching/runs/${run.id}`); assert.equal(run.status, 'IN_PROGRESS');
- for (let i = 0; i < item.steps; i++) {
- if (procedure) {
- await expect(page.locator('.bridge-procedure-current > small')).toHaveText(`第 ${i + 1} / ${item.steps} 步`);
- if (item.code === 'TASK-VIRTUAL-008' && [10,14].includes(i)) {
- await page.getByRole('button', {name: '完成本步并保存', exact: true}).click();
- await expect(page.locator('.bridge-procedure-progress')).toContainText(`已保存 ${i} /`);
- await page.getByRole('textbox', {name: '操作记录'}).fill('验收记录:原稿相反指令已标记,连接状态与处理依据交教员核对。');
- }
- // A rejected save must leave both UI and server at the same step.
- if (i === 0 && item.code === 'TASK-VIRTUAL-006') {
- await page.route(`**/teaching/runs/${run.id}/progress`, route => route.fulfill({status: 503, contentType:'application/json', body:JSON.stringify({code:'SERVICE_UNAVAILABLE',message:'验收模拟保存失败'})}), {times:1});
- await page.getByRole('button', {name:'完成本步并保存', exact:true}).click();
- await expect(page.locator('.bridge-procedure-progress')).toContainText('已保存 0 /');
- assert.equal((await api(`teaching/runs/${run.id}`)).progressPercent, 0);
- checks.push('步骤保存失败不前进,允许幂等重试');
- }
- await page.getByRole('button', {name:'完成本步并保存', exact:true}).click();
- await expect(page.getByRole('button', {name:'本步已保存', exact:true})).toBeDisabled();
- } else {
- await expect(page.locator('[data-role="step-count"]')).toHaveText(`${i + 1} / 5`);
- await page.locator('[data-role="part-label"]').click();
- if (i === 4) {
- await expect(page.locator('[data-role="part-label"]')).toContainText('互救开关');
- await page.locator('[data-role="part-label"]').click();
- }
- await expect(page.locator('[data-action="next"]')).toBeEnabled();
- }
- const saved = await api(`teaching/runs/${run.id}`);
- assert.ok(saved.progressPercent > (i / item.steps) * 100 - 1);
- const steps = await api(`teaching/runs/${run.id}/steps`);
- assert.equal(steps.filter(s => s.statusCode === 'COMPLETED').length, i + 1);
- if (i === 0) {
- await page.reload();
- if (procedure) await expect(page.locator('.bridge-procedure-progress')).toContainText('已保存 1 /');
- else {
- await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout:120000});
- await expect(page.locator('[data-role="step-count"]')).toHaveText('2 / 5');
- const bar = page.locator('[data-role="model-commandbar"]');
- for (const action of ['toggle-structure-tree','focus-selected-part','viewer-reset-view','toggle-part-labels','toggle-issue-highlight','toggle-hide-others','toggle-explosion']) await expect(bar.locator(`[data-action="${action}"]`)).toBeVisible();
- }
- checks.push(`${item.code} 刷新从服务端恢复已完成步骤`);
- } else if (i < item.steps - 1) {
- await (procedure ? page.getByRole('button', {name:'下一步', exact:true}) : page.locator('[data-action="next"]')).click();
- }
- }
- await (procedure ? page.getByRole('button', {name:'提交训练', exact:true}) : page.locator('[data-action="next"]')).click();
- await expect.poll(async () => (await api(`teaching/runs/${run.id}`)).status).toBe('SUBMITTED');
- run = await api(`teaching/runs/${run.id}`); assert.equal(run.score, null);
- await page.screenshot({path:`${dir}/normal-${item.code}-submitted.png`});
- run = await adminApi(`tran/v1/teaching/runs/${run.id}/review`, {version:run.version,commandId:crypto.randomUUID(),score:85,passed:true,feedback:'自动化验收样例评分,验证教员评定链路',rubric:{testFixture:true}});
- assert.equal(run.status,'REVIEWED'); assert.equal(run.score,85);
- const visible = await api(`teaching/runs/${run.id}`); assert.equal(visible.score,85);
- await page.reload();
- await expect(page.locator('.virtual-server-status')).toContainText('教员评定:85 分');
- if (!procedure) await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, {timeout:120000});
- await page.screenshot({path:`${dir}/normal-${item.code}-reviewed.png`});
- const analytics = await adminApi(`tran/v1/teaching/analytics/records?userId=${userId}&channel=VIRTUAL&assignmentKind=TRAINING&size=100`);
- assert.ok(analytics.records.some(r => String(r.runId || r.id) === run.id));
- checks.push(`${item.code} ${item.steps} 步:领取、开始、保存、恢复、提交、教员85分评定、统计记录通过`);
- await adminApi(`tran/v1/teaching/runs/${run.id}/archive`, {version:run.version});
- task = await adminApi(`tran/v1/teaching/assignments/${task.id}`);
- await adminApi(`tran/v1/teaching/assignments/${task.id}/archive`, {version:task.version, reason:'闭环验收完成,归档测试副本'});
- fixture.archived = true;
- fs.writeFileSync(`${dir}/normal-fixtures.json`,JSON.stringify(fixtures,null,2));
- console.log(checks.at(-1));
- }
- assert.deepEqual(errors,[]);
- fs.writeFileSync(`${dir}/normal-acceptance.json`,JSON.stringify({pass:true,checks,fixtures,errors},null,2));
- } catch (error) {
- console.log('Visible feedback:', await page?.locator('.el-message').allTextContents());
- await page?.screenshot({path:`${dir}/normal-failure.png`}).catch(()=>{});
- fs.writeFileSync(`${dir}/normal-acceptance.json`,JSON.stringify({pass:false,error:error.message,checks,fixtures,errors},null,2));
- throw error;
- } finally {
- await browser.close();
- for (const fixture of fixtures.filter(item => !item.archived)) {
- if (fixture.runId) {
- let run = await adminApi(`tran/v1/teaching/runs/${fixture.runId}`);
- if (['IN_PROGRESS','ACCEPTED','PENDING'].includes(run.status)) run = await adminApi(`tran/v1/teaching/runs/${run.id}/terminate`, {version:run.version,commandId:crypto.randomUUID(),reason:'自动化验收中止,清理测试副本'});
- if (run.status === 'SUBMITTED') run = await adminApi(`tran/v1/teaching/runs/${run.id}/review`, {version:run.version,commandId:crypto.randomUUID(),score:0,passed:false,feedback:'未完成的自动化验收副本'});
- if (run.status === 'REVIEWED') await adminApi(`tran/v1/teaching/runs/${run.id}/archive`, {version:run.version});
- }
- const task = await adminApi(`tran/v1/teaching/assignments/${fixture.taskId}`);
- await adminApi(`tran/v1/teaching/assignments/${task.id}/archive`, {version:task.version,reason:'自动化测试副本归档'});
- fixture.archived = true;
- }
- fs.appendFileSync(`${dir}/normal-attempts.jsonl`, JSON.stringify({time:new Date().toISOString(),fixtures})+'\n');
- const user = await adminApi(`auth/v1/users/${userId}`);
- await adminApi(`auth/v1/users/${userId}`, {version:user.version}, 'DELETE');
- }
|