Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

115 lignes
8.4 KiB

  1. import fs from 'node:fs';
  2. import assert from 'node:assert/strict';
  3. import {chromium, expect} from '@playwright/test';
  4. import {login} from './role-permissions-live.mjs';
  5. import videoManifest from '../../unreal_tran_web/src/features/virtual-training-scripts/narration-video-manifest.js';
  6. const dir='reports/bridge-introduction-layout-20260920';
  7. fs.mkdirSync(dir,{recursive:true});
  8. const signed=await login('root'), checks=[], errors=[], writes=[];
  9. const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']});
  10. const page=await browser.newPage({viewport:{width:1920,height:1080}});
  11. await page.addInitScript(s=>{
  12. sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);
  13. sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken);
  14. localStorage.setItem('unreal-tran:web:preferences:v1',JSON.stringify({themeMode:'light'}));
  15. },signed);
  16. await page.route(/\/src\/features\/virtual-training-legacy\/upstream\/views\/training\.js(?:\?|$)/,async route=>{
  17. const response=await route.fetch();
  18. await route.fulfill({response,body:await response.text()+'\nglobalThis.introLayoutState=()=>activeTraining?{step:activeTraining.session.stepIndex,completed:[...activeTraining.session.completed]}:null;'});
  19. });
  20. page.on('pageerror',e=>errors.push(e.message));
  21. page.on('request',r=>{if(r.url().includes('/api/tran/')&&!['GET','HEAD'].includes(r.method()))writes.push(new URL(r.url()).pathname);});
  22. const state=()=>page.evaluate(()=>globalThis.introLayoutState?.());
  23. const overlay=page.locator('.training-introduction');
  24. try {
  25. for(const [id,pages,steps] of [[86,6,5],[87,3,7],[88,1,11],[89,1,16]]) {
  26. await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${id}`);
  27. await expect(overlay).toBeVisible({timeout:120000});
  28. await expect(page.locator('[data-intro-page]')).toHaveCount(pages);
  29. await expect(page.locator('[data-step-index]')).toHaveCount(steps);
  30. await expect(page.locator('[data-role="step-count"]')).toHaveText(`1 / ${pages+steps}`);
  31. await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0)),{timeout:30000}).toBe(true);
  32. const original=await state(), writeCount=writes.length;
  33. await page.screenshot({path:`${dir}/${id}-desktop.png`});
  34. for(const viewport of [{width:1366,height:768},{width:1280,height:720}]) {
  35. await page.setViewportSize(viewport);
  36. const layout=await page.evaluate(()=>{
  37. const scroller=document.querySelector('.training-sequence-scroll');
  38. const root=document.querySelector('[data-role="steps"]');
  39. const content=document.querySelector('.training-introduction-content');
  40. const image=document.querySelector('.introduction-visual'),text=document.querySelector('.introduction-reading');
  41. const before=scroller.querySelector('[data-intro-page]').getBoundingClientRect().top;
  42. scroller.scrollTop=scroller.scrollHeight;
  43. return {scrollable:scroller.scrollHeight>scroller.clientHeight,
  44. introMoves:scroller.querySelector('[data-intro-page]').getBoundingClientRect().top<before,
  45. stepsOverflow:getComputedStyle(root).overflowY,
  46. horizontalOverflow:content.scrollWidth>content.clientWidth,
  47. imageTextOverlap:image&&text?image.getBoundingClientRect().right>text.getBoundingClientRect().left:false};
  48. });
  49. assert.ok(layout.scrollable);assert.ok(layout.introMoves);
  50. assert.equal(layout.stepsOverflow,'visible');assert.equal(layout.horizontalOverflow,false);assert.equal(layout.imageTextOverlap,false);
  51. await expect(page.locator('[data-step-index]').last()).toBeInViewport();
  52. await expect(overlay.locator('[data-intro-command="skip"]')).toBeInViewport();
  53. await expect(overlay.locator('[data-intro-command="next"]')).toBeInViewport();
  54. await page.locator('.training-sequence-scroll').evaluate(el=>el.scrollTop=0);
  55. await page.screenshot({path:`${dir}/${id}-${viewport.width}.png`});
  56. checks.push({id,viewport,...layout});
  57. }
  58. for(let i=1;i<pages;i++) {
  59. await overlay.locator('[data-intro-command="next"]').click();
  60. await expect(page.locator(`[data-intro-page="${i}"]`)).toHaveAttribute('aria-current','page');
  61. assert.deepEqual(await state(),original);
  62. }
  63. if(pages>1)await page.screenshot({path:`${dir}/${id}-lecture.png`});
  64. await overlay.locator('[data-intro-command="next"]').click();
  65. await expect(overlay).toBeHidden();
  66. assert.deepEqual(await state(),original);
  67. await expect(page.locator('[data-step-index="0"]')).toBeInViewport();
  68. await page.locator('[data-intro-page="0"]').click();
  69. await expect(overlay).toBeVisible();
  70. await overlay.locator('[data-intro-command="skip"]').click();
  71. await expect(overlay).toBeHidden();assert.deepEqual(await state(),original);
  72. assert.equal(writes.length,writeCount);
  73. await page.setViewportSize({width:1920,height:1080});
  74. console.log('PASS task',id);
  75. }
  76. // Browser-only response fixture validates optional media without changing any saved task.
  77. await page.route(/\/api\/tran\/v1\/teaching\/assignments\/86$/,async route=>{
  78. const response=await route.fetch(),body=await response.json();
  79. body.data.definitionSnapshot.introduction={background:{enabled:true,title:'纯文字背景',body:'没有背景图时,正文保持居中阅读。\n讲解切换不推进操作步骤。'},lectures:[{id:'video-check',title:'视频讲解',format:'video',videoUrl:videoManifest.scripts[0].steps[0].url}]};
  80. await route.fulfill({response,json:body});
  81. });
  82. await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');
  83. await expect(overlay).toBeVisible({timeout:120000});
  84. await expect(overlay.locator('.is-text-only')).toBeVisible();
  85. await expect(overlay.locator('.introduction-reading p')).toHaveCount(2);
  86. await page.screenshot({path:`${dir}/text-only.png`});
  87. await overlay.locator('[data-intro-command="next"]').click();
  88. const video=overlay.locator('video');await expect(video).toBeVisible();
  89. await video.evaluate(el=>el.play());
  90. await expect.poll(()=>video.evaluate(el=>el.currentTime),{timeout:30000}).toBeGreaterThan(0);
  91. await page.screenshot({path:`${dir}/video.png`});
  92. await page.evaluate(()=>globalThis.previousIntroVideo=document.querySelector('.training-introduction video'));
  93. await overlay.locator('[data-intro-command="skip"]').click();
  94. assert.equal(await page.evaluate(()=>globalThis.previousIntroVideo.paused&&!globalThis.previousIntroVideo.getAttribute('src')),true);
  95. checks.push({optionalTextAndVideo:true,videoStopsOnSkip:true});
  96. await page.unroute(/\/api\/tran\/v1\/teaching\/assignments\/86$/);
  97. await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/86');
  98. await expect(overlay).toBeVisible({timeout:120000});
  99. await expect.poll(()=>overlay.locator('img').evaluateAll(images=>images.every(i=>i.complete&&i.naturalWidth>0))).toBe(true);
  100. for(const mode of ['dark','military']) {
  101. await page.evaluate(mode=>{document.documentElement.classList.toggle('dark',mode==='dark');document.documentElement.dataset.colorTheme=mode;document.documentElement.dataset.theme=mode==='dark'?'dark':'light';},mode);
  102. await page.screenshot({path:`${dir}/theme-${mode}.png`});
  103. }
  104. const lan=await page.request.get('http://192.168.31.168:6180/api/auth/v1/system-config/public');
  105. assert.equal(lan.status(),200);checks.push({lanGatewayProxy:true});
  106. assert.deepEqual(errors,[]);
  107. fs.writeFileSync(`${dir}/result.json`,JSON.stringify({pass:true,checks,errors,writes,accountType:'internal maintenance preview',testDataCreated:false},null,2));
  108. fs.writeFileSync(`${dir}/index.html`,`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>背景讲解布局验收</title><style>body{font:16px system-ui;margin:32px;background:#eff5f3;color:#16463e}img{width:100%;border:1px solid #bed5d0;border-radius:8px}section{margin:24px 0}a{color:#078773}</style><h1>背景讲解布局验收</h1><p>四个任务通过:统一滚动、图文分栏、小屏布局、翻页与跳过;操作进度保持不变。未写入任务数据。</p>${[86,87,88,89].map((id,i)=>`<section><h2>TASK-VIRTUAL-00${i+5}</h2><a href="${id}-desktop.png">桌面截图</a> · <a href="${id}-1280.png">1280 × 720</a><img src="${id}-1366.png"></section>`).join('')}</html>`);
  109. } catch(error) {
  110. await page.screenshot({path:`${dir}/failure.png`});
  111. fs.writeFileSync(`${dir}/failure.json`,JSON.stringify({error:error.stack,errors},null,2));
  112. throw error;
  113. } finally {await browser.close();}