@empiricalrun/playwright-utils 0.19.26 → 0.19.28
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/README.md +8 -0
- package/dist/overlay-tests/click.spec.d.ts +2 -0
- package/dist/overlay-tests/click.spec.d.ts.map +1 -0
- package/dist/overlay-tests/click.spec.js +61 -0
- package/dist/test/scripts/pw-locator-patch/highlight/click.d.ts +4 -0
- package/dist/test/scripts/pw-locator-patch/highlight/click.d.ts.map +1 -1
- package/dist/test/scripts/pw-locator-patch/highlight/click.js +126 -31
- package/package.json +7 -4
- package/playwright.config.ts +21 -0
- package/vitest.config.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @empiricalrun/playwright-utils
|
|
2
2
|
|
|
3
|
+
## 0.19.28
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3dff70d: fix: add survicate specific handler for overlays
|
|
8
|
+
|
|
9
|
+
## 0.19.27
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 3cb3120: test: two more tests for overlay dismissals
|
|
14
|
+
- 588c5b3: fix: error handling in overlay dismissals
|
|
15
|
+
- c599f74: test: failing test for survicate overlay dismissal
|
|
16
|
+
|
|
3
17
|
## 0.19.26
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -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,
|
|
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,EAAQ,MAAM,kBAAkB,CAAC;AA6BtD,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAC3D;IACE,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,GACD,SAAS,CAiCZ;AAmDD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,GAAG;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,QAwCzE"}
|
|
@@ -1,10 +1,115 @@
|
|
|
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
|
+
}
|
|
109
|
+
function isSurvicateOverlay(errorMessage) {
|
|
110
|
+
const element = overlayElementInfo(errorMessage);
|
|
111
|
+
return element?.includes("survicate");
|
|
112
|
+
}
|
|
8
113
|
function patchClick(LocatorClass) {
|
|
9
114
|
// Check if already patched
|
|
10
115
|
if (isClickPatched.get(LocatorClass)) {
|
|
@@ -16,39 +121,29 @@ function patchClick(LocatorClass) {
|
|
|
16
121
|
try {
|
|
17
122
|
await originalClick.apply(this, [options]);
|
|
18
123
|
}
|
|
19
|
-
catch (
|
|
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
|
-
}
|
|
124
|
+
catch (originalError) {
|
|
25
125
|
if (process.env.TEST_GEN_TOKEN) {
|
|
26
|
-
|
|
126
|
+
// Happening during test-gen, we ignore this
|
|
127
|
+
throw originalError;
|
|
128
|
+
}
|
|
129
|
+
if (!originalError.message?.includes(ERROR_SUBSTRING_INTERCEPTION)) {
|
|
130
|
+
throw originalError;
|
|
131
|
+
}
|
|
132
|
+
annotateForReport(`Attempting to auto-dismiss overlay ${overlayElementInfo(originalError.message)}`);
|
|
133
|
+
try {
|
|
134
|
+
if (isSurvicateOverlay(originalError.message)) {
|
|
135
|
+
// TODO: Survicate specific handling that bypasses the agent; this is temporary!
|
|
136
|
+
await this._frame._page.getByLabel("Close the survey").click();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
await runAgentOnOverlay(this._frame._page);
|
|
140
|
+
}
|
|
27
141
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
|
142
|
+
catch (agentError) {
|
|
143
|
+
annotateForReport(`Error in overlay dismiss: ${agentError.toString()}`);
|
|
144
|
+
throw originalError;
|
|
37
145
|
}
|
|
38
|
-
|
|
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
|
-
});
|
|
146
|
+
annotateForReport("Overlay was dismissed, retrying original action");
|
|
52
147
|
await originalClick.apply(this, [options]);
|
|
53
148
|
}
|
|
54
149
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empiricalrun/playwright-utils",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.28",
|
|
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",
|
|
45
|
+
"@empiricalrun/llm": "^0.9.32",
|
|
43
46
|
"@empiricalrun/r2-uploader": "^0.3.8",
|
|
44
|
-
"@empiricalrun/test-gen": "^0.42.18"
|
|
45
|
-
"@empiricalrun/llm": "^0.9.32"
|
|
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
|
+
});
|