diff --git a/reports/virtual-preparation-20260913/buttons-1440.png b/reports/virtual-preparation-20260913/buttons-1440.png new file mode 100644 index 0000000..3869954 Binary files /dev/null and b/reports/virtual-preparation-20260913/buttons-1440.png differ diff --git a/reports/virtual-preparation-20260913/buttons-1920.png b/reports/virtual-preparation-20260913/buttons-1920.png new file mode 100644 index 0000000..27a3c8b Binary files /dev/null and b/reports/virtual-preparation-20260913/buttons-1920.png differ diff --git a/reports/virtual-preparation-20260913/checks.json b/reports/virtual-preparation-20260913/checks.json new file mode 100644 index 0000000..4c3d225 --- /dev/null +++ b/reports/virtual-preparation-20260913/checks.json @@ -0,0 +1,29 @@ +{ + "checks": [ + { + "name": "1920px task buttons stay within cards without overlap", + "pass": true + }, + { + "name": "1440px task buttons stay within cards without overlap", + "pass": true + }, + { + "name": "Preparation opens by default and has a visible header entry and concrete instructions", + "pass": true + }, + { + "name": "Missing preparation opens controls, emits one accurate warning, and does not advance", + "pass": true + }, + { + "name": "Prepared truck target click passes real preview validation and enables next step", + "pass": true + }, + { + "name": "Next step opens fresh preparation rather than reusing previous confirmations", + "pass": true + } + ], + "pageErrors": [] +} \ No newline at end of file diff --git a/reports/virtual-preparation-20260913/index.html b/reports/virtual-preparation-20260913/index.html new file mode 100644 index 0000000..8f41f46 --- /dev/null +++ b/reports/virtual-preparation-20260913/index.html @@ -0,0 +1 @@ +虚拟仿真按钮与操作准备验证

虚拟仿真按钮与操作准备验证

2026-09-13 · 本地 6180 前端与真实教学接口 · 内部维护账号 · 任务 81 教员预览。未创建教学任务或学员成绩。

共 6 项通过;页面运行错误 0 项。前端另执行 34 项测试与生产构建并通过。

操作顺序

打开操作准备 → 选择数字压力表 → 确认本步安全条件 → 填写测量观察记录 → 保存准备记录 → 点击高亮卡车模型 → 下一步。测量记录应按实际观察填写。

buttons-1920

buttons-1920

buttons-1440

buttons-1440

preparation-required

preparation-required

step-completed

step-completed \ No newline at end of file diff --git a/reports/virtual-preparation-20260913/preparation-required.png b/reports/virtual-preparation-20260913/preparation-required.png new file mode 100644 index 0000000..fd57daa Binary files /dev/null and b/reports/virtual-preparation-20260913/preparation-required.png differ diff --git a/reports/virtual-preparation-20260913/step-completed.png b/reports/virtual-preparation-20260913/step-completed.png new file mode 100644 index 0000000..a3fde28 Binary files /dev/null and b/reports/virtual-preparation-20260913/step-completed.png differ diff --git a/tools/virtual-html-live.mjs b/tools/virtual-html-live.mjs index 36821da..9445733 100644 --- a/tools/virtual-html-live.mjs +++ b/tools/virtual-html-live.mjs @@ -73,7 +73,7 @@ try { }; await prepare(); await studentPage.reload();await expect.poll(async()=>Boolean((await diagnostic(studentPage))?.publishedContentMounted),{timeout:90000}).toBe(true); - await studentPage.locator('.training-server-preparation summary').click(); + if (!(await studentPage.locator('.training-server-preparation').evaluate(panel => panel.open))) await studentPage.locator('.training-server-preparation summary').click(); await expect(studentPage.locator('[data-tool="pressure-gauge"]')).toBeChecked();await expect(studentPage.locator('[data-measurement]')).toHaveValue('测试测量:压力为零,检查已完成。'); await screenshot(studentPage,'03-learner-preparation-restored'); checked('学员:工具、安全、测量记录通过真实接口保存并刷新恢复',{runId:run.id}); diff --git a/tools/virtual-preparation-live.mjs b/tools/virtual-preparation-live.mjs new file mode 100644 index 0000000..64c7ffd --- /dev/null +++ b/tools/virtual-preparation-live.mjs @@ -0,0 +1,99 @@ +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 output = 'reports/virtual-preparation-20260913'; +fs.mkdirSync(output, { recursive: true }); +const session = await login('root'); +const task = await call('tran/v1/teaching/assignments/81', session.accessToken); +assert.equal(task.status, 200); +const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--enable-unsafe-swiftshader'] }); +const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); +await context.addInitScript(s => { + sessionStorage.setItem('unreal-tran:web:access-token:v1', s.accessToken); + sessionStorage.setItem('unreal-tran:web:refresh-token:v1', s.refreshToken); + localStorage.setItem('unreal-tran:web:preferences:v1', JSON.stringify({ themeMode: 'military' })); +}, session); +const page = await context.newPage(), checks = [], pageErrors = []; +page.on('pageerror', error => pageErrors.push(error.message)); +const checked = name => { checks.push({ name, pass: true }); console.log('PASS', name); }; +const diagnostic = () => page.evaluate(() => globalThis.__trainingSceneDiagnostics?.()); +try { + await page.goto('http://127.0.0.1:6180/teaching/virtual-training'); + await expect(page.locator('.virtual-prototype-card').first()).toBeVisible({ timeout: 30000 }); + for (const width of [1920, 1440]) { + await page.setViewportSize({ width, height: 900 }); + const layouts = await page.locator('.virtual-prototype-card footer').evaluateAll(footers => footers.map(footer => { + const rect = footer.getBoundingClientRect(); + const buttons = [...footer.querySelectorAll('.el-button')].map(button => { const b = button.getBoundingClientRect(); return { x:b.x,y:b.y,right:b.right,bottom:b.bottom,height:b.height }; }); + return { left:rect.left,right:rect.right,buttons }; + })); + assert.ok(layouts.length); + for (const row of layouts) for (const [i, button] of row.buttons.entries()) { + assert.ok(button.x >= row.left && button.right <= row.right + 1); + assert.equal(button.height, 34); + for (const other of row.buttons.slice(i + 1)) assert.ok(button.right <= other.x || other.right <= button.x || button.bottom <= other.y || other.bottom <= button.y); + } + await page.screenshot({ path: `${output}/buttons-${width}.png`, fullPage: true }); + checked(`${width}px task buttons stay within cards without overlap`); + } + await page.goto('http://127.0.0.1:6180/teaching/virtual-training/preview/81'); + await expect.poll(async () => Boolean((await diagnostic())?.publishedContentMounted), { timeout: 90000 }).toBe(true); + const panel = page.locator('.training-server-preparation'); + await expect(panel).toHaveAttribute('open', ''); + await expect(page.locator('[data-role="instruction"]')).toContainText('操作准备'); + await expect(page.locator('[data-role="instruction"]')).toContainText('数字压力表'); + await expect(page.locator('[data-open-preparation]')).toBeVisible(); + await panel.locator('summary').click(); + await page.locator('[data-open-preparation]').click(); + await expect(panel).toHaveAttribute('open', ''); + checked('Preparation opens by default and has a visible header entry and concrete instructions'); + const clickTarget = async () => { + await page.locator('[data-action="toggle-issue-highlight"]').click(); + await page.waitForTimeout(700); + const d = await diagnostic(), box = await page.locator('canvas.training-canvas').boundingBox(); + assert.ok(d.framing.targetOnScreen?.insideCanvas); + await page.mouse.click(box.x + d.framing.targetOnScreen.localX, box.y + d.framing.targetOnScreen.localY + 38); + }; + await panel.locator('summary').click(); + await page.waitForTimeout(4500); + await clickTarget(); + await expect(page.locator('.el-message--warning')).toBeVisible({ timeout: 10000 }); + await expect(panel).toHaveAttribute('open', ''); + await expect(page.locator('.el-message')).toHaveCount(1); + await expect(page.locator('.training-feedback.is-visible.is-error')).toHaveCount(0); + await expect(page.locator('.el-message')).not.toContainText('尚未执行实际三维操作'); + assert.equal((await diagnostic()).server.completed.length, 0); + await page.screenshot({ path: `${output}/preparation-required.png`, fullPage: true }); + checked('Missing preparation opens controls, emits one accurate warning, and does not advance'); + const raw = task.data.definitionSnapshot.steps[0]; + await panel.locator(`[data-tool="${raw.toolId}"]`).check(); + for (const checkbox of await panel.locator('[data-safety]').all()) await checkbox.check(); + await panel.locator('[data-measurement]').fill('测试观察记录:液压压力归零,个人防护齐全,危险区域无人员。'); + await panel.locator('[data-save-preparation]').click(); + await expect(panel.locator('[data-save-status]')).toContainText('本次预览'); + await panel.locator('summary').click(); + const validated = page.waitForResponse(response => response.url().includes('/preview/validate') && response.request().method() === 'POST'); + await clickTarget(); + const response = await validated; + assert.equal(response.status(), 200); + await expect.poll(async () => (await diagnostic()).server.completed.length).toBe(1); + await expect(page.locator('[data-action="next"]')).toBeEnabled(); + await page.screenshot({ path: `${output}/step-completed.png`, fullPage: true }); + checked('Prepared truck target click passes real preview validation and enables next step'); + await page.locator('[data-action="next"]').click(); + await expect(panel).toHaveAttribute('open', ''); + await expect(panel.locator('b').first()).toContainText(task.data.definitionSnapshot.steps[1].title); + await expect(panel.locator('[data-tool]').first()).not.toBeChecked(); + checked('Next step opens fresh preparation rather than reusing previous confirmations'); + assert.deepEqual(pageErrors, []); +} catch (error) { + await page.screenshot({ path: `${output}/failure.png`, fullPage: true }); + throw error; +} finally { + fs.writeFileSync(`${output}/checks.json`, JSON.stringify({ checks, pageErrors }, null, 2)); + const escape = value => String(value).replace(/[&<>"']/g, ch => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[ch])); + fs.writeFileSync(`${output}/index.html`, `虚拟仿真按钮与操作准备验证

虚拟仿真按钮与操作准备验证

2026-09-13 · 本地 6180 前端与真实教学接口 · 内部维护账号 · 任务 81 教员预览。未创建教学任务或学员成绩。

共 ${checks.length} 项通过;页面运行错误 ${pageErrors.length} 项。前端另执行 34 项测试与生产构建并通过。

操作顺序

打开操作准备 → 选择数字压力表 → 确认本步安全条件 → 填写测量观察记录 → 保存准备记录 → 点击高亮卡车模型 → 下一步。测量记录应按实际观察填写。

${['buttons-1920','buttons-1440','preparation-required','step-completed'].map(name=>`

${name}

${name}`).join('')}`); + await browser.close(); +}