@dev-blinq/bvt-playwright-js 1.0.0-dev.4.latest.142.1 → 1.0.0-dev.4.latest.158.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 +111 -5
- package/index.mjs +519 -23
- package/index.mjs.map +1 -1
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -6,8 +6,8 @@ import path, { dirname } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { chromium, firefox, webkit } from "playwright";
|
|
8
8
|
import { expect } from "playwright/test";
|
|
9
|
-
import { mkdirSync, readFileSync as readFileSync$1, statSync } from "fs";
|
|
10
|
-
import { dirname as dirname$1 } from "path";
|
|
9
|
+
import { mkdirSync, readFileSync as readFileSync$1, rmSync, statSync } from "fs";
|
|
10
|
+
import { dirname as dirname$1, join } from "path";
|
|
11
11
|
import { createContext, runInContext } from "node:vm";
|
|
12
12
|
import { test } from "@playwright/test";
|
|
13
13
|
|
|
@@ -6562,6 +6562,7 @@ const RichElementTargetDetailsSchema = object({
|
|
|
6562
6562
|
}).passthrough();
|
|
6563
6563
|
const elementTargetSchema = object({
|
|
6564
6564
|
name: string(),
|
|
6565
|
+
isElementHidden: boolean().optional(),
|
|
6565
6566
|
uniqueSelectors: array(UniqueSelectorSchema),
|
|
6566
6567
|
frames: array(object({
|
|
6567
6568
|
name: string(),
|
|
@@ -7067,12 +7068,24 @@ const ExecResultSchema = discriminatedUnion("type", [
|
|
|
7067
7068
|
}).strict(),
|
|
7068
7069
|
object({ type: literal$1("skipped") }).strict()
|
|
7069
7070
|
]);
|
|
7071
|
+
/**
|
|
7072
|
+
* S3 keys for the before/after viewport screenshots captured around a single
|
|
7073
|
+
* command. Keys are deterministic (see the screenshot key convention in the
|
|
7074
|
+
* presigned-urls router) and are populated even before the batched upload runs;
|
|
7075
|
+
* the testcase-level {@link TestCaseSchema.shape.screenshotsAvailable} flag
|
|
7076
|
+
* indicates whether the objects were actually uploaded to S3.
|
|
7077
|
+
*/
|
|
7078
|
+
const CommandScreenshotsSchema = object({
|
|
7079
|
+
before: string().min(1).max(1024).optional(),
|
|
7080
|
+
after: string().min(1).max(1024).optional()
|
|
7081
|
+
}).strict();
|
|
7070
7082
|
const CommandResultSchema = object({
|
|
7071
7083
|
commandId: string().min(1).max(255),
|
|
7072
7084
|
startedAt: date().optional(),
|
|
7073
7085
|
completedAt: date().optional(),
|
|
7074
7086
|
result: ExecResultSchema,
|
|
7075
|
-
recovery: RecoveryMetadataSchema.optional()
|
|
7087
|
+
recovery: RecoveryMetadataSchema.optional(),
|
|
7088
|
+
screenshots: CommandScreenshotsSchema.optional()
|
|
7076
7089
|
}).strict();
|
|
7077
7090
|
const StepResultSchema = object({
|
|
7078
7091
|
stepId: string().min(1).max(255),
|
|
@@ -7100,6 +7113,7 @@ const TestCaseAiRecoverySchema = object({
|
|
|
7100
7113
|
}).strict();
|
|
7101
7114
|
const TestCaseWarningReasonSchema = _enum([
|
|
7102
7115
|
"TraceUploadFailed",
|
|
7116
|
+
"ScreenshotUploadFailed",
|
|
7103
7117
|
"ScenarioEmpty",
|
|
7104
7118
|
"AIRecoveryPendingApproval",
|
|
7105
7119
|
"AIRecoveryArtifactFailed",
|
|
@@ -7120,6 +7134,7 @@ const TestCaseSchema = object({
|
|
|
7120
7134
|
error: TestCaseErrorSchema.optional(),
|
|
7121
7135
|
aiRecovery: TestCaseAiRecoverySchema.optional(),
|
|
7122
7136
|
warnings: array(TestCaseWarningSchema).optional(),
|
|
7137
|
+
screenshotsAvailable: boolean().optional(),
|
|
7123
7138
|
startedAt: date(),
|
|
7124
7139
|
completedAt: date().nullable(),
|
|
7125
7140
|
stepResults: array(StepResultSchema),
|
|
@@ -26274,11 +26289,37 @@ function getResolvedChosenSelectorIndex(result) {
|
|
|
26274
26289
|
const candidate = result.resolvedChosenSelectorIndex;
|
|
26275
26290
|
return Number.isInteger(candidate) ? candidate : void 0;
|
|
26276
26291
|
}
|
|
26292
|
+
async function delayNewTourMaintenanceRecovery(signal) {
|
|
26293
|
+
if (signal?.aborted) throw new Error("Execution aborted");
|
|
26294
|
+
await new Promise((resolve, reject) => {
|
|
26295
|
+
let timeout;
|
|
26296
|
+
function cleanup() {
|
|
26297
|
+
signal?.removeEventListener("abort", handleAbort);
|
|
26298
|
+
}
|
|
26299
|
+
function handleAbort() {
|
|
26300
|
+
clearTimeout(timeout);
|
|
26301
|
+
cleanup();
|
|
26302
|
+
reject(/* @__PURE__ */ new Error("Execution aborted"));
|
|
26303
|
+
}
|
|
26304
|
+
timeout = setTimeout(() => {
|
|
26305
|
+
cleanup();
|
|
26306
|
+
resolve();
|
|
26307
|
+
}, NEW_TOUR_MAINTENANCE_RECOVERY_DELAY_MS);
|
|
26308
|
+
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
26309
|
+
});
|
|
26310
|
+
}
|
|
26277
26311
|
const browserTypesMap = {
|
|
26278
26312
|
chromium,
|
|
26279
26313
|
firefox,
|
|
26280
26314
|
webkit
|
|
26281
26315
|
};
|
|
26316
|
+
const NEW_TOUR_MAINTENANCE_RECOVERY_MODE = "new-tour-maintenance";
|
|
26317
|
+
const NEW_TOUR_MAINTENANCE_RECOVERY_DELAY_MS = 600;
|
|
26318
|
+
const NEW_TOUR_MAINTENANCE_RECOVERY_ARTIFACT = {
|
|
26319
|
+
rootCauseAnalysis: "The application changed the Gender field from a dropdown in \"Old Version\" to radio buttons in \"New Version\".",
|
|
26320
|
+
aiLog: "BlinqIO matched the original gender-selection intent to the new radio button UI and rewrote the stale command.",
|
|
26321
|
+
description: "The repaired step selects the gender radio option, then continues the registration flow successfully."
|
|
26322
|
+
};
|
|
26282
26323
|
const unconfiguredGetAPIClient = () => {
|
|
26283
26324
|
throw new Error("Tester was created without an API client.");
|
|
26284
26325
|
};
|
|
@@ -26312,6 +26353,19 @@ var Tester = class {
|
|
|
26312
26353
|
sessionToken = "";
|
|
26313
26354
|
activeBrowserContext = null;
|
|
26314
26355
|
reportId = ulid();
|
|
26356
|
+
tempPathForAssets = "";
|
|
26357
|
+
/**
|
|
26358
|
+
* Per-testcase log of before/after command screenshots captured on disk,
|
|
26359
|
+
* pending batch upload to S3 at the end of the run. Reset in onTestCaseStart.
|
|
26360
|
+
*/
|
|
26361
|
+
capturedScreenshots = [];
|
|
26362
|
+
/**
|
|
26363
|
+
* Per-testcase screenshot counters (reset in onTestCaseStart) used purely for
|
|
26364
|
+
* observability — how many commands were eligible for capture and how many
|
|
26365
|
+
* capture attempts failed. Upload counters are derived at upload time.
|
|
26366
|
+
*/
|
|
26367
|
+
screenshotCommandsConsidered = 0;
|
|
26368
|
+
screenshotCaptureFailures = 0;
|
|
26315
26369
|
constructor(getAPIClient = unconfiguredGetAPIClient, observabilityOrOptions, optionsArg = {}) {
|
|
26316
26370
|
this.getAPIClient = getAPIClient;
|
|
26317
26371
|
const isTesterOptions = observabilityOrOptions !== null && typeof observabilityOrOptions === "object" && ("commandPreprocessors" in observabilityOrOptions || "recoveryController" in observabilityOrOptions || "restoreStepStartForRepair" in observabilityOrOptions || "provider" in observabilityOrOptions || "getApiFetchImpl" in observabilityOrOptions || "stepTraceChunkCallbacks" in observabilityOrOptions || "context" in observabilityOrOptions || "onTestDataChange" in observabilityOrOptions || "customCodeRuntime" in observabilityOrOptions);
|
|
@@ -26388,6 +26442,87 @@ var Tester = class {
|
|
|
26388
26442
|
default: return `Unknown Command`;
|
|
26389
26443
|
}
|
|
26390
26444
|
}
|
|
26445
|
+
screenshotsEnabled(session) {
|
|
26446
|
+
return this.isRunSession(session) && session.screenshots?.enabled === true;
|
|
26447
|
+
}
|
|
26448
|
+
/**
|
|
26449
|
+
* Deterministic S3 key for a command screenshot. Must match the convention
|
|
26450
|
+
* used by the server's `getPresignedUrlsForScreenshotUpload` route so the
|
|
26451
|
+
* keys recorded on the command report point at the uploaded objects.
|
|
26452
|
+
*/
|
|
26453
|
+
buildScreenshotKey(session, stepId, commandId, phase) {
|
|
26454
|
+
return `${this.dataContext?.projectId ?? ""}/${session.reportId}/testcases/${session.testCaseId}/resources/screenshots/${stepId}-${commandId}-${phase}.jpg`;
|
|
26455
|
+
}
|
|
26456
|
+
getScreenshotTempDir(session) {
|
|
26457
|
+
if (!this.tempPathForAssets) this.tempPathForAssets = join("/tmp", `blinq-screenshots-${session.reportId}-${session.testCaseId}`);
|
|
26458
|
+
return this.tempPathForAssets;
|
|
26459
|
+
}
|
|
26460
|
+
/**
|
|
26461
|
+
* Captures a viewport JPEG of the active page for the given command/phase,
|
|
26462
|
+
* writes it to the per-testcase temp dir, and records it for later upload.
|
|
26463
|
+
* Best-effort: any failure (no page, screenshot error) is logged and yields
|
|
26464
|
+
* `undefined` so command execution is never affected.
|
|
26465
|
+
*/
|
|
26466
|
+
async captureCommandScreenshot(session, phase, stepId, commandId) {
|
|
26467
|
+
if (!this.screenshotsEnabled(session) || !this.isRunSession(session)) return;
|
|
26468
|
+
const page = this.lastActivePage;
|
|
26469
|
+
if (!page) {
|
|
26470
|
+
this.obs.metrics.count("bvt_agent.screenshots.capture.skipped.total", 1, {
|
|
26471
|
+
phase,
|
|
26472
|
+
reason: "no-active-page"
|
|
26473
|
+
});
|
|
26474
|
+
this.obs.logger.warn("Skipping command screenshot capture: no active page", {
|
|
26475
|
+
stepId,
|
|
26476
|
+
commandId,
|
|
26477
|
+
phase
|
|
26478
|
+
});
|
|
26479
|
+
return;
|
|
26480
|
+
}
|
|
26481
|
+
const startMs = Date.now();
|
|
26482
|
+
try {
|
|
26483
|
+
const key = this.buildScreenshotKey(session, stepId, commandId, phase);
|
|
26484
|
+
const dir = this.getScreenshotTempDir(session);
|
|
26485
|
+
mkdirSync(dir, { recursive: true });
|
|
26486
|
+
const localPath = join(dir, `${stepId}-${commandId}-${phase}.jpg`);
|
|
26487
|
+
await page.screenshot({
|
|
26488
|
+
path: localPath,
|
|
26489
|
+
type: "jpeg",
|
|
26490
|
+
quality: 70
|
|
26491
|
+
});
|
|
26492
|
+
this.capturedScreenshots.push({
|
|
26493
|
+
stepId,
|
|
26494
|
+
commandId,
|
|
26495
|
+
phase,
|
|
26496
|
+
localPath
|
|
26497
|
+
});
|
|
26498
|
+
this.obs.metrics.count("bvt_agent.screenshots.capture.succeeded.total", 1, { phase });
|
|
26499
|
+
this.obs.metrics.latency("bvt_agent.screenshots.capture.duration", startMs, { phase });
|
|
26500
|
+
this.obs.logger.log("Captured command screenshot", {
|
|
26501
|
+
stepId,
|
|
26502
|
+
commandId,
|
|
26503
|
+
phase,
|
|
26504
|
+
key
|
|
26505
|
+
});
|
|
26506
|
+
return key;
|
|
26507
|
+
} catch (error) {
|
|
26508
|
+
this.screenshotCaptureFailures += 1;
|
|
26509
|
+
this.obs.metrics.count("bvt_agent.screenshots.capture.failed.total", 1, { phase });
|
|
26510
|
+
this.obs.logger.warn("Command screenshot capture failed", {
|
|
26511
|
+
stepId,
|
|
26512
|
+
commandId,
|
|
26513
|
+
phase,
|
|
26514
|
+
error: this.summarizeErrorForExecutionLog(error)
|
|
26515
|
+
});
|
|
26516
|
+
return;
|
|
26517
|
+
}
|
|
26518
|
+
}
|
|
26519
|
+
toScreenshotRefs(before, after) {
|
|
26520
|
+
if (!before && !after) return;
|
|
26521
|
+
return {
|
|
26522
|
+
...before ? { before } : {},
|
|
26523
|
+
...after ? { after } : {}
|
|
26524
|
+
};
|
|
26525
|
+
}
|
|
26391
26526
|
async onCommandStart(command, stepDefinitionId, session) {
|
|
26392
26527
|
this.obs.logger.log("Starting execution of command", {
|
|
26393
26528
|
commandId: command._id,
|
|
@@ -26454,10 +26589,11 @@ var Tester = class {
|
|
|
26454
26589
|
completedAt,
|
|
26455
26590
|
result: { type: "success" },
|
|
26456
26591
|
...typeof metadata?.resolvedChosenSelectorIndex === "number" ? { resolvedChosenSelectorIndex: metadata.resolvedChosenSelectorIndex } : {},
|
|
26457
|
-
...metadata?.recovery ? { recovery: metadata.recovery } : {}
|
|
26592
|
+
...metadata?.recovery ? { recovery: metadata.recovery } : {},
|
|
26593
|
+
...metadata?.screenshots ? { screenshots: metadata.screenshots } : {}
|
|
26458
26594
|
};
|
|
26459
26595
|
}
|
|
26460
|
-
async onCommandFail(command, stepDefinitionId, error, session, recovery) {
|
|
26596
|
+
async onCommandFail(command, stepDefinitionId, error, session, recovery, screenshots) {
|
|
26461
26597
|
this.obs.logger.error(`Error executing command ${command._id}:`, { error: this.summarizeErrorForExecutionLog(error) });
|
|
26462
26598
|
if (session?.type === "run") {
|
|
26463
26599
|
this.obs.logger.log(`Ending Playwright tracing group for command: ${command._id}`);
|
|
@@ -26474,7 +26610,8 @@ var Tester = class {
|
|
|
26474
26610
|
stepDefinitionId,
|
|
26475
26611
|
completedAt,
|
|
26476
26612
|
result: this.toFailureResult(error),
|
|
26477
|
-
recovery
|
|
26613
|
+
recovery,
|
|
26614
|
+
...screenshots ? { screenshots } : {}
|
|
26478
26615
|
};
|
|
26479
26616
|
}
|
|
26480
26617
|
async onStepStart(step, session) {
|
|
@@ -26527,6 +26664,10 @@ var Tester = class {
|
|
|
26527
26664
|
}
|
|
26528
26665
|
async onTestCaseStart(session) {
|
|
26529
26666
|
this.obs.logger.log(`Starting test case execution with session: ${JSON.stringify(session)}`);
|
|
26667
|
+
this.capturedScreenshots = [];
|
|
26668
|
+
this.tempPathForAssets = "";
|
|
26669
|
+
this.screenshotCommandsConsidered = 0;
|
|
26670
|
+
this.screenshotCaptureFailures = 0;
|
|
26530
26671
|
if (this.isRunSession(session)) {
|
|
26531
26672
|
if (session.token) this.sessionToken = session.token;
|
|
26532
26673
|
this.obs.logger.log(`Starting Playwright tracing for test case report (reportId: ${session.reportId}, testCaseId: ${session.testCaseId})...`);
|
|
@@ -26551,6 +26692,178 @@ var Tester = class {
|
|
|
26551
26692
|
shouldAttemptAiRecovery(session) {
|
|
26552
26693
|
return !session || this.isRecordingReplaySession(session) || session.type === "run" && session.runWithAiRecovery === true;
|
|
26553
26694
|
}
|
|
26695
|
+
isNewTourMaintenanceRecoverySession(session) {
|
|
26696
|
+
return session?.type === "run" && session.runWithAiRecovery === true && session.deterministicRecoveryMode === NEW_TOUR_MAINTENANCE_RECOVERY_MODE;
|
|
26697
|
+
}
|
|
26698
|
+
shouldUseNewTourMaintenanceRecovery(input) {
|
|
26699
|
+
if (!this.isNewTourMaintenanceRecoverySession(input.session)) return false;
|
|
26700
|
+
if (input.command.type !== "element.action" || input.commandIndex !== 0) return false;
|
|
26701
|
+
const stepText = `${input.recorderStep.step.text} ${input.recorderStep.definition.displayName ?? ""}`;
|
|
26702
|
+
return /gender/i.test(stepText) || /gender/i.test(input.command.target.name ?? "") || input.command.target.uniqueSelectors.some((selector) => selector.type === "pw.selectorString" ? /gender/i.test(selector.selectorString) : false);
|
|
26703
|
+
}
|
|
26704
|
+
inferNewTourGenderOption(recorderStep, failedCommandIndex) {
|
|
26705
|
+
const nextCommand = recorderStep.definition.commands[failedCommandIndex + 1];
|
|
26706
|
+
if (nextCommand?.type === "element.action" && (nextCommand.target.name === "Male" || nextCommand.target.name === "Female")) return nextCommand.target.name;
|
|
26707
|
+
return "Male";
|
|
26708
|
+
}
|
|
26709
|
+
buildNewTourGenderRadioCommand(input) {
|
|
26710
|
+
const optionSlug = input.option.toLowerCase();
|
|
26711
|
+
return {
|
|
26712
|
+
...input.originalCommand,
|
|
26713
|
+
_id: ulid(),
|
|
26714
|
+
data: {
|
|
26715
|
+
type: "click",
|
|
26716
|
+
options: {
|
|
26717
|
+
force: true,
|
|
26718
|
+
timeout: 15e3
|
|
26719
|
+
}
|
|
26720
|
+
},
|
|
26721
|
+
target: {
|
|
26722
|
+
...input.originalCommand.target,
|
|
26723
|
+
name: input.option,
|
|
26724
|
+
chosenSelectorIndex: 0,
|
|
26725
|
+
uniqueSelectors: [
|
|
26726
|
+
{
|
|
26727
|
+
type: "pw.selectorString",
|
|
26728
|
+
selectorString: `internal:label="${input.option}"s`,
|
|
26729
|
+
strategy: "text",
|
|
26730
|
+
description: `The ${input.option} radio option`
|
|
26731
|
+
},
|
|
26732
|
+
{
|
|
26733
|
+
type: "pw.selectorString",
|
|
26734
|
+
selectorString: `internal:role=radio[name="${input.option}"s]`,
|
|
26735
|
+
strategy: "label",
|
|
26736
|
+
description: `The radio button labeled "${input.option}"`
|
|
26737
|
+
},
|
|
26738
|
+
{
|
|
26739
|
+
type: "pw.selectorString",
|
|
26740
|
+
selectorString: `internal:testid=[data-testid="register-gender-${optionSlug}"s]`,
|
|
26741
|
+
strategy: "css",
|
|
26742
|
+
description: `The ${input.option} gender option`
|
|
26743
|
+
},
|
|
26744
|
+
{
|
|
26745
|
+
type: "pw.selectorString",
|
|
26746
|
+
selectorString: `input[type="radio"][value="${optionSlug}"]`,
|
|
26747
|
+
strategy: "css",
|
|
26748
|
+
description: `The ${input.option} gender radio input`
|
|
26749
|
+
}
|
|
26750
|
+
]
|
|
26751
|
+
}
|
|
26752
|
+
};
|
|
26753
|
+
}
|
|
26754
|
+
buildNewTourGenderRepairPlan(input) {
|
|
26755
|
+
const nextCommand = input.recorderStep.definition.commands[input.failedCommandIndex + 1];
|
|
26756
|
+
if (!(nextCommand?.type === "element.action" && (nextCommand.target.name === "Male" || nextCommand.target.name === "Female"))) return {
|
|
26757
|
+
type: "single-command",
|
|
26758
|
+
commandIndex: input.failedCommandIndex,
|
|
26759
|
+
replacementCommand: input.replacementCommand,
|
|
26760
|
+
preserveSuffix: true
|
|
26761
|
+
};
|
|
26762
|
+
return {
|
|
26763
|
+
type: "command-operations",
|
|
26764
|
+
operations: [{
|
|
26765
|
+
type: "update",
|
|
26766
|
+
commandIndex: input.failedCommandIndex,
|
|
26767
|
+
replacementCommand: input.replacementCommand
|
|
26768
|
+
}, {
|
|
26769
|
+
type: "delete",
|
|
26770
|
+
commandIndex: input.failedCommandIndex + 1
|
|
26771
|
+
}],
|
|
26772
|
+
preserveSuffix: true
|
|
26773
|
+
};
|
|
26774
|
+
}
|
|
26775
|
+
async *recoverNewTourMaintenanceGenderCommand(input) {
|
|
26776
|
+
const recoveryAttemptId = crypto.randomUUID();
|
|
26777
|
+
const { recorderStep, command, commandIndex, commandContext, executionError, options } = input;
|
|
26778
|
+
const option = this.inferNewTourGenderOption(recorderStep, commandIndex);
|
|
26779
|
+
const replacementCommand = this.buildNewTourGenderRadioCommand({
|
|
26780
|
+
originalCommand: command,
|
|
26781
|
+
option
|
|
26782
|
+
});
|
|
26783
|
+
yield {
|
|
26784
|
+
type: "recovery_status",
|
|
26785
|
+
recoveryAttemptId,
|
|
26786
|
+
stepId: recorderStep.step._id,
|
|
26787
|
+
commandId: command._id,
|
|
26788
|
+
phase: "analyzing",
|
|
26789
|
+
message: "Analyzing failure",
|
|
26790
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
26791
|
+
};
|
|
26792
|
+
await delayNewTourMaintenanceRecovery(options.abortController?.signal);
|
|
26793
|
+
yield {
|
|
26794
|
+
type: "recovery_status",
|
|
26795
|
+
recoveryAttemptId,
|
|
26796
|
+
stepId: recorderStep.step._id,
|
|
26797
|
+
commandId: command._id,
|
|
26798
|
+
phase: "trying_fix",
|
|
26799
|
+
message: "AI Test Engineer is rewriting the gender selection step",
|
|
26800
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
26801
|
+
};
|
|
26802
|
+
await delayNewTourMaintenanceRecovery(options.abortController?.signal);
|
|
26803
|
+
yield {
|
|
26804
|
+
type: "recovery_status",
|
|
26805
|
+
recoveryAttemptId,
|
|
26806
|
+
stepId: recorderStep.step._id,
|
|
26807
|
+
commandId: command._id,
|
|
26808
|
+
phase: "checking_fix",
|
|
26809
|
+
message: "AI Test Engineer is checking the fix",
|
|
26810
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
26811
|
+
};
|
|
26812
|
+
await this.executeCommand({
|
|
26813
|
+
page: this.lastActivePage,
|
|
26814
|
+
command: replacementCommand,
|
|
26815
|
+
context: commandContext
|
|
26816
|
+
});
|
|
26817
|
+
const persistResult = persistStepRepair({
|
|
26818
|
+
stepDefinition: recorderStep.definition,
|
|
26819
|
+
plan: this.buildNewTourGenderRepairPlan({
|
|
26820
|
+
recorderStep,
|
|
26821
|
+
failedCommandIndex: commandIndex,
|
|
26822
|
+
replacementCommand
|
|
26823
|
+
})
|
|
26824
|
+
});
|
|
26825
|
+
if (!persistResult.success) throw new Error(persistResult.error);
|
|
26826
|
+
const recoveryMeta = {
|
|
26827
|
+
recoveryAttemptId,
|
|
26828
|
+
originalCommandId: command._id,
|
|
26829
|
+
stepId: recorderStep.step._id,
|
|
26830
|
+
originalErrorMessage: executionError.message,
|
|
26831
|
+
originalErrorStack: executionError.stack,
|
|
26832
|
+
rcaLabel: "demo-gender-control-changed",
|
|
26833
|
+
rcaConfidence: 1,
|
|
26834
|
+
rootCauseAnalysis: NEW_TOUR_MAINTENANCE_RECOVERY_ARTIFACT.rootCauseAnalysis,
|
|
26835
|
+
evidenceSignalNames: ["new_tour_maintenance_gender_radio_demo"],
|
|
26836
|
+
targetElementSnapshot: {
|
|
26837
|
+
accessibleName: option,
|
|
26838
|
+
role: "radio",
|
|
26839
|
+
text: option,
|
|
26840
|
+
labelText: option
|
|
26841
|
+
},
|
|
26842
|
+
actionVerb: "click",
|
|
26843
|
+
commandPatchSummary: "Updated the gender selection step from the old dropdown to the new radio button UI",
|
|
26844
|
+
retryOutcome: "passed",
|
|
26845
|
+
recoveredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26846
|
+
healedLocatorPersistedAt: persistResult.persistedAt,
|
|
26847
|
+
stepDefinitionPatch: {
|
|
26848
|
+
stepDefinitionId: recorderStep.definition._id,
|
|
26849
|
+
commands: persistResult.commands,
|
|
26850
|
+
changedCommandIds: persistResult.changedCommandIds,
|
|
26851
|
+
preservedCommandIds: persistResult.preservedCommandIds,
|
|
26852
|
+
persistedAt: persistResult.persistedAt
|
|
26853
|
+
}
|
|
26854
|
+
};
|
|
26855
|
+
yield {
|
|
26856
|
+
type: "recovery_status",
|
|
26857
|
+
recoveryAttemptId,
|
|
26858
|
+
stepId: recorderStep.step._id,
|
|
26859
|
+
commandId: command._id,
|
|
26860
|
+
phase: "fixed",
|
|
26861
|
+
message: "AI recovered the gender selection step",
|
|
26862
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26863
|
+
artifact: NEW_TOUR_MAINTENANCE_RECOVERY_ARTIFACT
|
|
26864
|
+
};
|
|
26865
|
+
return recoveryMeta;
|
|
26866
|
+
}
|
|
26554
26867
|
isSupportedRepairContext(decision, session) {
|
|
26555
26868
|
if (decision.kind !== "step-repair") return false;
|
|
26556
26869
|
if (this.isRecordingReplaySession(session) || !session) return decision.repairContext === "recorder-replay";
|
|
@@ -26633,12 +26946,12 @@ var Tester = class {
|
|
|
26633
26946
|
setTimeout(resolve, ms);
|
|
26634
26947
|
});
|
|
26635
26948
|
}
|
|
26636
|
-
async uploadFile(presignedUrl, filePath) {
|
|
26949
|
+
async uploadFile(presignedUrl, filePath, contentType = "application/zip") {
|
|
26637
26950
|
const fileData = readFileSync$1(filePath);
|
|
26638
26951
|
this.obs.logger.log(`Uploading file from ${filePath} (size=${fileData.byteLength} bytes) via PUT`);
|
|
26639
26952
|
const response = await fetch(presignedUrl, {
|
|
26640
26953
|
method: "PUT",
|
|
26641
|
-
headers: { "Content-Type":
|
|
26954
|
+
headers: { "Content-Type": contentType },
|
|
26642
26955
|
body: fileData
|
|
26643
26956
|
});
|
|
26644
26957
|
if (!response.ok) throw new Error(`Failed to upload file. Status: ${response.status}: ${await response.text()}`);
|
|
@@ -26668,6 +26981,146 @@ var Tester = class {
|
|
|
26668
26981
|
}
|
|
26669
26982
|
return false;
|
|
26670
26983
|
}
|
|
26984
|
+
async putFileWithRetries(input) {
|
|
26985
|
+
const maxAttempts = 3;
|
|
26986
|
+
const baseDelayMs = 250;
|
|
26987
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
26988
|
+
await this.uploadFile(input.presignedUrl, input.filePath, input.contentType);
|
|
26989
|
+
return true;
|
|
26990
|
+
} catch (error) {
|
|
26991
|
+
if (attempt === maxAttempts) {
|
|
26992
|
+
this.obs.logger.error(`${input.label} upload failed after ${maxAttempts} attempts`, error);
|
|
26993
|
+
return false;
|
|
26994
|
+
}
|
|
26995
|
+
const delayMs = baseDelayMs * 2 ** (attempt - 1);
|
|
26996
|
+
this.obs.logger.warn(`${input.label} upload failed, retrying`, {
|
|
26997
|
+
attempt,
|
|
26998
|
+
maxAttempts,
|
|
26999
|
+
delayMs,
|
|
27000
|
+
error: this.summarizeErrorForExecutionLog(error)
|
|
27001
|
+
});
|
|
27002
|
+
await this.wait(delayMs);
|
|
27003
|
+
}
|
|
27004
|
+
return false;
|
|
27005
|
+
}
|
|
27006
|
+
cleanupScreenshotTempDir() {
|
|
27007
|
+
if (!this.tempPathForAssets) return;
|
|
27008
|
+
try {
|
|
27009
|
+
rmSync(this.tempPathForAssets, {
|
|
27010
|
+
recursive: true,
|
|
27011
|
+
force: true
|
|
27012
|
+
});
|
|
27013
|
+
} catch (error) {
|
|
27014
|
+
this.obs.logger.warn("Failed to clean up screenshot temp dir", {
|
|
27015
|
+
dir: this.tempPathForAssets,
|
|
27016
|
+
error: this.summarizeErrorForExecutionLog(error)
|
|
27017
|
+
});
|
|
27018
|
+
} finally {
|
|
27019
|
+
this.tempPathForAssets = "";
|
|
27020
|
+
}
|
|
27021
|
+
}
|
|
27022
|
+
/**
|
|
27023
|
+
* Batch-uploads all command screenshots captured during the run to S3.
|
|
27024
|
+
* Fetches a presigned PUT URL per file in a single round-trip, then uploads
|
|
27025
|
+
* each with retries. Best-effort: returns true when at least one screenshot
|
|
27026
|
+
* was uploaded, false otherwise (or when there is nothing/no token). Always
|
|
27027
|
+
* cleans up the on-disk temp files. Never throws.
|
|
27028
|
+
*/
|
|
27029
|
+
async uploadScreenshots(session) {
|
|
27030
|
+
const screenshots = this.capturedScreenshots;
|
|
27031
|
+
const capturedCount = screenshots.length;
|
|
27032
|
+
const distinctCommandsCaptured = new Set(screenshots.map((shot) => shot.commandId)).size;
|
|
27033
|
+
const commandsConsidered = this.screenshotCommandsConsidered;
|
|
27034
|
+
const captureFailures = this.screenshotCaptureFailures;
|
|
27035
|
+
this.obs.metrics.gauge("bvt_agent.screenshots.commands_considered", commandsConsidered);
|
|
27036
|
+
this.obs.metrics.gauge("bvt_agent.screenshots.captured", capturedCount);
|
|
27037
|
+
this.obs.metrics.gauge("bvt_agent.screenshots.distinct_commands_captured", distinctCommandsCaptured);
|
|
27038
|
+
if (capturedCount === 0) {
|
|
27039
|
+
this.obs.logger.log("No command screenshots captured; skipping upload", {
|
|
27040
|
+
reportId: session.reportId,
|
|
27041
|
+
testCaseId: session.testCaseId,
|
|
27042
|
+
commandsConsidered,
|
|
27043
|
+
captureFailures
|
|
27044
|
+
});
|
|
27045
|
+
return false;
|
|
27046
|
+
}
|
|
27047
|
+
if (!session.token) {
|
|
27048
|
+
this.obs.metrics.count("bvt_agent.screenshots.upload.skipped.total", 1, { reason: "no-session-token" });
|
|
27049
|
+
this.obs.logger.warn(`Screenshot upload skipped because no session token was provided for reportId: ${session.reportId}, testCaseId: ${session.testCaseId}.`);
|
|
27050
|
+
this.cleanupScreenshotTempDir();
|
|
27051
|
+
this.capturedScreenshots = [];
|
|
27052
|
+
return false;
|
|
27053
|
+
}
|
|
27054
|
+
const uploadStartMs = Date.now();
|
|
27055
|
+
try {
|
|
27056
|
+
this.obs.logger.log(`Requesting ${screenshots.length} presigned URL(s) for screenshot upload`, {
|
|
27057
|
+
reportId: session.reportId,
|
|
27058
|
+
testCaseId: session.testCaseId
|
|
27059
|
+
});
|
|
27060
|
+
const targets = await this.getAPIClient(this.sessionToken).presignedUrls.getPresignedUrlsForScreenshotUpload.query({
|
|
27061
|
+
reportId: session.reportId,
|
|
27062
|
+
testCaseId: session.testCaseId,
|
|
27063
|
+
projectId: this.dataContext?.projectId ?? "",
|
|
27064
|
+
files: screenshots.map(({ stepId, commandId, phase }) => ({
|
|
27065
|
+
stepId,
|
|
27066
|
+
commandId,
|
|
27067
|
+
phase
|
|
27068
|
+
}))
|
|
27069
|
+
});
|
|
27070
|
+
const urlByTargetKey = new Map(targets.map((target) => [`${target.stepId}-${target.commandId}-${target.phase}`, target.url]));
|
|
27071
|
+
let uploadedCount = 0;
|
|
27072
|
+
let failedCount = 0;
|
|
27073
|
+
for (const shot of screenshots) {
|
|
27074
|
+
const presignedUrl = urlByTargetKey.get(`${shot.stepId}-${shot.commandId}-${shot.phase}`);
|
|
27075
|
+
if (!presignedUrl) {
|
|
27076
|
+
failedCount += 1;
|
|
27077
|
+
this.obs.metrics.count("bvt_agent.screenshots.upload.failed.total", 1, { reason: "no-presigned-url" });
|
|
27078
|
+
this.obs.logger.warn("No presigned URL returned for screenshot", {
|
|
27079
|
+
stepId: shot.stepId,
|
|
27080
|
+
commandId: shot.commandId,
|
|
27081
|
+
phase: shot.phase
|
|
27082
|
+
});
|
|
27083
|
+
continue;
|
|
27084
|
+
}
|
|
27085
|
+
if (await this.putFileWithRetries({
|
|
27086
|
+
presignedUrl,
|
|
27087
|
+
filePath: shot.localPath,
|
|
27088
|
+
contentType: "image/jpeg",
|
|
27089
|
+
label: "Screenshot"
|
|
27090
|
+
})) {
|
|
27091
|
+
uploadedCount += 1;
|
|
27092
|
+
this.obs.metrics.count("bvt_agent.screenshots.upload.succeeded.total", 1, { phase: shot.phase });
|
|
27093
|
+
} else {
|
|
27094
|
+
failedCount += 1;
|
|
27095
|
+
this.obs.metrics.count("bvt_agent.screenshots.upload.failed.total", 1, { reason: "put-failed" });
|
|
27096
|
+
}
|
|
27097
|
+
}
|
|
27098
|
+
this.obs.metrics.latency("bvt_agent.screenshots.upload.duration", uploadStartMs, { outcome: uploadedCount > 0 ? "success" : "failure" });
|
|
27099
|
+
this.obs.logger.log(`Screenshot upload finished: ${uploadedCount}/${capturedCount} uploaded, ${failedCount} failed`, {
|
|
27100
|
+
reportId: session.reportId,
|
|
27101
|
+
testCaseId: session.testCaseId,
|
|
27102
|
+
commandsConsidered,
|
|
27103
|
+
distinctCommandsCaptured,
|
|
27104
|
+
capturedCount,
|
|
27105
|
+
uploadedCount,
|
|
27106
|
+
failedCount,
|
|
27107
|
+
captureFailures
|
|
27108
|
+
});
|
|
27109
|
+
return uploadedCount > 0;
|
|
27110
|
+
} catch (error) {
|
|
27111
|
+
this.obs.metrics.count("bvt_agent.screenshots.upload.failed.total", capturedCount, { reason: "batch-error" });
|
|
27112
|
+
this.obs.metrics.latency("bvt_agent.screenshots.upload.duration", uploadStartMs, { outcome: "error" });
|
|
27113
|
+
this.obs.logger.warn(`Screenshot upload failed for reportId: ${session.reportId}, testCaseId: ${session.testCaseId}. Continuing without failing execution result.`, {
|
|
27114
|
+
error: this.summarizeErrorForExecutionLog(error),
|
|
27115
|
+
capturedCount,
|
|
27116
|
+
commandsConsidered
|
|
27117
|
+
});
|
|
27118
|
+
return false;
|
|
27119
|
+
} finally {
|
|
27120
|
+
this.cleanupScreenshotTempDir();
|
|
27121
|
+
this.capturedScreenshots = [];
|
|
27122
|
+
}
|
|
27123
|
+
}
|
|
26671
27124
|
async onTestCaseFail(error, session) {
|
|
26672
27125
|
this.obs.logger.error("Test case execution failed", {
|
|
26673
27126
|
session: this.summarizeSessionForExecutionLog(session),
|
|
@@ -26772,8 +27225,11 @@ var Tester = class {
|
|
|
26772
27225
|
commandId: command._id,
|
|
26773
27226
|
commandType: command.type
|
|
26774
27227
|
});
|
|
27228
|
+
let beforeScreenshotKey;
|
|
27229
|
+
if (this.screenshotsEnabled(session)) this.screenshotCommandsConsidered += 1;
|
|
26775
27230
|
try {
|
|
26776
27231
|
yield await this.onCommandStart(command, stepDefinitionId, session);
|
|
27232
|
+
beforeScreenshotKey = await this.captureCommandScreenshot(session, "before", recorderStep.step._id, command._id);
|
|
26777
27233
|
const commandResult = await this.executeCommand({
|
|
26778
27234
|
page: this.lastActivePage,
|
|
26779
27235
|
command,
|
|
@@ -26781,12 +27237,34 @@ var Tester = class {
|
|
|
26781
27237
|
});
|
|
26782
27238
|
if ((command.type === "custom" || command.type === "custom.code") && commandResult && isFailedCustomCommandResult(commandResult)) throw new Error(commandResult.error || `Custom command "${command._id}" failed.`);
|
|
26783
27239
|
this.obs.logger.log(`Finished executing command: ${command}`);
|
|
26784
|
-
|
|
27240
|
+
const afterScreenshotKey = await this.captureCommandScreenshot(session, "after", recorderStep.step._id, command._id);
|
|
27241
|
+
yield await this.onCommandPass(command, stepDefinitionId, session, {
|
|
27242
|
+
resolvedChosenSelectorIndex: getResolvedChosenSelectorIndex(commandResult),
|
|
27243
|
+
screenshots: this.toScreenshotRefs(beforeScreenshotKey, afterScreenshotKey)
|
|
27244
|
+
});
|
|
26785
27245
|
} catch (error) {
|
|
26786
27246
|
const executionError = this.toExecutionError(error);
|
|
26787
27247
|
let terminalExecutionError = executionError;
|
|
26788
27248
|
let failedRecoveryMeta;
|
|
26789
27249
|
if (this.isBrowserDerivedCommand(command)) {
|
|
27250
|
+
if (command.type === "element.action" && this.shouldUseNewTourMaintenanceRecovery({
|
|
27251
|
+
session,
|
|
27252
|
+
recorderStep,
|
|
27253
|
+
command,
|
|
27254
|
+
commandIndex
|
|
27255
|
+
})) {
|
|
27256
|
+
const recoveryMeta = yield* this.recoverNewTourMaintenanceGenderCommand({
|
|
27257
|
+
recorderStep,
|
|
27258
|
+
command,
|
|
27259
|
+
commandIndex,
|
|
27260
|
+
commandContext,
|
|
27261
|
+
executionError,
|
|
27262
|
+
options
|
|
27263
|
+
});
|
|
27264
|
+
stepRecovery = recoveryMeta;
|
|
27265
|
+
yield await this.onCommandPass(command, stepDefinitionId, session, { recovery: recoveryMeta });
|
|
27266
|
+
continue;
|
|
27267
|
+
}
|
|
26790
27268
|
if (this.shouldAttemptAiRecovery(session)) yield {
|
|
26791
27269
|
type: "recovery_status",
|
|
26792
27270
|
recoveryAttemptId: crypto.randomUUID(),
|
|
@@ -26909,7 +27387,10 @@ var Tester = class {
|
|
|
26909
27387
|
};
|
|
26910
27388
|
emittedTerminalRecoveryStatus = true;
|
|
26911
27389
|
this.obs.logger.log(`Recovered command ${command._id} with whole-step repair`);
|
|
26912
|
-
yield await this.onCommandPass(command, stepDefinitionId, session, {
|
|
27390
|
+
yield await this.onCommandPass(command, stepDefinitionId, session, {
|
|
27391
|
+
recovery: recoveryMeta,
|
|
27392
|
+
screenshots: this.toScreenshotRefs(beforeScreenshotKey, await this.captureCommandScreenshot(session, "after", recorderStep.step._id, command._id))
|
|
27393
|
+
});
|
|
26913
27394
|
commandIndex = recorderStep.definition.commands.length - 1;
|
|
26914
27395
|
continue;
|
|
26915
27396
|
} catch (retryError) {
|
|
@@ -27048,7 +27529,10 @@ var Tester = class {
|
|
|
27048
27529
|
};
|
|
27049
27530
|
emittedTerminalRecoveryStatus = true;
|
|
27050
27531
|
this.obs.logger.log(`Recovered command ${command._id} with step repair`);
|
|
27051
|
-
yield await this.onCommandPass(command, stepDefinitionId, session, {
|
|
27532
|
+
yield await this.onCommandPass(command, stepDefinitionId, session, {
|
|
27533
|
+
recovery: recoveryMeta,
|
|
27534
|
+
screenshots: this.toScreenshotRefs(beforeScreenshotKey, await this.captureCommandScreenshot(session, "after", recorderStep.step._id, command._id))
|
|
27535
|
+
});
|
|
27052
27536
|
if (decision.stepRepairPlan.type === "command-operations" || decision.stepRepairPlan.preserveSuffix) commandIndex = recorderStep.definition.commands.length - 1;
|
|
27053
27537
|
continue;
|
|
27054
27538
|
} catch (retryError) {
|
|
@@ -27122,7 +27606,8 @@ var Tester = class {
|
|
|
27122
27606
|
evidenceSignalNames: decision.evidenceSignalNames
|
|
27123
27607
|
};
|
|
27124
27608
|
}
|
|
27125
|
-
|
|
27609
|
+
const afterScreenshotKey = await this.captureCommandScreenshot(session, "after", recorderStep.step._id, command._id);
|
|
27610
|
+
yield await this.onCommandFail(command, stepDefinitionId, terminalExecutionError, session, failedRecoveryMeta, this.toScreenshotRefs(beforeScreenshotKey, afterScreenshotKey));
|
|
27126
27611
|
throw terminalExecutionError;
|
|
27127
27612
|
}
|
|
27128
27613
|
}
|
|
@@ -27288,7 +27773,7 @@ var Tester = class {
|
|
|
27288
27773
|
if (!frameElementDescriptors || frameElementDescriptors.length === 0) return scope.mainFrame();
|
|
27289
27774
|
let currentFrame = scope.mainFrame();
|
|
27290
27775
|
for (const frameDescriptor of frameElementDescriptors) for (const selectorInfo of frameDescriptor.uniqueSelectors) {
|
|
27291
|
-
const { target } = this.getLocator(currentFrame, selectorInfo);
|
|
27776
|
+
const { target } = this.getLocator(currentFrame, selectorInfo, { isElementHidden: false });
|
|
27292
27777
|
try {
|
|
27293
27778
|
if (!await target.elementHandle()) throw new Error(`Could not find element for frame selector: ${JSON.stringify(selectorInfo)}`);
|
|
27294
27779
|
const selector = target._selector;
|
|
@@ -27300,9 +27785,14 @@ var Tester = class {
|
|
|
27300
27785
|
}
|
|
27301
27786
|
return currentFrame;
|
|
27302
27787
|
}
|
|
27303
|
-
getLocator(scope, uniqueSelector) {
|
|
27788
|
+
getLocator(scope, uniqueSelector, options) {
|
|
27304
27789
|
switch (uniqueSelector.type) {
|
|
27305
|
-
case "pw.selectorString":
|
|
27790
|
+
case "pw.selectorString": {
|
|
27791
|
+
const visibleSuffix = options?.isElementHidden === true ? "" : ` >> visible=true`;
|
|
27792
|
+
const finalSelector = uniqueSelector.selectorString + visibleSuffix;
|
|
27793
|
+
this.obs.logger.info(`getLocator(pw.selectorString): isElementHidden=${options?.isElementHidden}, visibleAppended=${visibleSuffix !== ""}, finalSelector=${finalSelector}`);
|
|
27794
|
+
return { target: scope.locator(finalSelector) };
|
|
27795
|
+
}
|
|
27306
27796
|
case "bvt.selectorObject": throw new Error(`Selector type "bvt.selectorObject" is not yet supported in getLocator`);
|
|
27307
27797
|
default:
|
|
27308
27798
|
const _exhaustiveCheck = uniqueSelector;
|
|
@@ -27683,7 +28173,7 @@ var Tester = class {
|
|
|
27683
28173
|
const selectorInfo = target.uniqueSelectors[chosenSelectorIndex];
|
|
27684
28174
|
if (!selectorInfo) throw new Error(`No selector found for target "${target.name}" at chosenSelectorIndex ${chosenSelectorIndex}. Available selectors: ${JSON.stringify(target.uniqueSelectors)}`);
|
|
27685
28175
|
const frame = await this.getFrame(input.page, target.frames);
|
|
27686
|
-
const locator = this.getLocator(frame, selectorInfo);
|
|
28176
|
+
const locator = this.getLocator(frame, selectorInfo, target);
|
|
27687
28177
|
try {
|
|
27688
28178
|
const dataWithDefaultTimeout = {
|
|
27689
28179
|
...data,
|
|
@@ -27704,7 +28194,7 @@ var Tester = class {
|
|
|
27704
28194
|
for (let i = 0; i < target.uniqueSelectors.length; i++) {
|
|
27705
28195
|
if (i === chosenSelectorIndex) continue;
|
|
27706
28196
|
const selectorInfo = target.uniqueSelectors[i];
|
|
27707
|
-
const locator = this.getLocator(frame, selectorInfo);
|
|
28197
|
+
const locator = this.getLocator(frame, selectorInfo, target);
|
|
27708
28198
|
try {
|
|
27709
28199
|
this.obs.logger.info(`Retrying action "${data.type}" using next selector: ${JSON.stringify(selectorInfo)}`);
|
|
27710
28200
|
const dataWithModifiedTimeout = {
|
|
@@ -27735,7 +28225,7 @@ var Tester = class {
|
|
|
27735
28225
|
const chosenSelectorIndex = target.chosenSelectorIndex ?? 0;
|
|
27736
28226
|
const selectorInfo = target.uniqueSelectors[chosenSelectorIndex];
|
|
27737
28227
|
const frame = await this.getFrame(input.page, target.frames);
|
|
27738
|
-
const locator = this.getLocator(frame, selectorInfo);
|
|
28228
|
+
const locator = this.getLocator(frame, selectorInfo, target);
|
|
27739
28229
|
try {
|
|
27740
28230
|
this.obs.logger.info(`Executing assertion "${data.type}" using selector: ${JSON.stringify(selectorInfo)}`);
|
|
27741
28231
|
const dataWithDefaultTimeout = {
|
|
@@ -27755,7 +28245,7 @@ var Tester = class {
|
|
|
27755
28245
|
for (let i = 0; i < target.uniqueSelectors.length; i++) {
|
|
27756
28246
|
if (i === chosenSelectorIndex) continue;
|
|
27757
28247
|
const selectorInfo = target.uniqueSelectors[i];
|
|
27758
|
-
const locator = this.getLocator(frame, selectorInfo);
|
|
28248
|
+
const locator = this.getLocator(frame, selectorInfo, target);
|
|
27759
28249
|
const dataWithModifiedTimeout = {
|
|
27760
28250
|
...data,
|
|
27761
28251
|
options: {
|
|
@@ -27788,7 +28278,7 @@ var Tester = class {
|
|
|
27788
28278
|
const selectorInfo = target.uniqueSelectors[chosenSelectorIndex];
|
|
27789
28279
|
if (!selectorInfo) throw new Error(`No selector found for target "${target.name}" at chosenSelectorIndex ${chosenSelectorIndex}. Available selectors: ${JSON.stringify(target.uniqueSelectors)}`);
|
|
27790
28280
|
const frame = await this.getFrame(input.page, target.frames);
|
|
27791
|
-
const locator = this.getLocator(frame, selectorInfo);
|
|
28281
|
+
const locator = this.getLocator(frame, selectorInfo, target);
|
|
27792
28282
|
try {
|
|
27793
28283
|
this.obs.logger.info(`Executing extraction "${extract.type}" using selector: ${JSON.stringify(selectorInfo)}`);
|
|
27794
28284
|
await this.executeElementExtraction(locator.target, extract, storageDetails);
|
|
@@ -27801,7 +28291,7 @@ var Tester = class {
|
|
|
27801
28291
|
for (let i = 0; i < target.uniqueSelectors.length; i++) {
|
|
27802
28292
|
if (i === chosenSelectorIndex) continue;
|
|
27803
28293
|
const fallbackSelectorInfo = target.uniqueSelectors[i];
|
|
27804
|
-
const fallbackLocator = this.getLocator(frame, fallbackSelectorInfo);
|
|
28294
|
+
const fallbackLocator = this.getLocator(frame, fallbackSelectorInfo, target);
|
|
27805
28295
|
try {
|
|
27806
28296
|
this.obs.logger.info(`Retrying extraction "${extract.type}" using next selector: ${JSON.stringify(fallbackSelectorInfo)}`);
|
|
27807
28297
|
await this.executeElementExtraction(fallbackLocator.target, extract, storageDetails);
|
|
@@ -28017,14 +28507,17 @@ var Tester = class {
|
|
|
28017
28507
|
for (const recorderStep of input.recorderSteps) yield* this.executeStep(recorderStep, input, options);
|
|
28018
28508
|
await options.onFinish?.();
|
|
28019
28509
|
this.obs.logger.log("Finished executing all steps");
|
|
28510
|
+
let screenshotsAvailable = false;
|
|
28020
28511
|
if (this.isRunSession(session)) {
|
|
28021
28512
|
this.obs.logger.log(`Stopping Playwright tracing for reportId: ${session.reportId}, testCaseId: ${session.testCaseId}...`);
|
|
28022
28513
|
await this.stopTracingAndHandleTrace(session);
|
|
28514
|
+
screenshotsAvailable = await this.uploadScreenshots(session);
|
|
28023
28515
|
}
|
|
28024
28516
|
this.obs.logger.log(`Test case execution completed successfully with session: ${JSON.stringify(session)}`);
|
|
28025
28517
|
return {
|
|
28026
28518
|
type: "execution_completed",
|
|
28027
|
-
result: { type: "success" }
|
|
28519
|
+
result: { type: "success" },
|
|
28520
|
+
...screenshotsAvailable ? { screenshotsAvailable: true } : {}
|
|
28028
28521
|
};
|
|
28029
28522
|
} catch (error) {
|
|
28030
28523
|
const executionError = this.toExecutionError(error);
|
|
@@ -28034,13 +28527,16 @@ var Tester = class {
|
|
|
28034
28527
|
session: this.summarizeSessionForExecutionLog(session),
|
|
28035
28528
|
error: this.summarizeErrorForExecutionLog(executionError)
|
|
28036
28529
|
});
|
|
28530
|
+
let screenshotsAvailable = false;
|
|
28037
28531
|
if (this.isRunSession(session)) {
|
|
28038
28532
|
this.obs.logger.log(`Stopping Playwright tracing for reportId: ${session.reportId}, testCaseId: ${session.testCaseId}...`);
|
|
28039
28533
|
await this.stopTracingAndHandleTrace(session);
|
|
28534
|
+
screenshotsAvailable = await this.uploadScreenshots(session);
|
|
28040
28535
|
}
|
|
28041
28536
|
return {
|
|
28042
28537
|
type: "execution_completed",
|
|
28043
|
-
result: this.toFailureResult(executionError)
|
|
28538
|
+
result: this.toFailureResult(executionError),
|
|
28539
|
+
...screenshotsAvailable ? { screenshotsAvailable: true } : {}
|
|
28044
28540
|
};
|
|
28045
28541
|
} finally {
|
|
28046
28542
|
this.obs.metrics.count("bvt_agent.test_case.execute.completed.total", 1, {
|