You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

128 lines
9.5 KiB

  1. import fs from 'node:fs';
  2. import assert from 'node:assert/strict';
  3. import { chromium, expect } from '@playwright/test';
  4. import { login, call } from './role-permissions-live.mjs';
  5. const output = 'reports/virtual-floating-preparation-20260913';
  6. fs.mkdirSync(output, { recursive: true });
  7. const session = await login('root');
  8. const task = await call('tran/v1/teaching/assignments/81', session.accessToken);
  9. assert.equal(task.status, 200);
  10. const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] });
  11. const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
  12. await context.addInitScript(s => {
  13. sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken);
  14. sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken);
  15. localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode: 'military' }));
  16. }, session);
  17. const page = await context.newPage(), checks = [], pageErrors = [];
  18. page.on('pageerror', error => pageErrors.push(error.message));
  19. const checked = name => { checks.push({ name, pass: true }); console.log('PASS', name); };
  20. const diagnostic = () => page.evaluate(() => globalThis.__trainingSceneDiagnostics?.());
  21. try {
  22. await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/81');
  23. await expect.poll(async () => Boolean((await diagnostic())?.publishedContentMounted), { timeout: 90000 }).toBe(true);
  24. const panel = page.locator('.training-server-preparation');
  25. await expect(panel).not.toBeVisible();
  26. await expect(page.locator('[data-role="instruction"]')).toContainText('操作准备');
  27. await expect(page.locator('[data-role="instruction"]')).toContainText('数字压力表');
  28. await expect(page.locator('[data-open-preparation]')).toBeVisible();
  29. await page.locator('[data-open-preparation]').click();
  30. await expect(panel).toHaveAttribute('open', '');
  31. assert.equal(await page.locator('.training-steps-panel .training-server-preparation').count(), 0);
  32. for (const size of [{width:1920,height:1080},{width:1440,height:900},{width:1366,height:768}]) {
  33. await page.setViewportSize(size);
  34. await page.waitForTimeout(300);
  35. const bounds = await page.evaluate(() => {
  36. const rect = selector => { const r=document.querySelector(selector).getBoundingClientRect(); return {x:r.x,y:r.y,width:r.width,height:r.height,right:r.right,bottom:r.bottom}; };
  37. return {panel:rect('.training-server-preparation'), canvas:rect('.training-viewport'), left:rect('.training-steps-panel'), header:rect('.training-header'), dock:rect('.training-operation-dock'), footer:rect('.training-bottom-bar')};
  38. });
  39. assert.ok(bounds.left.width <= 220);
  40. assert.ok(bounds.header.height <= 46);
  41. assert.ok(bounds.footer.height <= 36);
  42. assert.ok(bounds.panel.x >= bounds.canvas.x && bounds.panel.right <= bounds.canvas.right);
  43. assert.ok(bounds.panel.y >= bounds.canvas.y && bounds.panel.bottom < bounds.dock.y);
  44. assert.ok(bounds.canvas.bottom <= size.height, '3D viewport stays within screen');
  45. assert.ok(bounds.footer.bottom <= size.height, 'Bottom actions stay within screen');
  46. await panel.locator('[data-measurement]').fill('浮窗收起与尺寸切换保留输入');
  47. await panel.locator('[data-close-preparation]').click();
  48. await expect(panel).not.toBeVisible();
  49. await page.screenshot({path:`${output}/workspace-${size.width}.png`,fullPage:true});
  50. await page.locator('[data-open-preparation]').click();
  51. await expect(panel.locator('[data-measurement]')).toHaveValue('浮窗收起与尺寸切换保留输入');
  52. const summary = await panel.locator('summary').boundingBox(), floating = await panel.boundingBox();
  53. assert.ok(summary.y >= floating.y && summary.y + summary.height <= floating.y + floating.height, 'Floating header stays visible after textarea scroll');
  54. await page.screenshot({path:`${output}/floating-${size.width}.png`,fullPage:true});
  55. checked(`${size.width}×${size.height}: compact layout, floating window stays inside viewport, input survives closing`);
  56. }
  57. await panel.locator('[data-measurement]').fill('');
  58. await panel.locator('[data-measurement]').press('Escape');
  59. await expect(panel).not.toBeVisible();
  60. await expect(page.locator('[data-open-preparation]')).toBeFocused();
  61. await page.locator('[data-open-preparation]').click();
  62. checked('Preparation starts collapsed, opens inside 3D area, and Escape returns focus');
  63. const clickTarget = async () => {
  64. await page.locator('[data-action="toggle-issue-highlight"]').click();
  65. await page.waitForTimeout(700);
  66. const d = await diagnostic(), box = await page.locator('canvas.training-canvas').boundingBox();
  67. assert.ok(d.framing.targetOnScreen?.insideCanvas);
  68. await page.mouse.click(box.x + d.framing.targetOnScreen.localX, box.y + d.framing.targetOnScreen.localY + 38);
  69. };
  70. await panel.locator('summary').click();
  71. await page.waitForTimeout(4500);
  72. await clickTarget();
  73. await expect(page.locator('.el-message--warning')).toBeVisible({ timeout: 10000 });
  74. await expect(panel).toHaveAttribute('open', '');
  75. await expect(page.locator('.el-message')).toHaveCount(1);
  76. await expect(page.locator('.training-feedback.is-visible.is-error')).toHaveCount(0);
  77. await expect(page.locator('.el-message')).not.toContainText('尚未执行实际三维操作');
  78. assert.equal((await diagnostic()).server.completed.length, 0);
  79. await page.screenshot({ path: `${output}/preparation-required.png`, fullPage: true });
  80. checked('Missing preparation opens controls, emits one accurate warning, and does not advance');
  81. const raw = task.data.definitionSnapshot.steps[0];
  82. await panel.locator(`[data-tool="${raw.toolId}"]`).check();
  83. for (const checkbox of await panel.locator('[data-safety]').all()) await checkbox.check();
  84. await panel.locator('[data-measurement]').fill('测试观察记录:液压压力归零,个人防护齐全,危险区域无人员。');
  85. await panel.locator('[data-save-preparation]').click();
  86. await expect(panel.locator('[data-save-status]')).toContainText('本次预览');
  87. await panel.locator('summary').click();
  88. const validated = page.waitForResponse(response => response.url().includes('/preview/validate') && response.request().method() === 'POST');
  89. await clickTarget();
  90. const response = await validated;
  91. assert.equal(response.status(), 200);
  92. await expect.poll(async () => (await diagnostic()).server.completed.length).toBe(1);
  93. await expect(page.locator('[data-action="next"]')).toBeEnabled();
  94. await page.screenshot({ path: `${output}/step-completed.png`, fullPage: true });
  95. checked('Prepared truck target click passes real preview validation and enables next step');
  96. await page.locator('[data-action="next"]').click();
  97. await expect(panel).not.toBeVisible();
  98. await page.locator('[data-open-preparation]').click();
  99. await expect(panel).toHaveAttribute('open', '');
  100. await expect(panel.locator('b').first()).toContainText(task.data.definitionSnapshot.steps[1].title);
  101. await expect(panel.locator('[data-tool]').first()).not.toBeChecked();
  102. checked('Next step keeps viewport clear and provides fresh preparation without previous confirmations');
  103. await page.evaluate(() => {
  104. document.documentElement.classList.add('dark');
  105. document.documentElement.dataset.theme = 'dark';
  106. document.documentElement.dataset.colorTheme = 'dark';
  107. document.documentElement.style.colorScheme = 'dark';
  108. });
  109. const colors = await panel.locator('[data-save-preparation]').evaluate(button => {
  110. const style = getComputedStyle(button); return {color:style.color,background:style.backgroundColor};
  111. });
  112. assert.notEqual(colors.color, colors.background);
  113. assert.equal(colors.color, 'rgb(5, 34, 30)', 'Dark theme uses dark text on mint action buttons');
  114. await expect(panel.locator('summary')).toBeVisible();
  115. await page.screenshot({path:`${output}/floating-dark.png`,fullPage:true});
  116. checked('Dark theme floating controls use readable surface and button colors');
  117. assert.deepEqual(pageErrors, []);
  118. } catch (error) {
  119. await page.screenshot({ path: `${output}/failure.png`, fullPage: true });
  120. throw error;
  121. } finally {
  122. fs.writeFileSync(`${output}/checks.json`, JSON.stringify({ checks, pageErrors }, null, 2));
  123. const escape = value => String(value).replace(/[&<>"']/g, ch => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[ch]));
  124. fs.writeFileSync(`${output}/index.html`, `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>虚拟仿真浮窗与空间布局验证</title><style>body{max-width:1100px;margin:32px auto;padding:0 20px;font:15px/1.7 system-ui;color:#163d37}img{width:100%;border:1px solid #cadcd7}h2{margin-top:32px}li{margin:8px 0}</style><h1>虚拟仿真浮窗与空间布局验证</h1><p>2026-09-13 · 本地 6180 前端与真实教学接口 · 内部维护账号 · 任务 81 教员预览。未创建教学任务或学员成绩。</p><p>共 ${checks.length} 项通过;页面运行错误 ${pageErrors.length} 项。本报告记录本轮实际完成的浏览器检查,未写入学员成绩。</p><ul>${checks.map(check=>`<li>通过:${escape(check.name)}</li>`).join('')}</ul><h2>操作顺序</h2><p>打开操作准备 → 选择数字压力表 → 确认本步安全条件 → 填写测量观察记录 → 保存准备记录 → 点击高亮卡车模型 → 下一步。测量记录应按实际观察填写。</p>${['workspace-1920','floating-1920','workspace-1440','floating-1440','workspace-1366','floating-1366','preparation-required','step-completed'].map(name=>`<h2>${name}</h2><img src="${name}.png" alt="${name}">`).join('')}</html>`);
  125. await browser.close();
  126. }