@checksum-ai/runtime 3.0.3 → 4.0.0-alpha

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/.env CHANGED
@@ -1 +1 @@
1
- CHECKSUM_RUNTIME_BUILD_TIME=2026-05-07T08:40:25.654Z
1
+ CHECKSUM_RUNTIME_BUILD_TIME=2026-05-12T11:12:18.644Z
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@checksum-ai/runtime",
3
- "version": "3.0.3",
3
+ "version": "4.0.0-alpha",
4
4
  "description": "Checksum.ai test runtime",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
- "@playwright/test": "1.59.0",
7
+ "@playwright/test": "1.60.0",
8
8
  "dotenv": "^16.4.5",
9
9
  "jsdom": "^25.0.1",
10
10
  "playwright-extra": "^4.3.6",
@@ -0,0 +1,506 @@
1
+ const fs = require("fs");
2
+ const { join } = require("path");
3
+
4
+ // Args
5
+ const on = process.argv[2] !== "off";
6
+
7
+ // -------- [Modifiers] -------- //
8
+
9
+ // Amends the file with the given entry point text and append text
10
+ // When "on" is true, the append text is added to the entry point,
11
+ // otherwise the append text is completely removed from the file
12
+ function amend(filePath, entryPointText, appendText) {
13
+ const data = fs.readFileSync(filePath, "utf8");
14
+ if (!data.includes(entryPointText)) {
15
+ throw new Error("Entry point not found!", entryPointText);
16
+ }
17
+ // Ignore if the append text is already present
18
+ if (on && data.includes(appendText)) {
19
+ return;
20
+ }
21
+ // Add or clear according to on state
22
+ const result = on
23
+ ? data.replace(entryPointText, entryPointText + appendText)
24
+ : data.replace(appendText, "");
25
+
26
+ // Write
27
+ fs.writeFileSync(filePath, result, "utf8");
28
+ }
29
+
30
+ // Replaces content.
31
+ // When "on" is true, the new content is written to the file replacing the original content,
32
+ // otherwise the original content is restored.
33
+ function replaceContent(filePath, originalContent, newContent) {
34
+ // Read the file content
35
+ const fileContent = fs.readFileSync(filePath, "utf8");
36
+
37
+ // add a marker for newContent that can be later recognized for "off" state
38
+ newContent = `/* checksumai */ ${newContent}`;
39
+
40
+ if (on && fileContent.includes(newContent)) {
41
+ return;
42
+ }
43
+
44
+ // Join the lines back into a single string
45
+ const updatedContent = on
46
+ ? fileContent.replace(originalContent, newContent)
47
+ : fileContent.replace(newContent, originalContent);
48
+
49
+ // Write the modified content back to the file
50
+ fs.writeFileSync(filePath, updatedContent, "utf8");
51
+ }
52
+
53
+ function doesFileExist(filePath) {
54
+ if (!fs.existsSync(filePath)) {
55
+ console.warn("File not found", filePath);
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ // -------- [Modifications] -------- //
62
+
63
+ // Playwright 1.60 bundled the entire lib/ surface into a small number of
64
+ // esbuild outputs. The compiled file targets used by previous patch versions
65
+ // no longer exist:
66
+ // playwright-core/lib/server/browserContext.js -> coreBundle.js
67
+ // playwright-core/lib/generated/injectedScriptSource.js -> coreBundle.js
68
+ // playwright-core/lib/client/channelOwner.js -> coreBundle.js
69
+ // playwright-core/lib/utils/isomorphic/stackTrace.js -> coreBundle.js
70
+ // playwright/lib/worker/testInfo.js -> worker/workerProcessEntry.js
71
+ // playwright/lib/common/testType.js -> common/index.js
72
+ // playwright/lib/reporters/html.js -> runner/index.js
73
+ // Bundling also dropped the `(0, import_X.fn)(...)` IIFE wrapping older
74
+ // patches anchored against, and esbuild renamed identifiers to dedupe across
75
+ // inlined files (`options` -> `options2`, `import_fs` -> `import_fs5`, etc.).
76
+
77
+ // Remove conditions for injecting Playwright scripts
78
+ function alwaysInjectScripts(projectRoot) {
79
+ const file = join(
80
+ projectRoot,
81
+ "node_modules/playwright-core/lib/coreBundle.js"
82
+ );
83
+ if (!doesFileExist(file)) {
84
+ return;
85
+ }
86
+
87
+ const originalContent = 'if (debugMode() === "console")';
88
+
89
+ const newContent = "";
90
+
91
+ replaceContent(file, originalContent, newContent);
92
+ }
93
+
94
+ // Add implementation for generateSelectorAndLocator and inject to Playwright console API
95
+ function addGenerateSelectorAndLocator(projectRoot) {
96
+ const file = join(
97
+ projectRoot,
98
+ "node_modules/playwright-core/lib/coreBundle.js"
99
+ );
100
+ if (!doesFileExist(file)) {
101
+ return;
102
+ }
103
+ const entryPointText1 = "this._generateLocator(element, language),\\n ";
104
+ const appendText1 =
105
+ "generateSelectorAndLocator: (element, language) => this._generateSelectorAndLocator(element, language),\\n asLocator,\\n ";
106
+ amend(file, entryPointText1, appendText1);
107
+
108
+ const entryPointText2 = `return asLocator(language || "javascript", selector);\\n }\\n `;
109
+ const appendText2 =
110
+ '_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 ';
111
+ amend(file, entryPointText2, appendText2);
112
+ }
113
+
114
+ // -------- [Runtime modifications] -------- //
115
+
116
+ function expect(projectRoot) {
117
+ const file = join(
118
+ projectRoot,
119
+ "node_modules/playwright/lib/matchers/expect.js"
120
+ );
121
+ if (!doesFileExist(file)) {
122
+ return;
123
+ }
124
+ let originalContent, newContent;
125
+
126
+ // originalContent = `return (...args) => {
127
+ // const testInfo = (0, _globals.currentTestInfo)();`;
128
+ // newContent = `return (...args) => {
129
+ // let noSoft = false;
130
+ // if (args.find(arg=>arg==='no-soft')){
131
+ // noSoft = true;
132
+ // args.pop();
133
+ // }
134
+ // const testInfo = (0, _globals.currentTestInfo)();`;
135
+ // replaceContent(file, originalContent, newContent);
136
+
137
+ // originalContent = `step.complete({
138
+ // error
139
+ // })`;
140
+ // newContent = `step.complete({
141
+ // error,
142
+ // noSoft
143
+ // })`;
144
+ // replaceContent(file, originalContent, newContent);
145
+
146
+ // originalContent = `if (this._info.isSoft) testInfo._failWithError(error);else throw error;`;
147
+ // newContent = `if (this._info.isSoft && !noSoft) testInfo._failWithError(error);else throw error;`;
148
+ // replaceContent(file, originalContent, newContent);
149
+ }
150
+
151
+ function testInfo(projectRoot) {
152
+ const file = join(
153
+ projectRoot,
154
+ "node_modules/playwright/lib/worker/workerProcessEntry.js"
155
+ );
156
+ if (!doesFileExist(file)) {
157
+ return;
158
+ }
159
+ let originalContent, newContent;
160
+ let entryPointText, appendText;
161
+
162
+ // originalContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)());`;
163
+ // newContent = `const filteredStack = (0, _util.filteredStackTrace)((0, _utils.captureRawStack)().filter(s=>!s.includes('@checksum-ai/runtime')));`;
164
+ // replaceContent(file, originalContent, newContent);
165
+
166
+ entryPointText = `location = location || filteredStack[0];`;
167
+ appendText = `\nif (this._checksumInternal) {
168
+ location = undefined;
169
+ this._checksumInternal = false;
170
+ }
171
+ if (this._checksumNoLocation){
172
+ location = undefined;
173
+ }`;
174
+ amend(file, entryPointText, appendText);
175
+
176
+ originalContent = `if (childStep.error && childStep.infectParentStepsWithError) {`;
177
+ newContent = `if (childStep.error && childStep.infectParentStepsWithError && !step.preventInfectParentStepsWithError) {`;
178
+ replaceContent(file, originalContent, newContent);
179
+
180
+ originalContent = `_failWithError(error) {`;
181
+ newContent = `addError(error, message) {
182
+ const serialized = (0, import_util2.testInfoError)(error);
183
+ serialized.message = [message, serialized.message].join('\\n\\n');
184
+ serialized.stack = [message, serialized.stack].join('\\n\\n');
185
+ const step = error[stepSymbol];
186
+ if (step && step.boxedStack) serialized.stack = \`\${error.name}: \${error.message}\\n\${(0, import_utils.stringifyStackFrames)(step.boxedStack).join('\\n')}\`;
187
+ this.errors.push(serialized);
188
+ }
189
+ _failWithError(error) {`;
190
+ replaceContent(file, originalContent, newContent);
191
+ }
192
+
193
+ function testType(projectRoot) {
194
+ const file = join(projectRoot, "node_modules/playwright/lib/common/index.js");
195
+ if (!doesFileExist(file)) {
196
+ return;
197
+ }
198
+
199
+ entryPointText = `return await currentZone().with("stepZone", step).run(async () => {`;
200
+ appendText = `\nif (options.obtainStep){
201
+ options.obtainStep(step);
202
+ }`;
203
+ amend(file, entryPointText, appendText);
204
+ }
205
+
206
+ function indexContent(projectRoot) {
207
+ const file = join(projectRoot, "node_modules/playwright/lib/index.js");
208
+ if (!doesFileExist(file)) {
209
+ return;
210
+ }
211
+ let originalContent, newContent;
212
+ originalContent = `const browser = await playwright[browserName].launch();`;
213
+ newContent = `
214
+ let browser = playwright[browserName];
215
+ try {
216
+ const { playwrightExtra } = workerInfo?.project?.use || {};
217
+ if (playwrightExtra && playwrightExtra?.length) {
218
+ const pw = require("playwright-extra")
219
+ const PupeteerExtraPlugin = require("puppeteer-extra-plugin").PuppeteerExtraPlugin
220
+ const chromium = pw.chromium;
221
+
222
+ playwrightExtra.forEach((plugin, i) => {
223
+ try {
224
+ if(!(plugin instanceof PupeteerExtraPlugin)){
225
+ console.warn(\`Plugin at index \${i} is not an instance of PupeteerExtraPlugin\`);
226
+ }
227
+ chromium.use(plugin);
228
+ } catch (e) {
229
+ console.warn(e);
230
+ }
231
+ });
232
+ browser = chromium;
233
+ }
234
+ } catch (e) {
235
+ console.warn(
236
+ "CHECKSUM: Failed to load Playwright Extra, using Playwright instead.",
237
+ e
238
+ );
239
+ }
240
+ browser = await browser.launch();
241
+ `;
242
+ replaceContent(file, originalContent, newContent);
243
+ }
244
+
245
+ function channelOwner(projectRoot) {
246
+ const file = join(
247
+ projectRoot,
248
+ "node_modules/playwright-core/lib/coreBundle.js"
249
+ );
250
+ if (!doesFileExist(file)) {
251
+ return;
252
+ }
253
+ let originalContent, newContent;
254
+ let entryPointText, appendText;
255
+
256
+ entryPointText = `async _wrapApiCall(func, options2) {`;
257
+
258
+ appendText = `\nif (this._checksumInternal){
259
+ options2 = options2 || {};
260
+ options2.internal = true;
261
+ }`;
262
+ amend(file, entryPointText, appendText);
263
+
264
+ entryPointText = `const apiZone = { title: options2?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options2?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };`;
265
+
266
+ appendText = `\nif (!apiZone.internal && this._checksumTitle){
267
+ apiZone.apiName = this._checksumTitle;
268
+ this._checksumTitle = undefined;
269
+ }
270
+ if (!apiZone.apiName){
271
+ options2 = options2 || {};
272
+ options2.internal = true;
273
+ apiZone.internal = true;
274
+ apiZone.reported = true;
275
+ }
276
+ if (apiZone.apiName && apiZone.apiName.startsWith('proxy')) {
277
+ apiZone.apiName = apiZone.apiName.replace('proxy', 'page');
278
+ }`;
279
+ amend(file, entryPointText, appendText);
280
+ }
281
+
282
+ function stackTrace(projectRoot) {
283
+ const file = join(
284
+ projectRoot,
285
+ "node_modules/playwright-core/lib/coreBundle.js"
286
+ );
287
+ if (!doesFileExist(file)) {
288
+ return;
289
+ }
290
+
291
+ let originalContent, newContent;
292
+
293
+ // Create a regex for getting a file for each line in the stacktrace that overrides the original regex
294
+ // This regex is only used for getting files, the original regex is sitll used for the rest of the content
295
+ originalContent = `let file = match[7];`;
296
+ const fileRe = /(\/.*?\.[a-zA-Z0-9]+)(?=:\d+:\d+)/;
297
+ newContent = `
298
+ const fileRe = new RegExp(${JSON.stringify(fileRe.source)}, "${
299
+ fileRe.flags
300
+ }");
301
+ const m = fileRe.exec(match[0] ?? "");
302
+ let file = m ? m[1] : undefined;
303
+ `;
304
+ replaceContent(file, originalContent, newContent);
305
+
306
+ // Filter out checksum-ai/runtime from stack traces
307
+ originalContent = `return stack.split("\\n");`;
308
+ newContent = `return stack.split("\\n").filter(s=>!s.includes('@checksum-ai/runtime'));`;
309
+ replaceContent(file, originalContent, newContent);
310
+ }
311
+
312
+ function reportTraceFile(projectRoot) {
313
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
314
+ if (!doesFileExist(file)) {
315
+ return;
316
+ }
317
+
318
+ let originalContent, newContent;
319
+
320
+ originalContent = `const buffer = import_fs5.default.readFileSync(a.path);`;
321
+ newContent = `let buffer = import_fs5.default.readFileSync(a.path);
322
+ if (a.name === "trace") {
323
+ let retries = 2;
324
+ while (!buffer.slice(0,100).toString().startsWith("checksum-playwright-trace") && retries > 0) {
325
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
326
+ buffer = import_fs5.default.readFileSync(a.path)
327
+ retries--;
328
+ }
329
+ }`;
330
+ replaceContent(file, originalContent, newContent);
331
+ }
332
+
333
+ function htmlReporter(projectRoot) {
334
+ const file = join(projectRoot, "node_modules/playwright/lib/runner/index.js");
335
+ if (!doesFileExist(file)) {
336
+ return;
337
+ }
338
+
339
+ let originalContent, newContent;
340
+
341
+ // Filter out runtime files from snippet generation
342
+ originalContent = `function createSnippets(stepsInFile) {
343
+ for (const file of stepsInFile.keys()) {
344
+ let source;
345
+ try {
346
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
347
+ } catch (e) {
348
+ continue;
349
+ }`;
350
+ newContent = `function createSnippets(stepsInFile) {
351
+ for (const file of stepsInFile.keys()) {
352
+ // Skip runtime files to reduce report size
353
+ if (file.includes('@checksum-ai/runtime') || file.includes('node_modules')) {
354
+ continue;
355
+ }
356
+ let source;
357
+ try {
358
+ source = import_fs5.default.readFileSync(file, "utf-8") + "\\n//";
359
+ } catch (e) {
360
+ continue;
361
+ }`;
362
+ replaceContent(file, originalContent, newContent);
363
+
364
+ // Also filter from error codeframe generation
365
+ originalContent = `function createErrorCodeframe(message, location) {
366
+ let source;
367
+ try {
368
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
369
+ } catch (e) {
370
+ return;
371
+ }`;
372
+ newContent = `function createErrorCodeframe(message, location) {
373
+ // Skip runtime files to reduce report size
374
+ if (location.file && (location.file.includes('@checksum-ai/runtime') || location.file.includes('node_modules'))) {
375
+ return;
376
+ }
377
+ let source;
378
+ try {
379
+ source = import_fs5.default.readFileSync(location.file, "utf-8") + "\\n//";
380
+ } catch (e) {
381
+ return;
382
+ }`;
383
+ replaceContent(file, originalContent, newContent);
384
+
385
+ // Filter out steps with locations in runtime/node_modules files completely
386
+ originalContent = `_createTestStep(dedupedStep, result) {
387
+ const { step, duration, count } = dedupedStep;
388
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
389
+ let title = step.title;
390
+ if (skipped)
391
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
392
+ const testStep = {
393
+ title,
394
+ startTime: step.startTime.toISOString(),
395
+ duration,
396
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)),
397
+ attachments: step.attachments.map((s) => {
398
+ const index = result.attachments.indexOf(s);
399
+ if (index === -1)
400
+ throw new Error("Unexpected, attachment not found");
401
+ return index;
402
+ }),
403
+ location: this._relativeLocation(step.location),
404
+ error: step.error?.message,
405
+ count,
406
+ skipped: !!skipped
407
+ };
408
+ if (step.location)
409
+ this._stepsInFile.set(step.location.file, testStep);
410
+ return testStep;
411
+ }`;
412
+ newContent = `_createTestStep(dedupedStep, result) {
413
+ const { step, duration, count } = dedupedStep;
414
+ // Skip "Evaluate" steps with locations in runtime/node_modules files
415
+ if (step.location && step.title === "Evaluate" && (step.location.file.includes('@checksum-ai/runtime') || step.location.file.includes('node_modules'))) {
416
+ // Return null to indicate this step should be filtered out
417
+ return null;
418
+ }
419
+ const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
420
+ let title = step.title;
421
+ if (skipped)
422
+ title = \`\${title} (skipped\${skipped.description ? ": " + skipped.description : ""})\`;
423
+ const testStep = {
424
+ title,
425
+ startTime: step.startTime.toISOString(),
426
+ duration,
427
+ steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),
428
+ attachments: step.attachments.map((s) => {
429
+ const index = result.attachments.indexOf(s);
430
+ if (index === -1)
431
+ throw new Error("Unexpected, attachment not found");
432
+ return index;
433
+ }),
434
+ location: this._relativeLocation(step.location),
435
+ error: step.error?.message,
436
+ count,
437
+ skipped: !!skipped
438
+ };
439
+ if (step.location)
440
+ this._stepsInFile.set(step.location.file, testStep);
441
+ return testStep;
442
+ }`;
443
+ replaceContent(file, originalContent, newContent);
444
+
445
+ // Also filter steps when creating test results
446
+ originalContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)),`;
447
+ newContent = `steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s, result)).filter(s => s !== null),`;
448
+ replaceContent(file, originalContent, newContent);
449
+
450
+ // Normalize locations for steps from runtime/index.js - hide location if it's index.js from runtime
451
+ originalContent = `_relativeLocation(location) {
452
+ if (!location)
453
+ return void 0;
454
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
455
+ return {
456
+ file,
457
+ line: location.line,
458
+ column: location.column
459
+ };
460
+ }`;
461
+ newContent = `_relativeLocation(location) {
462
+ if (!location)
463
+ return void 0;
464
+ // Hide location for steps from runtime/index.js to reduce clutter
465
+ if (location.file && location.file.includes('@checksum-ai/runtime') && location.file.endsWith('index.js')) {
466
+ return void 0;
467
+ }
468
+ const file = toPosixPath2(import_path8.default.relative(this._config.rootDir, location.file));
469
+ return {
470
+ file,
471
+ line: location.line,
472
+ column: location.column
473
+ };
474
+ }`;
475
+ replaceContent(file, originalContent, newContent);
476
+ }
477
+
478
+ // -------- [Run] -------- //
479
+
480
+ const isRuntime = true || process.env.RUNTIME === "true";
481
+
482
+ function run(projectPath) {
483
+ try {
484
+ if (fs.existsSync(projectPath)) {
485
+ alwaysInjectScripts(projectPath);
486
+ addGenerateSelectorAndLocator(projectPath);
487
+ if (isRuntime) {
488
+ expect(projectPath);
489
+ testInfo(projectPath);
490
+ testType(projectPath);
491
+ channelOwner(projectPath);
492
+ stackTrace(projectPath);
493
+ indexContent(projectPath);
494
+ reportTraceFile(projectPath);
495
+ htmlReporter(projectPath);
496
+ }
497
+ } else {
498
+ console.warn("Project path not found", projectPath);
499
+ }
500
+ } catch (e) {
501
+ // ignore
502
+ console.error(e);
503
+ }
504
+ }
505
+
506
+ module.exports = run;