@stablyai/playwright-base 2.1.10 → 2.1.11

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