@stablyai/playwright-base 0.1.1 → 0.1.2
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/dist/ai/extract.js +43 -7
- package/dist/ai/verify-prompt.js +12 -9
- package/dist/expect.js +12 -9
- package/dist/image-compare.js +47 -7
- package/dist/index.js +15 -5
- package/dist/playwright-augment/augment.js +18 -11
- package/dist/playwright-augment/methods/agent.js +6 -3
- package/dist/playwright-augment/methods/extract.js +10 -5
- package/dist/playwright-type-predicates.js +6 -2
- package/dist/runtime.js +8 -3
- package/dist/screenshot.js +8 -5
- package/package.json +4 -1
- package/src/ai/extract.ts +0 -97
- package/src/ai/verify-prompt.ts +0 -57
- package/src/expect.ts +0 -77
- package/src/image-compare.ts +0 -69
- package/src/index.ts +0 -111
- package/src/playwright-augment/augment.ts +0 -207
- package/src/playwright-augment/methods/agent.ts +0 -19
- package/src/playwright-augment/methods/extract.ts +0 -48
- package/src/playwright-type-predicates.ts +0 -19
- package/src/runtime.ts +0 -19
- package/src/screenshot.ts +0 -52
- package/tsconfig.build.json +0 -6
- package/tsconfig.json +0 -19
package/dist/ai/extract.js
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extract = extract;
|
|
37
|
+
const z4 = __importStar(require("zod/v4/core"));
|
|
38
|
+
const zod_1 = require("zod");
|
|
39
|
+
const runtime_1 = require("../runtime");
|
|
4
40
|
const EXTRACT_ENDPOINT = "https://api.stably.ai/internal/v1/extract";
|
|
5
|
-
const zSuccess = z.object({ value: z.unknown() });
|
|
6
|
-
const zError = z.object({ error: z.string() });
|
|
41
|
+
const zSuccess = zod_1.z.object({ value: zod_1.z.unknown() });
|
|
42
|
+
const zError = zod_1.z.object({ error: zod_1.z.string() });
|
|
7
43
|
class ExtractValidationError extends Error {
|
|
8
44
|
issues;
|
|
9
45
|
constructor(message, issues) {
|
|
@@ -19,9 +55,9 @@ async function validateWithSchema(schema, value) {
|
|
|
19
55
|
}
|
|
20
56
|
return result.data;
|
|
21
57
|
}
|
|
22
|
-
|
|
58
|
+
async function extract({ prompt, pageOrLocator, schema, }) {
|
|
23
59
|
const jsonSchema = schema ? z4.toJSONSchema(schema) : undefined;
|
|
24
|
-
const apiKey = requireApiKey();
|
|
60
|
+
const apiKey = (0, runtime_1.requireApiKey)();
|
|
25
61
|
const form = new FormData();
|
|
26
62
|
form.append("prompt", prompt);
|
|
27
63
|
if (jsonSchema) {
|
package/dist/ai/verify-prompt.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyPrompt = verifyPrompt;
|
|
1
4
|
const PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const zSuccess = z.object({
|
|
5
|
-
success: z.boolean(),
|
|
6
|
-
reason: z.string().optional(),
|
|
5
|
+
const zod_1 = require("zod");
|
|
6
|
+
const runtime_1 = require("../runtime");
|
|
7
|
+
const zSuccess = zod_1.z.object({
|
|
8
|
+
success: zod_1.z.boolean(),
|
|
9
|
+
reason: zod_1.z.string().optional(),
|
|
7
10
|
});
|
|
8
|
-
const zError = z.object({
|
|
9
|
-
error: z.string(),
|
|
11
|
+
const zError = zod_1.z.object({
|
|
12
|
+
error: zod_1.z.string(),
|
|
10
13
|
});
|
|
11
|
-
|
|
12
|
-
const apiKey = requireApiKey();
|
|
14
|
+
async function verifyPrompt({ prompt, screenshot, }) {
|
|
15
|
+
const apiKey = (0, runtime_1.requireApiKey)();
|
|
13
16
|
const form = new FormData();
|
|
14
17
|
form.append("prompt", prompt);
|
|
15
18
|
const u8 = Uint8Array.from(screenshot);
|
package/dist/expect.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stablyPlaywrightMatchers = void 0;
|
|
4
|
+
const playwright_type_predicates_1 = require("./playwright-type-predicates");
|
|
5
|
+
const verify_prompt_1 = require("./ai/verify-prompt");
|
|
6
|
+
const screenshot_1 = require("./screenshot");
|
|
4
7
|
function createFailureMessage({ targetType, condition, didPass, isNot, reason, }) {
|
|
5
8
|
const expectation = isNot ? "not to satisfy" : "to satisfy";
|
|
6
9
|
const result = didPass ? "it did" : "it did not";
|
|
@@ -10,21 +13,21 @@ function createFailureMessage({ targetType, condition, didPass, isNot, reason, }
|
|
|
10
13
|
}
|
|
11
14
|
return message;
|
|
12
15
|
}
|
|
13
|
-
|
|
16
|
+
exports.stablyPlaywrightMatchers = {
|
|
14
17
|
async toMatchScreenshotPrompt(received, condition, options) {
|
|
15
|
-
const target = isPage(received)
|
|
18
|
+
const target = (0, playwright_type_predicates_1.isPage)(received)
|
|
16
19
|
? received
|
|
17
|
-
: isLocator(received)
|
|
20
|
+
: (0, playwright_type_predicates_1.isLocator)(received)
|
|
18
21
|
? received
|
|
19
22
|
: undefined;
|
|
20
23
|
if (!target) {
|
|
21
24
|
// Should never happen
|
|
22
25
|
throw new Error("toMatchScreenshotPrompt only supports Playwright Page and Locator instances.");
|
|
23
26
|
}
|
|
24
|
-
const targetType = isPage(target) ? "page" : "locator";
|
|
27
|
+
const targetType = (0, playwright_type_predicates_1.isPage)(target) ? "page" : "locator";
|
|
25
28
|
// Wait for two consecutive identical screenshots before sending to AI
|
|
26
|
-
const screenshot = await takeStableScreenshot(target, options);
|
|
27
|
-
const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
|
|
29
|
+
const screenshot = await (0, screenshot_1.takeStableScreenshot)(target, options);
|
|
30
|
+
const verifyResult = await (0, verify_prompt_1.verifyPrompt)({ prompt: condition, screenshot });
|
|
28
31
|
return {
|
|
29
32
|
pass: verifyResult.pass,
|
|
30
33
|
message: () => createFailureMessage({
|
package/dist/image-compare.js
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.imagesAreSimilar = void 0;
|
|
40
|
+
const pixelmatch_1 = __importDefault(require("pixelmatch"));
|
|
41
|
+
const pngjs_1 = require("pngjs");
|
|
42
|
+
const jpeg = __importStar(require("jpeg-js"));
|
|
4
43
|
const isPng = (buffer) => {
|
|
5
44
|
return (buffer.length >= 8 &&
|
|
6
45
|
buffer[0] === 0x89 &&
|
|
@@ -17,7 +56,7 @@ const isJpeg = (buffer) => {
|
|
|
17
56
|
};
|
|
18
57
|
const decodeImage = (buffer) => {
|
|
19
58
|
if (isPng(buffer)) {
|
|
20
|
-
const png = PNG.sync.read(buffer);
|
|
59
|
+
const png = pngjs_1.PNG.sync.read(buffer);
|
|
21
60
|
return { data: png.data, width: png.width, height: png.height };
|
|
22
61
|
}
|
|
23
62
|
if (isJpeg(buffer)) {
|
|
@@ -25,10 +64,10 @@ const decodeImage = (buffer) => {
|
|
|
25
64
|
return { data: img.data, width: img.width, height: img.height };
|
|
26
65
|
}
|
|
27
66
|
// Default to PNG decode; if it fails upstream, treat as different sizes
|
|
28
|
-
const png = PNG.sync.read(buffer);
|
|
67
|
+
const png = pngjs_1.PNG.sync.read(buffer);
|
|
29
68
|
return { data: png.data, width: png.width, height: png.height };
|
|
30
69
|
};
|
|
31
|
-
|
|
70
|
+
const imagesAreSimilar = ({ image1, image2, threshold, }) => {
|
|
32
71
|
const decodedImage1 = decodeImage(image1);
|
|
33
72
|
const decodedImage2 = decodeImage(image2);
|
|
34
73
|
if (decodedImage1.width !== decodedImage2.width ||
|
|
@@ -36,6 +75,7 @@ export const imagesAreSimilar = ({ image1, image2, threshold, }) => {
|
|
|
36
75
|
return false;
|
|
37
76
|
}
|
|
38
77
|
const diffRgbaData = new Uint8Array(decodedImage1.width * decodedImage1.height * 4);
|
|
39
|
-
const numDiffPixels =
|
|
78
|
+
const numDiffPixels = (0, pixelmatch_1.default)(decodedImage1.data, decodedImage2.data, diffRgbaData, decodedImage1.width, decodedImage1.height, { threshold });
|
|
40
79
|
return numDiffPixels === 0;
|
|
41
80
|
};
|
|
81
|
+
exports.imagesAreSimilar = imagesAreSimilar;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requireApiKey = exports.stablyPlaywrightMatchers = exports.augmentPage = exports.augmentLocator = exports.augmentBrowserType = exports.augmentBrowserContext = exports.augmentBrowser = exports.setApiKey = void 0;
|
|
4
|
+
const augment_1 = require("./playwright-augment/augment");
|
|
5
|
+
Object.defineProperty(exports, "augmentBrowser", { enumerable: true, get: function () { return augment_1.augmentBrowser; } });
|
|
6
|
+
Object.defineProperty(exports, "augmentBrowserContext", { enumerable: true, get: function () { return augment_1.augmentBrowserContext; } });
|
|
7
|
+
Object.defineProperty(exports, "augmentBrowserType", { enumerable: true, get: function () { return augment_1.augmentBrowserType; } });
|
|
8
|
+
Object.defineProperty(exports, "augmentLocator", { enumerable: true, get: function () { return augment_1.augmentLocator; } });
|
|
9
|
+
Object.defineProperty(exports, "augmentPage", { enumerable: true, get: function () { return augment_1.augmentPage; } });
|
|
10
|
+
const expect_1 = require("./expect");
|
|
11
|
+
Object.defineProperty(exports, "stablyPlaywrightMatchers", { enumerable: true, get: function () { return expect_1.stablyPlaywrightMatchers; } });
|
|
12
|
+
const runtime_1 = require("./runtime");
|
|
13
|
+
Object.defineProperty(exports, "requireApiKey", { enumerable: true, get: function () { return runtime_1.requireApiKey; } });
|
|
14
|
+
var runtime_2 = require("./runtime");
|
|
15
|
+
Object.defineProperty(exports, "setApiKey", { enumerable: true, get: function () { return runtime_2.setApiKey; } });
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.augmentLocator = augmentLocator;
|
|
4
|
+
exports.augmentPage = augmentPage;
|
|
5
|
+
exports.augmentBrowserContext = augmentBrowserContext;
|
|
6
|
+
exports.augmentBrowser = augmentBrowser;
|
|
7
|
+
exports.augmentBrowserType = augmentBrowserType;
|
|
8
|
+
const extract_1 = require("./methods/extract");
|
|
9
|
+
const agent_1 = require("./methods/agent");
|
|
3
10
|
const LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
|
|
4
11
|
const LOCATOR_DESCRIBE_WRAPPED = Symbol.for("stably.playwright.locatorDescribeWrapped");
|
|
5
12
|
const PAGE_PATCHED = Symbol.for("stably.playwright.pagePatched");
|
|
@@ -14,11 +21,11 @@ function defineHiddenProperty(target, key, value) {
|
|
|
14
21
|
writable: true,
|
|
15
22
|
});
|
|
16
23
|
}
|
|
17
|
-
|
|
24
|
+
function augmentLocator(locator) {
|
|
18
25
|
if (locator[LOCATOR_PATCHED]) {
|
|
19
26
|
return locator;
|
|
20
27
|
}
|
|
21
|
-
defineHiddenProperty(locator, "extract", createLocatorExtract(locator));
|
|
28
|
+
defineHiddenProperty(locator, "extract", (0, extract_1.createLocatorExtract)(locator));
|
|
22
29
|
const markerTarget = locator;
|
|
23
30
|
if (typeof locator.describe === "function" &&
|
|
24
31
|
!markerTarget[LOCATOR_DESCRIBE_WRAPPED]) {
|
|
@@ -33,7 +40,7 @@ export function augmentLocator(locator) {
|
|
|
33
40
|
defineHiddenProperty(locator, LOCATOR_PATCHED, true);
|
|
34
41
|
return locator;
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
function augmentPage(page) {
|
|
37
44
|
if (page[PAGE_PATCHED]) {
|
|
38
45
|
return page;
|
|
39
46
|
}
|
|
@@ -42,11 +49,11 @@ export function augmentPage(page) {
|
|
|
42
49
|
const locator = originalLocator(...args);
|
|
43
50
|
return augmentLocator(locator);
|
|
44
51
|
});
|
|
45
|
-
defineHiddenProperty(page, "extract", createPageExtract(page));
|
|
52
|
+
defineHiddenProperty(page, "extract", (0, extract_1.createPageExtract)(page));
|
|
46
53
|
defineHiddenProperty(page, PAGE_PATCHED, true);
|
|
47
54
|
return page;
|
|
48
55
|
}
|
|
49
|
-
|
|
56
|
+
function augmentBrowserContext(context) {
|
|
50
57
|
if (context[CONTEXT_PATCHED]) {
|
|
51
58
|
return context;
|
|
52
59
|
}
|
|
@@ -60,12 +67,12 @@ export function augmentBrowserContext(context) {
|
|
|
60
67
|
context.pages = (() => originalPages().map((page) => augmentPage(page)));
|
|
61
68
|
}
|
|
62
69
|
if (!context.agent) {
|
|
63
|
-
defineHiddenProperty(context, "agent", createAgentStub());
|
|
70
|
+
defineHiddenProperty(context, "agent", (0, agent_1.createAgentStub)());
|
|
64
71
|
}
|
|
65
72
|
defineHiddenProperty(context, CONTEXT_PATCHED, true);
|
|
66
73
|
return context;
|
|
67
74
|
}
|
|
68
|
-
|
|
75
|
+
function augmentBrowser(browser) {
|
|
69
76
|
if (browser[BROWSER_PATCHED]) {
|
|
70
77
|
return browser;
|
|
71
78
|
}
|
|
@@ -82,12 +89,12 @@ export function augmentBrowser(browser) {
|
|
|
82
89
|
const originalContexts = browser.contexts.bind(browser);
|
|
83
90
|
browser.contexts = (() => originalContexts().map((context) => augmentBrowserContext(context)));
|
|
84
91
|
if (!browser.agent) {
|
|
85
|
-
defineHiddenProperty(browser, "agent", createAgentStub());
|
|
92
|
+
defineHiddenProperty(browser, "agent", (0, agent_1.createAgentStub)());
|
|
86
93
|
}
|
|
87
94
|
defineHiddenProperty(browser, BROWSER_PATCHED, true);
|
|
88
95
|
return browser;
|
|
89
96
|
}
|
|
90
|
-
|
|
97
|
+
function augmentBrowserType(browserType) {
|
|
91
98
|
if (browserType[BROWSER_TYPE_PATCHED]) {
|
|
92
99
|
return browserType;
|
|
93
100
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAgentStub = createAgentStub;
|
|
4
|
+
const runtime_1 = require("../../runtime");
|
|
5
|
+
function createAgentStub() {
|
|
3
6
|
return async (prompt, options) => {
|
|
4
|
-
requireApiKey();
|
|
7
|
+
(0, runtime_1.requireApiKey)();
|
|
5
8
|
void prompt;
|
|
6
9
|
void options;
|
|
7
10
|
return { success: true };
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPageExtract = exports.createLocatorExtract = void 0;
|
|
4
|
+
const extract_1 = require("../../ai/extract");
|
|
2
5
|
function createExtract(pageOrLocator) {
|
|
3
6
|
const impl = (async (prompt, options) => {
|
|
4
7
|
if (options?.schema) {
|
|
5
|
-
return extract({
|
|
8
|
+
return (0, extract_1.extract)({
|
|
6
9
|
prompt,
|
|
7
10
|
schema: options.schema,
|
|
8
11
|
pageOrLocator,
|
|
9
12
|
});
|
|
10
13
|
}
|
|
11
|
-
return extract({ prompt, pageOrLocator });
|
|
14
|
+
return (0, extract_1.extract)({ prompt, pageOrLocator });
|
|
12
15
|
});
|
|
13
16
|
return impl;
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const createLocatorExtract = (locator) => createExtract(locator);
|
|
19
|
+
exports.createLocatorExtract = createLocatorExtract;
|
|
20
|
+
const createPageExtract = (page) => createExtract(page);
|
|
21
|
+
exports.createPageExtract = createPageExtract;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isPage = isPage;
|
|
4
|
+
exports.isLocator = isLocator;
|
|
5
|
+
function isPage(candidate) {
|
|
2
6
|
return (typeof candidate === "object" &&
|
|
3
7
|
candidate !== null &&
|
|
4
8
|
typeof candidate.screenshot === "function" &&
|
|
5
9
|
typeof candidate.goto === "function");
|
|
6
10
|
}
|
|
7
|
-
|
|
11
|
+
function isLocator(candidate) {
|
|
8
12
|
return (typeof candidate === "object" &&
|
|
9
13
|
candidate !== null &&
|
|
10
14
|
typeof candidate.screenshot === "function" &&
|
package/dist/runtime.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setApiKey = setApiKey;
|
|
4
|
+
exports.getApiKey = getApiKey;
|
|
5
|
+
exports.requireApiKey = requireApiKey;
|
|
1
6
|
let configuredApiKey = process.env.STABLY_API_KEY;
|
|
2
|
-
|
|
7
|
+
function setApiKey(apiKey) {
|
|
3
8
|
configuredApiKey = apiKey;
|
|
4
9
|
}
|
|
5
|
-
|
|
10
|
+
function getApiKey() {
|
|
6
11
|
return configuredApiKey;
|
|
7
12
|
}
|
|
8
|
-
|
|
13
|
+
function requireApiKey() {
|
|
9
14
|
const apiKey = getApiKey();
|
|
10
15
|
if (!apiKey) {
|
|
11
16
|
throw new Error("Missing Stably API key. Call setApiKey(apiKey) or set the STABLY_API_KEY environment variable.");
|
package/dist/screenshot.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.takeStableScreenshot = takeStableScreenshot;
|
|
4
|
+
const playwright_type_predicates_1 = require("./playwright-type-predicates");
|
|
5
|
+
const image_compare_1 = require("./image-compare");
|
|
6
|
+
async function takeStableScreenshot(target, options) {
|
|
7
|
+
const page = (0, playwright_type_predicates_1.isPage)(target) ? target : target.page();
|
|
5
8
|
// Use a small budget for stabilization within the overall assertion timeout.
|
|
6
9
|
// We allocate up to 25% of the total timeout (bounded between 300ms and 2000ms).
|
|
7
10
|
const totalTimeout = options?.timeout ?? 5000;
|
|
@@ -25,7 +28,7 @@ export async function takeStableScreenshot(target, options) {
|
|
|
25
28
|
if (!isFirstIteration &&
|
|
26
29
|
actual &&
|
|
27
30
|
previous &&
|
|
28
|
-
imagesAreSimilar({
|
|
31
|
+
(0, image_compare_1.imagesAreSimilar)({
|
|
29
32
|
image1: previous,
|
|
30
33
|
image2: actual,
|
|
31
34
|
threshold: options?.threshold ?? 0.02,
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stablyai/playwright-base",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Shared augmentation runtime for Stably Playwright wrappers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
7
10
|
"engines": {
|
|
8
11
|
"node": ">=18"
|
|
9
12
|
},
|
package/src/ai/extract.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { Locator, Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import * as z4 from "zod/v4/core";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { requireApiKey } from "../runtime";
|
|
5
|
-
|
|
6
|
-
export type ExtractSchema = z4.$ZodType;
|
|
7
|
-
|
|
8
|
-
export type SchemaOutput<T extends ExtractSchema> = z4.output<T>;
|
|
9
|
-
|
|
10
|
-
type ExtractIssue = z4.$ZodIssue;
|
|
11
|
-
|
|
12
|
-
const EXTRACT_ENDPOINT = "https://api.stably.ai/internal/v1/extract";
|
|
13
|
-
|
|
14
|
-
const zSuccess = z.object({ value: z.unknown() });
|
|
15
|
-
const zError = z.object({ error: z.string() });
|
|
16
|
-
|
|
17
|
-
class ExtractValidationError extends Error {
|
|
18
|
-
constructor(
|
|
19
|
-
message: string,
|
|
20
|
-
readonly issues: ReadonlyArray<ExtractIssue>,
|
|
21
|
-
) {
|
|
22
|
-
super(message);
|
|
23
|
-
this.name = "ExtractValidationError";
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function validateWithSchema<T extends ExtractSchema>(
|
|
28
|
-
schema: T,
|
|
29
|
-
value: unknown,
|
|
30
|
-
): Promise<SchemaOutput<T>> {
|
|
31
|
-
const result = await z4.safeParseAsync(schema, value);
|
|
32
|
-
if (!result.success) {
|
|
33
|
-
throw new ExtractValidationError("Validation failed", result.error.issues);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return result.data;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type ExtractSubject = Page | Locator;
|
|
40
|
-
|
|
41
|
-
type BaseExtractArgs = {
|
|
42
|
-
prompt: string;
|
|
43
|
-
pageOrLocator: ExtractSubject;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
type ExtractArgsWithSchema<T extends ExtractSchema> = BaseExtractArgs & {
|
|
47
|
-
schema: T;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export async function extract(args: BaseExtractArgs): Promise<string>;
|
|
51
|
-
export async function extract<T extends ExtractSchema>(
|
|
52
|
-
args: ExtractArgsWithSchema<T>,
|
|
53
|
-
): Promise<SchemaOutput<T>>;
|
|
54
|
-
export async function extract<T extends ExtractSchema>({
|
|
55
|
-
prompt,
|
|
56
|
-
pageOrLocator,
|
|
57
|
-
schema,
|
|
58
|
-
}: BaseExtractArgs & { schema?: T }): Promise<string | SchemaOutput<T>> {
|
|
59
|
-
const jsonSchema = schema ? z4.toJSONSchema(schema) : undefined;
|
|
60
|
-
|
|
61
|
-
const apiKey = requireApiKey();
|
|
62
|
-
|
|
63
|
-
const form = new FormData();
|
|
64
|
-
form.append("prompt", prompt);
|
|
65
|
-
if (jsonSchema) {
|
|
66
|
-
form.append("jsonSchema", JSON.stringify(jsonSchema));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const pngBuffer = await pageOrLocator.screenshot({ type: "png" }); // Buffer
|
|
70
|
-
const u8 = Uint8Array.from(pngBuffer); // strips Buffer type → plain Uint8Array
|
|
71
|
-
const blob = new Blob([u8], { type: "image/png" });
|
|
72
|
-
form.append("image", blob, "screenshot.png");
|
|
73
|
-
|
|
74
|
-
const response = await fetch(EXTRACT_ENDPOINT, {
|
|
75
|
-
method: "POST",
|
|
76
|
-
headers: {
|
|
77
|
-
Authorization: `Bearer ${apiKey}`,
|
|
78
|
-
},
|
|
79
|
-
body: form,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const parsed = await response.json().catch(() => undefined as unknown);
|
|
83
|
-
|
|
84
|
-
if (response.ok) {
|
|
85
|
-
const { value } = zSuccess.parse(parsed);
|
|
86
|
-
return schema
|
|
87
|
-
? await validateWithSchema(schema, value)
|
|
88
|
-
: typeof value === "string"
|
|
89
|
-
? value
|
|
90
|
-
: JSON.stringify(value);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const err = zError.safeParse(parsed);
|
|
94
|
-
throw new Error(
|
|
95
|
-
`Extract failed (${response.status})${err.success ? `: ${err.data.error}` : ""}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
package/src/ai/verify-prompt.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
const PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
|
|
2
|
-
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { requireApiKey } from "../runtime";
|
|
5
|
-
|
|
6
|
-
const zSuccess = z.object({
|
|
7
|
-
success: z.boolean(),
|
|
8
|
-
reason: z.string().optional(),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const zError = z.object({
|
|
12
|
-
error: z.string(),
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export async function verifyPrompt({
|
|
16
|
-
prompt,
|
|
17
|
-
screenshot,
|
|
18
|
-
}: {
|
|
19
|
-
prompt: string;
|
|
20
|
-
screenshot: Uint8Array;
|
|
21
|
-
}): Promise<{
|
|
22
|
-
pass: boolean;
|
|
23
|
-
reason?: string;
|
|
24
|
-
}> {
|
|
25
|
-
const apiKey = requireApiKey();
|
|
26
|
-
|
|
27
|
-
const form = new FormData();
|
|
28
|
-
form.append("prompt", prompt);
|
|
29
|
-
const u8 = Uint8Array.from(screenshot);
|
|
30
|
-
const blob = new Blob([u8], { type: "image/png" });
|
|
31
|
-
form.append("image", blob, "screenshot.png");
|
|
32
|
-
|
|
33
|
-
const response = await fetch(PROMPT_ASSERTION_ENDPOINT, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: {
|
|
36
|
-
Authorization: `Bearer ${apiKey}`,
|
|
37
|
-
},
|
|
38
|
-
body: form,
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const parsed = await response.json().catch(() => undefined as unknown);
|
|
42
|
-
|
|
43
|
-
if (response.ok) {
|
|
44
|
-
const { success, reason } = zSuccess.parse(parsed);
|
|
45
|
-
return {
|
|
46
|
-
pass: success,
|
|
47
|
-
reason,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const err = zError.safeParse(parsed);
|
|
52
|
-
throw new Error(
|
|
53
|
-
`Verify prompt failed (${response.status})${
|
|
54
|
-
err.success ? `: ${err.data.error}` : ""
|
|
55
|
-
}`,
|
|
56
|
-
);
|
|
57
|
-
}
|
package/src/expect.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { Locator, Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import type { ScreenshotPromptOptions } from "./index";
|
|
3
|
-
|
|
4
|
-
import { isLocator, isPage } from "./playwright-type-predicates";
|
|
5
|
-
|
|
6
|
-
import { verifyPrompt } from "./ai/verify-prompt";
|
|
7
|
-
import { takeStableScreenshot } from "./screenshot";
|
|
8
|
-
|
|
9
|
-
type VerifyTargetType = "page" | "locator";
|
|
10
|
-
|
|
11
|
-
type MatcherContext = {
|
|
12
|
-
isNot: boolean;
|
|
13
|
-
message?: () => string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
function createFailureMessage({
|
|
17
|
-
targetType,
|
|
18
|
-
condition,
|
|
19
|
-
didPass,
|
|
20
|
-
isNot,
|
|
21
|
-
reason,
|
|
22
|
-
}: {
|
|
23
|
-
targetType: VerifyTargetType;
|
|
24
|
-
condition: string;
|
|
25
|
-
didPass: boolean;
|
|
26
|
-
isNot: boolean;
|
|
27
|
-
reason?: string;
|
|
28
|
-
}): string {
|
|
29
|
-
const expectation = isNot ? "not to satisfy" : "to satisfy";
|
|
30
|
-
const result = didPass ? "it did" : "it did not";
|
|
31
|
-
|
|
32
|
-
let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
|
|
33
|
-
if (reason) {
|
|
34
|
-
message += `\n\nReason: ${reason}`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return message;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const stablyPlaywrightMatchers = {
|
|
41
|
-
async toMatchScreenshotPrompt(
|
|
42
|
-
this: MatcherContext,
|
|
43
|
-
received: Page | Locator,
|
|
44
|
-
condition: string,
|
|
45
|
-
options?: ScreenshotPromptOptions,
|
|
46
|
-
) {
|
|
47
|
-
const target = isPage(received)
|
|
48
|
-
? received
|
|
49
|
-
: isLocator(received)
|
|
50
|
-
? received
|
|
51
|
-
: undefined;
|
|
52
|
-
if (!target) {
|
|
53
|
-
// Should never happen
|
|
54
|
-
throw new Error(
|
|
55
|
-
"toMatchScreenshotPrompt only supports Playwright Page and Locator instances.",
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const targetType: VerifyTargetType = isPage(target) ? "page" : "locator";
|
|
59
|
-
|
|
60
|
-
// Wait for two consecutive identical screenshots before sending to AI
|
|
61
|
-
const screenshot = await takeStableScreenshot(target, options);
|
|
62
|
-
|
|
63
|
-
const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
pass: verifyResult.pass,
|
|
67
|
-
message: () =>
|
|
68
|
-
createFailureMessage({
|
|
69
|
-
targetType,
|
|
70
|
-
condition,
|
|
71
|
-
didPass: verifyResult.pass,
|
|
72
|
-
reason: verifyResult.reason,
|
|
73
|
-
isNot: this.isNot,
|
|
74
|
-
}),
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
} as const;
|
package/src/image-compare.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import pixelmatch from "pixelmatch";
|
|
2
|
-
import { PNG } from "pngjs";
|
|
3
|
-
import * as jpeg from "jpeg-js";
|
|
4
|
-
|
|
5
|
-
const isPng = (buffer: Buffer): boolean => {
|
|
6
|
-
return (
|
|
7
|
-
buffer.length >= 8 &&
|
|
8
|
-
buffer[0] === 0x89 &&
|
|
9
|
-
buffer[1] === 0x50 &&
|
|
10
|
-
buffer[2] === 0x4e &&
|
|
11
|
-
buffer[3] === 0x47 &&
|
|
12
|
-
buffer[4] === 0x0d &&
|
|
13
|
-
buffer[5] === 0x0a &&
|
|
14
|
-
buffer[6] === 0x1a &&
|
|
15
|
-
buffer[7] === 0x0a
|
|
16
|
-
);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const isJpeg = (buffer: Buffer): boolean => {
|
|
20
|
-
return buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xd8;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const decodeImage = (
|
|
24
|
-
buffer: Buffer,
|
|
25
|
-
): { data: Uint8Array; width: number; height: number } => {
|
|
26
|
-
if (isPng(buffer)) {
|
|
27
|
-
const png = PNG.sync.read(buffer);
|
|
28
|
-
return { data: png.data, width: png.width, height: png.height };
|
|
29
|
-
}
|
|
30
|
-
if (isJpeg(buffer)) {
|
|
31
|
-
const img = jpeg.decode(buffer, { maxMemoryUsageInMB: 1024 });
|
|
32
|
-
return { data: img.data, width: img.width, height: img.height };
|
|
33
|
-
}
|
|
34
|
-
// Default to PNG decode; if it fails upstream, treat as different sizes
|
|
35
|
-
const png = PNG.sync.read(buffer);
|
|
36
|
-
return { data: png.data, width: png.width, height: png.height };
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const imagesAreSimilar = ({
|
|
40
|
-
image1,
|
|
41
|
-
image2,
|
|
42
|
-
threshold,
|
|
43
|
-
}: {
|
|
44
|
-
image1: Buffer;
|
|
45
|
-
image2: Buffer;
|
|
46
|
-
threshold: number;
|
|
47
|
-
}): boolean => {
|
|
48
|
-
const decodedImage1 = decodeImage(image1);
|
|
49
|
-
const decodedImage2 = decodeImage(image2);
|
|
50
|
-
if (
|
|
51
|
-
decodedImage1.width !== decodedImage2.width ||
|
|
52
|
-
decodedImage1.height !== decodedImage2.height
|
|
53
|
-
) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
const diffRgbaData = new Uint8Array(
|
|
57
|
-
decodedImage1.width * decodedImage1.height * 4,
|
|
58
|
-
);
|
|
59
|
-
const numDiffPixels = pixelmatch(
|
|
60
|
-
decodedImage1.data,
|
|
61
|
-
decodedImage2.data,
|
|
62
|
-
diffRgbaData,
|
|
63
|
-
decodedImage1.width,
|
|
64
|
-
decodedImage1.height,
|
|
65
|
-
{ threshold },
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return numDiffPixels === 0;
|
|
69
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import type { LocatorDescribeOptions } from "./playwright-augment/augment";
|
|
3
|
-
import type { ExtractSchema, SchemaOutput } from "./ai/extract";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
augmentBrowser,
|
|
7
|
-
augmentBrowserContext,
|
|
8
|
-
augmentBrowserType,
|
|
9
|
-
augmentLocator,
|
|
10
|
-
augmentPage,
|
|
11
|
-
} from "./playwright-augment/augment";
|
|
12
|
-
import { stablyPlaywrightMatchers } from "./expect";
|
|
13
|
-
import { requireApiKey } from "./runtime";
|
|
14
|
-
|
|
15
|
-
export { setApiKey } from "./runtime";
|
|
16
|
-
|
|
17
|
-
export type { LocatorDescribeOptions } from "./playwright-augment/augment";
|
|
18
|
-
export type { ExtractSchema, SchemaOutput } from "./ai/extract";
|
|
19
|
-
export type ScreenshotPromptOptions =
|
|
20
|
-
import("@stablyai/internal-playwright-test").PageAssertionsToHaveScreenshotOptions;
|
|
21
|
-
export {
|
|
22
|
-
augmentBrowser,
|
|
23
|
-
augmentBrowserContext,
|
|
24
|
-
augmentBrowserType,
|
|
25
|
-
augmentLocator,
|
|
26
|
-
augmentPage,
|
|
27
|
-
stablyPlaywrightMatchers,
|
|
28
|
-
requireApiKey,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export interface Expect<T = Page> {
|
|
32
|
-
toMatchScreenshotPrompt(
|
|
33
|
-
condition: string,
|
|
34
|
-
options?: ScreenshotPromptOptions,
|
|
35
|
-
): Promise<void>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
declare module "@stablyai/internal-playwright-test" {
|
|
39
|
-
interface Locator {
|
|
40
|
-
/**
|
|
41
|
-
* Extracts information from this locator using Stably AI.
|
|
42
|
-
*
|
|
43
|
-
* Takes a screenshot of the locator and uses AI to extract information based on the
|
|
44
|
-
* provided prompt. When a schema is provided, the extracted data is validated and
|
|
45
|
-
* typed according to the schema.
|
|
46
|
-
*
|
|
47
|
-
* @param prompt - A natural language description of what information to extract
|
|
48
|
-
* @returns A string containing the extracted information
|
|
49
|
-
*/
|
|
50
|
-
extract(prompt: string): Promise<string>;
|
|
51
|
-
/**
|
|
52
|
-
* Extracts information from this locator using Stably AI.
|
|
53
|
-
*
|
|
54
|
-
* Takes a screenshot of the locator and uses AI to extract information based on the
|
|
55
|
-
* provided prompt. The extracted data is validated and typed according to the schema.
|
|
56
|
-
*
|
|
57
|
-
* @param prompt - A natural language description of what information to extract
|
|
58
|
-
* @param options - Configuration object containing the Zod schema for validation
|
|
59
|
-
* @param options.schema - Zod schema to validate and type the extracted data
|
|
60
|
-
* @returns Typed data matching the provided schema
|
|
61
|
-
*/
|
|
62
|
-
extract<T extends ExtractSchema>(
|
|
63
|
-
prompt: string,
|
|
64
|
-
options: { schema: T },
|
|
65
|
-
): Promise<SchemaOutput<T>>;
|
|
66
|
-
describe(description: string, options?: LocatorDescribeOptions): Locator;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface Page {
|
|
70
|
-
/**
|
|
71
|
-
* Extracts information from this page using Stably AI.
|
|
72
|
-
*
|
|
73
|
-
* Takes a screenshot of the page and uses AI to extract information based on the
|
|
74
|
-
* provided prompt. When a schema is provided, the extracted data is validated and
|
|
75
|
-
* typed according to the schema.
|
|
76
|
-
*
|
|
77
|
-
* @param prompt - A natural language description of what information to extract
|
|
78
|
-
* @returns A string containing the extracted information
|
|
79
|
-
*/
|
|
80
|
-
extract(prompt: string): Promise<string>;
|
|
81
|
-
/**
|
|
82
|
-
* Extracts information from this page using Stably AI.
|
|
83
|
-
*
|
|
84
|
-
* Takes a screenshot of the page and uses AI to extract information based on the
|
|
85
|
-
* provided prompt. The extracted data is validated and typed according to the schema.
|
|
86
|
-
*
|
|
87
|
-
* @param prompt - A natural language description of what information to extract
|
|
88
|
-
* @param options - Configuration object containing the Zod schema for validation
|
|
89
|
-
* @param options.schema - Zod schema to validate and type the extracted data
|
|
90
|
-
* @returns Typed data matching the provided schema
|
|
91
|
-
*/
|
|
92
|
-
extract<T extends ExtractSchema>(
|
|
93
|
-
prompt: string,
|
|
94
|
-
options: { schema: T },
|
|
95
|
-
): Promise<SchemaOutput<T>>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface BrowserContext {
|
|
99
|
-
agent(
|
|
100
|
-
prompt: string,
|
|
101
|
-
options: { page: Page; maxCycles?: number },
|
|
102
|
-
): Promise<{ success: boolean }>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface Browser {
|
|
106
|
-
agent(
|
|
107
|
-
prompt: string,
|
|
108
|
-
options: { page: Page; maxCycles?: number },
|
|
109
|
-
): Promise<{ success: boolean }>;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Browser,
|
|
3
|
-
BrowserContext,
|
|
4
|
-
BrowserType,
|
|
5
|
-
Locator,
|
|
6
|
-
Page,
|
|
7
|
-
} from "@stablyai/internal-playwright-test";
|
|
8
|
-
|
|
9
|
-
import { createLocatorExtract, createPageExtract } from "./methods/extract";
|
|
10
|
-
import { createAgentStub } from "./methods/agent";
|
|
11
|
-
|
|
12
|
-
export interface LocatorDescribeOptions {
|
|
13
|
-
autoHeal?: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
|
|
17
|
-
const LOCATOR_DESCRIBE_WRAPPED = Symbol.for(
|
|
18
|
-
"stably.playwright.locatorDescribeWrapped",
|
|
19
|
-
);
|
|
20
|
-
const PAGE_PATCHED = Symbol.for("stably.playwright.pagePatched");
|
|
21
|
-
const CONTEXT_PATCHED = Symbol.for("stably.playwright.contextPatched");
|
|
22
|
-
const BROWSER_PATCHED = Symbol.for("stably.playwright.browserPatched");
|
|
23
|
-
const BROWSER_TYPE_PATCHED = Symbol.for("stably.playwright.browserTypePatched");
|
|
24
|
-
|
|
25
|
-
function defineHiddenProperty<T, K extends PropertyKey>(
|
|
26
|
-
target: T,
|
|
27
|
-
key: K,
|
|
28
|
-
value: unknown,
|
|
29
|
-
): void {
|
|
30
|
-
Object.defineProperty(target as unknown as object, key, {
|
|
31
|
-
value,
|
|
32
|
-
enumerable: false,
|
|
33
|
-
configurable: true,
|
|
34
|
-
writable: true,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function augmentLocator<T extends Locator>(locator: T): T {
|
|
39
|
-
if (
|
|
40
|
-
(locator as unknown as { [LOCATOR_PATCHED]?: boolean })[LOCATOR_PATCHED]
|
|
41
|
-
) {
|
|
42
|
-
return locator;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
defineHiddenProperty(locator, "extract", createLocatorExtract(locator));
|
|
46
|
-
|
|
47
|
-
const markerTarget = locator as unknown as Record<PropertyKey, unknown>;
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
typeof locator.describe === "function" &&
|
|
51
|
-
!markerTarget[LOCATOR_DESCRIBE_WRAPPED]
|
|
52
|
-
) {
|
|
53
|
-
const originalDescribe = locator.describe.bind(locator);
|
|
54
|
-
locator.describe = ((
|
|
55
|
-
description: string,
|
|
56
|
-
options?: LocatorDescribeOptions,
|
|
57
|
-
) => {
|
|
58
|
-
void options;
|
|
59
|
-
const result = originalDescribe(description);
|
|
60
|
-
return result ? augmentLocator(result as Locator) : result;
|
|
61
|
-
}) as Locator["describe"];
|
|
62
|
-
|
|
63
|
-
defineHiddenProperty(locator, LOCATOR_DESCRIBE_WRAPPED, true);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
defineHiddenProperty(locator, LOCATOR_PATCHED, true);
|
|
67
|
-
|
|
68
|
-
return locator;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function augmentPage<T extends Page>(page: T): T {
|
|
72
|
-
if ((page as unknown as { [PAGE_PATCHED]?: boolean })[PAGE_PATCHED]) {
|
|
73
|
-
return page;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const originalLocator = page.locator.bind(page);
|
|
77
|
-
page.locator = ((...args: Parameters<Page["locator"]>) => {
|
|
78
|
-
const locator = originalLocator(...args);
|
|
79
|
-
return augmentLocator(locator);
|
|
80
|
-
}) as Page["locator"];
|
|
81
|
-
|
|
82
|
-
defineHiddenProperty(page, "extract", createPageExtract(page));
|
|
83
|
-
defineHiddenProperty(page, PAGE_PATCHED, true);
|
|
84
|
-
|
|
85
|
-
return page;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function augmentBrowserContext<T extends BrowserContext>(context: T): T {
|
|
89
|
-
if (
|
|
90
|
-
(context as unknown as { [CONTEXT_PATCHED]?: boolean })[CONTEXT_PATCHED]
|
|
91
|
-
) {
|
|
92
|
-
return context;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const originalNewPage = context.newPage.bind(context);
|
|
96
|
-
context.newPage = (async (...args: Parameters<BrowserContext["newPage"]>) => {
|
|
97
|
-
const page = await originalNewPage(...args);
|
|
98
|
-
return augmentPage(page);
|
|
99
|
-
}) as BrowserContext["newPage"];
|
|
100
|
-
|
|
101
|
-
const originalPages = context.pages?.bind(context);
|
|
102
|
-
if (originalPages) {
|
|
103
|
-
context.pages = (() =>
|
|
104
|
-
originalPages().map((page) =>
|
|
105
|
-
augmentPage(page),
|
|
106
|
-
)) as BrowserContext["pages"];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (!(context as unknown as { agent?: unknown }).agent) {
|
|
110
|
-
defineHiddenProperty(context, "agent", createAgentStub());
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
defineHiddenProperty(context, CONTEXT_PATCHED, true);
|
|
114
|
-
|
|
115
|
-
return context;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function augmentBrowser<T extends Browser>(browser: T): T {
|
|
119
|
-
if (
|
|
120
|
-
(browser as unknown as { [BROWSER_PATCHED]?: boolean })[BROWSER_PATCHED]
|
|
121
|
-
) {
|
|
122
|
-
return browser;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const originalNewContext = browser.newContext.bind(browser);
|
|
126
|
-
browser.newContext = (async (...args: Parameters<Browser["newContext"]>) => {
|
|
127
|
-
const context = await originalNewContext(...args);
|
|
128
|
-
return augmentBrowserContext(context);
|
|
129
|
-
}) as Browser["newContext"];
|
|
130
|
-
|
|
131
|
-
const originalNewPage = browser.newPage.bind(browser);
|
|
132
|
-
browser.newPage = (async (...args: Parameters<Browser["newPage"]>) => {
|
|
133
|
-
const page = await originalNewPage(...args);
|
|
134
|
-
return augmentPage(page);
|
|
135
|
-
}) as Browser["newPage"];
|
|
136
|
-
|
|
137
|
-
const originalContexts = browser.contexts.bind(browser);
|
|
138
|
-
browser.contexts = (() =>
|
|
139
|
-
originalContexts().map((context) =>
|
|
140
|
-
augmentBrowserContext(context),
|
|
141
|
-
)) as Browser["contexts"];
|
|
142
|
-
|
|
143
|
-
if (!(browser as unknown as { agent?: unknown }).agent) {
|
|
144
|
-
defineHiddenProperty(browser, "agent", createAgentStub());
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
defineHiddenProperty(browser, BROWSER_PATCHED, true);
|
|
148
|
-
|
|
149
|
-
return browser;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function augmentBrowserType<TBrowser extends Browser>(
|
|
153
|
-
browserType: BrowserType<TBrowser>,
|
|
154
|
-
): BrowserType<TBrowser> {
|
|
155
|
-
if (
|
|
156
|
-
(browserType as unknown as { [BROWSER_TYPE_PATCHED]?: boolean })[
|
|
157
|
-
BROWSER_TYPE_PATCHED
|
|
158
|
-
]
|
|
159
|
-
) {
|
|
160
|
-
return browserType;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const originalLaunch = browserType.launch.bind(browserType);
|
|
164
|
-
browserType.launch = (async (
|
|
165
|
-
...args: Parameters<BrowserType<TBrowser>["launch"]>
|
|
166
|
-
) => {
|
|
167
|
-
const browser = await originalLaunch(...args);
|
|
168
|
-
return augmentBrowser(browser);
|
|
169
|
-
}) as BrowserType<TBrowser>["launch"];
|
|
170
|
-
|
|
171
|
-
const originalConnect = browserType.connect?.bind(browserType);
|
|
172
|
-
if (originalConnect) {
|
|
173
|
-
browserType.connect = (async (
|
|
174
|
-
...args: Parameters<NonNullable<BrowserType<TBrowser>["connect"]>>
|
|
175
|
-
) => {
|
|
176
|
-
const browser = await originalConnect(...args);
|
|
177
|
-
return augmentBrowser(browser);
|
|
178
|
-
}) as NonNullable<BrowserType<TBrowser>["connect"]>;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const originalConnectOverCDP = browserType.connectOverCDP?.bind(browserType);
|
|
182
|
-
if (originalConnectOverCDP) {
|
|
183
|
-
browserType.connectOverCDP = (async (
|
|
184
|
-
...args: Parameters<NonNullable<BrowserType<TBrowser>["connectOverCDP"]>>
|
|
185
|
-
) => {
|
|
186
|
-
const browser = await originalConnectOverCDP(...args);
|
|
187
|
-
return augmentBrowser(browser);
|
|
188
|
-
}) as NonNullable<BrowserType<TBrowser>["connectOverCDP"]>;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const originalLaunchPersistentContext =
|
|
192
|
-
browserType.launchPersistentContext?.bind(browserType);
|
|
193
|
-
if (originalLaunchPersistentContext) {
|
|
194
|
-
browserType.launchPersistentContext = (async (
|
|
195
|
-
...args: Parameters<
|
|
196
|
-
NonNullable<BrowserType<TBrowser>["launchPersistentContext"]>
|
|
197
|
-
>
|
|
198
|
-
) => {
|
|
199
|
-
const context = await originalLaunchPersistentContext(...args);
|
|
200
|
-
return augmentBrowserContext(context);
|
|
201
|
-
}) as NonNullable<BrowserType<TBrowser>["launchPersistentContext"]>;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
defineHiddenProperty(browserType, BROWSER_TYPE_PATCHED, true);
|
|
205
|
-
|
|
206
|
-
return browserType;
|
|
207
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import { requireApiKey } from "../../runtime";
|
|
3
|
-
|
|
4
|
-
type AgentOptions = {
|
|
5
|
-
page: Page;
|
|
6
|
-
maxCycles?: number;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function createAgentStub(): (
|
|
10
|
-
prompt: string,
|
|
11
|
-
options: AgentOptions,
|
|
12
|
-
) => Promise<{ success: boolean }> {
|
|
13
|
-
return async (prompt: string, options: AgentOptions) => {
|
|
14
|
-
requireApiKey();
|
|
15
|
-
void prompt;
|
|
16
|
-
void options;
|
|
17
|
-
return { success: true };
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { Locator, Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import {
|
|
3
|
-
type ExtractSchema,
|
|
4
|
-
type SchemaOutput,
|
|
5
|
-
extract,
|
|
6
|
-
} from "../../ai/extract";
|
|
7
|
-
|
|
8
|
-
type ExtractOptions<T extends ExtractSchema> = {
|
|
9
|
-
schema: T;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type ExtractMethod = {
|
|
13
|
-
(prompt: string): Promise<string>;
|
|
14
|
-
<T extends ExtractSchema>(
|
|
15
|
-
prompt: string,
|
|
16
|
-
options: ExtractOptions<T>,
|
|
17
|
-
): Promise<SchemaOutput<T>>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type LocatorExtract = ExtractMethod;
|
|
21
|
-
type PageExtract = ExtractMethod;
|
|
22
|
-
|
|
23
|
-
type ExtractSubject = Locator | Page;
|
|
24
|
-
|
|
25
|
-
function createExtract(pageOrLocator: ExtractSubject): ExtractMethod {
|
|
26
|
-
const impl = (async (
|
|
27
|
-
prompt: string,
|
|
28
|
-
options?: ExtractOptions<ExtractSchema>,
|
|
29
|
-
) => {
|
|
30
|
-
if (options?.schema) {
|
|
31
|
-
return extract({
|
|
32
|
-
prompt,
|
|
33
|
-
schema: options.schema,
|
|
34
|
-
pageOrLocator,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return extract({ prompt, pageOrLocator });
|
|
39
|
-
}) as ExtractMethod;
|
|
40
|
-
|
|
41
|
-
return impl;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const createLocatorExtract = (locator: Locator): LocatorExtract =>
|
|
45
|
-
createExtract(locator);
|
|
46
|
-
|
|
47
|
-
export const createPageExtract = (page: Page): PageExtract =>
|
|
48
|
-
createExtract(page);
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Page, Locator } from "@stablyai/internal-playwright-test";
|
|
2
|
-
|
|
3
|
-
export function isPage(candidate: unknown): candidate is Page {
|
|
4
|
-
return (
|
|
5
|
-
typeof candidate === "object" &&
|
|
6
|
-
candidate !== null &&
|
|
7
|
-
typeof (candidate as Page).screenshot === "function" &&
|
|
8
|
-
typeof (candidate as Page).goto === "function"
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function isLocator(candidate: unknown): candidate is Locator {
|
|
13
|
-
return (
|
|
14
|
-
typeof candidate === "object" &&
|
|
15
|
-
candidate !== null &&
|
|
16
|
-
typeof (candidate as Locator).screenshot === "function" &&
|
|
17
|
-
typeof (candidate as Locator).nth === "function"
|
|
18
|
-
);
|
|
19
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
let configuredApiKey: string | undefined = process.env.STABLY_API_KEY;
|
|
2
|
-
|
|
3
|
-
export function setApiKey(apiKey: string): void {
|
|
4
|
-
configuredApiKey = apiKey;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function getApiKey(): string | undefined {
|
|
8
|
-
return configuredApiKey;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function requireApiKey(): string {
|
|
12
|
-
const apiKey = getApiKey();
|
|
13
|
-
if (!apiKey) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
"Missing Stably API key. Call setApiKey(apiKey) or set the STABLY_API_KEY environment variable.",
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
return apiKey;
|
|
19
|
-
}
|
package/src/screenshot.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { Locator, Page } from "@stablyai/internal-playwright-test";
|
|
2
|
-
import type { ScreenshotPromptOptions } from "./index";
|
|
3
|
-
import { isPage } from "./playwright-type-predicates";
|
|
4
|
-
import { imagesAreSimilar } from "./image-compare";
|
|
5
|
-
|
|
6
|
-
export async function takeStableScreenshot(
|
|
7
|
-
target: Page | Locator,
|
|
8
|
-
options?: ScreenshotPromptOptions,
|
|
9
|
-
): Promise<Buffer> {
|
|
10
|
-
const page = isPage(target) ? target : target.page();
|
|
11
|
-
|
|
12
|
-
// Use a small budget for stabilization within the overall assertion timeout.
|
|
13
|
-
// We allocate up to 25% of the total timeout (bounded between 300ms and 2000ms).
|
|
14
|
-
const totalTimeout =
|
|
15
|
-
(options as { timeout?: number } | undefined)?.timeout ?? 5000;
|
|
16
|
-
// Budget is 25% of the total timeout
|
|
17
|
-
const stabilizationBudgetMs = Math.floor(totalTimeout * 0.25);
|
|
18
|
-
const stabilityBudgetMs = Math.min(
|
|
19
|
-
2000,
|
|
20
|
-
Math.max(300, stabilizationBudgetMs),
|
|
21
|
-
);
|
|
22
|
-
const deadline = Date.now() + stabilityBudgetMs;
|
|
23
|
-
|
|
24
|
-
let actual: Buffer | undefined;
|
|
25
|
-
let previous: Buffer | undefined;
|
|
26
|
-
const pollIntervals = [0, 100, 250, 500];
|
|
27
|
-
let isFirstIteration = true;
|
|
28
|
-
|
|
29
|
-
while (true) {
|
|
30
|
-
if (Date.now() >= deadline) break;
|
|
31
|
-
const delay = pollIntervals.length ? pollIntervals.shift()! : 1000;
|
|
32
|
-
if (delay) {
|
|
33
|
-
await page.waitForTimeout(delay);
|
|
34
|
-
}
|
|
35
|
-
previous = actual;
|
|
36
|
-
actual = await target.screenshot(options);
|
|
37
|
-
if (
|
|
38
|
-
!isFirstIteration &&
|
|
39
|
-
actual &&
|
|
40
|
-
previous &&
|
|
41
|
-
imagesAreSimilar({
|
|
42
|
-
image1: previous,
|
|
43
|
-
image2: actual,
|
|
44
|
-
threshold: options?.threshold ?? 0.02,
|
|
45
|
-
})
|
|
46
|
-
) {
|
|
47
|
-
return actual;
|
|
48
|
-
}
|
|
49
|
-
isFirstIteration = false;
|
|
50
|
-
}
|
|
51
|
-
return actual ?? (await target.screenshot(options));
|
|
52
|
-
}
|
package/tsconfig.build.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "@tsconfig/node18/tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"declaration": true,
|
|
5
|
-
"outDir": "./dist",
|
|
6
|
-
"rootDir": "src",
|
|
7
|
-
"forceConsistentCasingInFileNames": true,
|
|
8
|
-
"resolveJsonModule": true,
|
|
9
|
-
"noEmit": true,
|
|
10
|
-
"moduleResolution": "bundler",
|
|
11
|
-
"module": "ESNext",
|
|
12
|
-
"baseUrl": ".",
|
|
13
|
-
"paths": {
|
|
14
|
-
"~/*": ["src/*"]
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"include": ["src/**/*"],
|
|
18
|
-
"exclude": ["node_modules", "dist"]
|
|
19
|
-
}
|