@matware/e2e-runner 1.1.0 → 1.1.1
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/README.md +64 -6
- package/bin/cli.js +81 -1
- package/package.json +2 -2
- package/src/actions.js +11 -3
- package/src/ai-generate.js +35 -4
- package/src/config.js +30 -0
- package/src/dashboard.js +17 -4
- package/src/db.js +28 -7
- package/src/mcp-tools.js +131 -2
- package/src/reporter.js +13 -1
- package/src/runner.js +69 -3
- package/src/verify.js +16 -4
- package/templates/dashboard.html +248 -11
package/src/mcp-tools.js
CHANGED
|
@@ -13,11 +13,11 @@ import path from 'path';
|
|
|
13
13
|
import http from 'http';
|
|
14
14
|
|
|
15
15
|
import { loadConfig } from './config.js';
|
|
16
|
-
import { waitForPool, getPoolStatus } from './pool.js';
|
|
16
|
+
import { waitForPool, getPoolStatus, connectToPool } from './pool.js';
|
|
17
17
|
import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from './runner.js';
|
|
18
18
|
import { generateReport, saveReport, persistRun } from './reporter.js';
|
|
19
19
|
import { startDashboard, stopDashboard } from './dashboard.js';
|
|
20
|
-
import { lookupScreenshotHash } from './db.js';
|
|
20
|
+
import { lookupScreenshotHash, ensureProject, computeScreenshotHash, registerScreenshotHash } from './db.js';
|
|
21
21
|
import { fetchIssue, checkCliAuth, detectProvider } from './issues.js';
|
|
22
22
|
import { buildPrompt, hasApiKey } from './ai-generate.js';
|
|
23
23
|
import { verifyIssue } from './verify.js';
|
|
@@ -56,6 +56,10 @@ export const TOOLS = [
|
|
|
56
56
|
type: 'number',
|
|
57
57
|
description: 'Number of retries for failed tests',
|
|
58
58
|
},
|
|
59
|
+
failOnNetworkError: {
|
|
60
|
+
type: 'boolean',
|
|
61
|
+
description: 'Fail tests when network requests fail (e.g. ERR_CONNECTION_REFUSED). Default: false.',
|
|
62
|
+
},
|
|
59
63
|
cwd: {
|
|
60
64
|
type: 'string',
|
|
61
65
|
description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
|
|
@@ -95,6 +99,7 @@ export const TOOLS = [
|
|
|
95
99
|
type: 'object',
|
|
96
100
|
properties: {
|
|
97
101
|
name: { type: 'string', description: 'Test name' },
|
|
102
|
+
expect: { type: 'string', description: 'Human-readable description of the expected visual outcome. After the test runs, a verification screenshot is captured and Claude Code judges pass/fail against this description.' },
|
|
98
103
|
actions: {
|
|
99
104
|
type: 'array',
|
|
100
105
|
description: 'Sequential browser actions',
|
|
@@ -206,6 +211,49 @@ export const TOOLS = [
|
|
|
206
211
|
description:
|
|
207
212
|
'prompt = return issue + prompt for Claude Code to create tests (default). verify = auto-generate tests via Claude API and run them.',
|
|
208
213
|
},
|
|
214
|
+
authToken: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'JWT or auth token to inject into localStorage before running tests (for authenticated apps)',
|
|
217
|
+
},
|
|
218
|
+
authStorageKey: {
|
|
219
|
+
type: 'string',
|
|
220
|
+
description: 'localStorage key name for the auth token (default: "accessToken")',
|
|
221
|
+
},
|
|
222
|
+
cwd: {
|
|
223
|
+
type: 'string',
|
|
224
|
+
description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
required: ['url'],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'e2e_capture',
|
|
232
|
+
description:
|
|
233
|
+
'Capture a screenshot of any URL on demand. Connects to the Chrome pool, navigates to the URL, takes a screenshot, and returns the image with its ss:HASH.',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
url: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'Full URL to capture (e.g. "https://example.com" or "http://host.docker.internal:3000/dashboard")',
|
|
240
|
+
},
|
|
241
|
+
filename: {
|
|
242
|
+
type: 'string',
|
|
243
|
+
description: 'Output filename (default: capture-<timestamp>.png)',
|
|
244
|
+
},
|
|
245
|
+
fullPage: {
|
|
246
|
+
type: 'boolean',
|
|
247
|
+
description: 'Capture full scrollable page (default: false)',
|
|
248
|
+
},
|
|
249
|
+
selector: {
|
|
250
|
+
type: 'string',
|
|
251
|
+
description: 'Wait for this CSS selector before capturing',
|
|
252
|
+
},
|
|
253
|
+
delay: {
|
|
254
|
+
type: 'number',
|
|
255
|
+
description: 'Wait N milliseconds after page load before capturing (default: 0)',
|
|
256
|
+
},
|
|
209
257
|
cwd: {
|
|
210
258
|
type: 'string',
|
|
211
259
|
description: 'Absolute path to the project root directory. Claude Code should pass its current working directory.',
|
|
@@ -262,8 +310,10 @@ async function handleRun(args) {
|
|
|
262
310
|
if (args.concurrency) configOverrides.concurrency = args.concurrency;
|
|
263
311
|
if (args.baseUrl) configOverrides.baseUrl = args.baseUrl;
|
|
264
312
|
if (args.retries !== undefined) configOverrides.retries = args.retries;
|
|
313
|
+
if (args.failOnNetworkError !== undefined) configOverrides.failOnNetworkError = args.failOnNetworkError;
|
|
265
314
|
|
|
266
315
|
const config = await loadConfig(configOverrides, args.cwd);
|
|
316
|
+
config.triggeredBy = 'mcp';
|
|
267
317
|
|
|
268
318
|
await waitForPool(config.poolUrl);
|
|
269
319
|
|
|
@@ -324,10 +374,28 @@ async function handleRun(args) {
|
|
|
324
374
|
.filter(r => r.networkErrors?.length > 0)
|
|
325
375
|
.map(r => ({ name: r.name, errors: r.networkErrors }));
|
|
326
376
|
|
|
377
|
+
const networkLogs = report.results
|
|
378
|
+
.filter(r => r.networkLogs?.length > 0)
|
|
379
|
+
.map(r => ({ name: r.name, requests: r.networkLogs }));
|
|
380
|
+
|
|
381
|
+
const verifications = report.results
|
|
382
|
+
.filter(r => r.expect && r.verificationScreenshot)
|
|
383
|
+
.map(r => ({
|
|
384
|
+
name: r.name,
|
|
385
|
+
expect: r.expect,
|
|
386
|
+
success: r.success,
|
|
387
|
+
screenshotHash: 'ss:' + computeScreenshotHash(r.verificationScreenshot),
|
|
388
|
+
}));
|
|
389
|
+
|
|
327
390
|
if (flaky.length > 0) summary.flaky = flaky;
|
|
328
391
|
if (failures.length > 0) summary.failures = failures;
|
|
329
392
|
if (consoleErrors.length > 0) summary.consoleErrors = consoleErrors;
|
|
330
393
|
if (networkErrors.length > 0) summary.networkErrors = networkErrors;
|
|
394
|
+
if (networkLogs.length > 0) summary.networkLogs = networkLogs;
|
|
395
|
+
if (verifications.length > 0) {
|
|
396
|
+
summary.verifications = verifications;
|
|
397
|
+
summary.verificationInstructions = 'For each verification, call e2e_screenshot with the screenshotHash to view the screenshot. Then compare what you see against the "expect" description. Report any mismatches as FAIL.';
|
|
398
|
+
}
|
|
331
399
|
|
|
332
400
|
return textResult(JSON.stringify(summary, null, 2));
|
|
333
401
|
}
|
|
@@ -441,6 +509,9 @@ async function handleIssue(args) {
|
|
|
441
509
|
return errorResult('ANTHROPIC_API_KEY is required for verify mode. Set it as an environment variable.');
|
|
442
510
|
}
|
|
443
511
|
|
|
512
|
+
if (args.authToken) config.authToken = args.authToken;
|
|
513
|
+
if (args.authStorageKey) config.authStorageKey = args.authStorageKey;
|
|
514
|
+
|
|
444
515
|
const result = await verifyIssue(args.url, config);
|
|
445
516
|
const status = result.bugConfirmed ? 'BUG CONFIRMED' : 'NOT REPRODUCIBLE';
|
|
446
517
|
const summary = {
|
|
@@ -467,6 +538,62 @@ async function handleIssue(args) {
|
|
|
467
538
|
return textResult(promptData.prompt);
|
|
468
539
|
}
|
|
469
540
|
|
|
541
|
+
async function handleCapture(args) {
|
|
542
|
+
if (!args.url) return errorResult('Missing required parameter: url');
|
|
543
|
+
|
|
544
|
+
const config = await loadConfig({}, args.cwd);
|
|
545
|
+
|
|
546
|
+
await waitForPool(config.poolUrl);
|
|
547
|
+
|
|
548
|
+
let browser;
|
|
549
|
+
try {
|
|
550
|
+
browser = await connectToPool(config.poolUrl);
|
|
551
|
+
const page = await browser.newPage();
|
|
552
|
+
await page.setViewport(config.viewport);
|
|
553
|
+
await page.goto(args.url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
554
|
+
|
|
555
|
+
if (args.selector) {
|
|
556
|
+
await page.waitForSelector(args.selector, { timeout: 10000 });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (args.delay && args.delay > 0) {
|
|
560
|
+
await new Promise(r => setTimeout(r, args.delay));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Build filename: sanitize and ensure .png
|
|
564
|
+
let filename = args.filename || `capture-${Date.now()}.png`;
|
|
565
|
+
filename = path.basename(filename);
|
|
566
|
+
if (!filename.endsWith('.png')) filename += '.png';
|
|
567
|
+
|
|
568
|
+
if (!fs.existsSync(config.screenshotsDir)) {
|
|
569
|
+
fs.mkdirSync(config.screenshotsDir, { recursive: true });
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const screenshotPath = path.join(config.screenshotsDir, filename);
|
|
573
|
+
await page.screenshot({ path: screenshotPath, fullPage: !!args.fullPage });
|
|
574
|
+
|
|
575
|
+
// Register hash in SQLite
|
|
576
|
+
const cwd = args.cwd || process.cwd();
|
|
577
|
+
const projectName = config.projectName || path.basename(cwd);
|
|
578
|
+
const projectId = ensureProject(cwd, projectName, config.screenshotsDir, config.testsDir);
|
|
579
|
+
const hash = computeScreenshotHash(screenshotPath);
|
|
580
|
+
registerScreenshotHash(hash, screenshotPath, projectId, null);
|
|
581
|
+
|
|
582
|
+
// Read image for response
|
|
583
|
+
const data = fs.readFileSync(screenshotPath);
|
|
584
|
+
const base64 = data.toString('base64');
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
content: [
|
|
588
|
+
{ type: 'text', text: `Screenshot saved: ${screenshotPath}\nHash: ss:${hash}` },
|
|
589
|
+
{ type: 'image', data: base64, mimeType: 'image/png' },
|
|
590
|
+
],
|
|
591
|
+
};
|
|
592
|
+
} finally {
|
|
593
|
+
if (browser) browser.disconnect();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
470
597
|
// Module-level state for stdio path only
|
|
471
598
|
let dashboardHandle = null;
|
|
472
599
|
|
|
@@ -521,6 +648,8 @@ export async function dispatchTool(name, args = {}) {
|
|
|
521
648
|
return await handleDashboardStop();
|
|
522
649
|
case 'e2e_issue':
|
|
523
650
|
return await handleIssue(args);
|
|
651
|
+
case 'e2e_capture':
|
|
652
|
+
return await handleCapture(args);
|
|
524
653
|
default:
|
|
525
654
|
return errorResult(`Unknown tool: ${name}`);
|
|
526
655
|
}
|
package/src/reporter.js
CHANGED
|
@@ -152,7 +152,7 @@ export function persistRun(report, config, suiteName) {
|
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
154
|
const projectId = ensureProject(config._cwd, config.projectName, config.screenshotsDir, config.testsDir);
|
|
155
|
-
saveRunToDb(projectId, report, runId, suiteName || null);
|
|
155
|
+
saveRunToDb(projectId, report, runId, suiteName || null, config.triggeredBy || null);
|
|
156
156
|
} catch (err) {
|
|
157
157
|
process.stderr.write(`[e2e-runner] SQLite write failed: ${err.message}\n`);
|
|
158
158
|
}
|
|
@@ -210,6 +210,18 @@ export function printReport(report, screenshotsDir) {
|
|
|
210
210
|
});
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
const networkRequests = report.results.filter(r => r.networkLogs?.length > 0);
|
|
214
|
+
if (networkRequests.length > 0) {
|
|
215
|
+
console.log(`\n${C.cyan}${C.bold}NETWORK REQUESTS:${C.reset}`);
|
|
216
|
+
networkRequests.forEach(r => {
|
|
217
|
+
console.log(` ${C.cyan}▸${C.reset} ${r.name}:`);
|
|
218
|
+
r.networkLogs.forEach(n => {
|
|
219
|
+
const statusColor = n.status < 300 ? C.green : n.status < 400 ? C.yellow : C.red;
|
|
220
|
+
console.log(` ${C.dim}${n.method}${C.reset} ${statusColor}${n.status}${C.reset} ${n.url} ${C.dim}(${n.duration}ms)${C.reset}`);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
213
225
|
if (screenshotsDir) {
|
|
214
226
|
console.log(`\n${C.dim}Report: ${path.join(screenshotsDir, 'report.json')}${C.reset}`);
|
|
215
227
|
console.log(`${C.dim}Screenshots: ${screenshotsDir}${C.reset}\n`);
|
package/src/runner.js
CHANGED
|
@@ -80,7 +80,9 @@ export async function runTest(test, config, hooks = {}, progressFn = () => {}) {
|
|
|
80
80
|
error: null,
|
|
81
81
|
consoleLogs: [],
|
|
82
82
|
networkErrors: [],
|
|
83
|
+
networkLogs: [],
|
|
83
84
|
};
|
|
85
|
+
const pendingBodies = [];
|
|
84
86
|
|
|
85
87
|
try {
|
|
86
88
|
await waitForSlot(config.poolUrl);
|
|
@@ -95,6 +97,37 @@ export async function runTest(test, config, hooks = {}, progressFn = () => {}) {
|
|
|
95
97
|
result.networkErrors.push({ url: req.url(), error: req.failure()?.errorText });
|
|
96
98
|
});
|
|
97
99
|
|
|
100
|
+
const requestTimings = new Map();
|
|
101
|
+
page.on('request', (req) => {
|
|
102
|
+
const rt = req.resourceType();
|
|
103
|
+
if (rt === 'xhr' || rt === 'fetch') requestTimings.set(req, Date.now());
|
|
104
|
+
});
|
|
105
|
+
page.on('response', (resp) => {
|
|
106
|
+
const req = resp.request();
|
|
107
|
+
const startMs = requestTimings.get(req);
|
|
108
|
+
if (startMs !== undefined) {
|
|
109
|
+
requestTimings.delete(req);
|
|
110
|
+
const entry = {
|
|
111
|
+
url: req.url(),
|
|
112
|
+
method: req.method(),
|
|
113
|
+
status: resp.status(),
|
|
114
|
+
statusText: resp.statusText(),
|
|
115
|
+
duration: Date.now() - startMs,
|
|
116
|
+
requestHeaders: req.headers(),
|
|
117
|
+
requestBody: null,
|
|
118
|
+
responseHeaders: resp.headers(),
|
|
119
|
+
responseBody: null,
|
|
120
|
+
};
|
|
121
|
+
try { entry.requestBody = req.postData() || null; } catch { /* */ }
|
|
122
|
+
result.networkLogs.push(entry);
|
|
123
|
+
// Read response body async — collect promise for later flush
|
|
124
|
+
const bodyPromise = resp.text().then(body => {
|
|
125
|
+
entry.responseBody = body && body.length > 51200 ? body.slice(0, 51200) + '\n...[truncated]' : body;
|
|
126
|
+
}).catch(() => { /* response may be unavailable */ });
|
|
127
|
+
pendingBodies.push(bodyPromise);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
98
131
|
// Run beforeEach hook
|
|
99
132
|
if (hooks.beforeEach?.length) {
|
|
100
133
|
await executeHookActions(page, hooks.beforeEach, config);
|
|
@@ -104,7 +137,17 @@ export async function runTest(test, config, hooks = {}, progressFn = () => {}) {
|
|
|
104
137
|
const action = test.actions[i];
|
|
105
138
|
const actionStart = Date.now();
|
|
106
139
|
try {
|
|
107
|
-
|
|
140
|
+
let actionResult;
|
|
141
|
+
if (action.type === 'assert_no_network_errors') {
|
|
142
|
+
// Handled inline — needs access to result.networkErrors
|
|
143
|
+
if (result.networkErrors.length > 0) {
|
|
144
|
+
const summary = result.networkErrors.map(e => `${e.url} (${e.error})`).join(', ');
|
|
145
|
+
throw new Error(`assert_no_network_errors failed: ${result.networkErrors.length} error(s): ${summary}`);
|
|
146
|
+
}
|
|
147
|
+
actionResult = null;
|
|
148
|
+
} else {
|
|
149
|
+
actionResult = await executeAction(page, action, config);
|
|
150
|
+
}
|
|
108
151
|
const actionDuration = Date.now() - actionStart;
|
|
109
152
|
result.actions.push({
|
|
110
153
|
...action,
|
|
@@ -126,6 +169,23 @@ export async function runTest(test, config, hooks = {}, progressFn = () => {}) {
|
|
|
126
169
|
}
|
|
127
170
|
}
|
|
128
171
|
|
|
172
|
+
// Fail the test if failOnNetworkError is enabled and network errors occurred
|
|
173
|
+
if (config.failOnNetworkError && result.networkErrors.length > 0) {
|
|
174
|
+
const summary = result.networkErrors.map(e => `${e.url} (${e.error})`).join(', ');
|
|
175
|
+
throw new Error(`Network errors detected (failOnNetworkError=true): ${result.networkErrors.length} error(s): ${summary}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Auto-capture verification screenshot if test has "expect"
|
|
179
|
+
if (test.expect && page) {
|
|
180
|
+
result.expect = test.expect;
|
|
181
|
+
try {
|
|
182
|
+
const safeName = test.name.replace(/[^a-zA-Z0-9_\-. ]/g, '_');
|
|
183
|
+
const verifyPath = path.join(config.screenshotsDir, `verify-${safeName}-${Date.now()}.png`);
|
|
184
|
+
await page.screenshot({ path: verifyPath, fullPage: true });
|
|
185
|
+
result.verificationScreenshot = verifyPath;
|
|
186
|
+
} catch { /* page may be dead */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
129
189
|
// Run afterEach hook (success path)
|
|
130
190
|
if (hooks.afterEach?.length) {
|
|
131
191
|
await executeHookActions(page, hooks.afterEach, config);
|
|
@@ -148,6 +208,10 @@ export async function runTest(test, config, hooks = {}, progressFn = () => {}) {
|
|
|
148
208
|
} catch { /* page may be dead */ }
|
|
149
209
|
}
|
|
150
210
|
} finally {
|
|
211
|
+
// Flush pending response body reads before disconnecting
|
|
212
|
+
if (pendingBodies.length > 0) {
|
|
213
|
+
try { await Promise.allSettled(pendingBodies); } catch { /* */ }
|
|
214
|
+
}
|
|
151
215
|
result.endTime = new Date().toISOString();
|
|
152
216
|
if (page) {
|
|
153
217
|
try { result.finalUrl = page.url(); } catch { /* */ }
|
|
@@ -186,7 +250,8 @@ export async function runTestsParallel(tests, config, suiteHooks = {}) {
|
|
|
186
250
|
const _runId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
187
251
|
const _proj = config.projectName || null;
|
|
188
252
|
const _cwd = config._cwd || null;
|
|
189
|
-
const
|
|
253
|
+
const _triggeredBy = config.triggeredBy || 'unknown';
|
|
254
|
+
const _progress = (data) => config.onProgress && config.onProgress({ ...data, runId: _runId, project: _proj, cwd: _cwd, triggeredBy: _triggeredBy });
|
|
190
255
|
_progress({ event: 'run:start', total: tests.length, concurrency, timestamp: new Date().toISOString() });
|
|
191
256
|
|
|
192
257
|
const results = [];
|
|
@@ -222,6 +287,7 @@ export async function runTestsParallel(tests, config, suiteHooks = {}) {
|
|
|
222
287
|
error: error.message,
|
|
223
288
|
consoleLogs: [],
|
|
224
289
|
networkErrors: [],
|
|
290
|
+
networkLogs: [],
|
|
225
291
|
};
|
|
226
292
|
}
|
|
227
293
|
|
|
@@ -238,7 +304,7 @@ export async function runTestsParallel(tests, config, suiteHooks = {}) {
|
|
|
238
304
|
activeCount--;
|
|
239
305
|
|
|
240
306
|
const screenshots = result.actions.filter(a => a.result?.screenshot).map(a => a.result.screenshot);
|
|
241
|
-
_progress({ event: 'test:complete', name: test.name, success: result.success, duration: timeDiff(result.startTime, result.endTime), error: result.error, consoleLogs: result.consoleLogs, networkErrors: result.networkErrors, errorScreenshot: result.errorScreenshot, screenshots });
|
|
307
|
+
_progress({ event: 'test:complete', name: test.name, success: result.success, duration: timeDiff(result.startTime, result.endTime), error: result.error, consoleLogs: result.consoleLogs, networkErrors: result.networkErrors, networkLogs: result.networkLogs, errorScreenshot: result.errorScreenshot, screenshots });
|
|
242
308
|
|
|
243
309
|
if (result.success) {
|
|
244
310
|
const flaky = result.attempt > 1 ? ` ${C.yellow}(flaky, passed on attempt ${result.attempt}/${result.maxAttempts})${C.reset}` : '';
|
package/src/verify.js
CHANGED
|
@@ -35,19 +35,31 @@ export async function verifyIssue(url, config) {
|
|
|
35
35
|
fs.writeFileSync(testFile, JSON.stringify(tests, null, 2));
|
|
36
36
|
|
|
37
37
|
try {
|
|
38
|
-
// 4.
|
|
38
|
+
// 4. Build hooks (inject auth if provided)
|
|
39
|
+
const hooks = {};
|
|
40
|
+
if (config.authToken) {
|
|
41
|
+
const storageKey = config.authStorageKey || 'accessToken';
|
|
42
|
+
hooks.beforeEach = [
|
|
43
|
+
{ type: 'goto', value: '/' },
|
|
44
|
+
{ type: 'evaluate', value: `localStorage.setItem('${storageKey}', '${config.authToken}')` },
|
|
45
|
+
{ type: 'goto', value: '/' },
|
|
46
|
+
{ type: 'wait', value: '1000' },
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 5. Wait for pool and run
|
|
39
51
|
await waitForPool(config.poolUrl);
|
|
40
|
-
const results = await runTestsParallel(tests, config,
|
|
52
|
+
const results = await runTestsParallel(tests, config, hooks);
|
|
41
53
|
const report = generateReport(results);
|
|
42
54
|
saveReport(report, config.screenshotsDir, config);
|
|
43
55
|
persistRun(report, config, suiteName);
|
|
44
56
|
|
|
45
|
-
//
|
|
57
|
+
// 6. Interpret results
|
|
46
58
|
const bugConfirmed = report.summary.failed > 0;
|
|
47
59
|
|
|
48
60
|
return { issue, report, bugConfirmed, tests, suiteName };
|
|
49
61
|
} finally {
|
|
50
|
-
//
|
|
62
|
+
// 7. Clean up temp file
|
|
51
63
|
try { fs.unlinkSync(testFile); } catch { /* already gone */ }
|
|
52
64
|
}
|
|
53
65
|
}
|