@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 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
- let server;
11
- let PORT = 1234;
12
- test_1.test.beforeAll(async () => {
13
- server = http_server_1.default.createServer({
14
- root: path_1.default.join(process.cwd(), "test-data"),
15
- });
16
- // Start the server
17
- await new Promise((resolve) => {
18
- server.listen(PORT, () => {
19
- console.log(`Server running at http://localhost:${PORT}`);
20
- resolve(true);
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
- test_1.test.afterAll(() => {
25
- if (server) {
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
- (0, test_1.test)("should dismiss survicate for click", async ({ page }) => {
33
- await page.goto(`http://localhost:${PORT}/survey.html`);
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
- (0, test_1.test)("should dismiss two-step overlay for click", async ({ page }) => {
47
- await page.goto(`http://localhost:${PORT}/tos.html`);
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
- (0, test_1.test)("should choose and dismiss the correct overlay", async ({ page }) => {
56
- await page.goto(`http://localhost:${PORT}/two-overlays.html`);
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
- (0, test_1.test)("should not return hover patch method in error stack", async ({ page, }) => {
68
- await page.goto(`http://localhost:${PORT}/tos.html`);
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:85");
88
+ (0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:97");
85
89
  });
86
- (0, test_1.test)("should not return click patch method in error stack", async ({ page, }) => {
87
- await page.goto(`http://localhost:${PORT}/tos.html`);
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:106");
107
+ (0, test_1.expect)(firstLineThatStartsWithAt).toContain("overlay-tests/click.spec.ts:119");
104
108
  });
105
- test_1.test.fail("should return from master agent planner", async ({ page }) => {
106
- await page.goto(`http://localhost:${PORT}/no-overlay.html`);
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
- (0, test_1.test)("should be able to pass through pf overlay", async ({ page }) => {
111
- await page.goto(`http://localhost:${PORT}/productfruits.html`);
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
- test_1.test.skip("should be able to fill form and dismiss overlay", async ({ page, }) => {
116
- await page.goto(`http://localhost:${PORT}/overlay-form.html`);
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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":"AACA,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AA8BrC,KAAK,cAAc,GAAG;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,CAAC;AAEF,wBAAgB,0BAA0B,CACxC,YAAY,EAAE,MAAM,GACnB,cAAc,GAAG,SAAS,CAiC5B;AA0HD,wBAAgB,UAAU,CACxB,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,EAC/C,MAAM,EAAE,MAAM,QA+Df"}
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 = exports.extractInterceptingElement = void 0;
4
- const run_1 = require("@empiricalrun/test-gen/agent/master/run");
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, utils_1.description)(this)})`;
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?.includes(ERROR_SUBSTRING_INTERCEPTION)) {
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.21.4",
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.2"
47
+ "@empiricalrun/test-gen": "^0.46.3"
48
48
  },
49
49
  "scripts": {
50
50
  "dev": "tsc --build --watch",
@@ -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