|
- import fs from 'node:fs';
- import assert from 'node:assert/strict';
- import {chromium,expect} from '@playwright/test';
- import {login,call} from './role-permissions-live.mjs';
- const dir='reports/bridge-introduction-20260918';fs.mkdirSync(dir,{recursive:true});
- const signed=await login('root'),checks=[],errors=[],writes=[],network=[];
- const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
- const page=await browser.newPage({viewport:{width:1366,height:768}});
- await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);},signed);
- await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{const response=await route.fetch();await route.fulfill({response,body:await response.text()+`\nglobalThis.introState=()=>activeTraining?{active:!!activeTraining.introductionActive,step:activeTraining.session.stepIndex,completed:[...activeTraining.session.completed],ready:activeTraining.sceneReady,errors:activeTraining.errorCount}:null;`});});
- page.on('pageerror',e=>errors.push(e.message));page.on('request',r=>{if(r.url().includes('/api/tran/')&&!['GET','HEAD'].includes(r.method()))writes.push(new URL(r.url()).pathname);});
- page.on('response',r=>{if(r.status()>=400)network.push({path:new URL(r.url()).pathname,status:r.status()});});
- page.on('requestfailed',r=>network.push({path:new URL(r.url()).pathname,error:r.failure()?.errorText}));
- const state=()=>page.evaluate(()=>globalThis.introState?.());
- const overlay=page.locator('.training-introduction');
- try{
- for(const [id,count,steps] of [[86,6,5],[87,3,7],[88,1,11],[89,1,16],[116,6,5]].filter(([id])=>!process.env.TASK_IDS||process.env.TASK_IDS.split(',').includes(String(id)))){
- await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/'+id);
- await expect(overlay).toBeVisible({timeout:120000});
- await expect(page.locator('[data-intro-page]')).toHaveCount(count);
- await expect(page.locator('[data-step-index]')).toHaveCount(steps);
- await expect(overlay.locator('h2')).toContainText('项目背景');
- assert.equal((await state()).step,0);assert.deepEqual((await state()).completed,[]);
- await page.screenshot({path:dir+'/task-'+id+'-background.png'});
- const beforeWrites=writes.length;
- for(let i=1;i<count;i++){
- await overlay.locator('[data-intro-command="next"]').click();
- await expect(page.locator('[data-intro-page="'+i+'"]').first()).toHaveAttribute('aria-current','page');
- assert.equal((await state()).step,0);assert.deepEqual((await state()).completed,[]);
- }
- if(count>1)await page.screenshot({path:dir+'/task-'+id+'-lecture.png'});
- assert.equal(writes.length,beforeWrites,'intro navigation does not post operations or scores');
- const playing=await page.locator('audio,video').evaluateAll(items=>items.filter(m=>!m.paused).length);assert.equal(playing,0,'no hidden operation narration during intro');
- await overlay.locator('[data-intro-command="next"]').click();await expect(overlay).toBeHidden();
- await expect.poll(async()=>!!(await state())?.ready,{timeout:120000}).toBe(true);
- assert.equal((await state()).step,0);assert.deepEqual((await state()).completed,[]);
- await page.locator('[data-intro-page="0"]').click();await expect(overlay).toBeVisible();
- await overlay.locator('[data-intro-command="skip"]').click();await expect(overlay).toBeHidden();
- if(count>1){await page.locator('[data-intro-page="1"]').click();await overlay.locator('[data-intro-command="skip"]').click();await expect(overlay).toBeHidden();}
- assert.equal((await state()).step,0);assert.equal((await state()).errors,0);
- await page.screenshot({path:dir+'/task-'+id+'-operations.png'});
- checks.push({id,pages:count,operationSteps:steps,next:true,skip:true,operationStateUnchanged:true});console.log('PASS',id,count);
- }
- // Isolated presentation fixture; no changes to the published task or scoring.
- const project=(await call('tran/v1/content/projects/458',signed.accessToken)).data;
- const inventory=(await call('tran/v1/teaching/assignments/116/assets',signed.accessToken)).data;
- const video=inventory.find(a=>a.type==='VIDEO');assert.ok(video,'existing controlled video asset');
- await page.route(/\/api\/tran\/v1\/teaching\/assignments\/116$/,async route=>{const response=await route.fetch(),body=await response.json();body.data.definitionSnapshot.introduction.lectures=[{id:'test-video',title:'视频讲解验证',format:'video',videoAssetCode:video.assetCode}];await route.fulfill({response,json:body});});
- await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/116');await expect(overlay).toBeVisible({timeout:120000});
- await overlay.locator('[data-intro-command="next"]').click();const media=overlay.locator('video');await expect(media).toBeVisible();
- await media.evaluate(m=>m.play());await expect.poll(()=>media.evaluate(m=>m.currentTime>0&&m.readyState>=2),{timeout:30000}).toBe(true);
- await page.screenshot({path:dir+'/video-lecture.png'});
- await page.evaluate(()=>globalThis.previousIntroVideo=document.querySelector('.training-introduction video'));
- await overlay.locator('[data-intro-command="skip"]').click();assert.equal(await page.evaluate(()=>globalThis.previousIntroVideo.paused&&!globalThis.previousIntroVideo.getAttribute('src')),true);
- checks.push({name:'controlled video plays; skip stops playback and releases source',isolatedResponseFixture:true,pass:true});
- await page.unroute(/\/api\/tran\/v1\/teaching\/assignments\/116$/);
- // Exercise the actual authoring dialog and save/reopen path on the existing pilot project.
- await page.goto('http://127.0.0.1:6180/content/training-projects/458/legacy');
- const open=page.getByRole('button',{name:'背景与前置讲解',exact:true});await expect(open).toBeEnabled({timeout:120000});
- await expect(page.locator('[data-action="save"]')).toBeVisible({timeout:120000});await open.click();
- const dialog=page.getByRole('dialog',{name:'背景与前置讲解'});await expect(dialog).toBeVisible();
- await expect(dialog.locator('textarea')).toHaveValue(project.currentVersion.content.introduction.background.body);
- await dialog.getByRole('button',{name:/讲解1 ·/}).click();await expect(dialog.locator('textarea')).toHaveValue(project.currentVersion.content.introduction.lectures[0].body);
- await page.screenshot({path:dir+'/authoring.png'});
- await dialog.getByRole('button',{name:'应用到编排',exact:true}).click();
- const saved=page.waitForResponse(r=>r.url().includes('/content/projects/458')&&r.request().method()==='PUT');
- await page.locator('[data-action="save"]').click();assert.equal((await saved).status(),200);
- const after=(await call('tran/v1/content/projects/458',signed.accessToken)).data;
- assert.deepEqual(after.currentVersion.content.introduction,project.currentVersion.content.introduction);
- await page.reload();await expect(open).toBeEnabled({timeout:120000});await expect(page.locator('[data-action="save"]')).toBeVisible({timeout:120000});await open.click();
- await expect(dialog.locator('textarea')).toHaveValue(project.currentVersion.content.introduction.background.body);
- await dialog.getByRole('button',{name:'取消',exact:true}).click();
- checks.push({name:'authoring save and reload preserve introduction',pass:true,projectId:'458',draftVersionId:after.currentVersion.id});
- assert.deepEqual(errors,[]);fs.writeFileSync(dir+'/result.json',JSON.stringify({pass:true,checks,errors},null,2));console.log('ALL PASS');
- }catch(error){await page.screenshot({path:dir+'/failure.png'});fs.writeFileSync(dir+'/error.json',JSON.stringify({error:error.stack,errors,network,body:(await page.locator('body').innerText()).slice(-8000)},null,2));throw error;}finally{await browser.close();}
|