diff --git a/src/worker/runner/test-runner/index.js b/src/worker/runner/test-runner/index.js index 7a022dadf..df9577c29 100644 --- a/src/worker/runner/test-runner/index.js +++ b/src/worker/runner/test-runner/index.js @@ -14,6 +14,7 @@ const { filterExtraStackFrames } = require("../../../browser/stacktrace/utils"); const { extendWithCodeSnippet } = require("../../../error-snippets"); const { startSelectivity } = require("../../../browser/cdp/selectivity"); const { noopProfilerRuntime } = require("../../../profiler/runtime/noop"); +const { flushSessionManagerErrors } = require("@testplane/webdriverio"); const SNAPSHOTS_TIMEOUT_MS = 10000; const SNAPSHOTS_WARNING_TIMEOUT_MS = 2000; @@ -108,6 +109,25 @@ module.exports = class TestRunner { async finishRun(error) { const testplaneCtx = this._test.testplaneCtx; const { callstackHistory } = this._browser; + let additionalContextError; + + // Older WebdriverIO versions do not expose the test-boundary check. + if (typeof flushSessionManagerErrors === "function") { + try { + await flushSessionManagerErrors(this._browser.publicAPI); + } catch (contextError) { + // A failed context update may leave the session pointing at a stale frame. + // The master pool discards browsers marked as broken instead of reusing them. + if (!this._browser.state.isBroken) { + this._browser.markAsBroken({ stubBrowserCommands: true }); + } + if (error && error !== contextError) { + additionalContextError = contextError; + } else { + error = contextError; + } + } + } const assertViewResults = testplaneCtx.assertViewResults; if (!error && assertViewResults && assertViewResults.hasFails()) { @@ -172,6 +192,10 @@ module.exports = class TestRunner { // The original cause must be removed to avoid possible duplicates later. delete error.cause; + if (additionalContextError) { + error.stack += `\n\nAdditional context manager error: ${additionalContextError.stack}`; + } + await extendWithCodeSnippet(error); throw Object.assign(error, results); diff --git a/test/src/worker/runner/test-runner/index.js b/test/src/worker/runner/test-runner/index.js index 42b517a8f..bbfdc938d 100644 --- a/test/src/worker/runner/test-runner/index.js +++ b/test/src/worker/runner/test-runner/index.js @@ -19,6 +19,7 @@ describe("worker/runner/test-runner", () => { const sandbox = sinon.createSandbox(); let historyRunGroupStub; let captureFailScreenshotStub; + let flushSessionManagerErrorsStub; let TestRunner; const mkTest_ = (opts = {}) => { @@ -91,6 +92,7 @@ describe("worker/runner/test-runner", () => { beforeEach(() => { historyRunGroupStub = sandbox.stub().callsFake(history.runGroup); captureFailScreenshotStub = sandbox.stub().resolves(null); + flushSessionManagerErrorsStub = sandbox.stub().resolves(); TestRunner = proxyquire("src/worker/runner/test-runner", { "../../../browser/history": { @@ -102,6 +104,9 @@ describe("worker/runner/test-runner", () => { "./capture-fail-screenshot": { captureFailScreenshot: captureFailScreenshotStub, }, + "@testplane/webdriverio": { + flushSessionManagerErrors: flushSessionManagerErrorsStub, + }, }); sandbox.stub(BrowserAgent.prototype, "getBrowser").resolves(mkBrowser_()); @@ -838,6 +843,21 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWith(BrowserAgent.prototype.freeBrowser, browser); }); + it("should fail on a context manager error after the last command", async () => { + const browser = mkBrowser_(); + BrowserAgent.prototype.getBrowser.resolves(browser); + flushSessionManagerErrorsStub.rejects(new Error("late context failure")); + + await assert.isRejected(run_(), "late context failure"); + + assert.calledOnceWithExactly(flushSessionManagerErrorsStub, browser.publicAPI); + assert.calledOnceWithExactly(BrowserAgent.prototype.freeBrowser, browser); + assert.callOrder(flushSessionManagerErrorsStub, BrowserAgent.prototype.freeBrowser); + assert.isTrue(browser.state.isLastTestFailed); + assert.calledOnceWithExactly(browser.markAsBroken, { stubBrowserCommands: true }); + assert.isTrue(browser.state.isBroken); + }); + it("should release browser after all browser data has been used", async () => { ExecutionThread.create.callsFake(({ browser }) => { ExecutionThread.prototype.run.callsFake(() => { @@ -976,6 +996,19 @@ describe("worker/runner/test-runner", () => { assert.calledOnceWith(BrowserAgent.prototype.freeBrowser, browser); }); + it("should retain the original test error when context manager also fails", async () => { + const browser = mkBrowser_(); + BrowserAgent.prototype.getBrowser.resolves(browser); + ExecutionThread.prototype.run.rejects(new Error("test failure")); + flushSessionManagerErrorsStub.rejects(new Error("context failure")); + + const error = await run_().catch(err => err); + assert.include(error.message, "test failure"); + assert.include(error.stack, "context failure"); + assert.calledOnce(flushSessionManagerErrorsStub); + assert.calledOnceWithExactly(browser.markAsBroken, { stubBrowserCommands: true }); + }); + [SAVE_HISTORY_MODE.ONLY_FAILED, SAVE_HISTORY_MODE.ALL].forEach(saveHistoryMode => { it(`should pass history if "saveHistory" is ${saveHistoryMode}`, async () => { const history = Symbol();