import { chromium, expect } from '@playwright/test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { login, call } from './role-permissions-live.mjs'; import manifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-manifest.js'; import videos from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js'; const staticMode = process.env.NARRATION_STATIC === '1'; const dir = process.env.REPORT_DIR || 'reports/bridge-digital-video-20260914'; fs.mkdirSync(dir, { recursive: true }); const origin = process.env.BASE_URL || 'http://127.0.0.1:6180'; const root = await login('root'); const response = await call('tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100', root.accessToken); assert.equal(response.status, 200); const codes = process.argv.slice(2); const packs = manifest.scripts.filter(p => !codes.length || codes.includes(p.code)); const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] }); const results = [], errors = [], requests = []; let page; try { page = await browser.newPage({ viewport: { width: 1600, height: 1000 } }); await page.addInitScript(token => { sessionStorage.setItem('unreal-tran:web:access-token:v1', token); localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode: 'military' })); }, root.accessToken); page.on('pageerror', e => errors.push(e.message)); page.on('request', request => requests.push(new URL(request.url()).pathname)); const ready = async () => { await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, { timeout: 120000 }); await expect(page.locator('[data-role="step-count"]')).toBeVisible(); }; const media = page.locator('.training-server-video'); const play = async () => { if (await page.locator('[data-role="teacher-launch"]').isVisible()) await page.locator('[data-role="teacher-launch"]').click(); await page.locator('[data-action="teacher-speak"]').click(); await expect(media).toBeVisible(); await expect.poll(() => media.evaluate(v => v.currentTime), { timeout: 15000 }).toBeGreaterThan(.1); assert.equal(await media.evaluate(v => v.error), null); assert.ok(await media.evaluate(v => v.videoWidth === 480 && v.videoHeight === 640)); }; for (const pack of packs) { const task = response.data.records.find(t => t.code === pack.code); assert.ok(task); const videoPack = staticMode ? videos.scripts.find(item => item.id === pack.id && item.version === pack.version) : null; const obsoleteDownloads = []; if (staticMode) { assert.equal(videoPack.steps.length, pack.steps.length); const assets = await call(`tran/v1/teaching/assignments/${task.id}/assets`, root.accessToken); assert.equal(assets.status, 200); const replaced = new Set(task.definitionSnapshot.steps.map(step => step.audio?.narrationVideoAssetCode?.toUpperCase()).filter(Boolean)); for (const asset of assets.data) if (asset.sourceVersionId === task.contentVersionId && replaced.has(asset.assetCode.toUpperCase())) { obsoleteDownloads.push(new URL(asset.downloadUrl, origin).pathname); } requests.length = 0; } await page.goto(`${origin}/teaching/virtual-training/preview/${task.id}`); await ready(); await play(); if (staticMode) assert.equal(await media.evaluate(v => new URL(v.currentSrc).pathname), videoPack.steps[0].url); assert.equal(await page.locator('[data-role="teacher-image"]').evaluate(e => getComputedStyle(e).visibility), 'hidden'); await page.screenshot({ path: `${dir}/${task.code}-video.png` }); const button = page.locator('[data-action="toggle-teacher-media"]'); await button.click(); await expect(media).toBeHidden(); await expect.poll(() => media.evaluate(v => new URL(v.currentSrc || v.src, location.href).pathname)).toBe(pack.steps[0].url); await expect.poll(() => media.evaluate(v => v.currentTime)).toBeGreaterThan(.1); await button.click(); await expect(media).toBeVisible(); const voice = page.locator('[data-action="toggle-teacher-voice"]'); if (await voice.getAttribute('aria-pressed') === 'true') await voice.click(); for (let op = 0; op < task.definitionSnapshot.steps[0].action.mechanism.steps.length; op++) { await page.locator('[data-role="part-label"]').click(); if (pack.code !== 'TASK-VIRTUAL-005') await expect.poll(() => page.evaluate(() => globalThis.__trainingSceneDiagnostics().bridgeWorkshop.pending)).toBe(false); else await page.waitForTimeout(1800); } await expect(page.locator('[data-action="next"]')).toBeEnabled({ timeout: 20000 }); await play(); const first = await media.getAttribute('src'); await page.locator('[data-action="next"]').click(); await expect(page.locator('[data-role="step-count"]')).toHaveText(`2 / ${pack.steps.length}`); assert.equal(await media.evaluate(v => v.paused), true); await play(); assert.notEqual(await media.getAttribute('src'), first); if (staticMode) assert.equal(await media.evaluate(v => new URL(v.currentSrc).pathname), videoPack.steps[1].url); await media.evaluate(v => { v.src = 'data:video/mp4;base64,AA=='; v.load(); }); await expect(media).toBeHidden(); await expect.poll(() => media.evaluate(v => new URL(v.currentSrc || v.src, location.href).pathname)).toBe(pack.steps[1].url); await expect.poll(() => media.evaluate(v => v.currentTime)).toBeGreaterThan(.1); await page.reload(); await ready(); await play(); await page.locator('.training-teacher-bubble [data-action="toggle-teacher"]').click(); assert.equal(await media.evaluate(v => v.paused), true); if (staticMode) assert.deepEqual(requests.filter(url => obsoleteDownloads.includes(url)), [], 'Static narration must not download obsolete video assets'); results.push({ code: pack.code, firstAndSecondVideo: true, audioToggle: true, stopOnStepChange: true, errorFallback: true, reloadAndClose: true, ...(staticMode ? { staticVideoUrls: videoPack.steps.slice(0, 2).map(clip => clip.url), obsoleteAssetDownloads: 0 } : {}) }); console.log('PASS', pack.code); } assert.deepEqual(errors, []); fs.writeFileSync(`${dir}/${origin.includes('127.0.0.1') ? 'live' : 'lan'}-result.json`, JSON.stringify({ origin, results, errors }, null, 2)); } catch (error) { if (page) await page.screenshot({ path: `${dir}/failure.png` }).catch(() => {}); fs.writeFileSync(`${dir}/failure.json`, JSON.stringify({ message: error.message, results, errors }, null, 2)); throw error; } finally { await browser.close(); }