@stablyai/playwright-base 2.1.10 → 2.1.11-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.10"
50
+ "X-Client-Version": "2.1.11-rc.0"
51
51
  };
52
52
 
53
53
  const PROMPT_ASSERTION_PATH = "internal/v2/assert";
@@ -374,23 +374,60 @@ async function takeStableScreenshot(target, options) {
374
374
  const MAX_ATTACHMENT_NAME_LENGTH = 80;
375
375
  const DEFAULT_AI_ASSERT_TIMEOUT_MS = 3e4;
376
376
  const RETRY_DELAY_MS = 1e3;
377
+ class AiAssertTimeoutError extends Error {
378
+ constructor(timeoutMs) {
379
+ super(`aiAssert timed out after ${timeoutMs}ms`);
380
+ this.name = "AiAssertTimeoutError";
381
+ }
382
+ }
377
383
  function delay(ms) {
378
384
  return new Promise((resolve) => setTimeout(resolve, ms));
379
385
  }
386
+ function addAssertAttachments(condition, verifyResult, screenshot) {
387
+ const testInfo = test.test.info();
388
+ const sanitizedName = condition.trim().toLowerCase().replace(/[^\w]+/g, "_").replace(/^_+|_+$/g, "").slice(0, MAX_ATTACHMENT_NAME_LENGTH) || "aiAssert";
389
+ testInfo.attachments.push(
390
+ {
391
+ body: Buffer.from(
392
+ JSON.stringify(
393
+ {
394
+ pass: verifyResult.pass,
395
+ prompt: condition,
396
+ reasoning: verifyResult.reason
397
+ },
398
+ null,
399
+ 2
400
+ ),
401
+ "utf-8"
402
+ ),
403
+ contentType: "application/json",
404
+ name: `${sanitizedName}-reasoning`
405
+ },
406
+ {
407
+ body: screenshot,
408
+ contentType: "application/octet-stream",
409
+ name: `${sanitizedName}-screenshot.png`
410
+ }
411
+ );
412
+ }
380
413
  function createFailureMessage({
381
414
  condition,
382
415
  didPass,
383
416
  isNot,
384
417
  reason,
385
- targetType
418
+ targetType,
419
+ timeoutMs
386
420
  }) {
387
421
  const expectation = isNot ? "not to satisfy" : "to satisfy";
388
422
  const result = didPass ? "it did" : "it did not";
389
423
  let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
390
424
  if (reason) {
391
425
  message += `
392
-
393
426
  Reason: ${reason}`;
427
+ }
428
+ if (timeoutMs != null) {
429
+ message += `
430
+ Timeout: ${timeoutMs}ms`;
394
431
  }
395
432
  return message;
396
433
  }
@@ -425,17 +462,24 @@ const stablyPlaywrightMatchers = {
425
462
  const { isNot } = this;
426
463
  const deadline = Date.now() + timeoutMs;
427
464
  let aborted = false;
465
+ const lastAttempt = {
466
+ hasCompleted: false,
467
+ screenshot: Buffer.alloc(0),
468
+ verifyResult: { pass: false }
469
+ };
470
+ function recordAttempt(verifyResult, screenshot) {
471
+ lastAttempt.verifyResult = verifyResult;
472
+ lastAttempt.screenshot = screenshot;
473
+ lastAttempt.hasCompleted = true;
474
+ }
475
+ let timeoutId;
428
476
  const timeoutPromise = new Promise((_, reject) => {
429
- setTimeout(() => {
477
+ timeoutId = setTimeout(() => {
430
478
  aborted = true;
431
- reject(new Error(`aiAssert timed out after ${timeoutMs}ms`));
479
+ reject(new AiAssertTimeoutError(timeoutMs));
432
480
  }, timeoutMs);
433
481
  });
434
482
  const work = async () => {
435
- let lastVerifyResult = {
436
- pass: false
437
- };
438
- let lastScreenshot = Buffer.alloc(0);
439
483
  let isFirstAttempt = true;
440
484
  while (!aborted) {
441
485
  if (!isFirstAttempt) {
@@ -449,58 +493,63 @@ const stablyPlaywrightMatchers = {
449
493
  }
450
494
  }
451
495
  isFirstAttempt = false;
452
- lastScreenshot = await takeStableScreenshot(target, options);
453
- lastVerifyResult = await verifyPrompt({
496
+ const screenshot = await takeStableScreenshot(target, options);
497
+ const verifyResult = await verifyPrompt({
454
498
  model: options?.model,
455
499
  pageMetadata: isPage(target) ? { title: await target.title(), url: target.url() } : void 0,
456
500
  prompt: condition,
457
- screenshot: lastScreenshot
501
+ screenshot
458
502
  });
459
- const assertionPasses = lastVerifyResult.pass !== isNot;
503
+ recordAttempt(verifyResult, screenshot);
504
+ const assertionPasses = verifyResult.pass !== isNot;
460
505
  if (assertionPasses) {
461
506
  break;
462
507
  }
463
508
  }
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
- );
509
+ const assertionPassed = lastAttempt.verifyResult.pass !== isNot;
491
510
  return {
492
511
  message: () => createFailureMessage({
493
512
  condition,
494
- didPass: lastVerifyResult.pass,
513
+ didPass: lastAttempt.verifyResult.pass,
495
514
  isNot,
496
- reason: lastVerifyResult.reason,
497
- targetType
515
+ reason: lastAttempt.verifyResult.reason,
516
+ targetType,
517
+ // Include timeout in the message when the assertion didn't pass,
518
+ // matching how Playwright's built-in matchers report timeouts.
519
+ timeoutMs: assertionPassed ? void 0 : timeoutMs
498
520
  }),
499
521
  name: "aiAssert",
500
- pass: lastVerifyResult.pass
522
+ pass: lastAttempt.verifyResult.pass
501
523
  };
502
524
  };
503
- return Promise.race([work(), timeoutPromise]);
525
+ try {
526
+ return await Promise.race([work(), timeoutPromise]);
527
+ } catch (error) {
528
+ if (error instanceof AiAssertTimeoutError) {
529
+ return {
530
+ message: () => createFailureMessage({
531
+ condition,
532
+ didPass: lastAttempt.verifyResult.pass,
533
+ isNot,
534
+ reason: lastAttempt.verifyResult.reason,
535
+ targetType,
536
+ timeoutMs
537
+ }),
538
+ name: "aiAssert",
539
+ pass: lastAttempt.verifyResult.pass
540
+ };
541
+ }
542
+ throw error;
543
+ } finally {
544
+ clearTimeout(timeoutId);
545
+ if (lastAttempt.hasCompleted) {
546
+ addAssertAttachments(
547
+ condition,
548
+ lastAttempt.verifyResult,
549
+ lastAttempt.screenshot
550
+ );
551
+ }
552
+ }
504
553
  },
505
554
  /**
506
555
  * @deprecated Use `aiAssert` instead. This method will be removed in a future version.