@empiricalrun/playwright-utils 0.20.14 → 0.21.1
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 +13 -0
- package/dist/test/expect.d.ts +7 -0
- package/dist/test/expect.d.ts.map +1 -0
- package/dist/test/expect.js +102 -0
- package/dist/test/index.d.ts +11 -2
- package/dist/test/index.d.ts.map +1 -1
- package/dist/test/index.js +7 -1
- package/dist/test/scripts/mouse-pointer.d.ts +2 -0
- package/dist/test/scripts/mouse-pointer.d.ts.map +1 -1
- package/dist/test/scripts/mouse-pointer.js +114 -155
- package/dist/test/scripts/pw-locator-patch/highlight/click.d.ts.map +1 -1
- package/dist/test/scripts/pw-locator-patch/highlight/click.js +48 -39
- package/docs/fixtures.md +32 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @empiricalrun/playwright-utils
|
|
2
2
|
|
|
3
|
+
## 0.21.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d2513a2: fix: don't save screenshot to disk for visual assertions
|
|
8
|
+
- 1ff54e7: fix: use CSS.escape for overlay text content and wrap in try-catch
|
|
9
|
+
|
|
10
|
+
## 0.21.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 0622b6e: feat: add custom expect for visual usability assertions
|
|
15
|
+
|
|
3
16
|
## 0.20.14
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
export type VisualMatcherResult = {
|
|
3
|
+
message: () => string;
|
|
4
|
+
pass: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function toLookRight(page: Page, pageDescription: string): Promise<VisualMatcherResult>;
|
|
7
|
+
//# sourceMappingURL=expect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/test/expect.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAO7C,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,MAAM,CAAC;IACtB,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AA0BF,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,mBAAmB,CAAC,CA4E9B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toLookRight = void 0;
|
|
4
|
+
const llm_1 = require("@empiricalrun/llm");
|
|
5
|
+
const vision_1 = require("@empiricalrun/llm/vision");
|
|
6
|
+
const mouse_pointer_1 = require("./scripts/mouse-pointer");
|
|
7
|
+
const SYSTEM_PROMPT = `
|
|
8
|
+
You are a software QA tester who specializes in visual testing. You look at a
|
|
9
|
+
UI screenshot and evaluate whether the UI is rendered correctly. While you don't
|
|
10
|
+
have a reference design for it, you can use the page description that is provided
|
|
11
|
+
to you to evaluate the UI.
|
|
12
|
+
|
|
13
|
+
There are signs when a UI is not rendered correctly. UI elements are overlapping
|
|
14
|
+
each other, text is cut off, or the UI components are not aligned. Margins are
|
|
15
|
+
uneven, or the UI components are not spaced correctly.
|
|
16
|
+
|
|
17
|
+
You need to ensure that the UI is rendered correctly. Among other things, you
|
|
18
|
+
should check the following:
|
|
19
|
+
- The page must be coherent. For example, the title of the page should be consistent
|
|
20
|
+
with the content.
|
|
21
|
+
- The page should not have large empty white/gray areas. These indicate UI layout
|
|
22
|
+
issues or page loading bugs.
|
|
23
|
+
- Images and other media on the page should be aligned and sized correctly.
|
|
24
|
+
- The page should have legible text that is easy to read.
|
|
25
|
+
- The page should not have popups or UI elements that are blocking the content.
|
|
26
|
+
|
|
27
|
+
You are to respond to your visual evaluation with a Pass or Fail, and provide
|
|
28
|
+
a reason for your evaluation.
|
|
29
|
+
`;
|
|
30
|
+
async function toLookRight(page, pageDescription) {
|
|
31
|
+
await (0, mouse_pointer_1.removeMousePointerHighlighter)(page);
|
|
32
|
+
const screenshot = await page.screenshot();
|
|
33
|
+
const base64Image = screenshot.toString("base64");
|
|
34
|
+
await (0, mouse_pointer_1.addHighlighterScriptsToPage)(page);
|
|
35
|
+
const llm = new llm_1.LLM({
|
|
36
|
+
provider: "openai",
|
|
37
|
+
defaultModel: "gpt-4o",
|
|
38
|
+
});
|
|
39
|
+
const response = await llm.createChatCompletion({
|
|
40
|
+
messages: [
|
|
41
|
+
{
|
|
42
|
+
role: "system",
|
|
43
|
+
content: SYSTEM_PROMPT,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
role: "user",
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: "Page description:\n" + pageDescription,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "image_url",
|
|
54
|
+
image_url: {
|
|
55
|
+
url: (0, vision_1.imageFormatForProvider)("anthropic", base64Image),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
modelParameters: {
|
|
62
|
+
temperature: 0.1,
|
|
63
|
+
tool_choice: {
|
|
64
|
+
type: "function",
|
|
65
|
+
function: { name: "send_response" },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
tools: [
|
|
69
|
+
{
|
|
70
|
+
type: "function",
|
|
71
|
+
function: {
|
|
72
|
+
name: "send_response",
|
|
73
|
+
description: "Send your response after evaluating the image",
|
|
74
|
+
parameters: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
reason: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Reasoning for the evaluation, shared as a step-by-step chain of thought.",
|
|
80
|
+
},
|
|
81
|
+
result: { type: "string", enum: ["Pass", "Fail"] },
|
|
82
|
+
most_critical_reason: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: `
|
|
85
|
+
Pick the most critical reason for your evaluation. For example, if you chose Fail as the result,
|
|
86
|
+
go through the reasons and pick the one that made you choose Fail`,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["reason", "result", "most_critical_reason"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
const rawResponse = response.tool_calls[0];
|
|
96
|
+
const result = JSON.parse(rawResponse.function.arguments);
|
|
97
|
+
return {
|
|
98
|
+
message: () => result.most_critical_reason,
|
|
99
|
+
pass: result.result === "Pass",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
exports.toLookRight = toLookRight;
|
package/dist/test/index.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { BrowserContext, BrowserContextOptions, Page, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, TestType } from "@playwright/test";
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
2
3
|
import { injectLocatorHighlightScripts } from "./scripts";
|
|
3
4
|
import { HighlighterOpts } from "./types";
|
|
5
|
+
declare global {
|
|
6
|
+
namespace PlaywrightTest {
|
|
7
|
+
interface Matchers<R> {
|
|
8
|
+
toLookRight(description: string): Promise<R>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
declare const extendExpect: (expectInstance: typeof expect) => import("@playwright/test").Expect<{}>;
|
|
4
13
|
type TestOptions = {
|
|
5
14
|
page: Page;
|
|
6
15
|
customContextPageProvider: (options?: BrowserContextOptions) => Promise<{
|
|
@@ -9,6 +18,6 @@ type TestOptions = {
|
|
|
9
18
|
}>;
|
|
10
19
|
saveVideos: void;
|
|
11
20
|
};
|
|
12
|
-
|
|
13
|
-
export { injectLocatorHighlightScripts };
|
|
21
|
+
declare const baseTestFixture: (testFn: TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>, options?: HighlighterOpts) => TestType<PlaywrightTestArgs & PlaywrightTestOptions & TestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
|
|
22
|
+
export { baseTestFixture, extendExpect, injectLocatorHighlightScripts };
|
|
14
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/test/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,IAAI,EACJ,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,QAAQ,EACT,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,IAAI,EACJ,kBAAkB,EAClB,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,QAAQ,EACT,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAM1C,OAAO,EAAE,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,cAAc,CAAC;QACvB,UAAU,QAAQ,CAAC,CAAC;YAClB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;SAC9C;KACF;CACF;AAED,QAAA,MAAM,YAAY,mBAA6B,aAAa,0CAG3D,CAAC;AAEF,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,yBAAyB,EAAE,CACzB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC;QAAE,OAAO,EAAE,cAAc,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACtD,UAAU,EAAE,IAAI,CAAC;CAClB,CAAC;AAIF,QAAA,MAAM,eAAe,WACX,SACN,kBAAkB,GAAG,qBAAqB,EAC1C,oBAAoB,GAAG,uBAAuB,CAC/C,YACQ,eAAe,uHA0DzB,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,6BAA6B,EAAE,CAAC"}
|
package/dist/test/index.js
CHANGED
|
@@ -3,12 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.injectLocatorHighlightScripts = exports.baseTestFixture = void 0;
|
|
6
|
+
exports.injectLocatorHighlightScripts = exports.extendExpect = exports.baseTestFixture = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const rimraf_1 = require("rimraf");
|
|
10
|
+
const expect_1 = require("./expect");
|
|
10
11
|
const scripts_1 = require("./scripts");
|
|
11
12
|
Object.defineProperty(exports, "injectLocatorHighlightScripts", { enumerable: true, get: function () { return scripts_1.injectLocatorHighlightScripts; } });
|
|
13
|
+
const extendExpect = function (expectInstance) {
|
|
14
|
+
expectInstance.extend({ toLookRight: expect_1.toLookRight });
|
|
15
|
+
return expectInstance;
|
|
16
|
+
};
|
|
17
|
+
exports.extendExpect = extendExpect;
|
|
12
18
|
const videoStore = "videos-store";
|
|
13
19
|
const baseTestFixture = function (testFn, options = {
|
|
14
20
|
mousePointerHighlighter: true,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Page } from "@playwright/test";
|
|
2
|
+
export declare function addHighlighterScriptsToPage(page: Page): Promise<void>;
|
|
3
|
+
export declare function removeMousePointerHighlighter(page: Page): Promise<void>;
|
|
2
4
|
export declare function addMousePointerHighlighter(page: Page): void;
|
|
3
5
|
//# sourceMappingURL=mouse-pointer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mouse-pointer.d.ts","sourceRoot":"","sources":["../../../src/test/scripts/mouse-pointer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"mouse-pointer.d.ts","sourceRoot":"","sources":["../../../src/test/scripts/mouse-pointer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAmD7C,wBAAsB,2BAA2B,CAAC,IAAI,EAAE,IAAI,iBAqD3D;AAED,wBAAsB,6BAA6B,CAAC,IAAI,EAAE,IAAI,iBA6B7D;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,IAAI,QAUpD"}
|
|
@@ -1,166 +1,125 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addMousePointerHighlighter = void 0;
|
|
3
|
+
exports.addMousePointerHighlighter = exports.removeMousePointerHighlighter = exports.addHighlighterScriptsToPage = void 0;
|
|
4
4
|
const logger_1 = require("../../logger");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
5
|
+
const STYLES_FOR_MOUSE_POINTER = `
|
|
6
|
+
.empirical-mouse-pointer {
|
|
7
|
+
pointer-events: none;
|
|
8
|
+
position: absolute;
|
|
9
|
+
top: 0;
|
|
10
|
+
z-index: 10000;
|
|
11
|
+
left: 0;
|
|
12
|
+
width: 20px;
|
|
13
|
+
height: 20px;
|
|
14
|
+
background: rgba(0,0,0,.4);
|
|
15
|
+
border: 1px dashed #FF000D;
|
|
16
|
+
border-radius: 10px;
|
|
17
|
+
margin: -10px 0 0 -10px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
transition: background .2s, border-radius .2s, border-color .2s;
|
|
20
|
+
}
|
|
21
|
+
.empirical-mouse-pointer.button-1 {
|
|
22
|
+
transition: none;
|
|
23
|
+
background: rgba(0,0,0,0.9);
|
|
24
|
+
}
|
|
25
|
+
.empirical-mouse-pointer.button-2 {
|
|
26
|
+
transition: none;
|
|
27
|
+
border-color: rgba(0,0,255,0.9);
|
|
28
|
+
}
|
|
29
|
+
.empirical-mouse-pointer.button-3 {
|
|
30
|
+
transition: none;
|
|
31
|
+
border-radius: 4px;
|
|
32
|
+
}
|
|
33
|
+
.empirical-mouse-pointer.button-4 {
|
|
34
|
+
transition: none;
|
|
35
|
+
border-color: rgba(255,0,0,0.9);
|
|
36
|
+
}
|
|
37
|
+
.empirical-mouse-pointer.button-5 {
|
|
38
|
+
transition: none;
|
|
39
|
+
border-color: rgba(0,255,0,0.9);
|
|
40
|
+
}
|
|
41
|
+
.empirical-element-grab-highlight {
|
|
42
|
+
outline: 2px dashed cyan !important;
|
|
43
|
+
}
|
|
44
|
+
*:focus {
|
|
45
|
+
outline: 2px dashed #FF000D !important;
|
|
46
|
+
}
|
|
47
|
+
*:focus-visible {
|
|
48
|
+
outline: 2px dashed #FF000D !important;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
async function addHighlighterScriptsToPage(page) {
|
|
52
|
+
try {
|
|
53
|
+
await page.evaluate((stylesString) => {
|
|
54
|
+
const mouseBox = document.createElement("div");
|
|
55
|
+
mouseBox.classList.add("empirical-mouse-pointer");
|
|
56
|
+
const styleElement = document.createElement("style");
|
|
57
|
+
styleElement.textContent = stylesString;
|
|
58
|
+
document.head.appendChild(styleElement);
|
|
59
|
+
document.body.appendChild(mouseBox);
|
|
60
|
+
document.addEventListener("mousemove", (event) => {
|
|
61
|
+
mouseBox.style.left = `${event.pageX}px`;
|
|
62
|
+
mouseBox.style.top = `${event.pageY}px`;
|
|
63
|
+
updateButtons(event.buttons);
|
|
64
|
+
}, true);
|
|
65
|
+
document.addEventListener("mousedown", (event) => {
|
|
66
|
+
updateButtons(event.buttons);
|
|
67
|
+
mouseBox.classList.add(`button-${event.button + 1}`);
|
|
68
|
+
}, true);
|
|
69
|
+
document.addEventListener("mouseup", (event) => {
|
|
70
|
+
updateButtons(event.buttons);
|
|
71
|
+
mouseBox.classList.remove(`button-${event.button + 1}`);
|
|
72
|
+
}, true);
|
|
73
|
+
// @ts-ignore
|
|
74
|
+
function updateButtons(buttons) {
|
|
75
|
+
for (let i = 0; i < 5; i++) {
|
|
76
|
+
mouseBox.classList.toggle(`button-${i + 1}`, (buttons / Math.pow(2, i)) % 2 >= 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, STYLES_FOR_MOUSE_POINTER);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
logger_1.logger.debug(`Error adding mouse pointer highlighter`, e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.addHighlighterScriptsToPage = addHighlighterScriptsToPage;
|
|
86
|
+
async function removeMousePointerHighlighter(page) {
|
|
87
|
+
try {
|
|
88
|
+
await page.evaluate(() => {
|
|
89
|
+
// check if any element has the class empirical-element-grab-highlight
|
|
90
|
+
// if so, remove the class
|
|
91
|
+
const hasHighlight = document.querySelectorAll(".empirical-element-grab-highlight");
|
|
92
|
+
if (hasHighlight) {
|
|
93
|
+
hasHighlight.forEach((highlight) => {
|
|
94
|
+
highlight.classList.remove("empirical-element-grab-highlight");
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// find div.empirical-mouse-pointer and remove it
|
|
98
|
+
const mousePointer = document.querySelector(".empirical-mouse-pointer");
|
|
99
|
+
if (mousePointer) {
|
|
100
|
+
mousePointer.remove();
|
|
101
|
+
}
|
|
102
|
+
// find style element with text content of STYLE_FOR_MOUSE_POINTER
|
|
103
|
+
const styleElements = document.querySelectorAll("style");
|
|
104
|
+
styleElements.forEach((styleElement) => {
|
|
105
|
+
if (styleElement.textContent?.includes("empirical-mouse-pointer")) {
|
|
106
|
+
styleElement.remove();
|
|
78
107
|
}
|
|
79
108
|
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
logger_1.logger.debug(`Error removing mouse pointer highlighter`, e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.removeMousePointerHighlighter = removeMousePointerHighlighter;
|
|
116
|
+
function addMousePointerHighlighter(page) {
|
|
117
|
+
page.on("load", async () => {
|
|
118
|
+
await addHighlighterScriptsToPage(page);
|
|
84
119
|
});
|
|
85
120
|
page.context().on("page", async (newPage) => {
|
|
86
121
|
newPage?.on("load", async () => {
|
|
87
|
-
|
|
88
|
-
await newPage?.evaluate(() => {
|
|
89
|
-
const box = document.createElement("div");
|
|
90
|
-
box.classList.add("empirical-mouse-pointer");
|
|
91
|
-
const styleElement = document.createElement("style");
|
|
92
|
-
styleElement.textContent = `
|
|
93
|
-
.empirical-mouse-pointer {
|
|
94
|
-
pointer-events: none;
|
|
95
|
-
position: absolute;
|
|
96
|
-
top: 0;
|
|
97
|
-
z-index: 10000;
|
|
98
|
-
left: 0;
|
|
99
|
-
width: 20px;
|
|
100
|
-
height: 20px;
|
|
101
|
-
background: rgba(0,0,0,.4);
|
|
102
|
-
border: 1px dashed #FF000D;
|
|
103
|
-
border-radius: 10px;
|
|
104
|
-
margin: -10px 0 0 -10px;
|
|
105
|
-
padding: 0;
|
|
106
|
-
transition: background .2s, border-radius .2s, border-color .2s;
|
|
107
|
-
}
|
|
108
|
-
.empirical-mouse-pointer.button-1 {
|
|
109
|
-
transition: none;
|
|
110
|
-
background: rgba(0,0,0,0.9);
|
|
111
|
-
}
|
|
112
|
-
.empirical-mouse-pointer.button-2 {
|
|
113
|
-
transition: none;
|
|
114
|
-
border-color: rgba(0,0,255,0.9);
|
|
115
|
-
}
|
|
116
|
-
.empirical-mouse-pointer.button-3 {
|
|
117
|
-
transition: none;
|
|
118
|
-
border-radius: 4px;
|
|
119
|
-
}
|
|
120
|
-
.empirical-mouse-pointer.button-4 {
|
|
121
|
-
transition: none;
|
|
122
|
-
border-color: rgba(255,0,0,0.9);
|
|
123
|
-
}
|
|
124
|
-
.empirical-mouse-pointer.button-5 {
|
|
125
|
-
transition: none;
|
|
126
|
-
border-color: rgba(0,255,0,0.9);
|
|
127
|
-
}
|
|
128
|
-
.empirical-element-grab-highlight {
|
|
129
|
-
outline: 2px dashed cyan !important;
|
|
130
|
-
}
|
|
131
|
-
*:focus {
|
|
132
|
-
outline: 2px dashed #FF000D !important;
|
|
133
|
-
}
|
|
134
|
-
*:focus-visible {
|
|
135
|
-
outline: 2px dashed #FF000D !important;
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
document.head.appendChild(styleElement);
|
|
139
|
-
document.body.appendChild(box);
|
|
140
|
-
document.addEventListener("mousemove", (event) => {
|
|
141
|
-
box.style.left = `${event.pageX}px`;
|
|
142
|
-
box.style.top = `${event.pageY}px`;
|
|
143
|
-
updateButtons(event.buttons);
|
|
144
|
-
}, true);
|
|
145
|
-
document.addEventListener("mousedown", (event) => {
|
|
146
|
-
updateButtons(event.buttons);
|
|
147
|
-
box.classList.add(`button-${event.button + 1}`);
|
|
148
|
-
}, true);
|
|
149
|
-
document.addEventListener("mouseup", (event) => {
|
|
150
|
-
updateButtons(event.buttons);
|
|
151
|
-
box.classList.remove(`button-${event.button + 1}`);
|
|
152
|
-
}, true);
|
|
153
|
-
// @ts-ignore
|
|
154
|
-
function updateButtons(buttons) {
|
|
155
|
-
for (let i = 0; i < 5; i++) {
|
|
156
|
-
box.classList.toggle(`button-${i + 1}`, (buttons / Math.pow(2, i)) % 2 >= 1);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
catch (e) {
|
|
162
|
-
logger_1.logger.debug(`Error adding mouse pointer highlighter`, e);
|
|
163
|
-
}
|
|
122
|
+
await addHighlighterScriptsToPage(newPage);
|
|
164
123
|
});
|
|
165
124
|
});
|
|
166
125
|
}
|
|
@@ -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;
|
|
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"}
|
|
@@ -75,46 +75,51 @@ function overlayDescription(element) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
async function textContent(pageRef, element) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (!startTag || !endTag) {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
return await pageRef.evaluate((tags) => {
|
|
91
|
-
const [startTag, endTag] = tags;
|
|
92
|
-
const htmlString = `${startTag}temp${endTag}`;
|
|
93
|
-
const parser = new DOMParser();
|
|
94
|
-
const doc = parser.parseFromString(htmlString, "text/html");
|
|
95
|
-
const tempEl = doc.body.firstElementChild;
|
|
96
|
-
let selector = tempEl.tagName.toLowerCase(); // start with the tag name, e.g., 'div'
|
|
97
|
-
// If there's an id, add it (id is unique and very specific)
|
|
98
|
-
if (tempEl.id) {
|
|
99
|
-
selector += `#${tempEl.id}`;
|
|
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}>`;
|
|
100
87
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
tempEl.classList.forEach((cls) => {
|
|
104
|
-
selector += `.${cls}`;
|
|
105
|
-
});
|
|
88
|
+
if (!startTag || !endTag) {
|
|
89
|
+
return undefined;
|
|
106
90
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
selector
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
118
123
|
}
|
|
119
124
|
}
|
|
120
125
|
function isProductFruitsOverlay(element) {
|
|
@@ -126,7 +131,11 @@ async function runAgentOnOverlay(pageRef, element) {
|
|
|
126
131
|
const promptAddition = content
|
|
127
132
|
? `
|
|
128
133
|
The popup can be identified with its text content:
|
|
129
|
-
|
|
134
|
+
|
|
135
|
+
<popup_text_content>
|
|
136
|
+
${content}
|
|
137
|
+
</popup_text_content>
|
|
138
|
+
`
|
|
130
139
|
: ``;
|
|
131
140
|
let task = isProductFruitsOverlay(element)
|
|
132
141
|
? `
|
package/docs/fixtures.md
CHANGED
|
@@ -4,20 +4,45 @@ The playwright-utils package provides fixtures that wrap around Playwright's bui
|
|
|
4
4
|
`page`, `context` fixtures to provide a mouse highlighter (which makes it easier to
|
|
5
5
|
see actions taken in a video).
|
|
6
6
|
|
|
7
|
-
To use this, you
|
|
8
|
-
fixtures file.
|
|
7
|
+
To use this, you can use the `baseTestFixture` and `extendExpect` imports
|
|
8
|
+
in your fixtures file.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { test as base,
|
|
12
|
-
import { baseTestFixture } from "@empiricalrun/playwright-utils/test";
|
|
11
|
+
import { test as base, expect as baseExpect } from "@playwright/test";
|
|
12
|
+
import { baseTestFixture, extendExpect } from "@empiricalrun/playwright-utils/test";
|
|
13
13
|
|
|
14
14
|
export const test = baseTestFixture(base);
|
|
15
|
-
export const expect =
|
|
15
|
+
export const expect = extendExpect(baseExpect);
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## Highlighters and agentic behaviors
|
|
19
|
+
|
|
20
|
+
`baseTestFixture` patches the playwright page object to add the following methods to add
|
|
21
|
+
mouse highlights and locator highlights.
|
|
22
|
+
|
|
23
|
+
These methods also enable agentic behaviors like dismissing overlays.
|
|
24
|
+
|
|
25
|
+
## Custom Matchers
|
|
26
|
+
|
|
27
|
+
The `extendExpect` method adds custom matchers that can be used with Playwright's expect:
|
|
28
|
+
|
|
29
|
+
### Visual Testing
|
|
30
|
+
|
|
31
|
+
The `toLookRight` matcher uses LLM to analyze screenshots for visual inconsistencies:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
test('page looks visually correct', async ({ page }) => {
|
|
35
|
+
await page.goto('https://your-site.com');
|
|
36
|
+
const description = "A login page with email and password fields, and a submit button"
|
|
37
|
+
await expect(page).toLookRight(description);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Advanced Use Cases
|
|
42
|
+
|
|
18
43
|
The packages also expose methods to enable advanced use-cases.
|
|
19
44
|
|
|
20
|
-
|
|
45
|
+
### Get a new browser context
|
|
21
46
|
|
|
22
47
|
This package provides a fixture `customContextPageProvider` which is a good way to create
|
|
23
48
|
a fresh, new browser context, and a page inside it.
|
|
@@ -36,7 +61,7 @@ test("Example test", async ({ page: builtInPage, customContextPageProvider }) =>
|
|
|
36
61
|
});
|
|
37
62
|
```
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
### Inject mouse highlighter manually
|
|
40
65
|
|
|
41
66
|
In some cases, you want to inject mouse highlights to a page. This is useful if the test
|
|
42
67
|
requires `launchPersistentContext`, which means the browser context is created manually.
|