@stablyai/playwright-base 2.1.5 → 2.1.6-rc.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 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.1.5"
50
+ "X-Client-Version": "2.1.6-rc.0"
51
51
  };
52
52
 
53
53
  const PROMPT_ASSERTION_PATH = "internal/v2/assert";
@@ -372,6 +372,11 @@ async function takeStableScreenshot(target, options) {
372
372
  }
373
373
 
374
374
  const MAX_ATTACHMENT_NAME_LENGTH = 80;
375
+ const DEFAULT_AI_ASSERT_TIMEOUT_MS = 3e4;
376
+ const RETRY_DELAY_MS = 1e3;
377
+ function delay(ms) {
378
+ return new Promise((resolve) => setTimeout(resolve, ms));
379
+ }
375
380
  function createFailureMessage({
376
381
  condition,
377
382
  didPass,
@@ -416,51 +421,86 @@ const stablyPlaywrightMatchers = {
416
421
  );
417
422
  }
418
423
  const targetType = isPage(target) ? "page" : "locator";
419
- const screenshot = await takeStableScreenshot(target, options);
420
- const verifyResult = await verifyPrompt({
421
- model: options?.model,
422
- pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
423
- prompt: condition,
424
- screenshot
424
+ const timeoutMs = options?.timeout ?? DEFAULT_AI_ASSERT_TIMEOUT_MS;
425
+ const { isNot } = this;
426
+ const deadline = Date.now() + timeoutMs;
427
+ let aborted = false;
428
+ const timeoutPromise = new Promise((_, reject) => {
429
+ setTimeout(() => {
430
+ aborted = true;
431
+ reject(new Error(`aiAssert timed out after ${timeoutMs}ms`));
432
+ }, timeoutMs);
425
433
  });
426
- const testInfo = test.test.info();
427
- const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
428
- testInfo.attachments.push(
429
- {
430
- body: Buffer.from(
431
- JSON.stringify(
432
- {
433
- pass: verifyResult.pass,
434
- prompt: condition,
435
- reasoning: verifyResult.reason
436
- },
437
- null,
438
- 2
439
- ),
440
- "utf-8"
441
- ),
442
- contentType: "application/json",
443
- name: `${sanitizedName}-reasoning`
444
- },
445
- {
446
- body: screenshot,
447
- // Use binary type to avoid inline previews in the report
448
- // Ensures the screenshot is paired with the reasoning attachment instead of being rendered in a separate section.
449
- contentType: "application/octet-stream",
450
- name: `${sanitizedName}-screenshot.png`
434
+ const work = async () => {
435
+ let lastVerifyResult = {
436
+ pass: false
437
+ };
438
+ let lastScreenshot = Buffer.alloc(0);
439
+ let isFirstAttempt = true;
440
+ while (!aborted) {
441
+ if (!isFirstAttempt) {
442
+ const remaining = deadline - Date.now();
443
+ if (remaining <= 0) {
444
+ break;
445
+ }
446
+ await delay(Math.min(RETRY_DELAY_MS, remaining));
447
+ if (aborted) {
448
+ break;
449
+ }
450
+ }
451
+ isFirstAttempt = false;
452
+ lastScreenshot = await takeStableScreenshot(target, options);
453
+ lastVerifyResult = await verifyPrompt({
454
+ model: options?.model,
455
+ pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
456
+ prompt: condition,
457
+ screenshot: lastScreenshot
458
+ });
459
+ const assertionPasses = lastVerifyResult.pass !== isNot;
460
+ if (assertionPasses) {
461
+ break;
462
+ }
451
463
  }
452
- );
453
- return {
454
- message: () => createFailureMessage({
455
- condition,
456
- didPass: verifyResult.pass,
457
- isNot: this.isNot,
458
- reason: verifyResult.reason,
459
- targetType
460
- }),
461
- name: "aiAssert",
462
- pass: verifyResult.pass
464
+ const testInfo = test.test.info();
465
+ const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
466
+ testInfo.attachments.push(
467
+ {
468
+ body: Buffer.from(
469
+ JSON.stringify(
470
+ {
471
+ pass: lastVerifyResult.pass,
472
+ prompt: condition,
473
+ reasoning: lastVerifyResult.reason
474
+ },
475
+ null,
476
+ 2
477
+ ),
478
+ "utf-8"
479
+ ),
480
+ contentType: "application/json",
481
+ name: `${sanitizedName}-reasoning`
482
+ },
483
+ {
484
+ body: lastScreenshot,
485
+ // Use binary type to avoid inline previews in the report
486
+ // Ensures the screenshot is paired with the reasoning attachment instead of being rendered in a separate section.
487
+ contentType: "application/octet-stream",
488
+ name: `${sanitizedName}-screenshot.png`
489
+ }
490
+ );
491
+ return {
492
+ message: () => createFailureMessage({
493
+ condition,
494
+ didPass: lastVerifyResult.pass,
495
+ isNot,
496
+ reason: lastVerifyResult.reason,
497
+ targetType
498
+ }),
499
+ name: "aiAssert",
500
+ pass: lastVerifyResult.pass
501
+ };
463
502
  };
503
+ return Promise.race([work(), timeoutPromise]);
464
504
  },
465
505
  /**
466
506
  * @deprecated Use `aiAssert` instead. This method will be removed in a future version.