@stablyai/playwright-base 2.1.5 → 2.1.6

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.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.1.5"
29
+ "X-Client-Version": "2.1.6"
30
30
  };
31
31
 
32
32
  const PROMPT_ASSERTION_PATH = "internal/v2/assert";
@@ -351,6 +351,11 @@ async function takeStableScreenshot(target, options) {
351
351
  }
352
352
 
353
353
  const MAX_ATTACHMENT_NAME_LENGTH = 80;
354
+ const DEFAULT_AI_ASSERT_TIMEOUT_MS = 3e4;
355
+ const RETRY_DELAY_MS = 1e3;
356
+ function delay(ms) {
357
+ return new Promise((resolve) => setTimeout(resolve, ms));
358
+ }
354
359
  function createFailureMessage({
355
360
  condition,
356
361
  didPass,
@@ -395,51 +400,86 @@ const stablyPlaywrightMatchers = {
395
400
  );
396
401
  }
397
402
  const targetType = isPage(target) ? "page" : "locator";
398
- const screenshot = await takeStableScreenshot(target, options);
399
- const verifyResult = await verifyPrompt({
400
- model: options?.model,
401
- pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
402
- prompt: condition,
403
- screenshot
403
+ const timeoutMs = options?.timeout ?? DEFAULT_AI_ASSERT_TIMEOUT_MS;
404
+ const { isNot } = this;
405
+ const deadline = Date.now() + timeoutMs;
406
+ let aborted = false;
407
+ const timeoutPromise = new Promise((_, reject) => {
408
+ setTimeout(() => {
409
+ aborted = true;
410
+ reject(new Error(`aiAssert timed out after ${timeoutMs}ms`));
411
+ }, timeoutMs);
404
412
  });
405
- const testInfo = test.info();
406
- const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
407
- testInfo.attachments.push(
408
- {
409
- body: Buffer.from(
410
- JSON.stringify(
411
- {
412
- pass: verifyResult.pass,
413
- prompt: condition,
414
- reasoning: verifyResult.reason
415
- },
416
- null,
417
- 2
418
- ),
419
- "utf-8"
420
- ),
421
- contentType: "application/json",
422
- name: `${sanitizedName}-reasoning`
423
- },
424
- {
425
- body: screenshot,
426
- // Use binary type to avoid inline previews in the report
427
- // Ensures the screenshot is paired with the reasoning attachment instead of being rendered in a separate section.
428
- contentType: "application/octet-stream",
429
- name: `${sanitizedName}-screenshot.png`
413
+ const work = async () => {
414
+ let lastVerifyResult = {
415
+ pass: false
416
+ };
417
+ let lastScreenshot = Buffer.alloc(0);
418
+ let isFirstAttempt = true;
419
+ while (!aborted) {
420
+ if (!isFirstAttempt) {
421
+ const remaining = deadline - Date.now();
422
+ if (remaining <= 0) {
423
+ break;
424
+ }
425
+ await delay(Math.min(RETRY_DELAY_MS, remaining));
426
+ if (aborted) {
427
+ break;
428
+ }
429
+ }
430
+ isFirstAttempt = false;
431
+ lastScreenshot = await takeStableScreenshot(target, options);
432
+ lastVerifyResult = await verifyPrompt({
433
+ model: options?.model,
434
+ pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
435
+ prompt: condition,
436
+ screenshot: lastScreenshot
437
+ });
438
+ const assertionPasses = lastVerifyResult.pass !== isNot;
439
+ if (assertionPasses) {
440
+ break;
441
+ }
430
442
  }
431
- );
432
- return {
433
- message: () => createFailureMessage({
434
- condition,
435
- didPass: verifyResult.pass,
436
- isNot: this.isNot,
437
- reason: verifyResult.reason,
438
- targetType
439
- }),
440
- name: "aiAssert",
441
- pass: verifyResult.pass
443
+ const testInfo = test.info();
444
+ const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
445
+ testInfo.attachments.push(
446
+ {
447
+ body: Buffer.from(
448
+ JSON.stringify(
449
+ {
450
+ pass: lastVerifyResult.pass,
451
+ prompt: condition,
452
+ reasoning: lastVerifyResult.reason
453
+ },
454
+ null,
455
+ 2
456
+ ),
457
+ "utf-8"
458
+ ),
459
+ contentType: "application/json",
460
+ name: `${sanitizedName}-reasoning`
461
+ },
462
+ {
463
+ body: lastScreenshot,
464
+ // Use binary type to avoid inline previews in the report
465
+ // Ensures the screenshot is paired with the reasoning attachment instead of being rendered in a separate section.
466
+ contentType: "application/octet-stream",
467
+ name: `${sanitizedName}-screenshot.png`
468
+ }
469
+ );
470
+ return {
471
+ message: () => createFailureMessage({
472
+ condition,
473
+ didPass: lastVerifyResult.pass,
474
+ isNot,
475
+ reason: lastVerifyResult.reason,
476
+ targetType
477
+ }),
478
+ name: "aiAssert",
479
+ pass: lastVerifyResult.pass
480
+ };
442
481
  };
482
+ return Promise.race([work(), timeoutPromise]);
443
483
  },
444
484
  /**
445
485
  * @deprecated Use `aiAssert` instead. This method will be removed in a future version.