@stablyai/playwright-base 2.0.16 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +126 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +136 -4
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +136 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +126 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -47,7 +47,7 @@ const isObject = (value) => {
|
|
|
47
47
|
|
|
48
48
|
const SDK_METADATA_HEADERS = {
|
|
49
49
|
"X-Client-Name": "stably-playwright-sdk-js",
|
|
50
|
-
"X-Client-Version": "2.0
|
|
50
|
+
"X-Client-Version": "2.1.0"
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const PROMPT_ASSERTION_PATH = "internal/v2/assert";
|
|
@@ -80,6 +80,7 @@ const parseErrorResponse = (value) => {
|
|
|
80
80
|
return typeof error !== "string" ? void 0 : { error };
|
|
81
81
|
};
|
|
82
82
|
async function verifyPrompt({
|
|
83
|
+
model,
|
|
83
84
|
pageMetadata,
|
|
84
85
|
prompt,
|
|
85
86
|
screenshot
|
|
@@ -91,6 +92,9 @@ async function verifyPrompt({
|
|
|
91
92
|
...pageMetadata?.title ? { pageTitle: pageMetadata.title } : {},
|
|
92
93
|
...pageMetadata?.url ? { pageUrl: pageMetadata.url } : {}
|
|
93
94
|
};
|
|
95
|
+
if (model) {
|
|
96
|
+
metadata.model = model;
|
|
97
|
+
}
|
|
94
98
|
form.append("metadata", JSON.stringify(metadata));
|
|
95
99
|
const u8 = Uint8Array.from(screenshot);
|
|
96
100
|
const blob = new Blob([u8], { type: "image/png" });
|
|
@@ -386,22 +390,41 @@ Reason: ${reason}`;
|
|
|
386
390
|
return message;
|
|
387
391
|
}
|
|
388
392
|
const stablyPlaywrightMatchers = {
|
|
389
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Asserts that the page or locator satisfies a natural language condition using AI vision.
|
|
395
|
+
*
|
|
396
|
+
* Takes a screenshot of the target and uses AI to verify whether the specified condition is met.
|
|
397
|
+
* The AI analyzes the visual content and provides reasoning for its determination.
|
|
398
|
+
*
|
|
399
|
+
* @param condition - A natural language description of what should be true about the page/locator
|
|
400
|
+
* @param options - Optional screenshot options (e.g., fullPage, timeout)
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* ```typescript
|
|
404
|
+
* // Assert page content
|
|
405
|
+
* await expect(page).aiAssert('The login form is visible with email and password fields');
|
|
406
|
+
*
|
|
407
|
+
* // Assert locator content
|
|
408
|
+
* await expect(page.locator('.header')).aiAssert('The navigation menu contains a logout button');
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
async aiAssert(received, condition, options) {
|
|
390
412
|
const target = isPage(received) ? received : isLocator(received) ? received : void 0;
|
|
391
413
|
if (!target) {
|
|
392
414
|
throw new Error(
|
|
393
|
-
"
|
|
415
|
+
"aiAssert only supports Playwright Page and Locator instances."
|
|
394
416
|
);
|
|
395
417
|
}
|
|
396
418
|
const targetType = isPage(target) ? "page" : "locator";
|
|
397
419
|
const screenshot = await takeStableScreenshot(target, options);
|
|
398
420
|
const verifyResult = await verifyPrompt({
|
|
421
|
+
model: options?.model,
|
|
399
422
|
pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
|
|
400
423
|
prompt: condition,
|
|
401
424
|
screenshot
|
|
402
425
|
});
|
|
403
426
|
const testInfo = test.test.info();
|
|
404
|
-
const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "
|
|
427
|
+
const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
|
|
405
428
|
testInfo.attachments.push(
|
|
406
429
|
{
|
|
407
430
|
body: Buffer.from(
|
|
@@ -435,9 +458,20 @@ const stablyPlaywrightMatchers = {
|
|
|
435
458
|
reason: verifyResult.reason,
|
|
436
459
|
targetType
|
|
437
460
|
}),
|
|
438
|
-
name: "
|
|
461
|
+
name: "aiAssert",
|
|
439
462
|
pass: verifyResult.pass
|
|
440
463
|
};
|
|
464
|
+
},
|
|
465
|
+
/**
|
|
466
|
+
* @deprecated Use `aiAssert` instead. This method will be removed in a future version.
|
|
467
|
+
*/
|
|
468
|
+
async toMatchScreenshotPrompt(received, condition, options) {
|
|
469
|
+
return stablyPlaywrightMatchers.aiAssert.call(
|
|
470
|
+
this,
|
|
471
|
+
received,
|
|
472
|
+
condition,
|
|
473
|
+
options
|
|
474
|
+
);
|
|
441
475
|
}
|
|
442
476
|
};
|
|
443
477
|
|
|
@@ -858,7 +892,7 @@ const isExtractionResponse = (value) => {
|
|
|
858
892
|
}
|
|
859
893
|
return value.success === false && typeof value.error === "string";
|
|
860
894
|
};
|
|
861
|
-
const isErrorResponse = (value) => {
|
|
895
|
+
const isErrorResponse$1 = (value) => {
|
|
862
896
|
return isObject(value) && typeof value.error === "string";
|
|
863
897
|
};
|
|
864
898
|
class ExtractValidationError extends Error {
|
|
@@ -879,6 +913,7 @@ async function validateWithSchema(schema, value) {
|
|
|
879
913
|
return result.data;
|
|
880
914
|
}
|
|
881
915
|
async function extract({
|
|
916
|
+
model,
|
|
882
917
|
pageOrLocator,
|
|
883
918
|
prompt,
|
|
884
919
|
schema
|
|
@@ -899,6 +934,9 @@ async function extract({
|
|
|
899
934
|
if (jsonSchema) {
|
|
900
935
|
form.append("jsonSchema", JSON.stringify(jsonSchema));
|
|
901
936
|
}
|
|
937
|
+
if (model) {
|
|
938
|
+
form.append("model", model);
|
|
939
|
+
}
|
|
902
940
|
const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
|
|
903
941
|
const u8 = Uint8Array.from(pngBuffer);
|
|
904
942
|
const blob = new Blob([u8], { type: "image/png" });
|
|
@@ -931,7 +969,7 @@ async function extract({
|
|
|
931
969
|
});
|
|
932
970
|
return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
|
|
933
971
|
}
|
|
934
|
-
throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
|
|
972
|
+
throw new Error(isErrorResponse$1(raw) ? raw.error : "Extract failed");
|
|
935
973
|
});
|
|
936
974
|
}
|
|
937
975
|
|
|
@@ -939,18 +977,97 @@ function createExtract(pageOrLocator) {
|
|
|
939
977
|
const impl = (async (prompt, options) => {
|
|
940
978
|
if (options?.schema) {
|
|
941
979
|
return extract({
|
|
980
|
+
model: options.model,
|
|
942
981
|
pageOrLocator,
|
|
943
982
|
prompt,
|
|
944
983
|
schema: options.schema
|
|
945
984
|
});
|
|
946
985
|
}
|
|
947
|
-
return extract({ pageOrLocator, prompt });
|
|
986
|
+
return extract({ model: options?.model, pageOrLocator, prompt });
|
|
948
987
|
});
|
|
949
988
|
return impl;
|
|
950
989
|
}
|
|
951
990
|
const createLocatorExtract = (locator) => createExtract(locator);
|
|
952
991
|
const createPageExtract = (page) => createExtract(page);
|
|
953
992
|
|
|
993
|
+
const GET_LOCATOR_PATH = "internal/v3/get-aria-refs";
|
|
994
|
+
const EMPTY_LOCATOR_SELECTOR = "__stably_empty_locator__";
|
|
995
|
+
function getEndpoint() {
|
|
996
|
+
const baseUrl = process.env.STABLY_API_URL || "https://api.stably.ai";
|
|
997
|
+
return new URL(GET_LOCATOR_PATH, baseUrl).toString();
|
|
998
|
+
}
|
|
999
|
+
const isAPIResponse = (value) => {
|
|
1000
|
+
if (!isObject(value)) {
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
return Array.isArray(value.refs) && value.refs.every((ref) => typeof ref === "string") && typeof value.reason === "string";
|
|
1004
|
+
};
|
|
1005
|
+
const isErrorResponse = (value) => {
|
|
1006
|
+
return isObject(value) && typeof value.error === "string";
|
|
1007
|
+
};
|
|
1008
|
+
async function getLocatorsByAI(page, prompt, options) {
|
|
1009
|
+
return await test.test.step(`[GetLocatorByAI] ${prompt}`, async (stepInfo) => {
|
|
1010
|
+
const apiKey = requireApiKey();
|
|
1011
|
+
const pageWithSnapshot = page;
|
|
1012
|
+
if (typeof pageWithSnapshot._snapshotForAI !== "function") {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
"getLocatorsByAI requires Playwright v1.54.1 or higher. Please upgrade your Playwright version."
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
const snapshotResult = await pageWithSnapshot._snapshotForAI();
|
|
1018
|
+
const ariaSnapshot = typeof snapshotResult === "string" ? snapshotResult : snapshotResult.full;
|
|
1019
|
+
const response = await (async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
return await fetch(getEndpoint(), {
|
|
1022
|
+
body: JSON.stringify({
|
|
1023
|
+
ariaSnapshot,
|
|
1024
|
+
prompt,
|
|
1025
|
+
...options?.model && { model: options.model }
|
|
1026
|
+
}),
|
|
1027
|
+
headers: {
|
|
1028
|
+
...SDK_METADATA_HEADERS,
|
|
1029
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1030
|
+
"Content-Type": "application/json"
|
|
1031
|
+
},
|
|
1032
|
+
method: "POST"
|
|
1033
|
+
});
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`getLocatorsByAI failed to connect to Stably API: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
})();
|
|
1040
|
+
const raw = await response.json().catch(() => void 0);
|
|
1041
|
+
if (!response.ok) {
|
|
1042
|
+
const err = isErrorResponse(raw) ? raw.error : "Unknown error";
|
|
1043
|
+
throw new Error(`getLocatorsByAI failed (${response.status}): ${err}`);
|
|
1044
|
+
}
|
|
1045
|
+
if (!isAPIResponse(raw)) {
|
|
1046
|
+
throw new Error("getLocatorsByAI returned unexpected response shape");
|
|
1047
|
+
}
|
|
1048
|
+
await stepInfo.attach("[GetLocatorByAI] reason", {
|
|
1049
|
+
body: raw.reason,
|
|
1050
|
+
contentType: "text/plain"
|
|
1051
|
+
});
|
|
1052
|
+
const locator = raw.refs.length === 0 ? (
|
|
1053
|
+
// Empty locator that matches nothing
|
|
1054
|
+
page.locator(EMPTY_LOCATOR_SELECTOR)
|
|
1055
|
+
) : (
|
|
1056
|
+
// Combine refs using .or()
|
|
1057
|
+
raw.refs.map((ref) => page.locator(`aria-ref=${ref}`)).reduce((acc, loc) => acc.or(loc))
|
|
1058
|
+
);
|
|
1059
|
+
return {
|
|
1060
|
+
count: raw.refs.length,
|
|
1061
|
+
locator,
|
|
1062
|
+
reason: raw.reason
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const createPageGetLocatorByAI = (page) => {
|
|
1068
|
+
return (prompt, options) => getLocatorsByAI(page, prompt, options);
|
|
1069
|
+
};
|
|
1070
|
+
|
|
954
1071
|
const LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
|
|
955
1072
|
const PAGE_PATCHED = Symbol.for("stably.playwright.pagePatched");
|
|
956
1073
|
const CONTEXT_PATCHED = Symbol.for("stably.playwright.contextPatched");
|
|
@@ -982,6 +1099,7 @@ function augmentPage(page) {
|
|
|
982
1099
|
return augmentLocator(locator);
|
|
983
1100
|
});
|
|
984
1101
|
defineHiddenProperty(page, "extract", createPageExtract(page));
|
|
1102
|
+
defineHiddenProperty(page, "getLocatorsByAI", createPageGetLocatorByAI(page));
|
|
985
1103
|
defineHiddenProperty(page, PAGE_PATCHED, true);
|
|
986
1104
|
return page;
|
|
987
1105
|
}
|