@empiricalrun/playwright-utils 0.19.25 → 0.19.27

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,22 @@
1
1
  # @empiricalrun/playwright-utils
2
2
 
3
+ ## 0.19.27
4
+
5
+ ### Patch Changes
6
+
7
+ - 3cb3120: test: two more tests for overlay dismissals
8
+ - 588c5b3: fix: error handling in overlay dismissals
9
+ - c599f74: test: failing test for survicate overlay dismissal
10
+
11
+ ## 0.19.26
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [6f876ea]
16
+ - Updated dependencies [658451e]
17
+ - @empiricalrun/test-gen@0.42.18
18
+ - @empiricalrun/llm@0.9.32
19
+
3
20
  ## 0.19.25
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -13,3 +13,11 @@ Playwright utils for test code repos of our customers
13
13
  - Captcha
14
14
  - [Email automation](./docs/email.md)
15
15
  - [Authentication](./docs/auth.md)
16
+
17
+ ## Development
18
+
19
+ To run locator patching tests
20
+
21
+ ```sh
22
+ pnpm run test-browser --headed
23
+ ```
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=click.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"click.spec.d.ts","sourceRoot":"","sources":["../../src/overlay-tests/click.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,61 @@
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
+ const test_1 = require("@playwright/test");
7
+ const http_server_1 = __importDefault(require("http-server"));
8
+ const path_1 = __importDefault(require("path"));
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
+ });
23
+ });
24
+ test_1.test.afterAll(() => {
25
+ if (server) {
26
+ server.close();
27
+ }
28
+ });
29
+ test_1.test.beforeEach(async ({ page }) => {
30
+ // Patch methods
31
+ (0, test_2.injectLocatorHighlightScripts)(page);
32
+ });
33
+ (0, test_1.test)("should dismiss survicate for click", async ({ page }) => {
34
+ await page.goto(`http://localhost:${PORT}/survey.html`);
35
+ // Assert that Survicate and button loads
36
+ await (0, test_1.expect)(page.getByText("Start survey")).toBeVisible();
37
+ await (0, test_1.expect)(page.getByRole("button", { name: "Target" })).toBeVisible();
38
+ // Do the click, which should pass
39
+ await page.getByRole("button", { name: "Target" }).click();
40
+ });
41
+ (0, test_1.test)("should dismiss two-step overlay for click", async ({ page }) => {
42
+ await page.goto(`http://localhost:${PORT}/tos.html`);
43
+ // Assert that button and overlay load
44
+ await (0, test_1.expect)(page.getByRole("button", { name: "Target" })).toBeVisible();
45
+ await (0, test_1.expect)(page.getByRole("heading", { name: "Terms of Service" })).toBeVisible();
46
+ // Do the click, which should pass
47
+ await page.getByRole("button", { name: "Target" }).click();
48
+ await (0, test_1.expect)(page.getByRole("heading", { name: "Terms of Service" })).not.toBeVisible();
49
+ });
50
+ (0, test_1.test)("should choose and dismiss the correct overlay", async ({ page }) => {
51
+ await page.goto(`http://localhost:${PORT}/two-overlays.html`);
52
+ // Assert that button and overlays load
53
+ await (0, test_1.expect)(page.getByRole("button", { name: "Target" })).toBeVisible();
54
+ await (0, test_1.expect)(page.getByText("This is a toast message")).toBeVisible();
55
+ await (0, test_1.expect)(page.getByRole("button", { name: "Close" })).toBeVisible();
56
+ // Do the click, which should pass
57
+ await page.getByRole("button", { name: "Target" }).click();
58
+ // Assert correct overlay was dismissed
59
+ await (0, test_1.expect)(page.getByText("This is a toast message")).not.toBeVisible();
60
+ await (0, test_1.expect)(page.getByRole("button", { name: "Close" })).toBeVisible();
61
+ });
@@ -1,4 +1,8 @@
1
1
  import type { Locator } from "@playwright/test";
2
+ export declare function extractInterceptingElement(errorMessage: string): {
3
+ interceptor: string;
4
+ parent: string | undefined;
5
+ } | undefined;
2
6
  export declare function patchClick(LocatorClass: Function & {
3
7
  prototype: Locator;
4
8
  }): void;
@@ -1 +1 @@
1
- {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/highlight/click.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAKhD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAuDzE"}
1
+ {"version":3,"file":"click.d.ts","sourceRoot":"","sources":["../../../../../src/test/scripts/pw-locator-patch/highlight/click.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AA6BhD,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAC3D;IACE,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,GACD,SAAS,CAiCZ;AA8CD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAmCzE"}
@@ -1,10 +1,111 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.patchClick = void 0;
3
+ exports.patchClick = exports.extractInterceptingElement = void 0;
4
4
  const planner_1 = require("@empiricalrun/test-gen/agent/master/planner");
5
5
  const run_1 = require("@empiricalrun/test-gen/agent/master/run");
6
6
  // Static flag to track if click has been patched
7
7
  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 overlayElementInfo(errorMessage) {
67
+ try {
68
+ const elements = extractInterceptingElement(errorMessage);
69
+ if (elements) {
70
+ const { interceptor, parent } = elements;
71
+ if (parent) {
72
+ return `${interceptor} inside ${parent}`;
73
+ }
74
+ else {
75
+ return `${interceptor}`;
76
+ }
77
+ }
78
+ }
79
+ catch (e) {
80
+ // Ignoring errors on this for now, since we're only using element
81
+ // info for logging.
82
+ }
83
+ }
84
+ async function runAgentOnOverlay(pageRef) {
85
+ const plannerResp = await (0, planner_1.runtimePlannerWithScreenshot)({
86
+ task: "Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately. Also note that you just need to dismiss popup and do nothing else.",
87
+ conversation: [],
88
+ page: pageRef,
89
+ });
90
+ if (plannerResp.isDone) {
91
+ throw new Error("No active popup found");
92
+ }
93
+ await (0, run_1.createTestUsingMasterAgent)({
94
+ task: "Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately. Also note that you just need to dismiss popup and do nothing else.",
95
+ page: pageRef,
96
+ options: {
97
+ useActionSpecificAnnotations: true,
98
+ },
99
+ });
100
+ }
101
+ function annotateForReport(description) {
102
+ console.log(`[Overlay dismissal]: ${description}`);
103
+ //@ts-ignore
104
+ global.testFn?.info().annotations.push({
105
+ type: "auto-dismiss-overlay",
106
+ description,
107
+ });
108
+ }
8
109
  function patchClick(LocatorClass) {
9
110
  // Check if already patched
10
111
  if (isClickPatched.get(LocatorClass)) {
@@ -16,39 +117,23 @@ function patchClick(LocatorClass) {
16
117
  try {
17
118
  await originalClick.apply(this, [options]);
18
119
  }
19
- catch (e) {
20
- //e is not typed, setting it as any to avoid ts-ignore
21
- // If its a test gen or it's not an intercept error, don't trigger the flow
22
- if (!e.message?.includes("intercepts pointer events")) {
23
- throw e;
24
- }
120
+ catch (originalError) {
25
121
  if (process.env.TEST_GEN_TOKEN) {
26
- throw e;
122
+ // Happening during test-gen, we ignore this
123
+ throw originalError;
124
+ }
125
+ if (!originalError.message?.includes(ERROR_SUBSTRING_INTERCEPTION)) {
126
+ throw originalError;
127
+ }
128
+ annotateForReport(`Attempting to auto-dismiss overlay ${overlayElementInfo(originalError.message)}`);
129
+ try {
130
+ await runAgentOnOverlay(this._frame._page);
27
131
  }
28
- const plannerResp = await (0, planner_1.runtimePlannerWithScreenshot)({
29
- task: "Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately. Also note that you just need to dismiss popup and do nothing else.",
30
- conversation: [],
31
- //Need to add the type for frame
32
- //@ts-ignore
33
- page: this._frame._page,
34
- });
35
- if (plannerResp.isDone) {
36
- throw new Error("No active popup found. Original error: ", e);
132
+ catch (agentError) {
133
+ annotateForReport(`Error in overlay dismiss: ${agentError.toString()}`);
134
+ throw originalError;
37
135
  }
38
- await (0, run_1.createTestUsingMasterAgent)({
39
- task: "Find a way to dismiss the popup. If the popup is non dismissible or there is no popup then return immediately. Also note that you just need to dismiss popup and do nothing else.",
40
- //Need to add the type for frame
41
- //@ts-ignore
42
- page: this._frame._page,
43
- options: {
44
- useActionSpecificAnnotations: true,
45
- },
46
- });
47
- //@ts-ignore
48
- global.testFn?.info().annotations.push({
49
- type: "auto-overlay-dismissed",
50
- description: "This test run had an overlay that was dismissed during runtime",
51
- });
136
+ annotateForReport("Overlay was dismissed, retrying original action");
52
137
  await originalClick.apply(this, [options]);
53
138
  }
54
139
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@empiricalrun/playwright-utils",
3
- "version": "0.19.25",
3
+ "version": "0.19.27",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/",
6
6
  "access": "public"
@@ -26,9 +26,11 @@
26
26
  "@types/async-retry": "^1.4.8",
27
27
  "@types/babel__code-frame": "^7.0.6",
28
28
  "@types/console-log-level": "^1.4.5",
29
+ "@types/http-server": "^0.12.4",
29
30
  "@types/md5": "^2.3.5",
30
31
  "@types/mime": "3.0.0",
31
- "@types/node": "^20.14.9"
32
+ "@types/node": "^20.14.9",
33
+ "http-server": "^14.1.1"
32
34
  },
33
35
  "dependencies": {
34
36
  "@babel/code-frame": "^7.24.7",
@@ -40,9 +42,9 @@
40
42
  "playwright-core": "1.47.1",
41
43
  "puppeteer-extra-plugin-recaptcha": "^3.6.8",
42
44
  "rimraf": "^6.0.1",
43
- "@empiricalrun/llm": "^0.9.31",
45
+ "@empiricalrun/llm": "^0.9.32",
44
46
  "@empiricalrun/r2-uploader": "^0.3.8",
45
- "@empiricalrun/test-gen": "^0.42.17"
47
+ "@empiricalrun/test-gen": "^0.42.18"
46
48
  },
47
49
  "scripts": {
48
50
  "dev": "tsc --build --watch",
@@ -50,6 +52,7 @@
50
52
  "clean": "tsc --build --clean",
51
53
  "lint": "eslint .",
52
54
  "test": "vitest run",
55
+ "test-browser": "npx playwright test",
53
56
  "test:watch": "vitest"
54
57
  }
55
58
  }
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from "@playwright/test";
2
+
3
+ // This config is for browser tests to verify the
4
+ // functionality of features inside playwright-utils
5
+ export default defineConfig({
6
+ testDir: "./src/overlay-tests",
7
+ retries: 0,
8
+ // Test timeout needs to be large enough for master agent etc.
9
+ // to run overlay dismissal. Action timeout can be small.
10
+ timeout: 90_000,
11
+ use: {
12
+ // Setting user agent for headed browser because Survicate
13
+ // uses this to detect headless and not render in that scenario
14
+ userAgent:
15
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
16
+ actionTimeout: 10_000,
17
+ },
18
+ expect: {
19
+ timeout: 10_000,
20
+ },
21
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ globals: true,
7
+ },
8
+ });