Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 

79 righe
5.9 KiB

  1. import { chromium, expect } from '@playwright/test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import { login, call } from './role-permissions-live.mjs';
  5. import manifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-manifest.js';
  6. import { scriptTrainingDefinition } from '../../unreal_tran_web/src/features/virtual-training-scripts/registry.js';
  7. const dir = 'reports/bridge-narration-20260914'; fs.mkdirSync(dir, { recursive: true });
  8. const root = await login('root');
  9. const response = await call('tran/v1/teaching/assignments?channel=VIRTUAL&assignmentKind=TRAINING&size=100', root.accessToken);
  10. assert.equal(response.status, 200);
  11. const tasks = manifest.scripts.map(s => response.data.records.find(t => t.code === s.code));
  12. const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] });
  13. const results = [], errors = [];
  14. try {
  15. const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
  16. await page.addInitScript(token => sessionStorage.setItem('unreal-tran:web:access-token:v1', token), root.accessToken);
  17. page.on('pageerror', e => errors.push(e.message));
  18. const ready = async () => {
  19. await expect(page.locator('[data-role="loading"]')).toHaveClass(/is-hidden/, { timeout: 120000 });
  20. await expect(page.locator('[data-role="step-count"]')).toBeVisible();
  21. };
  22. const audioState = () => page.locator('.training-server-video').evaluate(a => ({ src: new URL(a.currentSrc || a.src, location.href).pathname, paused: a.paused, time: a.currentTime, error: a.error?.message }));
  23. const play = async expected => {
  24. const launch = page.locator('[data-role="teacher-launch"]');
  25. if (await launch.isVisible()) await launch.click();
  26. await page.locator('[data-action="teacher-speak"]').click();
  27. await expect.poll(async () => (await audioState()).src).toBe(expected);
  28. await expect.poll(async () => (await audioState()).time, { timeout: 15000 }).toBeGreaterThan(.1);
  29. assert.equal((await audioState()).error, undefined);
  30. };
  31. for (const [index, task] of tasks.entries()) {
  32. assert.ok(task, manifest.scripts[index].code);
  33. const pack = manifest.scripts[index], bound = scriptTrainingDefinition(task);
  34. assert.deepEqual(bound.steps.map(s => s.audio.narrationUrl), pack.steps.map(s => s.url));
  35. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${task.id}`);
  36. await ready(); await play(pack.steps[0].url);
  37. const toggle = page.locator('[data-action="toggle-teacher-voice"]');
  38. if (await toggle.getAttribute('aria-pressed') === 'true') await toggle.click();
  39. // Complete the real first-step model actions in preview, then change step.
  40. for (let op = 0; op < task.definitionSnapshot.steps[0].action.mechanism.steps.length; op++) {
  41. await page.locator('[data-role="part-label"]').click();
  42. if (index > 0) await expect.poll(() => page.evaluate(() => globalThis.__trainingSceneDiagnostics().bridgeWorkshop.pending)).toBe(false);
  43. else await page.waitForTimeout(1800);
  44. }
  45. await expect(page.locator('[data-action="next"]')).toBeEnabled({ timeout: 20000 });
  46. await play(pack.steps[0].url);
  47. await page.locator('[data-action="next"]').click();
  48. await expect(page.locator('[data-role="step-count"]')).toHaveText(`2 / ${pack.steps.length}`);
  49. assert.equal((await audioState()).paused, true, 'Old narration stops on step change');
  50. await play(pack.steps[1].url);
  51. await page.screenshot({ path: `${dir}/${task.code}-step-2.png` });
  52. await page.reload(); await ready(); await play(pack.steps[0].url);
  53. await page.locator('.training-teacher-bubble [data-action="toggle-teacher"]').click();
  54. assert.equal((await audioState()).paused, true, 'Closing instructor stops narration');
  55. results.push({ code: task.code, stepsBound: pack.steps.length, firstAndSecondPlayed: true, stopOnStepChange: true, reloadPlayed: true });
  56. console.log(task.code, 'play / step change / reload passed');
  57. }
  58. // Download and decode every published clip with the browser's actual codec.
  59. const clips = manifest.scripts.flatMap(s => s.steps.flatMap(step => [{ url: step.url, duration: step.stepDuration }, ...(step.safetyUrl ? [{ url: step.safetyUrl, duration: step.safetyDuration }] : [])]));
  60. const decoded = await page.evaluate(async clips => {
  61. const context = new AudioContext(), results = [];
  62. try {
  63. for (const clip of clips) {
  64. const response = await fetch(clip.url);
  65. if (!response.ok) throw new Error(`Audio HTTP ${response.status}: ${clip.url}`);
  66. const buffer = await context.decodeAudioData(await response.arrayBuffer());
  67. if (Math.abs(buffer.duration - clip.duration) > .02) throw new Error(`Duration mismatch: ${clip.url}`);
  68. results.push({ url: clip.url, duration: buffer.duration, sampleRate: buffer.sampleRate });
  69. }
  70. } finally { await context.close(); }
  71. return results;
  72. }, clips);
  73. assert.equal(decoded.length, 42); assert.deepEqual(errors, []);
  74. fs.writeFileSync(`${dir}/result.json`, JSON.stringify({ results, decoded, errors }, null, 2));
  75. fs.writeFileSync(`${dir}/index.html`, `<!doctype html><meta charset="utf-8"><title>桥梁脚本语音验收</title><style>body{font:16px sans-serif;margin:32px;color:#134e45}section{margin:24px 0;padding:16px;border:1px solid #bddbd4}audio{width:420px}img{width:100%;max-width:1000px}</style><h1>四套桥梁脚本语音验收</h1><p>39段步骤讲解、3段核对提醒,全部真实下载和浏览器解码通过。四个原任务前两步实际播放、切步停止、刷新重播、关闭停止通过。教员预览未写入成绩。</p>${manifest.scripts.map(s => `<section><h2>${s.code} ${s.title}</h2><p>${s.source} · ${s.steps.length}步</p><img src="${s.code}-step-2.png">${s.steps.map(step => `<p>${step.text}</p><audio controls preload="none" src="http://127.0.0.1:6180${step.url}"></audio>`).join('')}</section>`).join('')}`);
  76. console.log('PASS', decoded.length, 'WAVs decoded');
  77. } finally { await browser.close(); }