Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-163.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 32 additions & 7 deletions packages/browserstack-service/src/cli/modules/automateModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
Loading