Skip to content
Open
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-308.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wdio/browserstack-service": patch
---

- Fixed builds being reported as timed out on Test Reporting & Analytics when a Mocha `before` / `beforeEach` / `afterEach` hook is declared outside any `describe` block.
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default class WdioMochaTestFramework extends TestFramework {
private owedBailCascades: Array<{ attempt: TestAttempt, result: Frameworks.TestResult }> = []
/** settleTestFinishes() has run its cascades; anything later cascades inline. */
private settled = false
private _suiteFile?: string

/** One attempt of a test: mocha retries a test as a new runnable with `_currentRetry` + 1. */
static attemptKey(test: Frameworks.Test): string {
Expand Down Expand Up @@ -131,6 +132,10 @@ export default class WdioMochaTestFramework extends TestFramework {
super(testFrameworks, testFrameworkVersions, binSessionId)
}

setSuiteFile(filename?: string) {
this._suiteFile = filename
}

/**
* Find instance and track any state for the test framework
* @param {TestFrameworkState} testFrameworkState
Expand Down Expand Up @@ -520,7 +525,7 @@ export default class WdioMochaTestFramework extends TestFramework {
async getTestData(instance: TestFrameworkInstance, test: Frameworks.Test) {
const framework = TestFramework.getState(instance, TestFrameworkConstants.KEY_TEST_FRAMEWORK_NAME)
const fullTitle = getUniqueIdentifier(test, framework)
const filename = test.file // || this._suiteFile
const filename = test.file || this._suiteFile
const scopes = getMochaTestHierarchy(test)

const testData: Record<string, unknown> = {
Expand Down Expand Up @@ -714,7 +719,8 @@ export default class WdioMochaTestFramework extends TestFramework {
}

if (hookState === HookState.PRE) {
const filename = test.file
// root-level hooks belong to mocha's root suite, which has no file
const filename = test.file || this._suiteFile
const hook: Record<string, unknown> = {
key,
[TestFrameworkConstants.KEY_HOOK_ID]: TestFramework.getState(instance, TestFrameworkConstants.KEY_HOOK_ID) || '',
Expand Down
21 changes: 21 additions & 0 deletions packages/browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ import { HookState } from './cli/states/hookState.js'
import { AutomationFrameworkConstants } from './cli/frameworks/constants/automationFrameworkConstants.js'
import TestFramework from './cli/frameworks/testFramework.js'
import WdioCucumberTestFramework from './cli/frameworks/wdioCucumberTestFramework.js'
import WdioMochaTestFramework from './cli/frameworks/wdioMochaTestFramework.js'
import { TestFrameworkState } from './cli/states/testFrameworkState.js'
import { TestFrameworkConstants } from './cli/frameworks/constants/testFrameworkConstants.js'
import AccessibilityModule from './cli/modules/accessibilityModule.js'

import util from 'node:util'
import { fileURLToPath } from 'node:url'

export default class BrowserstackService implements Services.ServiceInstance {
private _sessionBaseUrl = 'https://api.browserstack.com/automate/sessions'
Expand Down Expand Up @@ -217,6 +219,9 @@ export default class BrowserstackService implements Services.ServiceInstance {
this._browser = browser ? browser : globalThis.browser
PerformanceTester.browser = this._browser

// a root-level mocha `before` runs ahead of the first beforeSuite
this._setCliSuiteFile(specs?.[0])

UploadAttachmentModule.installNoopFallback(this._browser)

// Healing Support:
Expand Down Expand Up @@ -430,6 +435,7 @@ export default class BrowserstackService implements Services.ServiceInstance {
this._suiteTitle = suite.title
this._insightsHandler?.setSuiteFile(suite.file)
this._accessibilityHandler?.setSuiteFile(suite.file)
this._setCliSuiteFile(suite.file)

if (suite.title && suite.title !== 'Jasmine__TopLevel__Suite') {
if (!BrowserstackCLI.getInstance().isRunning() || this._config.framework !== 'mocha'){
Expand Down Expand Up @@ -1271,6 +1277,21 @@ export default class BrowserstackService implements Services.ServiceInstance {
})
}

/** The file the CLI mocha framework gives root-level hooks, which mocha leaves without one. */
private _setCliSuiteFile(file?: string) {
const cli = BrowserstackCLI.getInstance()
const cliFramework = cli.isRunning() ? cli.getTestFramework() : null
if (!file || !(cliFramework instanceof WdioMochaTestFramework)) {
return
}
try {
// a runner spec is a file URL; MochaAdapter converts it the same way for suite.file
cliFramework.setSuiteFile(file.startsWith('file://') ? fileURLToPath(file) : file)
} catch (err) {
BStackLogger.debug(`Could not resolve the suite file from ${file}: ${err}`)
}
}

private async _setSessionName(suiteTitle: string | undefined, test?: Frameworks.Test) {
if (!this._options.setSessionName || !suiteTitle) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,66 @@ describe('SDK-7233 — test_file_path must be the absolute spec path', () => {
})
})

describe('root-level mocha hooks fall back to the suite file, as on the non-CLI flow', () => {
const ROOT_HOOK = { title: '"before each" hook: wrappedHook for "t1"' } as Frameworks.Test
let framework: WdioMochaTestFramework

beforeEach(() => {
framework = Object.create(WdioMochaTestFramework.prototype)
})

afterEach(() => {
vi.restoreAllMocks()
})

const startHook = async (test: Frameworks.Test) => {
const data = new Map<string, unknown>([
[TestFrameworkConstants.KEY_HOOKS_STARTED, new Map()],
[TestFrameworkConstants.KEY_HOOKS_FINISHED, new Map()],
[TestFrameworkConstants.KEY_HOOK_ID, 'hook-1'],
])
const instance = {
getAllData: () => data,
updateMultipleEntries: (entries: Record<string, unknown>) => Object.entries(entries).forEach(([k, v]) => data.set(k, v)),
} as unknown as TestFrameworkInstance
await framework.trackHookEvents(instance, TestFrameworkState.BEFORE_EACH, HookState.PRE, { test })
return (data.get(TestFrameworkConstants.KEY_HOOKS_STARTED) as Map<string, Record<string, unknown>[]>).get('BEFORE_EACH')![0]
}

it('sends the current suite file for a hook mocha gives no file', async () => {
// a beforeEach/afterEach declared outside any describe hangs off mocha's root suite, which
// has no file; without a path the binary threw, dropped the hook events, and the test's
// reference to the never-sent hook timed the build out
framework.setSuiteFile(SPEC)

const hook = await startHook(ROOT_HOOK)

expect(hook[TestFrameworkConstants.KEY_HOOK_ID]).toBe('hook-1')
expect(hook[TestFrameworkConstants.KEY_TEST_FILE_PATH]).toBe(SPEC)
expect(hook[TestFrameworkConstants.KEY_TEST_LOCATION]).toBe(path.relative(process.cwd(), SPEC))
})

it('keeps the file mocha gives a hook declared inside a describe', async () => {
framework.setSuiteFile('/Users/someone/projects/arenaclub/test/specs/other.test.ts')

const hook = await startHook({ ...ROOT_HOOK, file: SPEC } as Frameworks.Test)

expect(hook[TestFrameworkConstants.KEY_TEST_FILE_PATH]).toBe(SPEC)
})

it('applies the same fallback to test data', async () => {
vi.spyOn(TestFramework, 'getState').mockReturnValue('WebdriverIO-mocha')
vi.spyOn(util, 'getUniqueIdentifier').mockReturnValue('t1')
vi.spyOn(util, 'getMochaTestHierarchy').mockReturnValue([])
framework.setSuiteFile(SPEC)

// @ts-expect-error — partial TestFrameworkInstance / Frameworks.Test are enough here
const data = await framework.getTestData({}, { title: 't1', body: '' })

expect(data[TestFrameworkConstants.KEY_TEST_FILE_PATH]).toBe(SPEC)
})
})

describe('SDK-4177 — loadLogEntries must not relabel a log that carries its own kind', () => {
let entries: unknown[]
let instance: any
Expand Down
39 changes: 39 additions & 0 deletions packages/browserstack-service/tests/service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path'
import { pathToFileURL } from 'node:url'

import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import type { Frameworks } from '@wdio/types'
Expand All @@ -8,6 +9,7 @@ import BrowserstackService from '../src/service.js'
import * as utils from '../src/util.js'
import InsightsHandler from '../src/insights-handler.js'
import { BrowserstackCLI } from '../src/cli/index.js'
import WdioMochaTestFramework from '../src/cli/frameworks/wdioMochaTestFramework.js'
import AccessibilityModule from '../src/cli/modules/accessibilityModule.js'
import * as bstackLogger from '../src/bstackLogger.js'
import * as skipReporter from '../src/cli/skipReporter.js'
Expand Down Expand Up @@ -928,6 +930,43 @@ describe('beforeScenario', () => {
})

describe('beforeSuite', () => {
describe('suite file for the CLI mocha framework', () => {
const SPEC = path.resolve('/specs/root hooks.spec.ts')
let framework: WdioMochaTestFramework

beforeEach(() => {
framework = Object.create(WdioMochaTestFramework.prototype) as WdioMochaTestFramework
vi.spyOn(BrowserstackCLI, 'getInstance').mockReturnValue({
isRunning: () => true,
getTestFramework: () => framework,
getAutomationFramework: () => ({ trackEvent: vi.fn() }),
} as any)
})

afterEach(() => {
vi.mocked(BrowserstackCLI.getInstance).mockRestore()
})

it('hands the suite file to the CLI mocha framework, as the non-CLI flow does to its insights handler', async () => {
await service.beforeSuite({ title: 'foobar', file: '/specs/foobar.spec.ts' } as any)

expect(framework['_suiteFile']).toBe('/specs/foobar.spec.ts')
})

it('seeds it from the worker spec in before(), as a root-level `before` runs ahead of the first beforeSuite', async () => {
await service.before(service['_config'] as any, [pathToFileURL(SPEC).href], browser)

expect(framework['_suiteFile']).toBe(SPEC)
})

it('keeps the spec file when a suite comes without one', async () => {
await service.before(service['_config'] as any, [pathToFileURL(SPEC).href], browser)
await service.beforeSuite({ title: 'root-level tests' } as any)

expect(framework['_suiteFile']).toBe(SPEC)
})
})

it('should send request to set the session name as suite name for Mocha tests', async () => {
await service.before(service['_config'] as any, [], browser)
expect(service['_suiteTitle']).toBeUndefined()
Expand Down
Loading