diff --git a/api-error-messages.config.ts b/api-error-messages.config.ts new file mode 100644 index 0000000..4ec9e8f --- /dev/null +++ b/api-error-messages.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from '@playwright/test' + +// Real Vue/Element Plus in Edge; every /api/ request is intercepted by the spec. +export default defineConfig({ + testDir: './tests', + testMatch: 'api-error-messages.spec.ts', + outputDir: './reports/api-error-messages-20260906/test-results', + fullyParallel: false, + workers: 1, + retries: 0, + timeout: 60_000, + expect: { timeout: 12_000 }, + reporter: [ + ['list'], + ['html', { outputFolder: 'reports/api-error-messages-20260906/playwright', open: 'never' }], + ['json', { outputFile: 'reports/api-error-messages-20260906/results.json' }], + ], + use: { + baseURL: 'http://127.0.0.1:6180', + browserName: 'chromium', channel: 'msedge', headless: true, + locale: 'zh-CN', timezoneId: 'Asia/Shanghai', + viewport: { width: 1440, height: 1000 }, + actionTimeout: 12_000, navigationTimeout: 35_000, + screenshot: 'only-on-failure', trace: 'off', video: 'off', + }, +}) diff --git a/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png b/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png new file mode 100644 index 0000000..0d864ce Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/071e42a9c4b1b61b2a068c87f745fbde764b7e62.png differ diff --git a/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md b/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md new file mode 100644 index 0000000..5ad12ed --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/html/data/9292f5c4d6b2dfd83de339568ce711babd89154e.md @@ -0,0 +1,10 @@ +# Page snapshot + +```yaml +- status [ref=e3]: + - generic [ref=e4]: + - strong [ref=e7]: 装备数字车间 + - generic [ref=e10]: + - generic [ref=e11]: 正在进入系统 + - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候… +``` \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png b/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png new file mode 100644 index 0000000..723a12d Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/9599d34967a3fef35e0bb2b1686dca14e940d437.png differ diff --git a/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png b/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png new file mode 100644 index 0000000..5bc0d6b Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/acdc2bb53abb72efa451d16cc43612265dee61f5.png differ diff --git a/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png b/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png new file mode 100644 index 0000000..4f0f49e Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/e1c6cff12a63fad65e74a58d6db58e175a0452da.png differ diff --git a/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png b/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png new file mode 100644 index 0000000..b2aee88 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/html/data/ee4a5843da13d74b3518c8ba9b9eb38087378136.png differ diff --git a/reports/api-error-messages-20260906/authentication/html/index.html b/reports/api-error-messages-20260906/authentication/html/index.html new file mode 100644 index 0000000..5ae2de3 --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/html/index.html @@ -0,0 +1,76 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png b/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png new file mode 100644 index 0000000..9b54113 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/html/data/2b25574bd5f531457708c96732dfba86687cae2e.png differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png b/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png new file mode 100644 index 0000000..d109da8 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/html/data/4f9a3687e54a18461593aa85754af63334217de0.png differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/html/index.html b/reports/api-error-messages-20260906/authentication/login-message/html/index.html new file mode 100644 index 0000000..3807666 --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/login-message/html/index.html @@ -0,0 +1,76 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/login-message/results.json b/reports/api-error-messages-20260906/authentication/login-message/results.json new file mode 100644 index 0000000..ffecedd --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/login-message/results.json @@ -0,0 +1,130 @@ +{ + "config": { + "configFile": "E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts", + "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "forbidOnly": false, + "fullyParallel": false, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "list", + null + ], + [ + "html", + { + "outputFolder": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/html", + "open": "never" + } + ], + [ + "json", + { + "outputFile": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 300000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/login-message/test-results", + "repeatEach": 1, + "retries": 0, + "metadata": { + "actualWorkers": 1 + }, + "id": "edge-auth-mock", + "name": "edge-auth-mock", + "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "testIgnore": [], + "testMatch": [ + "zentao-auth-859-861-864.spec.ts" + ], + "timeout": 90000 + } + ], + "shard": null, + "updateSnapshots": "missing", + "updateSourceMethod": "patch", + "version": "1.55.0", + "workers": 1, + "webServer": null + }, + "suites": [ + { + "title": "zentao-auth-859-861-864.spec.ts", + "file": "zentao-auth-859-861-864.spec.ts", + "column": 0, + "line": 0, + "specs": [ + { + "title": "当前登录接口失败只显示临时 Message,自动消失且不留下登录错误条", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 90000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "edge-auth-mock", + "projectName": "edge-auth-mock", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 8478, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:32:36.419Z", + "annotations": [], + "attachments": [ + { + "name": "success-login-api-error-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\login-message\\test-results\\zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock\\attachments\\success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png" + }, + { + "name": "success-login-api-error-message-dismissed", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\login-message\\test-results\\zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock\\attachments\\success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png" + } + ] + } + ], + "status": "expected" + } + ], + "id": "52adcd77a1936aed13a9-423cf20f38d8d58278a5", + "file": "zentao-auth-859-861-864.spec.ts", + "line": 160, + "column": 1 + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2026-09-05T17:32:35.175Z", + "duration": 10788.947, + "expected": 1, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } +} \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/login-message/run.log b/reports/api-error-messages-20260906/authentication/login-message/run.log new file mode 100644 index 0000000..e6af136 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/run.log differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png new file mode 100644 index 0000000..d109da8 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message-dismissed.png differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png new file mode 100644 index 0000000..9b54113 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/screenshots/login-api-error-message.png differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png new file mode 100644 index 0000000..9b54113 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-3f0f8df181e67a1ff135d013664efb82b9b5f8b0.png differ diff --git a/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png new file mode 100644 index 0000000..d109da8 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/login-message/test-results/zentao-auth-859-861-864-当前登录接口失败只显示临时-Message,自动消失且不留下登录错误条-edge-auth-mock/attachments/success-login-api-error-message-dismissed-5f52bcb97a02d086fed56c692ffe02065362ffd1.png differ diff --git a/reports/api-error-messages-20260906/authentication/results.json b/reports/api-error-messages-20260906/authentication/results.json new file mode 100644 index 0000000..705fe15 --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/results.json @@ -0,0 +1,307 @@ +{ + "config": { + "configFile": "E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts", + "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "forbidOnly": false, + "fullyParallel": false, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "list", + null + ], + [ + "html", + { + "outputFolder": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/html", + "open": "never" + } + ], + [ + "json", + { + "outputFile": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 300000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/authentication/test-results", + "repeatEach": 1, + "retries": 0, + "metadata": { + "actualWorkers": 1 + }, + "id": "edge-auth-mock", + "name": "edge-auth-mock", + "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "testIgnore": [], + "testMatch": [ + "zentao-auth-859-861-864.spec.ts" + ], + "timeout": 90000 + } + ], + "shard": null, + "updateSnapshots": "missing", + "updateSourceMethod": "patch", + "version": "1.55.0", + "workers": 1, + "webServer": null + }, + "suites": [ + { + "title": "zentao-auth-859-861-864.spec.ts", + "file": "zentao-auth-859-861-864.spec.ts", + "column": 0, + "line": 0, + "specs": [ + { + "title": "859:部门没有可登录教员时账号选择器明确禁用且不可展开", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 90000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "edge-auth-mock", + "projectName": "edge-auth-mock", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 3850, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:30:12.947Z", + "annotations": [], + "attachments": [ + { + "name": "success-859-empty-teacher-account-disabled", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock\\attachments\\success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png" + } + ] + } + ], + "status": "expected" + } + ], + "id": "52adcd77a1936aed13a9-d7d9043f0521ae86a405", + "file": "zentao-auth-859-861-864.spec.ts", + "line": 94, + "column": 1 + }, + { + "title": "861:首次改密两次输入不一致时立即显示字段提示且不提交", + "ok": false, + "tags": [], + "tests": [ + { + "timeout": 90000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "edge-auth-mock", + "projectName": "edge-auth-mock", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "failed", + "duration": 16889, + "error": { + "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n", + "stack": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:114:23", + "location": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 114 + }, + "snippet": " 112 | await page.goto('/change-password')\n 113 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 114 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 115 | await inputs.nth(1).fill('Different#Pass9')\n 116 |\n 117 | await expect(page.getByText('两次输入的新密码不一致', { exact: true })).toBeVisible()" + }, + "errors": [ + { + "location": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 114 + }, + "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n\n 112 | await page.goto('/change-password')\n 113 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 114 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 115 | await inputs.nth(1).fill('Different#Pass9')\n 116 |\n 117 | await expect(page.getByText('两次输入的新密码不一致', { exact: true })).toBeVisible()\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:114:23" + } + ], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:30:17.307Z", + "annotations": [], + "attachments": [ + { + "name": "screenshot", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\test-failed-1.png" + }, + { + "name": "failure-screenshot", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\attachments\\failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png" + }, + { + "name": "error-context", + "contentType": "text/markdown", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock\\error-context.md" + } + ], + "errorLocation": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 114 + } + } + ], + "status": "unexpected" + } + ], + "id": "52adcd77a1936aed13a9-025f02a9349d3fb78768", + "file": "zentao-auth-859-861-864.spec.ts", + "line": 109, + "column": 1 + }, + { + "title": "864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌", + "ok": false, + "tags": [], + "tests": [ + { + "timeout": 90000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "edge-auth-mock", + "projectName": "edge-auth-mock", + "results": [ + { + "workerIndex": 1, + "parallelIndex": 0, + "status": "failed", + "duration": 16450, + "error": { + "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n", + "stack": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:128:23", + "location": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 128 + }, + "snippet": " 126 | await page.goto('/change-password')\n 127 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 128 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 129 | await inputs.nth(1).fill('Strong#Password8')\n 130 | await page.getByRole('button', { name: '确认修改并登录' }).click()\n 131 |" + }, + "errors": [ + { + "location": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 128 + }, + "message": "TimeoutError: locator.fill: Timeout 15000ms exceeded.\nCall log:\n\u001b[2m - waiting for locator('.password-card input[autocomplete=\"new-password\"]').first()\u001b[22m\n\n\n 126 | await page.goto('/change-password')\n 127 | const inputs = page.locator('.password-card input[autocomplete=\"new-password\"]')\n> 128 | await inputs.nth(0).fill('Strong#Password8')\n | ^\n 129 | await inputs.nth(1).fill('Strong#Password8')\n 130 | await page.getByRole('button', { name: '确认修改并登录' }).click()\n 131 |\n at E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts:128:23" + } + ], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:30:35.470Z", + "annotations": [], + "attachments": [ + { + "name": "screenshot", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\test-failed-1.png" + }, + { + "name": "failure-screenshot", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\attachments\\failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png" + }, + { + "name": "error-context", + "contentType": "text/markdown", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\authentication\\test-results\\zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock\\error-context.md" + } + ], + "errorLocation": { + "file": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\tests\\zentao-auth-859-861-864.spec.ts", + "column": 23, + "line": 128 + } + } + ], + "status": "unexpected" + } + ], + "id": "52adcd77a1936aed13a9-e99934e8cad148ac65c9", + "file": "zentao-auth-859-861-864.spec.ts", + "line": 123, + "column": 1 + }, + { + "title": "普通会话过期只跳转登录,不展示后端错误详情或停用账号提示", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 90000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "edge-auth-mock", + "projectName": "edge-auth-mock", + "results": [ + { + "workerIndex": 2, + "parallelIndex": 0, + "status": "passed", + "duration": 2493, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:30:53.683Z", + "annotations": [], + "attachments": [] + } + ], + "status": "expected" + } + ], + "id": "52adcd77a1936aed13a9-cdec245d1f279f3a22ad", + "file": "zentao-auth-859-861-864.spec.ts", + "line": 143, + "column": 1 + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2026-09-05T17:30:11.840Z", + "duration": 45375.929000000004, + "expected": 2, + "skipped": 0, + "unexpected": 2, + "flaky": 0 + } +} \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/run.log b/reports/api-error-messages-20260906/authentication/run.log new file mode 100644 index 0000000..fc1912a Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/run.log differ diff --git a/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png b/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png new file mode 100644 index 0000000..723a12d Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/859-passed.png differ diff --git a/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png b/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png new file mode 100644 index 0000000..0d864ce Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/861-failed.png differ diff --git a/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png b/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png new file mode 100644 index 0000000..5bc0d6b Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/screenshots/864-failed.png differ diff --git a/reports/api-error-messages-20260906/authentication/summary.json b/reports/api-error-messages-20260906/authentication/summary.json new file mode 100644 index 0000000..6273127 --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/summary.json @@ -0,0 +1,66 @@ +{ + "title": "认证提示附加回归", + "status": "PASS_WITH_LEGACY_BLOCKERS", + "result": "共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。", + "screenshotPathBase": "api-error-messages-20260906", + "environment": "Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。", + "counts": { "executed": 5, "passed": 3, "legacyBlocked": 2, "skipped": 0 }, + "runs": [ + { + "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts", + "result": "首轮运行当时文件中的 4 项旧回归,2 passed / 2 failed(均为输入定位超时),用时 45.4 秒;原始结果保留。", + "json": "authentication/results.json", + "html": "authentication/html/index.html", + "log": "authentication/run.log" + }, + { + "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts", + "result": "新增当前登录 API 错误回归独立执行,1 passed,用时 10.8 秒。", + "json": "authentication/login-message/results.json", + "html": "authentication/login-message/html/index.html", + "log": "authentication/login-message/run.log" + } + ], + "checks": [ + { + "title": "859:无可登录教员时账号选择器禁用", + "status": "PASS", + "result": "保留业务空态,选择器不可展开。", + "screenshots": [{ "title": "无教员业务空态", "path": "authentication/screenshots/859-passed.png" }] + }, + { + "title": "861:旧强制改密不一致校验", + "status": "BLOCKED", + "rawStatus": "failed", + "result": "当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。", + "screenshots": [{ "title": "旧改密入口阻断", "path": "authentication/screenshots/861-failed.png" }] + }, + { + "title": "864:旧强制改密停用账号提示", + "status": "BLOCKED", + "rawStatus": "failed", + "result": "与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。", + "screenshots": [{ "title": "旧停用账号流程阻断", "path": "authentication/screenshots/864-failed.png" }] + }, + { + "title": "普通会话过期不泄露后端内部详情", + "status": "PASS", + "result": "跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。", + "screenshots": [] + }, + { + "title": "当前管理员登录 API 401 错误使用自动消失的 Message", + "status": "PASS", + "result": "实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。", + "screenshots": [ + { "title": "登录失败 Message", "path": "authentication/login-message/screenshots/login-api-error-message.png" }, + { "title": "Message 自动消失后", "path": "authentication/login-message/screenshots/login-api-error-message-dismissed.png" } + ] + } + ], + "limitations": [ + "仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。", + "未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。", + "两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。" + ] +} diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png new file mode 100644 index 0000000..723a12d Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-859:部门没有可登录教员时账号选择器明确禁用且不可展开-edge-auth-mock/attachments/success-859-empty-teacher-account-disabled-2f99562734701ae457b390f2ea2477898d51907c.png differ diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png new file mode 100644 index 0000000..b2aee88 Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/attachments/failure-screenshot-6fd2d23a12532f4a17e77e33991ddeb1f050286e.png differ diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md new file mode 100644 index 0000000..5ad12ed --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/error-context.md @@ -0,0 +1,10 @@ +# Page snapshot + +```yaml +- status [ref=e3]: + - generic [ref=e4]: + - strong [ref=e7]: 装备数字车间 + - generic [ref=e10]: + - generic [ref=e11]: 正在进入系统 + - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候… +``` \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png new file mode 100644 index 0000000..0d864ce Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-861:首次改密两次输入不一致时立即显示字段提示且不提交-edge-auth-mock/test-failed-1.png differ diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png new file mode 100644 index 0000000..4f0f49e Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/attachments/failure-screenshot-2a08b4d1dc5dcad4acf34c6b179a1b6ae4b903de.png differ diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md new file mode 100644 index 0000000..5ad12ed --- /dev/null +++ b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/error-context.md @@ -0,0 +1,10 @@ +# Page snapshot + +```yaml +- status [ref=e3]: + - generic [ref=e4]: + - strong [ref=e7]: 装备数字车间 + - generic [ref=e10]: + - generic [ref=e11]: 正在进入系统 + - paragraph [ref=e12]: 正在验证登录状态并加载工作台,请稍候… +``` \ No newline at end of file diff --git a/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png new file mode 100644 index 0000000..5bc0d6b Binary files /dev/null and b/reports/api-error-messages-20260906/authentication/test-results/zentao-auth-859-861-864-864:首次改密期间账号停用后仅显示一次固定登录提示且不刷新令牌-edge-auth-mock/test-failed-1.png differ diff --git a/reports/api-error-messages-20260906/evidence.json b/reports/api-error-messages-20260906/evidence.json new file mode 100644 index 0000000..04d9fd0 --- /dev/null +++ b/reports/api-error-messages-20260906/evidence.json @@ -0,0 +1,625 @@ +{ + "title": "接口错误 Message 回归报告", + "status": "PASS_WITH_LEGACY_BLOCKERS", + "generatedAt": "2026-09-05T17:36:20.720Z", + "startedAt": "2026-09-05T17:32:19.650Z", + "durationMs": 84090.698, + "passed": 9, + "total": 9, + "screenshotCount": 17, + "requestCount": 58, + "pageErrorCount": 0, + "validation": { + "commands": [ + { + "command": "pnpm run build", + "scope": "unreal_tran_web", + "status": "PASS", + "details": "最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。", + "log": "validation/build.log" + }, + { + "command": "node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs", + "scope": "ute2e", + "status": "PASS", + "details": "20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。", + "log": "validation/runtime-tests.log" + }, + { + "command": "pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts", + "scope": "ute2e", + "status": "PASS", + "details": "本轮新增浏览器测试与配置类型检查通过。" + }, + { + "command": "git diff --check", + "scope": "unreal_tran_web、ute2e", + "status": "PASS", + "details": "主任务检查通过;报告生成后再次核验本轮文件。" + } + ] + }, + "authentication": { + "title": "认证提示附加回归", + "status": "PASS_WITH_LEGACY_BLOCKERS", + "result": "共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。", + "screenshotPathBase": "api-error-messages-20260906", + "environment": "Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。", + "counts": { + "executed": 5, + "passed": 3, + "legacyBlocked": 2, + "skipped": 0 + }, + "runs": [ + { + "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-message-regression.config.ts", + "result": "首轮运行当时文件中的 4 项旧回归,2 passed / 2 failed(均为输入定位超时),用时 45.4 秒;原始结果保留。", + "json": "authentication/results.json", + "html": "authentication/html/index.html", + "log": "authentication/run.log" + }, + { + "command": "pnpm exec playwright test --config E:\\work\\edit_3d\\.codex-tmp\\auth-login-message.config.ts", + "result": "新增当前登录 API 错误回归独立执行,1 passed,用时 10.8 秒。", + "json": "authentication/login-message/results.json", + "html": "authentication/login-message/html/index.html", + "log": "authentication/login-message/run.log" + } + ], + "checks": [ + { + "title": "859:无可登录教员时账号选择器禁用", + "status": "PASS", + "result": "保留业务空态,选择器不可展开。", + "screenshots": [ + { + "title": "无教员业务空态", + "path": "authentication/screenshots/859-passed.png" + } + ] + }, + { + "title": "861:旧强制改密不一致校验", + "status": "BLOCKED", + "rawStatus": "failed", + "result": "当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。", + "screenshots": [ + { + "title": "旧改密入口阻断", + "path": "authentication/screenshots/861-failed.png" + } + ] + }, + { + "title": "864:旧强制改密停用账号提示", + "status": "BLOCKED", + "rawStatus": "failed", + "result": "与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。", + "screenshots": [ + { + "title": "旧停用账号流程阻断", + "path": "authentication/screenshots/864-failed.png" + } + ] + }, + { + "title": "普通会话过期不泄露后端内部详情", + "status": "PASS", + "result": "跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。", + "screenshots": [] + }, + { + "title": "当前管理员登录 API 401 错误使用自动消失的 Message", + "status": "PASS", + "result": "实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。", + "screenshots": [ + { + "title": "登录失败 Message", + "path": "authentication/login-message/screenshots/login-api-error-message.png" + }, + { + "title": "Message 自动消失后", + "path": "authentication/login-message/screenshots/login-api-error-message-dismissed.png" + } + ] + } + ], + "limitations": [ + "仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。", + "未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。", + "两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。" + ] + }, + "stages": [ + { + "title": "资料", + "text": "按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。" + }, + { + "title": "原型", + "text": "错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。" + }, + { + "title": "设计", + "text": "共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。" + }, + { + "title": "开发", + "text": "代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。" + }, + { + "title": "测试", + "text": "使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。" + } + ], + "checks": [ + { + "title": "内容列表:错误自动消失、重试恢复、再次失败保留旧数据", + "status": "PASS", + "durationMs": 12754, + "screenshots": [ + { + "title": "01-content-error-message", + "path": "screenshots/01-content-error-message.png" + }, + { + "title": "02-content-old-data-retained", + "path": "screenshots/02-content-old-data-retained.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "系统用户:并行同错合并一条,重复搜索可再次提示", + "status": "PASS", + "durationMs": 12705, + "screenshots": [ + { + "title": "03-users-grouped-message", + "path": "screenshots/03-users-grouped-message.png" + }, + { + "title": "04-users-retry-without-banner", + "path": "screenshots/04-users-retry-without-banner.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/auth/v1/roles/all" + }, + { + "method": "GET", + "path": "/api/auth/v1/departments/tree" + }, + { + "method": "GET", + "path": "/api/auth/v1/users" + }, + { + "method": "GET", + "path": "/api/auth/v1/users/stats" + }, + { + "method": "GET", + "path": "/api/auth/v1/users" + }, + { + "method": "GET", + "path": "/api/auth/v1/users/stats" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "训练编辑器:加载失败提示消失后保留重新加载按钮", + "status": "PASS", + "durationMs": 6385, + "screenshots": [ + { + "title": "05-legacy-training-message", + "path": "screenshots/05-legacy-training-message.png" + }, + { + "title": "06-legacy-training-retry", + "path": "screenshots/06-legacy-training-retry.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/message-training-901" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "实装预览:任务上下文错误用 Message,离线与教员模式持续可见", + "status": "PASS", + "durationMs": 6450, + "screenshots": [ + { + "title": "07-physical-context-message", + "path": "screenshots/07-physical-context-message.png" + }, + { + "title": "08-physical-mode-preserved", + "path": "screenshots/08-physical-mode-preserved.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/teaching/assignments/mock-physical" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "新建训练:业务说明与必填校验保留,不发起创建请求", + "status": "PASS", + "durationMs": 2485, + "screenshots": [ + { + "title": "09-create-validation-preserved", + "path": "screenshots/09-create-validation-preserved.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "缓存列表:失活时晚到错误不弹出,返回不重放旧错误", + "status": "PASS", + "durationMs": 1947, + "screenshots": [ + { + "title": "10-inactive-page-no-message", + "path": "screenshots/10-inactive-page-no-message.png" + }, + { + "title": "11-reactivated-no-stale-message", + "path": "screenshots/11-reactivated-no-stale-message.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + }, + { + "method": "GET", + "path": "/api/auth/v1/roles/all" + }, + { + "method": "GET", + "path": "/api/auth/v1/departments/tree" + }, + { + "method": "GET", + "path": "/api/auth/v1/users" + }, + { + "method": "GET", + "path": "/api/auth/v1/users/stats" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗", + "status": "PASS", + "durationMs": 1942, + "screenshots": [ + { + "title": "12-http-silent-errors", + "path": "screenshots/12-http-silent-errors.png" + }, + { + "title": "15-business-message-keeps-original-text", + "path": "screenshots/15-business-message-keeps-original-text.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects/summary" + }, + { + "method": "GET", + "path": "/api/tran/v1/content/projects" + }, + { + "method": "GET", + "path": "/api/auth/v1/__message_e2e/timeout" + }, + { + "method": "GET", + "path": "/api/auth/v1/__message_e2e/forbidden" + }, + { + "method": "GET", + "path": "/api/auth/v1/__message_e2e/conflict" + }, + { + "method": "GET", + "path": "/api/auth/v1/__message_e2e/timeout" + }, + { + "method": "GET", + "path": "/api/auth/v1/__message_e2e/business-text" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + }, + "http": [ + { + "endpoint": "timeout", + "message": "请求超时,请稍后重试", + "code": "NETWORK_ERROR", + "status": null, + "requestId": "" + }, + { + "endpoint": "forbidden", + "message": "无权访问测试数据", + "code": "ACCESS_DENIED", + "status": 403, + "requestId": "mock-403" + }, + { + "endpoint": "conflict", + "message": "版本已变化,请重新加载", + "code": "VERSION_CONFLICT", + "status": 409, + "requestId": "mock-409" + } + ], + "httpBoundaries": { + "cancellation": { + "name": "CanceledError", + "code": "ERR_CANCELED" + }, + "businessError": { + "message": "Network Error", + "code": "ASSET_VALIDATION", + "requestId": "mock-business", + "status": 400 + } + } + }, + { + "title": "局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置", + "status": "PASS", + "durationMs": 6935, + "screenshots": [ + { + "title": "13-lan-teaching-message", + "path": "screenshots/13-lan-teaching-message.png" + }, + { + "title": "14-lan-teaching-after-dismiss", + "path": "screenshots/14-lan-teaching-after-dismiss.png" + } + ], + "isolation": { + "requests": [ + { + "method": "GET", + "path": "/api/auth/v1/system-config/public" + }, + { + "method": "GET", + "path": "/api/auth/v1/auth/me" + }, + { + "method": "GET", + "path": "/api/auth/v1/menus/navigation" + }, + { + "method": "GET", + "path": "/api/tran/v1/teaching/assignments" + }, + { + "method": "GET", + "path": "/api/tran/v1/teaching/assignments/summary" + } + ], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + }, + { + "title": "模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互", + "status": "PASS", + "durationMs": 791, + "screenshots": [ + { + "title": "16-model-bridge-neutral-retry", + "path": "screenshots/16-model-bridge-neutral-retry.png" + }, + { + "title": "17-model-bridge-ready", + "path": "screenshots/17-model-bridge-ready.png" + } + ], + "isolation": { + "requests": [], + "unexpected": [], + "writes": [], + "pageErrors": [] + } + } + ], + "limitations": [ + "这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。", + "页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。", + "模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。", + "保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。" + ], + "reproduction": "pnpm exec playwright test -c api-error-messages.config.ts" +} diff --git a/reports/api-error-messages-20260906/index.html b/reports/api-error-messages-20260906/index.html new file mode 100644 index 0000000..9e20726 --- /dev/null +++ b/reports/api-error-messages-20260906/index.html @@ -0,0 +1,5 @@ + +接口错误 Message 回归报告 +

接口错误 Message 回归报告

资料 → 原型 → 设计 → 开发 → 测试

主回归:代表性接口错误按统一 Message 展示;自动消失、重试与业务状态保留已通过浏览器验证。

认证附加:3 项通过,2 项旧改密入口受当前路由契约阻断;原始失败保留,未计为通过。

主回归 9 / 9 通过主回归 17 张截图58 次模拟 API 请求0 个未捕获页面异常
+

审查与实施

资料

按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。

原型

错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。

设计

共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。

开发

代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。

测试

使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。

浏览器验证(主回归)

PASS

1. 内容列表:错误自动消失、重试恢复、再次失败保留旧数据

12.8 秒
01-content-error-message
01-content-error-message
02-content-old-data-retained
02-content-old-data-retained
PASS

2. 系统用户:并行同错合并一条,重复搜索可再次提示

12.7 秒
03-users-grouped-message
03-users-grouped-message
04-users-retry-without-banner
04-users-retry-without-banner
PASS

3. 训练编辑器:加载失败提示消失后保留重新加载按钮

6.4 秒
05-legacy-training-message
05-legacy-training-message
06-legacy-training-retry
06-legacy-training-retry
PASS

4. 实装预览:任务上下文错误用 Message,离线与教员模式持续可见

6.5 秒
07-physical-context-message
07-physical-context-message
08-physical-mode-preserved
08-physical-mode-preserved
PASS

5. 新建训练:业务说明与必填校验保留,不发起创建请求

2.5 秒
09-create-validation-preserved
09-create-validation-preserved
PASS

6. 缓存列表:失活时晚到错误不弹出,返回不重放旧错误

1.9 秒
10-inactive-page-no-message
10-inactive-page-no-message
11-reactivated-no-stale-message
11-reactivated-no-stale-message
PASS

7. HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗

1.9 秒
12-http-silent-errors
12-http-silent-errors
15-business-message-keeps-original-text
15-business-message-keeps-original-text
PASS

8. 局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置

6.9 秒
13-lan-teaching-message
13-lan-teaching-message
14-lan-teaching-after-dismiss
14-lan-teaching-after-dismiss
PASS

9. 模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互

0.8 秒
16-model-bridge-neutral-retry
16-model-bridge-neutral-retry
17-model-bridge-ready
17-model-bridge-ready

认证附加回归(独立统计)

共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。

Edge headless,http://127.0.0.1:6180,认证 API 全部由 Playwright 拦截模拟;trace/video 关闭。

仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。

未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。

两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。

PASS

859:无可登录教员时账号选择器禁用

保留业务空态,选择器不可展开。

无教员业务空态
无教员业务空态
BLOCKED

861:旧强制改密不一致校验

当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。

Playwright 原始状态:failed。

旧改密入口阻断
旧改密入口阻断
BLOCKED

864:旧强制改密停用账号提示

与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。

Playwright 原始状态:failed。

旧停用账号流程阻断
旧停用账号流程阻断
PASS

普通会话过期不泄露后端内部详情

跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。

PASS

当前管理员登录 API 401 错误使用自动消失的 Message

实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。

登录失败 Message
登录失败 Message
Message 自动消失后
Message 自动消失后

构建与聚焦逻辑验证

PASS · unreal_tran_web
pnpm run build
最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。 日志

PASS · ute2e
node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs
20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。 日志

PASS · ute2e
pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts
本轮新增浏览器测试与配置类型检查通过。

PASS · unreal_tran_web、ute2e
git diff --check
主任务检查通过;报告生成后再次核验本轮文件。

范围与复现

这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。

页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。

模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。

保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。

pnpm exec playwright test -c api-error-messages.config.ts

报告重新生成:node tools/build-api-error-message-report.mjs

\ No newline at end of file diff --git a/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png b/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png new file mode 100644 index 0000000..94bd489 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/00f52af7460dd91a453fcee86f535438b924886c.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png b/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png new file mode 100644 index 0000000..2491173 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/0f2eafc0ded875e82e42b7e809566dee5c2fb2cd.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png b/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png new file mode 100644 index 0000000..9ccbac1 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/17507cdbdd99dd7f0d9fd151ae290df25c7a0642.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png b/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png new file mode 100644 index 0000000..ba86979 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/2813e75186e0fad9d37770a9ed59535edec099f4.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png b/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png new file mode 100644 index 0000000..8a5e92c Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/2bd9442f53c5bcf42a8e5d1575cf86e60f09727b.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png b/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png new file mode 100644 index 0000000..b6c5f5e Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3592c1eb0bc207597d80a118da25996fdf09ab67.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png b/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png new file mode 100644 index 0000000..77b55cd Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3c895f0ca75f16bf707237cf74af25adab2b8f61.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png b/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png new file mode 100644 index 0000000..0595481 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/3de3e9e62a6c772cfa010aa73d55231e84271d60.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png b/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png new file mode 100644 index 0000000..3eaf707 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/4e786aaa7c17ef4a09607c55f134c6be7d2d85b9.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png b/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png new file mode 100644 index 0000000..0da9ee9 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/67a05f8fe6a1471fd8f0c5ebf37698dcfa6f3ea1.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png b/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png new file mode 100644 index 0000000..02d4676 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/791f083567254786a0d25d46f5990309148370fc.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png b/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png new file mode 100644 index 0000000..a61e369 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/8c5e81abf643c9014dc8bcfc84cb1dfb48192ae5.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png b/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png new file mode 100644 index 0000000..1a96d4a Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/b4922029a6ab37c56716c59f6aaa50e4d3189113.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png b/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png new file mode 100644 index 0000000..cbd8b9d Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/cebc174fab800fdfa4a213bc86d68f50d2971ad3.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png b/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png new file mode 100644 index 0000000..4599717 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/d078cc25a7aec6aaa723751aaf8365a14afa242d.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png b/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png new file mode 100644 index 0000000..daead15 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/d44504e5959e7f0e0f46a63bc5fee46707b0121d.png differ diff --git a/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png b/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png new file mode 100644 index 0000000..d3fb731 Binary files /dev/null and b/reports/api-error-messages-20260906/playwright/data/e225277a4b743cee1aaeba6b1471fb1a3e09b29f.png differ diff --git a/reports/api-error-messages-20260906/playwright/index.html b/reports/api-error-messages-20260906/playwright/index.html new file mode 100644 index 0000000..f0d0a10 --- /dev/null +++ b/reports/api-error-messages-20260906/playwright/index.html @@ -0,0 +1,76 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/reports/api-error-messages-20260906/report.md b/reports/api-error-messages-20260906/report.md new file mode 100644 index 0000000..6928cec --- /dev/null +++ b/reports/api-error-messages-20260906/report.md @@ -0,0 +1,159 @@ +# 接口错误 Message 回归报告 + +主回归:**9/9 通过**。17 张截图,58 次模拟 API 请求,0 个未捕获页面异常。 + +认证附加:3 项通过,2 项旧改密入口受当前路由契约阻断。原始失败保留,未计为通过。总状态:PASS_WITH_LEGACY_BLOCKERS。 + +## 资料 + +按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。 + +## 原型 + +错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。 + +## 设计 + +共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。 + +## 开发 + +代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。 + +## 测试 + +使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。 + +## 浏览器结果 + +### 1. 内容列表:错误自动消失、重试恢复、再次失败保留旧数据 + +状态:PASS。耗时 12.8 秒。 + +![01-content-error-message](screenshots/01-content-error-message.png) + +![02-content-old-data-retained](screenshots/02-content-old-data-retained.png) + +### 2. 系统用户:并行同错合并一条,重复搜索可再次提示 + +状态:PASS。耗时 12.7 秒。 + +![03-users-grouped-message](screenshots/03-users-grouped-message.png) + +![04-users-retry-without-banner](screenshots/04-users-retry-without-banner.png) + +### 3. 训练编辑器:加载失败提示消失后保留重新加载按钮 + +状态:PASS。耗时 6.4 秒。 + +![05-legacy-training-message](screenshots/05-legacy-training-message.png) + +![06-legacy-training-retry](screenshots/06-legacy-training-retry.png) + +### 4. 实装预览:任务上下文错误用 Message,离线与教员模式持续可见 + +状态:PASS。耗时 6.5 秒。 + +![07-physical-context-message](screenshots/07-physical-context-message.png) + +![08-physical-mode-preserved](screenshots/08-physical-mode-preserved.png) + +### 5. 新建训练:业务说明与必填校验保留,不发起创建请求 + +状态:PASS。耗时 2.5 秒。 + +![09-create-validation-preserved](screenshots/09-create-validation-preserved.png) + +### 6. 缓存列表:失活时晚到错误不弹出,返回不重放旧错误 + +状态:PASS。耗时 1.9 秒。 + +![10-inactive-page-no-message](screenshots/10-inactive-page-no-message.png) + +![11-reactivated-no-stale-message](screenshots/11-reactivated-no-stale-message.png) + +### 7. HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗 + +状态:PASS。耗时 1.9 秒。 + +![12-http-silent-errors](screenshots/12-http-silent-errors.png) + +![15-business-message-keeps-original-text](screenshots/15-business-message-keeps-original-text.png) + +### 8. 局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置 + +状态:PASS。耗时 6.9 秒。 + +![13-lan-teaching-message](screenshots/13-lan-teaching-message.png) + +![14-lan-teaching-after-dismiss](screenshots/14-lan-teaching-after-dismiss.png) + +### 9. 模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互 + +状态:PASS。耗时 0.8 秒。 + +![16-model-bridge-neutral-retry](screenshots/16-model-bridge-neutral-retry.png) + +![17-model-bridge-ready](screenshots/17-model-bridge-ready.png) + +## 认证附加回归(独立统计) + +共实际运行 5 项:3 项通过;2 项旧强制改密用例受当前路由契约阻断,未验证到原断言。主报告 9 项结果单独统计。 + +### 859:无可登录教员时账号选择器禁用 + +状态:PASS。保留业务空态,选择器不可展开。 + +![无教员业务空态](authentication/screenshots/859-passed.png) + +### 861:旧强制改密不一致校验 + +状态:BLOCKED;Playwright 原始状态:failed。当前 /change-password 对已认证用户重定向 /dashboard;旧 fixture 只有强制改密标记而无工作台权限,后续导航取消,停留在启动页,未进入改密表单。该旧用例不适用于当前路由契约,本次未改路由、未跳过或记为通过。 + +![旧改密入口阻断](authentication/screenshots/861-failed.png) + +### 864:旧强制改密停用账号提示 + +状态:BLOCKED;Playwright 原始状态:failed。与 861 相同的既有路由契约阻断,未能提交初始改密接口,也未验证一次性登录提示。原始失败证据保留。 + +![旧停用账号流程阻断](authentication/screenshots/864-failed.png) + +### 普通会话过期不泄露后端内部详情 + +状态:PASS。跳转登录页,登录错误栏为空,不显示内部会话或刷新令牌详情,仅发起一次刷新请求。 + +### 当前管理员登录 API 401 错误使用自动消失的 Message + +状态:PASS。实际点击登录;mock POST /auth/login 返回 401。只出现一条 .el-message--error,#login-error 始终为空;未点击关闭,Message 自动消失。登录请求 1 次,刷新请求 0 次。 + +![登录失败 Message](authentication/login-message/screenshots/login-api-error-message.png) + +![Message 自动消失后](authentication/login-message/screenshots/login-api-error-message-dismissed.png) + +- 仅模拟认证接口与浏览器交互,不使用真实账号,不验证后端认证规则或真实密码修改。 +- 未运行依赖真实账号的 auth.spec.ts 全量;仅更新其中接口错误提示的断言选择器。 +- 两条旧强制改密用例没有通过,不能作为当前改密功能验证证据;既有路由策略不在本轮提示展示调整范围。 + +[认证汇总证据](authentication/summary.json) + +## 范围与复现 + +- 这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。 +- 页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。 +- 模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。 +- 保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。 + +pnpm run build:PASS。最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。 + +node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs:PASS。20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。 + +pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts:PASS。本轮新增浏览器测试与配置类型检查通过。 + +git diff --check:PASS。主任务检查通过;报告生成后再次核验本轮文件。 + +```powershell +pnpm exec playwright test -c api-error-messages.config.ts +node tools/build-api-error-message-report.mjs +``` + +测试开始:2026-09-05T17:32:19.650Z。报告生成:2026-09-05T17:36:20.720Z。 diff --git a/reports/api-error-messages-20260906/results.json b/reports/api-error-messages-20260906/results.json new file mode 100644 index 0000000..c346585 --- /dev/null +++ b/reports/api-error-messages-20260906/results.json @@ -0,0 +1,556 @@ +{ + "config": { + "configFile": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\api-error-messages.config.ts", + "rootDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "forbidOnly": false, + "fullyParallel": false, + "globalSetup": null, + "globalTeardown": null, + "globalTimeout": 0, + "grep": {}, + "grepInvert": null, + "maxFailures": 0, + "metadata": { + "actualWorkers": 1 + }, + "preserveOutput": "always", + "reporter": [ + [ + "list", + null + ], + [ + "html", + { + "outputFolder": "reports/api-error-messages-20260906/playwright", + "open": "never" + } + ], + [ + "json", + { + "outputFile": "reports/api-error-messages-20260906/results.json" + } + ] + ], + "reportSlowTests": { + "max": 5, + "threshold": 300000 + }, + "quiet": false, + "projects": [ + { + "outputDir": "E:/work/edit_3d/unreal_tran/ute2e/reports/api-error-messages-20260906/test-results", + "repeatEach": 1, + "retries": 0, + "metadata": { + "actualWorkers": 1 + }, + "id": "", + "name": "", + "testDir": "E:/work/edit_3d/unreal_tran/ute2e/tests", + "testIgnore": [], + "testMatch": [ + "api-error-messages.spec.ts" + ], + "timeout": 60000 + } + ], + "shard": null, + "updateSnapshots": "missing", + "updateSourceMethod": "patch", + "version": "1.55.0", + "workers": 1, + "webServer": null + }, + "suites": [ + { + "title": "api-error-messages.spec.ts", + "file": "api-error-messages.spec.ts", + "column": 0, + "line": 0, + "specs": [ + { + "title": "内容列表:错误自动消失、重试恢复、再次失败保留旧数据", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 12754, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:32:20.820Z", + "annotations": [], + "attachments": [ + { + "name": "01-content-error-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据\\attachments\\01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png" + }, + { + "name": "02-content-old-data-retained", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据\\attachments\\02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvY29udGVudC9wcm9qZWN0cy9zdW1tYXJ5IgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvY29udGVudC9wcm9qZWN0cyIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ==" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-2038675176804df1ab00", + "file": "api-error-messages.spec.ts", + "line": 129, + "column": 1 + }, + { + "title": "系统用户:并行同错合并一条,重复搜索可再次提示", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 12705, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "steps": [ + { + "title": "Expect \"poll toBe\"", + "duration": 1 + } + ], + "startTime": "2026-09-05T17:32:34.098Z", + "annotations": [], + "attachments": [ + { + "name": "03-users-grouped-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示\\attachments\\03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png" + }, + { + "name": "04-users-retry-without-banner", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示\\attachments\\04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3JvbGVzL2FsbCIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL2RlcGFydG1lbnRzL3RyZWUiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS91c2VycyIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3VzZXJzL3N0YXRzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvdXNlcnMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS91c2Vycy9zdGF0cyIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ==" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-ff7858551a9850a38ecf", + "file": "api-error-messages.spec.ts", + "line": 151, + "column": 1 + }, + { + "title": "训练编辑器:加载失败提示消失后保留重新加载按钮", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 6385, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:32:46.827Z", + "annotations": [], + "attachments": [ + { + "name": "05-legacy-training-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮\\attachments\\05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png" + }, + { + "name": "06-legacy-training-retry", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮\\attachments\\06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvbWVzc2FnZS10cmFpbmluZy05MDEiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0=" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-7ef94e791c36ee1ddedd", + "file": "api-error-messages.spec.ts", + "line": 169, + "column": 1 + }, + { + "title": "实装预览:任务上下文错误用 Message,离线与教员模式持续可见", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 6450, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:32:53.229Z", + "annotations": [], + "attachments": [ + { + "name": "07-physical-context-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见\\attachments\\07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png" + }, + { + "name": "08-physical-mode-preserved", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见\\attachments\\08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL3RlYWNoaW5nL2Fzc2lnbm1lbnRzL21vY2stcGh5c2ljYWwiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0=" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-364cf753ab5214963c3e", + "file": "api-error-messages.spec.ts", + "line": 181, + "column": 1 + }, + { + "title": "新建训练:业务说明与必填校验保留,不发起创建请求", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 2485, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:32:59.693Z", + "annotations": [], + "attachments": [ + { + "name": "09-create-validation-preserved", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求\\attachments\\09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0=" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-31297312774d5e164ef8", + "file": "api-error-messages.spec.ts", + "line": 195, + "column": 1 + }, + { + "title": "缓存列表:失活时晚到错误不弹出,返回不重放旧错误", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 1947, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "steps": [ + { + "title": "Expect \"poll toBeGreaterThan\"", + "duration": 356 + }, + { + "title": "Expect \"poll toBe\"", + "duration": 17 + } + ], + "startTime": "2026-09-05T17:33:02.191Z", + "annotations": [], + "attachments": [ + { + "name": "10-inactive-page-no-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误\\attachments\\10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png" + }, + { + "name": "11-reactivated-no-stale-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误\\attachments\\11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzL3N1bW1hcnkiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvcm9sZXMvYWxsIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvZGVwYXJ0bWVudHMvdHJlZSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL3VzZXJzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvdXNlcnMvc3RhdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzL3N1bW1hcnkiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvdHJhbi92MS9jb250ZW50L3Byb2plY3RzIgogICAgfQogIF0sCiAgInVuZXhwZWN0ZWQiOiBbXSwKICAid3JpdGVzIjogW10sCiAgInBhZ2VFcnJvcnMiOiBbXQp9" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-4825b4e93934bda5a681", + "file": "api-error-messages.spec.ts", + "line": 206, + "column": 1 + }, + { + "title": "HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 1942, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:33:04.152Z", + "annotations": [], + "attachments": [ + { + "name": "HTTP 合同证据", + "contentType": "application/json", + "body": "WwogIHsKICAgICJlbmRwb2ludCI6ICJ0aW1lb3V0IiwKICAgICJtZXNzYWdlIjogIuivt+axgui2heaXtu+8jOivt+eojeWQjumHjeivlSIsCiAgICAiY29kZSI6ICJORVRXT1JLX0VSUk9SIiwKICAgICJzdGF0dXMiOiBudWxsLAogICAgInJlcXVlc3RJZCI6ICIiCiAgfSwKICB7CiAgICAiZW5kcG9pbnQiOiAiZm9yYmlkZGVuIiwKICAgICJtZXNzYWdlIjogIuaXoOadg+iuv+mXrua1i+ivleaVsOaNriIsCiAgICAiY29kZSI6ICJBQ0NFU1NfREVOSUVEIiwKICAgICJzdGF0dXMiOiA0MDMsCiAgICAicmVxdWVzdElkIjogIm1vY2stNDAzIgogIH0sCiAgewogICAgImVuZHBvaW50IjogImNvbmZsaWN0IiwKICAgICJtZXNzYWdlIjogIueJiOacrOW3suWPmOWMlu+8jOivt+mHjeaWsOWKoOi9vSIsCiAgICAiY29kZSI6ICJWRVJTSU9OX0NPTkZMSUNUIiwKICAgICJzdGF0dXMiOiA0MDksCiAgICAicmVxdWVzdElkIjogIm1vY2stNDA5IgogIH0KXQ==" + }, + { + "name": "12-http-silent-errors", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗\\attachments\\12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png" + }, + { + "name": "15-business-message-keeps-original-text", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗\\attachments\\15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png" + }, + { + "name": "取消请求与业务原文证据", + "contentType": "application/json", + "body": "ewogICJjYW5jZWxsYXRpb24iOiB7CiAgICAibmFtZSI6ICJDYW5jZWxlZEVycm9yIiwKICAgICJjb2RlIjogIkVSUl9DQU5DRUxFRCIKICB9LAogICJidXNpbmVzc0Vycm9yIjogewogICAgIm1lc3NhZ2UiOiAiTmV0d29yayBFcnJvciIsCiAgICAiY29kZSI6ICJBU1NFVF9WQUxJREFUSU9OIiwKICAgICJyZXF1ZXN0SWQiOiAibW9jay1idXNpbmVzcyIsCiAgICAic3RhdHVzIjogNDAwCiAgfQp9" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMvc3VtbWFyeSIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL2NvbnRlbnQvcHJvamVjdHMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL3RpbWVvdXQiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL2ZvcmJpZGRlbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS9hdXRoL3YxL19fbWVzc2FnZV9lMmUvY29uZmxpY3QiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL3RpbWVvdXQiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9fX21lc3NhZ2VfZTJlL2J1c2luZXNzLXRleHQiCiAgICB9CiAgXSwKICAidW5leHBlY3RlZCI6IFtdLAogICJ3cml0ZXMiOiBbXSwKICAicGFnZUVycm9ycyI6IFtdCn0=" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-eae6f6dfb8332cc93a68", + "file": "api-error-messages.spec.ts", + "line": 235, + "column": 1 + }, + { + "title": "局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 6935, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:33:06.107Z", + "annotations": [], + "attachments": [ + { + "name": "13-lan-teaching-message", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置\\attachments\\13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png" + }, + { + "name": "14-lan-teaching-after-dismiss", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置\\attachments\\14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFsKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvc3lzdGVtLWNvbmZpZy9wdWJsaWMiCiAgICB9LAogICAgewogICAgICAibWV0aG9kIjogIkdFVCIsCiAgICAgICJwYXRoIjogIi9hcGkvYXV0aC92MS9hdXRoL21lIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL2F1dGgvdjEvbWVudXMvbmF2aWdhdGlvbiIKICAgIH0sCiAgICB7CiAgICAgICJtZXRob2QiOiAiR0VUIiwKICAgICAgInBhdGgiOiAiL2FwaS90cmFuL3YxL3RlYWNoaW5nL2Fzc2lnbm1lbnRzIgogICAgfSwKICAgIHsKICAgICAgIm1ldGhvZCI6ICJHRVQiLAogICAgICAicGF0aCI6ICIvYXBpL3RyYW4vdjEvdGVhY2hpbmcvYXNzaWdubWVudHMvc3VtbWFyeSIKICAgIH0KICBdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ==" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-bc26053607f6ace6ff92", + "file": "api-error-messages.spec.ts", + "line": 295, + "column": 1 + }, + { + "title": "模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互", + "ok": true, + "tags": [], + "tests": [ + { + "timeout": 60000, + "annotations": [], + "expectedStatus": "passed", + "projectId": "", + "projectName": "", + "results": [ + { + "workerIndex": 0, + "parallelIndex": 0, + "status": "passed", + "duration": 791, + "errors": [], + "stdout": [], + "stderr": [], + "retry": 0, + "startTime": "2026-09-05T17:33:13.059Z", + "annotations": [], + "attachments": [ + { + "name": "16-model-bridge-neutral-retry", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互\\attachments\\16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png" + }, + { + "name": "17-model-bridge-ready", + "contentType": "image/png", + "path": "E:\\work\\edit_3d\\unreal_tran\\ute2e\\reports\\api-error-messages-20260906\\test-results\\api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互\\attachments\\17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png" + }, + { + "name": "API 隔离与浏览器错误", + "contentType": "application/json", + "body": "ewogICJyZXF1ZXN0cyI6IFtdLAogICJ1bmV4cGVjdGVkIjogW10sCiAgIndyaXRlcyI6IFtdLAogICJwYWdlRXJyb3JzIjogW10KfQ==" + } + ] + } + ], + "status": "expected" + } + ], + "id": "a01d8293b6a423e3d18f-c8d259ab5093a7d5f9c8", + "file": "api-error-messages.spec.ts", + "line": 307, + "column": 1 + } + ] + } + ], + "errors": [], + "stats": { + "startTime": "2026-09-05T17:32:19.650Z", + "duration": 84090.698, + "expected": 9, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } +} \ No newline at end of file diff --git a/reports/api-error-messages-20260906/screenshots/01-content-error-message.png b/reports/api-error-messages-20260906/screenshots/01-content-error-message.png new file mode 100644 index 0000000..635c767 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/01-content-error-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png b/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png new file mode 100644 index 0000000..fb89ff7 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/02-content-old-data-retained.png differ diff --git a/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png b/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png new file mode 100644 index 0000000..2491173 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/03-users-grouped-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png b/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png new file mode 100644 index 0000000..8a5e92c Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/04-users-retry-without-banner.png differ diff --git a/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png b/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png new file mode 100644 index 0000000..0595481 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/05-legacy-training-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png b/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png new file mode 100644 index 0000000..4599717 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/06-legacy-training-retry.png differ diff --git a/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png b/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png new file mode 100644 index 0000000..9ccbac1 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/07-physical-context-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png b/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png new file mode 100644 index 0000000..0da9ee9 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/08-physical-mode-preserved.png differ diff --git a/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png b/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png new file mode 100644 index 0000000..02d4676 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/09-create-validation-preserved.png differ diff --git a/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png b/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png new file mode 100644 index 0000000..1a96d4a Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/10-inactive-page-no-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png b/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png new file mode 100644 index 0000000..ba86979 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/11-reactivated-no-stale-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png b/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png new file mode 100644 index 0000000..77b55cd Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/12-http-silent-errors.png differ diff --git a/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png b/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png new file mode 100644 index 0000000..b6c5f5e Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/13-lan-teaching-message.png differ diff --git a/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png b/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png new file mode 100644 index 0000000..94bd489 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/14-lan-teaching-after-dismiss.png differ diff --git a/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png b/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png new file mode 100644 index 0000000..a61e369 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/15-business-message-keeps-original-text.png differ diff --git a/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png b/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png new file mode 100644 index 0000000..d3fb731 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/16-model-bridge-neutral-retry.png differ diff --git a/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png b/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png new file mode 100644 index 0000000..daead15 Binary files /dev/null and b/reports/api-error-messages-20260906/screenshots/17-model-bridge-ready.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png new file mode 100644 index 0000000..77b55cd Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/12-http-silent-errors-95eb3df2d354eee7c7755066bf80544b01b9e891.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png new file mode 100644 index 0000000..a61e369 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-HTTP-合同-39274-中文化,403-409-元数据保留,静默请求不自动弹窗/attachments/15-business-message-keeps-original-text-3e1f30e539433cd6d073a3a94a6b616d1b7f9691.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png new file mode 100644 index 0000000..cbd8b9d Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/01-content-error-message-ac439d2f66329cf28022531899833b96ec5b23da.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png new file mode 100644 index 0000000..3eaf707 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-内容列表:错误自动消失、重试恢复、再次失败保留旧数据/attachments/02-content-old-data-retained-0cbf8d445ed5097f84d18d5be7de9ba814a92620.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png new file mode 100644 index 0000000..9ccbac1 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/07-physical-context-message-63fb383ff8eca8d595add99a6198d4d2882e9aaf.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png new file mode 100644 index 0000000..0da9ee9 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-实装预览:任务上下文错误用-Message,离线与教员模式持续可见/attachments/08-physical-mode-preserved-350411ad11a7f8a6727bd50289e51b171f32550b.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png new file mode 100644 index 0000000..b6c5f5e Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/13-lan-teaching-message-bf868d286d097355c25d44a190d448de0fac2dcf.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png new file mode 100644 index 0000000..94bd489 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-局域网教学列表:既有-ElMessage-error-使用全局关闭和自动消失配置/attachments/14-lan-teaching-after-dismiss-cca08e9d67eaa7575f446c040ea339316e6f8b8b.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png b/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png new file mode 100644 index 0000000..02d4676 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-新建训练:业务说明与必填校验保留,不发起创建请求/attachments/09-create-validation-preserved-4c333aadab5e6c9da93d389a099dd07c104ac11f.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png new file mode 100644 index 0000000..d3fb731 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/16-model-bridge-neutral-retry-9e8444c1b83af1d3b6be9d12b894d0ced4d17299.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png new file mode 100644 index 0000000..daead15 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-模型桥接-DOM-合同:初次错误与晚写详情转为重试说明,ready-恢复交互/attachments/17-model-bridge-ready-80cdec397323585143136b2d8ae522630c30257e.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png new file mode 100644 index 0000000..2491173 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/03-users-grouped-message-0575ee194c8ced5662a122dcc67aa169686ac8a6.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png new file mode 100644 index 0000000..8a5e92c Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-系统用户:并行同错合并一条,重复搜索可再次提示/attachments/04-users-retry-without-banner-e04f7c4569a80610fc43865d44b28f1d75fb8df9.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png new file mode 100644 index 0000000..1a96d4a Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/10-inactive-page-no-message-8860efdb888160db1c16587fccb41079a6a3face.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png new file mode 100644 index 0000000..ba86979 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-缓存列表:失活时晚到错误不弹出,返回不重放旧错误/attachments/11-reactivated-no-stale-message-d113250e56afaff950f860ca351e366466cfcdea.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png new file mode 100644 index 0000000..0595481 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/05-legacy-training-message-c8e11a1d6b9a8f408d0a84d20b4223c775b7c413.png differ diff --git a/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png new file mode 100644 index 0000000..4599717 Binary files /dev/null and b/reports/api-error-messages-20260906/test-results/api-error-messages-训练编辑器:加载失败提示消失后保留重新加载按钮/attachments/06-legacy-training-retry-ecc6b9f71cbe20b889313296c4d899241b950a74.png differ diff --git a/reports/api-error-messages-20260906/validation.json b/reports/api-error-messages-20260906/validation.json new file mode 100644 index 0000000..b4977d4 --- /dev/null +++ b/reports/api-error-messages-20260906/validation.json @@ -0,0 +1,30 @@ +{ + "commands": [ + { + "command": "pnpm run build", + "scope": "unreal_tran_web", + "status": "PASS", + "details": "最终 vue-tsc、Vite 与 legacy iframe public/dist 哈希校验通过;仍有既有 chunk 体积提示。", + "log": "validation/build.log" + }, + { + "command": "node --test tools/scene-cover-api.test.mjs tools/guide-error-message.test.mjs", + "scope": "ute2e", + "status": "PASS", + "details": "20/20 通过,包含保存失败只提示一次、阻断发布与离开,以及指导书/场景错误提示回归。", + "log": "validation/runtime-tests.log" + }, + { + "command": "pnpm exec tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --strict --types 'node,@playwright/test' tests/api-error-messages.spec.ts api-error-messages.config.ts", + "scope": "ute2e", + "status": "PASS", + "details": "本轮新增浏览器测试与配置类型检查通过。" + }, + { + "command": "git diff --check", + "scope": "unreal_tran_web、ute2e", + "status": "PASS", + "details": "主任务检查通过;报告生成后再次核验本轮文件。" + } + ] +} diff --git a/reports/api-error-messages-20260906/validation/build.log b/reports/api-error-messages-20260906/validation/build.log new file mode 100644 index 0000000..7d3c4d7 Binary files /dev/null and b/reports/api-error-messages-20260906/validation/build.log differ diff --git a/reports/api-error-messages-20260906/validation/runtime-tests.log b/reports/api-error-messages-20260906/validation/runtime-tests.log new file mode 100644 index 0000000..c8464f5 Binary files /dev/null and b/reports/api-error-messages-20260906/validation/runtime-tests.log differ diff --git a/tests/api-error-messages.spec.ts b/tests/api-error-messages.spec.ts new file mode 100644 index 0000000..ac0078d --- /dev/null +++ b/tests/api-error-messages.spec.ts @@ -0,0 +1,337 @@ +import { expect, test as base, type Page, type Route, type TestInfo } from '@playwright/test' +import fs from 'node:fs' +import path from 'node:path' + +/** UI/error-contract regression only. Synthetic authentication and API responses; + * no real accounts, real API writes, or training execution are involved. + */ +type Json = Record +const ROOT = path.resolve('reports/api-error-messages-20260906') +const SHOTS = path.join(ROOT, 'screenshots') +fs.mkdirSync(SHOTS, { recursive: true }) + +const permissions = [ + 'content.training', 'content.model', 'content.scene', 'content.ofd', 'content.create', 'content.update', 'content.publish', + 'system.users', 'system.users.create', 'system.roles', 'system.departments', + 'teaching.physical', 'teaching.virtual', 'teaching.confrontation', 'teaching.tasks.create', 'teaching.tasks.publish', +] +const roles = [ + { id: 'mock-teacher', code: 'teacher', name: '教员', shortName: '教', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'ALL', permissions }, + { id: 'mock-admin', code: 'admin', name: '系统管理员', shortName: '管', status: 1, builtIn: 1, isSuperAdmin: 0, dataScopeCode: 'ALL', permissions }, +] +const envelope = (data: unknown) => ({ code: 200, message: 'OK', data, timestamp: Date.now(), requestId: 'mock-message-regression' }) +const ok = (route: Route, data: unknown) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(envelope(data)) }) +const fail = (route: Route, message: string, status = 503, code: number | string = 'MOCK_UNAVAILABLE', requestId = 'mock-message-regression') => route.fulfill({ + status, contentType: 'application/json', body: JSON.stringify({ code, message, data: null, timestamp: Date.now(), requestId }), +}) +const pageOf = (records: unknown[]) => ({ records, total: records.length, page: 1, size: 10 }) +const project = { + id: 'message-training-901', type: 'TRAINING', code: 'MESSAGE-REGRESSION-901', name: 'Message 回归训练', + description: '浏览器隔离测试数据', categoryCode: 'MEDIUM_REPAIR', coverUri: '', status: 'DRAFT', + trainingMode: 'VIRTUAL', version: 1, currentVersionId: 'mock-version', currentVersionNo: 1, + currentVersionStatus: 'DRAFT', ownerUserId: 'mock-user', ownerName: '测试教员', departmentId: 'mock-department', + addTime: 1788624000, updateTime: 1788624000, dependencyCount: 0, assetCount: 0, + content: { level: '初级', durationMinutes: 10, totalScore: 100, steps: [] }, +} + +class ApiMock { + projectError = '' + projectDetailError = '' + systemError = '' + assignmentError = '' + assignmentDetailError = '' + holdProjects = false + readonly heldProjects: Route[] = [] + readonly requests: Array<{ method: string; path: string }> = [] + readonly unexpected: string[] = [] + readonly writes: string[] = [] + readonly pageErrors: string[] = [] + + async install(page: Page) { + page.on('pageerror', error => this.pageErrors.push(error.message)) + await page.addInitScript(() => { + sessionStorage.setItem('unreal-tran:web:access-token:v1', 'synthetic-message-test-token-not-valid') + }) + await page.route(url => url.pathname.startsWith('/api/'), async route => { + const request = route.request() + const target = new URL(request.url()).pathname + this.requests.push({ method: request.method(), path: target }) + if (request.method() !== 'GET') { + this.writes.push(`${request.method()} ${target}`) + return fail(route, '测试禁止业务写入', 405) + } + if (target === '/api/auth/v1/auth/me') return ok(route, { + user: { id: 'mock-user', username: 'message.regression', displayName: '消息回归测试', departmentId: 'mock-department', departmentName: '测试部门', mustChangePassword: false, version: 1 }, + roles, activeRoleId: 'mock-teacher', permissions, authorizationMode: 'UNION', loginTime: 1788624000, + }) + if (target === '/api/auth/v1/menus/navigation') return ok(route, []) + if (target === '/api/auth/v1/system-config/public') return ok(route, { + systemName: '装备数字车间', loginDescription: '', defaultTheme: 'light', digitalHumanWidget: null, + }) + if (target === '/api/tran/v1/content/projects/summary') return ok(route, { total: 1, draft: 1, review: 0, published: 0, byType: {}, byTrainingMode: { VIRTUAL: { total: 1, draft: 1 } } }) + if (target === '/api/tran/v1/content/projects') { + if (this.holdProjects) { this.heldProjects.push(route); return } + return this.projectError ? fail(route, this.projectError) : ok(route, pageOf([project])) + } + if (target === '/api/tran/v1/content/projects/message-training-901') return fail(route, this.projectDetailError || '测试工程详情不可用') + if (target === '/api/tran/v1/content/catalog') return ok(route, pageOf([])) + if (['/api/auth/v1/users', '/api/auth/v1/users/stats', '/api/auth/v1/departments/tree', '/api/auth/v1/roles/all'].includes(target)) { + if (this.systemError) return fail(route, this.systemError) + if (target.endsWith('/users')) return ok(route, pageOf([])) + if (target.endsWith('/stats')) return ok(route, { total: 0, enabled: 0, disabled: 0, passwordPending: 0 }) + if (target.endsWith('/roles/all')) return ok(route, roles) + return ok(route, [{ id: 'mock-department', name: '测试部门', parentId: null, status: 1, children: [] }]) + } + if (target === '/api/tran/v1/teaching/assignments') return this.assignmentError ? fail(route, this.assignmentError) : ok(route, pageOf([])) + if (target === '/api/tran/v1/teaching/assignments/summary') return ok(route, { total: 0, draft: 0, published: 0, active: 0, completed: 0, archived: 0, byChannel: {} }) + if (target === '/api/tran/v1/teaching/runs') return ok(route, pageOf([])) + if (target === '/api/tran/v1/teaching/assignments/mock-physical') return fail(route, this.assignmentDetailError || '测试任务上下文不可用') + if (target.startsWith('/api/auth/v1/__message_e2e/')) { + if (target.endsWith('/timeout')) { + await new Promise(resolve => setTimeout(resolve, 400)) + return ok(route, {}).catch(() => undefined) + } + if (target.endsWith('/forbidden')) return fail(route, '无权访问测试数据', 403, 'ACCESS_DENIED', 'mock-403') + if (target.endsWith('/conflict')) return fail(route, '版本已变化,请重新加载', 409, 'VERSION_CONFLICT', 'mock-409') + if (target.endsWith('/business-text')) return fail(route, 'Network Error', 400, 'ASSET_VALIDATION', 'mock-business') + } + this.unexpected.push(target) + return fail(route, '测试未覆盖的接口', 404) + }) + } +} + +const test = base.extend<{ api: ApiMock }>({ + api: [async ({ page }, use) => { + const api = new ApiMock() + await api.install(page) + await use(api) + }, { auto: true }], +}) + +const shot = async (page: Page, info: TestInfo, name: string) => { + const target = path.join(SHOTS, `${name}.png`) + await page.screenshot({ path: target, fullPage: true, animations: 'disabled' }) + await info.attach(name, { path: target, contentType: 'image/png' }) +} +const message = (page: Page, text: string) => page.locator('.el-message--error').filter({ hasText: text }) +const noPersistentError = async (page: Page, text: string) => { + await expect(page.locator('.el-alert--error')).toHaveCount(0) + await expect(page.locator('body')).not.toContainText(text) +} +test.afterEach(async ({ api }, info) => { + await info.attach('API 隔离与浏览器错误', { body: JSON.stringify({ requests: api.requests, unexpected: api.unexpected, writes: api.writes, pageErrors: api.pageErrors }, null, 2), contentType: 'application/json' }) + expect(api.unexpected, '所有 API 请求都有明确 mock 合同').toEqual([]) + expect(api.writes, '没有发起真实或模拟业务写入').toEqual([]) + expect(api.pageErrors, '没有未捕获浏览器异常').toEqual([]) +}) + +test('内容列表:错误自动消失、重试恢复、再次失败保留旧数据', async ({ page, api }, info) => { + const text = '测试:训练列表服务暂不可用' + api.projectError = text + await page.goto('/content/training-projects') + await expect(message(page, text)).toBeVisible() + await expect(message(page, text).locator('.el-message__closeBtn')).toBeVisible() + await shot(page, info, '01-content-error-message') + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await expect(page.getByRole('button', { name: '重新加载', exact: true })).toBeVisible() + api.projectError = '' + await page.getByRole('button', { name: '重新加载', exact: true }).click() + await expect(page.locator('.training-project-card')).toContainText(project.name) + api.projectError = text + await page.getByRole('button', { name: '刷新', exact: true }).click() + await expect(message(page, text)).toBeVisible() + await expect(page.locator('.training-project-card')).toContainText(project.name) + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await shot(page, info, '02-content-old-data-retained') +}) + +test('系统用户:并行同错合并一条,重复搜索可再次提示', async ({ page, api }, info) => { + const text = '测试:用户目录服务暂不可用' + api.systemError = text + await page.goto('/system/users') + await expect(message(page, text)).toHaveCount(1) + await expect(message(page, text)).toBeVisible() + await expect.poll(() => api.requests.filter(entry => ['/api/auth/v1/users', '/api/auth/v1/users/stats', '/api/auth/v1/departments/tree', '/api/auth/v1/roles/all'].includes(entry.path)).length).toBe(4) + await expect(message(page, text)).toHaveCount(1) + await shot(page, info, '03-users-grouped-message') + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await page.getByRole('button', { name: '搜索', exact: true }).click() + await expect(message(page, text)).toHaveCount(1) + await expect(message(page, text)).toBeVisible() + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await shot(page, info, '04-users-retry-without-banner') +}) + +test('训练编辑器:加载失败提示消失后保留重新加载按钮', async ({ page, api }, info) => { + const text = '测试:训练工程加载失败' + api.projectDetailError = text + await page.goto('/content/training-projects/message-training-901/legacy') + await expect(message(page, text)).toBeVisible() + await shot(page, info, '05-legacy-training-message') + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await expect(page.getByRole('button', { name: '重新加载', exact: true })).toBeVisible() + await shot(page, info, '06-legacy-training-retry') +}) + +test('实装预览:任务上下文错误用 Message,离线与教员模式持续可见', async ({ page, api }, info) => { + const text = '测试:实装任务上下文读取失败' + api.assignmentDetailError = text + await page.goto('/teaching/physical-training/preview/mock-physical') + await expect(message(page, text)).toBeVisible() + await expect(page.locator('.legacy-physical-page__warning')).toHaveText('离线识别回放') + await expect(page.locator('.legacy-physical-page__notice')).toContainText('教员预览态') + await shot(page, info, '07-physical-context-message') + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await expect(page.locator('.legacy-physical-page__warning')).toBeVisible() + await shot(page, info, '08-physical-mode-preserved') +}) + +test('新建训练:业务说明与必填校验保留,不发起创建请求', async ({ page }, info) => { + await page.goto('/content/training-projects') + await expect(page.locator('.training-project-card')).toBeVisible() + await page.getByRole('button', { name: '新建训练项目', exact: true }).click() + const dialog = page.getByRole('dialog') + await expect(dialog.locator('.el-alert--info')).toContainText('项目编码将在创建后由系统自动生成') + await dialog.getByRole('button', { name: '创建并编辑', exact: true }).click() + await expect(dialog.locator('.el-form-item__error').filter({ hasText: '请输入项目名称' })).toBeVisible() + await shot(page, info, '09-create-validation-preserved') +}) + +test('缓存列表:失活时晚到错误不弹出,返回不重放旧错误', async ({ page, api }, info) => { + const text = '测试:失活页面晚到错误' + api.holdProjects = true + await page.goto('/content/training-projects') + await expect.poll(() => api.heldProjects.length).toBeGreaterThan(0) + await page.evaluate(() => { + const state = window as unknown as { messageObservations: string[] } + state.messageObservations = [] + new MutationObserver(() => document.querySelectorAll('.el-message').forEach(node => state.messageObservations.push(node.textContent || ''))) + .observe(document.body, { childList: true, subtree: true }) + }) + await page.getByRole('button', { name: '系统管理', exact: true }).click() + await expect(page.locator('.users-table')).toBeVisible() + api.holdProjects = false + await Promise.all(api.heldProjects.splice(0).map(route => fail(route, text))) + await expect.poll(() => page.evaluate(async () => { + const modulePath = '/src/stores/content.ts' + const { useContentStore } = await import(modulePath) + return useContentStore().errorFor('projects:TRAINING') + })).toBe(text) + await expect(message(page, text)).toHaveCount(0) + await shot(page, info, '10-inactive-page-no-message') + await page.getByRole('tab', { name: '训练编排', exact: true }).click() + await expect(page.locator('.training-project-card')).toBeVisible() + await expect(message(page, text)).toHaveCount(0) + expect(await page.evaluate(value => (window as unknown as { messageObservations: string[] }).messageObservations.filter(entry => entry.includes(value)), text)).toEqual([]) + await shot(page, info, '11-reactivated-no-stale-message') +}) + +test('HTTP 合同:真实 Axios 超时中文化,403/409 元数据保留,静默请求不自动弹窗', async ({ page }, info) => { + await page.goto('/content/training-projects') + await expect(page.locator('.training-project-card')).toBeVisible() + const errors = await page.evaluate(async () => { + const modulePath = '/src/api/http.ts' + const { requestData } = await import(modulePath) + const result = [] + for (const endpoint of ['timeout', 'forbidden', 'conflict']) { + try { await requestData({ method: 'GET', url: `/__message_e2e/${endpoint}`, timeout: endpoint === 'timeout' ? 80 : 1_000 }) } + catch (cause) { + const error = cause as { message: string; code: string; status?: number; requestId: string } + result.push({ endpoint, message: error.message, code: error.code, status: error.status ?? null, requestId: error.requestId }) + } + } + return result + }) + expect(errors).toEqual([ + { endpoint: 'timeout', message: '请求超时,请稍后重试', code: 'NETWORK_ERROR', status: null, requestId: '' }, + { endpoint: 'forbidden', message: '无权访问测试数据', code: 'ACCESS_DENIED', status: 403, requestId: 'mock-403' }, + { endpoint: 'conflict', message: '版本已变化,请重新加载', code: 'VERSION_CONFLICT', status: 409, requestId: 'mock-409' }, + ]) + await expect(page.locator('.el-message')).toHaveCount(0) + await info.attach('HTTP 合同证据', { body: JSON.stringify(errors, null, 2), contentType: 'application/json' }) + const cancellation = await page.evaluate(async () => { + const httpPath = '/src/api/http.ts' + const messagePath = '/src/utils/errorMessage.ts' + const { requestData } = await import(httpPath) + const { showErrorMessage } = await import(messagePath) + const controller = new AbortController() + setTimeout(() => controller.abort(), 20) + try { await requestData({ method: 'GET', url: '/__message_e2e/timeout', signal: controller.signal, timeout: 1_000 }) } + catch (cause) { + showErrorMessage(cause) + const error = cause as { name: string; code: string } + return { name: error.name, code: error.code } + } + return null + }) + expect(cancellation).toEqual({ name: 'CanceledError', code: 'ERR_CANCELED' }) + await expect(page.locator('.el-message')).toHaveCount(0) + await shot(page, info, '12-http-silent-errors') + const businessError = await page.evaluate(async () => { + const httpPath = '/src/api/http.ts' + const messagePath = '/src/utils/errorMessage.ts' + const { requestData } = await import(httpPath) + const { showErrorMessage } = await import(messagePath) + try { await requestData({ method: 'GET', url: '/__message_e2e/business-text' }) } + catch (cause) { + showErrorMessage(cause) + const error = cause as { message: string; code: string; requestId: string; status: number } + return { message: error.message, code: error.code, requestId: error.requestId, status: error.status } + } + return null + }) + expect(businessError).toEqual({ message: 'Network Error', code: 'ASSET_VALIDATION', requestId: 'mock-business', status: 400 }) + await expect(message(page, 'Network Error')).toBeVisible() + await shot(page, info, '15-business-message-keeps-original-text') + await info.attach('取消请求与业务原文证据', { body: JSON.stringify({ cancellation, businessError }, null, 2), contentType: 'application/json' }) +}) + +test('局域网教学列表:既有 ElMessage.error 使用全局关闭和自动消失配置', async ({ page, api }, info) => { + const text = '测试:局域网教学列表读取失败' + api.assignmentError = text + await page.goto('http://192.168.31.168:6180/teaching/physical-training') + await expect(message(page, text)).toBeVisible() + await expect(message(page, text).locator('.el-message__closeBtn')).toBeVisible() + await shot(page, info, '13-lan-teaching-message') + await expect(message(page, text)).toBeHidden({ timeout: 8_000 }) + await noPersistentError(page, text) + await shot(page, info, '14-lan-teaching-after-dismiss') +}) + +test('模型桥接 DOM 合同:初次错误与晚写详情转为重试说明,ready 恢复交互', async ({ page }, info) => { + await page.route(url => url.pathname === '/__message_bridge_test', route => route.fulfill({ + status: 200, contentType: 'text/html', body: `模型加载桥接 DOM 测试夹具

模型加载桥接 DOM 合同

浏览器测试夹具,仅验证真实宿主桥接的错误文案、重试与交互恢复;不代表加载真实 GLB。

正在载入工程模型初始化
`, + })) + await page.goto('/__message_bridge_test') + await page.evaluate(async () => { + const state = window as unknown as { editorApp: { renderer: { domElement: Element | null } }; bridgeRetries: number } + state.editorApp = { renderer: { domElement: document.querySelector('canvas') } } + state.bridgeRetries = 0 + const modulePath = '/src/features/editors/model/legacy-iframe-loading-scope.ts' + const { installLegacyModelIframeLoadingScope } = await import(modulePath) + installLegacyModelIframeLoadingScope(document, { onRetry: () => { state.bridgeRetries += 1 } }) + document.querySelector('.editor-shell')!.dataset.modelApiState = 'error' + document.querySelector('#loading-progress')!.textContent = '测试:初次模型接口错误详情' + }) + await expect(page.locator('#loading-progress')).toHaveText('请重新加载工程') + await expect(page.locator('body')).not.toContainText('测试:初次模型接口错误详情') + expect(await page.locator('.topbar').evaluate(element => (element as HTMLElement).inert)).toBe(true) + await page.locator('#loading-progress').evaluate(element => { element.textContent = '测试:晚写模型接口错误详情' }) + await expect(page.locator('#loading-progress')).toHaveText('请重新加载工程') + await expect(page.locator('body')).not.toContainText('测试:晚写模型接口错误详情') + await page.getByRole('button', { name: '重新加载工程', exact: true }).click() + expect(await page.evaluate(() => (window as unknown as { bridgeRetries: number }).bridgeRetries)).toBe(1) + await shot(page, info, '16-model-bridge-neutral-retry') + await page.locator('.editor-shell').evaluate(element => { (element as HTMLElement).dataset.modelApiState = 'ready' }) + await expect(page.locator('#loading-overlay')).toBeHidden() + expect(await page.locator('.topbar').evaluate(element => (element as HTMLElement).inert)).toBe(false) + await expect(page.locator('.editor-shell')).toHaveAttribute('data-model-project-ready', 'true') + await expect(page.locator('.editor-shell')).toHaveAttribute('data-model-interaction-blocked', 'false') + await shot(page, info, '17-model-bridge-ready') +}) diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts index 9e0a298..c8e71ed 100644 --- a/tests/auth.spec.ts +++ b/tests/auth.spec.ts @@ -27,7 +27,7 @@ test('管理员可以登录并安全退出', async ({ page }, testInfo) => { await page.locator('input[name="username"]').fill('__ute2e_missing_account__') await page.locator('input[name="password"]').fill('Uae2e-Invalid-Only') await page.getByRole('button', { name: /登录系统/ }).click() - await expect(page.locator('.login-error')).toContainText('账号、密码或登录身份不匹配') + await expect(page.locator('.el-message--error')).toContainText('账号、密码或登录身份不匹配') await login(page, adminCredentials()) await expect(page).toHaveURL(/\/dashboard(?:\?|$)/) await captureScreenshot(page, testInfo, 'login-success') diff --git a/tests/zentao-auth-859-861-864.spec.ts b/tests/zentao-auth-859-861-864.spec.ts index 8a69282..a545c53 100644 --- a/tests/zentao-auth-859-861-864.spec.ts +++ b/tests/zentao-auth-859-861-864.spec.ts @@ -43,14 +43,19 @@ const fulfill = (route: Route, data: unknown, status = 200, message = '成功', async function mockAuth( page: Page, - options: { me?: 'forced' | 'expired'; initialPassword401?: boolean } = {}, + options: { me?: 'forced' | 'expired'; initialPassword401?: boolean; loginFailure?: boolean } = {}, ) { let refreshRequests = 0 let initialPasswordRequests = 0 + let loginRequests = 0 await page.route('**/api/auth/v1/**', async (route) => { const path = new URL(route.request().url()).pathname if (path.endsWith('/auth/login-context')) return fulfill(route, loginContext) if (path.endsWith('/auth/login-identities')) return fulfill(route, []) + if (path.endsWith('/auth/login') && route.request().method() === 'POST') { + loginRequests += 1 + if (options.loginFailure) return fulfill(route, null, 401, '账号、密码或登录身份不匹配', 40102) + } if (path.endsWith('/menus/navigation')) return fulfill(route, []) if (path.endsWith('/auth/me')) { if (options.me === 'expired') { @@ -74,6 +79,7 @@ async function mockAuth( return { refreshRequests: () => refreshRequests, initialPasswordRequests: () => initialPasswordRequests, + loginRequests: () => loginRequests, } } @@ -130,7 +136,7 @@ test('864:首次改密期间账号停用后仅显示一次固定登录提示 await page.getByRole('button', { name: '确认修改并登录' }).click() await expect(page).toHaveURL(/\/login\?redirect=/) - await expect(page.locator('#login-error')).toHaveText('账号、密码或登录身份不匹配') + await expect(page.locator('.el-message--error')).toHaveText('账号、密码或登录身份不匹配') await expect(page.getByText(/internal-user-id|refresh-token-internal-detail/)).toHaveCount(0) expect(mocked.initialPasswordRequests()).toBe(1) expect(mocked.refreshRequests()).toBe(0) @@ -150,3 +156,25 @@ test('普通会话过期只跳转登录,不展示后端错误详情或停用 await expect(page.getByText(/内部会话详情|refresh-token-internal-detail|账号、密码或登录身份不匹配/)).toHaveCount(0) expect(mocked.refreshRequests()).toBe(1) }) + +test('当前登录接口失败只显示临时 Message,自动消失且不留下登录错误条', async ({ page }, testInfo) => { + const mocked = await mockAuth(page, { loginFailure: true }) + await page.goto('/login') + await page.locator('[data-role-code="admin"]').click() + await page.locator('input[name="username"]').fill('__ute2e_message_mock__') + await page.locator('input[name="password"]').fill('Invalid#Only8') + await page.getByRole('button', { name: '登录系统', exact: true }).click() + + const message = page.locator('.el-message--error') + await expect(message).toHaveCount(1) + await expect(message).toHaveText('账号、密码或登录身份不匹配') + await expect(page.locator('#login-error')).toHaveText('') + expect(mocked.loginRequests()).toBe(1) + expect(mocked.refreshRequests()).toBe(0) + await captureScreenshot(page, testInfo, 'login-api-error-message') + + await expect(message).toHaveCount(0, { timeout: 7_000 }) + await expect(page.locator('#login-error')).toHaveText('') + await expect(page).toHaveURL(/\/login(?:\?|$)/) + await captureScreenshot(page, testInfo, 'login-api-error-message-dismissed') +}) diff --git a/tools/build-api-error-message-report.mjs b/tools/build-api-error-message-report.mjs new file mode 100644 index 0000000..6bf15cc --- /dev/null +++ b/tools/build-api-error-message-report.mjs @@ -0,0 +1,88 @@ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const root = path.join(repo, 'reports/api-error-messages-20260906') +const result = JSON.parse(fs.readFileSync(path.join(root, 'results.json'), 'utf8')) +const validationPath = path.join(root, 'validation.json') +const validation = fs.existsSync(validationPath) ? JSON.parse(fs.readFileSync(validationPath, 'utf8')) : null +const authenticationPath = path.join(root, 'authentication/summary.json') +const authentication = fs.existsSync(authenticationPath) ? JSON.parse(fs.readFileSync(authenticationPath, 'utf8')) : null +const escape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]) +const decode = attachment => { + if (!attachment?.body) return null + try { return JSON.parse(Buffer.from(attachment.body, 'base64').toString('utf8')) } catch { return null } +} +const specs = [] +function visit(suite) { + specs.push(...(suite.specs || [])) + for (const child of suite.suites || []) visit(child) +} +for (const suite of result.suites || []) visit(suite) +const checks = specs.flatMap(spec => (spec.tests || []).map(test => { + const run = test.results?.at(-1) || {} + const screenshots = (run.attachments || []).filter(item => item.contentType === 'image/png' && fs.existsSync(path.join(root, 'screenshots', `${item.name}.png`))) + .map(item => ({ title: item.name, path: `screenshots/${item.name}.png` })) + const isolation = decode((run.attachments || []).find(item => item.name === 'API 隔离与浏览器错误')) + const http = decode((run.attachments || []).find(item => item.name === 'HTTP 合同证据')) + const httpBoundaries = decode((run.attachments || []).find(item => item.name === '取消请求与业务原文证据')) + return { + title: spec.title, status: run.status === 'passed' ? 'PASS' : String(run.status || 'NOT_RUN').toUpperCase(), + durationMs: run.duration || 0, screenshots, isolation, ...(http ? { http } : {}), ...(httpBoundaries ? { httpBoundaries } : {}), + } +})) +const passed = checks.filter(item => item.status === 'PASS').length +const failed = checks.length - passed +const screenshotCount = checks.reduce((sum, item) => sum + item.screenshots.length, 0) +const requestCount = checks.reduce((sum, item) => sum + (item.isolation?.requests.length || 0), 0) +const pageErrorCount = checks.reduce((sum, item) => sum + (item.isolation?.pageErrors.length || 0), 0) +for (const check of checks) { + if (check.status === 'PASS' && !check.isolation) throw new Error(`缺少隔离证据:${check.title}`) +} +const authenticationShots = authentication?.checks?.flatMap(check => check.screenshots || []) || [] +for (const shot of authenticationShots) if (!fs.existsSync(path.join(root, shot.path))) throw new Error(`认证报告缺图:${shot.path}`) +const stages = [ + ['资料', '按本轮“接口错误使用 Message,不保留常驻错误横条”的要求检查。沿用已确认的教学业务边界:离线识别回放、观察模式、步骤反馈和表单校验仍需可见。'], + ['原型', '错误发生时顶部短暂提示;可以手动关闭,约 4.5 秒自动消失。同一错误合并展示;失败后仍可重新加载,已成功取得的数据不因后续刷新失败消失。'], + ['设计', '共享 Message 处理负责统一展示与缓存页面生命周期;请求层保留业务错误码、HTTP 状态和 requestId,静默请求不会自行弹窗。未取得数据与空结果保持区分。'], + ['开发', '代表性覆盖内容制作、系统用户与教学实施,包括训练编辑器加载失败的重试入口。接口异常详情仅在 Message 中展示;表单必填错误、项目编码说明和离线运行模式继续保留。'], + ['测试', '使用真实 Vue / Element Plus 页面、Edge 无头浏览器和浏览器级 API mock。覆盖本机与局域网入口。身份及接口数据均为合成测试数据;不登录真实账号、不写业务库,不将本轮测试称为真实服务端或设备联调。'], +] +const limitations = [ + '这是接口失败展示的聚焦浏览器回归,未逐一遍历所有业务页面。', + '页面渲染、Element Plus Message、缓存路由和 Axios 超时均运行于真实浏览器;服务端响应由测试模拟。', + '模型 iframe 项为真实宿主桥接的 DOM 合同测试,使用空画布夹具,不代表完整 GLB 模型加载测试。', + '保留训练业务状态、表单校验与重试入口。未扩大教学流程、权限或接口合同。', +] +const evidence = { + title: '接口错误 Message 回归报告', status: failed ? 'FAILED' : authentication?.counts?.legacyBlocked ? 'PASS_WITH_LEGACY_BLOCKERS' : 'PASS', + generatedAt: new Date().toISOString(), startedAt: result.stats?.startTime, + durationMs: result.stats?.duration, passed, total: checks.length, screenshotCount, requestCount, pageErrorCount, + validation, authentication, stages: stages.map(([title, text]) => ({ title, text })), checks, limitations, + reproduction: 'pnpm exec playwright test -c api-error-messages.config.ts', +} +fs.writeFileSync(path.join(root, 'evidence.json'), JSON.stringify(evidence, null, 2) + '\n', 'utf8') + +const validationText = validation ? validation.commands.map(item => `${item.command}:${item.status}。${item.details}`).join('\n\n') : '构建与源代码审查由主任务单独记录;本报告统计浏览器回归结果。' +const validationHtml = validation ? `

构建与聚焦逻辑验证

${validation.commands.map(item => `

${escape(item.status)} · ${escape(item.scope)}
${escape(item.command)}
${escape(item.details)}${item.log ? ` 日志` : ''}

`).join('')}
` : '' +const checkHtml = checks.map((check, index) => `
${escape(check.status)}

${index + 1}. ${escape(check.title)}

${(check.durationMs / 1000).toFixed(1)} 秒
${check.screenshots.map(shot => `
${escape(shot.title)}
${escape(shot.title)}
`).join('')}
`).join('') +const authenticationHtml = authentication ? `

认证附加回归(独立统计)

${escape(authentication.result)}

${escape(authentication.environment)}

${authentication.limitations.map(text => `

${escape(text)}

`).join('')}
${authentication.checks.map(check => `
${escape(check.status)}

${escape(check.title)}

${escape(check.result)}

${check.rawStatus ? `

Playwright 原始状态:${escape(check.rawStatus)}。

` : ''}
${check.screenshots.map(shot => `
${escape(shot.title)}
${escape(shot.title)}
`).join('')}
`).join('')}` : '' +fs.writeFileSync(path.join(root, 'index.html'), ` +接口错误 Message 回归报告 +

接口错误 Message 回归报告

资料 → 原型 → 设计 → 开发 → 测试

${failed ? '主回归存在未通过项,详见下方结果。' : '主回归:代表性接口错误按统一 Message 展示;自动消失、重试与业务状态保留已通过浏览器验证。'}

${authentication ? `

认证附加:${authentication.counts.passed} 项通过,${authentication.counts.legacyBlocked} 项旧改密入口受当前路由契约阻断;原始失败保留,未计为通过。

` : ''}
主回归 ${passed} / ${checks.length} 通过主回归 ${screenshotCount} 张截图${requestCount} 次模拟 API 请求${pageErrorCount} 个未捕获页面异常
+

审查与实施

${stages.map(([title, text]) => `
${title}

${escape(text)}

`).join('')}

浏览器验证(主回归)

${checkHtml}${authenticationHtml}${validationHtml}

范围与复现

${limitations.map(text => `

${escape(text)}

`).join('')}

${escape(evidence.reproduction)}

报告重新生成:node tools/build-api-error-message-report.mjs

`, 'utf8') +const md = [ + '# 接口错误 Message 回归报告', '', `主回归:**${passed}/${checks.length} 通过**。${screenshotCount} 张截图,${requestCount} 次模拟 API 请求,${pageErrorCount} 个未捕获页面异常。`, '', + ...(authentication ? [`认证附加:${authentication.counts.passed} 项通过,${authentication.counts.legacyBlocked} 项旧改密入口受当前路由契约阻断。原始失败保留,未计为通过。总状态:${evidence.status}。`, ''] : []), + ...stages.flatMap(([title, text]) => [`## ${title}`, '', text, '']), + '## 浏览器结果', '', + ...checks.flatMap((check, index) => [`### ${index + 1}. ${check.title}`, '', `状态:${check.status}。耗时 ${(check.durationMs / 1000).toFixed(1)} 秒。`, '', ...check.screenshots.flatMap(shot => [`![${shot.title}](${shot.path})`, ''])]), + ...(authentication ? ['## 认证附加回归(独立统计)', '', authentication.result, '', ...authentication.checks.flatMap(check => [`### ${check.title}`, '', `状态:${check.status}${check.rawStatus ? `;Playwright 原始状态:${check.rawStatus}` : ''}。${check.result}`, '', ...check.screenshots.flatMap(shot => [`![${shot.title}](${shot.path})`, ''])]), ...authentication.limitations.map(text => `- ${text}`), '', '[认证汇总证据](authentication/summary.json)', ''] : []), + '## 范围与复现', '', ...limitations.map(text => `- ${text}`), '', validationText, '', + '```powershell', evidence.reproduction, 'node tools/build-api-error-message-report.mjs', '```', '', + `测试开始:${evidence.startedAt}。报告生成:${evidence.generatedAt}。`, '', +] +fs.writeFileSync(path.join(root, 'report.md'), md.join('\n'), 'utf8') +console.log(JSON.stringify({ status: evidence.status, passed, total: checks.length, screenshotCount, requestCount, pageErrorCount })) diff --git a/tools/guide-error-message.test.mjs b/tools/guide-error-message.test.mjs new file mode 100644 index 0000000..7160aef --- /dev/null +++ b/tools/guide-error-message.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { readFileSync } from 'node:fs' + +const source = readFileSync(new URL('../../unreal_tran_web/src/features/guide-legacy/demo-src/ofd-render-interactive.js', import.meta.url), 'utf8') +const handlerSource = source.slice(source.indexOf(' const reportedLoadErrors = new WeakSet();'), source.indexOf(' const syncAnimationControls =')) + +function fixture(destroyed = false) { + const statuses = [] + const messages = [] + const report = new Function('destroyed', 'setStatus', 'onError', `${handlerSource}\nreturn reportLoadError`)( + destroyed, + (text, error) => statuses.push({ text, error }), + error => messages.push(error), + ) + return { report, statuses, messages } +} + +test('GLB load and ready rejection handlers report the same failure only once', () => { + const f = fixture() + const error = new Error('HTTP 503: asset service unavailable') + f.report(error) + f.report(error) + assert.deepEqual(f.messages, [error]) + assert.ok(f.statuses.length > 0) + assert.ok(f.statuses.every(status => status.error && !status.text.includes(error.message))) + assert.match(f.statuses.at(-1).text, /重新加载/) +}) + +test('retrying an unavailable GLB still reports the new failed request', () => { + const f = fixture() + f.report(new Error('HTTP 503')) + f.report(new Error('HTTP 503')) + assert.equal(f.messages.length, 2) +}) + +test('cancelled and destroyed model viewers do not surface stale load errors', () => { + const f = fixture() + f.report(Object.assign(new Error('cancelled'), { name: 'AbortError' })) + assert.equal(f.messages.length, 0) + assert.equal(f.statuses.length, 0) + const destroyed = fixture(true) + destroyed.report(new Error('late HTTP 503')) + assert.equal(destroyed.messages.length, 0) + assert.equal(destroyed.statuses.length, 0) +}) diff --git a/tools/scene-cover-api.test.mjs b/tools/scene-cover-api.test.mjs index a879331..732a739 100644 --- a/tools/scene-cover-api.test.mjs +++ b/tools/scene-cover-api.test.mjs @@ -205,3 +205,63 @@ test('changes made while cover uploads remain dirty after the earlier snapshot s assert.equal(editor.dirty, true) assert.match(editor.messages.at(-1).message, /新修改仍待保存/) }) + +const publishMethod = editorSource.slice(editorSource.indexOf(' async publishScene() {'), editorSource.indexOf(' /*\n * 原实现只设置原生 hidden')).trim() +const { publishScene } = new Function(`return ({ ${publishMethod} })`)() +function publishingEditor(saveDocument) { + const editor = editorFixture(saveDocument) + editor.canPublish = true + editor.objects = [] + editor.assetStore = { validateReferences: async () => ({ valid: true }) } + editor.verifyScene = () => [{ ok: true }] + editor.saveProject = saveProject + editor.publishCalls = 0 + editor.context.publishDocument = async () => { editor.publishCalls += 1 } + return editor +} + +const hostSource = readFileSync(new URL('../../unreal_tran_web/src/views/content/LegacyScenePageView.vue', import.meta.url), 'utf8') +const leaveSource = hostSource.slice(hostSource.indexOf('async function confirmLeave()'), hostSource.indexOf('function resetProjectSession()')) +const leaveCompiled = ts.transpileModule(leaveSource, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.None } }).outputText +function leaveEditor(editor) { + return new Function('editorHandle', 'ElMessageBox', 'ElMessage', `${leaveCompiled}\nreturn confirmLeave()`)( + editor, + { confirm: async () => undefined }, + { error: message => editor.toast(message, 'error') }, + ) +} + +for (const status of [409, 503]) { + test(`publish after PUT ${status} stays blocked and reports the save failure only once`, async () => { + const editor = publishingEditor(async () => { throw Object.assign(new Error(`HTTP ${status}`), { status }) }) + await publishScene.call(editor) + assert.equal(editor.publishCalls, 0) + assert.equal(editor.dirty, true) + assert.equal(editor.publishing, false) + assert.equal(editor.messages.length, 1) + assert.match(editor.messages[0].message, new RegExp(`HTTP ${status}`)) + }) +} + +test('save-and-leave keeps the editor open with one failure message', async () => { + const editor = publishingEditor(async () => { throw new Error('version conflict') }) + editor.dirty = true + assert.equal(await leaveEditor(editor), false) + assert.equal(editor.dirty, true) + assert.equal(editor.messages.length, 1) + assert.match(editor.messages[0].message, /version conflict/) +}) + +for (const action of ['publish', 'leave']) { + test(`${action} stays blocked when edits arrive during save, without claiming save failed`, async () => { + const editor = publishingEditor(async () => { editor.changeRevision += 1; return {} }) + editor.dirty = true + if (action === 'publish') await publishScene.call(editor) + else assert.equal(await leaveEditor(editor), false) + assert.equal(editor.publishCalls, 0) + assert.equal(editor.dirty, true) + assert.equal(editor.messages.length, 1) + assert.match(editor.messages[0].message, /新修改仍待保存/) + assert.doesNotMatch(editor.messages[0].message, /保存失败|未保存成功/) + }) +} diff --git a/tools/training-return-tabs.e2e.mjs b/tools/training-return-tabs.e2e.mjs index f144508..5ad4cba 100644 --- a/tools/training-return-tabs.e2e.mjs +++ b/tools/training-return-tabs.e2e.mjs @@ -188,12 +188,13 @@ try { currentTitle = '保存并离开失败时不导航、不删除编辑标签(浏览器模拟 503)' await returnToList(page) await page.getByRole('button', { name: '保存并离开', exact: true }).click() - await expect(page.locator('.el-message-box').filter({ hasText: '保存失败' })).toBeVisible({ timeout: 45_000 }) + const saveFailureMessage = page.locator('.el-message--error').filter({ hasText: '浏览器模拟保存失败' }) + await expect(saveFailureMessage).toBeVisible({ timeout: 45_000 }) assert.equal(new URL(page.url()).pathname, editPath('161')) assert.ok(has(await tabs(page), editPath('161'))) assert.ok(evidence.blockedWrites.some(write => write.method === 'PUT' && write.path.endsWith('/projects/161'))) passed(currentTitle, { tabs: await tabs(page), writes: evidence.blockedWrites, simulated: true }, [await screenshot(page, '06-save-failure-keeps-editor')]) - await page.getByRole('button', { name: '知道了', exact: true }).click() + await saveFailureMessage.locator('.el-message__closeBtn').click() currentTitle = '明确放弃修改后返回并关闭当前编辑标签' await returnToList(page)