jest-roblox-assassin 1.0.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/src/docs.js ADDED
@@ -0,0 +1,101 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { ensureCache } from "./cache.js";
5
+
6
+ const DOCS_URL =
7
+ "https://raw.githubusercontent.com/Roblox/jest-roblox/master/docs/docs/CLI.md";
8
+
9
+ export async function getCliOptions() {
10
+ const cliOptionsPath = path.join(ensureCache(), "cli-options.json");
11
+
12
+ // Check if cached JSON exists
13
+ if (fs.existsSync(cliOptionsPath)) {
14
+ try {
15
+ const cached = JSON.parse(fs.readFileSync(cliOptionsPath, "utf-8"));
16
+ return cached;
17
+ } catch (error) {
18
+ console.warn(
19
+ "Failed to read cached CLI options, fetching fresh..."
20
+ );
21
+ }
22
+ }
23
+
24
+ // Fetch and parse
25
+ try {
26
+ const response = await fetch(DOCS_URL);
27
+ if (!response.ok) {
28
+ throw new Error(
29
+ `Failed to fetch documentation: ${response.statusText}`
30
+ );
31
+ }
32
+ const markdown = await response.text();
33
+ const options = parseMarkdown(markdown);
34
+
35
+ // Save to JSON for future use
36
+ fs.mkdirSync(path.dirname(cliOptionsPath), { recursive: true });
37
+ fs.writeFileSync(cliOptionsPath, JSON.stringify(options, null, 2));
38
+
39
+ return options;
40
+ } catch (error) {
41
+ console.error(
42
+ chalk.red(`Error fetching documentation: ${error.message}`)
43
+ );
44
+ return [];
45
+ }
46
+ }
47
+
48
+ export async function showHelp() {
49
+ const options = await getCliOptions();
50
+
51
+ console.log("Usage: jestrbx [TestPathPatterns]");
52
+ console.log("");
53
+
54
+ for (const doc of options) {
55
+ console.log(`${chalk.green.bold(doc.name)} ${chalk.cyan(doc.type)}`);
56
+ console.log(`${doc.description.trim()}`);
57
+ console.log("");
58
+ }
59
+
60
+ console.log(
61
+ chalk.gray("Source: https://roblox.github.io/jest-roblox-internal/cli")
62
+ );
63
+ }
64
+
65
+ function parseMarkdown(markdown) {
66
+ const options = [];
67
+
68
+ // Remove HTML comments
69
+ const cleanMarkdown = markdown.replace(/<!--[\s\S]*?-->/g, "");
70
+
71
+ // Match ### `optionName` [type]
72
+ const sectionRegex = /### `([^`]+)` \\?\[([^\]]+)\]([\s\S]*?)(?=\n### |$)/g;
73
+ let match;
74
+
75
+ while ((match = sectionRegex.exec(cleanMarkdown)) !== null) {
76
+ const name = match[1];
77
+ let type = match[2];
78
+ let description = match[3];
79
+
80
+ // Decode HTML entities in type
81
+ type = type.replace(/&lt;?/g, "<").replace(/&gt;?/g, ">");
82
+
83
+ // Remove images and links like [![Jest](/img/jestjs.svg)](...) ![Aligned](/img/aligned.svg)
84
+ description = description.replace(/\[!\[.*?\]\(.*?\)\]\(.*?\)/g, "");
85
+ description = description.replace(/!\[.*?\]\(.*?\)/g, "");
86
+
87
+ // Remove tip blocks
88
+ description = description.replace(/:::tip[\s\S]*?:::/g, "");
89
+
90
+ // Remove extra whitespace and newlines
91
+ description = description.trim();
92
+
93
+ options.push({
94
+ name: `--${name}`,
95
+ type: `[${type}]`,
96
+ description,
97
+ });
98
+ }
99
+
100
+ return options;
101
+ }
@@ -0,0 +1,562 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ export class ResultRewriter {
6
+ INTERNAL_FRAME_PATTERNS = [
7
+ /rbxts_include\.node_modules/i,
8
+ /@rbxts-js\.Promise/i,
9
+ /@rbxts-js\.JestCircus/i,
10
+ ];
11
+
12
+ constructor({
13
+ workspaceRoot,
14
+ projectRoot,
15
+ rootDir,
16
+ outDir,
17
+ datamodelPrefixSegments,
18
+ }) {
19
+ this.workspaceRoot = workspaceRoot || projectRoot;
20
+ this.projectRoot = projectRoot;
21
+ this.rootDir = rootDir;
22
+ this.outDir = outDir;
23
+ this.datamodelPrefixSegments = datamodelPrefixSegments;
24
+
25
+ const firstSegment = this.datamodelPrefixSegments[0];
26
+ this.stackFramePattern = new RegExp(
27
+ `(?:\\[string\\s+")?(${firstSegment}[^\\]":\\n]+)(?:"\\])?:([0-9]+)`,
28
+ "g"
29
+ );
30
+
31
+ /**
32
+ * A map from datamodel paths to their corresponding Luau and source file paths.
33
+ * @type {Map<string, { luauPath: string, sourcePath: string | undefined }>}
34
+ */
35
+ this.modulePathMap = (() => {
36
+ const map = new Map();
37
+ const outRoot = path.join(this.projectRoot, this.outDir);
38
+ if (!fs.existsSync(outRoot)) return map;
39
+
40
+ function visit(folder) {
41
+ for (const entry of fs.readdirSync(folder, {
42
+ withFileTypes: true,
43
+ })) {
44
+ const abs = path.join(folder, entry.name);
45
+ if (entry.isDirectory()) {
46
+ visit(abs);
47
+ continue;
48
+ }
49
+ if (!entry.name.endsWith(".luau")) continue;
50
+
51
+ const rel = path.relative(outRoot, abs);
52
+ const withoutExt = rel.slice(0, -".luau".length);
53
+ const datamodelPath = [
54
+ ...datamodelPrefixSegments,
55
+ ...withoutExt.split(path.sep),
56
+ ].join(".");
57
+
58
+ const candidateBases = [
59
+ withoutExt + ".ts",
60
+ withoutExt + ".tsx",
61
+ withoutExt + ".lua",
62
+ withoutExt + ".luau",
63
+ ];
64
+ let sourcePath;
65
+ for (const candidate of candidateBases) {
66
+ const candidatePath = path.join(
67
+ projectRoot,
68
+ rootDir,
69
+ candidate
70
+ );
71
+ if (fs.existsSync(candidatePath)) {
72
+ sourcePath = candidatePath;
73
+ break;
74
+ }
75
+ }
76
+
77
+ map.set(datamodelPath, {
78
+ luauPath: abs,
79
+ sourcePath,
80
+ });
81
+ }
82
+ }
83
+
84
+ visit(outRoot);
85
+ return map;
86
+ })();
87
+ }
88
+
89
+ fileCache = new Map();
90
+
91
+ /**
92
+ * Reads lines from a file, with caching.
93
+ * @param {string} filePath The file path to read.
94
+ * @returns {string[]} The lines of the file.
95
+ */
96
+ readLines(filePath) {
97
+ if (!filePath || !fs.existsSync(filePath)) return [];
98
+ if (this.fileCache.has(filePath)) return this.fileCache.get(filePath);
99
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
100
+ this.fileCache.set(filePath, lines);
101
+ return lines;
102
+ }
103
+
104
+ /**
105
+ * Finds the corresponding source line for a given Luau line.
106
+ * @param {string} luauPath The path to the Luau file.
107
+ * @param {string} sourcePath The path to the source file.
108
+ * @param {number} luauLineNumber The line number in the Luau file.
109
+ * @returns {number} The corresponding line number in the source file.
110
+ */
111
+ findSourceLine(luauPath, sourcePath, luauLineNumber) {
112
+ const luauLines = this.readLines(luauPath);
113
+ const sourceLines = this.readLines(sourcePath);
114
+ if (!luauLines.length || !sourceLines.length) return luauLineNumber;
115
+
116
+ const target = (luauLines[luauLineNumber - 1] || "").trim();
117
+ if (!target) return luauLineNumber;
118
+ const normalizedTarget = target.replace(/\s+/g, "");
119
+
120
+ const exactIndex = sourceLines.findIndex(
121
+ (line) => line.trim() === target
122
+ );
123
+ if (exactIndex >= 0) return exactIndex + 1;
124
+
125
+ const looseIndex = sourceLines.findIndex((line) =>
126
+ line.replace(/\s+/g, "").includes(normalizedTarget)
127
+ );
128
+ if (looseIndex >= 0) return looseIndex + 1;
129
+
130
+ return luauLineNumber;
131
+ }
132
+
133
+ /**
134
+ * Formats a file path relative to the project root with forward slashes.
135
+ * @param {string} filePath The file path to format.
136
+ * @returns {string} The formatted path.
137
+ */
138
+ formatPath(filePath) {
139
+ return path
140
+ .relative(this.workspaceRoot, filePath)
141
+ .split(path.sep)
142
+ .join("/")
143
+ .replace(/\\/g, "/");
144
+ }
145
+
146
+ /**
147
+ * Maps a datamodel stack frame to its source location.
148
+ * @param {string} datamodelPath The path in the datamodel.
149
+ * @param {number} lineNumber The line number in the datamodel file.
150
+ * @returns {string | undefined} The mapped source location as "path:line" or undefined if not found.
151
+ */
152
+ mapDatamodelFrame(datamodelPath, lineNumber) {
153
+ const entry = this.modulePathMap.get(datamodelPath);
154
+ if (!entry) return undefined;
155
+ const mappedLine = this.findSourceLine(
156
+ entry.luauPath,
157
+ entry.sourcePath,
158
+ lineNumber
159
+ );
160
+ const displayPath = entry.sourcePath
161
+ ? this.formatPath(entry.sourcePath)
162
+ : datamodelPath;
163
+ return `${displayPath}:${mappedLine}`;
164
+ }
165
+
166
+ /**
167
+ * Cleans internal stack trace lines from a text block.
168
+ * @param {string} text The text block to clean.
169
+ * @returns {string} The cleaned text block.
170
+ */
171
+ cleanInternalLines(text) {
172
+ if (!text) return text;
173
+ const lines = text.split(/\r?\n/);
174
+ const kept = lines.filter(
175
+ (line) =>
176
+ !this.INTERNAL_FRAME_PATTERNS.some((pat) => pat.test(line))
177
+ );
178
+ const squashed = kept.join("\n").replace(/\n{3,}/g, "\n\n");
179
+ return squashed.trimEnd();
180
+ }
181
+
182
+ /**
183
+ * Formats failure messages with syntax highlighting and colors.
184
+ * @param {string} text The failure message text.
185
+ * @returns {string} The formatted failure message.
186
+ */
187
+ formatFailureMessage(text) {
188
+ if (!text) return text;
189
+
190
+ // Color test header lines (e.g., "● test name")
191
+ text = text.replace(/^(\s*●.*)$/gm, (match) => chalk.bold.red(match));
192
+
193
+ // Fix indentation and color Expected: and Received: lines
194
+ text = text.replace(
195
+ /^\s+Expected:(.*)$/gm,
196
+ (match, value) => `\n Expected:${chalk.green(value)}`
197
+ );
198
+ text = text.replace(
199
+ /^\s+Received:(.*)$/gm,
200
+ (match, value) => ` Received:${chalk.red(value)}`
201
+ );
202
+
203
+ // Color expect assertions
204
+ text = text.replace(
205
+ /expect\((received)\)\.(\w+)\((expected)\)(\s*--\s*.+)?/g,
206
+ (match, receivedWord, method, expectedWord, description) => {
207
+ const colored =
208
+ chalk.gray("expect(") +
209
+ chalk.red(receivedWord) +
210
+ chalk.gray(").") +
211
+ chalk.white(method) +
212
+ chalk.gray("(") +
213
+ chalk.green(expectedWord) +
214
+ chalk.gray(")") +
215
+ (description ? chalk.gray(description) : "");
216
+ return colored;
217
+ }
218
+ );
219
+
220
+ // Color stack trace file paths
221
+ text = text.replace(
222
+ /((?:[\w@.\/\\-]*[\/\.\\][\w@.\/\\-]+)):(\d+)(?::(\d+))?/g,
223
+ (match, filePart, line, col) => {
224
+ const lineCol = chalk.gray(`:${line}${col ? `:${col}` : ""}`);
225
+ return `${chalk.cyan(filePart)}${lineCol}`;
226
+ }
227
+ );
228
+
229
+ return text;
230
+ }
231
+
232
+ /**
233
+ * Rewrites stack strings to map datamodel paths to source paths.
234
+ * @param {string} value The stack string to rewrite.
235
+ * @returns {string} The rewritten stack string.
236
+ */
237
+ rewriteStackString(value) {
238
+ return typeof value === "string"
239
+ ? this.cleanInternalLines(
240
+ value.replace(
241
+ this.stackFramePattern,
242
+ (match, dmPath, line) => {
243
+ const mapped = this.mapDatamodelFrame(
244
+ dmPath,
245
+ Number(line)
246
+ );
247
+ return mapped || match;
248
+ }
249
+ )
250
+ )
251
+ : value;
252
+ }
253
+
254
+ /**
255
+ * Rewrites a test case's failure messages and details.
256
+ * @param {object} testCase The test case to rewrite.
257
+ */
258
+ rewriteTestCase(testCase) {
259
+ if (Array.isArray(testCase.failureMessages)) {
260
+ const rewritten = testCase.failureMessages.map(
261
+ this.rewriteStackString.bind(this)
262
+ );
263
+ let candidateFrame;
264
+
265
+ // Search all failure messages for a valid file frame
266
+ for (const msg of rewritten) {
267
+ const frame = this.parseFrame(msg);
268
+ if (frame) {
269
+ candidateFrame = frame;
270
+ break;
271
+ }
272
+ }
273
+
274
+ // Fallback to failureDetails if no frame found in messages
275
+ if (!candidateFrame && Array.isArray(testCase.failureDetails)) {
276
+ for (const detail of testCase.failureDetails) {
277
+ const stack =
278
+ this.rewriteStackString(detail.stack) ||
279
+ this.rewriteStackString(detail.__stack) ||
280
+ this.rewriteStackString(detail.message);
281
+ const frame = this.parseFrame(stack);
282
+ if (frame) {
283
+ candidateFrame = frame;
284
+ break;
285
+ }
286
+ }
287
+ }
288
+
289
+ const codeFrame = candidateFrame
290
+ ? this.buildCodeFrame(
291
+ candidateFrame.absPath,
292
+ candidateFrame.line,
293
+ candidateFrame.column
294
+ )
295
+ : undefined;
296
+
297
+ // Only append the code frame to the first message to avoid duplication
298
+ if (codeFrame && rewritten.length > 0) {
299
+ rewritten[0] = this.injectCodeFrame(
300
+ rewritten[0],
301
+ candidateFrame,
302
+ codeFrame
303
+ );
304
+ }
305
+
306
+ testCase.failureMessages = rewritten.map(
307
+ this.formatFailureMessage.bind(this)
308
+ );
309
+ }
310
+ if (Array.isArray(testCase.failureDetails)) {
311
+ testCase.failureDetails = testCase.failureDetails.map((detail) => ({
312
+ ...detail,
313
+ stack: this.formatFailureMessage(
314
+ this.rewriteStackString(detail.stack)
315
+ ),
316
+ __stack: this.formatFailureMessage(
317
+ this.rewriteStackString(detail.__stack)
318
+ ),
319
+ message: this.formatFailureMessage(
320
+ this.rewriteStackString(detail.message)
321
+ ),
322
+ }));
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Extends a test file path by checking for common extensions.
328
+ * @param {string} testFilePath The original test file path.
329
+ * @returns {string} The extended test file path.
330
+ */
331
+ extendTestFilePath(testFilePath) {
332
+ if (!testFilePath) return testFilePath;
333
+ const withExts = [".ts", ".tsx", ""]; // last entry preserves original
334
+ for (const ext of withExts) {
335
+ const candidate = path.join(
336
+ this.projectRoot,
337
+ `${testFilePath}${ext}`
338
+ );
339
+ if (fs.existsSync(candidate)) return candidate;
340
+ }
341
+ return path.join(this.projectRoot, testFilePath);
342
+ }
343
+
344
+ /**
345
+ * Strips ANSI escape codes from a string.
346
+ * @param {string} str The string to strip.
347
+ * @returns {string} The stripped string.
348
+ */
349
+ stripAnsi(str) {
350
+ return typeof str === "string"
351
+ ? str.replace(/\u001b\[[0-9;]*m/g, "")
352
+ : str;
353
+ }
354
+
355
+ /**
356
+ * Parses a stack frame from text and returns file path, line, and column.
357
+ * @param {string} text The text to parse.
358
+ * @returns {{ absPath: string, line: number, column: number } | undefined} The parsed frame info or undefined if not found.
359
+ */
360
+ parseFrame(text) {
361
+ if (!text) return undefined;
362
+ const cleanText = this.stripAnsi(text);
363
+ // Match patterns like "src/test.ts:10" or "C:\path\test.ts:10"
364
+ // We look for something that looks like a file path followed by a colon and a number
365
+ const pattern =
366
+ /(?:^|\s|")((?:[a-zA-Z]:[\\\/][^:\s\n"]+|[\w@.\/\\-]+\.[a-z0-9]+)):(\d+)(?::(\d+))?/gi;
367
+ let match;
368
+ const candidates = [];
369
+ while ((match = pattern.exec(cleanText)) !== null) {
370
+ const [, filePart, lineStr, colStr] = match;
371
+ const absPath = path.isAbsolute(filePart)
372
+ ? filePart
373
+ : path.join(this.workspaceRoot, filePart);
374
+ if (fs.existsSync(absPath)) {
375
+ candidates.push({
376
+ absPath,
377
+ line: Number(lineStr),
378
+ column: colStr ? Number(colStr) : 1,
379
+ score:
380
+ (filePart.includes(".") ? 1 : 0) +
381
+ (filePart.includes("/") || filePart.includes("\\")
382
+ ? 1
383
+ : 0),
384
+ });
385
+ }
386
+ }
387
+
388
+ if (candidates.length === 0) return undefined;
389
+ // Sort by score descending, then by line number (prefer higher line numbers which are usually the actual error)
390
+ return candidates.sort(
391
+ (a, b) => b.score - a.score || b.line - a.line
392
+ )[0];
393
+ }
394
+
395
+ /**
396
+ * Syntax highlights code for terminal output.
397
+ * @param {string} text The code text to highlight.
398
+ * @returns {string} The highlighted code.
399
+ */
400
+ highlightCode(text) {
401
+ if (!text) return text;
402
+ return text.replace(
403
+ /((["'`])(?:(?=(\\?))\3.)*?\2)|(\b\d+(?:\.\d+)?\b)|(=>|[,\.\+\-\*\/%=<>!?:;&|\[\]])/g,
404
+ (match, string, quote, escape, number, punctuation) => {
405
+ if (string) return chalk.green(match);
406
+ if (number) return chalk.magenta(match);
407
+ if (punctuation) return chalk.yellow(match);
408
+ return match;
409
+ }
410
+ );
411
+ }
412
+
413
+ /**
414
+ * Builds a code frame for a given file and line.
415
+ * @param {string} absPath The absolute file path.
416
+ * @param {number} line The line to build the frame around.
417
+ * @param {number} column The column to point to.
418
+ * @param {number} context The number of context lines to include.
419
+ * @returns {string|undefined} The code frame as a string, or undefined if file not found.
420
+ */
421
+ buildCodeFrame(absPath, line, column = 1, context = 2) {
422
+ const lines = this.readLines(absPath);
423
+ if (!lines.length) return undefined;
424
+ const start = Math.max(1, line - context);
425
+ const end = Math.min(lines.length, line + context + 1);
426
+ const frame = [];
427
+
428
+ for (let i = start; i <= end; i++) {
429
+ const isBright = i === start;
430
+ const lineNum = String(i).padStart(String(end).length, " ");
431
+ const gutter = `${
432
+ i === line ? chalk.bold.red(">") : " "
433
+ } ${chalk.grey(lineNum + " |")}`;
434
+ const rawContent = lines[i - 1] || "";
435
+ const highlightedContent = this.highlightCode(rawContent);
436
+ const content = ` ${gutter} ${highlightedContent}`;
437
+ frame.push(isBright ? content : chalk.dim(content));
438
+ }
439
+ return frame.join("\n");
440
+ }
441
+
442
+ /**
443
+ * Appends code frames to messages.
444
+ * @param {Array|string} messages The messages to append to.
445
+ * @param {string} frameText The code frame text to append.
446
+ * @returns {Array|string} The messages with appended code frames.
447
+ */
448
+ appendCodeFrames(messages, frameText) {
449
+ if (!Array.isArray(messages) || !frameText) return messages;
450
+ return messages.map((msg) =>
451
+ typeof msg === "string" ? `${msg}\n\n${frameText}` : msg
452
+ );
453
+ }
454
+
455
+ /**
456
+ * Injects a code frame into a text block, moving the corresponding stack frame line to the bottom.
457
+ * @param {string} text The text block.
458
+ * @param {object} frame The parsed frame info.
459
+ * @param {string} codeFrame The code frame text.
460
+ * @returns {string} The updated text block.
461
+ */
462
+ injectCodeFrame(text, frame, codeFrame) {
463
+ if (!codeFrame || !frame) return text;
464
+
465
+ const lines = text.split(/\r?\n/);
466
+ let frameLine = "";
467
+ let frameLineIndex = -1;
468
+
469
+ const displayPath = this.formatPath(frame.absPath);
470
+ const searchPath1 = `${displayPath}:${frame.line}`;
471
+ const searchPath2 = `${frame.absPath}:${frame.line}`;
472
+
473
+ for (let i = 0; i < lines.length; i++) {
474
+ const line = lines[i];
475
+ if (line.includes(searchPath1) || line.includes(searchPath2)) {
476
+ frameLine = line;
477
+ frameLineIndex = i;
478
+ break;
479
+ }
480
+ }
481
+
482
+ if (frameLineIndex !== -1) {
483
+ lines.splice(frameLineIndex, 1);
484
+ return `${lines
485
+ .join("\n")
486
+ .trimEnd()}\n\n${codeFrame}\n\n${frameLine}`;
487
+ }
488
+
489
+ return `${text.trimEnd()}\n\n${codeFrame}`;
490
+ }
491
+
492
+ /**
493
+ * Rewrites a test suite's results.
494
+ * @param {object} suite The test suite result to rewrite.
495
+ */
496
+ rewriteSuiteResult(suite) {
497
+ if (!suite) return;
498
+ suite.testFilePath = this.formatPath(
499
+ this.extendTestFilePath(suite.testFilePath)
500
+ );
501
+
502
+ if (Array.isArray(suite.testResults)) {
503
+ suite.testResults.forEach((value) => {
504
+ this.rewriteTestCase(value);
505
+ });
506
+ }
507
+
508
+ if (suite.failureMessage) {
509
+ let rewritten = this.rewriteStackString(suite.failureMessage);
510
+
511
+ // Split by the test header " ● " to handle multiple failures in one string
512
+ const sections = rewritten.split(/(\s+●\s+)/);
513
+ if (sections.length > 1) {
514
+ for (let i = 2; i < sections.length; i += 2) {
515
+ const sectionContent = sections[i];
516
+ const frame = this.parseFrame(sectionContent);
517
+ if (frame) {
518
+ const codeFrame = this.buildCodeFrame(
519
+ frame.absPath,
520
+ frame.line,
521
+ frame.column
522
+ );
523
+ sections[i] =
524
+ this.injectCodeFrame(
525
+ sectionContent,
526
+ frame,
527
+ codeFrame
528
+ ) + "\n";
529
+ }
530
+ }
531
+ rewritten = sections.join("");
532
+ } else {
533
+ const frame = this.parseFrame(rewritten);
534
+ if (frame) {
535
+ const codeFrame = this.buildCodeFrame(
536
+ frame.absPath,
537
+ frame.line,
538
+ frame.column
539
+ );
540
+ rewritten = this.injectCodeFrame(
541
+ rewritten,
542
+ frame,
543
+ codeFrame
544
+ ) + "\n";
545
+ }
546
+ }
547
+
548
+ suite.failureMessage = this.formatFailureMessage(rewritten);
549
+ }
550
+ }
551
+
552
+ /**
553
+ * Rewrites parsed test results.
554
+ * @param {object} results The parsed test results.
555
+ */
556
+ rewriteParsedResults(results) {
557
+ if (!results?.testResults) return;
558
+ for (const suite of results.testResults) {
559
+ this.rewriteSuiteResult(suite);
560
+ }
561
+ }
562
+ }