@checksum-ai/runtime 4.18.1 → 5.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.
@@ -0,0 +1,600 @@
1
+ const fs = require("fs");
2
+ const crypto = require("crypto");
3
+ const { join } = require("path");
4
+
5
+ // Args
6
+ const on = process.argv[2] !== "off";
7
+
8
+ // -------- [Modifiers] -------- //
9
+
10
+ // Amends the file with the given entry point text and append text
11
+ // When "on" is true, the append text is added to the entry point,
12
+ // otherwise the append text is completely removed from the file
13
+ function amend(filePath, entryPointText, appendText) {
14
+ const data = fs.readFileSync(filePath, "utf8");
15
+ if (!data.includes(entryPointText)) {
16
+ throw new Error("Entry point not found!", entryPointText);
17
+ }
18
+ // Ignore if the append text is already present
19
+ if (on && data.includes(appendText)) {
20
+ return;
21
+ }
22
+ // Add or clear according to on state
23
+ const result = on
24
+ ? data.replace(entryPointText, entryPointText + appendText)
25
+ : data.replace(appendText, "");
26
+
27
+ // Write
28
+ fs.writeFileSync(filePath, result, "utf8");
29
+ }
30
+
31
+ // Bracket each injected block with a per-patch unique marker derived from a
32
+ // hash of `originalContent`. This guarantees OFF mode reverts only the patch
33
+ // that produced the marker, instead of accidentally matching another patch's
34
+ // shared `/* checksumai */ ` prefix (which previously caused
35
+ // `alwaysInjectScripts` OFF — whose newContent collapses to the bare marker —
36
+ // to clobber the FIRST `/* checksumai */ ` it found, corrupting whichever
37
+ // patch happened to live at the lowest line number).
38
+ //
39
+ // Backward-compat for the legacy single-marker format: in OFF mode, if the
40
+ // new bracketed form is not present but the legacy `/* checksumai */ <body>`
41
+ // form is, revert that and exit. New ON installs always write the bracketed
42
+ // form.
43
+ function replaceContent(filePath, originalContent, newContent) {
44
+ const fileContent = fs.readFileSync(filePath, "utf8");
45
+
46
+ const id = crypto
47
+ .createHash("sha1")
48
+ .update(originalContent)
49
+ .digest("hex")
50
+ .slice(0, 8);
51
+ const openMarker = `/*checksumai:${id}*/`;
52
+ const closeMarker = `/*checksumai:end:${id}*/`;
53
+ const taggedNewContent = `${openMarker}${newContent}${closeMarker}`;
54
+
55
+ if (on) {
56
+ if (fileContent.includes(taggedNewContent)) {
57
+ // Already patched.
58
+ return;
59
+ }
60
+ if (!fileContent.includes(originalContent)) {
61
+ // Nothing to replace. Either already migrated to a different shape or
62
+ // the playwright internals changed and this patch needs updating.
63
+ return;
64
+ }
65
+ fs.writeFileSync(
66
+ filePath,
67
+ fileContent.replace(originalContent, taggedNewContent),
68
+ "utf8"
69
+ );
70
+ return;
71
+ }
72
+
73
+ // OFF: precise revert via the unique bracketed block.
74
+ if (fileContent.includes(taggedNewContent)) {
75
+ fs.writeFileSync(
76
+ filePath,
77
+ fileContent.replace(taggedNewContent, originalContent),
78
+ "utf8"
79
+ );
80
+ return;
81
+ }
82
+
83
+ // OFF (legacy): older patched files used `/* checksumai */ ${newContent}`
84
+ // with a shared marker. Revert that exact body if present so users coming
85
+ // from a previous runtime version aren't stranded with stale injections.
86
+ const legacyTagged = `/* checksumai */ ${newContent}`;
87
+ if (fileContent.includes(legacyTagged)) {
88
+ fs.writeFileSync(
89
+ filePath,
90
+ fileContent.replace(legacyTagged, originalContent),
91
+ "utf8"
92
+ );
93
+ }
94
+ }
95
+
96
+ function doesFileExist(filePath) {
97
+ if (!fs.existsSync(filePath)) {
98
+ console.warn("File not found", filePath);
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ // -------- [Modifications] -------- //
105
+
106
+ // File targets are unchanged from 1.60 (the big lib/ -> esbuild-bundle
107
+ // collapse landed there):
108
+ // playwright-core/lib/coreBundle.js
109
+ // playwright/lib/worker/workerProcessEntry.js
110
+ // playwright/lib/common/index.js
111
+ // playwright/lib/index.js
112
+ // playwright/lib/runner/index.js
113
+ //
114
+ // 1.62 -> 1.63 moved two anchors, both from Playwright's own source rather
115
+ // than the bundler:
116
+ // - `_wrapApiCall` now resolves a private API name against the title first,
117
+ // so the apiZone literal reads the local `apiName` instead of
118
+ // `stackTrace.apiName`.
119
+ // - `TestInfoImpl._addStep` made the stack capture lazy: the
120
+ // `const filteredStack = ...` hoist is gone and
121
+ // `location = location || filteredStack[0]` became
122
+ // `location ??= filteredStackTrace3(captureRawStack())[0]`.
123
+ // Everything else — including the esbuild dedup suffixes `import_fs5`,
124
+ // `toPosixPath2`, `import_path8` and `stringifyStackFrames2` — survived. Those
125
+ // suffixes are build-pipeline artifacts and have moved between minors before,
126
+ // so re-validate every anchor on each upgrade rather than assuming the
127
+ // previous patch applies (playwright_patches.spec.ts does exactly that).
128
+
129
+ // Remove conditions for injecting Playwright scripts
130
+ function alwaysInjectScripts(projectRoot) {
131
+ const file = join(
132
+ projectRoot,
133
+ "node_modules/playwright-core/lib/coreBundle.js"
134
+ );
135
+ if (!doesFileExist(file)) {
136
+ return;
137
+ }
138
+
139
+ const originalContent = 'if (debugMode() === "console")';
140
+
141
+ const newContent = "";
142
+
143
+ replaceContent(file, originalContent, newContent);
144
+ }
145
+
146
+ // Add implementation for generateSelectorAndLocator and inject to Playwright console API
147
+ function addGenerateSelectorAndLocator(projectRoot) {
148
+ const file = join(
149
+ projectRoot,
150
+ "node_modules/playwright-core/lib/coreBundle.js"
151
+ );
152
+ if (!doesFileExist(file)) {
153
+ return;
154
+ }
155
+ const entryPointText1 = "this._generateLocator(element, language),\\n ";
156
+ const appendText1 =
157
+ "generateSelectorAndLocator: (element, language) => this._generateSelectorAndLocator(element, language),\\n asLocator,\\n ";
158
+ amend(file, entryPointText1, appendText1);
159
+
160
+ const entryPointText2 = `return asLocator(language || "javascript", selector);\\n }\\n `;
161
+ const appendText2 =
162
+ '_generateSelectorAndLocator(element, language) {\\n if (!(element instanceof Element))\\n throw new Error(`Usage: playwright.locator(element).`);\\n const selector = this._injectedScript.generateSelectorSimple(element);\\n return {selector, locator: asLocator(language || \\"javascript\\", selector)};\\n }\\n ';
163
+ amend(file, entryPointText2, appendText2);
164
+ }
165
+
166
+ // -------- [Runtime modifications] -------- //
167
+
168
+ function expect(projectRoot) {
169
+ const file = join(
170
+ projectRoot,
171
+ "node_modules/playwright/lib/matchers/expect.js"
172
+ );
173
+ if (!doesFileExist(file)) {
174
+ return;
175
+ }
176
+ let originalContent, newContent;
177
+
178
+ // originalContent = `return (...args) => {
179
+ // const testInfo = (0, _globals.currentTestInfo)();`;
180
+ // newContent = `return (...args) => {
181
+ // let noSoft = false;
182
+ // if (args.find(arg=>arg==='no-soft')){
183
+ // noSoft = true;
184
+ // args.pop();
185
+ // }
186
+ // const testInfo = (0, _globals.currentTestInfo)();`;
187
+ // replaceContent(file, originalContent, newContent);
188
+
189
+ // originalContent = `step.complete({
190
+ // error
191
+ // })`;
192
+ // newContent = `step.complete({
193
+ // error,
194
+ // noSoft
195
+ // })`;
196
+ // replaceContent(file, originalContent, newContent);
197
+
198
+ // originalContent = `if (this._info.isSoft) testInfo._failWithError(error);else throw error;`;
199
+ // newContent = `if (this._info.isSoft && !noSoft) testInfo._failWithError(error);else throw error;`;
200
+ // replaceContent(file, originalContent, newContent);
201
+ }
202
+
203
+ function testInfo(projectRoot) {
204
+ const file = join(
205
+ projectRoot,
206
+ "node_modules/playwright/lib/worker/workerProcessEntry.js"
207
+ );
208
+ if (!doesFileExist(file)) {
209
+ return;
210
+ }
211
+ let originalContent, newContent;
212
+ let entryPointText, appendText;
213
+
214
+ // originalContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());`;
215
+ // newContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)().filter(s=>!s.includes('@checksum-ai/runtime')));`;
216
+ // replaceContent(file, originalContent, newContent);
217
+
218
+ entryPointText = `location ??= filteredStackTrace3(captureRawStack())[0];`;
219
+ appendText = `\nif (this._checksumInternal) {
220
+ location = undefined;
221
+ this._checksumInternal = false;
222
+ }
223
+ if (this._checksumNoLocation){
224
+ location = undefined;
225
+ }`;
226
+ amend(file, entryPointText, appendText);
227
+
228
+ originalContent = `if (childStep.error && childStep.infectParentStepsWithError) {`;
229
+ newContent = `if (childStep.error && childStep.infectParentStepsWithError && !step.preventInfectParentStepsWithError) {`;
230
+ replaceContent(file, originalContent, newContent);
231
+
232
+ originalContent = `_failWithError(root) {`;
233
+ newContent = `addError(error, message) {
234
+ const serialized = testInfoError(error);
235
+ serialized.message = [message, serialized.message].join('\\n\\n');
236
+ serialized.stack = [message, serialized.stack].join('\\n\\n');
237
+ const step = error[stepSymbol];
238
+ if (step && step.boxedStack) serialized.stack = \`\${error.name}: \${error.message}\\n\${stringifyStackFrames2(step.boxedStack).join('\\n')}\`;
239
+ this.errors.push(serialized);
240
+ }
241
+ _failWithError(root) {`;
242
+ replaceContent(file, originalContent, newContent);
243
+ }
244
+
245
+ function testType(projectRoot) {
246
+ const file = join(projectRoot, "node_modules/playwright/lib/common/index.js");
247
+ if (!doesFileExist(file)) {
248
+ return;
249
+ }
250
+
251
+ entryPointText = `return await currentZone().with("stepZone", step).run(async () => {`;
252
+ appendText = `\nif (options.obtainStep){
253
+ options.obtainStep(step);
254
+ }`;
255
+ amend(file, entryPointText, appendText);
256
+ }
257
+
258
+ function indexContent(projectRoot) {
259
+ const file = join(projectRoot, "node_modules/playwright/lib/index.js");
260
+ if (!doesFileExist(file)) {
261
+ return;
262
+ }
263
+ let originalContent, newContent;
264
+ originalContent = `const browser = await playwright[browserName].launch();`;
265
+ newContent = `
266
+ let browser = playwright[browserName];
267
+ try {
268
+ const { playwrightExtra } = workerInfo?.project?.use || {};
269
+ if (playwrightExtra && playwrightExtra?.length) {
270
+ const pw = require("playwright-extra")
271
+ const PupeteerExtraPlugin = require("puppeteer-extra-plugin").PuppeteerExtraPlugin
272
+ const chromium = pw.chromium;
273
+
274
+ playwrightExtra.forEach((plugin, i) => {
275
+ try {
276
+ if(!(plugin instanceof PupeteerExtraPlugin)){
277
+ console.warn(\`Plugin at index \${i} is not an instance of PupeteerExtraPlugin\`);
278
+ }
279
+ chromium.use(plugin);
280
+ } catch (e) {
281
+ console.warn(e);
282
+ }
283
+ });
284
+ browser = chromium;
285
+ }
286
+ } catch (e) {
287
+ console.warn(
288
+ "CHECKSUM: Failed to load Playwright Extra, using Playwright instead.",
289
+ e
290
+ );
291
+ }
292
+ browser = await browser.launch();
293
+ `;
294
+ replaceContent(file, originalContent, newContent);
295
+ }
296
+
297
+ function channelOwner(projectRoot) {
298
+ const file = join(
299
+ projectRoot,
300
+ "node_modules/playwright-core/lib/coreBundle.js"
301
+ );
302
+ if (!doesFileExist(file)) {
303
+ return;
304
+ }
305
+ let originalContent, newContent;
306
+ let entryPointText, appendText;
307
+
308
+ entryPointText = `async _wrapApiCall(func, options) {`;
309
+
310
+ appendText = `\nif (this._checksumInternal){
311
+ options = options || {};
312
+ options.internal = true;
313
+ }`;
314
+ amend(file, entryPointText, appendText);
315
+
316
+ entryPointText = `const apiZone = { title: options?.title, apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };`;
317
+
318
+ appendText = `\nif (!apiZone.internal && this._checksumTitle){
319
+ apiZone.apiName = this._checksumTitle;
320
+ this._checksumTitle = undefined;
321
+ }
322
+ if (!apiZone.apiName){
323
+ options = options || {};
324
+ options.internal = true;
325
+ apiZone.internal = true;
326
+ apiZone.reported = true;
327
+ }
328
+ if (apiZone.apiName && apiZone.apiName.startsWith('proxy')) {
329
+ apiZone.apiName = apiZone.apiName.replace('proxy', 'page');
330
+ }`;
331
+ amend(file, entryPointText, appendText);
332
+ }
333
+
334
+ function stackTrace(projectRoot) {
335
+ const file = join(
336
+ projectRoot,
337
+ "node_modules/playwright-core/lib/coreBundle.js"
338
+ );
339
+ if (!doesFileExist(file)) {
340
+ return;
341
+ }
342
+
343
+ let originalContent, newContent;
344
+
345
+ // Create a regex for getting a file for each line in the stacktrace that overrides the original regex
346
+ // This regex is only used for getting files, the original regex is sitll used for the rest of the content
347
+ originalContent = `let file = match[7];`;
348
+ const fileRe = /(\/.*?\.[a-zA-Z0-9]+)(?=:\d+:\d+)/;
349
+ newContent = `
350
+ const fileRe = new RegExp(${JSON.stringify(fileRe.source)}, "${
351
+ fileRe.flags
352
+ }");
353
+ const m = fileRe.exec(match[0] ?? "");
354
+ let file = m ? m[1] : undefined;
355
+ `;
356
+ replaceContent(file, originalContent, newContent);
357
+
358
+ // Filter out checksum-ai/runtime from stack traces
359
+ originalContent = `return stack.split("\\n");`;
360
+ newContent = `return stack.split("\\n").filter(s=>!s.includes('@checksum-ai/runtime'));`;
361
+ replaceContent(file, originalContent, newContent);
362
+ }
363
+
364
+ function reportTraceFile(projectRoot) {
365
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
366
+ if (!doesFileExist(file)) {
367
+ return;
368
+ }
369
+
370
+ let originalContent, newContent;
371
+
372
+ originalContent = `const buffer = import_fs5.default.readFileSync(a.path);`;
373
+ newContent = `let buffer = import_fs5.default.readFileSync(a.path);
374
+ if (a.name === "trace") {
375
+ let retries = 2;
376
+ while (!buffer.slice(0,100).toString().startsWith("checksum-playwright-trace") && retries > 0) {
377
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
378
+ buffer = import_fs5.default.readFileSync(a.path)
379
+ retries--;
380
+ }
381
+ }`;
382
+ replaceContent(file, originalContent, newContent);
383
+ }
384
+
385
+ function htmlReporter(projectRoot) {
386
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
387
+ if (!doesFileExist(file)) {
388
+ return;
389
+ }
390
+
391
+ let originalContent, newContent;
392
+
393
+ // Filter out runtime files from snippet generation
394
+ originalContent = `function createSnippets(stepsInFile) {
395
+ for (const file of stepsInFile.keys()) {
396
+ let source;
397
+ try {
398
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
399
+ } catch (e) {
400
+ continue;
401
+ }`;
402
+ newContent = `function createSnippets(stepsInFile) {
403
+ for (const file of stepsInFile.keys()) {
404
+ // Skip runtime files to reduce report size
405
+ if (file.includes('@checksum-ai/runtime') || file.includes('node_modules')) {
406
+ continue;
407
+ }
408
+ let source;
409
+ try {
410
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
411
+ } catch (e) {
412
+ continue;
413
+ }`;
414
+ replaceContent(file, originalContent, newContent);
415
+
416
+ // Also filter from error codeframe generation
417
+ originalContent = `function createErrorCodeframe(message, location) {
418
+ let source;
419
+ try {
420
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
421
+ } catch (e) {
422
+ return;
423
+ }`;
424
+ newContent = `function createErrorCodeframe(message, location) {
425
+ // Skip runtime files to reduce report size
426
+ if (location.file && (location.file.includes('@checksum-ai/runtime') || location.file.includes('node_modules'))) {
427
+ return;
428
+ }
429
+ let source;
430
+ try {
431
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
432
+ } catch (e) {
433
+ return;
434
+ }`;
435
+ replaceContent(file, originalContent, newContent);
436
+
437
+ // Filter out steps with locations in runtime/node_modules files completely
438
+ originalContent = `_createTestStep(dedupedStep, result) {
439
+ const { step, duration, count } = dedupedStep;
440
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
441
+ let title = step.title;
442
+ if (skipped)
443
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
444
+ const testStep = {
445
+ title,
446
+ subtitle: step.subtitle,
447
+ startTime: step.startTime.toISOString(),
448
+ duration,
449
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)),
450
+ attachments: step.attachments.map((s) => {
451
+ const index = result.attachments.indexOf(s);
452
+ if (index === -1)
453
+ throw new Error("Unexpected, attachment not found");
454
+ return index;
455
+ }),
456
+ location: this._relativeLocation(step.location),
457
+ error: step.error?.message,
458
+ count,
459
+ skipped: !!skipped
460
+ };
461
+ if (step.location)
462
+ this._stepsInFile.set(step.location.file, testStep);
463
+ return testStep;
464
+ }`;
465
+ newContent = `_createTestStep(dedupedStep, result) {
466
+ const { step, duration, count } = dedupedStep;
467
+ // Skip "Evaluate" steps with locations in runtime/node_modules files
468
+ if (step.location && step.title === "Evaluate" && (step.location.file.includes('@checksum-ai/runtime') || step.location.file.includes('node_modules'))) {
469
+ // Return null to indicate this step should be filtered out
470
+ return null;
471
+ }
472
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
473
+ let title = step.title;
474
+ if (skipped)
475
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
476
+ const testStep = {
477
+ title,
478
+ subtitle: step.subtitle,
479
+ startTime: step.startTime.toISOString(),
480
+ duration,
481
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),
482
+ attachments: step.attachments.map((s) => {
483
+ const index = result.attachments.indexOf(s);
484
+ if (index === -1)
485
+ throw new Error("Unexpected, attachment not found");
486
+ return index;
487
+ }),
488
+ location: this._relativeLocation(step.location),
489
+ error: step.error?.message,
490
+ count,
491
+ skipped: !!skipped
492
+ };
493
+ if (step.location)
494
+ this._stepsInFile.set(step.location.file, testStep);
495
+ return testStep;
496
+ }`;
497
+ replaceContent(file, originalContent, newContent);
498
+
499
+ // Also filter steps when creating test results
500
+ originalContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)),`;
501
+ newContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),`;
502
+ replaceContent(file, originalContent, newContent);
503
+
504
+ // Normalize locations for steps from runtime/index.js - hide location if it's index.js from runtime
505
+ originalContent = `_relativeLocation(location) {
506
+ if (!location)
507
+ return void 0;
508
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
509
+ return {
510
+ file,
511
+ line: location.line,
512
+ column: location.column
513
+ };
514
+ }`;
515
+ newContent = `_relativeLocation(location) {
516
+ if (!location)
517
+ return void 0;
518
+ // Hide location for steps from runtime/index.js to reduce clutter
519
+ if (location.file && location.file.includes('@checksum-ai/runtime') && location.file.endsWith('index.js')) {
520
+ return void 0;
521
+ }
522
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
523
+ return {
524
+ file,
525
+ line: location.line,
526
+ column: location.column
527
+ };
528
+ }`;
529
+ replaceContent(file, originalContent, newContent);
530
+ }
531
+
532
+ // -------- [Shutdown safety] -------- //
533
+
534
+ // Playwright leaves two teardown fixtures at `timeout: 0`, and TimeoutManager
535
+ // reads that as `deadline = kMaxDeadline` — unbounded, not "use the default".
536
+ // Every other phase is capped at the project timeout, so these are the only two
537
+ // ways a worker can stop reporting for good: an unreaped browser leaves
538
+ // `browser.close()` awaiting a `close` event that never arrives, which is how
539
+ // run cc44a300 went silent for 10h42m until the k8s job deadline killed the pod.
540
+ // Playwright's own 5-minute worker watchdog cannot catch it — heartbeats run on
541
+ // an interval independent of any pending await, so a wedged worker keeps
542
+ // heartbeating and the deadline slides forever.
543
+ //
544
+ // `> 0 ? : ` rather than `||`: a zero, negative or unparseable override must
545
+ // fall back to the default, never restore the unbounded wait.
546
+ function boundUnboundedFixtures(projectRoot) {
547
+ const file = join(projectRoot, "node_modules/playwright/lib/index.js");
548
+ if (!doesFileExist(file)) {
549
+ return;
550
+ }
551
+
552
+ let originalContent, newContent;
553
+
554
+ // ArtifactsRecorder.didFinishTest(): screenshot, trace stop, aria snapshot.
555
+ originalContent = `}, { auto: "all-hooks-included", title: "trace recording", box: true, timeout: 0 }],`;
556
+ newContent = `}, { auto: "all-hooks-included", title: "trace recording", box: true, timeout: Number(process.env["CHECKSUM_PW_ARTIFACTS_TIMEOUT_MS"]) > 0 ? Number(process.env["CHECKSUM_PW_ARTIFACTS_TIMEOUT_MS"]) : 120000 }],`;
557
+ replaceContent(file, originalContent, newContent);
558
+
559
+ // The `browser` fixture: launch on setup, `browser.close()` on teardown.
560
+ originalContent = `}, { scope: "worker", timeout: 0 }],`;
561
+ newContent = `}, { scope: "worker", timeout: Number(process.env["CHECKSUM_PW_BROWSER_FIXTURE_TIMEOUT_MS"]) > 0 ? Number(process.env["CHECKSUM_PW_BROWSER_FIXTURE_TIMEOUT_MS"]) : 180000 }],`;
562
+ replaceContent(file, originalContent, newContent);
563
+ }
564
+
565
+ // -------- [Run] -------- //
566
+
567
+ const isRuntime = true || process.env.RUNTIME === "true";
568
+
569
+ function run(projectPath) {
570
+ try {
571
+ if (fs.existsSync(projectPath)) {
572
+ alwaysInjectScripts(projectPath);
573
+ addGenerateSelectorAndLocator(projectPath);
574
+ if (isRuntime) {
575
+ expect(projectPath);
576
+ testInfo(projectPath);
577
+ testType(projectPath);
578
+ channelOwner(projectPath);
579
+ stackTrace(projectPath);
580
+ indexContent(projectPath);
581
+ // Skipped in merge mode (CHECKSUM_SKIP_TRACE_PATCH): this patch makes
582
+ // the HTML reporter sleep ~10s per `trace` attachment waiting for a
583
+ // `checksum-playwright-trace` file that only the live run produces; in
584
+ // the merge path (real Playwright traces) it is pure dead time.
585
+ if (process.env.CHECKSUM_SKIP_TRACE_PATCH !== "true") {
586
+ reportTraceFile(projectPath);
587
+ }
588
+ htmlReporter(projectPath);
589
+ boundUnboundedFixtures(projectPath);
590
+ }
591
+ } else {
592
+ console.warn("Project path not found", projectPath);
593
+ }
594
+ } catch (e) {
595
+ // ignore
596
+ console.error(e);
597
+ }
598
+ }
599
+
600
+ module.exports = run;