From ba3c7e94a1922e6b92a0c7958fb8d8dfb9e379ea Mon Sep 17 00:00:00 2001 From: Egor Ivanov Date: Mon, 16 Jun 2025 16:10:08 +0200 Subject: [PATCH] feat: add keep-browser CLI options for session persistence (#1096) --- docs/debugging.md | 59 ++++++++++++++ src/browser-installer/chrome/index.ts | 19 ++++- src/browser-installer/edge/index.ts | 14 +++- src/browser-installer/firefox/index.ts | 14 +++- src/browser-installer/safari/index.ts | 13 +++- src/browser/browser.ts | 2 + src/cli/index.ts | 8 ++ src/runner/test-runner/regular-test-runner.js | 36 +++++++++ src/test-reader/index.ts | 11 ++- src/testplane.ts | 32 ++++++-- src/worker/runner/test-runner/index.js | 4 + test/src/cli/index.js | 35 +++++++++ .../runner/test-runner/regular-test-runner.js | 76 +++++++++++++++++++ test/src/testplane.js | 17 ++++- test/src/worker/runner/test-runner/index.js | 18 +++++ 15 files changed, 334 insertions(+), 24 deletions(-) diff --git a/docs/debugging.md b/docs/debugging.md index cba315417..7c660cde3 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -7,3 +7,62 @@ Testpalne supports [OpenTelemetry](https://opentelemetry.io) standard for tracin You can use `traceparent` to trace the execution of individual tests. +### Keep Browser for Debugging + +Testplane provides CLI options to keep browser sessions alive after test completion for debugging purposes. This feature has two main use cases: + +1. **Local debugging**: After test fails, you can still interact with the browser and see what's wrong +2. **AI agents integration**: Let AI agents run tests to perform complex logic (e.g. custom authentication), then attach to browser via MCP and perform additional actions + +#### Why not REPL? + +While REPL mode pauses test execution for interactive debugging, keep-browser options preserve the final browser state after tests finish. For AI agents, it's much easier to say "write test with the same beforeEach as in this file and run it to prepare browser" rather than forcing AI to use REPL interactively. + +#### `--keep-browser` + +Keep browser session alive after test completion for debugging. + +```bash +npx testplane --keep-browser --browser chrome tests/login.test.js +``` + +#### `--keep-browser-on-fail` + +Keep browser session alive only when test fails for debugging. + +```bash +npx testplane --keep-browser-on-fail --browser chrome tests/login.test.js +``` + +**Note**: These options work only when running a single test in a single browser. + +#### Session Information + +When a browser is kept alive, Testplane outputs a message with session information for programmatic access: + +``` +Testplane run has finished, but the browser won't be closed, because you passed the --keep-browser argument. +You may attach to this browser using the following capabilities: +{ + "sessionId": "abc123...", + "capabilities": { + "browserName": "chrome", + "debuggerAddress": "127.0.0.1:9222" + }, + "sessionOptions": { + "hostname": "127.0.0.1", + "port": 4444, + "path": "/wd/hub", + "protocol": "http" + } +} +``` + +#### Attaching to Kept Session + +You can use the outputted session information to attach to the kept browser through: + +- **MCP (Model Context Protocol)** tools that support WebDriver session attachment +- **CDP (Chrome DevTools Protocol)** using the `debuggerAddress` from capabilities +- **Direct WebDriver** connection using the session ID and connection details +- **Custom automation tools** that can reuse existing browser sessions \ No newline at end of file diff --git a/src/browser-installer/chrome/index.ts b/src/browser-installer/chrome/index.ts index 301551d03..aee560eec 100644 --- a/src/browser-installer/chrome/index.ts +++ b/src/browser-installer/chrome/index.ts @@ -7,13 +7,14 @@ import { getMilestone } from "../utils"; import { installChrome, resolveLatestChromeVersion } from "./browser"; import { installChromeDriver } from "./driver"; import { isUbuntu, getUbuntuLinkerEnv } from "../ubuntu-packages"; +import RuntimeConfig from "../../config/runtime-config"; export { installChrome, resolveLatestChromeVersion, installChromeDriver }; export const runChromeDriver = async ( chromeVersion: string, { debug = false } = {}, -): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { +): Promise<{ gridUrl: string; process: ChildProcess; port: number; kill: () => void }> => { const [chromeDriverPath, randomPort, chromeDriverEnv] = await Promise.all([ installChromeDriver(chromeVersion), getPort(), @@ -22,9 +23,12 @@ export const runChromeDriver = async ( .then(extraEnv => (extraEnv ? { ...process.env, ...extraEnv } : process.env)), ]); + const runtimeConfig = RuntimeConfig.getInstance(); + const keepBrowserModeEnabled = runtimeConfig.keepBrowserMode.enabled; + const chromeDriver = spawn(chromeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { windowsHide: true, - detached: false, + detached: keepBrowserModeEnabled || false, env: chromeDriverEnv, }); @@ -34,9 +38,16 @@ export const runChromeDriver = async ( const gridUrl = `http://127.0.0.1:${randomPort}`; - process.once("exit", () => chromeDriver.kill()); + if (!keepBrowserModeEnabled) { + process.once("exit", () => chromeDriver.kill()); + } await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); - return { gridUrl, process: chromeDriver, port: randomPort }; + return { + gridUrl, + process: chromeDriver, + port: randomPort, + kill: () => chromeDriver.kill(), + }; }; diff --git a/src/browser-installer/edge/index.ts b/src/browser-installer/edge/index.ts index 62d9abff1..3928f3fb7 100644 --- a/src/browser-installer/edge/index.ts +++ b/src/browser-installer/edge/index.ts @@ -4,6 +4,7 @@ import getPort from "get-port"; import waitPort from "wait-port"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; +import RuntimeConfig from "../../config/runtime-config"; export { resolveEdgeVersion } from "./browser"; export { installEdgeDriver }; @@ -11,12 +12,15 @@ export { installEdgeDriver }; export const runEdgeDriver = async ( edgeVersion: string, { debug = false }: { debug?: boolean } = {}, -): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { +): Promise<{ gridUrl: string; process: ChildProcess; port: number; kill: () => void }> => { const [edgeDriverPath, randomPort] = await Promise.all([installEdgeDriver(edgeVersion), getPort()]); + const runtimeConfig = RuntimeConfig.getInstance(); + const keepBrowserModeEnabled = runtimeConfig.keepBrowserMode.enabled; + const edgeDriver = spawn(edgeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { windowsHide: true, - detached: false, + detached: keepBrowserModeEnabled || false, }); if (debug) { @@ -25,9 +29,11 @@ export const runEdgeDriver = async ( const gridUrl = `http://127.0.0.1:${randomPort}`; - process.once("exit", () => edgeDriver.kill()); + if (!keepBrowserModeEnabled) { + process.once("exit", () => edgeDriver.kill()); + } await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); - return { gridUrl, process: edgeDriver, port: randomPort }; + return { gridUrl, process: edgeDriver, port: randomPort, kill: () => edgeDriver.kill() }; }; diff --git a/src/browser-installer/firefox/index.ts b/src/browser-installer/firefox/index.ts index c661eb9b3..c1818d409 100644 --- a/src/browser-installer/firefox/index.ts +++ b/src/browser-installer/firefox/index.ts @@ -7,13 +7,14 @@ import { installLatestGeckoDriver } from "./driver"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; import { getUbuntuLinkerEnv, isUbuntu } from "../ubuntu-packages"; +import RuntimeConfig from "../../config/runtime-config"; export { installFirefox, resolveLatestFirefoxVersion, installLatestGeckoDriver }; export const runGeckoDriver = async ( firefoxVersion: string, { debug = false } = {}, -): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { +): Promise<{ gridUrl: string; process: ChildProcess; port: number; kill: () => void }> => { const [geckoDriverPath, randomPort, geckoDriverEnv] = await Promise.all([ installLatestGeckoDriver(firefoxVersion), getPort(), @@ -22,13 +23,16 @@ export const runGeckoDriver = async ( .then(extraEnv => (extraEnv ? { ...process.env, ...extraEnv } : process.env)), ]); + const runtimeConfig = RuntimeConfig.getInstance(); + const keepBrowserModeEnabled = runtimeConfig.keepBrowserMode.enabled; + const geckoDriver = await startGeckoDriver({ customGeckoDriverPath: geckoDriverPath, port: randomPort, log: debug ? "debug" : "fatal", spawnOpts: { windowsHide: true, - detached: false, + detached: keepBrowserModeEnabled || false, env: geckoDriverEnv, }, }); @@ -39,9 +43,11 @@ export const runGeckoDriver = async ( const gridUrl = `http://127.0.0.1:${randomPort}`; - process.once("exit", () => geckoDriver.kill()); + if (!keepBrowserModeEnabled) { + process.once("exit", () => geckoDriver.kill()); + } await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); - return { gridUrl, process: geckoDriver, port: randomPort }; + return { gridUrl, process: geckoDriver, port: randomPort, kill: () => geckoDriver.kill() }; }; diff --git a/src/browser-installer/safari/index.ts b/src/browser-installer/safari/index.ts index e406170e6..d1a8bfebd 100644 --- a/src/browser-installer/safari/index.ts +++ b/src/browser-installer/safari/index.ts @@ -3,6 +3,7 @@ import getPort from "get-port"; import waitPort from "wait-port"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT, SAFARIDRIVER_PATH } from "../constants"; +import RuntimeConfig from "../../config/runtime-config"; export { resolveSafariVersion } from "./browser"; @@ -10,12 +11,16 @@ export const runSafariDriver = async ({ debug = false }: { debug?: boolean } = { gridUrl: string; process: ChildProcess; port: number; + kill: () => void; }> => { const randomPort = await getPort(); + const runtimeConfig = RuntimeConfig.getInstance(); + const keepBrowserModeEnabled = runtimeConfig.keepBrowserMode.enabled; + const safariDriver = spawn(SAFARIDRIVER_PATH, [`--port=${randomPort}`], { windowsHide: true, - detached: false, + detached: keepBrowserModeEnabled || false, }); if (debug) { @@ -24,9 +29,11 @@ export const runSafariDriver = async ({ debug = false }: { debug?: boolean } = { const gridUrl = `http://127.0.0.1:${randomPort}`; - process.once("exit", () => safariDriver.kill()); + if (!keepBrowserModeEnabled) { + process.once("exit", () => safariDriver.kill()); + } await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); - return { gridUrl, process: safariDriver, port: randomPort }; + return { gridUrl, process: safariDriver, port: randomPort, kill: () => safariDriver.kill() }; }; diff --git a/src/browser/browser.ts b/src/browser/browser.ts index a7f304561..5b9f521f3 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -39,6 +39,7 @@ export type BrowserState = { testXReqId?: string; traceparent?: string; isBroken?: boolean; + isLastTestFailed?: boolean; }; export type CustomCommand = { name: string; elementScope: boolean }; @@ -76,6 +77,7 @@ export class Browser { this._state = { ...opts.state, isBroken: false, + isLastTestFailed: false, }; this._customCommands = new Set(); this._wdPool = opts.wdPool; diff --git a/src/cli/index.ts b/src/cli/index.ts index 2ce078cab..73ac4220a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -77,6 +77,8 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise => { .option("--repl-on-fail [type]", "open repl interface on test fail only", Boolean, false) .option("--devtools", "switches the browser to the devtools mode with using CDP protocol") .option("--local", "use local browsers, managed by testplane (same as 'gridUrl': 'local')") + .option("--keep-browser", "do not close browser session after test completion") + .option("--keep-browser-on-fail", "do not close browser session when test fails") .arguments("[paths...]") .action(async (paths: string[]) => { try { @@ -93,6 +95,8 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise => { replOnFail, devtools, local, + keepBrowser, + keepBrowserOnFail, } = program; const isTestsSuccess = await testplane.run(paths, { @@ -110,6 +114,10 @@ export const run = async (opts: TestplaneRunOpts = {}): Promise => { }, devtools: devtools || false, local: local || false, + keepBrowserMode: { + enabled: keepBrowser || keepBrowserOnFail || false, + onFail: keepBrowserOnFail || false, + }, }); process.exit(isTestsSuccess ? 0 : 1); diff --git a/src/runner/test-runner/regular-test-runner.js b/src/runner/test-runner/regular-test-runner.js index c815eea65..c75eeb45c 100644 --- a/src/runner/test-runner/regular-test-runner.js +++ b/src/runner/test-runner/regular-test-runner.js @@ -6,6 +6,7 @@ const { Runner } = require("../runner"); const logger = require("../../utils/logger"); const { MasterEvents } = require("../../events"); const AssertViewResults = require("../../browser/commands/assert-view/assert-view-results"); +const RuntimeConfig = require("../../config/runtime-config"); module.exports = class RegularTestRunner extends Runner { constructor(test, browserAgent) { @@ -121,6 +122,19 @@ module.exports = class RegularTestRunner extends Runner { return; } + const runtimeConfig = RuntimeConfig.getInstance(); + const keepBrowserMode = runtimeConfig.keepBrowserMode; + + if (keepBrowserMode?.enabled) { + const hasError = !!this._test.err || !!browserState?.isLastTestFailed; + const shouldKeep = keepBrowserMode.onFail ? hasError : true; + + if (shouldKeep) { + this._logKeepBrowserInfo(); + return; + } + } + const browser = this._browser; this._browser = null; @@ -132,4 +146,26 @@ module.exports = class RegularTestRunner extends Runner { logger.warn(`WARNING: can not release browser: ${error}`); } } + + _logKeepBrowserInfo() { + if (!this._browser) { + return; + } + + logger.log( + "Testplane run has finished, but the browser won't be closed, because you passed the --keep-browser argument.", + ); + logger.log("You may attach to this browser using the following capabilities:"); + logger.log( + JSON.stringify( + { + sessionId: this._browser.sessionId, + capabilities: this._browser.capabilities, + sessionOptions: this._browser.publicAPI.options, + }, + null, + 2, + ), + ); + } }; diff --git a/src/test-reader/index.ts b/src/test-reader/index.ts index 3d983761f..655eac77f 100644 --- a/src/test-reader/index.ts +++ b/src/test-reader/index.ts @@ -62,13 +62,18 @@ export class TestReader extends EventEmitter { function validateTests(testsByBro: Record, options: TestReaderOpts): void { const tests = _.flatten(Object.values(testsByBro)); - if (options.replMode?.enabled) { + const singleTestModes = [ + { condition: options.replMode?.enabled, name: "repl mode" }, + { condition: options.keepBrowserMode?.enabled, name: "keep-browser mode" }, + ].filter(mode => mode.condition); + + for (const mode of singleTestModes) { const testsToRun = tests.filter(test => !test.disabled && !test.pending); const browsersToRun = _.uniq(testsToRun.map(test => test.browserId)); if (testsToRun.length !== 1) { throw new Error( - `In repl mode only 1 test in 1 browser should be run, but found ${testsToRun.length} tests` + + `In ${mode.name} only 1 test in 1 browser should be run, but found ${testsToRun.length} tests` + `${testsToRun.length === 0 ? ". " : ` that run in ${browsersToRun.join(", ")} browsers. `}` + `Try to specify cli-options: "--grep" and "--browser" or use "testplane.only.in" in the test file.`, ); @@ -79,7 +84,7 @@ function validateTests(testsByBro: Record, options: TestReaderOp return; } - const stringifiedOpts = convertOptions(_.omit(options, "replMode")); + const stringifiedOpts = convertOptions(_.omit(options, "replMode", "replMode")); if (_.isEmpty(stringifiedOpts)) { throw new Error(`There are no tests found. Try to specify [${Object.keys(options).join(", ")}] options`); } else { diff --git a/src/testplane.ts b/src/testplane.ts index 137b1b0ec..bbfe35f91 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -37,6 +37,10 @@ interface RunOpts { }; devtools: boolean; local: boolean; + keepBrowserMode: { + enabled: boolean; + onFail: boolean; + }; } export type FailedListItem = { @@ -49,7 +53,7 @@ interface RunnableOpts { saveLocations?: boolean; } -export interface ReadTestsOpts extends Pick { +export interface ReadTestsOpts extends Pick { silent: boolean; ignore: string | string[]; failed: FailedListItem[]; @@ -101,12 +105,21 @@ export class Testplane extends BaseTestplane { replMode, devtools, local, + keepBrowserMode, reporters = [], }: Partial = {}, ): Promise { validateUnknownBrowsers(browsers!, _.keys(this._config.browsers)); - RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode, replMode, devtools, local }); + RuntimeConfig.getInstance().extend({ + updateRefs, + requireModules, + inspectMode, + replMode, + devtools, + local, + keepBrowserMode, + }); if (replMode?.enabled) { this._config.system.mochaOpts.timeout = 0; @@ -135,7 +148,7 @@ export class Testplane extends BaseTestplane { await this._init(); runner.init(); await runner.run( - await this._readTests(testPaths, { browsers, sets, grep, replMode }), + await this._readTests(testPaths, { browsers, sets, grep, replMode, keepBrowserMode }), RunnerStats.create(this), ); @@ -159,7 +172,7 @@ export class Testplane extends BaseTestplane { async readTests( testPaths: string[], - { browsers, sets, grep, silent, ignore, replMode, runnableOpts }: Partial = {}, + { browsers, sets, grep, silent, ignore, replMode, keepBrowserMode, runnableOpts }: Partial = {}, ): Promise { const testReader = TestReader.create(this._config); @@ -172,7 +185,16 @@ export class Testplane extends BaseTestplane { ]); } - const specs = await testReader.read({ paths: testPaths, browsers, ignore, sets, grep, replMode, runnableOpts }); + const specs = await testReader.read({ + paths: testPaths, + browsers, + ignore, + sets, + grep, + replMode, + keepBrowserMode, + runnableOpts, + }); const collection = TestCollection.create(specs); collection.getBrowsers().forEach(bro => { diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index ea6c44ccd..d6c90eef7 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -93,6 +93,10 @@ module.exports = class TestRunner extends Runner { break; } + if (error && this._browser.state) { + this._browser.state.isLastTestFailed = true; + } + this._browserAgent.freeBrowser(this._browser); if (error) { diff --git a/test/src/cli/index.js b/test/src/cli/index.js index 987216e10..82a1d49b4 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -315,6 +315,41 @@ describe("cli", () => { }); }); + describe("keep browser mode", () => { + it("should be disabled by default", async () => { + await run_(); + + assert.calledWithMatch(Testplane.prototype.run, any, { + keepBrowserMode: { + enabled: false, + onFail: false, + }, + }); + }); + + it('should be enabled when specify "keep-browser" flag', async () => { + await run_("--keep-browser"); + + assert.calledWithMatch(Testplane.prototype.run, any, { + keepBrowserMode: { + enabled: true, + onFail: false, + }, + }); + }); + + it('should be enabled when specify "keep-browser-on-fail" flag', async () => { + await run_("--keep-browser-on-fail"); + + assert.calledWithMatch(Testplane.prototype.run, any, { + keepBrowserMode: { + enabled: true, + onFail: true, + }, + }); + }); + }); + it("should turn on devtools mode from cli", async () => { await run_("--devtools"); diff --git a/test/src/runner/test-runner/regular-test-runner.js b/test/src/runner/test-runner/regular-test-runner.js index 4473aff02..a254b9697 100644 --- a/test/src/runner/test-runner/regular-test-runner.js +++ b/test/src/runner/test-runner/regular-test-runner.js @@ -11,6 +11,7 @@ const { Test } = require("src/test-reader/test-object"); const { promiseDelay } = require("../../../../src/utils/promise"); const { EventEmitter } = require("events"); const proxyquire = require("proxyquire"); +const RuntimeConfig = require("src/config/runtime-config"); describe("runner/test-runner/regular-test-runner", () => { const sandbox = sinon.createSandbox(); @@ -68,6 +69,7 @@ describe("runner/test-runner/regular-test-runner", () => { RegularTestRunner = proxyquire("src/runner/test-runner/regular-test-runner", { "../../utils/logger": { warn: sandbox.stub(), + log: sandbox.stub(), }, }); @@ -83,6 +85,8 @@ describe("runner/test-runner/regular-test-runner", () => { sandbox.stub(crypto, "randomBytes").callsFake(size => { return Buffer.from("11".repeat(size), "hex"); }); + + sandbox.stub(RuntimeConfig, "getInstance").returns({}); }); afterEach(() => sandbox.restore()); @@ -644,6 +648,78 @@ describe("runner/test-runner/regular-test-runner", () => { assert.calledOnce(BrowserAgent.prototype.freeBrowser); }); + + describe("keep-browser mode", () => { + it("should not release browser when --keep-browser is enabled", async () => { + RuntimeConfig.getInstance.returns({ + keepBrowserMode: { enabled: true, onFail: false }, + }); + + const browser = stubBrowser_({ sessionId: "100500" }); + BrowserAgent.prototype.getBrowser.resolves(browser); + + await runTest_({ + onRun: ({ workers }) => { + workers.emit(`worker.${browser.sessionId}.freeBrowser`); + }, + }); + + assert.notCalled(BrowserAgent.prototype.freeBrowser); + }); + + it("should not release browser when --keep-browser-on-fail is enabled and test fails", async () => { + RuntimeConfig.getInstance.returns({ + keepBrowserMode: { enabled: true, onFail: true }, + }); + + const browser = stubBrowser_({ sessionId: "100500" }); + BrowserAgent.prototype.getBrowser.resolves(browser); + + const workers = mkWorkers_(); + workers.runTest.callsFake(() => { + workers.emit(`worker.${browser.sessionId}.freeBrowser`, { isLastTestFailed: true }); + return Promise.reject(new Error("Test failed")); + }); + + await run_({ workers }); + + assert.notCalled(BrowserAgent.prototype.freeBrowser); + }); + + it("should release browser when --keep-browser-on-fail is enabled and test passes", async () => { + RuntimeConfig.getInstance.returns({ + keepBrowserMode: { enabled: true, onFail: true }, + }); + + const browser = stubBrowser_({ sessionId: "100500" }); + BrowserAgent.prototype.getBrowser.resolves(browser); + + await runTest_({ + onRun: ({ workers }) => { + workers.emit(`worker.${browser.sessionId}.freeBrowser`); + }, + }); + + assert.calledOnce(BrowserAgent.prototype.freeBrowser); + }); + + it("should release browser when keep-browser mode is disabled", async () => { + RuntimeConfig.getInstance.returns({ + keepBrowserMode: { enabled: false, onFail: false }, + }); + + const browser = stubBrowser_({ sessionId: "100500" }); + BrowserAgent.prototype.getBrowser.resolves(browser); + + await runTest_({ + onRun: ({ workers }) => { + workers.emit(`worker.${browser.sessionId}.freeBrowser`); + }, + }); + + assert.calledOnce(BrowserAgent.prototype.freeBrowser); + }); + }); }); }); }); diff --git a/test/src/testplane.js b/test/src/testplane.js index 00d955fd1..3ee07fa65 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -216,6 +216,10 @@ describe("testplane", () => { }, devtools: true, local: false, + keepBrowserMode: { + enabled: false, + onFail: false, + }, }); assert.calledOnce(RuntimeConfig.getInstance); @@ -226,6 +230,7 @@ describe("testplane", () => { replMode: { enabled: true }, devtools: true, local: false, + keepBrowserMode: { enabled: false, onFail: false }, }); assert.callOrder(RuntimeConfig.getInstance, NodejsEnvRunner.create); }); @@ -335,16 +340,24 @@ describe("testplane", () => { const grep = "baz.*"; const sets = ["set1", "set2"]; const replMode = { enabled: false }; + const keepBrowserMode = { enabled: false }; sandbox.spy(Testplane.prototype, "readTests"); - await runTestplane(testPaths, { browsers, grep, sets, replMode }); + await runTestplane(testPaths, { + browsers, + grep, + sets, + replMode, + keepBrowserMode, + }); assert.calledOnceWith(Testplane.prototype.readTests, testPaths, { browsers, grep, sets, replMode, + keepBrowserMode, }); }); @@ -652,6 +665,7 @@ describe("testplane", () => { sets: ["s1", "s2"], grep: "grep", replMode: { enabled: false }, + keepBrowserMode: { enabled: false, onFail: false }, runnableOpts: { saveLocations: true, }, @@ -664,6 +678,7 @@ describe("testplane", () => { sets: ["s1", "s2"], grep: "grep", replMode: { enabled: false }, + keepBrowserMode: { enabled: false, onFail: false }, runnableOpts: { saveLocations: true, }, diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index 1ad08e2a7..6d4be024e 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -915,4 +915,22 @@ describe("worker/runner/test-runner", () => { }); }); }); + + describe("isLastTestFailed flag initialization", () => { + it("should automatically reset isLastTestFailed flag when creating browser", async () => { + const browser = mkBrowser_(); + browser.state.isLastTestFailed = false; + BrowserAgent.prototype.getBrowser.resolves(browser); + + const runner = mkRunner_(); + await runner.prepareToRun({ + sessionId: "session-id", + sessionCaps: {}, + sessionOpts: {}, + state: { isLastTestFailed: true }, + }); + + assert.strictEqual(browser.state.isLastTestFailed, false); + }); + }); });