@checksum-ai/runtime 1.1.22 → 1.1.23

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.
@@ -1,65 +1,3 @@
1
- # Checksum.ai Tests
1
+ ### Checksum.ai Runtime
2
2
 
3
- ## Quick Start
4
-
5
- 1. Run `npm install -D checksumai`.
6
- 2. Navigate to the directory where you want to add Checksum tests and run `npx checksumai init`.
7
- 3. Run `npx playwright install --with-deps` to install Playwright dependencies.
8
- 4. Edit `checksum.config.ts` to include necessary configurations such as:
9
- - `apiKey`
10
- - `baseURL`
11
- - `username`
12
- - `password`
13
- 5. Update `login.ts` with your login function using Playwright. See the Login Function section below for guidance.
14
- 6. Run `npx checksumai test` to execute the example test and verify successful login.
15
- 7. If you haven't already, visit [app.checksum.ai](https://app.checksum.ai) to complete the configuration and generate a test. Then, wait for the pull request (PR) to be created and approve it.
16
-
17
- ## Login Function
18
-
19
- 1. This function is executed at the start of each test.
20
- 2. We recommend using a consistent seeded user for every test. For example, before each test, call a webhook that creates a user, seeds it with data, and returns the username and password. This approach ensures test reliability and allows parallel test execution. Configure this webhook [in your project](https://app.checksum.ai/#/settings/wizard) for consistent test generation.
21
- 3. After logging in, assert that the login was successful. Playwright waits for assertions to be correct, ensuring that the page is ready for interaction before proceeding.
22
- 4. To reuse authentication states between tests, refer to the Playwright guide on [authentication](https://playwright.dev/docs/auth). At the start of the login function, check if the user is already authenticated and return if so.
23
-
24
- ## Checksum AI Magic
25
-
26
- The tests generated by Checksum are based on Playwright. When executed using the Checksum CLI with an API key, Checksum enhances Playwright's functionality, improving test reliability and automatically maintaining tests.
27
-
28
- ### Autonomous Test Agent
29
-
30
- Checksum runs your Playwright tests regularly, but we added a few extra features to make tests more reliable. All of the features can be turned on/off through `checksum.config.ts`
31
-
32
- **Smart Selectors**
33
- When generating tests, Checksum stores extensive metadata for each action (see the `test-data` folder). If a traditional selector fails, this metadata is used for correction. For example, if a test identifies an element by its ID but the ID changes, Checksum utilizes other data points (e.g., element class, text, parents) to locate the element. Use the `checksumSelector("<id>")` method to link an action to its metadata. Do not alter the IDs.
34
-
35
- **Checksum AI**
36
- If Smart Selectors also fail, Checksum's custom-trained model can regenerate the failed section of the test. In such cases, the model might add, remove, or alter actions to achieve the same objectives, without changing the assertions. The assumption is that as long as the assertions pass, the model has successfully fixed the test. Use the `.checksumAI("<natural language description of the test>")` method to guide the model in fixing the test.
37
-
38
- You can modify the description as needed for our model. Additionally, you can include steps with only ChecksumAI descriptions, prompting our model to generate the Playwright code. For example, `await page.checksumAI("Click on 'New Task' button")` without the actual action will have our model generate the necessary Playwright code. You can even author entire tests in this manner.
39
-
40
- ### Run Modes
41
-
42
- Checksum offers three run modes:
43
-
44
- 1. **Normal** - Tests are executed using the Autonomous Test Agent as defined in the config file.
45
- 2. **Heal** - If the Autonomous Test Agent corrects a test, a new test file with the fix is created. By default, this file is created locally, but you can also configure the Agent to open a PR to your GitHub repository by setting `autoHealPRs` to true.
46
- 3. **Refactor (Work in Progress)** - The Autonomous Test Agent runs the test and, for each action, regenerates a regular Playwright selector, a Smart Selector, and a Checksum AI description.
47
-
48
- ### Mock Data
49
-
50
- When generating tests, Checksum records all backend responses, allowing tests to run in the same backend context. This is particularly useful for debugging or initial test runs, especially if your testing database/context differs from that used for test generation. Note that if your backend response format changes, the mocked data may not work as expected.
51
-
52
- ### CLI Commands
53
-
54
- 1. `init` - Initialize the Checksum directory and configurations.
55
- 2. `test` - Run Checksum tests. Accepts all [Playwright command line flags](https://playwright.dev/docs/test-cli). To override `checksum.config.ts`, pass full or partial JSON as a string, e.g., `--checksum-config='{"baseURL": "https://example.com"}'`.
56
-
57
- ## Running with GitHub Actions
58
-
59
- See the example file `github-actions.example.yml`.
60
-
61
- ## Troubleshooting
62
-
63
- **Q: I'm seeing various exceptions when I run "npx checksumai test", even before the test starts.**
64
-
65
- A: If you had a pre-installed version of Playwright, it might not be compatible with Checksum. Uninstall the Playwright and Checksum libraries, delete the relevant folder from `node_modules`, and run `npm install -D checksumai`.
3
+ To learn more about how to run tests using the Checksum.ai Runtime please refer to the [Github repository's readme](https://github.com/checksum-ai/checksum-ai-runtime?tab=readme-ov-file#checksumai-runtime)
package/checksumlib.js CHANGED
@@ -22123,10 +22123,16 @@
22123
22123
  this.start = /* @__PURE__ */ __name(() => {
22124
22124
  this.stopRRWebRecording = record({
22125
22125
  emit: this.eventHandler,
22126
+ // [rrweb config changes]
22126
22127
  sampling: {
22127
22128
  mousemove: false
22128
22129
  },
22129
- userTriggeredOnInput: true
22130
+ //mousemoveWait: 100,
22131
+ userTriggeredOnInput: true,
22132
+ maskInputOptions: {
22133
+ password: false
22134
+ // Do not mask password inputs
22135
+ }
22130
22136
  });
22131
22137
  if (this.logNativeMutationObserver) {
22132
22138
  this.selfObserve();
@@ -22733,32 +22739,40 @@
22733
22739
  this.rrwebEventsLoaded = false;
22734
22740
  this.loadRRwebEvents = [];
22735
22741
  this.rrwebCrossFrameEventIdCounter = 0;
22742
+ this.firstRequestedIndex = void 0;
22736
22743
  }
22737
22744
  static {
22738
22745
  __name(this, "RrwebEventsStorageManager");
22739
22746
  }
22740
22747
  async initRRwebEvents() {
22741
- if (this.initialized) {
22742
- console.warn(
22743
- "[RrwebEventsStorageManager] initRRwebEvents called more than once"
22744
- );
22745
- return;
22746
- }
22747
- this.initialized = true;
22748
- this.rrwebEventsIndexedDB = new IndexedDBClient("checksum", "rrwebEvents");
22749
22748
  try {
22750
- await this.rrwebEventsIndexedDB.open();
22749
+ if (this.initialized) {
22750
+ console.warn(
22751
+ "[RrwebEventsStorageManager] initRRwebEvents called more than once"
22752
+ );
22753
+ return;
22754
+ }
22755
+ this.initialized = true;
22756
+ this.rrwebEventsIndexedDB = new IndexedDBClient(
22757
+ "checksum",
22758
+ "rrwebEvents"
22759
+ );
22760
+ try {
22761
+ await this.rrwebEventsIndexedDB.open();
22762
+ } catch (e2) {
22763
+ return;
22764
+ }
22765
+ this.rrwebCrossFrameEventIdCounter = await this.rrwebEventsIndexedDB.count();
22766
+ this.loadRRwebEvents.forEach((event) => {
22767
+ this.rrwebEventsIndexedDB.set(
22768
+ event,
22769
+ this.rrwebCrossFrameEventIdCounter++
22770
+ );
22771
+ });
22772
+ this.rrwebEventsLoaded = true;
22751
22773
  } catch (e2) {
22752
- return;
22774
+ console.log(e2);
22753
22775
  }
22754
- this.rrwebCrossFrameEventIdCounter = await this.rrwebEventsIndexedDB.count();
22755
- this.loadRRwebEvents.forEach((event) => {
22756
- this.rrwebEventsIndexedDB.set(
22757
- event,
22758
- this.rrwebCrossFrameEventIdCounter++
22759
- );
22760
- });
22761
- this.rrwebEventsLoaded = true;
22762
22776
  }
22763
22777
  onRRwebEvent(event) {
22764
22778
  if (this.rrwebEventsLoaded) {
@@ -22792,14 +22806,24 @@
22792
22806
  body: JSON.stringify(rrwebEvents)
22793
22807
  });
22794
22808
  }
22795
- async getRRwebEvents(lowerBoundKey) {
22796
- if (!this.rrwebEventsLoaded) {
22809
+ async getRRwebEvents(lowerBoundKey, size) {
22810
+ try {
22811
+ if (this.firstRequestedIndex === void 0) {
22812
+ this.firstRequestedIndex = lowerBoundKey;
22813
+ }
22814
+ lowerBoundKey -= this.firstRequestedIndex;
22815
+ if (!this.rrwebEventsLoaded) {
22816
+ return [];
22817
+ }
22818
+ const bound = size ? IDBKeyRange.bound(lowerBoundKey, lowerBoundKey + size) : IDBKeyRange.lowerBound(lowerBoundKey);
22819
+ const events = await this.rrwebEventsIndexedDB.getAll(
22820
+ bound
22821
+ );
22822
+ return events;
22823
+ } catch (e2) {
22824
+ console.log(e2);
22797
22825
  return [];
22798
22826
  }
22799
- const events = await this.rrwebEventsIndexedDB.getAll(
22800
- IDBKeyRange.lowerBound(lowerBoundKey)
22801
- );
22802
- return events;
22803
22827
  }
22804
22828
  async getRRwebEvent(key) {
22805
22829
  if (!this.rrwebEventsLoaded) {
@@ -24955,6 +24979,116 @@
24955
24979
  }
24956
24980
  };
24957
24981
 
24982
+ // src/lib/test-generator/files-observer.ts
24983
+ var FilesObserver = class {
24984
+ static {
24985
+ __name(this, "FilesObserver");
24986
+ }
24987
+ constructor(sessionMirror) {
24988
+ this.fileInputsWithListeners = /* @__PURE__ */ new Set();
24989
+ this.filesMap = {};
24990
+ this.observer = null;
24991
+ this.sessionMirror = sessionMirror;
24992
+ }
24993
+ // Get files for a specific input by rrwebId
24994
+ async getFilesByRrwebId(rrwebId) {
24995
+ const files = this.filesMap[rrwebId] ?? [];
24996
+ try {
24997
+ const filesAsBase64 = await Promise.all(
24998
+ files.map(async (file) => ({
24999
+ data: await convertFileToBase64(file),
25000
+ fileName: file.name
25001
+ }))
25002
+ );
25003
+ this.filesMap[rrwebId] = [];
25004
+ return filesAsBase64;
25005
+ } catch {
25006
+ console.log("Error while parsing files to base 64");
25007
+ }
25008
+ }
25009
+ // Initialize the observer to track changes in the window
25010
+ init() {
25011
+ console.log("Starting observation...");
25012
+ console.log(this.sessionMirror);
25013
+ this.observeDOMChanges();
25014
+ window.addEventListener("unload", this.cleanup.bind(this));
25015
+ }
25016
+ // Handle the file input change event
25017
+ handleFileChange(event) {
25018
+ console.log("handleFileChange", this.sessionMirror);
25019
+ const target = event.target;
25020
+ const newFiles = Array.from(target.files || []);
25021
+ const rrwebMetaData = this.sessionMirror.getMeta(target);
25022
+ this.filesMap[rrwebMetaData.id] = newFiles;
25023
+ console.log(`Files updated for ID: ${rrwebMetaData.id}`, newFiles);
25024
+ }
25025
+ // Add listeners to file input elements
25026
+ addFileInputListeners(fileInputs) {
25027
+ fileInputs.forEach((input) => {
25028
+ if (!this.fileInputsWithListeners.has(input)) {
25029
+ input.addEventListener("change", this.handleFileChange.bind(this));
25030
+ this.fileInputsWithListeners.add(input);
25031
+ }
25032
+ });
25033
+ }
25034
+ // Observe DOM changes in the window's document
25035
+ observeDOMChanges() {
25036
+ const document2 = window.document;
25037
+ const initialFileInputs = document2.querySelectorAll(
25038
+ 'input[type="file"]'
25039
+ );
25040
+ this.addFileInputListeners(initialFileInputs);
25041
+ const onBodyReady = /* @__PURE__ */ __name(() => {
25042
+ this.observer = new MutationObserver((mutationsList) => {
25043
+ mutationsList.forEach((mutation) => {
25044
+ if (mutation.type === "childList") {
25045
+ const fileInputs = mutation.target.querySelectorAll(
25046
+ 'input[type="file"]'
25047
+ );
25048
+ this.addFileInputListeners(fileInputs);
25049
+ }
25050
+ });
25051
+ });
25052
+ this.observer.observe(document2.body, { childList: true, subtree: true });
25053
+ }, "onBodyReady");
25054
+ const bodyObserver = new MutationObserver(function() {
25055
+ if (document2.body) {
25056
+ bodyObserver.disconnect();
25057
+ onBodyReady();
25058
+ }
25059
+ });
25060
+ bodyObserver.observe(document2.documentElement, { childList: true });
25061
+ }
25062
+ // Cleanup the observer and remove event listeners
25063
+ cleanup() {
25064
+ console.log("Cleaning up files observer...");
25065
+ if (this.observer) {
25066
+ this.observer.disconnect();
25067
+ }
25068
+ this.fileInputsWithListeners.forEach((input) => {
25069
+ input.removeEventListener("change", this.handleFileChange);
25070
+ });
25071
+ this.fileInputsWithListeners.clear();
25072
+ this.filesMap = {};
25073
+ }
25074
+ };
25075
+ var convertFileToBase64 = /* @__PURE__ */ __name((file) => {
25076
+ return new Promise((resolve, reject) => {
25077
+ const reader = new FileReader();
25078
+ reader.onloadend = () => {
25079
+ if (reader.result) {
25080
+ resolve(reader.result.toString());
25081
+ } else {
25082
+ reject(new Error("File reading failed"));
25083
+ }
25084
+ };
25085
+ reader.onerror = () => {
25086
+ reject(new Error("File reading failed"));
25087
+ };
25088
+ reader.readAsDataURL(file);
25089
+ });
25090
+ }, "convertFileToBase64");
25091
+
24958
25092
  // src/lib/test-generator/test-generator.ts
24959
25093
  var LOGS_PREFIX = "$checksum";
24960
25094
  var ChecksumTestGenerator = class {
@@ -25066,7 +25200,8 @@
25066
25200
  }
25067
25201
  // -------- [API] -------- //
25068
25202
  init(appSpecificRules, config = {}, {
25069
- sessionRecorder: initSessionRecorder = true
25203
+ sessionRecorder: initSessionRecorder = true,
25204
+ filesObserver: initFilesObserver = false
25070
25205
  } = {}, options = {}) {
25071
25206
  this.appSpecificRules = appSpecificRules;
25072
25207
  this.config = config;
@@ -25078,10 +25213,20 @@
25078
25213
  }
25079
25214
  if (initSessionRecorder) {
25080
25215
  this.sessionMirror = new SessionRecorder((event) => {
25216
+ console.log("send events");
25217
+ window.checksumSendMessage?.("vtg", { type: "event", data: event });
25218
+ try {
25219
+ window["onRrwebEvents"]?.([event]);
25220
+ } catch (e2) {
25221
+ }
25081
25222
  rrwebEventsStorageManager.onRRwebEvent(event);
25082
25223
  });
25083
25224
  rrwebEventsStorageManager.initRRwebEvents();
25084
25225
  }
25226
+ if (initFilesObserver) {
25227
+ this.filesObserver = new FilesObserver(this.sessionMirror);
25228
+ this.filesObserver.init();
25229
+ }
25085
25230
  if (this.sessionMirror) {
25086
25231
  this.sessionMirror.start();
25087
25232
  this.htmlReducer.setSessionRecorder(this.sessionMirror);
@@ -25904,8 +26049,13 @@
25904
26049
  super.setEventHandlers(eventHandlers);
25905
26050
  }
25906
26051
  async handleEvents(events, len) {
25907
- console.log("starting handleEvents", len);
25908
26052
  try {
26053
+ events = events.filter(
26054
+ (event) => event.timestamp >= (this.lastSeenTimestamp ? this.lastSeenTimestamp : 0)
26055
+ );
26056
+ if (events.length) {
26057
+ this.lastSeenTimestamp = events[events.length - 1].timestamp;
26058
+ }
25909
26059
  for (const event of events) {
25910
26060
  await this.eventProcessor.processEvent(event, this.shouldHandleEvents);
25911
26061
  await this.skipEvents([event]);
@@ -26138,6 +26288,7 @@
26138
26288
  constructor() {
26139
26289
  super();
26140
26290
  this.events = [];
26291
+ this.previousEvent = null;
26141
26292
  this.interactions = [];
26142
26293
  this.lastInteractionEventIndex = 0;
26143
26294
  this.sequences = [];
@@ -26186,7 +26337,9 @@
26186
26337
  if (!draggableNode) {
26187
26338
  return;
26188
26339
  }
26189
- const draggableSelector = await this.elementSelector.getSelector(draggableNode);
26340
+ const draggableSelector = await this.elementSelector.getSelector(
26341
+ draggableNode
26342
+ );
26190
26343
  if (!draggableSelector) {
26191
26344
  return;
26192
26345
  }
@@ -26196,7 +26349,9 @@
26196
26349
  if (!dropzoneNode) {
26197
26350
  return;
26198
26351
  }
26199
- const dropzoneSelector = await this.elementSelector.getSelector(dropzoneNode);
26352
+ const dropzoneSelector = await this.elementSelector.getSelector(
26353
+ dropzoneNode
26354
+ );
26200
26355
  if (!dropzoneSelector) {
26201
26356
  return;
26202
26357
  }
@@ -26247,6 +26402,7 @@
26247
26402
  this.events.push(event);
26248
26403
  }
26249
26404
  async postEvent(event, result) {
26405
+ this.previousEvent = event;
26250
26406
  try {
26251
26407
  await this.performSequenceSearch();
26252
26408
  } catch (e2) {
@@ -26276,7 +26432,9 @@
26276
26432
  event,
26277
26433
  selector
26278
26434
  );
26435
+ action.rrwebId = data.id;
26279
26436
  if (action.eventCode === "input" /* Input */) {
26437
+ action.timestamp = this.previousEvent?.timestamp ?? event.timestamp;
26280
26438
  action.fillValue = data.text;
26281
26439
  this.inputFilter = {
26282
26440
  action,
@@ -26288,7 +26446,7 @@
26288
26446
  }
26289
26447
  if (action.eventCode === "upload_files" /* UploadFiles */) {
26290
26448
  action.files = [];
26291
- action.files.push(data.text);
26449
+ action.files.push(data.text.split("\\").pop());
26292
26450
  }
26293
26451
  this.addInteraction(action);
26294
26452
  }
@@ -26421,6 +26579,9 @@
26421
26579
  }
26422
26580
  }
26423
26581
  addInteraction(action) {
26582
+ if (!action.id) {
26583
+ action.id = Date.now().toString();
26584
+ }
26424
26585
  this.interactions.push(action);
26425
26586
  this.lastInteractionEventIndex = this.events.length;
26426
26587
  }
@@ -26485,6 +26646,9 @@
26485
26646
  this.singleSelection = true;
26486
26647
  this.subDocumentInspector = null;
26487
26648
  this.listening = false;
26649
+ this.onMouseOut = /* @__PURE__ */ __name((event) => {
26650
+ elementHighlighter.clearHighlights();
26651
+ }, "onMouseOut");
26488
26652
  this.onMouseOver = /* @__PURE__ */ __name(async (event) => {
26489
26653
  const target = event.composedPath()[0];
26490
26654
  if ("getRootNode" in target) {
@@ -26556,6 +26720,13 @@
26556
26720
  if (this.singleSelection) {
26557
26721
  this.topLevelInspector ? this.topLevelInspector.stop() : this.stop();
26558
26722
  }
26723
+ window.parent.postMessage(
26724
+ {
26725
+ type: "inspector-selection",
26726
+ data: this.selected.at(this.selected.length - 1)
26727
+ },
26728
+ "*"
26729
+ );
26559
26730
  console.log("selected", this.selected);
26560
26731
  }, "onClick");
26561
26732
  this.handleSubDocument = /* @__PURE__ */ __name((newRootDocument, defaultView) => {
@@ -26592,6 +26763,7 @@
26592
26763
  this.stop();
26593
26764
  this.cleanSelection();
26594
26765
  this.rootDocument.addEventListener("mouseover", this.onMouseOver);
26766
+ this.rootDocument.addEventListener("mouseout", this.onMouseOut);
26595
26767
  this.listening = true;
26596
26768
  }
26597
26769
  stop(clean = false) {
@@ -26641,14 +26813,18 @@
26641
26813
  static {
26642
26814
  __name(this, "VisualTestGenerator");
26643
26815
  }
26644
- init(shouldHandleEvents = true) {
26645
- this.timeMachine.setEventHandlers(this.eventHandlers, shouldHandleEvents);
26816
+ init() {
26817
+ this.timeMachine.setEventHandlers(this.eventHandlers, true);
26646
26818
  }
26647
26819
  consumeInteractions() {
26648
26820
  const interactions = this.eventHandlers.getInteractions(
26649
26821
  this.lengthOfConsumedInteractions
26650
26822
  );
26651
26823
  this.lengthOfConsumedInteractions += interactions.length;
26824
+ window.checksumSendMessage("vtg", {
26825
+ type: "consume-interactions",
26826
+ data: interactions
26827
+ });
26652
26828
  return interactions;
26653
26829
  }
26654
26830
  startInspector(singleSelection = true, rootDocument) {
@@ -26661,7 +26837,7 @@
26661
26837
  return document.querySelector(".replayer-wrapper > iframe").contentDocument;
26662
26838
  }
26663
26839
  stopInspector() {
26664
- this.elementInspector.stop();
26840
+ this.elementInspector?.stop();
26665
26841
  }
26666
26842
  consumeSelections() {
26667
26843
  return this.elementInspector.consumeSelections();