@dev-blinq/bvt-playwright-js 1.0.0-dev.4.latest.158.1 → 1.0.0-dev.4.latest.177.1
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/index.d.mts +17 -0
- package/index.mjs +69 -10
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/index.d.mts
CHANGED
|
@@ -277,6 +277,7 @@ declare const elementAssertionSchema: z.ZodDiscriminatedUnion<[
|
|
|
277
277
|
type: z.ZodLiteral<"toHaveProperty">;
|
|
278
278
|
name: z.ZodString;
|
|
279
279
|
operator: z.ZodOptional<z.ZodEnum<{
|
|
280
|
+
regex: "regex";
|
|
280
281
|
equals: "equals";
|
|
281
282
|
notEquals: "notEquals";
|
|
282
283
|
contains: "contains";
|
|
@@ -2021,6 +2022,7 @@ declare const CommandSchema: z.ZodDiscriminatedUnion<[
|
|
|
2021
2022
|
type: z.ZodLiteral<"toHaveProperty">;
|
|
2022
2023
|
name: z.ZodString;
|
|
2023
2024
|
operator: z.ZodOptional<z.ZodEnum<{
|
|
2025
|
+
regex: "regex";
|
|
2024
2026
|
equals: "equals";
|
|
2025
2027
|
notEquals: "notEquals";
|
|
2026
2028
|
contains: "contains";
|
|
@@ -5031,6 +5033,7 @@ declare const StepDefinitionSchema: z.ZodObject<{
|
|
|
5031
5033
|
type: z.ZodLiteral<"toHaveProperty">;
|
|
5032
5034
|
name: z.ZodString;
|
|
5033
5035
|
operator: z.ZodOptional<z.ZodEnum<{
|
|
5036
|
+
regex: "regex";
|
|
5034
5037
|
equals: "equals";
|
|
5035
5038
|
notEquals: "notEquals";
|
|
5036
5039
|
contains: "contains";
|
|
@@ -9305,6 +9308,19 @@ type TesterSession = {
|
|
|
9305
9308
|
*/
|
|
9306
9309
|
screenshots?: {
|
|
9307
9310
|
enabled: boolean;
|
|
9311
|
+
/**
|
|
9312
|
+
* "upload" (default): Tester requests presigned URLs and uploads the
|
|
9313
|
+
* captured JPEGs to S3 itself at the end of the testcase.
|
|
9314
|
+
* "local": Tester captures JPEGs to `outputDir` and leaves them on disk
|
|
9315
|
+
* for an out-of-band uploader (mirrors the local-trace mode used by the
|
|
9316
|
+
* execution worker, which has no presigned-URL API client).
|
|
9317
|
+
*/
|
|
9318
|
+
mode?: "upload" | "local";
|
|
9319
|
+
/**
|
|
9320
|
+
* Local-mode capture directory. When omitted a per-testcase /tmp dir is
|
|
9321
|
+
* used. Ignored in "upload" mode.
|
|
9322
|
+
*/
|
|
9323
|
+
outputDir?: string;
|
|
9308
9324
|
};
|
|
9309
9325
|
deterministicRecoveryMode?: "new-tour-maintenance";
|
|
9310
9326
|
};
|
|
@@ -9323,6 +9339,7 @@ type ExecuteStepsOptions = {
|
|
|
9323
9339
|
onFinish?: () => Promise<void>;
|
|
9324
9340
|
onError?: (error: Error) => Promise<void>;
|
|
9325
9341
|
abortController?: AbortController;
|
|
9342
|
+
recoveryAbortController?: AbortController;
|
|
9326
9343
|
};
|
|
9327
9344
|
type GetAPIClient = (sessionToken: string) => TesterApiClient;
|
|
9328
9345
|
declare class Tester {
|
package/index.mjs
CHANGED
|
@@ -5170,7 +5170,8 @@ const ASSERTION_OPERATORS = [
|
|
|
5170
5170
|
"contains",
|
|
5171
5171
|
"lessThan",
|
|
5172
5172
|
"greaterThan",
|
|
5173
|
-
"exists"
|
|
5173
|
+
"exists",
|
|
5174
|
+
"regex"
|
|
5174
5175
|
];
|
|
5175
5176
|
|
|
5176
5177
|
//#endregion
|
|
@@ -10936,6 +10937,7 @@ const AiChatDataExecutionStatusPartSchema = object({
|
|
|
10936
10937
|
* from the call sites in `apps/ai-server/src/mastra/agents/`:
|
|
10937
10938
|
* - `observe-act-loop` (observe-act-completion happy path)
|
|
10938
10939
|
* - `observe-act-loop-cap-reached` (loop hit iteration cap mid-run)
|
|
10940
|
+
* - `form-fill-batch` (on-mode form-fill fast-path success)
|
|
10939
10941
|
* - `segment-followup` (apps/ai-server/src/trpc/router.ts segment-followup path)
|
|
10940
10942
|
* Plus the AiStepDraftMode values used by client-side fixtures that did not
|
|
10941
10943
|
* originate from the observe-act loop (legacy: `new-ai-step`, `edit-ai-step`).
|
|
@@ -10946,6 +10948,7 @@ const AiChatDataExecutionStatusPartSchema = object({
|
|
|
10946
10948
|
const AiChatDataCompleteModeSchema = _enum([
|
|
10947
10949
|
"observe-act-loop",
|
|
10948
10950
|
"observe-act-loop-cap-reached",
|
|
10951
|
+
"form-fill-batch",
|
|
10949
10952
|
"segment-followup",
|
|
10950
10953
|
"new-ai-step",
|
|
10951
10954
|
"edit-ai-step"
|
|
@@ -26320,6 +26323,9 @@ const NEW_TOUR_MAINTENANCE_RECOVERY_ARTIFACT = {
|
|
|
26320
26323
|
aiLog: "BlinqIO matched the original gender-selection intent to the new radio button UI and rewrote the stale command.",
|
|
26321
26324
|
description: "The repaired step selects the gender radio option, then continues the registration flow successfully."
|
|
26322
26325
|
};
|
|
26326
|
+
function isAbortError(error) {
|
|
26327
|
+
return error instanceof Error && error.name === "AbortError";
|
|
26328
|
+
}
|
|
26323
26329
|
const unconfiguredGetAPIClient = () => {
|
|
26324
26330
|
throw new Error("Tester was created without an API client.");
|
|
26325
26331
|
};
|
|
@@ -26454,7 +26460,7 @@ var Tester = class {
|
|
|
26454
26460
|
return `${this.dataContext?.projectId ?? ""}/${session.reportId}/testcases/${session.testCaseId}/resources/screenshots/${stepId}-${commandId}-${phase}.jpg`;
|
|
26455
26461
|
}
|
|
26456
26462
|
getScreenshotTempDir(session) {
|
|
26457
|
-
if (!this.tempPathForAssets) this.tempPathForAssets = join("/tmp", `blinq-screenshots-${session.reportId}-${session.testCaseId}`);
|
|
26463
|
+
if (!this.tempPathForAssets) this.tempPathForAssets = session.screenshots?.outputDir ?? join("/tmp", `blinq-screenshots-${session.reportId}-${session.testCaseId}`);
|
|
26458
26464
|
return this.tempPathForAssets;
|
|
26459
26465
|
}
|
|
26460
26466
|
/**
|
|
@@ -27044,6 +27050,16 @@ var Tester = class {
|
|
|
27044
27050
|
});
|
|
27045
27051
|
return false;
|
|
27046
27052
|
}
|
|
27053
|
+
if (session.screenshots?.mode === "local") {
|
|
27054
|
+
this.obs.logger.log("Screenshots captured in local mode; leaving files for out-of-band upload", {
|
|
27055
|
+
reportId: session.reportId,
|
|
27056
|
+
testCaseId: session.testCaseId,
|
|
27057
|
+
capturedCount,
|
|
27058
|
+
dir: this.tempPathForAssets
|
|
27059
|
+
});
|
|
27060
|
+
this.capturedScreenshots = [];
|
|
27061
|
+
return capturedCount > 0;
|
|
27062
|
+
}
|
|
27047
27063
|
if (!session.token) {
|
|
27048
27064
|
this.obs.metrics.count("bvt_agent.screenshots.upload.skipped.total", 1, { reason: "no-session-token" });
|
|
27049
27065
|
this.obs.logger.warn(`Screenshot upload skipped because no session token was provided for reportId: ${session.reportId}, testCaseId: ${session.testCaseId}.`);
|
|
@@ -27275,7 +27291,21 @@ var Tester = class {
|
|
|
27275
27291
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
27276
27292
|
};
|
|
27277
27293
|
const failureContext = this.buildFailureContext(command, recorderStep, stepDefinitionId, commandIndex, commandContext, executionError);
|
|
27278
|
-
const
|
|
27294
|
+
const recoveryContext = {
|
|
27295
|
+
...failureContext,
|
|
27296
|
+
abortSignal: options.recoveryAbortController?.signal ?? failureContext.abortSignal
|
|
27297
|
+
};
|
|
27298
|
+
let recoveryResult;
|
|
27299
|
+
try {
|
|
27300
|
+
recoveryResult = await this.recoveryController.analyzeAndPropose(recoveryContext);
|
|
27301
|
+
} catch (recoveryError) {
|
|
27302
|
+
if (isAbortError(recoveryError)) {
|
|
27303
|
+
this.obs.logger.log("AI recovery cancelled; preserving original command failure", { commandId: command._id });
|
|
27304
|
+
yield await this.onCommandFail(command, stepDefinitionId, executionError, session);
|
|
27305
|
+
throw executionError;
|
|
27306
|
+
}
|
|
27307
|
+
throw recoveryError;
|
|
27308
|
+
}
|
|
27279
27309
|
const { decision } = recoveryResult;
|
|
27280
27310
|
let artifact = recoveryResult.artifact;
|
|
27281
27311
|
if (this.shouldAttemptAiRecovery(session)) {
|
|
@@ -27344,7 +27374,7 @@ var Tester = class {
|
|
|
27344
27374
|
if (!persistResult.success) throw new Error(persistResult.error);
|
|
27345
27375
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27346
27376
|
decision,
|
|
27347
|
-
failureContext,
|
|
27377
|
+
failureContext: recoveryContext,
|
|
27348
27378
|
recoverySucceeded: true,
|
|
27349
27379
|
stoppedReason: "accept-repair",
|
|
27350
27380
|
attemptLog: this.buildStepRepairAttemptLog(decision, "succeeded")
|
|
@@ -27403,7 +27433,7 @@ var Tester = class {
|
|
|
27403
27433
|
});
|
|
27404
27434
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27405
27435
|
decision,
|
|
27406
|
-
failureContext,
|
|
27436
|
+
failureContext: recoveryContext,
|
|
27407
27437
|
recoverySucceeded: false,
|
|
27408
27438
|
stoppedReason: "retry_failed",
|
|
27409
27439
|
attemptLog: this.buildStepRepairAttemptLog(decision, "failed")
|
|
@@ -27424,7 +27454,7 @@ var Tester = class {
|
|
|
27424
27454
|
terminalExecutionError = /* @__PURE__ */ new Error(`${decision.userFacingMessages.failed}. Repair application failure: ${appliedRepair.error}. Original failure: ${executionError.message}`);
|
|
27425
27455
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27426
27456
|
decision,
|
|
27427
|
-
failureContext,
|
|
27457
|
+
failureContext: recoveryContext,
|
|
27428
27458
|
recoverySucceeded: false,
|
|
27429
27459
|
stoppedReason: "repair_application_failed",
|
|
27430
27460
|
attemptLog: this.buildStepRepairAttemptLog(decision, "failed")
|
|
@@ -27485,7 +27515,7 @@ var Tester = class {
|
|
|
27485
27515
|
if (!persistResult.success) throw new Error(persistResult.error);
|
|
27486
27516
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27487
27517
|
decision,
|
|
27488
|
-
failureContext,
|
|
27518
|
+
failureContext: recoveryContext,
|
|
27489
27519
|
recoverySucceeded: true,
|
|
27490
27520
|
stoppedReason: "accept-repair",
|
|
27491
27521
|
attemptLog: this.buildStepRepairAttemptLog(decision, "succeeded")
|
|
@@ -27545,7 +27575,7 @@ var Tester = class {
|
|
|
27545
27575
|
});
|
|
27546
27576
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27547
27577
|
decision,
|
|
27548
|
-
failureContext,
|
|
27578
|
+
failureContext: recoveryContext,
|
|
27549
27579
|
recoverySucceeded: false,
|
|
27550
27580
|
stoppedReason: "retry_failed",
|
|
27551
27581
|
attemptLog: this.buildStepRepairAttemptLog(decision, "failed")
|
|
@@ -27565,7 +27595,7 @@ var Tester = class {
|
|
|
27565
27595
|
else {
|
|
27566
27596
|
artifact = await this.generateRecoveryArtifactForOutcome({
|
|
27567
27597
|
decision,
|
|
27568
|
-
failureContext,
|
|
27598
|
+
failureContext: recoveryContext,
|
|
27569
27599
|
recoverySucceeded: false,
|
|
27570
27600
|
stoppedReason: "repair_application_failed",
|
|
27571
27601
|
attemptLog: this.buildStepRepairAttemptLog(decision, "failed")
|
|
@@ -27978,6 +28008,33 @@ var Tester = class {
|
|
|
27978
28008
|
if (!(actual > threshold)) throw new Error(`Property "${assertion.name}" expected to be greater than ${threshold}, but got ${actual}`);
|
|
27979
28009
|
}
|
|
27980
28010
|
break;
|
|
28011
|
+
case "regex":
|
|
28012
|
+
if (assertion.value !== void 0) {
|
|
28013
|
+
const raw = getValueFromPrimitiveOrRegex(assertion.value);
|
|
28014
|
+
const pattern = raw instanceof RegExp ? raw : new RegExp(String(raw));
|
|
28015
|
+
this.obs.logger.info(`Expected property "${assertion.name}" to match pattern: ${pattern}`);
|
|
28016
|
+
const propName = assertion.name;
|
|
28017
|
+
const isTextProp = propName === "textContent" || propName === "innerText";
|
|
28018
|
+
const isHtmlAttr = [
|
|
28019
|
+
"href",
|
|
28020
|
+
"src",
|
|
28021
|
+
"class",
|
|
28022
|
+
"id",
|
|
28023
|
+
"placeholder",
|
|
28024
|
+
"title",
|
|
28025
|
+
"alt",
|
|
28026
|
+
"name",
|
|
28027
|
+
"type",
|
|
28028
|
+
"action",
|
|
28029
|
+
"target",
|
|
28030
|
+
"rel",
|
|
28031
|
+
"value"
|
|
28032
|
+
].includes(propName) || propName.startsWith("data-") || propName.startsWith("aria-");
|
|
28033
|
+
if (isTextProp) await expect(locator).toHaveText(pattern, assertion.options);
|
|
28034
|
+
else if (isHtmlAttr) await expect(locator).toHaveAttribute(propName, pattern, assertion.options);
|
|
28035
|
+
else await expect.poll(async () => locator.evaluate((el, name) => String(el[name] ?? ""), propName), { timeout: assertion.options?.timeout }).toMatch(pattern);
|
|
28036
|
+
}
|
|
28037
|
+
break;
|
|
27981
28038
|
case "exists": {
|
|
27982
28039
|
const exists = await locator.evaluate((el, name) => el[name] !== void 0, assertion.name);
|
|
27983
28040
|
this.obs.logger.info(`Expected property "${assertion.name}" to exist: ${exists}`);
|
|
@@ -28033,7 +28090,9 @@ var Tester = class {
|
|
|
28033
28090
|
const frame = await this.getFrameObjectFromFrameScope(frameScope);
|
|
28034
28091
|
this.obs.logger.info(`Checking for absence of element with text ${value} using "${locator.first()?._selector}" `);
|
|
28035
28092
|
this.obs.logger.info(`Waiting for page to load and stabilize before checking for absence of text to reduce flakiness...`);
|
|
28036
|
-
await frame.waitForLoadState("networkidle", { timeout: 3e4 })
|
|
28093
|
+
await frame.waitForLoadState("networkidle", { timeout: 3e4 }).catch((error) => {
|
|
28094
|
+
this.obs.logger.error(`Frame load timeout, the page may not be in the desired state yet`, error);
|
|
28095
|
+
});
|
|
28037
28096
|
this.obs.logger.info(`frame load detected. Waiting an additional 500ms for stabilization...`);
|
|
28038
28097
|
await frame.waitForTimeout(500);
|
|
28039
28098
|
this.obs.logger.info(`Checking for element with text "${value}" after page load and stabilization delay...`);
|