@checksum-ai/runtime 1.1.18 → 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.
- package/checksum-root/.gitignore.example +1 -0
- package/checksum-root/README.md +2 -64
- package/checksum-root/checksum.config.ts +2 -0
- package/checksumlib.js +268 -68
- package/cli.js +554 -53
- package/index.d.ts +79 -20
- package/index.js +74 -71
- package/package.json +2 -1
package/checksum-root/README.md
CHANGED
|
@@ -1,65 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
### Checksum.ai Runtime
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -22885,6 +22909,14 @@
|
|
|
22885
22909
|
return str.substring(0, firstParantheses + 1) + "`" + str.substring(firstParantheses + 2, str.length - 2) + "`)";
|
|
22886
22910
|
}
|
|
22887
22911
|
__name(replaceLocatorQuotesWithTicks, "replaceLocatorQuotesWithTicks");
|
|
22912
|
+
function getElementWindowPlaywright(node) {
|
|
22913
|
+
const elementWindow = node.ownerDocument.defaultView;
|
|
22914
|
+
if (elementWindow !== window && !elementWindow.playwright) {
|
|
22915
|
+
elementWindow.playwright = window.playwright;
|
|
22916
|
+
}
|
|
22917
|
+
return elementWindow.playwright;
|
|
22918
|
+
}
|
|
22919
|
+
__name(getElementWindowPlaywright, "getElementWindowPlaywright");
|
|
22888
22920
|
var COVERED_ATTRIBUTES = [
|
|
22889
22921
|
"role",
|
|
22890
22922
|
"aria-label",
|
|
@@ -22903,7 +22935,8 @@
|
|
|
22903
22935
|
"style",
|
|
22904
22936
|
"href",
|
|
22905
22937
|
"src",
|
|
22906
|
-
"path"
|
|
22938
|
+
"path",
|
|
22939
|
+
"type"
|
|
22907
22940
|
];
|
|
22908
22941
|
|
|
22909
22942
|
// ../../node_modules/css-selector-generator/esm/utilities-iselement.js
|
|
@@ -23592,6 +23625,7 @@
|
|
|
23592
23625
|
}, "guardReturn");
|
|
23593
23626
|
|
|
23594
23627
|
// src/lib/test-generator/selectors/pw-custom-locator-generator.ts
|
|
23628
|
+
var DEBUG_MODE = false;
|
|
23595
23629
|
var PlaywrightCustomLocatorGenerator = class _PlaywrightCustomLocatorGenerator {
|
|
23596
23630
|
constructor(rootNode = document) {
|
|
23597
23631
|
this.rootNode = rootNode;
|
|
@@ -23871,7 +23905,9 @@
|
|
|
23871
23905
|
});
|
|
23872
23906
|
element = element.parentElement;
|
|
23873
23907
|
}
|
|
23874
|
-
|
|
23908
|
+
if (added.length) {
|
|
23909
|
+
console.log("Added CSS key features", added);
|
|
23910
|
+
}
|
|
23875
23911
|
} catch (error) {
|
|
23876
23912
|
console.error("Error extracting CSS key features", error);
|
|
23877
23913
|
}
|
|
@@ -23995,13 +24031,6 @@
|
|
|
23995
24031
|
console.error("[filterListItemsSelectors] error", error.message);
|
|
23996
24032
|
}
|
|
23997
24033
|
}
|
|
23998
|
-
getElementWindowPlaywright(node) {
|
|
23999
|
-
const elementWindow = node.ownerDocument.defaultView;
|
|
24000
|
-
if (elementWindow !== window && !elementWindow.playwright) {
|
|
24001
|
-
elementWindow.playwright = window.playwright;
|
|
24002
|
-
}
|
|
24003
|
-
return elementWindow.playwright;
|
|
24004
|
-
}
|
|
24005
24034
|
/**
|
|
24006
24035
|
* Returns the relevant locator for testing the element
|
|
24007
24036
|
* If we are generating a relative selector - returns playwright entry point
|
|
@@ -24010,7 +24039,7 @@
|
|
|
24010
24039
|
* @returns Locator or playwright entry point
|
|
24011
24040
|
*/
|
|
24012
24041
|
getLocatorBase(element) {
|
|
24013
|
-
const playwright =
|
|
24042
|
+
const playwright = getElementWindowPlaywright(element);
|
|
24014
24043
|
if (this.rootNode === document) {
|
|
24015
24044
|
return playwright;
|
|
24016
24045
|
}
|
|
@@ -24020,9 +24049,10 @@
|
|
|
24020
24049
|
* Returns locator representation of a selector
|
|
24021
24050
|
*/
|
|
24022
24051
|
getSelectorLocator(selector, element) {
|
|
24023
|
-
return this.
|
|
24024
|
-
|
|
24025
|
-
|
|
24052
|
+
return getElementWindowPlaywright(element ?? this.targetElement).asLocator(
|
|
24053
|
+
"javascript",
|
|
24054
|
+
selector
|
|
24055
|
+
);
|
|
24026
24056
|
}
|
|
24027
24057
|
/**
|
|
24028
24058
|
* This method tries to reduce the number of elements returned by the selectors that return multiple elements
|
|
@@ -24175,16 +24205,31 @@
|
|
|
24175
24205
|
)
|
|
24176
24206
|
]);
|
|
24177
24207
|
}
|
|
24178
|
-
|
|
24179
|
-
|
|
24180
|
-
|
|
24181
|
-
|
|
24182
|
-
|
|
24208
|
+
const getAccessibleName = /* @__PURE__ */ __name(() => {
|
|
24209
|
+
const normalize = /* @__PURE__ */ __name((txt) => this.limitTextLength(this.normalize(txt), options.exact), "normalize");
|
|
24210
|
+
const none = { text: "", exact: false };
|
|
24211
|
+
const attributeVal = element.getAttribute("aria-label") ?? element.getAttribute("aria-labelledby") ?? element.getAttribute("title");
|
|
24212
|
+
if (attributeVal) {
|
|
24213
|
+
return normalize(attributeVal);
|
|
24214
|
+
}
|
|
24215
|
+
const text = normalizeWhiteSpace(elementText(element).full);
|
|
24216
|
+
if (!text?.length) {
|
|
24217
|
+
return none;
|
|
24218
|
+
}
|
|
24219
|
+
const textVal = normalize(text);
|
|
24220
|
+
if (this.usedFilters.hasText.includes(textVal.text)) {
|
|
24221
|
+
return none;
|
|
24222
|
+
}
|
|
24223
|
+
this.usedFilters.hasText.push(textVal.text);
|
|
24224
|
+
return textVal;
|
|
24225
|
+
}, "getAccessibleName");
|
|
24226
|
+
const { text: name, exact } = getAccessibleName();
|
|
24183
24227
|
if (name && name.length > 0 && name.length < 30) {
|
|
24184
24228
|
props.push(["name", escapeForAttributeSelector(name, exact)]);
|
|
24185
24229
|
}
|
|
24186
24230
|
return options.returnLocator ? `getByRole('${role}', { ${props.join(", ")} })` : `internal:role=${role}${props.map(([n2, v2]) => `[${n2}=${v2}]`).join("")}`;
|
|
24187
24231
|
} catch (error) {
|
|
24232
|
+
console.error("Error getting role locator", error);
|
|
24188
24233
|
}
|
|
24189
24234
|
}
|
|
24190
24235
|
getByTextLocator(element, options = {}) {
|
|
@@ -24197,10 +24242,7 @@
|
|
|
24197
24242
|
return;
|
|
24198
24243
|
}
|
|
24199
24244
|
if (Array.from(element.children).filter((el) => el instanceof HTMLElement).some(
|
|
24200
|
-
// (child) => (child as HTMLElement).innerText === textContent
|
|
24201
24245
|
(child) => normalizeWhiteSpace(elementText(child).full) === text
|
|
24202
|
-
// this.normalize((child as HTMLElement).textContent) === text
|
|
24203
|
-
// this.normalize((child as HTMLElement).textContent) === text
|
|
24204
24246
|
)) {
|
|
24205
24247
|
if (!this.usedFilters.hasText.includes(text)) {
|
|
24206
24248
|
this.usedFilters.hasText.push(text);
|
|
@@ -24305,7 +24347,9 @@
|
|
|
24305
24347
|
return { text, exact };
|
|
24306
24348
|
}
|
|
24307
24349
|
checkTimeout(throwError = true) {
|
|
24308
|
-
|
|
24350
|
+
if (DEBUG_MODE) {
|
|
24351
|
+
return false;
|
|
24352
|
+
}
|
|
24309
24353
|
if (this.MAX_PROCESSING_TIME && Date.now() - this.startTimestamp > this.MAX_PROCESSING_TIME) {
|
|
24310
24354
|
if (throwError) {
|
|
24311
24355
|
throw new TimeoutError();
|
|
@@ -24408,7 +24452,10 @@
|
|
|
24408
24452
|
).filter((s2) => s2 && s2.trim().length > 0),
|
|
24409
24453
|
playwrightSelectors: await new PlaywrightCustomLocatorGenerator(
|
|
24410
24454
|
commonRoot
|
|
24411
|
-
).generate(targetElement.element
|
|
24455
|
+
).generate(targetElement.element, [], {
|
|
24456
|
+
addCSSSelectorGenerator: false,
|
|
24457
|
+
expandCSSKeyElements: false
|
|
24458
|
+
}),
|
|
24412
24459
|
featuresMetadata: targetElement.featuresMetadata
|
|
24413
24460
|
},
|
|
24414
24461
|
contextElements
|
|
@@ -24430,13 +24477,14 @@
|
|
|
24430
24477
|
}
|
|
24431
24478
|
});
|
|
24432
24479
|
const commonParents = this.getSmallestCommonParents(elementGroups).map(this.calculateElementSize).sort((a2, b) => a2.size - b.size).map((e2) => e2.element);
|
|
24433
|
-
|
|
24434
|
-
|
|
24435
|
-
const { element } = locator.locator(targetSelector);
|
|
24436
|
-
if (element) {
|
|
24437
|
-
return window.playwright.selector(element);
|
|
24438
|
-
}
|
|
24480
|
+
if (!targetSelector) {
|
|
24481
|
+
return commonParents.map((cp) => getElementWindowPlaywright(cp).selector(cp)).filter((s2) => s2);
|
|
24439
24482
|
}
|
|
24483
|
+
return commonParents.map((cp) => {
|
|
24484
|
+
const playwright = getElementWindowPlaywright(cp);
|
|
24485
|
+
const { element } = playwright.locator(playwright.selector(cp)).locator(targetSelector);
|
|
24486
|
+
return element ? playwright.selector(element) : null;
|
|
24487
|
+
}).filter((e2) => e2);
|
|
24440
24488
|
}
|
|
24441
24489
|
/**
|
|
24442
24490
|
* Performs a compound selection from metadata
|
|
@@ -24465,10 +24513,10 @@
|
|
|
24465
24513
|
compoundSelector.targetSelection
|
|
24466
24514
|
);
|
|
24467
24515
|
if (histogram.size > 0) {
|
|
24468
|
-
const
|
|
24469
|
-
|
|
24516
|
+
const selectors = Array.from(histogram.keys()).slice(0, 20).map(
|
|
24517
|
+
(el) => getElementWindowPlaywright(el).generateSelectorAndLocator(el)
|
|
24470
24518
|
);
|
|
24471
|
-
return
|
|
24519
|
+
return selectors;
|
|
24472
24520
|
}
|
|
24473
24521
|
}
|
|
24474
24522
|
}
|
|
@@ -24492,7 +24540,7 @@
|
|
|
24492
24540
|
targetSelection.cssSelectors.forEach((selector) => {
|
|
24493
24541
|
try {
|
|
24494
24542
|
addElementstoHistogram(
|
|
24495
|
-
|
|
24543
|
+
querySelectorAllExtended(selector, commonParent)
|
|
24496
24544
|
);
|
|
24497
24545
|
} catch (error) {
|
|
24498
24546
|
}
|
|
@@ -24638,7 +24686,7 @@
|
|
|
24638
24686
|
(combination) => this.createContextualSelector(commonRoot, target, combination)
|
|
24639
24687
|
).filter((s2) => s2).filter((s2) => {
|
|
24640
24688
|
try {
|
|
24641
|
-
const result =
|
|
24689
|
+
const result = querySelectorAllExtended(s2, commonRoot);
|
|
24642
24690
|
return result.length === 1 && result[0] === target;
|
|
24643
24691
|
} catch (error) {
|
|
24644
24692
|
return false;
|
|
@@ -24771,7 +24819,7 @@
|
|
|
24771
24819
|
getSmallestCommonParentsFromMetadata(contextElements) {
|
|
24772
24820
|
const elementsBySelector = contextElements.map((ce) => {
|
|
24773
24821
|
try {
|
|
24774
|
-
const elements =
|
|
24822
|
+
const elements = querySelectorAllExtended(ce.selector);
|
|
24775
24823
|
return ce.selection.type === "content" ? elements.filter(
|
|
24776
24824
|
(element) => element.textContent === ce.selection.value
|
|
24777
24825
|
) : elements;
|
|
@@ -24931,6 +24979,116 @@
|
|
|
24931
24979
|
}
|
|
24932
24980
|
};
|
|
24933
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
|
+
|
|
24934
25092
|
// src/lib/test-generator/test-generator.ts
|
|
24935
25093
|
var LOGS_PREFIX = "$checksum";
|
|
24936
25094
|
var ChecksumTestGenerator = class {
|
|
@@ -25042,7 +25200,8 @@
|
|
|
25042
25200
|
}
|
|
25043
25201
|
// -------- [API] -------- //
|
|
25044
25202
|
init(appSpecificRules, config = {}, {
|
|
25045
|
-
sessionRecorder: initSessionRecorder = true
|
|
25203
|
+
sessionRecorder: initSessionRecorder = true,
|
|
25204
|
+
filesObserver: initFilesObserver = false
|
|
25046
25205
|
} = {}, options = {}) {
|
|
25047
25206
|
this.appSpecificRules = appSpecificRules;
|
|
25048
25207
|
this.config = config;
|
|
@@ -25054,10 +25213,20 @@
|
|
|
25054
25213
|
}
|
|
25055
25214
|
if (initSessionRecorder) {
|
|
25056
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
|
+
}
|
|
25057
25222
|
rrwebEventsStorageManager.onRRwebEvent(event);
|
|
25058
25223
|
});
|
|
25059
25224
|
rrwebEventsStorageManager.initRRwebEvents();
|
|
25060
25225
|
}
|
|
25226
|
+
if (initFilesObserver) {
|
|
25227
|
+
this.filesObserver = new FilesObserver(this.sessionMirror);
|
|
25228
|
+
this.filesObserver.init();
|
|
25229
|
+
}
|
|
25061
25230
|
if (this.sessionMirror) {
|
|
25062
25231
|
this.sessionMirror.start();
|
|
25063
25232
|
this.htmlReducer.setSessionRecorder(this.sessionMirror);
|
|
@@ -25880,8 +26049,13 @@
|
|
|
25880
26049
|
super.setEventHandlers(eventHandlers);
|
|
25881
26050
|
}
|
|
25882
26051
|
async handleEvents(events, len) {
|
|
25883
|
-
console.log("starting handleEvents", len);
|
|
25884
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
|
+
}
|
|
25885
26059
|
for (const event of events) {
|
|
25886
26060
|
await this.eventProcessor.processEvent(event, this.shouldHandleEvents);
|
|
25887
26061
|
await this.skipEvents([event]);
|
|
@@ -26114,6 +26288,7 @@
|
|
|
26114
26288
|
constructor() {
|
|
26115
26289
|
super();
|
|
26116
26290
|
this.events = [];
|
|
26291
|
+
this.previousEvent = null;
|
|
26117
26292
|
this.interactions = [];
|
|
26118
26293
|
this.lastInteractionEventIndex = 0;
|
|
26119
26294
|
this.sequences = [];
|
|
@@ -26162,7 +26337,9 @@
|
|
|
26162
26337
|
if (!draggableNode) {
|
|
26163
26338
|
return;
|
|
26164
26339
|
}
|
|
26165
|
-
const draggableSelector = await this.elementSelector.getSelector(
|
|
26340
|
+
const draggableSelector = await this.elementSelector.getSelector(
|
|
26341
|
+
draggableNode
|
|
26342
|
+
);
|
|
26166
26343
|
if (!draggableSelector) {
|
|
26167
26344
|
return;
|
|
26168
26345
|
}
|
|
@@ -26172,7 +26349,9 @@
|
|
|
26172
26349
|
if (!dropzoneNode) {
|
|
26173
26350
|
return;
|
|
26174
26351
|
}
|
|
26175
|
-
const dropzoneSelector = await this.elementSelector.getSelector(
|
|
26352
|
+
const dropzoneSelector = await this.elementSelector.getSelector(
|
|
26353
|
+
dropzoneNode
|
|
26354
|
+
);
|
|
26176
26355
|
if (!dropzoneSelector) {
|
|
26177
26356
|
return;
|
|
26178
26357
|
}
|
|
@@ -26223,6 +26402,7 @@
|
|
|
26223
26402
|
this.events.push(event);
|
|
26224
26403
|
}
|
|
26225
26404
|
async postEvent(event, result) {
|
|
26405
|
+
this.previousEvent = event;
|
|
26226
26406
|
try {
|
|
26227
26407
|
await this.performSequenceSearch();
|
|
26228
26408
|
} catch (e2) {
|
|
@@ -26252,7 +26432,9 @@
|
|
|
26252
26432
|
event,
|
|
26253
26433
|
selector
|
|
26254
26434
|
);
|
|
26435
|
+
action.rrwebId = data.id;
|
|
26255
26436
|
if (action.eventCode === "input" /* Input */) {
|
|
26437
|
+
action.timestamp = this.previousEvent?.timestamp ?? event.timestamp;
|
|
26256
26438
|
action.fillValue = data.text;
|
|
26257
26439
|
this.inputFilter = {
|
|
26258
26440
|
action,
|
|
@@ -26264,7 +26446,7 @@
|
|
|
26264
26446
|
}
|
|
26265
26447
|
if (action.eventCode === "upload_files" /* UploadFiles */) {
|
|
26266
26448
|
action.files = [];
|
|
26267
|
-
action.files.push(data.text);
|
|
26449
|
+
action.files.push(data.text.split("\\").pop());
|
|
26268
26450
|
}
|
|
26269
26451
|
this.addInteraction(action);
|
|
26270
26452
|
}
|
|
@@ -26397,6 +26579,9 @@
|
|
|
26397
26579
|
}
|
|
26398
26580
|
}
|
|
26399
26581
|
addInteraction(action) {
|
|
26582
|
+
if (!action.id) {
|
|
26583
|
+
action.id = Date.now().toString();
|
|
26584
|
+
}
|
|
26400
26585
|
this.interactions.push(action);
|
|
26401
26586
|
this.lastInteractionEventIndex = this.events.length;
|
|
26402
26587
|
}
|
|
@@ -26461,6 +26646,9 @@
|
|
|
26461
26646
|
this.singleSelection = true;
|
|
26462
26647
|
this.subDocumentInspector = null;
|
|
26463
26648
|
this.listening = false;
|
|
26649
|
+
this.onMouseOut = /* @__PURE__ */ __name((event) => {
|
|
26650
|
+
elementHighlighter.clearHighlights();
|
|
26651
|
+
}, "onMouseOut");
|
|
26464
26652
|
this.onMouseOver = /* @__PURE__ */ __name(async (event) => {
|
|
26465
26653
|
const target = event.composedPath()[0];
|
|
26466
26654
|
if ("getRootNode" in target) {
|
|
@@ -26532,6 +26720,13 @@
|
|
|
26532
26720
|
if (this.singleSelection) {
|
|
26533
26721
|
this.topLevelInspector ? this.topLevelInspector.stop() : this.stop();
|
|
26534
26722
|
}
|
|
26723
|
+
window.parent.postMessage(
|
|
26724
|
+
{
|
|
26725
|
+
type: "inspector-selection",
|
|
26726
|
+
data: this.selected.at(this.selected.length - 1)
|
|
26727
|
+
},
|
|
26728
|
+
"*"
|
|
26729
|
+
);
|
|
26535
26730
|
console.log("selected", this.selected);
|
|
26536
26731
|
}, "onClick");
|
|
26537
26732
|
this.handleSubDocument = /* @__PURE__ */ __name((newRootDocument, defaultView) => {
|
|
@@ -26568,6 +26763,7 @@
|
|
|
26568
26763
|
this.stop();
|
|
26569
26764
|
this.cleanSelection();
|
|
26570
26765
|
this.rootDocument.addEventListener("mouseover", this.onMouseOver);
|
|
26766
|
+
this.rootDocument.addEventListener("mouseout", this.onMouseOut);
|
|
26571
26767
|
this.listening = true;
|
|
26572
26768
|
}
|
|
26573
26769
|
stop(clean = false) {
|
|
@@ -26617,14 +26813,18 @@
|
|
|
26617
26813
|
static {
|
|
26618
26814
|
__name(this, "VisualTestGenerator");
|
|
26619
26815
|
}
|
|
26620
|
-
init(
|
|
26621
|
-
this.timeMachine.setEventHandlers(this.eventHandlers,
|
|
26816
|
+
init() {
|
|
26817
|
+
this.timeMachine.setEventHandlers(this.eventHandlers, true);
|
|
26622
26818
|
}
|
|
26623
26819
|
consumeInteractions() {
|
|
26624
26820
|
const interactions = this.eventHandlers.getInteractions(
|
|
26625
26821
|
this.lengthOfConsumedInteractions
|
|
26626
26822
|
);
|
|
26627
26823
|
this.lengthOfConsumedInteractions += interactions.length;
|
|
26824
|
+
window.checksumSendMessage("vtg", {
|
|
26825
|
+
type: "consume-interactions",
|
|
26826
|
+
data: interactions
|
|
26827
|
+
});
|
|
26628
26828
|
return interactions;
|
|
26629
26829
|
}
|
|
26630
26830
|
startInspector(singleSelection = true, rootDocument) {
|
|
@@ -26637,7 +26837,7 @@
|
|
|
26637
26837
|
return document.querySelector(".replayer-wrapper > iframe").contentDocument;
|
|
26638
26838
|
}
|
|
26639
26839
|
stopInspector() {
|
|
26640
|
-
this.elementInspector
|
|
26840
|
+
this.elementInspector?.stop();
|
|
26641
26841
|
}
|
|
26642
26842
|
consumeSelections() {
|
|
26643
26843
|
return this.elementInspector.consumeSelections();
|