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
50 changes: 48 additions & 2 deletions packages/browserstack-service/src/accessibility-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
validateCapsWithAppA11y,
getAppA11yResults,
executeAccessibilityScript,
isFalse
isFalse,
getHookType,
frameworkSupportsHook
} from './util.js'
import accessibilityScripts from './scripts/accessibility-scripts.js'
import PerformanceTester from './instrumentation/performance/performance-tester.js'
Expand All @@ -87,6 +89,8 @@
private _autoScanning: boolean = true
private _testIdentifier: string | null = null
private _testMetadata: TestMetadata = {}
/* Set while a supported hook is executing; scans fired in this window are stamped with it. */
private _currentHookRunUuid: string | null = null
private static _a11yScanSessionMap: A11yScanSessionMap = {}
private _sessionId: string | null = null
private listener = Listener.getInstance()
Expand Down Expand Up @@ -418,6 +422,48 @@
}
}

/**
* Hook scans. A driver command executed inside a test hook (before/after, beforeEach/afterEach,
* cucumber hooks) should fire an accessibility scan carrying the hook's run UUID so the backend
* (SeleniumHub appAllyHandler -> app-accessibility) reconciles it onto the wrapping test case
* instead of collapsing into a NULL row. `hookRunUuid` is the SAME uuid the SDK reports to
* TestHub as HookRunStarted (InsightsHandler.getCurrentHook) = the hook's BTCER uuid.
* Additive: when hookRunUuid is absent, in-test scan behaviour is unchanged.
*/
async beforeHook (test: Frameworks.Test | undefined, context: unknown, hookRunUuid?: string | null) {
try {
if (!this._accessibility || !this.shouldRunTestHooks(this._browser, this._accessibility)) {
return
}
if (!frameworkSupportsHook('before', this._framework)) {
return
}

this._currentHookRunUuid = hookRunUuid || null

if (this._framework === 'mocha' && this._sessionId) {
let shouldScan = this._autoScanning
const hookType = (test && typeof test.title === 'string') ? getHookType(test.title) : 'unknown'
const wrappedTest = (context as { currentTest?: Frameworks.Test } | undefined)?.currentTest
if ((hookType === 'BEFORE_EACH' || hookType === 'AFTER_EACH') && wrappedTest) {
let suiteTitle: unknown = wrappedTest.parent
if (suiteTitle && typeof suiteTitle === 'object') {
suiteTitle = (suiteTitle as { title?: string }).title
}
shouldScan = this._autoScanning && shouldScanTestForAccessibility(suiteTitle as string | undefined, wrappedTest.title, this._accessibilityOptions)
}
AccessibilityHandler._a11yScanSessionMap[this._sessionId] = shouldScan
}
} catch (error) {
BStackLogger.error(`Exception in accessibility automation beforeHook: ${error}`)
}
}

async afterHook (_test?: Frameworks.Test, _context?: unknown, _result?: Frameworks.TestResult, _hookRunUuid?: string | null) {

Check failure on line 462 in packages/browserstack-service/src/accessibility-handler.ts

View workflow job for this annotation

GitHub Actions / Lint

'_hookRunUuid' is defined but never used

Check failure on line 462 in packages/browserstack-service/src/accessibility-handler.ts

View workflow job for this annotation

GitHub Actions / Lint

'_result' is defined but never used

Check failure on line 462 in packages/browserstack-service/src/accessibility-handler.ts

View workflow job for this annotation

GitHub Actions / Lint

'_context' is defined but never used

Check failure on line 462 in packages/browserstack-service/src/accessibility-handler.ts

View workflow job for this annotation

GitHub Actions / Lint

'_test' is defined but never used
// Hook finished: subsequent (test-body) scans must not be stamped as hook scans.
this._currentHookRunUuid = null
}

/*
* private methods
*/
Expand All @@ -433,7 +479,7 @@
)
) {
BStackLogger.debug(`Performing scan for ${command.class} ${command.name}`)
await performA11yScan(this.isAppAutomate, this._browser, true, true, command.name)
await performA11yScan(this.isAppAutomate, this._browser, true, true, command.name, undefined, this._currentHookRunUuid)
} else if (skipScanForBidiWindowCommand) {
BStackLogger.debug(`SDK-5047: skipping accessibility scan for BiDi window/context command '${command.name}' to avoid racing the WebdriverIO ContextManager during session-start window churn`)
}
Expand Down
6 changes: 6 additions & 0 deletions packages/browserstack-service/src/insights-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ class _InsightsHandler {
}
}

/* Exposes the current hook run so other handlers (e.g. AccessibilityHandler) can
stamp the same hook UUID we report to TestHub as HookRunStarted. */
getCurrentHook(): CurrentRunInfo {
return this._currentHook
}

async sendScenarioObjectSkipped(scenario: Scenario, feature: Feature, uri: string) {
const testMetaData: TestMeta = {
uuid: uuidv4(),
Expand Down
4 changes: 4 additions & 0 deletions packages/browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ export default class BrowserstackService implements Services.ServiceInstance {
}

await this._insightsHandler?.beforeHook(test, context)
// Reuse the exact hook UUID InsightsHandler just reported to TestHub (HookRunStarted)
// so a11y hook scans carry a hook_run_uuid that matches the hook's BTCER row.
await this._accessibilityHandler?.beforeHook(test as Frameworks.Test, context, this._insightsHandler?.getCurrentHook()?.uuid)
}

@PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'afterHook' })
Expand Down Expand Up @@ -440,6 +443,7 @@ export default class BrowserstackService implements Services.ServiceInstance {
}

await this._insightsHandler?.afterHook(test, result)
await this._accessibilityHandler?.afterHook(test as Frameworks.Test, context, result, this._insightsHandler?.getCurrentHook()?.uuid)
}

@PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'beforeTest' })
Expand Down
9 changes: 6 additions & 3 deletions packages/browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,9 +571,12 @@ export const formatString = (template: (string | null), ...values: (string | nul
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const _getParamsForAppAccessibility = ( commandName?: string, testName?: string ): { thTestRunUuid: any, thBuildUuid: any, thJwtToken: any, authHeader: any, scanTimestamp: number, method: string | undefined, testName: string | undefined } => {
export const _getParamsForAppAccessibility = ( commandName?: string, testName?: string, hookRunUuid?: string | null ): { thTestRunUuid: any, thHookRunUuid: any, thBuildUuid: any, thJwtToken: any, authHeader: any, scanTimestamp: number, method: string | undefined, testName: string | undefined } => {
return {
'thTestRunUuid': process.env.TEST_ANALYTICS_ID,
// Present only when the scan fires inside a hook (dropped by JSON.stringify when undefined,
// so in-test scans are unchanged). SeleniumHub appAllyHandler relays this as `hook_run_uuid`.
'thHookRunUuid': hookRunUuid || undefined,
'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID,
'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT,
'authHeader': process.env.BSTACK_A11Y_JWT,
Expand All @@ -584,7 +587,7 @@ export const _getParamsForAppAccessibility = ( commandName?: string, testName?:
}

/* eslint-disable @typescript-eslint/no-explicit-any */
export const performA11yScan = async (isAppAutomate: boolean, browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string, commandName?: string, testName?: string,) : Promise<{ [key: string]: any; } | undefined> => {
export const performA11yScan = async (isAppAutomate: boolean, browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string, commandName?: string, testName?: string, hookRunUuid?: string | null,) : Promise<{ [key: string]: any; } | undefined> => {

if (!isAccessibilityAutomationSession(isAccessibility)) {
BStackLogger.warn('Not an Accessibility Automation session, cannot perform Accessibility scan.')
Expand All @@ -593,7 +596,7 @@ export const performA11yScan = async (isAppAutomate: boolean, browser: Webdriver

try {
if (isAppAccessibilityAutomationSession(isAccessibility, isAppAutomate)) {
const results: unknown = await (browser as WebdriverIO.Browser).execute(formatString(AccessibilityScripts.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName, testName))) as string, {})
const results: unknown = await (browser as WebdriverIO.Browser).execute(formatString(AccessibilityScripts.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName, testName, hookRunUuid))) as string, {})
BStackLogger.debug(util.format(results as string))
return ( results as { [key: string]: any; } | undefined )
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,43 @@ describe('afterScenario', () => {
})
})

describe('beforeHook / afterHook (hook scans)', () => {
beforeEach(() => {
accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'mocha', true, false, accessibilityOpts)
vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true)
vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true)
accessibilityHandler['_sessionId'] = 'session123'
})

it('stamps the hook run uuid inside a supported mocha per-test hook', async () => {
vi.spyOn(utils, 'shouldScanTestForAccessibility').mockReturnValue(true)
await accessibilityHandler.beforeHook(
{ title: '"before each" hook', parent: 'suite' } as any,
{ currentTest: { parent: 'suite', title: 'test' } },
'hook-uuid-1'
)
expect(accessibilityHandler['_currentHookRunUuid']).toBe('hook-uuid-1')
})

it('clears the hook run uuid on afterHook so test-body scans are not stamped', async () => {
await accessibilityHandler.beforeHook(
{ title: '"after each" hook', parent: 'suite' } as any,
{ currentTest: { parent: 'suite', title: 'test' } },
'hook-uuid-2'
)
expect(accessibilityHandler['_currentHookRunUuid']).toBe('hook-uuid-2')
await accessibilityHandler.afterHook({ title: '"after each" hook' } as any, {}, { passed: true } as any, 'hook-uuid-2')
expect(accessibilityHandler['_currentHookRunUuid']).toBeNull()
})

it('does not stamp when the framework does not support hooks', async () => {
const jasmineHandler = new AccessibilityHandler(browser, caps, options, false, config, 'jasmine', true, false, accessibilityOpts)
jasmineHandler['_sessionId'] = 'session123'
await jasmineHandler.beforeHook({ title: '"before each" hook' } as any, {}, 'hook-uuid-3')
expect(jasmineHandler['_currentHookRunUuid']).toBeNull()
})
})

describe('beforeTest', () => {
let executeAsyncSpy: any
let executeSpy: any
Expand Down
Loading