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;i1)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();}