| @@ -0,0 +1 @@ | |||||
| <!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>数字人 APPID + Secret 简化验证</title><style>body{font:16px/1.7 system-ui;max-width:1200px;margin:32px auto;color:#163d35}h1{font-size:26px}li{margin:5px 0}img{width:100%;border:1px solid #ccdcd5}section{margin-top:28px}</style><h1>数字人 APPID + Secret 简化验证</h1><p>2026-09-09 · 开发环境真实联调 · 18 项通过</p><p>移除两个额外密钥文件及运行依赖;现有 Secret 迁移至接口配置表 app_secret。继续使用现有登录身份调用 Auth,数字人开放服务通过 APPID + Secret 换取短期令牌。</p><ul><li>✓ integration-secret.key 已移除</li><li>✓ integration-internal.key 已移除</li><li>✓ 接口配置列表不返回 Secret 字段</li><li>✓ 原有 APPID 和密钥配置保留</li><li>✓ 留空 Secret 保存配置成功且不回显密钥</li><li>✓ 移除密钥文件后 APPID + Secret 连接测试成功</li><li>✓ 内部调用继续要求现有登录身份</li><li>✓ 复用登录身份获取运行令牌,无额外服务密钥</li><li>✓ Tran → Auth → 数字人系统能力调用成功</li><li>✓ 授权资源 videos 查询成功</li><li>✓ 授权资源 video-folders 查询成功</li><li>✓ 授权资源 avatars 查询成功</li><li>✓ 授权资源 voices 查询成功</li><li>✓ 授权资源 backgrounds 查询成功</li><li>✓ 配置弹窗密钥留空,仅显示已配置状态</li><li>✓ 页面留空密钥保存成功</li><li>✓ 页面连接检查实际响应成功</li><li>✓ 页面连接测试成功</li></ul><p>后端 mvn clean package 通过;新增认证、密钥保存和转发测试。039 迁移的密文保护、正常迁移、重复执行已验证。原有应用保持不变;本轮未重新创建视频生成任务。</p><section><h2>配置弹窗</h2><img src="01-configuration.png"></section><section><h2>真实连接测试</h2><img src="02-connection.png"></section></html> | |||||
| @@ -0,0 +1,77 @@ | |||||
| { | |||||
| "time": "2026-09-09T15:49:58.729Z", | |||||
| "checks": [ | |||||
| { | |||||
| "name": "integration-secret.key 已移除", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "integration-internal.key 已移除", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "接口配置列表不返回 Secret 字段", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "原有 APPID 和密钥配置保留", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "留空 Secret 保存配置成功且不回显密钥", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "移除密钥文件后 APPID + Secret 连接测试成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "内部调用继续要求现有登录身份", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "复用登录身份获取运行令牌,无额外服务密钥", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "Tran → Auth → 数字人系统能力调用成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "授权资源 videos 查询成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "授权资源 video-folders 查询成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "授权资源 avatars 查询成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "授权资源 voices 查询成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "授权资源 backgrounds 查询成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "配置弹窗密钥留空,仅显示已配置状态", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "页面留空密钥保存成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "页面连接检查实际响应成功", | |||||
| "result": "PASS" | |||||
| }, | |||||
| { | |||||
| "name": "页面连接测试成功", | |||||
| "result": "PASS" | |||||
| } | |||||
| ] | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| <!doctype html> | |||||
| <html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |||||
| <title>作业指导页面风格统一 · 验证报告</title> | |||||
| <style> | |||||
| body{margin:0;background:#edf4f1;color:#143d36;font:15px/1.8 system-ui,"Microsoft YaHei",sans-serif}main{max-width:1300px;margin:32px auto;padding:0 24px}header,section,figure{margin:0 0 20px;padding:24px;border:1px solid #c9ddd7;border-radius:12px;background:white}h1{margin:0;font-size:27px}h2{font-size:19px;margin:0 0 12px}p{margin:8px 0}a{color:#087963}img{display:block;width:100%;border:1px solid #d4e3dd;border-radius:7px}figcaption{font-weight:600;margin-bottom:12px}.pass{display:inline-block;border-radius:16px;background:#e0f5eb;color:#087044;padding:2px 14px}table{border-collapse:collapse;width:100%}td,th{padding:10px 14px;text-align:left;border:1px solid #d6e3dd}th{background:#f1f7f4}.note{color:#57776d;font-size:14px}nav{display:flex;gap:18px;flex-wrap:wrap}summary{cursor:pointer} | |||||
| </style> | |||||
| <main><header><span class="pass">构建与浏览器检查通过</span><h1>作业指导页面风格统一</h1><p>2026-09-09 · 数字车间 / 内容制作</p><p>参照模型制作、场景制作的系统主题,统一指导书列表、模板库、编制、新建、预览和对外接口页面。</p><nav><a href="results.json">浏览器验证结果</a><a href="build.log">构建日志</a><a href="../../tools/guide-style-review.mjs">测试脚本</a></nav></header> | |||||
| <section><h2>本次调整</h2><p>补充调整:模板库以完整白色内容卡片承载模板、分页与结构预览;对外接口的主内容、接口说明及契约区域统一使用系统白色背景。截图已更新,并重新通过构建与浏览器回归。</p><table><tr><th>页面</th><th>调整内容</th></tr> | |||||
| <tr><td>指导书列表</td><td>使用系统搜索框、状态/排序选择器、带边框表格和统一分页;搜索需明确提交,支持重置。进入编制为主要操作,详情、预览、版本、下载和删除收进“更多”,权限及禁用条件保留。</td></tr> | |||||
| <tr><td>模板库</td><td>移除重复大标题,统一页面边距、模板卡片、选中态和分页;较窄窗口改为两列,结构预览保留。</td></tr> | |||||
| <tr><td>编制、新建、预览</td><td>统一工具栏、按钮、表单、侧栏和选中态颜色。编制器按视口分配高度,内部滚动。文档纸张及插图保留原有颜色。</td></tr> | |||||
| <tr><td>对外接口</td><td>统一三栏面板、留白、输入框及按钮;长路径换行,较窄窗口将契约说明放到下方。暗色主题补齐背景与文字对比。</td></tr></table></section> | |||||
| <section><h2>验证范围及结果</h2><table><tr><th>检查</th><th>结果</th></tr> | |||||
| <tr><td>pnpm run build:类型检查、Vite 构建、模型编辑器产物检查</td><td>通过</td></tr> | |||||
| <tr><td>12 条临时指导书:明确搜索、无结果、重置、下一页/上一页</td><td>通过</td></tr> | |||||
| <tr><td>更多菜单、项目详情打开/关闭、进入编制、指导书预览、新建入口</td><td>通过</td></tr> | |||||
| <tr><td>4 个模板:选择切换、结构预览、模板预览弹窗</td><td>通过</td></tr> | |||||
| <tr><td>接口查询:执行 GET 项目列表,自检返回 200</td><td>通过</td></tr> | |||||
| <tr><td>1920×911、1366×768;军绿、科技、石墨、暗色主题截图检查</td><td>通过</td></tr> | |||||
| <tr><td>所有截取页面无整体横向溢出;无浏览器未捕获异常;局域网入口返回 200</td><td>通过</td></tr></table> | |||||
| <p class="note">使用独立、用后关闭的浏览器上下文,在其本地指导书仓库准备 STYLE-1~STYLE-12 测试数据,没有修改用户浏览器中的现有指导书。未执行发布、删除、外部写入或下载生成的全面业务回归。本轮为样式调整与主要交互回归;接口页现有查询仍是本地工程自检,不代表新增了 HTTP 服务。</p> | |||||
| <p class="note">编制器和模板预览保留现有自定义渲染及事件处理,宿主页负责系统控件和主题适配,避免样式调整影响文档编制与导出逻辑。构建保留现有大分包、Three.js URL 提示,无新增构建失败。</p></section> | |||||
| <figure><figcaption>指导书列表:统一筛选、表格和主要操作</figcaption><a href="01-list.png"><img src="01-list.png" alt="指导书列表"></a></figure> | |||||
| <figure><figcaption>更多菜单:保留全部次要操作及禁用状态</figcaption><img loading="lazy" src="02-list-actions.png" alt="列表更多菜单"></figure> | |||||
| <figure><figcaption>模板库:统一外边距、卡片、分页和结构预览</figcaption><img loading="lazy" src="04-templates.png" alt="模板库"></figure> | |||||
| <figure><figcaption>模板预览</figcaption><img loading="lazy" src="05-template-preview.png" alt="模板预览"></figure> | |||||
| <figure><figcaption>指导书编制</figcaption><img loading="lazy" src="03-editor.png" alt="指导书编制"></figure> | |||||
| <figure><figcaption>指导书编制 · 暗色主题</figcaption><img loading="lazy" src="03-editor-dark.png" alt="暗色编制器"></figure> | |||||
| <figure><figcaption>指导书动态预览:纸张、插图保持原色</figcaption><img loading="lazy" src="03-guide-preview.png" alt="指导书预览"></figure> | |||||
| <figure><figcaption>对外接口:导航、参数及契约说明</figcaption><img loading="lazy" src="06-api.png" alt="对外接口"></figure> | |||||
| <figure><figcaption>接口查询结果</figcaption><img loading="lazy" src="07-api-result.png" alt="接口查询结果"></figure> | |||||
| <figure><figcaption>暗色主题下的接口页面</figcaption><img loading="lazy" src="08-api-dark.png" alt="暗色接口"></figure> | |||||
| <figure><figcaption>较窄窗口:模板库自动调整列数</figcaption><img loading="lazy" src="10-templates-laptop.png" alt="较窄模板库"></figure> | |||||
| <figure><figcaption>新建指导书入口</figcaption><img loading="lazy" src="13-create-wizard.png" alt="新建指导书"></figure> | |||||
| <section><h2>更多主题截图</h2><nav><a href="08-api-technology.png">科技主题</a><a href="08-api-graphite.png">石墨主题</a><a href="09-api-laptop.png">接口页 · 较窄窗口</a><a href="11-templates-dark.png">模板库 · 暗色</a><a href="12-list-dark.png">列表 · 暗色</a></nav></section> | |||||
| </main></html> | |||||
| @@ -0,0 +1,94 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "name": "01-list", | |||||
| "result": "PASS", | |||||
| "screenshot": "01-list.png" | |||||
| }, | |||||
| { | |||||
| "name": "02-list-actions", | |||||
| "result": "PASS", | |||||
| "screenshot": "02-list-actions.png" | |||||
| }, | |||||
| { | |||||
| "name": "03-editor", | |||||
| "result": "PASS", | |||||
| "screenshot": "03-editor.png" | |||||
| }, | |||||
| { | |||||
| "name": "03-editor-dark", | |||||
| "result": "PASS", | |||||
| "screenshot": "03-editor-dark.png" | |||||
| }, | |||||
| { | |||||
| "name": "03-guide-preview", | |||||
| "result": "PASS", | |||||
| "screenshot": "03-guide-preview.png" | |||||
| }, | |||||
| { | |||||
| "name": "04-templates", | |||||
| "result": "PASS", | |||||
| "screenshot": "04-templates.png" | |||||
| }, | |||||
| { | |||||
| "name": "05-template-preview", | |||||
| "result": "PASS", | |||||
| "screenshot": "05-template-preview.png" | |||||
| }, | |||||
| { | |||||
| "name": "06-api", | |||||
| "result": "PASS", | |||||
| "screenshot": "06-api.png" | |||||
| }, | |||||
| { | |||||
| "name": "07-api-result", | |||||
| "result": "PASS", | |||||
| "screenshot": "07-api-result.png" | |||||
| }, | |||||
| { | |||||
| "name": "08-api-technology", | |||||
| "result": "PASS", | |||||
| "screenshot": "08-api-technology.png" | |||||
| }, | |||||
| { | |||||
| "name": "08-api-graphite", | |||||
| "result": "PASS", | |||||
| "screenshot": "08-api-graphite.png" | |||||
| }, | |||||
| { | |||||
| "name": "08-api-dark", | |||||
| "result": "PASS", | |||||
| "screenshot": "08-api-dark.png" | |||||
| }, | |||||
| { | |||||
| "name": "09-api-laptop", | |||||
| "result": "PASS", | |||||
| "screenshot": "09-api-laptop.png" | |||||
| }, | |||||
| { | |||||
| "name": "10-templates-laptop", | |||||
| "result": "PASS", | |||||
| "screenshot": "10-templates-laptop.png" | |||||
| }, | |||||
| { | |||||
| "name": "11-templates-dark", | |||||
| "result": "PASS", | |||||
| "screenshot": "11-templates-dark.png" | |||||
| }, | |||||
| { | |||||
| "name": "12-list-dark", | |||||
| "result": "PASS", | |||||
| "screenshot": "12-list-dark.png" | |||||
| }, | |||||
| { | |||||
| "name": "13-create-wizard", | |||||
| "result": "PASS", | |||||
| "screenshot": "13-create-wizard.png" | |||||
| }, | |||||
| { | |||||
| "name": "搜索/重置、详情、编制入口、模板选择/预览、GET 接口自检、主题切换、局域网入口", | |||||
| "result": "PASS" | |||||
| } | |||||
| ], | |||||
| "errors": [] | |||||
| } | |||||
| @@ -0,0 +1 @@ | |||||
| <!doctype html><meta charset="utf-8"><title>Service configuration dialog</title><style>body{max-width:1200px;margin:30px auto;font:16px/1.6 system-ui}img{max-width:100%;border:1px solid #ddd}section{margin:24px 0}</style><h1>Service configuration dialog</h1><p>2026-09-09. Local and LAN development environment. Account: root. 4 viewport checks passed: dialog stays inside viewport, form scrolls to final field, footer remains fixed and visible, cancel works. No business data modified.</p><section><h2>1366x768 / military / PASS</h2><img src="1366x768-military.png"></section><section><h2>1280x600 / dark / PASS</h2><img src="1280x600-dark.png"></section><section><h2>390x844 / military / PASS</h2><img src="390x844-military.png"></section><section><h2>844x390 / military / PASS</h2><img src="844x390-military.png"></section> | |||||
| @@ -0,0 +1,31 @@ | |||||
| { | |||||
| "checks": [ | |||||
| { | |||||
| "viewport": "1366x768", | |||||
| "theme": "military", | |||||
| "result": "PASS", | |||||
| "screenshot": "1366x768-military.png" | |||||
| }, | |||||
| { | |||||
| "viewport": "1280x600", | |||||
| "theme": "dark", | |||||
| "result": "PASS", | |||||
| "screenshot": "1280x600-dark.png" | |||||
| }, | |||||
| { | |||||
| "viewport": "390x844", | |||||
| "theme": "military", | |||||
| "result": "PASS", | |||||
| "screenshot": "390x844-military.png" | |||||
| }, | |||||
| { | |||||
| "viewport": "844x390", | |||||
| "theme": "military", | |||||
| "result": "PASS", | |||||
| "screenshot": "844x390-military.png" | |||||
| } | |||||
| ], | |||||
| "lan": "PASS", | |||||
| "account": "root", | |||||
| "dataChanges": "none" | |||||
| } | |||||
| @@ -0,0 +1,77 @@ | |||||
| import fs from 'node:fs' | |||||
| import path from 'node:path' | |||||
| import assert from 'node:assert/strict' | |||||
| import { fileURLToPath } from 'node:url' | |||||
| import { createRequire } from 'node:module' | |||||
| import { login, call } from './role-permissions-live.mjs' | |||||
| const report = fileURLToPath(new URL('../reports/digital-human-simple-auth-20260909/', import.meta.url)) | |||||
| fs.mkdirSync(report, { recursive:true }) | |||||
| const checks = [] | |||||
| function check(name, condition) { assert.ok(condition, name); checks.push({ name, result:'PASS' }); console.log(`PASS: ${name}`) } | |||||
| const clean = value => !/"(?:appSecret|secretCipher|app_secret|secret_cipher)"\s*:/.test(JSON.stringify(value)) | |||||
| let browser | |||||
| try { | |||||
| for (const name of ['integration-secret.key', 'integration-internal.key']) { | |||||
| check(`${name} 已移除`, !fs.existsSync(fileURLToPath(new URL(`../../unreal_tran_api/config/${name}`, import.meta.url)))) | |||||
| } | |||||
| const session = await login('root') | |||||
| const token = session.accessToken | |||||
| const configs = await call('auth/v1/interfaces?size=100', token) | |||||
| check('接口配置列表不返回 Secret 字段', configs.status === 200 && clean(configs.data)) | |||||
| const row = configs.data.records.find(item => item.code === 'digital-human' && item.environmentCode === 'default') | |||||
| assert.ok(row, 'Digital-human configuration exists') | |||||
| const settings = JSON.parse(row.extraConfig) | |||||
| check('原有 APPID 和密钥配置保留', settings.appId === 'AP_238179E0A10F97927EA49FA3' && settings.secretConfigured) | |||||
| const configuration = { ...row, environment:row.environmentCode, version:row.dataVersion } | |||||
| const saved = await call(`auth/v1/interfaces/${row.id}/digital-human`, token, { configuration, appId:settings.appId, appSecret:'' }, 'PUT') | |||||
| check('留空 Secret 保存配置成功且不回显密钥', saved.status === 200 && saved.code === 0 && clean(saved.data)) | |||||
| const tested = await call(`auth/v1/interfaces/${row.id}/test`, token, {}) | |||||
| check('移除密钥文件后 APPID + Secret 连接测试成功', tested.status === 200 && tested.data?.success) | |||||
| const runtimeUrl = 'http://127.0.0.1:6101/internal/v1/interfaces/digital-human/runtime' | |||||
| const anonymous = await fetch(runtimeUrl, { method:'POST' }) | |||||
| check('内部调用继续要求现有登录身份', anonymous.status === 401) | |||||
| const runtime = await fetch(runtimeUrl, { method:'POST', headers:{ Authorization:`Bearer ${token}` } }) | |||||
| const runtimeData = await runtime.json() | |||||
| check('复用登录身份获取运行令牌,无额外服务密钥', runtime.status === 200 && runtimeData.data?.accessToken && clean(runtimeData) && runtime.headers.get('cache-control') === 'no-store') | |||||
| const capabilities = await call('tran/v1/content/digital-human/capabilities', token) | |||||
| check('Tran → Auth → 数字人系统能力调用成功', capabilities.status === 200 && capabilities.data?.appId === settings.appId) | |||||
| for (const kind of ['videos', 'video-folders', 'avatars', 'voices', 'backgrounds']) { | |||||
| const response = await call(`tran/v1/content/digital-human/resources/${kind}`, token) | |||||
| check(`授权资源 ${kind} 查询成功`, response.status === 200 && response.code === 0) | |||||
| } | |||||
| const { chromium, expect } = createRequire(new URL('../package.json', import.meta.url))('@playwright/test') | |||||
| browser = await chromium.launch({ channel:'msedge', headless:true }) | |||||
| const context = await browser.newContext({ viewport:{ width:1600, height:1000 } }) | |||||
| await context.addInitScript(({ accessToken, refreshToken }) => { | |||||
| sessionStorage.setItem('unreal-tran:web:access-token:v1', accessToken) | |||||
| sessionStorage.setItem('unreal-tran:web:refresh-token:v1', refreshToken) | |||||
| }, session) | |||||
| const page = await context.newPage() | |||||
| await page.goto('http://127.0.0.1:6180/system/interfaces') | |||||
| const configRow = page.locator('.el-table__body tr').filter({ hasText:'digital-human' }).first() | |||||
| await expect(configRow).toBeVisible({ timeout:60000 }) | |||||
| await configRow.getByRole('button', { name:'编辑配置' }).click() | |||||
| const dialog = page.getByRole('dialog', { name:'编辑服务配置' }) | |||||
| const secret = dialog.locator('input[type="password"]') | |||||
| await expect(secret).toHaveValue('') | |||||
| await expect(secret).toHaveAttribute('placeholder', '已配置,留空保留现有密钥') | |||||
| check('配置弹窗密钥留空,仅显示已配置状态', true) | |||||
| await page.screenshot({ path:path.join(report, '01-configuration.png'), animations:'disabled' }) | |||||
| const saveResponse = page.waitForResponse(response => response.request().method() === 'PUT' && response.url().endsWith(`/interfaces/${row.id}/digital-human`)) | |||||
| await dialog.getByRole('button', { name:'保存配置' }).click() | |||||
| check('页面留空密钥保存成功', (await saveResponse).status() === 200) | |||||
| await expect(dialog).toBeHidden() | |||||
| const testResponse = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith(`/interfaces/${row.id}/test`)) | |||||
| await configRow.getByRole('button', { name:'连接检查' }).click() | |||||
| const testedFromPage = await (await testResponse).json() | |||||
| check('页面连接检查实际响应成功', testedFromPage.data?.success) | |||||
| await expect(configRow.getByText('连接正常', { exact:true })).toBeVisible({ timeout:30000 }) | |||||
| await expect(page.locator('.el-loading-mask:visible')).toHaveCount(0) | |||||
| await page.screenshot({ path:path.join(report, '02-connection.png'), animations:'disabled' }) | |||||
| check('页面连接测试成功', true) | |||||
| } finally { | |||||
| await browser?.close() | |||||
| fs.writeFileSync(path.join(report, 'results.json'), JSON.stringify({ time:new Date().toISOString(), checks }, null, 2)) | |||||
| } | |||||
| fs.writeFileSync(path.join(report, 'index.html'), `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><title>数字人 APPID + Secret 简化验证</title><style>body{font:16px/1.7 system-ui;max-width:1200px;margin:32px auto;color:#163d35}h1{font-size:26px}li{margin:5px 0}img{width:100%;border:1px solid #ccdcd5}section{margin-top:28px}</style><h1>数字人 APPID + Secret 简化验证</h1><p>2026-09-09 · 开发环境真实联调 · ${checks.length} 项通过</p><p>移除两个额外密钥文件及运行依赖;现有 Secret 迁移至接口配置表 app_secret。继续使用现有登录身份调用 Auth,数字人开放服务通过 APPID + Secret 换取短期令牌。</p><ul>${checks.map(x=>`<li>✓ ${x.name}</li>`).join('')}</ul><p>后端 mvn clean package 通过;新增认证、密钥保存和转发测试。039 迁移的密文保护、正常迁移、重复执行已验证。原有应用保持不变;本轮未重新创建视频生成任务。</p><section><h2>配置弹窗</h2><img src="01-configuration.png"></section><section><h2>真实连接测试</h2><img src="02-connection.png"></section></html>`) | |||||
| @@ -0,0 +1,114 @@ | |||||
| import fs from 'node:fs' | |||||
| import path from 'node:path' | |||||
| import { createRequire } from 'node:module' | |||||
| import { fileURLToPath } from 'node:url' | |||||
| import assert from 'node:assert/strict' | |||||
| import { login } from './role-permissions-live.mjs' | |||||
| const { chromium, expect } = createRequire(new URL('../package.json', import.meta.url))('@playwright/test') | |||||
| const report = fileURLToPath(new URL('../reports/guide-style-20260909/', import.meta.url)) | |||||
| fs.mkdirSync(report, { recursive:true }) | |||||
| const browser = await chromium.launch({ channel:'msedge', headless:true, args:['--enable-unsafe-swiftshader'] }) | |||||
| const checks = [], errors = [] | |||||
| const session = await login('root') | |||||
| const context = await browser.newContext({ viewport:{ width:1920, height:911 } }) | |||||
| await context.addInitScript(({ accessToken, refreshToken }) => { | |||||
| sessionStorage.setItem('unreal-tran:web:access-token:v1', accessToken) | |||||
| sessionStorage.setItem('unreal-tran:web:refresh-token:v1', refreshToken) | |||||
| }, session) | |||||
| const page = await context.newPage() | |||||
| page.on('pageerror', error => errors.push(error.message)) | |||||
| async function shot(name) { | |||||
| const overflow = await page.locator('.legacy-guide-page').evaluate(el => el.scrollWidth > el.clientWidth + 2) | |||||
| assert.equal(overflow, false, `${name}: page has no horizontal overflow`) | |||||
| await page.screenshot({ path:path.join(report, `${name}.png`), animations:'disabled' }) | |||||
| checks.push({ name, result:'PASS', screenshot:`${name}.png` }) | |||||
| } | |||||
| async function theme(mode) { | |||||
| await page.evaluate(async mode => (await import('/src/stores/preferences.ts')).usePreferencesStore().setThemeMode(mode), mode) | |||||
| } | |||||
| try { | |||||
| await page.goto('http://127.0.0.1:6180/content/ofd') | |||||
| await expect(page.locator('.guide-project-list')).toBeVisible({ timeout:60000 }) | |||||
| await theme('military') | |||||
| // Fixtures exist only in this disposable browser's local guide repository. | |||||
| await page.evaluate(async () => { | |||||
| const { legacyGuideContentStore } = await import('/src/features/guide-legacy/runtime.ts') | |||||
| for (let i=1;i<=12;i++) legacyGuideContentStore.createOFDProject({ id:`STYLE-${i}`, code:`STYLE-${String(i).padStart(3,'0')}`, title:`液压系统检查作业指导书 · 样式验证 ${i}`, author:'样式验证' }) | |||||
| }) | |||||
| await expect(page.locator('.guide-project-title')).toHaveCount(10) | |||||
| console.log('List loaded', await page.locator('.guide-project-title').count()) | |||||
| await shot('01-list') | |||||
| const count = await page.locator('.guide-project-title').count() | |||||
| await page.getByRole('textbox', { name:'搜索指导书', exact:true }).fill('NO-MATCH-GUIDE-STYLE') | |||||
| assert.equal(await page.locator('.guide-project-title').count(), count, 'Search waits for explicit submission') | |||||
| await page.locator('.guide-project-list').getByRole('button', { name:'搜索', exact:true }).click() | |||||
| await expect(page.locator('.guide-project-title')).toHaveCount(0) | |||||
| await page.getByRole('button', { name:'重置', exact:true }).click() | |||||
| await expect(page.locator('.guide-project-title')).toHaveCount(count) | |||||
| await page.locator('.system-pagination .btn-next').click() | |||||
| await expect(page.locator('.guide-project-title')).toHaveCount(2) | |||||
| await page.locator('.system-pagination .btn-prev').click() | |||||
| await expect(page.locator('.guide-project-title')).toHaveCount(10) | |||||
| if (count) { | |||||
| await page.locator('.guide-project-actions').first().getByRole('button', { name:'更多' }).click() | |||||
| await expect(page.getByRole('menuitem', { name:'项目详情', exact:true })).toBeVisible() | |||||
| await shot('02-list-actions') | |||||
| await page.getByRole('menuitem', { name:'项目详情', exact:true }).click() | |||||
| await expect(page.getByRole('button', { name:'关闭项目详情' })).toBeVisible() | |||||
| await page.getByRole('button', { name:'关闭项目详情' }).click() | |||||
| await page.locator('.guide-project-actions').first().getByRole('button', { name:'进入编制', exact:true }).click() | |||||
| await expect(page.locator('.ofd-view-editor')).toBeVisible({ timeout:60000 }) | |||||
| await shot('03-editor') | |||||
| await theme('dark') | |||||
| await shot('03-editor-dark') | |||||
| await theme('military') | |||||
| await page.locator('.ofd-top-actions [data-action="open-preview"]').click() | |||||
| await expect(page.locator('.ofd-workspace-dialog.preview')).toBeVisible() | |||||
| await shot('03-guide-preview') | |||||
| console.log('Editor loaded', page.url()) | |||||
| } | |||||
| await page.goto('http://127.0.0.1:6180/content/ofd/templates') | |||||
| await expect(page.locator('.guide-tpl-card')).toHaveCount(4, { timeout:30000 }) | |||||
| await page.locator('[data-guide-tpl-action="select"]').nth(1).click() | |||||
| await expect(page.locator('.guide-tpl-card').nth(1)).toHaveClass(/is-active/) | |||||
| await shot('04-templates') | |||||
| await page.locator('.guide-tpl-card').nth(1).getByRole('button', { name:'预览', exact:true }).click() | |||||
| await expect(page.locator('.guide-tpl-modal')).toBeVisible() | |||||
| await shot('05-template-preview') | |||||
| await page.goto('http://127.0.0.1:6180/content/ofd/api') | |||||
| await expect(page.locator('.ofd-api-layout')).toBeVisible({ timeout:30000 }) | |||||
| await shot('06-api') | |||||
| await page.getByRole('button', { name:'执行调用', exact:true }).click() | |||||
| await expect(page.locator('.ofd-api-result')).toBeVisible() | |||||
| await expect(page.locator('.ofd-api-result header b')).toHaveText('200') | |||||
| await shot('07-api-result') | |||||
| for (const mode of ['technology','graphite','dark']) { | |||||
| await theme(mode) | |||||
| await shot(`08-api-${mode}`) | |||||
| } | |||||
| await page.setViewportSize({ width:1366, height:768 }) | |||||
| await theme('military') | |||||
| await shot('09-api-laptop') | |||||
| await page.goto('http://127.0.0.1:6180/content/ofd/templates') | |||||
| await expect(page.locator('.guide-tpl-card')).toHaveCount(4) | |||||
| await shot('10-templates-laptop') | |||||
| await theme('dark') | |||||
| await shot('11-templates-dark') | |||||
| await page.goto('http://127.0.0.1:6180/content/ofd') | |||||
| await expect(page.locator('.guide-project-list')).toBeVisible() | |||||
| await shot('12-list-dark') | |||||
| await theme('military') | |||||
| await page.getByRole('button', { name:'新建指导书', exact:true }).click() | |||||
| await expect(page.locator('.ofd-app')).toBeVisible() | |||||
| await shot('13-create-wizard') | |||||
| assert.equal((await context.request.get('http://192.168.31.168:6180/')).status(),200) | |||||
| assert.deepEqual(errors, [], 'No unhandled browser exceptions') | |||||
| checks.push({ name:'搜索/重置、详情、编制入口、模板选择/预览、GET 接口自检、主题切换、局域网入口', result:'PASS' }) | |||||
| } catch(error) { | |||||
| await page.screenshot({ path:path.join(report,'failure.png') }).catch(()=>{}) | |||||
| console.error('Browser errors:',errors) | |||||
| throw error | |||||
| } finally { | |||||
| fs.writeFileSync(path.join(report,'results.json'), JSON.stringify({ checks, errors },null,2)) | |||||
| await browser.close() | |||||
| } | |||||