|
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import {chromium,expect} from '@playwright/test';
- import {login,call} from './role-permissions-live.mjs';
- const report=path.resolve('reports/virtual-html-workspace-20260912');fs.mkdirSync(report,{recursive:true});
- const checks=[],errors=[],admin=await login('root');
- const api=async(endpoint,session=admin,body,method)=>{const r=await call(endpoint,session?.accessToken,body,method);assert.equal(r.status,200,`${endpoint}: ${r.message}`);return r.data;};
- const checked=(name,details)=>{checks.push({name,pass:true,details});console.log('PASS',name);};
- const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
- const createContext=async(session,width=1920)=>{const ctx=await browser.newContext({viewport:{width,height:1080}});await ctx.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'}));
- },session);return ctx;};
- const context=await createContext(admin),page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
- const diagnostic=p=>p.evaluate(()=>globalThis.__trainingSceneDiagnostics?.());
- const open=async(p,url)=>{await p.goto('http://127.0.0.1:6180'+url);await p.locator('.training-shell').waitFor({timeout:120000});await expect.poll(async()=>Boolean((await diagnostic(p))?.publishedContentMounted),{timeout:90000}).toBe(true);};
- const screenshot=async(p,name)=>{await p.screenshot({path:path.join(report,name+'.png'),fullPage:true});};
- let studentPage;
- try {
- await open(page,'/teaching/virtual-training/preview/54');
- await expect(page.locator('[data-role="model-commandbar"]')).toBeVisible();
- 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(page.locator(`[data-role="model-commandbar"] [data-action="${action}"]`)).toBeVisible();
- await page.locator('[data-role="model-commandbar"] [data-action="toggle-structure-tree"]').click();
- const tree=page.locator('[data-role="structure-tree"]');await expect(tree).toHaveClass(/is-open/);
- assert.ok((await tree.locator('[data-action="select-structure-part"]').count())>20);
- await tree.locator('[data-action="select-structure-part"]').first().click();
- await page.locator('[data-action="focus-selected-part"]').click();
- await page.locator('[data-role="model-commandbar"] [data-action="toggle-structure-tree"]').click();
- await page.locator('[data-action="toggle-issue-highlight"]').click();
- await expect.poll(async()=>(await diagnostic(page)).framing.keptTargets.length).toBeGreaterThan(0);
- await page.locator('[data-action="toggle-hide-others"]').click();
- assert.equal((await diagnostic(page)).framing.hideOthers,true);
- await page.locator('[data-action="toggle-hide-others"]').click();
- await page.locator('[data-action="toggle-part-labels"]').click();
- assert.equal((await diagnostic(page)).framing.labelsVisible,true);
- await screenshot(page,'01-bridge-tools');
- await page.locator('[data-action="toggle-explosion"]').click();
- await screenshot(page,'02-bridge-explosion');
- await page.locator('[data-action="viewer-reset-view"]').click();
- await expect(page.locator('[data-role="teacher-image"]')).toBeVisible();
- const bridge=await diagnostic(page);assert.equal(bridge.proceduralMachineVisible,false);assert.ok(bridge.server.registeredTargets.includes(bridge.framing.targetOnScreen.targetId));
- checked('桥梁:完整结构树、聚焦、高亮、隐藏、标签、爆炸、复位和数字教员',bridge.framing);
- const suffix=Date.now().toString(36),password='Vh!'+crypto.randomBytes(8).toString('hex');
- const roles=await api('auth/v1/roles?size=100');const role=(roles.records||roles).find(r=>r.code==='student');
- const user=await api('auth/v1/users',admin,{username:'vh_'+suffix,displayName:'HTML验收-'+suffix,departmentId:'5',roleIds:[role.id],defaultRoleId:role.id,enabled:true,password,remark:'JS/HTML正式工作区隔离测试账号'});
- const learner=await api('auth/v1/auth/login',null,{userId:(user.user||user).id,roleCode:'student',password,rememberMe:false});
- const source=await api('tran/v1/content/projects/419');
- source.currentVersion.content.steps.forEach((step,index)=>{
- step.mode=index===0?'measure':'remove';step.interaction=index===0?'rule-check':'spatial-drag';step.targetNodeId=step.targetId;
- step.destination=index===0?'压力测量检查':'编号存放区';
- step.action={...step.action,kind:step.interaction,mode:step.mode,targetId:step.targetId,requiresPlacement:index===1,tolerance:.55};
- });
- const content=await api('tran/v1/content/projects',admin,{type:'TRAINING',name:'HTML闭环验收-'+suffix,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:source.currentVersion.content,dependencies:source.currentVersion.dependencies,assets:[]});
- await api(`tran/v1/content/projects/${content.project.id}/publish`,admin,{version:content.project.version});
- const reference=(await api(`tran/v1/content/projects/${content.project.id}`)).project;
- const task=await api('tran/v1/teaching/assignments',admin,{code:'HTML-'+suffix,name:'HTML工作区-卡车动画闭环-'+suffix,description:'开发环境真实操作、断点恢复和动画验收',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'PRACTICE',audienceType:'ASSIGNED',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:true,contentProjectId:reference.id,contentVersionId:reference.publishedVersionId,members:[{userId:(user.user||user).id,memberType:'LEARNER',teamCode:'NEUTRAL',positionCode:'LEARNER',sortOrder:0}]});
- const published=await api(`tran/v1/teaching/assignments/${task.id}/publish`,admin,{version:task.version});
- const run=await api(`tran/v1/teaching/assignments/${task.id}/accept`,learner,{assignmentVersion:published.version,teamCode:'NEUTRAL',positionCode:'LEARNER'});
- fs.writeFileSync(path.join(report,'fixture.json'),JSON.stringify({userId:(user.user||user).id,assignmentId:task.id,runId:run.id},null,2));
- const ctx=await createContext(learner);studentPage=await ctx.newPage();studentPage.on('pageerror',e=>errors.push(e.message));
- await open(studentPage,`/teaching/virtual-training/runs/${run.id}`);
- await studentPage.waitForTimeout(5200);
- const animation=(await diagnostic(studentPage)).server.animations.find(a=>a.bindings);
- assert.equal(animation.bindings,4);assert.equal(animation.unresolved,0);assert.equal(animation.time,4);assert.equal(animation.flowVisible,true);
- checked('卡车:正式工作区播放四种已编排动画轨道并保持末帧',animation);
- const prepare=async()=>{
- const panel=studentPage.locator('.training-server-preparation');if(!(await panel.evaluate(p=>p.open)))await panel.locator('summary').click();
- await panel.locator('[data-tool="pressure-gauge"]').check();for(const checkbox of await panel.locator('[data-safety]').all())await checkbox.check();
- await panel.locator('textarea').fill('测试测量:压力为零,检查已完成。');
- await panel.locator('[data-save-preparation]').click();await expect(panel.locator('[data-save-status]')).toHaveText('已保存');
- };
- await prepare();
- await studentPage.reload();await expect.poll(async()=>Boolean((await diagnostic(studentPage))?.publishedContentMounted),{timeout:90000}).toBe(true);
- if (!(await studentPage.locator('.training-server-preparation').evaluate(panel => panel.open))) await studentPage.locator('.training-server-preparation summary').click();
- await expect(studentPage.locator('[data-tool="pressure-gauge"]')).toBeChecked();await expect(studentPage.locator('[data-measurement]')).toHaveValue('测试测量:压力为零,检查已完成。');
- await screenshot(studentPage,'03-learner-preparation-restored');
- checked('学员:工具、安全、测量记录通过真实接口保存并刷新恢复',{runId:run.id});
- await studentPage.locator('.training-server-preparation summary').click();
- const clickTarget=async()=>{
- await studentPage.locator('[data-action="toggle-issue-highlight"]').click();
- await studentPage.waitForTimeout(500);
- const d=await diagnostic(studentPage),rect=await studentPage.locator('canvas.training-canvas').boundingBox();
- assert.ok(d.framing.targetOnScreen?.insideCanvas,'Target must be on screen');
- await studentPage.mouse.click(rect.x+d.framing.targetOnScreen.localX,rect.y+d.framing.targetOnScreen.localY+38);
- };
- // Lose one progress response before it reaches the API; ensure no local advance.
- let rejected=false;
- await studentPage.route('**/api/tran/v1/teaching/runs/*/progress',async route=>{if(!rejected){rejected=true;await route.abort('failed');}else await route.continue();});
- await clickTarget();await studentPage.waitForTimeout(1500);
- assert.equal(rejected,true,'An actual progress request must have been attempted');
- assert.equal((await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,0);
- assert.equal((await diagnostic(studentPage)).server.completed.length,0);
- checked('进度请求失败时不在本地提前完成步骤');
- await clickTarget();
- await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,{timeout:20000}).toBe(50);
- await expect(studentPage.locator('[data-action="next"]')).toBeEnabled();
- await studentPage.locator('[data-action="next"]').click();await studentPage.waitForTimeout(5200);
- const reset=(await diagnostic(studentPage)).server.animations.find(a=>a.bindings);assert.equal(reset.time,0);assert.equal(reset.flowVisible,false);
- checked('步骤完成服务器确认后推进,下一步动画按发布片段复位',reset);
- await prepare();await studentPage.locator('.training-server-preparation summary').click();
- const dragState=await diagnostic(studentPage),canvasRect=await studentPage.locator('canvas.training-canvas').boundingBox(),drop=await studentPage.locator('[data-role="dropzone"]').boundingBox();
- await studentPage.mouse.move(canvasRect.x+dragState.framing.targetOnScreen.localX,canvasRect.y+dragState.framing.targetOnScreen.localY+38);
- await studentPage.mouse.down();await studentPage.mouse.move(drop.x+drop.width/2,drop.y+drop.height/2,{steps:25});await studentPage.mouse.up();
- await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).progressPercent,{timeout:20000}).toBe(100);
- await studentPage.locator('[data-action="next"]').click();
- await expect.poll(async()=>(await api(`tran/v1/teaching/runs/${run.id}`,learner)).status,{timeout:20000}).toBe('SUBMITTED');
- await screenshot(studentPage,'04-submitted');checked('真实两步操作完成、服务端判定、提交结果闭环',{runId:run.id});
- await open(page,`/teaching/virtual-training/runs/${run.id}`);assert.equal((await diagnostic(page)).server.readOnly,true);await screenshot(page,'05-observer');
- checked('维护账号观察他人运行时只读,不修改学员进度');
- assert.equal(errors.length,0,errors.join('\n'));
- } catch(error) {checks.push({name:'HTML工作区端到端',pass:false,error:error.message});console.error(error.message);process.exitCode=1;await screenshot(studentPage||page,'failure').catch(()=>{});}
- finally {fs.writeFileSync(path.join(report,'checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();}
|