From 2b7f7e9120ec3fa72ec45aa308c1d1591d375ed5 Mon Sep 17 00:00:00 2001 From: Harshit Date: Mon, 24 Aug 2026 17:20:26 +0530 Subject: [PATCH 1/2] fix(automate): name the session created by a mid-test reload (SDK-7270) Follow-up to #148. @wdio/mocha-framework binds beforeTest to the test function itself (wrapGlobalTestMethod), so onBeforeTest is the single per-test naming opportunity and it has already passed by the time the test body calls browser.reloadSession(). The replacement session is never registered in sessionMap, so flushSessionName() returns early on !sessionData and the onAfterExecute sweep -- which iterates the same map -- misses it too. service.onReload learns the new session id but only renames the outgoing one, leaving the live session on its creation-time sessionName capability. onAfterTest now re-resolves the live session id and adopts it into sessionMap before flushing the name, while that session is still open. The naming block is gated on skipSessionName rather than skipSessionStatus and sits above the status gate: gating a name repair behind a status flag would skip it for setSessionStatus:false users, and adopting unconditionally would pull setSessionName:false users into sessionMap and start issuing them a status PUT per session where they previously had none. Adoption also records the test result, so the post-reload session's status is marked at teardown instead of being dropped by the old `if (sessionData)` guard. Steady state costs nothing extra -- appliedName de-dupes the flush when the session did not change. Co-Authored-By: Claude Opus 5 --- .../src/cli/modules/automateModule.ts | 39 +++++-- .../tests/cli/modules/automateModule.test.ts | 108 ++++++++++++++++++ 2 files changed, 140 insertions(+), 7 deletions(-) diff --git a/packages/browserstack-service/src/cli/modules/automateModule.ts b/packages/browserstack-service/src/cli/modules/automateModule.ts index 712b8b2..fcdc336 100644 --- a/packages/browserstack-service/src/cli/modules/automateModule.ts +++ b/packages/browserstack-service/src/cli/modules/automateModule.ts @@ -155,7 +155,7 @@ export default class AutomateModule extends BaseModule { const suiteTitle = args.suiteTitle as string const testContextOptions = this.config.testContextOptions as TestContextOptions - if (testContextOptions.skipSessionStatus || !isBrowserstackSession(browser)) { + if (!isBrowserstackSession(browser)) { this.logger.info('Skipping session status update as per configuration') return } @@ -176,14 +176,39 @@ export default class AutomateModule extends BaseModule { name = `${pre}${test.parent}${post}` } + // SDK-7270 (residual): the session can be REPLACED mid-test. @wdio/mocha-framework binds + // beforeTest to the test function itself (wrapGlobalTestMethod), so onBeforeTest is the one + // and only per-test naming opportunity and it has already passed by the time the test body + // calls browser.reloadSession(). The replacement session is therefore never registered in + // sessionMap and keeps its creation-time `sessionName` capability. (A reload in a beforeEach + // hook is fine — that runs before beforeTest.) Re-resolve the live session id here + // (service.onReload has already pointed KEY_FRAMEWORK_SESSION_ID at it) and adopt it while + // it is still open. + // + // Deliberately gated on skipSessionName, NOT skipSessionStatus: naming and status are + // independent options, so a `setSessionStatus: false` user must still get the name repair, + // and a `setSessionName: false` user must not be pulled into sessionMap — that would hand + // onAfterExecute a session to status-mark where it previously had none. + if (sessionId && !testContextOptions.skipSessionName && !this.sessionMap.has(sessionId)) { + this.sessionMap.set(sessionId, { lastTestName: name, testResults: new Map() }) + } + // No-op for the steady state: when no mid-test reload happened, onBeforeTest already + // applied this exact name and `appliedName` de-dupes it away — no extra API call. + await this.flushSessionName(sessionId) + + if (testContextOptions.skipSessionStatus) { + this.logger.info('Skipping session status update as per configuration') + return + } + + const testResult: TestResult = { + testName: name, + status: status, + reason: reason + } + const sessionData = this.sessionMap.get(sessionId) if (sessionData) { - const testResult: TestResult = { - testName: name, - status: status, - reason: reason - } - sessionData.testResults.set(name, testResult) this.sessionMap.set(sessionId, sessionData) } diff --git a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts index d3f47c4..3fca63a 100644 --- a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts @@ -210,6 +210,114 @@ describe('AutomateModule', () => { expect(TestFramework.setState).toHaveBeenCalledTimes(2) // status and reason }) + it('names the session created by a mid-test reload (SDK-7270)', async () => { + // browser.reloadSession() from the test body runs AFTER mocha's EVENT_TEST_BEGIN, so + // onBeforeTest named the old session and the replacement was never registered. + vi.mocked(fetch).mockResolvedValue({ + json: vi.fn().mockResolvedValue({ success: true }) + } as any) + + const mockArgs = { + instance: mockTestInstance, + result: { error: null, passed: true }, + test: { title: 'test title', parent: 'suite title' }, + suiteTitle: 'suite title' + } + + await automateModule.onBeforeTest(mockArgs) + + // service.onReload repoints KEY_FRAMEWORK_SESSION_ID at the replacement session. + vi.mocked(AutomationFramework.getState).mockImplementation((instance, key) => { + if (key === 'framework_session_id') {return 'reloaded-session-id'} + if (key.includes('CAPABILITIES')) {return { browserName: 'chrome' }} + return {} + }) + vi.mocked(fetch).mockClear() + + await automateModule.onAfterTest(mockArgs) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('sessions/reloaded-session-id.json'), + expect.objectContaining({ method: 'PUT', body: JSON.stringify({ name: 'suite title - test title' }) }) + ) + }) + + it('issues no extra name call when the session is unchanged across the test (SDK-7270 de-dupe)', async () => { + vi.mocked(fetch).mockResolvedValue({ + json: vi.fn().mockResolvedValue({ success: true }) + } as any) + + const mockArgs = { + instance: mockTestInstance, + result: { error: null, passed: true }, + test: { title: 'test title', parent: 'suite title' }, + suiteTitle: 'suite title' + } + + await automateModule.onBeforeTest(mockArgs) + vi.mocked(fetch).mockClear() + + await automateModule.onAfterTest(mockArgs) + + // appliedName de-dupes — steady state must cost zero additional API calls. + expect(fetch).not.toHaveBeenCalled() + }) + + it('still names a mid-test-reload session when skipSessionStatus is true (SDK-7270 guard independence)', async () => { + // Naming and status are independent options; a status flag must not gate the name repair. + (automateModule.config as any).testContextOptions.skipSessionStatus = true + vi.mocked(fetch).mockResolvedValue({ + json: vi.fn().mockResolvedValue({ success: true }) + } as any) + + const mockArgs = { + instance: mockTestInstance, + result: { error: null, passed: true }, + test: { title: 'test title', parent: 'suite title' }, + suiteTitle: 'suite title' + } + + await automateModule.onBeforeTest(mockArgs) + + vi.mocked(AutomationFramework.getState).mockImplementation((instance, key) => { + if (key === 'framework_session_id') {return 'reloaded-session-id'} + if (key.includes('CAPABILITIES')) {return { browserName: 'chrome' }} + return {} + }) + vi.mocked(fetch).mockClear() + + await automateModule.onAfterTest(mockArgs) + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('sessions/reloaded-session-id.json'), + expect.objectContaining({ method: 'PUT', body: JSON.stringify({ name: 'suite title - test title' }) }) + ) + }) + + it('adds no status traffic for skipSessionName users (SDK-7270 inverse-leak guard)', async () => { + // With naming off, onBeforeTest never registers the session. onAfterTest must not adopt it + // either, or onAfterExecute would start status-marking sessions it previously ignored. + (automateModule.config as any).testContextOptions.skipSessionName = true + vi.mocked(fetch).mockResolvedValue({ + json: vi.fn().mockResolvedValue({ success: true }) + } as any) + + const mockArgs = { + instance: mockTestInstance, + result: { error: null, passed: true }, + test: { title: 'test title', parent: 'suite title' }, + suiteTitle: 'suite title' + } + + await automateModule.onBeforeTest(mockArgs) + await automateModule.onAfterTest(mockArgs) + vi.mocked(fetch).mockClear() + + await automateModule.onAfterExecute() + + expect(fetch).not.toHaveBeenCalled() + }) + it('should skip session status update when skipSessionStatus is true', async () => { (automateModule.config as any).testContextOptions.skipSessionStatus = true const mockArgs = { From 700a57adec95cee6f68afbaaa18edf5b40470769 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:51:10 +0000 Subject: [PATCH 2/2] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-163.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-163.md diff --git a/.changeset/pr-163.md b/.changeset/pr-163.md new file mode 100644 index 0000000..32c7f2f --- /dev/null +++ b/.changeset/pr-163.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed App Automate and Automate session names staying on the static `sessionName` capability when a test reloads the session part-way through — for example a deep-link or app-relaunch step. Such sessions now show the test title, and also carry their own pass/fail result.