@checksum-ai/runtime 4.16.2 → 4.16.3

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,562 @@
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.60 -> 1.62 moved three anchors:
115
+ // - `_wrapApiCall(func, options2)` -> `_wrapApiCall(func, options)`; the
116
+ // esbuild dedup suffix on `options` disappeared, so every `options2`
117
+ // reference in the channelOwner patch reverts to `options`.
118
+ // - `_failWithError(error)` -> `_failWithError(root)`; the method now walks
119
+ // an error tree via a local `visit()`.
120
+ // - Inside workerProcessEntry the helpers are no longer namespaced imports:
121
+ // `(0, import_util2.testInfoError)` -> `testInfoError` and
122
+ // `(0, import_utils.stringifyStackFrames)` -> `stringifyStackFrames2`.
123
+ // `import_fs5`, `toPosixPath2` and `import_path8` in runner/index.js kept
124
+ // their 1.60 suffixes — but these are build-pipeline artifacts, so re-validate
125
+ // every anchor on each upgrade rather than assuming the previous patch applies.
126
+
127
+ // Remove conditions for injecting Playwright scripts
128
+ function alwaysInjectScripts(projectRoot) {
129
+ const file = join(
130
+ projectRoot,
131
+ "node_modules/playwright-core/lib/coreBundle.js"
132
+ );
133
+ if (!doesFileExist(file)) {
134
+ return;
135
+ }
136
+
137
+ const originalContent = 'if (debugMode() === "console")';
138
+
139
+ const newContent = "";
140
+
141
+ replaceContent(file, originalContent, newContent);
142
+ }
143
+
144
+ // Add implementation for generateSelectorAndLocator and inject to Playwright console API
145
+ function addGenerateSelectorAndLocator(projectRoot) {
146
+ const file = join(
147
+ projectRoot,
148
+ "node_modules/playwright-core/lib/coreBundle.js"
149
+ );
150
+ if (!doesFileExist(file)) {
151
+ return;
152
+ }
153
+ const entryPointText1 = "this._generateLocator(element, language),\\n ";
154
+ const appendText1 =
155
+ "generateSelectorAndLocator: (element, language) => this._generateSelectorAndLocator(element, language),\\n asLocator,\\n ";
156
+ amend(file, entryPointText1, appendText1);
157
+
158
+ const entryPointText2 = `return asLocator(language || "javascript", selector);\\n }\\n `;
159
+ const appendText2 =
160
+ '_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 ';
161
+ amend(file, entryPointText2, appendText2);
162
+ }
163
+
164
+ // -------- [Runtime modifications] -------- //
165
+
166
+ function expect(projectRoot) {
167
+ const file = join(
168
+ projectRoot,
169
+ "node_modules/playwright/lib/matchers/expect.js"
170
+ );
171
+ if (!doesFileExist(file)) {
172
+ return;
173
+ }
174
+ let originalContent, newContent;
175
+
176
+ // originalContent = `return (...args) => {
177
+ // const testInfo = (0, _globals.currentTestInfo)();`;
178
+ // newContent = `return (...args) => {
179
+ // let noSoft = false;
180
+ // if (args.find(arg=>arg==='no-soft')){
181
+ // noSoft = true;
182
+ // args.pop();
183
+ // }
184
+ // const testInfo = (0, _globals.currentTestInfo)();`;
185
+ // replaceContent(file, originalContent, newContent);
186
+
187
+ // originalContent = `step.complete({
188
+ // error
189
+ // })`;
190
+ // newContent = `step.complete({
191
+ // error,
192
+ // noSoft
193
+ // })`;
194
+ // replaceContent(file, originalContent, newContent);
195
+
196
+ // originalContent = `if (this._info.isSoft) testInfo._failWithError(error);else throw error;`;
197
+ // newContent = `if (this._info.isSoft && !noSoft) testInfo._failWithError(error);else throw error;`;
198
+ // replaceContent(file, originalContent, newContent);
199
+ }
200
+
201
+ function testInfo(projectRoot) {
202
+ const file = join(
203
+ projectRoot,
204
+ "node_modules/playwright/lib/worker/workerProcessEntry.js"
205
+ );
206
+ if (!doesFileExist(file)) {
207
+ return;
208
+ }
209
+ let originalContent, newContent;
210
+ let entryPointText, appendText;
211
+
212
+ // originalContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());`;
213
+ // newContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)().filter(s=>!s.includes('@checksum-ai/runtime')));`;
214
+ // replaceContent(file, originalContent, newContent);
215
+
216
+ entryPointText = `location = location || filteredStack[0];`;
217
+ appendText = `\nif (this._checksumInternal) {
218
+ location = undefined;
219
+ this._checksumInternal = false;
220
+ }
221
+ if (this._checksumNoLocation){
222
+ location = undefined;
223
+ }`;
224
+ amend(file, entryPointText, appendText);
225
+
226
+ originalContent = `if (childStep.error && childStep.infectParentStepsWithError) {`;
227
+ newContent = `if (childStep.error && childStep.infectParentStepsWithError && !step.preventInfectParentStepsWithError) {`;
228
+ replaceContent(file, originalContent, newContent);
229
+
230
+ originalContent = `_failWithError(root) {`;
231
+ newContent = `addError(error, message) {
232
+ const serialized = testInfoError(error);
233
+ serialized.message = [message, serialized.message].join('\\n\\n');
234
+ serialized.stack = [message, serialized.stack].join('\\n\\n');
235
+ const step = error[stepSymbol];
236
+ if (step && step.boxedStack) serialized.stack = \`\${error.name}: \${error.message}\\n\${stringifyStackFrames2(step.boxedStack).join('\\n')}\`;
237
+ this.errors.push(serialized);
238
+ }
239
+ _failWithError(root) {`;
240
+ replaceContent(file, originalContent, newContent);
241
+ }
242
+
243
+ function testType(projectRoot) {
244
+ const file = join(projectRoot, "node_modules/playwright/lib/common/index.js");
245
+ if (!doesFileExist(file)) {
246
+ return;
247
+ }
248
+
249
+ entryPointText = `return await currentZone().with("stepZone", step).run(async () => {`;
250
+ appendText = `\nif (options.obtainStep){
251
+ options.obtainStep(step);
252
+ }`;
253
+ amend(file, entryPointText, appendText);
254
+ }
255
+
256
+ function indexContent(projectRoot) {
257
+ const file = join(projectRoot, "node_modules/playwright/lib/index.js");
258
+ if (!doesFileExist(file)) {
259
+ return;
260
+ }
261
+ let originalContent, newContent;
262
+ originalContent = `const browser = await playwright[browserName].launch();`;
263
+ newContent = `
264
+ let browser = playwright[browserName];
265
+ try {
266
+ const { playwrightExtra } = workerInfo?.project?.use || {};
267
+ if (playwrightExtra && playwrightExtra?.length) {
268
+ const pw = require("playwright-extra")
269
+ const PupeteerExtraPlugin = require("puppeteer-extra-plugin").PuppeteerExtraPlugin
270
+ const chromium = pw.chromium;
271
+
272
+ playwrightExtra.forEach((plugin, i) => {
273
+ try {
274
+ if(!(plugin instanceof PupeteerExtraPlugin)){
275
+ console.warn(\`Plugin at index \${i} is not an instance of PupeteerExtraPlugin\`);
276
+ }
277
+ chromium.use(plugin);
278
+ } catch (e) {
279
+ console.warn(e);
280
+ }
281
+ });
282
+ browser = chromium;
283
+ }
284
+ } catch (e) {
285
+ console.warn(
286
+ "CHECKSUM: Failed to load Playwright Extra, using Playwright instead.",
287
+ e
288
+ );
289
+ }
290
+ browser = await browser.launch();
291
+ `;
292
+ replaceContent(file, originalContent, newContent);
293
+ }
294
+
295
+ function channelOwner(projectRoot) {
296
+ const file = join(
297
+ projectRoot,
298
+ "node_modules/playwright-core/lib/coreBundle.js"
299
+ );
300
+ if (!doesFileExist(file)) {
301
+ return;
302
+ }
303
+ let originalContent, newContent;
304
+ let entryPointText, appendText;
305
+
306
+ entryPointText = `async _wrapApiCall(func, options) {`;
307
+
308
+ appendText = `\nif (this._checksumInternal){
309
+ options = options || {};
310
+ options.internal = true;
311
+ }`;
312
+ amend(file, entryPointText, appendText);
313
+
314
+ entryPointText = `const apiZone = { title: options?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };`;
315
+
316
+ appendText = `\nif (!apiZone.internal && this._checksumTitle){
317
+ apiZone.apiName = this._checksumTitle;
318
+ this._checksumTitle = undefined;
319
+ }
320
+ if (!apiZone.apiName){
321
+ options = options || {};
322
+ options.internal = true;
323
+ apiZone.internal = true;
324
+ apiZone.reported = true;
325
+ }
326
+ if (apiZone.apiName && apiZone.apiName.startsWith('proxy')) {
327
+ apiZone.apiName = apiZone.apiName.replace('proxy', 'page');
328
+ }`;
329
+ amend(file, entryPointText, appendText);
330
+ }
331
+
332
+ function stackTrace(projectRoot) {
333
+ const file = join(
334
+ projectRoot,
335
+ "node_modules/playwright-core/lib/coreBundle.js"
336
+ );
337
+ if (!doesFileExist(file)) {
338
+ return;
339
+ }
340
+
341
+ let originalContent, newContent;
342
+
343
+ // Create a regex for getting a file for each line in the stacktrace that overrides the original regex
344
+ // This regex is only used for getting files, the original regex is sitll used for the rest of the content
345
+ originalContent = `let file = match[7];`;
346
+ const fileRe = /(\/.*?\.[a-zA-Z0-9]+)(?=:\d+:\d+)/;
347
+ newContent = `
348
+ const fileRe = new RegExp(${JSON.stringify(fileRe.source)}, "${
349
+ fileRe.flags
350
+ }");
351
+ const m = fileRe.exec(match[0] ?? "");
352
+ let file = m ? m[1] : undefined;
353
+ `;
354
+ replaceContent(file, originalContent, newContent);
355
+
356
+ // Filter out checksum-ai/runtime from stack traces
357
+ originalContent = `return stack.split("\\n");`;
358
+ newContent = `return stack.split("\\n").filter(s=>!s.includes('@checksum-ai/runtime'));`;
359
+ replaceContent(file, originalContent, newContent);
360
+ }
361
+
362
+ function reportTraceFile(projectRoot) {
363
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
364
+ if (!doesFileExist(file)) {
365
+ return;
366
+ }
367
+
368
+ let originalContent, newContent;
369
+
370
+ originalContent = `const buffer = import_fs5.default.readFileSync(a.path);`;
371
+ newContent = `let buffer = import_fs5.default.readFileSync(a.path);
372
+ if (a.name === "trace") {
373
+ let retries = 2;
374
+ while (!buffer.slice(0,100).toString().startsWith("checksum-playwright-trace") && retries > 0) {
375
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
376
+ buffer = import_fs5.default.readFileSync(a.path)
377
+ retries--;
378
+ }
379
+ }`;
380
+ replaceContent(file, originalContent, newContent);
381
+ }
382
+
383
+ function htmlReporter(projectRoot) {
384
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
385
+ if (!doesFileExist(file)) {
386
+ return;
387
+ }
388
+
389
+ let originalContent, newContent;
390
+
391
+ // Filter out runtime files from snippet generation
392
+ originalContent = `function createSnippets(stepsInFile) {
393
+ for (const file of stepsInFile.keys()) {
394
+ let source;
395
+ try {
396
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
397
+ } catch (e) {
398
+ continue;
399
+ }`;
400
+ newContent = `function createSnippets(stepsInFile) {
401
+ for (const file of stepsInFile.keys()) {
402
+ // Skip runtime files to reduce report size
403
+ if (file.includes('@checksum-ai/runtime') || file.includes('node_modules')) {
404
+ continue;
405
+ }
406
+ let source;
407
+ try {
408
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
409
+ } catch (e) {
410
+ continue;
411
+ }`;
412
+ replaceContent(file, originalContent, newContent);
413
+
414
+ // Also filter from error codeframe generation
415
+ originalContent = `function createErrorCodeframe(message, location) {
416
+ let source;
417
+ try {
418
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
419
+ } catch (e) {
420
+ return;
421
+ }`;
422
+ newContent = `function createErrorCodeframe(message, location) {
423
+ // Skip runtime files to reduce report size
424
+ if (location.file && (location.file.includes('@checksum-ai/runtime') || location.file.includes('node_modules'))) {
425
+ return;
426
+ }
427
+ let source;
428
+ try {
429
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
430
+ } catch (e) {
431
+ return;
432
+ }`;
433
+ replaceContent(file, originalContent, newContent);
434
+
435
+ // Filter out steps with locations in runtime/node_modules files completely
436
+ originalContent = `_createTestStep(dedupedStep, result) {
437
+ const { step, duration, count } = dedupedStep;
438
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
439
+ let title = step.title;
440
+ if (skipped)
441
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
442
+ const testStep = {
443
+ title,
444
+ startTime: step.startTime.toISOString(),
445
+ duration,
446
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)),
447
+ attachments: step.attachments.map((s) => {
448
+ const index = result.attachments.indexOf(s);
449
+ if (index === -1)
450
+ throw new Error("Unexpected, attachment not found");
451
+ return index;
452
+ }),
453
+ location: this._relativeLocation(step.location),
454
+ error: step.error?.message,
455
+ count,
456
+ skipped: !!skipped
457
+ };
458
+ if (step.location)
459
+ this._stepsInFile.set(step.location.file, testStep);
460
+ return testStep;
461
+ }`;
462
+ newContent = `_createTestStep(dedupedStep, result) {
463
+ const { step, duration, count } = dedupedStep;
464
+ // Skip "Evaluate" steps with locations in runtime/node_modules files
465
+ if (step.location && step.title === "Evaluate" && (step.location.file.includes('@checksum-ai/runtime') || step.location.file.includes('node_modules'))) {
466
+ // Return null to indicate this step should be filtered out
467
+ return null;
468
+ }
469
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
470
+ let title = step.title;
471
+ if (skipped)
472
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
473
+ const testStep = {
474
+ title,
475
+ startTime: step.startTime.toISOString(),
476
+ duration,
477
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),
478
+ attachments: step.attachments.map((s) => {
479
+ const index = result.attachments.indexOf(s);
480
+ if (index === -1)
481
+ throw new Error("Unexpected, attachment not found");
482
+ return index;
483
+ }),
484
+ location: this._relativeLocation(step.location),
485
+ error: step.error?.message,
486
+ count,
487
+ skipped: !!skipped
488
+ };
489
+ if (step.location)
490
+ this._stepsInFile.set(step.location.file, testStep);
491
+ return testStep;
492
+ }`;
493
+ replaceContent(file, originalContent, newContent);
494
+
495
+ // Also filter steps when creating test results
496
+ originalContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)),`;
497
+ newContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),`;
498
+ replaceContent(file, originalContent, newContent);
499
+
500
+ // Normalize locations for steps from runtime/index.js - hide location if it's index.js from runtime
501
+ originalContent = `_relativeLocation(location) {
502
+ if (!location)
503
+ return void 0;
504
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
505
+ return {
506
+ file,
507
+ line: location.line,
508
+ column: location.column
509
+ };
510
+ }`;
511
+ newContent = `_relativeLocation(location) {
512
+ if (!location)
513
+ return void 0;
514
+ // Hide location for steps from runtime/index.js to reduce clutter
515
+ if (location.file && location.file.includes('@checksum-ai/runtime') && location.file.endsWith('index.js')) {
516
+ return void 0;
517
+ }
518
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
519
+ return {
520
+ file,
521
+ line: location.line,
522
+ column: location.column
523
+ };
524
+ }`;
525
+ replaceContent(file, originalContent, newContent);
526
+ }
527
+
528
+ // -------- [Run] -------- //
529
+
530
+ const isRuntime = true || process.env.RUNTIME === "true";
531
+
532
+ function run(projectPath) {
533
+ try {
534
+ if (fs.existsSync(projectPath)) {
535
+ alwaysInjectScripts(projectPath);
536
+ addGenerateSelectorAndLocator(projectPath);
537
+ if (isRuntime) {
538
+ expect(projectPath);
539
+ testInfo(projectPath);
540
+ testType(projectPath);
541
+ channelOwner(projectPath);
542
+ stackTrace(projectPath);
543
+ indexContent(projectPath);
544
+ // Skipped in merge mode (CHECKSUM_SKIP_TRACE_PATCH): this patch makes
545
+ // the HTML reporter sleep ~10s per `trace` attachment waiting for a
546
+ // `checksum-playwright-trace` file that only the live run produces; in
547
+ // the merge path (real Playwright traces) it is pure dead time.
548
+ if (process.env.CHECKSUM_SKIP_TRACE_PATCH !== "true") {
549
+ reportTraceFile(projectPath);
550
+ }
551
+ htmlReporter(projectPath);
552
+ }
553
+ } else {
554
+ console.warn("Project path not found", projectPath);
555
+ }
556
+ } catch (e) {
557
+ // ignore
558
+ console.error(e);
559
+ }
560
+ }
561
+
562
+ module.exports = run;