@toby1123yjh/test-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,155 @@
1
+ {
2
+ "welcome": "Welcome to Test CLI",
3
+ "config_not_found": "Config not found, please run /config first",
4
+ "config_invalid": "Config invalid: {{error}}",
5
+ "help": {
6
+ "header": "Available commands:",
7
+ "usage": "Use /command to run commands"
8
+ },
9
+ "config": {
10
+ "created": "Config file created",
11
+ "reloaded": "Config reloaded",
12
+ "reload_failed": "Config reload failed",
13
+ "project": "Project",
14
+ "baseUrl": "Base URL"
15
+ },
16
+ "commands": {
17
+ "config": "Reload config (create if not exists)",
18
+ "generate": "Generate module YAML/script",
19
+ "new": "Generate test case",
20
+ "update": "Update module YAML/script",
21
+ "validate": "Validate test files",
22
+ "run": "Run tests",
23
+ "help": "Show help",
24
+ "clear": "Clear screen"
25
+ },
26
+ "generate": {
27
+ "help": {
28
+ "summary": "Generate case.yaml and a runnable test script for a module.",
29
+ "options": {
30
+ "yaml": "Deprecated (ignored): /generate always generates YAML + script.",
31
+ "script": "Deprecated (ignored): /generate always generates YAML + script.",
32
+ "url": "Optional: include a https?:// URL in your prompt to generate frontend tests."
33
+ },
34
+ "examples": {
35
+ "yaml": "Example: /generate login https://example.com/login",
36
+ "script": "Example: /generate api",
37
+ "default": "Example: /generate login https://example.com/login"
38
+ }
39
+ }
40
+ },
41
+ "new": {
42
+ "generating": "Generating test case...",
43
+ "success": "Test case saved to {{path}}",
44
+ "duplicate": "Topic '{{topic}}' already exists in {{file}}",
45
+ "error": "Generation failed: {{error}}",
46
+ "invalidTopic": "Please provide a test topic"
47
+ },
48
+ "update": {
49
+ "help": {
50
+ "summary": "Update a module's case.yaml and regenerate its test script.",
51
+ "options": {
52
+ "yaml": "Deprecated (ignored): /update always updates YAML + script.",
53
+ "script": "Deprecated (ignored): /update always updates YAML + script."
54
+ },
55
+ "examples": {
56
+ "yaml": "Example: /update login",
57
+ "script": "Example: /update login selectors changed",
58
+ "default": "Example: /update login"
59
+ }
60
+ }
61
+ },
62
+ "validate": {
63
+ "scanning": "Scanning for test files...",
64
+ "validating": "Validating {{file}}...",
65
+ "success": "All {{count}} files passed validation",
66
+ "failed": "Validation failed: {{count}} errors found",
67
+ "noFiles": "No test files found",
68
+ "summary": "Validated {{total}} files: {{passed}} passed, {{failed}} failed"
69
+ },
70
+ "run": {
71
+ "selectedHeader": "Selected {{count}} tests:",
72
+ "starting": "Starting run ({{total}} tests)...",
73
+ "runningTest": "Running test {{index}}/{{total}}: {{name}}",
74
+ "caseEnd": "Test {{index}}/{{total}}: {{status}} ({{duration}}ms)",
75
+ "dependenciesForceSequential": "Dependencies detected; forcing sequential execution.",
76
+ "completed": "Run completed: {{passed}} passed, {{failed}} failed, {{skipped}} skipped ({{duration}}ms)",
77
+ "skipReason": "Skipped: {{reason}}",
78
+ "skip": {
79
+ "cyclic": "cyclic dependency detected",
80
+ "missingDeps": "missing dependencies: {{deps}}",
81
+ "depNotPassed": "dependency not passed: {{deps}}"
82
+ },
83
+ "noTests": "No tests selected",
84
+ "columns": {
85
+ "id": "ID",
86
+ "name": "Name",
87
+ "file": "File",
88
+ "group": "Group",
89
+ "tags": "Tags"
90
+ },
91
+ "status": {
92
+ "passed": "passed",
93
+ "failed": "failed",
94
+ "skipped": "skipped",
95
+ "error": "error"
96
+ },
97
+ "summary": {
98
+ "title": "Summary",
99
+ "counts": "Total: {{total}} | {{passed}} | {{failed}} | {{skipped}}",
100
+ "passRate": "Pass rate: {{passRate}}",
101
+ "duration": "Duration: {{duration}}ms",
102
+ "report": "Report: {{path}}"
103
+ },
104
+ "help": {
105
+ "noVideo": "Disable video recording (--no-video)."
106
+ },
107
+ "error": "Run failed: {{error}}"
108
+ },
109
+ "report": {
110
+ "notFound": "Report not found",
111
+ "loading": "Loading report...",
112
+ "summary": "Report Summary",
113
+ "runId": "Run ID",
114
+ "startTime": "Start time",
115
+ "endTime": "End time",
116
+ "duration": "Duration",
117
+ "total": "Total",
118
+ "passed": "Passed",
119
+ "failed": "Failed",
120
+ "skipped": "Skipped",
121
+ "caseDetails": "Case Details",
122
+ "error": "Error",
123
+ "noRuns": "No runs found",
124
+ "title": "Report",
125
+ "meta": "Run: {{runId}} | Start: {{startTime}} | End: {{endTime}} | Duration: {{duration}}ms",
126
+ "stats": "Total: {{total}} | {{passed}} | {{failed}} | {{skipped}}",
127
+ "detailsTitle": "Details",
128
+ "caseLine": "- {{caseId}} | {{name}} | {{status}} | {{duration}}ms",
129
+ "stepLine": " - Step {{stepIndex}} | {{status}} | action: {{action}} | expected: {{expected}} | actual: {{actual}}",
130
+ "errorLine": " - Error: {{message}}",
131
+ "status": {
132
+ "passed": "passed",
133
+ "failed": "failed",
134
+ "skipped": "skipped"
135
+ }
136
+ },
137
+ "errors": {
138
+ "unknown_command": "Unknown command: {{command}}",
139
+ "format": "? [{{code}}] {{message}}\n\nSuggestion: {{suggestion}}",
140
+ "messages": {
141
+ "CONFIG_NOT_FOUND": "Config file not found",
142
+ "CONFIG_INVALID": "Config invalid: {{details}}",
143
+ "UNKNOWN_COMMAND": "Unknown command: {{command}}",
144
+ "INIT_ALREADY_EXISTS": "Project already initialized",
145
+ "INTERNAL_ERROR": "Unexpected error: {{details}}"
146
+ },
147
+ "suggestions": {
148
+ "CONFIG_NOT_FOUND": "Run /config to create or reload config",
149
+ "CONFIG_INVALID": "Check config.yaml syntax",
150
+ "UNKNOWN_COMMAND": "Use /help to see available commands",
151
+ "INIT_ALREADY_EXISTS": "Delete .test-cli/ and try again",
152
+ "INTERNAL_ERROR": "Re-run the command; if it persists, report a bug"
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,155 @@
1
+ {
2
+ "welcome": "欢迎使用 Test CLI",
3
+ "config_not_found": "未找到配置文件,请先运行 /config",
4
+ "config_invalid": "配置无效:{{error}}",
5
+ "help": {
6
+ "header": "可用命令:",
7
+ "usage": "使用 /command 执行命令"
8
+ },
9
+ "config": {
10
+ "created": "已创建配置文件",
11
+ "reloaded": "配置已重新加载",
12
+ "reload_failed": "配置加载失败",
13
+ "project": "项目",
14
+ "baseUrl": "基础URL"
15
+ },
16
+ "commands": {
17
+ "config": "重新加载配置(不存在则创建)",
18
+ "generate": "生成模块 YAML/脚本",
19
+ "new": "生成测试用例",
20
+ "update": "更新模块 YAML/脚本",
21
+ "validate": "校验测试文件",
22
+ "run": "运行测试",
23
+ "help": "显示帮助",
24
+ "clear": "清空屏幕"
25
+ },
26
+ "generate": {
27
+ "help": {
28
+ "summary": "为模块生成 case.yaml 和可运行的测试脚本。",
29
+ "options": {
30
+ "yaml": "已废弃(忽略):/generate 总是生成 YAML + 脚本。",
31
+ "script": "已废弃(忽略):/generate 总是生成 YAML + 脚本。",
32
+ "url": "可选:在提示词里包含 https?:// URL 以生成前端测试。"
33
+ },
34
+ "examples": {
35
+ "yaml": "示例:/generate login https://example.com/login",
36
+ "script": "示例:/generate api",
37
+ "default": "示例:/generate login https://example.com/login"
38
+ }
39
+ }
40
+ },
41
+ "new": {
42
+ "generating": "正在生成测试用例...",
43
+ "success": "测试用例已保存到 {{path}}",
44
+ "duplicate": "主题 '{{topic}}' 已存在于 {{file}}",
45
+ "error": "生成失败: {{error}}",
46
+ "invalidTopic": "请提供测试主题"
47
+ },
48
+ "update": {
49
+ "help": {
50
+ "summary": "更新模块的 case.yaml 并重新生成测试脚本。",
51
+ "options": {
52
+ "yaml": "已废弃(忽略):/update 总是更新 YAML + 脚本。",
53
+ "script": "已废弃(忽略):/update 总是更新 YAML + 脚本。"
54
+ },
55
+ "examples": {
56
+ "yaml": "示例:/update login",
57
+ "script": "示例:/update login 选择器变了",
58
+ "default": "示例:/update login"
59
+ }
60
+ }
61
+ },
62
+ "validate": {
63
+ "scanning": "正在扫描测试文件...",
64
+ "validating": "正在验证 {{file}}...",
65
+ "success": "所有 {{count}} 个文件验证通过",
66
+ "failed": "验证失败:发现 {{count}} 个错误",
67
+ "noFiles": "未找到测试文件",
68
+ "summary": "已验证 {{total}} 个文件:{{passed}} 通过,{{failed}} 失败"
69
+ },
70
+ "run": {
71
+ "selectedHeader": "已选择 {{count}} 个测试:",
72
+ "starting": "开始执行(共 {{total}} 个测试)...",
73
+ "runningTest": "正在执行测试 {{index}}/{{total}}:{{name}}",
74
+ "caseEnd": "测试 {{index}}/{{total}}:{{status}}({{duration}}ms)",
75
+ "dependenciesForceSequential": "检测到依赖;强制串行执行。",
76
+ "completed": "执行完成:{{passed}} 通过,{{failed}} 失败,{{skipped}} 跳过({{duration}}ms)",
77
+ "skipReason": "跳过:{{reason}}",
78
+ "skip": {
79
+ "cyclic": "检测到循环依赖",
80
+ "missingDeps": "缺少依赖:{{deps}}",
81
+ "depNotPassed": "依赖未通过:{{deps}}"
82
+ },
83
+ "noTests": "未选择任何测试",
84
+ "columns": {
85
+ "id": "ID",
86
+ "name": "名称",
87
+ "file": "文件",
88
+ "group": "分组",
89
+ "tags": "标签"
90
+ },
91
+ "status": {
92
+ "passed": "通过",
93
+ "failed": "失败",
94
+ "skipped": "跳过",
95
+ "error": "错误"
96
+ },
97
+ "summary": {
98
+ "title": "摘要",
99
+ "counts": "总数: {{total}} | {{passed}} | {{failed}} | {{skipped}}",
100
+ "passRate": "通过率: {{passRate}}",
101
+ "duration": "总耗时: {{duration}}ms",
102
+ "report": "报告: {{path}}"
103
+ },
104
+ "help": {
105
+ "noVideo": "禁用视频录制(--no-video)。"
106
+ },
107
+ "error": "执行失败:{{error}}"
108
+ },
109
+ "report": {
110
+ "notFound": "未找到报告",
111
+ "loading": "正在加载报告...",
112
+ "summary": "报告摘要",
113
+ "runId": "运行ID",
114
+ "startTime": "开始时间",
115
+ "endTime": "结束时间",
116
+ "duration": "持续时间",
117
+ "total": "总计",
118
+ "passed": "通过",
119
+ "failed": "失败",
120
+ "skipped": "跳过",
121
+ "caseDetails": "用例详情",
122
+ "error": "错误信息",
123
+ "noRuns": "没有运行记录",
124
+ "title": "报告",
125
+ "meta": "运行: {{runId}} | 开始: {{startTime}} | 结束: {{endTime}} | 耗时: {{duration}}ms",
126
+ "stats": "总数: {{total}} | {{passed}} | {{failed}} | {{skipped}}",
127
+ "detailsTitle": "详情",
128
+ "caseLine": "- {{caseId}} | {{name}} | {{status}} | {{duration}}ms",
129
+ "stepLine": " - 步骤 {{stepIndex}} | {{status}} | 动作: {{action}} | 期望: {{expected}} | 实际: {{actual}}",
130
+ "errorLine": " - 错误: {{message}}",
131
+ "status": {
132
+ "passed": "通过",
133
+ "failed": "失败",
134
+ "skipped": "跳过"
135
+ }
136
+ },
137
+ "errors": {
138
+ "unknown_command": "未知命令:{{command}}",
139
+ "format": "? [{{code}}] {{message}}\n\n建议: {{suggestion}}",
140
+ "messages": {
141
+ "CONFIG_NOT_FOUND": "配置文件未找到",
142
+ "CONFIG_INVALID": "配置文件格式无效:{{details}}",
143
+ "UNKNOWN_COMMAND": "未知命令:{{command}}",
144
+ "INIT_ALREADY_EXISTS": "项目已初始化",
145
+ "INTERNAL_ERROR": "发生未预期错误:{{details}}"
146
+ },
147
+ "suggestions": {
148
+ "CONFIG_NOT_FOUND": "运行 /config 创建或重新加载配置",
149
+ "CONFIG_INVALID": "检查 config.yaml 语法",
150
+ "UNKNOWN_COMMAND": "使用 /help 查看可用命令",
151
+ "INIT_ALREADY_EXISTS": "删除 .test-cli/ 后重试",
152
+ "INTERNAL_ERROR": "请重试;如仍失败请提交 issue"
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,62 @@
1
+ <section class="case-details" aria-label="用例详情">
2
+ <% modules.forEach((module) => { %>
3
+ <% module.cases.forEach((testCase) => { %>
4
+ <% const caseStatusClass = `case-status--${testCase.status}`; %>
5
+ <article id="case-<%= testCase.id %>" class="case-detail" data-status="<%= testCase.status %>">
6
+ <header class="case-detail__header case-info">
7
+ <div class="case-detail__title-row">
8
+ <h2 class="case-detail__title"><%= testCase.name %></h2>
9
+ <span class="case-status <%= caseStatusClass %>"><%= testCase.status %></span>
10
+ </div>
11
+
12
+ <div class="case-detail__meta">
13
+ <span class="case-detail__id">ID: <%= testCase.id %></span>
14
+ <span class="case-detail__separator" aria-hidden="true">·</span>
15
+ <span class="case-detail__duration"><%= (testCase.duration / 1000).toFixed(2) %>s</span>
16
+ </div>
17
+ </header>
18
+
19
+ <section class="expected-behavior" aria-label="期望行为">
20
+ <h3 class="expected-behavior__title">期望行为</h3>
21
+ <div class="expected-behavior__content"><%= testCase.expectedBehavior %></div>
22
+ </section>
23
+
24
+ <section class="steps-section" aria-label="测试步骤">
25
+ <h3 class="steps-section__title">步骤</h3>
26
+ <ol class="steps-list">
27
+ <% testCase.steps.forEach((step) => { %>
28
+ <% const stepStatusClass = `step-status--${step.status}`; %>
29
+ <li class="step-item <%= stepStatusClass %>">
30
+ <div class="step-row">
31
+ <div class="step-index"><%= step.index %></div>
32
+ <div class="step-action"><%= step.action %></div>
33
+ <div class="step-meta">
34
+ <span class="step-status"><%= step.status %></span>
35
+ <span class="step-duration"><%= (step.duration / 1000).toFixed(2) %>s</span>
36
+ </div>
37
+ </div>
38
+
39
+ <% if (step.error) { %>
40
+ <div class="step-error">
41
+ <div class="step-error__message"><%= step.error.message %></div>
42
+ <% if (step.error.stack) { %>
43
+ <pre class="step-error__stack"><%= step.error.stack %></pre>
44
+ <% } %>
45
+ </div>
46
+ <% } %>
47
+ </li>
48
+ <% }) %>
49
+ </ol>
50
+ </section>
51
+
52
+ <section class="video-section" aria-label="视频">
53
+ <%- include('video-player', { video: testCase.video }) %>
54
+ </section>
55
+
56
+ <section class="evidence-section" aria-label="Evidence">
57
+ <%- include('evidence-tabs', { evidence: testCase.evidence, caseId: testCase.id }) %>
58
+ </section>
59
+ </article>
60
+ <% }) %>
61
+ <% }) %>
62
+ </section>
@@ -0,0 +1,249 @@
1
+ <%
2
+ const maxItems = 100;
3
+ const networkAll = evidence?.network ?? [];
4
+ const consoleAll = evidence?.console ?? [];
5
+ const network = networkAll.slice(0, maxItems);
6
+ const consoleLogs = consoleAll.slice(0, maxItems);
7
+ const networkTruncated = networkAll.length > maxItems;
8
+ const consoleTruncated = consoleAll.length > maxItems;
9
+ const aiMarkdown = evidence?.ai ?? '';
10
+ const asserts = evidence?.asserts ?? [];
11
+
12
+ const toDomId = (value) => String(value ?? '').trim().replace(/[^a-zA-Z0-9_-]/g, '_');
13
+ const instanceId = (() => {
14
+ const base = toDomId(caseId);
15
+ if (base) return `case-${base}`;
16
+ return `case-${Math.random().toString(36).slice(2, 10)}`;
17
+ })();
18
+ const tabId = (tab) => `${instanceId}-tab-${tab}`;
19
+ const panelId = (tab) => `${instanceId}-panel-${tab}`;
20
+ %>
21
+
22
+ <section class="evidence-tabs" data-default-tab="network">
23
+ <header class="tabs-header" role="tablist" aria-label="Evidence Tabs">
24
+ <button
25
+ type="button"
26
+ class="tab-btn active"
27
+ data-tab="network"
28
+ role="tab"
29
+ id="<%= tabId('network') %>"
30
+ aria-controls="<%= panelId('network') %>"
31
+ aria-selected="true"
32
+ tabindex="0"
33
+ >
34
+ Network
35
+ </button>
36
+ <button
37
+ type="button"
38
+ class="tab-btn"
39
+ data-tab="console"
40
+ role="tab"
41
+ id="<%= tabId('console') %>"
42
+ aria-controls="<%= panelId('console') %>"
43
+ aria-selected="false"
44
+ tabindex="-1"
45
+ >
46
+ Console
47
+ </button>
48
+ <button
49
+ type="button"
50
+ class="tab-btn"
51
+ data-tab="ai"
52
+ role="tab"
53
+ id="<%= tabId('ai') %>"
54
+ aria-controls="<%= panelId('ai') %>"
55
+ aria-selected="false"
56
+ tabindex="-1"
57
+ >
58
+ AI
59
+ </button>
60
+ <button
61
+ type="button"
62
+ class="tab-btn"
63
+ data-tab="assert"
64
+ role="tab"
65
+ id="<%= tabId('assert') %>"
66
+ aria-controls="<%= panelId('assert') %>"
67
+ aria-selected="false"
68
+ tabindex="-1"
69
+ >
70
+ Assert
71
+ </button>
72
+ </header>
73
+
74
+ <section class="tabs-body">
75
+ <section
76
+ class="tab-content active"
77
+ id="<%= panelId('network') %>"
78
+ data-tab="network"
79
+ role="tabpanel"
80
+ aria-labelledby="<%= tabId('network') %>"
81
+ tabindex="0"
82
+ >
83
+ <div class="network-toolbar">
84
+ <% if (networkTruncated) { %>
85
+ <div class="truncation-hint">已截断:仅显示前 <%= maxItems %> 条(共 <%= networkAll.length %> 条)</div>
86
+ <% } %>
87
+ <label class="network-filter">
88
+ <span class="network-filter__label">状态</span>
89
+ <select class="request-filter">
90
+ <option value="all">全部</option>
91
+ <option value="success">成功</option>
92
+ <option value="failed">失败</option>
93
+ </select>
94
+ </label>
95
+ </div>
96
+
97
+ <% if (network.length === 0) { %>
98
+ <div class="network-empty">暂无网络请求</div>
99
+ <% } else { %>
100
+ <div class="network-table__wrapper">
101
+ <table class="network-table">
102
+ <thead>
103
+ <tr>
104
+ <th>URL</th>
105
+ <th>Method</th>
106
+ <th>Status</th>
107
+ <th>Duration</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>
111
+ <% network.forEach((req) => { %>
112
+ <% const rowClass = req.status >= 400 ? 'request-row request-failed' : 'request-row'; %>
113
+ <% const detailRowId = `${instanceId}-req-${toDomId(req.id)}`; %>
114
+ <tr class="<%= rowClass %>" data-request-id="<%= req.id %>" data-status="<%= req.status %>">
115
+ <td class="request-url">
116
+ <button
117
+ type="button"
118
+ class="request-toggle"
119
+ aria-expanded="false"
120
+ aria-label="展开请求详情"
121
+ aria-controls="<%= detailRowId %>"
122
+ >
123
+
124
+ </button>
125
+ <span class="request-url__text"><%= req.url %></span>
126
+ </td>
127
+ <td class="request-method"><%= req.method %></td>
128
+ <td class="request-status"><%= req.status %></td>
129
+ <td class="request-duration"><%= (req.duration / 1000).toFixed(2) %>s</td>
130
+ </tr>
131
+ <tr id="<%= detailRowId %>" class="request-detail" data-request-id="<%= req.id %>" hidden>
132
+ <td colspan="4">
133
+ <div class="request-detail__grid">
134
+ <div class="request-detail__block">
135
+ <div class="request-detail__title">Request Body</div>
136
+ <pre class="request-detail__body"><%= req.request.body ?? '' %></pre>
137
+ </div>
138
+ <div class="request-detail__block">
139
+ <div class="request-detail__title">Response Body</div>
140
+ <pre class="request-detail__body"><%= req.response.body ?? '' %></pre>
141
+ </div>
142
+ </div>
143
+ </td>
144
+ </tr>
145
+ <% }) %>
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+ <% } %>
150
+ </section>
151
+
152
+ <section
153
+ class="tab-content"
154
+ id="<%= panelId('console') %>"
155
+ data-tab="console"
156
+ role="tabpanel"
157
+ aria-labelledby="<%= tabId('console') %>"
158
+ tabindex="0"
159
+ hidden
160
+ >
161
+ <div class="console-toolbar">
162
+ <% if (consoleTruncated) { %>
163
+ <div class="truncation-hint">已截断:仅显示前 <%= maxItems %> 条(共 <%= consoleAll.length %> 条)</div>
164
+ <% } %>
165
+ <label class="console-filter">
166
+ <span class="console-filter__label">级别</span>
167
+ <select class="console-level-filter">
168
+ <option value="all">全部</option>
169
+ <option value="error">error</option>
170
+ <option value="warning">warning</option>
171
+ <option value="info">info</option>
172
+ <option value="log">log</option>
173
+ </select>
174
+ </label>
175
+ </div>
176
+
177
+ <% if (consoleLogs.length === 0) { %>
178
+ <div class="console-empty">暂无日志</div>
179
+ <% } else { %>
180
+ <ul class="console-list" aria-label="Console Logs">
181
+ <% consoleLogs.forEach((log) => { %>
182
+ <li class="console-item log-level--<%= log.level %>" data-level="<%= log.level %>">
183
+ <span class="console-time"><%= log.timestamp %></span>
184
+ <span class="console-level"><%= log.level %></span>
185
+ <span class="console-message"><%= log.message %></span>
186
+ </li>
187
+ <% }) %>
188
+ </ul>
189
+ <% } %>
190
+ </section>
191
+
192
+ <section
193
+ class="tab-content"
194
+ id="<%= panelId('ai') %>"
195
+ data-tab="ai"
196
+ role="tabpanel"
197
+ aria-labelledby="<%= tabId('ai') %>"
198
+ tabindex="0"
199
+ hidden
200
+ >
201
+ <% if (!aiMarkdown || aiMarkdown.trim().length === 0) { %>
202
+ <div class="ai-empty">暂无 AI 分析</div>
203
+ <% } else { %>
204
+ <textarea class="ai-markdown-source" hidden><%= aiMarkdown %></textarea>
205
+ <div class="ai-markdown-output" aria-label="AI Analysis"></div>
206
+ <% } %>
207
+ </section>
208
+
209
+ <section
210
+ class="tab-content"
211
+ id="<%= panelId('assert') %>"
212
+ data-tab="assert"
213
+ role="tabpanel"
214
+ aria-labelledby="<%= tabId('assert') %>"
215
+ tabindex="0"
216
+ hidden
217
+ >
218
+ <% if (asserts.length === 0) { %>
219
+ <div class="assert-empty">暂无断言</div>
220
+ <% } else { %>
221
+ <div class="assert-table__wrapper">
222
+ <table class="assert-table">
223
+ <thead>
224
+ <tr>
225
+ <th>断言</th>
226
+ <th>状态</th>
227
+ <th>期望</th>
228
+ <th>实际</th>
229
+ <th>消息</th>
230
+ </tr>
231
+ </thead>
232
+ <tbody>
233
+ <% asserts.forEach((assert) => { %>
234
+ <% const assertRowClass = assert.status === 'failed' ? 'assert-row assert-failed' : 'assert-row'; %>
235
+ <tr class="<%= assertRowClass %>" data-status="<%= assert.status %>">
236
+ <td class="assert-name"><%= assert.name %></td>
237
+ <td class="assert-status"><%= assert.status %></td>
238
+ <td class="assert-expected"><code><%= assert.expected ?? '' %></code></td>
239
+ <td class="assert-actual"><code><%= assert.actual ?? '' %></code></td>
240
+ <td class="assert-message"><%= assert.message ?? '' %></td>
241
+ </tr>
242
+ <% }) %>
243
+ </tbody>
244
+ </table>
245
+ </div>
246
+ <% } %>
247
+ </section>
248
+ </section>
249
+ </section>
@@ -0,0 +1,27 @@
1
+ <header class="report-header">
2
+ <div class="report-header__left">
3
+ <div class="report-header__title-row">
4
+ <button
5
+ type="button"
6
+ class="sidebar-toggle"
7
+ aria-label="打开目录"
8
+ aria-controls="report-sidebar"
9
+ aria-expanded="false"
10
+ >
11
+ <span class="sidebar-toggle__icon" aria-hidden="true">☰</span>
12
+ </button>
13
+ <h1 class="report-header__title"><%= title %></h1>
14
+ </div>
15
+ <div class="report-header__meta">
16
+ <span class="report-header__timestamp"><%= timestamp %></span>
17
+ <span class="report-header__separator" aria-hidden="true">·</span>
18
+ <span class="report-header__duration"><%= (duration / 1000).toFixed(2) %>s</span>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="report-header__right">
23
+ <button type="button" class="theme-toggle" aria-label="切换主题" title="切换主题">
24
+ <span class="theme-toggle__icon" aria-hidden="true">🌙</span>
25
+ </button>
26
+ </div>
27
+ </header>