@stablyai/playwright-base 2.0.16 → 2.1.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/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.mjs
CHANGED
|
@@ -26,7 +26,7 @@ const isObject = (value) => {
|
|
|
26
26
|
|
|
27
27
|
const SDK_METADATA_HEADERS = {
|
|
28
28
|
"X-Client-Name": "stably-playwright-sdk-js",
|
|
29
|
-
"X-Client-Version": "2.
|
|
29
|
+
"X-Client-Version": "2.1.1"
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const PROMPT_ASSERTION_PATH = "internal/v2/assert";
|
|
@@ -59,6 +59,7 @@ const parseErrorResponse = (value) => {
|
|
|
59
59
|
return typeof error !== "string" ? void 0 : { error };
|
|
60
60
|
};
|
|
61
61
|
async function verifyPrompt({
|
|
62
|
+
model,
|
|
62
63
|
pageMetadata,
|
|
63
64
|
prompt,
|
|
64
65
|
screenshot
|
|
@@ -70,6 +71,9 @@ async function verifyPrompt({
|
|
|
70
71
|
...pageMetadata?.title ? { pageTitle: pageMetadata.title } : {},
|
|
71
72
|
...pageMetadata?.url ? { pageUrl: pageMetadata.url } : {}
|
|
72
73
|
};
|
|
74
|
+
if (model) {
|
|
75
|
+
metadata.model = model;
|
|
76
|
+
}
|
|
73
77
|
form.append("metadata", JSON.stringify(metadata));
|
|
74
78
|
const u8 = Uint8Array.from(screenshot);
|
|
75
79
|
const blob = new Blob([u8], { type: "image/png" });
|
|
@@ -365,22 +369,41 @@ Reason: ${reason}`;
|
|
|
365
369
|
return message;
|
|
366
370
|
}
|
|
367
371
|
const stablyPlaywrightMatchers = {
|
|
368
|
-
|
|
372
|
+
/**
|
|
373
|
+
* Asserts that the page or locator satisfies a natural language condition using AI vision.
|
|
374
|
+
*
|
|
375
|
+
* Takes a screenshot of the target and uses AI to verify whether the specified condition is met.
|
|
376
|
+
* The AI analyzes the visual content and provides reasoning for its determination.
|
|
377
|
+
*
|
|
378
|
+
* @param condition - A natural language description of what should be true about the page/locator
|
|
379
|
+
* @param options - Optional screenshot options (e.g., fullPage, timeout)
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```typescript
|
|
383
|
+
* // Assert page content
|
|
384
|
+
* await expect(page).aiAssert('The login form is visible with email and password fields');
|
|
385
|
+
*
|
|
386
|
+
* // Assert locator content
|
|
387
|
+
* await expect(page.locator('.header')).aiAssert('The navigation menu contains a logout button');
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
async aiAssert(received, condition, options) {
|
|
369
391
|
const target = isPage(received) ? received : isLocator(received) ? received : void 0;
|
|
370
392
|
if (!target) {
|
|
371
393
|
throw new Error(
|
|
372
|
-
"
|
|
394
|
+
"aiAssert only supports Playwright Page and Locator instances."
|
|
373
395
|
);
|
|
374
396
|
}
|
|
375
397
|
const targetType = isPage(target) ? "page" : "locator";
|
|
376
398
|
const screenshot = await takeStableScreenshot(target, options);
|
|
377
399
|
const verifyResult = await verifyPrompt({
|
|
400
|
+
model: options?.model,
|
|
378
401
|
pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
|
|
379
402
|
prompt: condition,
|
|
380
403
|
screenshot
|
|
381
404
|
});
|
|
382
405
|
const testInfo = test.info();
|
|
383
|
-
const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "
|
|
406
|
+
const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
|
|
384
407
|
testInfo.attachments.push(
|
|
385
408
|
{
|
|
386
409
|
body: Buffer.from(
|
|
@@ -414,9 +437,20 @@ const stablyPlaywrightMatchers = {
|
|
|
414
437
|
reason: verifyResult.reason,
|
|
415
438
|
targetType
|
|
416
439
|
}),
|
|
417
|
-
name: "
|
|
440
|
+
name: "aiAssert",
|
|
418
441
|
pass: verifyResult.pass
|
|
419
442
|
};
|
|
443
|
+
},
|
|
444
|
+
/**
|
|
445
|
+
* @deprecated Use `aiAssert` instead. This method will be removed in a future version.
|
|
446
|
+
*/
|
|
447
|
+
async toMatchScreenshotPrompt(received, condition, options) {
|
|
448
|
+
return stablyPlaywrightMatchers.aiAssert.call(
|
|
449
|
+
this,
|
|
450
|
+
received,
|
|
451
|
+
condition,
|
|
452
|
+
options
|
|
453
|
+
);
|
|
420
454
|
}
|
|
421
455
|
};
|
|
422
456
|
|
|
@@ -837,7 +871,7 @@ const isExtractionResponse = (value) => {
|
|
|
837
871
|
}
|
|
838
872
|
return value.success === false && typeof value.error === "string";
|
|
839
873
|
};
|
|
840
|
-
const isErrorResponse = (value) => {
|
|
874
|
+
const isErrorResponse$1 = (value) => {
|
|
841
875
|
return isObject(value) && typeof value.error === "string";
|
|
842
876
|
};
|
|
843
877
|
class ExtractValidationError extends Error {
|
|
@@ -858,6 +892,7 @@ async function validateWithSchema(schema, value) {
|
|
|
858
892
|
return result.data;
|
|
859
893
|
}
|
|
860
894
|
async function extract({
|
|
895
|
+
model,
|
|
861
896
|
pageOrLocator,
|
|
862
897
|
prompt,
|
|
863
898
|
schema
|
|
@@ -878,6 +913,9 @@ async function extract({
|
|
|
878
913
|
if (jsonSchema) {
|
|
879
914
|
form.append("jsonSchema", JSON.stringify(jsonSchema));
|
|
880
915
|
}
|
|
916
|
+
if (model) {
|
|
917
|
+
form.append("model", model);
|
|
918
|
+
}
|
|
881
919
|
const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
|
|
882
920
|
const u8 = Uint8Array.from(pngBuffer);
|
|
883
921
|
const blob = new Blob([u8], { type: "image/png" });
|
|
@@ -910,7 +948,7 @@ async function extract({
|
|
|
910
948
|
});
|
|
911
949
|
return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
|
|
912
950
|
}
|
|
913
|
-
throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
|
|
951
|
+
throw new Error(isErrorResponse$1(raw) ? raw.error : "Extract failed");
|
|
914
952
|
});
|
|
915
953
|
}
|
|
916
954
|
|
|
@@ -918,18 +956,97 @@ function createExtract(pageOrLocator) {
|
|
|
918
956
|
const impl = (async (prompt, options) => {
|
|
919
957
|
if (options?.schema) {
|
|
920
958
|
return extract({
|
|
959
|
+
model: options.model,
|
|
921
960
|
pageOrLocator,
|
|
922
961
|
prompt,
|
|
923
962
|
schema: options.schema
|
|
924
963
|
});
|
|
925
964
|
}
|
|
926
|
-
return extract({ pageOrLocator, prompt });
|
|
965
|
+
return extract({ model: options?.model, pageOrLocator, prompt });
|
|
927
966
|
});
|
|
928
967
|
return impl;
|
|
929
968
|
}
|
|
930
969
|
const createLocatorExtract = (locator) => createExtract(locator);
|
|
931
970
|
const createPageExtract = (page) => createExtract(page);
|
|
932
971
|
|
|
972
|
+
const GET_LOCATOR_PATH = "internal/v3/get-aria-refs";
|
|
973
|
+
const EMPTY_LOCATOR_SELECTOR = "__stably_empty_locator__";
|
|
974
|
+
function getEndpoint() {
|
|
975
|
+
const baseUrl = process.env.STABLY_API_URL || "https://api.stably.ai";
|
|
976
|
+
return new URL(GET_LOCATOR_PATH, baseUrl).toString();
|
|
977
|
+
}
|
|
978
|
+
const isAPIResponse = (value) => {
|
|
979
|
+
if (!isObject(value)) {
|
|
980
|
+
return false;
|
|
981
|
+
}
|
|
982
|
+
return Array.isArray(value.refs) && value.refs.every((ref) => typeof ref === "string") && typeof value.reason === "string";
|
|
983
|
+
};
|
|
984
|
+
const isErrorResponse = (value) => {
|
|
985
|
+
return isObject(value) && typeof value.error === "string";
|
|
986
|
+
};
|
|
987
|
+
async function getLocatorsByAI(page, prompt, options) {
|
|
988
|
+
return await test.step(`[GetLocatorByAI] ${prompt}`, async (stepInfo) => {
|
|
989
|
+
const apiKey = requireApiKey();
|
|
990
|
+
const pageWithSnapshot = page;
|
|
991
|
+
if (typeof pageWithSnapshot._snapshotForAI !== "function") {
|
|
992
|
+
throw new Error(
|
|
993
|
+
"getLocatorsByAI requires Playwright v1.54.1 or higher. Please upgrade your Playwright version."
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
const snapshotResult = await pageWithSnapshot._snapshotForAI();
|
|
997
|
+
const ariaSnapshot = typeof snapshotResult === "string" ? snapshotResult : snapshotResult.full;
|
|
998
|
+
const response = await (async () => {
|
|
999
|
+
try {
|
|
1000
|
+
return await fetch(getEndpoint(), {
|
|
1001
|
+
body: JSON.stringify({
|
|
1002
|
+
ariaSnapshot,
|
|
1003
|
+
prompt,
|
|
1004
|
+
...options?.model && { model: options.model }
|
|
1005
|
+
}),
|
|
1006
|
+
headers: {
|
|
1007
|
+
...SDK_METADATA_HEADERS,
|
|
1008
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1009
|
+
"Content-Type": "application/json"
|
|
1010
|
+
},
|
|
1011
|
+
method: "POST"
|
|
1012
|
+
});
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
throw new Error(
|
|
1015
|
+
`getLocatorsByAI failed to connect to Stably API: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
})();
|
|
1019
|
+
const raw = await response.json().catch(() => void 0);
|
|
1020
|
+
if (!response.ok) {
|
|
1021
|
+
const err = isErrorResponse(raw) ? raw.error : "Unknown error";
|
|
1022
|
+
throw new Error(`getLocatorsByAI failed (${response.status}): ${err}`);
|
|
1023
|
+
}
|
|
1024
|
+
if (!isAPIResponse(raw)) {
|
|
1025
|
+
throw new Error("getLocatorsByAI returned unexpected response shape");
|
|
1026
|
+
}
|
|
1027
|
+
await stepInfo.attach("[GetLocatorByAI] reason", {
|
|
1028
|
+
body: raw.reason,
|
|
1029
|
+
contentType: "text/plain"
|
|
1030
|
+
});
|
|
1031
|
+
const locator = raw.refs.length === 0 ? (
|
|
1032
|
+
// Empty locator that matches nothing
|
|
1033
|
+
page.locator(EMPTY_LOCATOR_SELECTOR)
|
|
1034
|
+
) : (
|
|
1035
|
+
// Combine refs using .or()
|
|
1036
|
+
raw.refs.map((ref) => page.locator(`aria-ref=${ref}`)).reduce((acc, loc) => acc.or(loc))
|
|
1037
|
+
);
|
|
1038
|
+
return {
|
|
1039
|
+
count: raw.refs.length,
|
|
1040
|
+
locator,
|
|
1041
|
+
reason: raw.reason
|
|
1042
|
+
};
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const createPageGetLocatorByAI = (page) => {
|
|
1047
|
+
return (prompt, options) => getLocatorsByAI(page, prompt, options);
|
|
1048
|
+
};
|
|
1049
|
+
|
|
933
1050
|
const LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
|
|
934
1051
|
const PAGE_PATCHED = Symbol.for("stably.playwright.pagePatched");
|
|
935
1052
|
const CONTEXT_PATCHED = Symbol.for("stably.playwright.contextPatched");
|
|
@@ -961,6 +1078,7 @@ function augmentPage(page) {
|
|
|
961
1078
|
return augmentLocator(locator);
|
|
962
1079
|
});
|
|
963
1080
|
defineHiddenProperty(page, "extract", createPageExtract(page));
|
|
1081
|
+
defineHiddenProperty(page, "getLocatorsByAI", createPageGetLocatorByAI(page));
|
|
964
1082
|
defineHiddenProperty(page, PAGE_PATCHED, true);
|
|
965
1083
|
return page;
|
|
966
1084
|
}
|