@empiricalrun/playwright-utils 0.21.4 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/overlay-tests/click.spec.js +42 -38
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.d.ts +7 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.d.ts.map +1 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/cache.js +80 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts +6 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.d.ts.map +1 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/index.js +121 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/types.d.ts +16 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/types.d.ts.map +1 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/types.js +2 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts +6 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.d.ts.map +1 -0
- package/dist/test/scripts/pw-locator-patch/dismiss-overlays/utils.js +69 -0
- package/dist/test/scripts/pw-locator-patch/highlight/click.d.ts +0 -6
- package/dist/test/scripts/pw-locator-patch/highlight/click.d.ts.map +1 -1
- package/dist/test/scripts/pw-locator-patch/highlight/click.js +9 -169
- package/package.json +2 -2
- package/playwright.config.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @empiricalrun/playwright-utils
|
|
2
2
|
|
|
3
|
+
## 0.22.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f9d8b70: feat: add caching to redo overlay dismissals faster
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f26142f: fix: browsing agent giving wrong executed action
|
|
12
|
+
- 4b545ca: test: enable parallel test execution in playwright
|
|
13
|
+
- Updated dependencies [f26142f]
|
|
14
|
+
- Updated dependencies [112b429]
|
|
15
|
+
- @empiricalrun/test-gen@0.46.3
|
|
16
|
+
|
|
3
17
|
## 0.21.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -7,30 +7,34 @@ const test_1 = require("@playwright/test");
|
|
|
7
7
|
const http_server_1 = __importDefault(require("http-server"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const test_2 = require("../test");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
10
|
+
const test = test_1.test.extend({
|
|
11
|
+
server: [
|
|
12
|
+
// eslint-disable-next-line no-empty-pattern
|
|
13
|
+
async ({}, use, workerInfo) => {
|
|
14
|
+
const port = 1234 + workerInfo.workerIndex;
|
|
15
|
+
const server = http_server_1.default.createServer({
|
|
16
|
+
root: path_1.default.join(process.cwd(), "test-data"),
|
|
17
|
+
});
|
|
18
|
+
await new Promise((resolve) => {
|
|
19
|
+
server.listen(port, () => {
|
|
20
|
+
console.log(`Server running at http://localhost:${port}`);
|
|
21
|
+
resolve();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
await use({
|
|
25
|
+
port,
|
|
26
|
+
baseURL: `http://localhost:${port}`,
|
|
27
|
+
});
|
|
28
|
+
server.close();
|
|
29
|
+
},
|
|
30
|
+
{ scope: "worker" },
|
|
31
|
+
],
|
|
23
32
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
server.close();
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
test_1.test.beforeEach(async ({ page }) => {
|
|
30
|
-
(0, test_2.injectLocatorHighlightScripts)(page, test_1.test);
|
|
33
|
+
test.beforeEach(async ({ page }) => {
|
|
34
|
+
(0, test_2.injectLocatorHighlightScripts)(page, test);
|
|
31
35
|
});
|
|
32
|
-
|
|
33
|
-
await page.goto(
|
|
36
|
+
test("should dismiss survicate for click", async ({ page, server }) => {
|
|
37
|
+
await page.goto(`${server.baseURL}/survey.html`);
|
|
34
38
|
// Assert that Survicate and button loads
|
|
35
39
|
await (0, test_1.expect)(page.getByText("Start survey")).toBeVisible();
|
|
36
40
|
const targetButton = page.getByRole("button", { name: "Target" });
|
|
@@ -43,8 +47,8 @@ test_1.test.beforeEach(async ({ page }) => {
|
|
|
43
47
|
const finalZIndex = await targetButton.evaluate((el) => window.getComputedStyle(el).getPropertyValue("z-index"));
|
|
44
48
|
(0, test_1.expect)(finalZIndex).toBe(initialZIndex);
|
|
45
49
|
});
|
|
46
|
-
|
|
47
|
-
await page.goto(
|
|
50
|
+
test("should dismiss two-step overlay for click", async ({ page, server }) => {
|
|
51
|
+
await page.goto(`${server.baseURL}/tos.html`);
|
|
48
52
|
// Assert that button and overlay load
|
|
49
53
|
await (0, test_1.expect)(page.getByRole("button", { name: "Target" })).toBeVisible();
|
|
50
54
|
await (0, test_1.expect)(page.getByRole("heading", { name: "Terms of Service" })).toBeVisible();
|
|
@@ -52,8 +56,8 @@ test_1.test.beforeEach(async ({ page }) => {
|
|
|
52
56
|
await page.getByRole("button", { name: "Target" }).click();
|
|
53
57
|
await (0, test_1.expect)(page.getByRole("heading", { name: "Terms of Service" })).not.toBeVisible();
|
|
54
58
|
});
|
|
55
|
-
|
|
56
|
-
await page.goto(
|
|
59
|
+
test("should choose and dismiss the correct overlay", async ({ page, server, }) => {
|
|
60
|
+
await page.goto(`${server.baseURL}/two-overlays.html`);
|
|
57
61
|
// Assert that button and overlays load
|
|
58
62
|
await (0, test_1.expect)(page.getByRole("button", { name: "Target" })).toBeVisible();
|
|
59
63
|
await (0, test_1.expect)(page.getByText("This is a toast message")).toBeVisible();
|
|
@@ -64,8 +68,8 @@ test_1.test.beforeEach(async ({ page }) => {
|
|
|
64
68
|
await (0, test_1.expect)(page.getByText("This is a toast message")).not.toBeVisible();
|
|
65
69
|
await (0, test_1.expect)(page.getByRole("button", { name: "Close" })).toBeVisible();
|
|
66
70
|
});
|
|
67
|
-
|
|
68
|
-
await page.goto(
|
|
71
|
+
test("should not return hover patch method in error stack", async ({ page, server, }) => {
|
|
72
|
+
await page.goto(`${server.baseURL}/tos.html`);
|
|
69
73
|
let error;
|
|
70
74
|
try {
|
|
71
75
|
await page.locator("div.some-random-class-name").hover();
|
|
@@ -81,10 +85,10 @@ test_1.test.beforeEach(async ({ page }) => {
|
|
|
81
85
|
.stack.split("\n")
|
|
82
86
|
.find((line) => line.trim().startsWith("at "));
|
|
83
87
|
(0, test_1.expect)(firstLineThatStartsWithAt).toBeDefined();
|
|
84
|
-
(0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:
|
|
88
|
+
(0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:97");
|
|
85
89
|
});
|
|
86
|
-
|
|
87
|
-
await page.goto(
|
|
90
|
+
test("should not return click patch method in error stack", async ({ page, server, }) => {
|
|
91
|
+
await page.goto(`${server.baseURL}/tos.html`);
|
|
88
92
|
let error;
|
|
89
93
|
try {
|
|
90
94
|
await page.locator("div.some-random-class-name").click();
|
|
@@ -100,20 +104,20 @@ test_1.test.beforeEach(async ({ page }) => {
|
|
|
100
104
|
.stack.split("\n")
|
|
101
105
|
.find((line) => line.trim().startsWith("at "));
|
|
102
106
|
(0, test_1.expect)(firstLineThatStartsWithAt).toBeDefined();
|
|
103
|
-
(0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:
|
|
107
|
+
(0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:119");
|
|
104
108
|
});
|
|
105
|
-
|
|
106
|
-
await page.goto(
|
|
109
|
+
test.fail("should return from master agent planner", async ({ page, server }) => {
|
|
110
|
+
await page.goto(`${server.baseURL}/no-overlay.html`);
|
|
107
111
|
// Deliberately click on a button that doesn't exist
|
|
108
112
|
await page.getByRole("button", { name: "Bottom Button" }).click();
|
|
109
113
|
});
|
|
110
|
-
|
|
111
|
-
await page.goto(
|
|
114
|
+
test("should be able to pass through pf overlay", async ({ page, server }) => {
|
|
115
|
+
await page.goto(`${server.baseURL}/productfruits.html`);
|
|
112
116
|
await page.locator("#sidebar-menu").getByText("Customise UI").click();
|
|
113
117
|
await (0, test_1.expect)(page.getByText("This is the Customise UI page")).toBeVisible();
|
|
114
118
|
});
|
|
115
|
-
|
|
116
|
-
await page.goto(
|
|
119
|
+
test("should be able to fill form and dismiss overlay", async ({ page, server, }) => {
|
|
120
|
+
await page.goto(`${server.baseURL}/overlay-form.html`);
|
|
117
121
|
await page.getByRole("button", { name: "Target" }).click();
|
|
118
122
|
await (0, test_1.expect)(page.getByText("Target was clicked")).toBeVisible();
|
|
119
123
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import { OverlayElement } from "./types";
|
|
3
|
+
export declare function setCodeToCache(pageRef: Page, element: OverlayElement | undefined, code: string): Promise<void>;
|
|
4
|
+
export declare function executeFromCache(pageRef: Page, element: OverlayElement | undefined): Promise<{
|
|
5
|
+
success: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAI7C,OAAO,EAA6B,cAAc,EAAE,MAAM,SAAS,CAAC;AAcpE,wBAAsB,cAAc,CAClC,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,EACnC,IAAI,EAAE,MAAM,iBAiCb;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,GAClC,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAiC/B"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.executeFromCache = exports.setCodeToCache = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const CACHE_FILE = path_1.default.join(process.cwd(), ".empiricalrun", `overlay-cache.json`);
|
|
11
|
+
function isCacheSupported(code) {
|
|
12
|
+
// Current release only supports caching for code that contains 1 `click` only
|
|
13
|
+
return code.includes("click") && code.trim().split("\n").length <= 1;
|
|
14
|
+
}
|
|
15
|
+
async function setCodeToCache(pageRef, element, code) {
|
|
16
|
+
if (!element || !element.interceptor) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!isCacheSupported(code)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const obj = {
|
|
23
|
+
element: {
|
|
24
|
+
dom: element.interceptor,
|
|
25
|
+
textContent: (await (0, utils_1.textContent)(pageRef, element))?.trim() || "",
|
|
26
|
+
},
|
|
27
|
+
code,
|
|
28
|
+
};
|
|
29
|
+
let cache = {
|
|
30
|
+
version: "2025-03-06",
|
|
31
|
+
data: [],
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
cache = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
35
|
+
cache.data.push(obj);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
cache = {
|
|
39
|
+
version: "2025-03-06",
|
|
40
|
+
data: [obj],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const dir = path_1.default.dirname(CACHE_FILE);
|
|
44
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
45
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
fs_1.default.writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
48
|
+
}
|
|
49
|
+
exports.setCodeToCache = setCodeToCache;
|
|
50
|
+
async function executeFromCache(pageRef, element) {
|
|
51
|
+
if (!element || !element.interceptor) {
|
|
52
|
+
return { success: false };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const cache = JSON.parse(fs_1.default.readFileSync(CACHE_FILE, "utf8"));
|
|
56
|
+
const text = (await (0, utils_1.textContent)(pageRef, element))?.trim() || "";
|
|
57
|
+
const match = cache.data.find((c) => {
|
|
58
|
+
return (c.element.dom === element.interceptor && c.element.textContent === text);
|
|
59
|
+
});
|
|
60
|
+
if (!match) {
|
|
61
|
+
return { success: false };
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
console.log(`Executing for element: ${element} and code: ${match.code}`);
|
|
65
|
+
// Ref: https://davidwalsh.name/async-function-class
|
|
66
|
+
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
67
|
+
const exec = new AsyncFunction("page", match.code);
|
|
68
|
+
await exec(pageRef);
|
|
69
|
+
return { success: true };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.log(`Failed to execute ${match.code} for element: ${element}`);
|
|
73
|
+
return { success: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return { success: false };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.executeFromCache = executeFromCache;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Page } from "@playwright/test";
|
|
2
|
+
import { OverlayElement } from "./types";
|
|
3
|
+
export declare function isErrorSupported(errorMessage: string | undefined): boolean;
|
|
4
|
+
export declare function runAgentOnOverlay(pageRef: Page, element: OverlayElement | undefined): Promise<void>;
|
|
5
|
+
export declare function extractInterceptingElement(errorMessage: string): OverlayElement | undefined;
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAKzC,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,WAKhE;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,iBAkDpC;AAED,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractInterceptingElement = exports.runAgentOnOverlay = exports.isErrorSupported = void 0;
|
|
4
|
+
const run_1 = require("@empiricalrun/test-gen/agent/master/run");
|
|
5
|
+
const cache_1 = require("./cache");
|
|
6
|
+
const utils_1 = require("./utils");
|
|
7
|
+
const ERROR_SUBSTRING_INTERCEPTION = "intercepts pointer events";
|
|
8
|
+
function isErrorSupported(errorMessage) {
|
|
9
|
+
if (!errorMessage) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
return errorMessage.includes(ERROR_SUBSTRING_INTERCEPTION);
|
|
13
|
+
}
|
|
14
|
+
exports.isErrorSupported = isErrorSupported;
|
|
15
|
+
async function runAgentOnOverlay(pageRef, element) {
|
|
16
|
+
const text = await (0, utils_1.textContent)(pageRef, element);
|
|
17
|
+
let task = `
|
|
18
|
+
Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately.
|
|
19
|
+
Also note that you just need to dismiss popup and do nothing else.`;
|
|
20
|
+
if ((0, utils_1.isProductFruitsOverlay)(element)) {
|
|
21
|
+
// Special handling for product fruits overlay: Overwrite the task
|
|
22
|
+
task = `
|
|
23
|
+
We are attempting to do a click action on Target element.
|
|
24
|
+
|
|
25
|
+
This action is failing because our Target element is covered with another element (called Overlapper).
|
|
26
|
+
|
|
27
|
+
The Overlapper element can be identifed with the following text content:
|
|
28
|
+
|
|
29
|
+
<overlapper_element_text_content>
|
|
30
|
+
${text}
|
|
31
|
+
</overlapper_element_text_content>
|
|
32
|
+
|
|
33
|
+
The only way to work around this is to Click on any other sidebar link element.
|
|
34
|
+
|
|
35
|
+
Don't reattempt the click on Target element, your job is done after the first click.
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// Append overlay text content to the task for better context
|
|
40
|
+
const promptAddition = text
|
|
41
|
+
? `
|
|
42
|
+
The popup can be identified with its text content:
|
|
43
|
+
|
|
44
|
+
<popup_text_content>
|
|
45
|
+
${text}
|
|
46
|
+
</popup_text_content>
|
|
47
|
+
`
|
|
48
|
+
: ``;
|
|
49
|
+
task += `${promptAddition}`;
|
|
50
|
+
}
|
|
51
|
+
const { success } = await (0, cache_1.executeFromCache)(pageRef, element);
|
|
52
|
+
if (success) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const result = await (0, run_1.createTestUsingMasterAgent)({
|
|
56
|
+
task,
|
|
57
|
+
page: pageRef,
|
|
58
|
+
options: {
|
|
59
|
+
useActionSpecificAnnotations: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
await (0, cache_1.setCodeToCache)(pageRef, element, result.code);
|
|
63
|
+
}
|
|
64
|
+
exports.runAgentOnOverlay = runAgentOnOverlay;
|
|
65
|
+
function extractInterceptingElement(errorMessage) {
|
|
66
|
+
// This extract element and parent info from interception error message
|
|
67
|
+
// Note that error message from the last retry is returned.
|
|
68
|
+
//
|
|
69
|
+
// [Depends on playwright error formatting]
|
|
70
|
+
// Upstream ref for error line
|
|
71
|
+
// https://github.com/microsoft/playwright/blob/bd74fc496461cc3c76b6d5def48580e2f535dac1/packages/playwright-core/src/server/dom.ts#L354
|
|
72
|
+
//
|
|
73
|
+
// Upstream ref for element description (with or without subtree)
|
|
74
|
+
// https://github.com/microsoft/playwright/blob/bd74fc496461cc3c76b6d5def48580e2f535dac1/packages/playwright-core/src/server/injected/injectedScript.ts#L938
|
|
75
|
+
const htmlElementRegex = /<([a-z][a-z0-9]*)\s+([^>]*)>/gi;
|
|
76
|
+
const linesWithElements = errorMessage
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map(removeAnsiCodes)
|
|
79
|
+
.filter((line) => line.includes(ERROR_SUBSTRING_INTERCEPTION));
|
|
80
|
+
const lastSuchLine = linesWithElements.pop();
|
|
81
|
+
if (!lastSuchLine) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const elements = findAllMatches(lastSuchLine, htmlElementRegex);
|
|
85
|
+
if (!elements) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
else if (elements.length === 1) {
|
|
89
|
+
return {
|
|
90
|
+
interceptor: elements[0].match,
|
|
91
|
+
parent: undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else if (elements.length > 1) {
|
|
95
|
+
return {
|
|
96
|
+
interceptor: elements[0].match,
|
|
97
|
+
parent: elements[1]?.match,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
exports.extractInterceptingElement = extractInterceptingElement;
|
|
102
|
+
function removeAnsiCodes(str) {
|
|
103
|
+
const ansiRegex = new RegExp(String.raw `\u001b\[[0-9;]*m`, "g");
|
|
104
|
+
return str.replace(ansiRegex, "");
|
|
105
|
+
}
|
|
106
|
+
function findAllMatches(str, regex) {
|
|
107
|
+
if (!regex.global) {
|
|
108
|
+
regex = new RegExp(regex.source, regex.flags + "g");
|
|
109
|
+
}
|
|
110
|
+
const matches = [];
|
|
111
|
+
let match;
|
|
112
|
+
while ((match = regex.exec(str)) !== null) {
|
|
113
|
+
matches.push({
|
|
114
|
+
match: match[0],
|
|
115
|
+
index: match.index,
|
|
116
|
+
groups: match.groups || {},
|
|
117
|
+
captures: match.slice(1),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return matches;
|
|
121
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type OverlayElement = {
|
|
2
|
+
interceptor: string;
|
|
3
|
+
parent: string | undefined;
|
|
4
|
+
};
|
|
5
|
+
export type CacheObject = {
|
|
6
|
+
element: {
|
|
7
|
+
dom: string;
|
|
8
|
+
textContent: string;
|
|
9
|
+
};
|
|
10
|
+
code: string;
|
|
11
|
+
};
|
|
12
|
+
export type OverlayCache = {
|
|
13
|
+
version: "2025-03-06";
|
|
14
|
+
data: Array<CacheObject>;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,OAAO,EAAE,YAAY,CAAC;IACtB,IAAI,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;CAC1B,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import { OverlayElement } from "./types";
|
|
3
|
+
export declare function overlayDescription(element: OverlayElement | undefined): string | undefined;
|
|
4
|
+
export declare function isProductFruitsOverlay(element: OverlayElement | undefined): boolean | undefined;
|
|
5
|
+
export declare function textContent(pageRef: Page, element: OverlayElement | undefined): Promise<string | undefined>;
|
|
6
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/dismiss-overlays/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAEzC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,sBASrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,uBAKzE;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,IAAI,EACb,OAAO,EAAE,cAAc,GAAG,SAAS,+BAiDpC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.textContent = exports.isProductFruitsOverlay = exports.overlayDescription = void 0;
|
|
4
|
+
function overlayDescription(element) {
|
|
5
|
+
if (element) {
|
|
6
|
+
const { interceptor, parent } = element;
|
|
7
|
+
if (parent) {
|
|
8
|
+
return `${interceptor} inside ${parent}`;
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
return `${interceptor}`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.overlayDescription = overlayDescription;
|
|
16
|
+
function isProductFruitsOverlay(element) {
|
|
17
|
+
return (element?.parent?.includes("productfruits--container") ||
|
|
18
|
+
element?.interceptor.includes("productfruits--container"));
|
|
19
|
+
}
|
|
20
|
+
exports.isProductFruitsOverlay = isProductFruitsOverlay;
|
|
21
|
+
async function textContent(pageRef, element) {
|
|
22
|
+
try {
|
|
23
|
+
if (element) {
|
|
24
|
+
const { interceptor, parent } = element;
|
|
25
|
+
let startTag = parent ? parent : interceptor;
|
|
26
|
+
let endTag;
|
|
27
|
+
const match = startTag.match(/^<\s*([^\s>]+)/);
|
|
28
|
+
if (match) {
|
|
29
|
+
const tagName = match[1]; // "div"
|
|
30
|
+
endTag = `</${tagName}>`;
|
|
31
|
+
}
|
|
32
|
+
if (!startTag || !endTag) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return await pageRef.evaluate((tags) => {
|
|
36
|
+
const [startTag, endTag] = tags;
|
|
37
|
+
const htmlString = `${startTag}temp${endTag}`;
|
|
38
|
+
const parser = new DOMParser();
|
|
39
|
+
const doc = parser.parseFromString(htmlString, "text/html");
|
|
40
|
+
const tempEl = doc.body.firstElementChild;
|
|
41
|
+
let selector = tempEl.tagName.toLowerCase(); // start with the tag name, e.g., 'div'
|
|
42
|
+
// If there's an id, add it (id is unique and very specific)
|
|
43
|
+
if (tempEl.id) {
|
|
44
|
+
selector += `#${CSS.escape(tempEl.id)}`;
|
|
45
|
+
}
|
|
46
|
+
// Add classes individually, escaping each one
|
|
47
|
+
if (tempEl.classList.length > 0) {
|
|
48
|
+
Array.from(tempEl.classList).forEach((cls) => {
|
|
49
|
+
selector += `.${CSS.escape(cls)}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Add other attributes, being careful with escaping
|
|
53
|
+
Array.from(tempEl.attributes).forEach((attr) => {
|
|
54
|
+
if (attr.name === "id" || attr.name === "class")
|
|
55
|
+
return;
|
|
56
|
+
selector += `[${attr.name}="${attr.value.replace(/"/g, '\\"')}"]`;
|
|
57
|
+
});
|
|
58
|
+
console.log("Generated selector:", selector);
|
|
59
|
+
const realElement = document.querySelector(selector);
|
|
60
|
+
return realElement?.textContent?.trim();
|
|
61
|
+
}, [startTag, endTag]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error("Unable to determine text content for overlay:", err);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.textContent = textContent;
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { Locator } from "@playwright/test";
|
|
2
2
|
import { TestFn } from "../../types";
|
|
3
|
-
type OverlayElement = {
|
|
4
|
-
interceptor: string;
|
|
5
|
-
parent: string | undefined;
|
|
6
|
-
};
|
|
7
|
-
export declare function extractInterceptingElement(errorMessage: string): OverlayElement | undefined;
|
|
8
3
|
export declare function patchClick(LocatorClass: Function & {
|
|
9
4
|
prototype: Locator;
|
|
10
5
|
}, testFn: TestFn): void;
|
|
11
|
-
export {};
|
|
12
6
|
//# sourceMappingURL=click.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/highlight/click.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/highlight/click.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAqBrC,wBAAgB,UAAU,CACxB,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,EAC/C,MAAM,EAAE,MAAM,QA+Df"}
|
|
@@ -1,171 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.patchClick =
|
|
4
|
-
const
|
|
5
|
-
const utils_1 = require("../utils");
|
|
3
|
+
exports.patchClick = void 0;
|
|
4
|
+
const dismiss_overlays_1 = require("../dismiss-overlays");
|
|
5
|
+
const utils_1 = require("../dismiss-overlays/utils");
|
|
6
|
+
const utils_2 = require("../utils");
|
|
6
7
|
// Static flag to track if click has been patched
|
|
7
8
|
const isClickPatched = new WeakMap();
|
|
8
|
-
const ERROR_SUBSTRING_INTERCEPTION = "intercepts pointer events";
|
|
9
|
-
function removeAnsiCodes(str) {
|
|
10
|
-
const ansiRegex = new RegExp(String.raw `\u001b\[[0-9;]*m`, "g");
|
|
11
|
-
return str.replace(ansiRegex, "");
|
|
12
|
-
}
|
|
13
|
-
function findAllMatches(str, regex) {
|
|
14
|
-
if (!regex.global) {
|
|
15
|
-
regex = new RegExp(regex.source, regex.flags + "g");
|
|
16
|
-
}
|
|
17
|
-
const matches = [];
|
|
18
|
-
let match;
|
|
19
|
-
while ((match = regex.exec(str)) !== null) {
|
|
20
|
-
matches.push({
|
|
21
|
-
match: match[0],
|
|
22
|
-
index: match.index,
|
|
23
|
-
groups: match.groups || {},
|
|
24
|
-
captures: match.slice(1),
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
return matches;
|
|
28
|
-
}
|
|
29
|
-
function extractInterceptingElement(errorMessage) {
|
|
30
|
-
// This extract element and parent info from interception error message
|
|
31
|
-
// Note that error message from the last retry is returned.
|
|
32
|
-
//
|
|
33
|
-
// [Depends on playwright error formatting]
|
|
34
|
-
// Upstream ref for error line
|
|
35
|
-
// https://github.com/microsoft/playwright/blob/bd74fc496461cc3c76b6d5def48580e2f535dac1/packages/playwright-core/src/server/dom.ts#L354
|
|
36
|
-
//
|
|
37
|
-
// Upstream ref for element description (with or without subtree)
|
|
38
|
-
// https://github.com/microsoft/playwright/blob/bd74fc496461cc3c76b6d5def48580e2f535dac1/packages/playwright-core/src/server/injected/injectedScript.ts#L938
|
|
39
|
-
const htmlElementRegex = /<([a-z][a-z0-9]*)\s+([^>]*)>/gi;
|
|
40
|
-
const linesWithElements = errorMessage
|
|
41
|
-
.split("\n")
|
|
42
|
-
.map(removeAnsiCodes)
|
|
43
|
-
.filter((line) => line.includes(ERROR_SUBSTRING_INTERCEPTION));
|
|
44
|
-
const lastSuchLine = linesWithElements.pop();
|
|
45
|
-
if (!lastSuchLine) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
const elements = findAllMatches(lastSuchLine, htmlElementRegex);
|
|
49
|
-
if (!elements) {
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
else if (elements.length === 1) {
|
|
53
|
-
return {
|
|
54
|
-
interceptor: elements[0].match,
|
|
55
|
-
parent: undefined,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
else if (elements.length > 1) {
|
|
59
|
-
return {
|
|
60
|
-
interceptor: elements[0].match,
|
|
61
|
-
parent: elements[1]?.match,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
exports.extractInterceptingElement = extractInterceptingElement;
|
|
66
|
-
function overlayDescription(element) {
|
|
67
|
-
if (element) {
|
|
68
|
-
const { interceptor, parent } = element;
|
|
69
|
-
if (parent) {
|
|
70
|
-
return `${interceptor} inside ${parent}`;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
return `${interceptor}`;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
async function textContent(pageRef, element) {
|
|
78
|
-
try {
|
|
79
|
-
if (element) {
|
|
80
|
-
const { interceptor, parent } = element;
|
|
81
|
-
let startTag = parent ? parent : interceptor;
|
|
82
|
-
let endTag;
|
|
83
|
-
const match = startTag.match(/^<\s*([^\s>]+)/);
|
|
84
|
-
if (match) {
|
|
85
|
-
const tagName = match[1]; // "div"
|
|
86
|
-
endTag = `</${tagName}>`;
|
|
87
|
-
}
|
|
88
|
-
if (!startTag || !endTag) {
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
return await pageRef.evaluate((tags) => {
|
|
92
|
-
const [startTag, endTag] = tags;
|
|
93
|
-
const htmlString = `${startTag}temp${endTag}`;
|
|
94
|
-
const parser = new DOMParser();
|
|
95
|
-
const doc = parser.parseFromString(htmlString, "text/html");
|
|
96
|
-
const tempEl = doc.body.firstElementChild;
|
|
97
|
-
let selector = tempEl.tagName.toLowerCase(); // start with the tag name, e.g., 'div'
|
|
98
|
-
// If there's an id, add it (id is unique and very specific)
|
|
99
|
-
if (tempEl.id) {
|
|
100
|
-
selector += `#${CSS.escape(tempEl.id)}`;
|
|
101
|
-
}
|
|
102
|
-
// Add classes individually, escaping each one
|
|
103
|
-
if (tempEl.classList.length > 0) {
|
|
104
|
-
Array.from(tempEl.classList).forEach((cls) => {
|
|
105
|
-
selector += `.${CSS.escape(cls)}`;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
// Add other attributes, being careful with escaping
|
|
109
|
-
Array.from(tempEl.attributes).forEach((attr) => {
|
|
110
|
-
if (attr.name === "id" || attr.name === "class")
|
|
111
|
-
return;
|
|
112
|
-
selector += `[${attr.name}="${attr.value.replace(/"/g, '\\"')}"]`;
|
|
113
|
-
});
|
|
114
|
-
console.log("Generated selector:", selector);
|
|
115
|
-
const realElement = document.querySelector(selector);
|
|
116
|
-
return realElement?.textContent?.trim();
|
|
117
|
-
}, [startTag, endTag]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
console.error("Unable to determine text content for overlay:", err);
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function isProductFruitsOverlay(element) {
|
|
126
|
-
return (element?.parent?.includes("productfruits--container") ||
|
|
127
|
-
element?.interceptor.includes("productfruits--container"));
|
|
128
|
-
}
|
|
129
|
-
async function runAgentOnOverlay(pageRef, element) {
|
|
130
|
-
const content = await textContent(pageRef, element);
|
|
131
|
-
const promptAddition = content
|
|
132
|
-
? `
|
|
133
|
-
The popup can be identified with its text content:
|
|
134
|
-
|
|
135
|
-
<popup_text_content>
|
|
136
|
-
${content}
|
|
137
|
-
</popup_text_content>
|
|
138
|
-
`
|
|
139
|
-
: ``;
|
|
140
|
-
let task = isProductFruitsOverlay(element)
|
|
141
|
-
? `
|
|
142
|
-
We are attempting to do a click action on Target element.
|
|
143
|
-
|
|
144
|
-
This action is failing because our Target element is covered with another element (called Overlapper).
|
|
145
|
-
|
|
146
|
-
The Overlapper element can be identifed with the following text content:
|
|
147
|
-
|
|
148
|
-
<overlapper_element_text_content>
|
|
149
|
-
${content}
|
|
150
|
-
</overlapper_element_text_content>
|
|
151
|
-
|
|
152
|
-
The only way to work around this is to Click on any other sidebar link element.
|
|
153
|
-
|
|
154
|
-
Don't reattempt the click on Target element, your job is done after the first click.
|
|
155
|
-
`
|
|
156
|
-
: `
|
|
157
|
-
Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately.
|
|
158
|
-
Also note that you just need to dismiss popup and do nothing else.
|
|
159
|
-
|
|
160
|
-
${promptAddition}`;
|
|
161
|
-
await (0, run_1.createTestUsingMasterAgent)({
|
|
162
|
-
task,
|
|
163
|
-
page: pageRef,
|
|
164
|
-
options: {
|
|
165
|
-
useActionSpecificAnnotations: true,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
9
|
function annotateForReport(testFn, description) {
|
|
170
10
|
console.log(`[Overlay dismissal]: ${description}`);
|
|
171
11
|
testFn.info().annotations.push({
|
|
@@ -181,7 +21,7 @@ function patchClick(LocatorClass, testFn) {
|
|
|
181
21
|
//ref: github.com/microsoft/playwright/blob/69287f26bc514b740eac40160503d6fac8185d37/packages/playwright-core/src/client/locator.ts#L255
|
|
182
22
|
LocatorClass.prototype.click = async function (options) {
|
|
183
23
|
let result;
|
|
184
|
-
const stepName = `locator.click(${(0,
|
|
24
|
+
const stepName = `locator.click(${(0, utils_2.description)(this)})`;
|
|
185
25
|
await testFn.step(stepName, async () => {
|
|
186
26
|
try {
|
|
187
27
|
result = await originalClick.apply(this, [options]);
|
|
@@ -194,19 +34,19 @@ function patchClick(LocatorClass, testFn) {
|
|
|
194
34
|
// Happening during test-gen, we ignore this
|
|
195
35
|
throw originalError;
|
|
196
36
|
}
|
|
197
|
-
if (!originalError.message
|
|
37
|
+
if (!(0, dismiss_overlays_1.isErrorSupported)(originalError.message)) {
|
|
198
38
|
throw originalError;
|
|
199
39
|
}
|
|
200
40
|
let overlayElement = undefined;
|
|
201
41
|
try {
|
|
202
|
-
overlayElement = extractInterceptingElement(originalError.message);
|
|
42
|
+
overlayElement = (0, dismiss_overlays_1.extractInterceptingElement)(originalError.message);
|
|
203
43
|
}
|
|
204
44
|
catch (err) {
|
|
205
45
|
// Ignoring this error
|
|
206
46
|
}
|
|
207
|
-
annotateForReport(testFn, `Attempting to auto-dismiss overlay: ${overlayDescription(overlayElement)}`);
|
|
47
|
+
annotateForReport(testFn, `Attempting to auto-dismiss overlay: ${(0, utils_1.overlayDescription)(overlayElement)}`);
|
|
208
48
|
try {
|
|
209
|
-
await runAgentOnOverlay(this._frame._page, overlayElement);
|
|
49
|
+
await (0, dismiss_overlays_1.runAgentOnOverlay)(this._frame._page, overlayElement);
|
|
210
50
|
}
|
|
211
51
|
catch (agentError) {
|
|
212
52
|
annotateForReport(testFn, `Error in overlay dismiss: ${agentError.toString()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/",
|
|
6
6
|
"access": "public"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"rimraf": "^6.0.1",
|
|
45
45
|
"@empiricalrun/llm": "^0.9.35",
|
|
46
46
|
"@empiricalrun/r2-uploader": "^0.3.8",
|
|
47
|
-
"@empiricalrun/test-gen": "^0.46.
|
|
47
|
+
"@empiricalrun/test-gen": "^0.46.3"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"dev": "tsc --build --watch",
|
package/playwright.config.ts
CHANGED
|
@@ -8,6 +8,8 @@ export default defineConfig({
|
|
|
8
8
|
// Test timeout needs to be large enough for master agent etc.
|
|
9
9
|
// to run overlay dismissal. Action timeout can be small.
|
|
10
10
|
timeout: 90_000,
|
|
11
|
+
fullyParallel: true,
|
|
12
|
+
workers: "50%",
|
|
11
13
|
use: {
|
|
12
14
|
// Setting user agent for headed browser because Survicate
|
|
13
15
|
// uses this to detect headless and not render in that scenario
|