@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.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.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.
|
|
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
|
|
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
|
-
|
|
432
|
-
|
|
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
|
|
480
|
+
screenshot
|
|
437
481
|
});
|
|
438
|
-
|
|
482
|
+
recordAttempt(verifyResult, screenshot);
|
|
483
|
+
const assertionPasses = verifyResult.pass !== isNot;
|
|
439
484
|
if (assertionPasses) {
|
|
440
485
|
break;
|
|
441
486
|
}
|
|
442
487
|
}
|
|
443
|
-
const
|
|
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:
|
|
492
|
+
didPass: lastAttempt.verifyResult.pass,
|
|
474
493
|
isNot,
|
|
475
|
-
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:
|
|
501
|
+
pass: lastAttempt.verifyResult.pass
|
|
480
502
|
};
|
|
481
503
|
};
|
|
482
|
-
|
|
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.
|