@stablyai/playwright-base 2.1.9 → 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 +94 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +94 -45
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
453
|
-
|
|
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
|
|
501
|
+
screenshot
|
|
458
502
|
});
|
|
459
|
-
|
|
503
|
+
recordAttempt(verifyResult, screenshot);
|
|
504
|
+
const assertionPasses = verifyResult.pass !== isNot;
|
|
460
505
|
if (assertionPasses) {
|
|
461
506
|
break;
|
|
462
507
|
}
|
|
463
508
|
}
|
|
464
|
-
const
|
|
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:
|
|
513
|
+
didPass: lastAttempt.verifyResult.pass,
|
|
495
514
|
isNot,
|
|
496
|
-
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:
|
|
522
|
+
pass: lastAttempt.verifyResult.pass
|
|
501
523
|
};
|
|
502
524
|
};
|
|
503
|
-
|
|
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.
|