@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 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.16"
50
+ "X-Client-Version": "2.1.1"
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
- async toMatchScreenshotPrompt(received, condition, options) {
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
- "toMatchScreenshotPrompt only supports Playwright Page and Locator instances."
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) || "toMatchScreenshotPrompt";
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: "toMatchScreenshotPrompt",
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
  }