@testsmith/testblocks 0.6.0 → 0.8.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.
- package/dist/cli/executor.d.ts +4 -1
- package/dist/cli/executor.js +101 -5
- package/dist/cli/index.js +148 -4
- package/dist/cli/reporters/ConsoleReporter.d.ts +12 -0
- package/dist/cli/reporters/ConsoleReporter.js +39 -0
- package/dist/cli/reporters/HTMLReporter.d.ts +19 -0
- package/dist/cli/reporters/HTMLReporter.js +506 -0
- package/dist/cli/reporters/JSONReporter.d.ts +15 -0
- package/dist/cli/reporters/JSONReporter.js +80 -0
- package/dist/cli/reporters/JUnitReporter.d.ts +19 -0
- package/dist/cli/reporters/JUnitReporter.js +105 -0
- package/dist/cli/reporters/index.d.ts +17 -0
- package/dist/cli/reporters/index.js +31 -0
- package/dist/cli/reporters/types.d.ts +28 -0
- package/dist/cli/reporters/types.js +2 -0
- package/dist/cli/reporters/utils.d.ts +31 -0
- package/dist/cli/reporters/utils.js +136 -0
- package/dist/cli/reporters.d.ts +13 -62
- package/dist/cli/reporters.js +16 -719
- package/dist/client/assets/index-Boo8ZrY_.js +2195 -0
- package/dist/client/assets/{index-dXniUrbi.js.map → index-Boo8ZrY_.js.map} +1 -1
- package/dist/client/assets/index-OxNH9dW-.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/blocks/api.js +3 -6
- package/dist/core/blocks/assertions.d.ts +31 -0
- package/dist/core/blocks/assertions.js +72 -0
- package/dist/core/blocks/index.d.ts +1 -0
- package/dist/core/blocks/index.js +6 -1
- package/dist/core/blocks/lifecycle.js +5 -3
- package/dist/core/blocks/logic.js +2 -3
- package/dist/core/blocks/playwright/assertions.d.ts +5 -0
- package/dist/core/blocks/playwright/assertions.js +321 -0
- package/dist/core/blocks/playwright/index.d.ts +17 -0
- package/dist/core/blocks/playwright/index.js +49 -0
- package/dist/core/blocks/playwright/interactions.d.ts +5 -0
- package/dist/core/blocks/playwright/interactions.js +191 -0
- package/dist/core/blocks/playwright/navigation.d.ts +5 -0
- package/dist/core/blocks/playwright/navigation.js +133 -0
- package/dist/core/blocks/playwright/retrieval.d.ts +5 -0
- package/dist/core/blocks/playwright/retrieval.js +144 -0
- package/dist/core/blocks/playwright/types.d.ts +65 -0
- package/dist/core/blocks/playwright/types.js +5 -0
- package/dist/core/blocks/playwright/utils.d.ts +26 -0
- package/dist/core/blocks/playwright/utils.js +137 -0
- package/dist/core/blocks/playwright.d.ts +13 -2
- package/dist/core/blocks/playwright.js +14 -761
- package/dist/core/executor/BaseTestExecutor.d.ts +60 -0
- package/dist/core/executor/BaseTestExecutor.js +297 -0
- package/dist/core/executor/index.d.ts +1 -0
- package/dist/core/executor/index.js +5 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/types.d.ts +12 -0
- package/dist/core/utils/blocklyParser.d.ts +18 -0
- package/dist/core/utils/blocklyParser.js +84 -0
- package/dist/core/utils/dataLoader.d.ts +9 -0
- package/dist/core/utils/dataLoader.js +117 -0
- package/dist/core/utils/index.d.ts +2 -0
- package/dist/core/utils/index.js +12 -0
- package/dist/core/utils/logger.d.ts +14 -0
- package/dist/core/utils/logger.js +48 -0
- package/dist/core/utils/variableResolver.d.ts +24 -0
- package/dist/core/utils/variableResolver.js +92 -0
- package/dist/server/executor.d.ts +6 -0
- package/dist/server/executor.js +207 -47
- package/dist/server/globals.d.ts +6 -1
- package/dist/server/globals.js +7 -0
- package/dist/server/startServer.js +15 -0
- package/package.json +1 -1
- package/dist/client/assets/index-dXniUrbi.js +0 -2193
- package/dist/client/assets/index-oTTttNKd.css +0 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HTMLReporter = void 0;
|
|
37
|
+
exports.generateHTMLReport = generateHTMLReport;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const utils_1 = require("./utils");
|
|
41
|
+
/**
|
|
42
|
+
* HTML reporter - generates a styled HTML report
|
|
43
|
+
*/
|
|
44
|
+
class HTMLReporter {
|
|
45
|
+
constructor(outputDir) {
|
|
46
|
+
this.allResults = [];
|
|
47
|
+
this.outputDir = outputDir;
|
|
48
|
+
}
|
|
49
|
+
onTestFileComplete(file, testFile, results) {
|
|
50
|
+
this.allResults.push({ file, testFile, results });
|
|
51
|
+
const passed = results.filter(r => r.status === 'passed').length;
|
|
52
|
+
const failed = results.filter(r => r.status !== 'passed').length;
|
|
53
|
+
console.log(` ${passed} passed, ${failed} failed\n`);
|
|
54
|
+
}
|
|
55
|
+
onComplete(allResults) {
|
|
56
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
57
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const timestamp = (0, utils_1.getTimestamp)();
|
|
60
|
+
const outputPath = path.join(this.outputDir, `report-${timestamp}.html`);
|
|
61
|
+
const totalTests = allResults.reduce((sum, r) => sum + r.results.length, 0);
|
|
62
|
+
const passed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0);
|
|
63
|
+
const failed = allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0);
|
|
64
|
+
const totalDuration = allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0);
|
|
65
|
+
const html = generateHTMLReport({
|
|
66
|
+
timestamp: new Date().toISOString(),
|
|
67
|
+
summary: { totalTests, passed, failed, duration: totalDuration },
|
|
68
|
+
testFiles: this.allResults,
|
|
69
|
+
});
|
|
70
|
+
fs.writeFileSync(outputPath, html);
|
|
71
|
+
console.log(`HTML report saved to: ${outputPath}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.HTMLReporter = HTMLReporter;
|
|
75
|
+
/**
|
|
76
|
+
* Generate HTML report from report data
|
|
77
|
+
*/
|
|
78
|
+
function generateHTMLReport(data) {
|
|
79
|
+
const { timestamp, summary, testFiles } = data;
|
|
80
|
+
// Separate lifecycle hooks from actual tests for counting
|
|
81
|
+
let actualTests = 0;
|
|
82
|
+
let actualPassed = 0;
|
|
83
|
+
let actualFailed = 0;
|
|
84
|
+
for (const { results } of testFiles) {
|
|
85
|
+
for (const result of results) {
|
|
86
|
+
if (!result.isLifecycle) {
|
|
87
|
+
actualTests++;
|
|
88
|
+
if (result.status === 'passed')
|
|
89
|
+
actualPassed++;
|
|
90
|
+
else
|
|
91
|
+
actualFailed++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const passRate = actualTests > 0
|
|
96
|
+
? ((actualPassed / actualTests) * 100).toFixed(1)
|
|
97
|
+
: '0';
|
|
98
|
+
let html = `<!DOCTYPE html>
|
|
99
|
+
<html lang="en">
|
|
100
|
+
<head>
|
|
101
|
+
<meta charset="UTF-8">
|
|
102
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
103
|
+
<title>TestBlocks Report - ${timestamp}</title>
|
|
104
|
+
<style>
|
|
105
|
+
${getHTMLStyles()}
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div class="container">
|
|
110
|
+
<h1>TestBlocks Test Report</h1>
|
|
111
|
+
<div class="timestamp">Generated: ${new Date(timestamp).toLocaleString()}</div>
|
|
112
|
+
|
|
113
|
+
<div class="summary">
|
|
114
|
+
<div class="summary-card">
|
|
115
|
+
<div class="value">${actualTests}</div>
|
|
116
|
+
<div class="label">Total Tests</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div class="summary-card passed">
|
|
119
|
+
<div class="value">${actualPassed}</div>
|
|
120
|
+
<div class="label">Passed</div>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="summary-card failed">
|
|
123
|
+
<div class="value">${actualFailed}</div>
|
|
124
|
+
<div class="label">Failed</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="summary-card">
|
|
127
|
+
<div class="value">${passRate}%</div>
|
|
128
|
+
<div class="label">Pass Rate</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="summary-card">
|
|
131
|
+
<div class="value">${(summary.duration / 1000).toFixed(1)}s</div>
|
|
132
|
+
<div class="label">Duration</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
for (const { file, testFile, results } of testFiles) {
|
|
137
|
+
html += renderTestFile(file, testFile, results);
|
|
138
|
+
}
|
|
139
|
+
html += `
|
|
140
|
+
</div>
|
|
141
|
+
</body>
|
|
142
|
+
</html>`;
|
|
143
|
+
return html;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Render a single test file section
|
|
147
|
+
*/
|
|
148
|
+
function renderTestFile(file, testFile, results) {
|
|
149
|
+
// Separate lifecycle hooks from tests
|
|
150
|
+
const lifecycleResults = results.filter(r => r.isLifecycle);
|
|
151
|
+
const testResults = results.filter(r => !r.isLifecycle);
|
|
152
|
+
const filePassed = testResults.filter(r => r.status === 'passed').length;
|
|
153
|
+
const fileFailed = testResults.filter(r => r.status !== 'passed').length;
|
|
154
|
+
let html = `
|
|
155
|
+
<div class="test-file">
|
|
156
|
+
<div class="test-file-header">
|
|
157
|
+
<span>${(0, utils_1.escapeHtml)(testFile.name)}</span>
|
|
158
|
+
<span class="test-file-path">${(0, utils_1.escapeHtml)(file)} • ${filePassed} passed, ${fileFailed} failed</span>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
// Render lifecycle hooks (beforeAll)
|
|
162
|
+
const beforeAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'beforeAll');
|
|
163
|
+
if (beforeAllHooks.length > 0) {
|
|
164
|
+
html += renderLifecycleSection('Before All', beforeAllHooks);
|
|
165
|
+
}
|
|
166
|
+
// Render actual tests
|
|
167
|
+
for (const result of testResults) {
|
|
168
|
+
html += renderTestCase(result, testFile);
|
|
169
|
+
}
|
|
170
|
+
// Render lifecycle hooks (afterAll)
|
|
171
|
+
const afterAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'afterAll');
|
|
172
|
+
if (afterAllHooks.length > 0) {
|
|
173
|
+
html += renderLifecycleSection('After All', afterAllHooks);
|
|
174
|
+
}
|
|
175
|
+
html += ` </div>\n`;
|
|
176
|
+
return html;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Render lifecycle section (beforeAll/afterAll)
|
|
180
|
+
*/
|
|
181
|
+
function renderLifecycleSection(title, hooks) {
|
|
182
|
+
let html = ` <div class="lifecycle-section">\n`;
|
|
183
|
+
html += ` <div class="lifecycle-header">${title}</div>\n`;
|
|
184
|
+
for (const hook of hooks) {
|
|
185
|
+
const iconClass = hook.status === 'passed' ? 'passed' : 'failed';
|
|
186
|
+
html += ` <div class="lifecycle-item">
|
|
187
|
+
<span class="step-dot ${iconClass}"></span>
|
|
188
|
+
<span>${hook.steps.length} steps</span>
|
|
189
|
+
<span style="color: var(--color-text-secondary); font-size: 12px;">${hook.duration}ms</span>
|
|
190
|
+
</div>\n`;
|
|
191
|
+
if (hook.error) {
|
|
192
|
+
html += ` <div class="error-message" style="margin: 4px 0 0 16px;">${(0, utils_1.escapeHtml)(hook.error.message)}</div>\n`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
html += ` </div>\n`;
|
|
196
|
+
return html;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Render a single test case
|
|
200
|
+
*/
|
|
201
|
+
function renderTestCase(result, testFile) {
|
|
202
|
+
const statusIcon = result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○';
|
|
203
|
+
const testId = `test-${Math.random().toString(36).substr(2, 9)}`;
|
|
204
|
+
let html = `
|
|
205
|
+
<div class="test-case ${result.status}">
|
|
206
|
+
<div class="test-case-header">
|
|
207
|
+
<div class="status-icon ${result.status}">${statusIcon}</div>
|
|
208
|
+
<div class="test-name">${(0, utils_1.escapeHtml)(result.testName)}</div>
|
|
209
|
+
<div class="test-duration">${result.duration}ms</div>
|
|
210
|
+
<button class="steps-toggle" onclick="document.getElementById('${testId}').classList.toggle('open'); this.textContent = this.textContent.includes('▶') ? '▼ ${result.steps.length} steps' : '▶ ${result.steps.length} steps'">
|
|
211
|
+
▶ ${result.steps.length} steps
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
`;
|
|
215
|
+
if (result.error) {
|
|
216
|
+
html += ` <div class="error-message">${(0, utils_1.escapeHtml)(result.error.message)}</div>\n`;
|
|
217
|
+
}
|
|
218
|
+
html += ` <div class="steps-list" id="${testId}">\n`;
|
|
219
|
+
for (const step of result.steps) {
|
|
220
|
+
html += renderStep(step);
|
|
221
|
+
}
|
|
222
|
+
html += ` </div>\n`;
|
|
223
|
+
html += ` </div>\n`;
|
|
224
|
+
return html;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Render a single step
|
|
228
|
+
*/
|
|
229
|
+
function renderStep(step) {
|
|
230
|
+
const summary = (0, utils_1.getStepSummary)(step.stepType, step.output);
|
|
231
|
+
const isApiRequest = (0, utils_1.isApiRequestStep)(step.stepType);
|
|
232
|
+
const response = step.output;
|
|
233
|
+
let html = ` <div class="step">
|
|
234
|
+
<div class="step-header">
|
|
235
|
+
<span class="step-dot ${step.status}"></span>
|
|
236
|
+
<span class="step-type">${(0, utils_1.escapeHtml)((0, utils_1.formatStepType)(step.stepType))}</span>
|
|
237
|
+
${summary ? `<span class="step-summary">${(0, utils_1.escapeHtml)(summary)}</span>` : ''}
|
|
238
|
+
<span class="step-duration">${step.duration}ms</span>
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
// Show step error
|
|
242
|
+
if (step.error) {
|
|
243
|
+
html += ` <div class="step-error">${(0, utils_1.escapeHtml)(step.error.message)}</div>\n`;
|
|
244
|
+
}
|
|
245
|
+
// Show API response details
|
|
246
|
+
if (isApiRequest && response) {
|
|
247
|
+
html += ` <div class="step-details">\n`;
|
|
248
|
+
if (response.status) {
|
|
249
|
+
const statusClass = response.status >= 200 && response.status < 300 ? 'success'
|
|
250
|
+
: response.status >= 400 && response.status < 500 ? 'client-error'
|
|
251
|
+
: response.status >= 500 ? 'server-error' : '';
|
|
252
|
+
html += ` <div class="response-status">
|
|
253
|
+
<span class="response-label">Status:</span>
|
|
254
|
+
<span class="status-code ${statusClass}">${response.status}</span>
|
|
255
|
+
</div>\n`;
|
|
256
|
+
}
|
|
257
|
+
if (response.headers && Object.keys(response.headers).length > 0) {
|
|
258
|
+
html += ` <details class="response-section">
|
|
259
|
+
<summary>Headers</summary>
|
|
260
|
+
<pre class="response-pre">${(0, utils_1.escapeHtml)(JSON.stringify(response.headers, null, 2))}</pre>
|
|
261
|
+
</details>\n`;
|
|
262
|
+
}
|
|
263
|
+
if (response.body !== undefined) {
|
|
264
|
+
const bodyStr = typeof response.body === 'string'
|
|
265
|
+
? response.body
|
|
266
|
+
: JSON.stringify(response.body, null, 2);
|
|
267
|
+
html += ` <details class="response-section">
|
|
268
|
+
<summary>Body</summary>
|
|
269
|
+
<pre class="response-pre">${(0, utils_1.escapeHtml)(bodyStr)}</pre>
|
|
270
|
+
</details>\n`;
|
|
271
|
+
}
|
|
272
|
+
html += ` </div>\n`;
|
|
273
|
+
}
|
|
274
|
+
else if (step.output !== undefined && step.output !== null && !step.screenshot) {
|
|
275
|
+
// Show generic output for non-API steps
|
|
276
|
+
const outputStr = (0, utils_1.formatStepOutput)(step.output, step.stepType);
|
|
277
|
+
if (outputStr) {
|
|
278
|
+
html += ` <div class="step-details">
|
|
279
|
+
<pre class="response-pre">${(0, utils_1.escapeHtml)(outputStr)}</pre>
|
|
280
|
+
</div>\n`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Show stack trace for errors
|
|
284
|
+
if (step.error?.stack) {
|
|
285
|
+
html += ` <details class="response-section">
|
|
286
|
+
<summary>Stack Trace</summary>
|
|
287
|
+
<pre class="response-pre stack-trace">${(0, utils_1.escapeHtml)(step.error.stack)}</pre>
|
|
288
|
+
</details>\n`;
|
|
289
|
+
}
|
|
290
|
+
// Show screenshot
|
|
291
|
+
if (step.screenshot) {
|
|
292
|
+
html += ` <img class="screenshot" src="${step.screenshot}" alt="Screenshot at failure" onclick="window.open(this.src, '_blank')">\n`;
|
|
293
|
+
}
|
|
294
|
+
html += ` </div>\n`;
|
|
295
|
+
return html;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Get CSS styles for HTML report
|
|
299
|
+
*/
|
|
300
|
+
function getHTMLStyles() {
|
|
301
|
+
return ` :root {
|
|
302
|
+
--color-passed: #22c55e;
|
|
303
|
+
--color-failed: #ef4444;
|
|
304
|
+
--color-skipped: #f59e0b;
|
|
305
|
+
--color-bg: #f8fafc;
|
|
306
|
+
--color-surface: #ffffff;
|
|
307
|
+
--color-border: #e2e8f0;
|
|
308
|
+
--color-text: #334155;
|
|
309
|
+
--color-text-secondary: #64748b;
|
|
310
|
+
}
|
|
311
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
312
|
+
body {
|
|
313
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
314
|
+
background: var(--color-bg);
|
|
315
|
+
color: var(--color-text);
|
|
316
|
+
line-height: 1.5;
|
|
317
|
+
padding: 24px;
|
|
318
|
+
}
|
|
319
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
320
|
+
h1 { font-size: 24px; font-weight: 600; margin-bottom: 8px; }
|
|
321
|
+
.timestamp { color: var(--color-text-secondary); font-size: 14px; margin-bottom: 24px; }
|
|
322
|
+
.summary {
|
|
323
|
+
display: grid;
|
|
324
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
325
|
+
gap: 16px;
|
|
326
|
+
margin-bottom: 32px;
|
|
327
|
+
}
|
|
328
|
+
.summary-card {
|
|
329
|
+
background: var(--color-surface);
|
|
330
|
+
border: 1px solid var(--color-border);
|
|
331
|
+
border-radius: 8px;
|
|
332
|
+
padding: 16px;
|
|
333
|
+
text-align: center;
|
|
334
|
+
}
|
|
335
|
+
.summary-card .value { font-size: 32px; font-weight: 700; }
|
|
336
|
+
.summary-card .label { font-size: 14px; color: var(--color-text-secondary); }
|
|
337
|
+
.summary-card.passed .value { color: var(--color-passed); }
|
|
338
|
+
.summary-card.failed .value { color: var(--color-failed); }
|
|
339
|
+
.test-file {
|
|
340
|
+
background: var(--color-surface);
|
|
341
|
+
border: 1px solid var(--color-border);
|
|
342
|
+
border-radius: 8px;
|
|
343
|
+
margin-bottom: 16px;
|
|
344
|
+
overflow: hidden;
|
|
345
|
+
}
|
|
346
|
+
.test-file-header {
|
|
347
|
+
padding: 16px;
|
|
348
|
+
border-bottom: 1px solid var(--color-border);
|
|
349
|
+
font-weight: 600;
|
|
350
|
+
display: flex;
|
|
351
|
+
justify-content: space-between;
|
|
352
|
+
align-items: center;
|
|
353
|
+
}
|
|
354
|
+
.test-file-path { font-size: 12px; color: var(--color-text-secondary); font-weight: normal; }
|
|
355
|
+
.lifecycle-section {
|
|
356
|
+
background: #f1f5f9;
|
|
357
|
+
padding: 8px 16px;
|
|
358
|
+
border-bottom: 1px solid var(--color-border);
|
|
359
|
+
}
|
|
360
|
+
.lifecycle-header {
|
|
361
|
+
font-size: 12px;
|
|
362
|
+
font-weight: 600;
|
|
363
|
+
color: var(--color-text-secondary);
|
|
364
|
+
text-transform: uppercase;
|
|
365
|
+
letter-spacing: 0.5px;
|
|
366
|
+
margin-bottom: 4px;
|
|
367
|
+
}
|
|
368
|
+
.lifecycle-item {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: center;
|
|
371
|
+
gap: 8px;
|
|
372
|
+
padding: 4px 0;
|
|
373
|
+
font-size: 13px;
|
|
374
|
+
}
|
|
375
|
+
.test-case {
|
|
376
|
+
padding: 12px 16px;
|
|
377
|
+
border-bottom: 1px solid var(--color-border);
|
|
378
|
+
}
|
|
379
|
+
.test-case:last-child { border-bottom: none; }
|
|
380
|
+
.test-case.passed { border-left: 3px solid var(--color-passed); }
|
|
381
|
+
.test-case.failed { border-left: 3px solid var(--color-failed); }
|
|
382
|
+
.test-case.skipped { border-left: 3px solid var(--color-skipped); }
|
|
383
|
+
.test-case-header {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 12px;
|
|
387
|
+
}
|
|
388
|
+
.status-icon {
|
|
389
|
+
width: 20px;
|
|
390
|
+
height: 20px;
|
|
391
|
+
border-radius: 50%;
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
font-size: 12px;
|
|
396
|
+
color: white;
|
|
397
|
+
flex-shrink: 0;
|
|
398
|
+
}
|
|
399
|
+
.status-icon.passed { background: var(--color-passed); }
|
|
400
|
+
.status-icon.failed { background: var(--color-failed); }
|
|
401
|
+
.status-icon.skipped { background: var(--color-skipped); }
|
|
402
|
+
.test-name { flex: 1; font-weight: 500; }
|
|
403
|
+
.test-duration { color: var(--color-text-secondary); font-size: 14px; }
|
|
404
|
+
.error-message {
|
|
405
|
+
background: #fef2f2;
|
|
406
|
+
border: 1px solid #fecaca;
|
|
407
|
+
border-radius: 4px;
|
|
408
|
+
padding: 8px 12px;
|
|
409
|
+
margin: 8px 0 0 32px;
|
|
410
|
+
font-size: 13px;
|
|
411
|
+
color: #991b1b;
|
|
412
|
+
}
|
|
413
|
+
.steps-toggle {
|
|
414
|
+
background: none;
|
|
415
|
+
border: none;
|
|
416
|
+
color: var(--color-text-secondary);
|
|
417
|
+
cursor: pointer;
|
|
418
|
+
font-size: 12px;
|
|
419
|
+
padding: 4px 8px;
|
|
420
|
+
}
|
|
421
|
+
.steps-toggle:hover { text-decoration: underline; }
|
|
422
|
+
.steps-list {
|
|
423
|
+
display: none;
|
|
424
|
+
margin: 12px 0 0 32px;
|
|
425
|
+
border-left: 2px solid var(--color-border);
|
|
426
|
+
padding-left: 16px;
|
|
427
|
+
}
|
|
428
|
+
.steps-list.open { display: block; }
|
|
429
|
+
.step {
|
|
430
|
+
padding: 8px 0;
|
|
431
|
+
border-bottom: 1px solid #f1f5f9;
|
|
432
|
+
}
|
|
433
|
+
.step:last-child { border-bottom: none; }
|
|
434
|
+
.step-header {
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
gap: 8px;
|
|
438
|
+
font-size: 13px;
|
|
439
|
+
}
|
|
440
|
+
.step-dot {
|
|
441
|
+
width: 8px;
|
|
442
|
+
height: 8px;
|
|
443
|
+
border-radius: 50%;
|
|
444
|
+
flex-shrink: 0;
|
|
445
|
+
}
|
|
446
|
+
.step-dot.passed { background: var(--color-passed); }
|
|
447
|
+
.step-dot.failed { background: var(--color-failed); }
|
|
448
|
+
.step-type { font-weight: 500; }
|
|
449
|
+
.step-summary { color: var(--color-text-secondary); }
|
|
450
|
+
.step-duration { color: var(--color-text-secondary); font-size: 12px; margin-left: auto; }
|
|
451
|
+
.step-details {
|
|
452
|
+
margin-top: 8px;
|
|
453
|
+
padding: 8px;
|
|
454
|
+
background: #f8fafc;
|
|
455
|
+
border-radius: 4px;
|
|
456
|
+
font-size: 12px;
|
|
457
|
+
}
|
|
458
|
+
.step-error {
|
|
459
|
+
color: #991b1b;
|
|
460
|
+
margin-top: 4px;
|
|
461
|
+
}
|
|
462
|
+
.response-status {
|
|
463
|
+
display: flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
gap: 8px;
|
|
466
|
+
margin-bottom: 8px;
|
|
467
|
+
}
|
|
468
|
+
.response-label { font-weight: 500; }
|
|
469
|
+
.status-code { font-family: monospace; padding: 2px 6px; border-radius: 4px; }
|
|
470
|
+
.status-code.success { background: #dcfce7; color: #166534; }
|
|
471
|
+
.status-code.client-error { background: #fef3c7; color: #92400e; }
|
|
472
|
+
.status-code.server-error { background: #fef2f2; color: #991b1b; }
|
|
473
|
+
.response-section {
|
|
474
|
+
margin-top: 8px;
|
|
475
|
+
}
|
|
476
|
+
.response-section summary {
|
|
477
|
+
cursor: pointer;
|
|
478
|
+
font-weight: 500;
|
|
479
|
+
font-size: 12px;
|
|
480
|
+
color: var(--color-text-secondary);
|
|
481
|
+
}
|
|
482
|
+
.response-pre {
|
|
483
|
+
background: #1e293b;
|
|
484
|
+
color: #e2e8f0;
|
|
485
|
+
padding: 12px;
|
|
486
|
+
border-radius: 4px;
|
|
487
|
+
overflow-x: auto;
|
|
488
|
+
font-family: monospace;
|
|
489
|
+
font-size: 12px;
|
|
490
|
+
margin-top: 4px;
|
|
491
|
+
white-space: pre-wrap;
|
|
492
|
+
word-break: break-word;
|
|
493
|
+
}
|
|
494
|
+
.stack-trace {
|
|
495
|
+
background: #fef2f2;
|
|
496
|
+
color: #991b1b;
|
|
497
|
+
}
|
|
498
|
+
.screenshot {
|
|
499
|
+
max-width: 100%;
|
|
500
|
+
max-height: 300px;
|
|
501
|
+
border: 1px solid var(--color-border);
|
|
502
|
+
border-radius: 4px;
|
|
503
|
+
margin-top: 8px;
|
|
504
|
+
cursor: pointer;
|
|
505
|
+
}`;
|
|
506
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TestFile, TestResult } from '../../core';
|
|
2
|
+
import { Reporter } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* JSON reporter - outputs results to a JSON file
|
|
5
|
+
*/
|
|
6
|
+
export declare class JSONReporter implements Reporter {
|
|
7
|
+
private outputDir;
|
|
8
|
+
private allResults;
|
|
9
|
+
constructor(outputDir: string);
|
|
10
|
+
onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
|
|
11
|
+
onComplete(allResults: {
|
|
12
|
+
file: string;
|
|
13
|
+
results: TestResult[];
|
|
14
|
+
}[]): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.JSONReporter = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const utils_1 = require("./utils");
|
|
40
|
+
/**
|
|
41
|
+
* JSON reporter - outputs results to a JSON file
|
|
42
|
+
*/
|
|
43
|
+
class JSONReporter {
|
|
44
|
+
constructor(outputDir) {
|
|
45
|
+
this.allResults = [];
|
|
46
|
+
this.outputDir = outputDir;
|
|
47
|
+
}
|
|
48
|
+
onTestFileComplete(file, testFile, results) {
|
|
49
|
+
this.allResults.push({ file, testFile, results });
|
|
50
|
+
const passed = results.filter(r => r.status === 'passed').length;
|
|
51
|
+
const failed = results.filter(r => r.status !== 'passed').length;
|
|
52
|
+
console.log(` ${passed} passed, ${failed} failed\n`);
|
|
53
|
+
}
|
|
54
|
+
onComplete(allResults) {
|
|
55
|
+
// Ensure output directory exists
|
|
56
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
57
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const timestamp = (0, utils_1.getTimestamp)();
|
|
60
|
+
const outputPath = path.join(this.outputDir, `results-${timestamp}.json`);
|
|
61
|
+
const report = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
summary: {
|
|
64
|
+
totalFiles: allResults.length,
|
|
65
|
+
totalTests: allResults.reduce((sum, r) => sum + r.results.length, 0),
|
|
66
|
+
passed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status === 'passed').length, 0),
|
|
67
|
+
failed: allResults.reduce((sum, r) => sum + r.results.filter(t => t.status !== 'passed').length, 0),
|
|
68
|
+
duration: allResults.reduce((sum, r) => sum + r.results.reduce((s, t) => s + t.duration, 0), 0),
|
|
69
|
+
},
|
|
70
|
+
testFiles: this.allResults.map(({ file, testFile, results }) => ({
|
|
71
|
+
file,
|
|
72
|
+
name: testFile.name,
|
|
73
|
+
results,
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2));
|
|
77
|
+
console.log(`JSON report saved to: ${outputPath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.JSONReporter = JSONReporter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TestFile, TestResult } from '../../core';
|
|
2
|
+
import { Reporter, ReportData } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* JUnit XML reporter - outputs results in JUnit XML format
|
|
5
|
+
*/
|
|
6
|
+
export declare class JUnitReporter implements Reporter {
|
|
7
|
+
private outputDir;
|
|
8
|
+
private allResults;
|
|
9
|
+
constructor(outputDir: string);
|
|
10
|
+
onTestFileComplete(file: string, testFile: TestFile, results: TestResult[]): void;
|
|
11
|
+
onComplete(allResults: {
|
|
12
|
+
file: string;
|
|
13
|
+
results: TestResult[];
|
|
14
|
+
}[]): void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate JUnit XML from report data
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateJUnitXML(data: ReportData): string;
|