From 237363a23fe327d3a60f2e486cd07e43d2dd3b33 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Sun, 12 Oct 2025 13:22:07 +0300 Subject: [PATCH 1/4] feat: implement launchBrowser tool --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 +- README.md | 30 +++ src/browser-context.ts | 29 ++- src/tools/index.ts | 2 + src/tools/launch-browser.ts | 114 ++++++++++ test/playground/mobile-info.html | 41 ++++ test/tools/launch-browser.test.ts | 353 ++++++++++++++++++++++++++++++ 8 files changed, 565 insertions(+), 10 deletions(-) create mode 100644 src/tools/launch-browser.ts create mode 100644 test/playground/mobile-info.html create mode 100644 test/tools/launch-browser.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08acd13..8a745c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ concurrency: jobs: test: name: Run tests on Node.js ${{ matrix.node-version }} - runs-on: self-hosted-arc + runs-on: ubuntu-latest strategy: matrix: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 694f601..9539acd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ permissions: jobs: release-please: - runs-on: self-hosted-arc + runs-on: ubuntu-latest outputs: release_created: ${{ steps.release.outputs.release_created }} steps: @@ -29,7 +29,7 @@ jobs: publish: needs: release-please if: ${{ needs.release-please.outputs.release_created }} - runs-on: self-hosted-arc + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 5fb8916..e814add 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,36 @@ Navigate to URL in the browser. ### `closeBrowser` Close the current browser session. +### `launchBrowser` +Launch a new browser session with custom configuration options. + +- **Parameters:** + - `desiredCapabilities` (object, optional): WebDriver [desired capabilities](https://www.selenium.dev/documentation/webdriver/capabilities/) to forward to the Testplane launcher. Example: + + ```json + { + "browserName": "chrome", + "goog:chromeOptions": { + "mobileEmulation": { + "deviceMetrics": { + "width": 375, + "height": 667, + "pixelRatio": 2.0 + } + } + } + } + ``` + + - `gridUrl` (string, optional): WebDriver endpoint to connect to. Default: `"local"` (lets Testplane MCP manage Chrome and Firefox automatically). Set a Selenium grid URL only when you need other browsers. + + - `windowSize` (object | string | null, optional): Viewport size for the browser session. Can be: + - Object format: `{"width": 1280, "height": 720}` + - String format: `"1280x720"` + - `null` to reset to default size + +> **Note:** Testplane MCP automatically downloads Chrome and Firefox. To launch additional browsers (for example, Safari, Edge, or mobile-specific builds), use the `gridUrl` parameter to point to your Selenium grid. +
diff --git a/src/browser-context.ts b/src/browser-context.ts index 0712255..bf67ff8 100644 --- a/src/browser-context.ts +++ b/src/browser-context.ts @@ -1,10 +1,23 @@ import { WdioBrowser, SessionOptions } from "testplane"; import { launchBrowser, attachToBrowser } from "testplane/unstable"; +import type { StandaloneBrowserOptionsInput } from "testplane/unstable"; export interface BrowserOptions { headless?: boolean; + desiredCapabilities?: StandaloneBrowserOptionsInput["desiredCapabilities"]; + gridUrl?: string; + windowSize?: StandaloneBrowserOptionsInput["windowSize"]; } +const getSandboxArgs = (): string[] => + process.env.DISABLE_BROWSER_SANDBOX ? ["--no-sandbox", "--disable-dev-shm-usage", "--disable-web-security"] : []; + +const buildSandboxCapabilities = (sandboxArgs: string[]): StandaloneBrowserOptionsInput["desiredCapabilities"] => ({ + "goog:chromeOptions": { + args: sandboxArgs, + }, +}); + export class BrowserContext { protected _browser: WdioBrowser | null = null; protected _options: BrowserOptions; @@ -27,15 +40,17 @@ export class BrowserContext { await this._browser.getUrl(); // Need to get exception if not attach } else { console.error("Launch browser"); + + const sandboxArgs = getSandboxArgs(); + const desiredCapabilities = + this._options.desiredCapabilities ?? + (sandboxArgs.length ? buildSandboxCapabilities(sandboxArgs) : undefined); + this._browser = await launchBrowser({ headless: this._options.headless ? "new" : false, - desiredCapabilities: { - "goog:chromeOptions": { - args: process.env.DISABLE_BROWSER_SANDBOX - ? ["--no-sandbox", "--disable-dev-shm-usage", "--disable-web-security"] - : [], - }, - }, + desiredCapabilities, + gridUrl: this._options.gridUrl, + windowSize: this._options.windowSize, }); } diff --git a/src/tools/index.ts b/src/tools/index.ts index 046e54b..96001a7 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { ToolDefinition } from "../types.js"; import { navigate } from "./navigate.js"; import { closeBrowser } from "./close-browser.js"; +import { launchBrowser } from "./launch-browser.js"; import { clickOnElement } from "./click-on-element.js"; import { hoverElement } from "./hover-element.js"; import { typeIntoElement } from "./type-into-element.js"; @@ -16,6 +17,7 @@ import { attachToBrowser } from "./attach-to-browser.js"; export const tools = [ navigate, closeBrowser, + launchBrowser, clickOnElement, hoverElement, typeIntoElement, diff --git a/src/tools/launch-browser.ts b/src/tools/launch-browser.ts new file mode 100644 index 0000000..23e957c --- /dev/null +++ b/src/tools/launch-browser.ts @@ -0,0 +1,114 @@ +import { z } from "zod"; +import { ToolDefinition, Context } from "../types.js"; +import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { contextProvider } from "../context-provider.js"; +import { createSimpleResponse, createErrorResponse } from "../responses/index.js"; +import { BrowserContext, type BrowserOptions } from "../browser-context.js"; + +const desiredCapabilitiesSchema = z + .object({}) + .catchall(z.unknown()) + .superRefine((value, ctx) => { + const browserName = value?.["browserName"]; + + if (browserName !== undefined && typeof browserName !== "string") { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: '"browserName" must be a string' }); + } + }) + .describe( + 'WebDriver desiredCapabilities that should be used when launching the browser. Example to launch Chrome with mobile emulation: {"browserName":"chrome","goog:chromeOptions":{"mobileEmulation":{"deviceMetrics":{"width":360,"height":800,"pixelRatio":1.0}}}}', + ); + +const windowSizeSchema = z + .union([ + z + .object({ + width: z.number().int().positive(), + height: z.number().int().positive(), + }) + .strict(), + z + .string() + .trim() + .regex(/^[0-9]+x[0-9]+$/, { + message: '"windowSize" should use the format "x" (e.g. "1600x900")', + }), + z.null(), + ]) + .optional() + .describe( + 'Viewport to use for the session. Provide {"width": number, "height": number} or a string like "1280x720"; use null to reset to the default size.', + ); + +export const launchBrowserSchema = { + desiredCapabilities: desiredCapabilitiesSchema.optional(), + gridUrl: z + .string() + .default("local") + .describe( + 'WebDriver endpoint to connect to. "local" (default) lets Testplane MCP manage Chrome and Firefox automatically; set a Selenium grid URL only when you need other browsers.', + ), + windowSize: windowSizeSchema, +}; + +const launchBrowserCb: ToolCallback = async args => { + try { + const context = contextProvider.getContext(); + const desiredCapabilities = args.desiredCapabilities as BrowserOptions["desiredCapabilities"]; + const gridUrl = args.gridUrl ?? "local"; + const windowSizeInput = args.windowSize; + + if (await context.browser.isActive()) { + console.error("Closing existing browser before launching a new one"); + await context.browser.close(); + } + + const updatedOptions: BrowserOptions = { + ...context.browser.getOptions(), + }; + + if (Object.prototype.hasOwnProperty.call(args, "desiredCapabilities")) { + updatedOptions.desiredCapabilities = desiredCapabilities; + } + + if (!gridUrl || gridUrl === "local") { + delete updatedOptions.gridUrl; + } else { + updatedOptions.gridUrl = gridUrl; + } + + if (Object.prototype.hasOwnProperty.call(args, "windowSize")) { + if (windowSizeInput === null) { + updatedOptions.windowSize = null; + } else if (typeof windowSizeInput === "string") { + const [width, height] = windowSizeInput.split("x").map(value => Number.parseInt(value, 10)); + updatedOptions.windowSize = { width, height }; + } else if (windowSizeInput === undefined) { + delete updatedOptions.windowSize; + } else { + updatedOptions.windowSize = windowSizeInput as BrowserOptions["windowSize"]; + } + } + + const browserContext = new BrowserContext(updatedOptions); + const newContext: Context = { + browser: browserContext, + }; + contextProvider.setContext(newContext); + + await browserContext.get(); + + return createSimpleResponse("Successfully launched browser session"); + } catch (error) { + console.error("Error launching browser:", error); + return createErrorResponse("Error launching browser", error instanceof Error ? error : undefined); + } +}; + +export const launchBrowser: ToolDefinition = { + name: "launchBrowser", + description: + "Launch a new browser session with custom desired capabilities. Avoid using this tool unless the user explicitly requests a custom browser configuration; browsers are launched automatically for commands like navigate to URL. Testplane MCP can ONLY download Chrome and Firefox automatically, for other browsers you MUST ensure that driver is launched and provide it as custom gridUrl.", + schema: launchBrowserSchema, + cb: launchBrowserCb, +}; diff --git a/test/playground/mobile-info.html b/test/playground/mobile-info.html new file mode 100644 index 0000000..5306d23 --- /dev/null +++ b/test/playground/mobile-info.html @@ -0,0 +1,41 @@ + + + + + Mobile Emulation Diagnostics + + + + +

Mobile Emulation Diagnostics

+

Viewport width:

+

Viewport height:

+

Device pixel ratio:

+

User agent:

+ + + diff --git a/test/tools/launch-browser.test.ts b/test/tools/launch-browser.test.ts new file mode 100644 index 0000000..4087228 --- /dev/null +++ b/test/tools/launch-browser.test.ts @@ -0,0 +1,353 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from "vitest"; +import { startClient } from "../utils"; +import { INTEGRATION_TEST_TIMEOUT } from "../constants"; +import { PlaygroundServer } from "../test-server"; +import { launchBrowser } from "testplane/unstable"; + +const checkProcessExists = (pid: number): boolean => { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}; + +describe( + "tools/launchBrowser", + () => { + let client: Client; + let playgroundUrl: string; + let testServer: PlaygroundServer; + + beforeAll(async () => { + testServer = new PlaygroundServer(); + playgroundUrl = await testServer.start(); + }, 20000); + + afterAll(async () => { + if (testServer) { + await testServer.stop(); + } + }); + + beforeEach(async () => { + client = await startClient(); + }); + + afterEach(async () => { + if (client) { + await client.close(); + } + }); + + describe("tool availability", () => { + it("should list launchBrowser tool in available tools", async () => { + const tools = await client.listTools(); + + const launchBrowserTool = tools.tools.find(tool => tool.name === "launchBrowser"); + + expect(launchBrowserTool).toBeDefined(); + expect(launchBrowserTool?.description).toContain("Launch a new browser session"); + expect(launchBrowserTool?.inputSchema.properties).toHaveProperty("desiredCapabilities"); + expect(launchBrowserTool?.inputSchema.properties).toHaveProperty("gridUrl"); + expect(launchBrowserTool?.inputSchema.properties).toHaveProperty("windowSize"); + }); + }); + + describe("basic browser launch", () => { + it("should launch browser with default settings", async () => { + const result = await client.callTool({ + name: "launchBrowser", + arguments: {}, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("Successfully launched browser session"); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: "https://example.com" }, + }); + + expect(navigateResult.isError).toBe(false); + const navigateContent = navigateResult.content as Array<{ type: string; text: string }>; + expect(navigateContent[0].text).toContain("✅ Successfully navigated to https://example.com"); + }); + + it("should launch browser with explicit local gridUrl", async () => { + const result = await client.callTool({ + name: "launchBrowser", + arguments: { gridUrl: "local" }, + }); + + expect(result.isError).toBe(false); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toBe("Successfully launched browser session"); + }); + }); + + describe("custom desiredCapabilities with mobile emulation", () => { + it("should launch browser with mobile emulation capabilities", async () => { + const mobileCapabilities = { + browserName: "chrome", + "goog:chromeOptions": { + mobileEmulation: { + deviceMetrics: { + width: 375, + height: 667, + pixelRatio: 2.0, + }, + }, + }, + }; + + const launchResult = await client.callTool({ + name: "launchBrowser", + arguments: { + desiredCapabilities: mobileCapabilities, + }, + }); + + expect(launchResult.isError).toBe(false); + const launchContent = launchResult.content as Array<{ type: string; text: string }>; + expect(launchContent[0].text).toBe("Successfully launched browser session"); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: `${playgroundUrl}/mobile-info.html` }, + }); + + expect(navigateResult.isError).toBe(false); + const content = navigateResult.content as Array<{ type: string; text: string }>; + const responseText = content[0].text; + + expect(responseText).toContain("Viewport width: 375"); + expect(responseText).toContain("Viewport height: 667"); + expect(responseText).toContain("Device pixel ratio: 2"); + }); + }); + + describe("window size configuration", () => { + it("should launch browser with window size as object", async () => { + const launchResult = await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: { width: 1280, height: 720 }, + }, + }); + + expect(launchResult.isError).toBe(false); + const launchContent = launchResult.content as Array<{ type: string; text: string }>; + expect(launchContent[0].text).toBe("Successfully launched browser session"); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: `${playgroundUrl}/mobile-info.html` }, + }); + + expect(navigateResult.isError).toBe(false); + const content = navigateResult.content as Array<{ type: string; text: string }>; + const responseText = content[0].text; + + expect(responseText).toContain("Viewport width: 1280"); + // Viewport height is less than window height due to browser chrome + expect(responseText).toContain("Viewport height: 528"); + }); + + it("should launch browser with window size as string", async () => { + const launchResult = await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: "1024x768", + }, + }); + + expect(launchResult.isError).toBe(false); + const launchContent = launchResult.content as Array<{ type: string; text: string }>; + expect(launchContent[0].text).toBe("Successfully launched browser session"); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: `${playgroundUrl}/mobile-info.html` }, + }); + + expect(navigateResult.isError).toBe(false); + const content = navigateResult.content as Array<{ type: string; text: string }>; + const responseText = content[0].text; + + expect(responseText).toContain("Viewport width: 1024"); + // Viewport height is less than window height due to browser chrome + expect(responseText).toContain("Viewport height: 576"); + }); + + it("should launch browser with windowSize null to reset to default", async () => { + const launchResult = await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: null, + }, + }); + + expect(launchResult.isError).toBe(false); + const launchContent = launchResult.content as Array<{ type: string; text: string }>; + expect(launchContent[0].text).toBe("Successfully launched browser session"); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: `${playgroundUrl}/mobile-info.html` }, + }); + + expect(navigateResult.isError).toBe(false); + const content = navigateResult.content as Array<{ type: string; text: string }>; + expect(content[0].text).toContain("✅ Successfully navigated to"); + }); + }); + + describe("grid URL configuration", () => { + it("should handle invalid gridUrl with understandable error message", async () => { + const result = await client.callTool({ + name: "launchBrowser", + arguments: { + gridUrl: "http://localhost:9999", + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toMatch( + /Error launching browser.*Unable to connect to.*http:\/\/localhost:9999/s, + ); + }); + }); + + describe("browser replacement", () => { + it("should close existing browser before launching new one", async () => { + const firstBrowser: WebdriverIO.Browser & { getDriverPid?: () => number | undefined } = + await launchBrowser({ + headless: "new", + desiredCapabilities: { + "goog:chromeOptions": { + args: process.env.DISABLE_BROWSER_SANDBOX + ? ["--no-sandbox", "--disable-dev-shm-usage"] + : [], + }, + }, + }); + const firstDriverPid = (await firstBrowser.getDriverPid!()) as number; + + const attachResult = await client.callTool({ + name: "attachToBrowser", + arguments: { + session: { + sessionId: firstBrowser.sessionId, + sessionCaps: firstBrowser.capabilities, + sessionOpts: { + capabilities: firstBrowser.capabilities, + ...firstBrowser.options, + }, + driverPid: firstDriverPid, + }, + }, + }); + + expect(attachResult.isError).toBe(false); + expect(checkProcessExists(firstDriverPid)).toBe(true); + + const launchResult = await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: { width: 800, height: 600 }, + }, + }); + + expect(launchResult.isError).toBe(false); + const launchContent = launchResult.content as Array<{ type: string; text: string }>; + expect(launchContent[0].text).toBe("Successfully launched browser session"); + + await firstBrowser.pause(100); + + expect(checkProcessExists(firstDriverPid)).toBe(false); + + const navigateResult = await client.callTool({ + name: "navigate", + arguments: { url: `${playgroundUrl}/mobile-info.html` }, + }); + + expect(navigateResult.isError).toBe(false); + const content = navigateResult.content as Array<{ type: string; text: string }>; + const responseText = content[0].text; + + expect(responseText).toContain("Viewport width: 800"); + // Viewport height is less than window height due to browser chrome + expect(responseText).toContain("Viewport height: 408"); + }); + }); + + describe("error handling", () => { + it("should reject unsupported browser name with actionable error message", async () => { + const result = await client.callTool({ + name: "launchBrowser", + arguments: { + desiredCapabilities: { + browserName: "unsupportedBrowser", + }, + }, + }); + + expect(result.isError).toBe(true); + const content = result.content as Array<{ type: string; text: string }>; + expect(content[0].text).toMatch( + /Error launching browser.*Running browser "unsupportedBrowser" is unsupported.*Supported browsers/s, + ); + }); + + it("should reject invalid browserName type in desiredCapabilities", async () => { + try { + await client.callTool({ + name: "launchBrowser", + arguments: { + desiredCapabilities: { + browserName: 123, + }, + }, + }); + expect.fail("Expected launchBrowser with invalid browserName to fail"); + } catch (error) { + expect(String(error)).toContain('\\"browserName\\" must be a string'); + } + }); + + it("should reject invalid windowSize format", async () => { + try { + await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: "invalid-format", + }, + }); + expect.fail("Expected launchBrowser with invalid windowSize to fail"); + } catch (error) { + expect(String(error)).toContain('should use the format \\"x\\"'); + } + }); + + it("should reject windowSize with negative dimensions", async () => { + try { + await client.callTool({ + name: "launchBrowser", + arguments: { + windowSize: { width: -100, height: 600 }, + }, + }); + expect.fail("Expected launchBrowser with negative width to fail"); + } catch (error) { + expect(String(error)).toContain("Number must be greater than 0"); + } + }); + }); + }, + INTEGRATION_TEST_TIMEOUT, +); From fe5f6b483dd057dff2b29e4b947a44adb23ecdca Mon Sep 17 00:00:00 2001 From: shadowusr Date: Sun, 12 Oct 2025 13:33:05 +0300 Subject: [PATCH 2/4] test: fix tests on example com --- test/tools/launch-browser.test.ts | 1 - test/tools/navigate.test.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/tools/launch-browser.test.ts b/test/tools/launch-browser.test.ts index 4087228..435e51d 100644 --- a/test/tools/launch-browser.test.ts +++ b/test/tools/launch-browser.test.ts @@ -139,7 +139,6 @@ describe( }, }); - expect(launchResult.isError).toBe(false); const launchContent = launchResult.content as Array<{ type: string; text: string }>; expect(launchContent[0].text).toBe("Successfully launched browser session"); diff --git a/test/tools/navigate.test.ts b/test/tools/navigate.test.ts index 9bd8416..837bd90 100644 --- a/test/tools/navigate.test.ts +++ b/test/tools/navigate.test.ts @@ -59,7 +59,9 @@ describe( expect(responseText).toContain("Title: Example Domain"); expect(responseText).toContain("## Current Tab Snapshot"); - expect(responseText).toContain("This domain is for use in illustrative examples in documents."); + expect(responseText).toContain( + "This domain is for use in documentation examples without needing permission. Avoid use in operations.", + ); }); }); From 82e504e7ce0dac09f3e75fbb43a9bffa5cd8e64a Mon Sep 17 00:00:00 2001 From: shadowusr Date: Sun, 12 Oct 2025 13:44:26 +0300 Subject: [PATCH 3/4] test: fix launch-browser test --- test/tools/launch-browser.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/tools/launch-browser.test.ts b/test/tools/launch-browser.test.ts index 435e51d..20736aa 100644 --- a/test/tools/launch-browser.test.ts +++ b/test/tools/launch-browser.test.ts @@ -111,7 +111,6 @@ describe( }, }); - expect(launchResult.isError).toBe(false); const launchContent = launchResult.content as Array<{ type: string; text: string }>; expect(launchContent[0].text).toBe("Successfully launched browser session"); From 90509037fcfbfb2d55e10a2618a96141ed363f09 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Sun, 12 Oct 2025 14:02:52 +0300 Subject: [PATCH 4/4] fix: correctly merge sandbox args with user-provided capabilities --- src/browser-context.ts | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/browser-context.ts b/src/browser-context.ts index bf67ff8..e530101 100644 --- a/src/browser-context.ts +++ b/src/browser-context.ts @@ -12,11 +12,36 @@ export interface BrowserOptions { const getSandboxArgs = (): string[] => process.env.DISABLE_BROWSER_SANDBOX ? ["--no-sandbox", "--disable-dev-shm-usage", "--disable-web-security"] : []; -const buildSandboxCapabilities = (sandboxArgs: string[]): StandaloneBrowserOptionsInput["desiredCapabilities"] => ({ - "goog:chromeOptions": { - args: sandboxArgs, - }, -}); +const mergeSandboxArgs = ( + desiredCapabilities: StandaloneBrowserOptionsInput["desiredCapabilities"], + sandboxArgs: string[], +): StandaloneBrowserOptionsInput["desiredCapabilities"] => { + if (!sandboxArgs.length) { + return desiredCapabilities; + } + + if (!desiredCapabilities) { + return { + "goog:chromeOptions": { + args: sandboxArgs, + }, + }; + } + + const chromeOptions = desiredCapabilities["goog:chromeOptions"] as Record | undefined; + const existingArgs = (chromeOptions?.args as string[]) || []; + + const mergedArgs = [...existingArgs, ...sandboxArgs]; + const uniqueArgs = Array.from(new Set(mergedArgs)); + + return { + ...desiredCapabilities, + "goog:chromeOptions": { + ...(chromeOptions || {}), + args: uniqueArgs, + }, + }; +}; export class BrowserContext { protected _browser: WdioBrowser | null = null; @@ -42,9 +67,7 @@ export class BrowserContext { console.error("Launch browser"); const sandboxArgs = getSandboxArgs(); - const desiredCapabilities = - this._options.desiredCapabilities ?? - (sandboxArgs.length ? buildSandboxCapabilities(sandboxArgs) : undefined); + const desiredCapabilities = mergeSandboxArgs(this._options.desiredCapabilities, sandboxArgs); this._browser = await launchBrowser({ headless: this._options.headless ? "new" : false,