From 1a9b2c87193e6cff13872fddc6d5d488c81b696c Mon Sep 17 00:00:00 2001 From: Sameel Date: Sun, 9 Nov 2025 13:34:36 -0500 Subject: [PATCH 1/8] use custom errors --- packages/core/lib/v3/api.ts | 5 +- .../core/lib/v3/handlers/extractHandler.ts | 3 +- .../core/lib/v3/handlers/v3AgentHandler.ts | 5 +- .../core/lib/v3/types/public/sdkErrors.ts | 9 ++ packages/core/lib/v3/v3.ts | 82 +++++++++++-------- 5 files changed, 63 insertions(+), 41 deletions(-) diff --git a/packages/core/lib/v3/api.ts b/packages/core/lib/v3/api.ts index b8ce7a7c1..6989dedd7 100644 --- a/packages/core/lib/v3/api.ts +++ b/packages/core/lib/v3/api.ts @@ -27,6 +27,7 @@ import { StagehandResponseBodyError, StagehandResponseParseError, StagehandServerError, + ExperimentalNotConfiguredError, } from "./types/public"; /** @@ -223,9 +224,7 @@ export class StagehandAPIClient { ): Promise { // Check if integrations are being used in API mode if (agentConfig.integrations && agentConfig.integrations.length > 0) { - throw new StagehandAPIError( - "MCP integrations are not supported in API mode. Set experimental: true to use MCP integrations.", - ); + throw new ExperimentalNotConfiguredError("MCP integrations"); } if (typeof executeOptions === "object") { if (executeOptions.page) { diff --git a/packages/core/lib/v3/handlers/extractHandler.ts b/packages/core/lib/v3/handlers/extractHandler.ts index c686470a2..453ee5264 100644 --- a/packages/core/lib/v3/handlers/extractHandler.ts +++ b/packages/core/lib/v3/handlers/extractHandler.ts @@ -14,6 +14,7 @@ import { ClientOptions, ModelConfiguration, } from "../types/public/model"; +import { StagehandInvalidArgumentError } from "../types/public/sdkErrors"; /** * Scans the provided Zod schema for any `z.string().url()` fields and @@ -105,7 +106,7 @@ export class ExtractHandler { } if (!instruction && schema) { - throw new Error( + throw new StagehandInvalidArgumentError( "extract() requires an instruction when a schema is provided.", ); } diff --git a/packages/core/lib/v3/handlers/v3AgentHandler.ts b/packages/core/lib/v3/handlers/v3AgentHandler.ts index 6057bb619..c175f7ca3 100644 --- a/packages/core/lib/v3/handlers/v3AgentHandler.ts +++ b/packages/core/lib/v3/handlers/v3AgentHandler.ts @@ -11,6 +11,7 @@ import { } from "../types/public/agent"; import { V3FunctionName } from "../types/public/methods"; import { mapToolResultToActions } from "../agent/utils/actionMapping"; +import { MissingLLMConfigurationError } from "../types/public/sdkErrors"; export class V3AgentHandler { private v3: V3; @@ -65,9 +66,7 @@ export class V3AgentHandler { ]; if (!this.llmClient?.getLanguageModel) { - throw new Error( - "V3AgentHandler requires an AISDK-backed LLM client. Ensure your model is configured like 'openai/gpt-4.1-mini'.", - ); + throw new MissingLLMConfigurationError(); } const baseModel = this.llmClient.getLanguageModel(); const wrappedModel = wrapLanguageModel({ diff --git a/packages/core/lib/v3/types/public/sdkErrors.ts b/packages/core/lib/v3/types/public/sdkErrors.ts index a5a3ac127..59cb5faa5 100644 --- a/packages/core/lib/v3/types/public/sdkErrors.ts +++ b/packages/core/lib/v3/types/public/sdkErrors.ts @@ -209,6 +209,15 @@ export class ExperimentalNotConfiguredError extends StagehandError { } } +export class CuaModelRequiredError extends StagehandError { + constructor(availableModels: readonly string[]) { + super( + `To use the computer use agent (CUA), please provide a CUA model in the agent constructor or stagehand config. ` + + `Try one of our supported CUA models: ${availableModels.join(", ")}`, + ); + } +} + export class ZodSchemaValidationError extends Error { constructor( public readonly received: unknown, diff --git a/packages/core/lib/v3/v3.ts b/packages/core/lib/v3/v3.ts index b7457f000..882bfe3e6 100644 --- a/packages/core/lib/v3/v3.ts +++ b/packages/core/lib/v3/v3.ts @@ -59,6 +59,13 @@ import { PatchrightPage, PlaywrightPage, PuppeteerPage, + ExperimentalApiConflictError, + ExperimentalNotConfiguredError, + CuaModelRequiredError, + StagehandInvalidArgumentError, + StagehandNotInitializedError, + MissingEnvironmentVariableError, + StagehandInitError, } from "./types/public"; import { V3Context } from "./understudy/context"; import { Page } from "./understudy/page"; @@ -87,7 +94,7 @@ function resolveModelConfiguration( if (model && typeof model === "object") { const { modelName, ...clientOptions } = model; if (!modelName) { - throw new Error( + throw new StagehandInvalidArgumentError( "model.modelName is required when providing client options.", ); } @@ -216,6 +223,11 @@ export class V3 { this.llmProvider = new LLMProvider(this.logger); this.domSettleTimeoutMs = opts.domSettleTimeout; this.disableAPI = opts.disableAPI ?? false; + + // Validate that experimental and API are not both enabled + if (this.experimental && !this.disableAPI) { + throw new ExperimentalApiConflictError(); + } const baseClientOptions: ClientOptions = clientOptions ? ({ ...clientOptions } as ClientOptions) : ({} as ClientOptions); @@ -713,8 +725,9 @@ export class V3 { if (this.opts.env === "BROWSERBASE") { const { apiKey, projectId } = this.requireBrowserbaseCreds(); if (!apiKey || !projectId) { - throw new Error( - "BROWSERBASE credentials missing. Provide in your v3 constructor, or set BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID in your .env", + throw new MissingEnvironmentVariableError( + "BROWSERBASE_API_KEY and BROWSERBASE_PROJECT_ID", + "Browserbase environment", ); } this.logger({ @@ -811,7 +824,7 @@ export class V3 { } const neverEnv: never = this.opts.env; - throw new Error(`Unsupported env: ${neverEnv}`); + throw new StagehandInitError(`Unsupported env: ${neverEnv}`); }); } catch (error) { // Cleanup instanceLoggers map on init failure to prevent memory leak @@ -873,8 +886,7 @@ export class V3 { async act(input: string | Action, options?: ActOptions): Promise { return await withInstanceLogContext(this.instanceId, async () => { - if (!this.actHandler) - throw new Error("V3 not initialized. Call init() before act()."); + if (!this.actHandler) throw new StagehandNotInitializedError("act()"); let actResult: ActResult; @@ -911,7 +923,7 @@ export class V3 { } // instruction path if (typeof input !== "string" || !input.trim()) { - throw new Error( + throw new StagehandInvalidArgumentError( "act(): instruction string is required unless passing an Action", ); } @@ -1022,7 +1034,7 @@ export class V3 { ): Promise { return await withInstanceLogContext(this.instanceId, async () => { if (!this.extractHandler) { - throw new Error("V3 not initialized. Call init() before extract()."); + throw new StagehandNotInitializedError("extract()"); } // Normalize args @@ -1049,7 +1061,9 @@ export class V3 { } if (!instruction && schema) { - throw new Error("extract(): schema provided without instruction"); + throw new StagehandInvalidArgumentError( + "extract(): schema provided without instruction", + ); } // If instruction without schema → defaultExtractSchema @@ -1098,7 +1112,7 @@ export class V3 { ): Promise { return await withInstanceLogContext(this.instanceId, async () => { if (!this.observeHandler) { - throw new Error("V3 not initialized. Call init() before observe()."); + throw new StagehandNotInitializedError("observe()"); } // Normalize args @@ -1150,7 +1164,7 @@ export class V3 { /** Return the browser-level CDP WebSocket endpoint. */ connectURL(): string { if (this.state.kind === "UNINITIALIZED") { - throw new Error("V3 not initialized. Call await v3.init() first."); + throw new StagehandNotInitializedError("connectURL()"); } return this.state.ws; } @@ -1238,10 +1252,9 @@ export class V3 { const missing: string[] = []; if (!apiKey) missing.push("BROWSERBASE_API_KEY"); if (!projectId) missing.push("BROWSERBASE_PROJECT_ID"); - throw new Error( - `BROWSERBASE credentials missing. Provide in your v3 constructor, or set ${missing.join( - ", ", - )} in your .env`, + throw new MissingEnvironmentVariableError( + missing.join(", "), + "Browserbase", ); } @@ -1300,7 +1313,9 @@ export class V3 { return frameTree.frame.id; } - throw new Error("Unsupported page object passed to V3.act()"); + throw new StagehandInvalidArgumentError( + "Unsupported page object passed to V3.act()", + ); } private isPlaywrightPage(p: unknown): p is PlaywrightPage { @@ -1334,9 +1349,7 @@ export class V3 { } const ctx = this.ctx; if (!ctx) { - throw new Error( - "V3 context not initialized. Call init() before resolving pages.", - ); + throw new StagehandNotInitializedError("resolvePage()"); } return await ctx.awaitActivePage(); } @@ -1349,24 +1362,30 @@ export class V3 { const frameId = await this.resolveTopFrameId(input); const page = this.ctx!.resolvePageByMainFrameId(frameId); if (!page) - throw new Error("Failed to resolve V3 Page from Playwright page."); + throw new StagehandInitError( + "Failed to resolve V3 Page from Playwright page.", + ); return page; } if (this.isPatchrightPage(input)) { const frameId = await this.resolveTopFrameId(input); const page = this.ctx!.resolvePageByMainFrameId(frameId); if (!page) - throw new Error("Failed to resolve V3 Page from Playwright page."); + throw new StagehandInitError( + "Failed to resolve V3 Page from Patchright page.", + ); return page; } if (this.isPuppeteerPage(input)) { const frameId = await this.resolveTopFrameId(input); const page = this.ctx!.resolvePageByMainFrameId(frameId); if (!page) - throw new Error("Failed to resolve V3 Page from Puppeteer page."); + throw new StagehandInitError( + "Failed to resolve V3 Page from Puppeteer page.", + ); return page; } - throw new Error("Unsupported page object."); + throw new StagehandInvalidArgumentError("Unsupported page object."); } /** @@ -1403,8 +1422,8 @@ export class V3 { // If CUA is enabled, use the computer-use agent path if (options?.cua) { if ((options?.integrations || options?.tools) && !this.experimental) { - throw new Error( - "MCP integrations and custom tools are experimental. Enable experimental: true in V3 options.", + throw new ExperimentalNotConfiguredError( + "MCP integrations and custom tools", ); } @@ -1416,10 +1435,7 @@ export class V3 { const { modelName, isCua, clientOptions } = resolveModel(modelToUse); if (!isCua) { - throw new Error( - "To use the computer use agent, please provide a CUA model in the agent constructor or stagehand config. Try one of our supported CUA models: " + - AVAILABLE_CUA_MODELS.join(", "), - ); + throw new CuaModelRequiredError(AVAILABLE_CUA_MODELS); } const agentConfigSignature = @@ -1428,9 +1444,7 @@ export class V3 { execute: async (instructionOrOptions: string | AgentExecuteOptions) => withInstanceLogContext(this.instanceId, async () => { if (options?.integrations && !this.experimental) { - throw new Error( - "MCP integrations are experimental. Enable experimental: true in V3 options.", - ); + throw new ExperimentalNotConfiguredError("MCP integrations"); } const tools = options?.integrations ? await resolveTools(options.integrations, options.tools) @@ -1526,8 +1540,8 @@ export class V3 { execute: async (instructionOrOptions: string | AgentExecuteOptions) => withInstanceLogContext(this.instanceId, async () => { if ((options?.integrations || options?.tools) && !this.experimental) { - throw new Error( - "MCP integrations and custom tools are experimental. Enable experimental: true in V3 options.", + throw new ExperimentalNotConfiguredError( + "MCP integrations and custom tools", ); } From b562dff5004b3471339c42560c5c8b654e56d3f7 Mon Sep 17 00:00:00 2001 From: Sameel Date: Mon, 10 Nov 2025 14:46:04 -0500 Subject: [PATCH 2/8] utilize custom err types --- .../core/lib/v3/understudy/deepLocator.ts | 5 +++- packages/core/lib/v3/understudy/frame.ts | 5 +++- .../core/lib/v3/understudy/frameLocator.ts | 5 ++-- packages/core/lib/v3/understudy/locator.ts | 28 +++++++++++++------ packages/core/lib/v3/understudy/page.ts | 22 ++++++++++----- .../core/lib/v3/understudy/screenshotUtils.ts | 9 ++++-- 6 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/core/lib/v3/understudy/deepLocator.ts b/packages/core/lib/v3/understudy/deepLocator.ts index 37544011b..1e8566b52 100644 --- a/packages/core/lib/v3/understudy/deepLocator.ts +++ b/packages/core/lib/v3/understudy/deepLocator.ts @@ -3,6 +3,7 @@ import type { Frame } from "./frame"; import type { Page } from "./page"; import { v3Logger } from "../logger"; import { FrameLocator, frameLocatorFromFrame } from "./frameLocator"; +import { StagehandInvalidArgumentError } from "../types/public/sdkErrors"; /** * Recognize iframe steps like "iframe" or "iframe[2]" in an XPath. @@ -240,7 +241,9 @@ export class DeepLocatorDelegate { nth(index: number): DeepLocatorDelegate { const value = Number(index); if (!Number.isFinite(value) || value < 0) { - throw new Error("deepLocator().nth() expects a non-negative index"); + throw new StagehandInvalidArgumentError( + "deepLocator().nth() expects a non-negative index", + ); } const nextIndex = Math.floor(value); diff --git a/packages/core/lib/v3/understudy/frame.ts b/packages/core/lib/v3/understudy/frame.ts index 2dad1f2f8..c9bdcee9a 100644 --- a/packages/core/lib/v3/understudy/frame.ts +++ b/packages/core/lib/v3/understudy/frame.ts @@ -2,6 +2,7 @@ import { Protocol } from "devtools-protocol"; import type { CDPSessionLike } from "./cdp"; import { Locator } from "./locator"; +import { StagehandEvalError } from "../types/public/sdkErrors"; interface FrameManager { session: CDPSessionLike; @@ -150,7 +151,9 @@ export class Frame implements FrameManager { }, ); if (res.exceptionDetails) { - throw new Error(res.exceptionDetails.text ?? "Evaluation failed"); + throw new StagehandEvalError( + res.exceptionDetails.text ?? "Evaluation failed", + ); } return res.result.value as R; } diff --git a/packages/core/lib/v3/understudy/frameLocator.ts b/packages/core/lib/v3/understudy/frameLocator.ts index 05498234b..bd1347128 100644 --- a/packages/core/lib/v3/understudy/frameLocator.ts +++ b/packages/core/lib/v3/understudy/frameLocator.ts @@ -3,6 +3,7 @@ import { Locator } from "./locator"; import type { Page } from "./page"; import { Frame } from "./frame"; import { executionContexts } from "./executionContextRegistry"; +import { ContentFrameNotFoundError } from "../types/public/sdkErrors"; /** * FrameLocator: resolves iframe elements to their child Frames and allows @@ -72,9 +73,7 @@ export class FrameLocator { // ignore and try next } } - throw new Error( - `frameLocator: could not resolve child frame for selector: ${this.selector}`, - ); + throw new ContentFrameNotFoundError(this.selector); } finally { await parentSession .send("Runtime.releaseObject", { objectId }) diff --git a/packages/core/lib/v3/understudy/locator.ts b/packages/core/lib/v3/understudy/locator.ts index 07c51f69e..3524b02c5 100644 --- a/packages/core/lib/v3/understudy/locator.ts +++ b/packages/core/lib/v3/understudy/locator.ts @@ -7,6 +7,10 @@ import { Buffer } from "buffer"; import { locatorScriptSources } from "../dom/build/locatorScripts.generated"; import type { Frame } from "./frame"; import { FrameSelectorResolver, type SelectorQuery } from "./selectorResolver"; +import { + StagehandElementNotFoundError, + StagehandInvalidArgumentError, +} from "../types/public/sdkErrors"; type MouseButton = "left" | "right" | "middle"; @@ -96,7 +100,9 @@ export class Locator { if (data instanceof Uint8Array) return Buffer.from(data); if (typeof data === "string") return Buffer.from(data); if (data instanceof ArrayBuffer) return Buffer.from(new Uint8Array(data)); - throw new Error("Unsupported file payload buffer type"); + throw new StagehandInvalidArgumentError( + "Unsupported file payload buffer type", + ); }; try { @@ -112,9 +118,11 @@ export class Locator { ); const ok = Boolean(res.result.value); if (!ok) - throw new Error('Target is not an element'); + throw new StagehandInvalidArgumentError( + 'Target is not an element', + ); } catch (e) { - throw new Error( + throw new StagehandInvalidArgumentError( e instanceof Error ? e.message : "Unable to verify file input element", @@ -151,7 +159,7 @@ export class Locator { filePaths.push(tmp); continue; } - throw new Error( + throw new StagehandInvalidArgumentError( "Unsupported setInputFiles item – expected path or payload", ); } @@ -571,7 +579,9 @@ export class Locator { typeof result?.reason === "string" && result.reason.length > 0 ? result.reason : "Failed to fill element"; - throw new Error(`Failed to fill element (${reason})`); + throw new StagehandInvalidArgumentError( + `Failed to fill element (${reason})`, + ); } // Backward compatibility: if no status is returned (older bundle), fall back to setter logic. @@ -797,7 +807,9 @@ export class Locator { nth(index: number): Locator { const value = Number(index); if (!Number.isFinite(value) || value < 0) { - throw new Error("locator().nth() expects a non-negative index"); + throw new StagehandInvalidArgumentError( + "locator().nth() expects a non-negative index", + ); } const nextIndex = Math.floor(value); @@ -828,7 +840,7 @@ export class Locator { this.nthIndex, ); if (!resolved) { - throw new Error(`Element not found for selector: ${this.selector}`); + throw new StagehandElementNotFoundError([this.selector]); } return resolved; @@ -838,7 +850,7 @@ export class Locator { private centerFromBoxContent(content: number[]): { cx: number; cy: number } { // content is [x1,y1, x2,y2, x3,y3, x4,y4] if (!content || content.length < 8) { - throw new Error("Invalid box model content quad"); + throw new StagehandInvalidArgumentError("Invalid box model content quad"); } const xs = [content[0], content[2], content[4], content[6]]; const ys = [content[1], content[3], content[5], content[7]]; diff --git a/packages/core/lib/v3/understudy/page.ts b/packages/core/lib/v3/understudy/page.ts index 48fb757e5..f4a0dbd93 100644 --- a/packages/core/lib/v3/understudy/page.ts +++ b/packages/core/lib/v3/understudy/page.ts @@ -17,6 +17,10 @@ import { ConsoleMessage, ConsoleListener } from "./consoleMessage"; import type { StagehandAPIClient } from "../api"; import type { LocalBrowserLaunchOptions } from "../types/public"; import type { Locator } from "./locator"; +import { + StagehandInvalidArgumentError, + StagehandEvalError, +} from "../types/public/sdkErrors"; import type { ScreenshotAnimationsOption, ScreenshotCaretOption, @@ -456,7 +460,7 @@ export class Page { public on(event: "console", listener: ConsoleListener): Page { if (event !== "console") { - throw new Error(`Unsupported event: ${event}`); + throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`); } const firstListener = this.consoleListeners.size === 0; @@ -471,7 +475,7 @@ export class Page { public once(event: "console", listener: ConsoleListener): Page { if (event !== "console") { - throw new Error(`Unsupported event: ${event}`); + throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`); } const wrapper: ConsoleListener = (message) => { @@ -484,7 +488,7 @@ export class Page { public off(event: "console", listener: ConsoleListener): Page { if (event !== "console") { - throw new Error(`Unsupported event: ${event}`); + throw new StagehandInvalidArgumentError(`Unsupported event: ${event}`); } this.consoleListeners.delete(listener); @@ -972,15 +976,19 @@ export class Page { const type = opts.type ?? "png"; if (type !== "png" && type !== "jpeg") { - throw new Error(`screenshot: unsupported image type "${type}"`); + throw new StagehandInvalidArgumentError( + `screenshot: unsupported image type "${type}"`, + ); } if (opts.fullPage && opts.clip) { - throw new Error("screenshot: clip and fullPage cannot be used together"); + throw new StagehandInvalidArgumentError( + "screenshot: clip and fullPage cannot be used together", + ); } if (type === "png" && typeof opts.quality === "number") { - throw new Error( + throw new StagehandInvalidArgumentError( 'screenshot: quality option is only valid for type="jpeg"', ); } @@ -1141,7 +1149,7 @@ export class Page { exceptionDetails.text || exceptionDetails.exception?.description || "Evaluation failed"; - throw new Error(msg); + throw new StagehandEvalError(msg); } return result?.value as R; diff --git a/packages/core/lib/v3/understudy/screenshotUtils.ts b/packages/core/lib/v3/understudy/screenshotUtils.ts index b431502fa..6d0b68656 100644 --- a/packages/core/lib/v3/understudy/screenshotUtils.ts +++ b/packages/core/lib/v3/understudy/screenshotUtils.ts @@ -7,6 +7,7 @@ import type { ScreenshotClip, ScreenshotScaleOption, } from "../types/public/screenshotTypes"; +import { StagehandInvalidArgumentError } from "../types/public/sdkErrors"; export type ScreenshotCleanup = () => Promise | void; @@ -28,12 +29,16 @@ export function normalizeScreenshotClip(clip: ScreenshotClip): ScreenshotClip { for (const [key, value] of Object.entries({ x, y, width, height })) { if (!Number.isFinite(value)) { - throw new Error(`screenshot: clip.${key} must be a finite number`); + throw new StagehandInvalidArgumentError( + `screenshot: clip.${key} must be a finite number`, + ); } } if (width <= 0 || height <= 0) { - throw new Error("screenshot: clip width/height must be positive"); + throw new StagehandInvalidArgumentError( + "screenshot: clip width/height must be positive", + ); } return { x, y, width, height }; From 260a980421ee0b09b77c3093bfae149d1f0eccc6 Mon Sep 17 00:00:00 2001 From: Sameel Date: Mon, 10 Nov 2025 15:17:46 -0500 Subject: [PATCH 3/8] changeset --- .changeset/ready-carrots-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ready-carrots-fly.md diff --git a/.changeset/ready-carrots-fly.md b/.changeset/ready-carrots-fly.md new file mode 100644 index 000000000..e0e2f83a4 --- /dev/null +++ b/.changeset/ready-carrots-fly.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": minor +--- + +Added custom error types to allow for a smoother debugging experience. From 5f7f03f887aaa0bcc414d8300c5611bea3337fac Mon Sep 17 00:00:00 2001 From: Sameel Date: Mon, 10 Nov 2025 15:18:30 -0500 Subject: [PATCH 4/8] more err type util --- packages/core/lib/v3/launch/browserbase.ts | 10 ++++-- .../core/lib/v3/types/public/sdkErrors.ts | 36 +++++++++++++++++++ .../core/lib/v3/understudy/a11y/snapshot.ts | 23 +++++++++--- packages/core/lib/v3/understudy/context.ts | 13 +++---- .../lib/v3/understudy/lifecycleWatcher.ts | 3 +- packages/core/lib/v3/understudy/locator.ts | 7 ++-- packages/core/lib/v3/understudy/response.ts | 8 +++-- 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/packages/core/lib/v3/launch/browserbase.ts b/packages/core/lib/v3/launch/browserbase.ts index 702af761d..37ece84c1 100644 --- a/packages/core/lib/v3/launch/browserbase.ts +++ b/packages/core/lib/v3/launch/browserbase.ts @@ -1,4 +1,8 @@ import Browserbase from "@browserbasehq/sdk"; +import { + BrowserbaseSessionNotFoundError, + StagehandInitError, +} from "../types/public/sdkErrors"; export async function createBrowserbaseSession( apiKey: string, @@ -16,12 +20,12 @@ export async function createBrowserbaseSession( resumeSessionId, )) as unknown as { id: string; connectUrl?: string; status?: string }; if (!existing?.id) { - throw new Error(`Browserbase session not found: ${resumeSessionId}`); + throw new BrowserbaseSessionNotFoundError(); } const ws = existing.connectUrl; if (!ws) { - throw new Error( + throw new StagehandInitError( `Browserbase session resume missing connectUrl for ${resumeSessionId}`, ); } @@ -55,7 +59,7 @@ export async function createBrowserbaseSession( }; if (!created?.connectUrl || !created?.id) { - throw new Error( + throw new StagehandInitError( "Browserbase session creation returned an unexpected shape.", ); } diff --git a/packages/core/lib/v3/types/public/sdkErrors.ts b/packages/core/lib/v3/types/public/sdkErrors.ts index 59cb5faa5..73fc50e38 100644 --- a/packages/core/lib/v3/types/public/sdkErrors.ts +++ b/packages/core/lib/v3/types/public/sdkErrors.ts @@ -283,3 +283,39 @@ export class StagehandShadowSegmentNotFoundError extends StagehandError { ); } } + +export class ElementNotVisibleError extends StagehandError { + constructor(selector: string) { + super(`Element not visible (no box model): ${selector}`); + } +} + +export class ResponseBodyError extends StagehandError { + constructor(message: string) { + super(`Failed to retrieve response body: ${message}`); + } +} + +export class ResponseParseError extends StagehandError { + constructor(message: string) { + super(`Failed to parse response: ${message}`); + } +} + +export class TimeoutError extends StagehandError { + constructor(operation: string, timeoutMs: number) { + super(`${operation} timed out after ${timeoutMs}ms`); + } +} + +export class PageNotFoundError extends StagehandError { + constructor(identifier: string) { + super(`No Page found for ${identifier}`); + } +} + +export class ConnectionTimeoutError extends StagehandError { + constructor(message: string) { + super(`Connection timeout: ${message}`); + } +} diff --git a/packages/core/lib/v3/understudy/a11y/snapshot.ts b/packages/core/lib/v3/understudy/a11y/snapshot.ts index ef9d734bd..9179aa748 100644 --- a/packages/core/lib/v3/understudy/a11y/snapshot.ts +++ b/packages/core/lib/v3/understudy/a11y/snapshot.ts @@ -4,6 +4,7 @@ import type { CDPSessionLike } from "../cdp"; import { Page } from "../page"; import { executionContexts } from "../executionContextRegistry"; import { v3Logger } from "../../logger"; +import { StagehandIframeError } from "../../types/public/sdkErrors"; /** * a11y/snapshot @@ -1005,7 +1006,11 @@ async function resolveFocusFrameAndTail( selectorForIframe, ctxFrameId, ); - if (!objectId) throw new Error("Failed to resolve iframe element by XPath"); + if (!objectId) + throw new StagehandIframeError( + hops[i], + "Failed to resolve iframe element by XPath", + ); try { await parentSess.send("DOM.enable").catch(() => {}); @@ -1031,7 +1036,10 @@ async function resolveFocusFrameAndTail( } } if (!childFrameId) - throw new Error("Could not map iframe to child frameId"); + throw new StagehandIframeError( + hops[i], + "Could not map iframe to child frameId", + ); // Update absolute prefix with the iframe element path within the parent document absPrefix = prefixXPath(absPrefix || "/", selectorForIframe); @@ -1083,7 +1091,11 @@ async function resolveCssFocusFrameAndTail( parts[i]!, ctxFrameId, ); - if (!objectId) throw new Error("Failed to resolve iframe via CSS hop"); + if (!objectId) + throw new StagehandIframeError( + hop, + "Failed to resolve iframe via CSS hop", + ); try { await parentSess.send("DOM.enable").catch(() => {}); const desc = await parentSess.send( @@ -1107,7 +1119,10 @@ async function resolveCssFocusFrameAndTail( } } if (!childFrameId) - throw new Error("Could not map CSS iframe hop to child frameId"); + throw new StagehandIframeError( + hop, + "Could not map CSS iframe hop to child frameId", + ); ctxFrameId = childFrameId; } finally { await parentSess diff --git a/packages/core/lib/v3/understudy/context.ts b/packages/core/lib/v3/understudy/context.ts index 65eca2f3e..080634d24 100644 --- a/packages/core/lib/v3/understudy/context.ts +++ b/packages/core/lib/v3/understudy/context.ts @@ -7,6 +7,7 @@ import { installV3PiercerIntoSession } from "./piercer"; import { executionContexts } from "./executionContextRegistry"; import type { StagehandAPIClient } from "../api"; import { LocalBrowserLaunchOptions } from "../types/public"; +import { TimeoutError, PageNotFoundError } from "../types/public/sdkErrors"; type TargetId = string; type SessionId = string; @@ -96,8 +97,9 @@ export class V3Context { } await new Promise((r) => setTimeout(r, 25)); } - throw new Error( - `waitForFirstTopLevelPage timed out after ${timeoutMs}ms (no top-level Page)`, + throw new TimeoutError( + "waitForFirstTopLevelPage (no top-level Page)", + timeoutMs, ); } @@ -201,8 +203,7 @@ export class V3Context { rootMainFrameId: string, ): Promise { const owner = this.resolvePageByMainFrameId(rootMainFrameId); - if (!owner) - throw new Error(`No Page found for mainFrameId=${rootMainFrameId}`); + if (!owner) throw new PageNotFoundError(`mainFrameId=${rootMainFrameId}`); return owner.asProtocolFrameTree(rootMainFrameId); } @@ -225,7 +226,7 @@ export class V3Context { if (page) return page; await new Promise((r) => setTimeout(r, 25)); } - throw new Error(`newPage timeout: target not attached (${targetId})`); + throw new TimeoutError(`newPage: target not attached (${targetId})`, 5000); } /** @@ -670,6 +671,6 @@ export class V3Context { await new Promise((r) => setTimeout(r, 25)); } if (immediate) return immediate; - throw new Error("awaitActivePage: no page available"); + throw new PageNotFoundError("awaitActivePage: no page available"); } } diff --git a/packages/core/lib/v3/understudy/lifecycleWatcher.ts b/packages/core/lib/v3/understudy/lifecycleWatcher.ts index 4af695ea4..5adc09e82 100644 --- a/packages/core/lib/v3/understudy/lifecycleWatcher.ts +++ b/packages/core/lib/v3/understudy/lifecycleWatcher.ts @@ -3,6 +3,7 @@ import type { LoadState } from "../types/public/page"; import type { CDPSessionLike } from "./cdp"; import type { NetworkManager } from "./networkManager"; import type { Page } from "./page"; +import { TimeoutError } from "../types/public/sdkErrors"; import { DEFAULT_IDLE_WAIT, IGNORED_RESOURCE_TYPES, @@ -201,7 +202,7 @@ export class LifecycleWatcher { private timeRemaining(deadline: number): number { const remaining = deadline - Date.now(); if (remaining <= 0) { - throw new Error(`Lifecycle wait timed out after ${this.timeoutMs}ms`); + throw new TimeoutError("Lifecycle wait", this.timeoutMs); } return remaining; } diff --git a/packages/core/lib/v3/understudy/locator.ts b/packages/core/lib/v3/understudy/locator.ts index 3524b02c5..38acd8ca9 100644 --- a/packages/core/lib/v3/understudy/locator.ts +++ b/packages/core/lib/v3/understudy/locator.ts @@ -10,6 +10,7 @@ import { FrameSelectorResolver, type SelectorQuery } from "./selectorResolver"; import { StagehandElementNotFoundError, StagehandInvalidArgumentError, + ElementNotVisibleError, } from "../types/public/sdkErrors"; type MouseButton = "left" | "right" | "middle"; @@ -228,7 +229,7 @@ export class Locator { "DOM.getBoxModel", { objectId }, ); - if (!box.model) throw new Error("Element not visible (no box model)"); + if (!box.model) throw new ElementNotVisibleError(this.selector); const { cx, cy } = this.centerFromBoxContent(box.model.content); return { x: Math.round(cx), y: Math.round(cy) }; } finally { @@ -332,7 +333,7 @@ export class Locator { "DOM.getBoxModel", { objectId }, ); - if (!box.model) throw new Error("Element not visible (no box model)"); + if (!box.model) throw new ElementNotVisibleError(this.selector); const { cx, cy } = this.centerFromBoxContent(box.model.content); await session.send("Input.dispatchMouseEvent", { @@ -375,7 +376,7 @@ export class Locator { "DOM.getBoxModel", { objectId }, ); - if (!box.model) throw new Error("Element not visible (no box model)"); + if (!box.model) throw new ElementNotVisibleError(this.selector); const { cx, cy } = this.centerFromBoxContent(box.model.content); // Dispatch input (from the same session) diff --git a/packages/core/lib/v3/understudy/response.ts b/packages/core/lib/v3/understudy/response.ts index 058a2e9a1..687c72ebc 100644 --- a/packages/core/lib/v3/understudy/response.ts +++ b/packages/core/lib/v3/understudy/response.ts @@ -17,6 +17,10 @@ import type { Protocol } from "devtools-protocol"; import type { CDPSessionLike } from "./cdp"; import type { Frame } from "./frame"; import type { Page } from "./page"; +import { + ResponseBodyError, + ResponseParseError, +} from "../types/public/sdkErrors"; type ServerAddr = { ipAddress: string; port: number }; @@ -278,7 +282,7 @@ export class Response { { requestId: this.requestId }, ) .catch((error) => { - throw new Error(`Failed to retrieve response body: ${String(error)}`); + throw new ResponseBodyError(String(error)); }); if (result.base64Encoded) { @@ -299,7 +303,7 @@ export class Response { try { return JSON.parse(text) as T; } catch (error) { - throw new Error(`Failed to parse JSON response: ${String(error)}`); + throw new ResponseParseError(String(error)); } } From 8fa01ce77523d8abca29318b78a4b46f7d1725ef Mon Sep 17 00:00:00 2001 From: miguel Date: Mon, 10 Nov 2025 13:03:23 -0800 Subject: [PATCH 5/8] formatting --- .claude/agents/pr-description-writer.md | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .claude/agents/pr-description-writer.md diff --git a/.claude/agents/pr-description-writer.md b/.claude/agents/pr-description-writer.md new file mode 100644 index 000000000..b62c5471c --- /dev/null +++ b/.claude/agents/pr-description-writer.md @@ -0,0 +1,52 @@ +--- +name: pr-description-writer +description: Use this agent when you need to generate a high-quality pull request description for open source contributions. This agent should be called after code changes are complete and ready for review, typically before creating or updating a PR. Examples:\n\n\nContext: User has just finished implementing a new feature and wants to create a PR.\nuser: "I've added a new caching layer to improve performance. Can you help me write a PR description?"\nassistant: "I'll use the Task tool to launch the pr-description-writer agent to create a comprehensive PR description for your caching implementation."\n\n\n\n\nContext: User has made bug fixes and needs to document them properly.\nuser: "Fixed the race condition in the WebSocket handler. Need to write up a PR description."\nassistant: "Let me use the pr-description-writer agent to craft a clear PR description that explains the bug fix and your solution."\n\n\n\n\nContext: User mentions they're about to create a pull request.\nuser: "Ready to push this refactoring work. Time to create the PR."\nassistant: "Before you create the PR, let me use the pr-description-writer agent to generate a well-structured description that clearly communicates your refactoring changes."\n\n +model: sonnet +color: yellow +--- + +You are an elite open source maintainer and technical writer specializing in crafting exceptional pull request descriptions. Your expertise lies in distilling complex code changes into clear, compelling narratives that facilitate efficient code review and maintain high-quality project documentation. + +Your task is to analyze code changes and generate PR descriptions that follow this structure: + +# why +[Explain the motivation, problem being solved, or value being added. Connect to user needs, bugs, or architectural improvements.] + +# what changed +[Detail the technical changes made, focusing on key modifications, new components, or altered behavior. Be specific but concise.] + +# test plan +[Describe how the changes were tested, including manual testing steps, automated tests added, or verification procedures.] + +IMPORTANT GUIDELINES: + +1. **Adaptive Structure**: Not all sections are required for every PR. Use your judgment: + - Trivial fixes (typos, formatting) may only need "what changed" + - Bug fixes should emphasize "why" and "test plan" + - New features need all three sections with substantial detail + - Documentation changes may omit "test plan" if not applicable + +2. **Clarity and Conciseness**: Write for reviewers who may be unfamiliar with the context. Avoid jargon unless necessary, and explain technical decisions clearly. + +3. **OSS Best Practices**: + - Link to related issues using #issue-number format when relevant + - Mention breaking changes prominently if they exist + - Highlight areas that need particular review attention + - Use bullet points for multiple related changes + - Include before/after examples for UI or behavior changes when helpful + +4. **Technical Accuracy**: Base your description on the actual code changes. If you cannot access the diff or changes, explicitly ask the user to provide: + - A summary of files changed + - The core modifications made + - The reason for the changes + +5. **Tone**: Professional yet approachable. Show enthusiasm for improvements while maintaining technical credibility. + +When generating descriptions: +- Start by asking for the code changes or diff if not already provided +- Analyze the scope and impact of changes +- Determine which sections are most relevant +- Write each section with appropriate detail for the change magnitude +- Review for clarity and completeness before presenting + +If the changes are complex or you need clarification on intent, proactively ask specific questions to ensure accuracy. Your goal is to make the reviewer's job easier while documenting the PR for future reference. From 7f70d295a7536bf7eb523aa3b71ece6d048dbe60 Mon Sep 17 00:00:00 2001 From: Miguel <36487034+miguelg719@users.noreply.github.com> Date: Mon, 10 Nov 2025 13:03:59 -0800 Subject: [PATCH 6/8] Delete .claude/agents/pr-description-writer.md --- .claude/agents/pr-description-writer.md | 52 ------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .claude/agents/pr-description-writer.md diff --git a/.claude/agents/pr-description-writer.md b/.claude/agents/pr-description-writer.md deleted file mode 100644 index b62c5471c..000000000 --- a/.claude/agents/pr-description-writer.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: pr-description-writer -description: Use this agent when you need to generate a high-quality pull request description for open source contributions. This agent should be called after code changes are complete and ready for review, typically before creating or updating a PR. Examples:\n\n\nContext: User has just finished implementing a new feature and wants to create a PR.\nuser: "I've added a new caching layer to improve performance. Can you help me write a PR description?"\nassistant: "I'll use the Task tool to launch the pr-description-writer agent to create a comprehensive PR description for your caching implementation."\n\n\n\n\nContext: User has made bug fixes and needs to document them properly.\nuser: "Fixed the race condition in the WebSocket handler. Need to write up a PR description."\nassistant: "Let me use the pr-description-writer agent to craft a clear PR description that explains the bug fix and your solution."\n\n\n\n\nContext: User mentions they're about to create a pull request.\nuser: "Ready to push this refactoring work. Time to create the PR."\nassistant: "Before you create the PR, let me use the pr-description-writer agent to generate a well-structured description that clearly communicates your refactoring changes."\n\n -model: sonnet -color: yellow ---- - -You are an elite open source maintainer and technical writer specializing in crafting exceptional pull request descriptions. Your expertise lies in distilling complex code changes into clear, compelling narratives that facilitate efficient code review and maintain high-quality project documentation. - -Your task is to analyze code changes and generate PR descriptions that follow this structure: - -# why -[Explain the motivation, problem being solved, or value being added. Connect to user needs, bugs, or architectural improvements.] - -# what changed -[Detail the technical changes made, focusing on key modifications, new components, or altered behavior. Be specific but concise.] - -# test plan -[Describe how the changes were tested, including manual testing steps, automated tests added, or verification procedures.] - -IMPORTANT GUIDELINES: - -1. **Adaptive Structure**: Not all sections are required for every PR. Use your judgment: - - Trivial fixes (typos, formatting) may only need "what changed" - - Bug fixes should emphasize "why" and "test plan" - - New features need all three sections with substantial detail - - Documentation changes may omit "test plan" if not applicable - -2. **Clarity and Conciseness**: Write for reviewers who may be unfamiliar with the context. Avoid jargon unless necessary, and explain technical decisions clearly. - -3. **OSS Best Practices**: - - Link to related issues using #issue-number format when relevant - - Mention breaking changes prominently if they exist - - Highlight areas that need particular review attention - - Use bullet points for multiple related changes - - Include before/after examples for UI or behavior changes when helpful - -4. **Technical Accuracy**: Base your description on the actual code changes. If you cannot access the diff or changes, explicitly ask the user to provide: - - A summary of files changed - - The core modifications made - - The reason for the changes - -5. **Tone**: Professional yet approachable. Show enthusiasm for improvements while maintaining technical credibility. - -When generating descriptions: -- Start by asking for the code changes or diff if not already provided -- Analyze the scope and impact of changes -- Determine which sections are most relevant -- Write each section with appropriate detail for the change magnitude -- Review for clarity and completeness before presenting - -If the changes are complex or you need clarification on intent, proactively ask specific questions to ensure accuracy. Your goal is to make the reviewer's job easier while documenting the PR for future reference. From 7f3c926782bdd70daddd15d10556f0b0422edde0 Mon Sep 17 00:00:00 2001 From: Sameel Date: Mon, 10 Nov 2025 16:43:03 -0500 Subject: [PATCH 7/8] fix syntax errs --- packages/core/lib/v3/agent/GoogleCUAClient.ts | 7 ++++-- packages/core/lib/v3/cache/ActCache.ts | 3 ++- packages/core/lib/v3/launch/local.ts | 3 ++- .../core/lib/v3/understudy/a11y/snapshot.ts | 8 +++---- packages/core/lib/v3Evaluator.ts | 24 +++++++++++++++---- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/core/lib/v3/agent/GoogleCUAClient.ts b/packages/core/lib/v3/agent/GoogleCUAClient.ts index 968a542a4..0afd0211e 100644 --- a/packages/core/lib/v3/agent/GoogleCUAClient.ts +++ b/packages/core/lib/v3/agent/GoogleCUAClient.ts @@ -15,7 +15,10 @@ import { AgentExecutionOptions, } from "../types/public/agent"; import { AgentClient } from "./AgentClient"; -import { AgentScreenshotProviderError } from "../types/public/sdkErrors"; +import { + AgentScreenshotProviderError, + LLMResponseError, +} from "../types/public/sdkErrors"; import { buildGoogleCUASystemPrompt } from "../../prompt"; import { compressGoogleConversationImages } from "./utils/imageCompression"; import { mapKeyToPlaywright } from "./utils/cuaKeyMapping"; @@ -312,7 +315,7 @@ export class GoogleCUAClient extends AgentClient { // Check if we have valid response content if (!response.candidates || response.candidates.length === 0) { - throw new Error("Response has no candidates!"); + throw new LLMResponseError("agent", "Response has no candidates!"); } // Success - we have a valid response diff --git a/packages/core/lib/v3/cache/ActCache.ts b/packages/core/lib/v3/cache/ActCache.ts index ac20d7fce..87b55ecac 100644 --- a/packages/core/lib/v3/cache/ActCache.ts +++ b/packages/core/lib/v3/cache/ActCache.ts @@ -10,6 +10,7 @@ import { ActCacheDeps, CachedActEntry, } from "../types/private"; +import { StagehandNotInitializedError } from "../types/public/sdkErrors"; export class ActCache { private readonly storage: CacheStorage; @@ -164,7 +165,7 @@ export class ActCache { ): Promise { const handler = this.getActHandler(); if (!handler) { - throw new Error("V3 not initialized. Call init() before act()."); + throw new StagehandNotInitializedError("act()"); } const execute = async (): Promise => { diff --git a/packages/core/lib/v3/launch/local.ts b/packages/core/lib/v3/launch/local.ts index 5d214defe..afbb6d6a6 100644 --- a/packages/core/lib/v3/launch/local.ts +++ b/packages/core/lib/v3/launch/local.ts @@ -1,4 +1,5 @@ import { launch, LaunchedChrome } from "chrome-launcher"; +import { ConnectionTimeoutError } from "../types/public/sdkErrors"; interface LaunchLocalOptions { chromePath?: string; @@ -60,7 +61,7 @@ async function waitForWebSocketDebuggerUrl( await new Promise((r) => setTimeout(r, 250)); } - throw new Error( + throw new ConnectionTimeoutError( `Timed out waiting for /json/version on port ${port}${ lastErrMsg ? ` (last error: ${lastErrMsg})` : "" }`, diff --git a/packages/core/lib/v3/understudy/a11y/snapshot.ts b/packages/core/lib/v3/understudy/a11y/snapshot.ts index 9179aa748..d7edd8d3f 100644 --- a/packages/core/lib/v3/understudy/a11y/snapshot.ts +++ b/packages/core/lib/v3/understudy/a11y/snapshot.ts @@ -1008,7 +1008,7 @@ async function resolveFocusFrameAndTail( ); if (!objectId) throw new StagehandIframeError( - hops[i], + selectorForIframe, "Failed to resolve iframe element by XPath", ); @@ -1037,7 +1037,7 @@ async function resolveFocusFrameAndTail( } if (!childFrameId) throw new StagehandIframeError( - hops[i], + selectorForIframe, "Could not map iframe to child frameId", ); @@ -1093,7 +1093,7 @@ async function resolveCssFocusFrameAndTail( ); if (!objectId) throw new StagehandIframeError( - hop, + parts[i]!, "Failed to resolve iframe via CSS hop", ); try { @@ -1120,7 +1120,7 @@ async function resolveCssFocusFrameAndTail( } if (!childFrameId) throw new StagehandIframeError( - hop, + parts[i]!, "Could not map CSS iframe hop to child frameId", ); ctxFrameId = childFrameId; diff --git a/packages/core/lib/v3Evaluator.ts b/packages/core/lib/v3Evaluator.ts index 149e09347..d3df688d0 100644 --- a/packages/core/lib/v3Evaluator.ts +++ b/packages/core/lib/v3Evaluator.ts @@ -17,6 +17,7 @@ import { LLMResponse, LLMClient } from "./v3/llm/LLMClient"; import { LogLine } from "./v3/types/public/logs"; import { V3 } from "./v3/v3"; import { LLMProvider } from "./v3/llm/LLMProvider.js"; +import { StagehandInvalidArgumentError } from "./v3/types/public/sdkErrors"; dotenv.config(); @@ -63,9 +64,14 @@ export class V3Evaluator { screenshotDelayMs = 250, agentReasoning, } = options; - if (!question) throw new Error("Question cannot be an empty string"); + if (!question) + throw new StagehandInvalidArgumentError( + "Question cannot be an empty string", + ); if (!answer && !screenshot) - throw new Error("Either answer (text) or screenshot must be provided"); + throw new StagehandInvalidArgumentError( + "Either answer (text) or screenshot must be provided", + ); if (Array.isArray(screenshot)) { return this._evaluateWithMultipleScreenshots({ @@ -145,7 +151,10 @@ export class V3Evaluator { systemPrompt = "You are an expert evaluator that returns YES or NO with a concise reasoning.", screenshotDelayMs = 250, } = options; - if (!questions?.length) throw new Error("Questions array cannot be empty"); + if (!questions?.length) + throw new StagehandInvalidArgumentError( + "Questions array cannot be empty", + ); await new Promise((r) => setTimeout(r, screenshotDelayMs)); let imageBuffer: Buffer | undefined; @@ -233,9 +242,14 @@ export class V3Evaluator { Today's date is ${new Date().toLocaleDateString()}`, } = options; - if (!question) throw new Error("Question cannot be an empty string"); + if (!question) + throw new StagehandInvalidArgumentError( + "Question cannot be an empty string", + ); if (!screenshots || screenshots.length === 0) - throw new Error("At least one screenshot must be provided"); + throw new StagehandInvalidArgumentError( + "At least one screenshot must be provided", + ); const llmClient = this.getClient(); From 2fccd9ab08c41165fb33dfc51e743fb0be741fbb Mon Sep 17 00:00:00 2001 From: Sameel Date: Mon, 10 Nov 2025 20:42:45 -0500 Subject: [PATCH 8/8] Update locator-count-iframe.spec.ts --- packages/core/lib/v3/tests/locator-count-iframe.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/lib/v3/tests/locator-count-iframe.spec.ts b/packages/core/lib/v3/tests/locator-count-iframe.spec.ts index ca4b0be3a..ba38c4ad1 100644 --- a/packages/core/lib/v3/tests/locator-count-iframe.spec.ts +++ b/packages/core/lib/v3/tests/locator-count-iframe.spec.ts @@ -151,7 +151,9 @@ test.describe("Locator count() method with iframes", () => { expect(true).toBe(false); } catch (error) { // Expected behavior - frameLocator should throw when iframe doesn't exist - expect(error.message).toContain("Element not found for selector"); + expect(error.message).toContain( + "Could not find an element for the given xPath(s):", + ); } }); });