Skip to content

Commit 11b8ab0

Browse files
feat(browserstack-service): stamp hook_run_uuid on App A11y scans fired inside hooks [APPA11Y-5542]
App A11y scans fired inside test hooks (before/after, beforeEach/afterEach) were dropped or misattributed. The SDK now stamps the hook's run UUID (thHookRunUuid) on scans fired inside a supported hook, so SeleniumHub (appAllyHandler, PR #14162) relays it as hook_run_uuid and app-accessibility reconciles the scan onto the wrapping test case. Additive: in-test scans are unchanged (field dropped when absent). - util: _getParamsForAppAccessibility / performA11yScan thread optional hookRunUuid (thHookRunUuid) - insights-handler: expose getCurrentHook() so a11y reuses the HookRunStarted uuid (= hook BTCER uuid) - accessibility-handler: beforeHook/afterHook set _currentHookRunUuid + enable the scan gate during mocha hook windows (per-test hooks honour include/exclude scope; suite hooks use autoScanning) - service: wire accessibilityHandler.beforeHook/afterHook, passing the shared hook uuid - tests: cover hook-uuid stamp/clear + unsupported-framework no-op Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ea4e64a commit 11b8ab0

5 files changed

Lines changed: 101 additions & 5 deletions

File tree

packages/browserstack-service/src/accessibility-handler.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ import {
6767
validateCapsWithAppA11y,
6868
getAppA11yResults,
6969
executeAccessibilityScript,
70-
isFalse
70+
isFalse,
71+
getHookType,
72+
frameworkSupportsHook
7173
} from './util.js'
7274
import accessibilityScripts from './scripts/accessibility-scripts.js'
7375
import PerformanceTester from './instrumentation/performance/performance-tester.js'
@@ -87,6 +89,8 @@ class _AccessibilityHandler {
8789
private _autoScanning: boolean = true
8890
private _testIdentifier: string | null = null
8991
private _testMetadata: TestMetadata = {}
92+
/* Set while a supported hook is executing; scans fired in this window are stamped with it. */
93+
private _currentHookRunUuid: string | null = null
9094
private static _a11yScanSessionMap: A11yScanSessionMap = {}
9195
private _sessionId: string | null = null
9296
private listener = Listener.getInstance()
@@ -418,6 +422,48 @@ class _AccessibilityHandler {
418422
}
419423
}
420424

425+
/**
426+
* Hook scans. A driver command executed inside a test hook (before/after, beforeEach/afterEach,
427+
* cucumber hooks) should fire an accessibility scan carrying the hook's run UUID so the backend
428+
* (SeleniumHub appAllyHandler -> app-accessibility) reconciles it onto the wrapping test case
429+
* instead of collapsing into a NULL row. `hookRunUuid` is the SAME uuid the SDK reports to
430+
* TestHub as HookRunStarted (InsightsHandler.getCurrentHook) = the hook's BTCER uuid.
431+
* Additive: when hookRunUuid is absent, in-test scan behaviour is unchanged.
432+
*/
433+
async beforeHook (test: Frameworks.Test | undefined, context: unknown, hookRunUuid?: string | null) {
434+
try {
435+
if (!this._accessibility || !this.shouldRunTestHooks(this._browser, this._accessibility)) {
436+
return
437+
}
438+
if (!frameworkSupportsHook('before', this._framework)) {
439+
return
440+
}
441+
442+
this._currentHookRunUuid = hookRunUuid || null
443+
444+
if (this._framework === 'mocha' && this._sessionId) {
445+
let shouldScan = this._autoScanning
446+
const hookType = (test && typeof test.title === 'string') ? getHookType(test.title) : 'unknown'
447+
const wrappedTest = (context as { currentTest?: Frameworks.Test } | undefined)?.currentTest
448+
if ((hookType === 'BEFORE_EACH' || hookType === 'AFTER_EACH') && wrappedTest) {
449+
let suiteTitle: unknown = wrappedTest.parent
450+
if (suiteTitle && typeof suiteTitle === 'object') {
451+
suiteTitle = (suiteTitle as { title?: string }).title
452+
}
453+
shouldScan = this._autoScanning && shouldScanTestForAccessibility(suiteTitle as string | undefined, wrappedTest.title, this._accessibilityOptions)
454+
}
455+
AccessibilityHandler._a11yScanSessionMap[this._sessionId] = shouldScan
456+
}
457+
} catch (error) {
458+
BStackLogger.error(`Exception in accessibility automation beforeHook: ${error}`)
459+
}
460+
}
461+
462+
async afterHook (_test?: Frameworks.Test, _context?: unknown, _result?: Frameworks.TestResult, _hookRunUuid?: string | null) {
463+
// Hook finished: subsequent (test-body) scans must not be stamped as hook scans.
464+
this._currentHookRunUuid = null
465+
}
466+
421467
/*
422468
* private methods
423469
*/
@@ -433,7 +479,7 @@ class _AccessibilityHandler {
433479
)
434480
) {
435481
BStackLogger.debug(`Performing scan for ${command.class} ${command.name}`)
436-
await performA11yScan(this.isAppAutomate, this._browser, true, true, command.name)
482+
await performA11yScan(this.isAppAutomate, this._browser, true, true, command.name, undefined, this._currentHookRunUuid)
437483
} else if (skipScanForBidiWindowCommand) {
438484
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`)
439485
}

packages/browserstack-service/src/insights-handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,12 @@ class _InsightsHandler {
203203
}
204204
}
205205

206+
/* Exposes the current hook run so other handlers (e.g. AccessibilityHandler) can
207+
stamp the same hook UUID we report to TestHub as HookRunStarted. */
208+
getCurrentHook(): CurrentRunInfo {
209+
return this._currentHook
210+
}
211+
206212
async sendScenarioObjectSkipped(scenario: Scenario, feature: Feature, uri: string) {
207213
const testMetaData: TestMeta = {
208214
uuid: uuidv4(),

packages/browserstack-service/src/service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ export default class BrowserstackService implements Services.ServiceInstance {
410410
}
411411

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

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

442445
await this._insightsHandler?.afterHook(test, result)
446+
await this._accessibilityHandler?.afterHook(test as Frameworks.Test, context, result, this._insightsHandler?.getCurrentHook()?.uuid)
443447
}
444448

445449
@PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_HOOK, { hookType: 'beforeTest' })

packages/browserstack-service/src/util.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,9 +571,12 @@ export const formatString = (template: (string | null), ...values: (string | nul
571571
}
572572

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

586589
/* eslint-disable @typescript-eslint/no-explicit-any */
587-
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> => {
590+
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> => {
588591

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

594597
try {
595598
if (isAppAccessibilityAutomationSession(isAccessibility, isAppAutomate)) {
596-
const results: unknown = await (browser as WebdriverIO.Browser).execute(formatString(AccessibilityScripts.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName, testName))) as string, {})
599+
const results: unknown = await (browser as WebdriverIO.Browser).execute(formatString(AccessibilityScripts.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName, testName, hookRunUuid))) as string, {})
597600
BStackLogger.debug(util.format(results as string))
598601
return ( results as { [key: string]: any; } | undefined )
599602
}

packages/browserstack-service/tests/accessibility-handler.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,43 @@ describe('afterScenario', () => {
433433
})
434434
})
435435

436+
describe('beforeHook / afterHook (hook scans)', () => {
437+
beforeEach(() => {
438+
accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'mocha', true, false, accessibilityOpts)
439+
vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true)
440+
vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true)
441+
accessibilityHandler['_sessionId'] = 'session123'
442+
})
443+
444+
it('stamps the hook run uuid inside a supported mocha per-test hook', async () => {
445+
vi.spyOn(utils, 'shouldScanTestForAccessibility').mockReturnValue(true)
446+
await accessibilityHandler.beforeHook(
447+
{ title: '"before each" hook', parent: 'suite' } as any,
448+
{ currentTest: { parent: 'suite', title: 'test' } },
449+
'hook-uuid-1'
450+
)
451+
expect(accessibilityHandler['_currentHookRunUuid']).toBe('hook-uuid-1')
452+
})
453+
454+
it('clears the hook run uuid on afterHook so test-body scans are not stamped', async () => {
455+
await accessibilityHandler.beforeHook(
456+
{ title: '"after each" hook', parent: 'suite' } as any,
457+
{ currentTest: { parent: 'suite', title: 'test' } },
458+
'hook-uuid-2'
459+
)
460+
expect(accessibilityHandler['_currentHookRunUuid']).toBe('hook-uuid-2')
461+
await accessibilityHandler.afterHook({ title: '"after each" hook' } as any, {}, { passed: true } as any, 'hook-uuid-2')
462+
expect(accessibilityHandler['_currentHookRunUuid']).toBeNull()
463+
})
464+
465+
it('does not stamp when the framework does not support hooks', async () => {
466+
const jasmineHandler = new AccessibilityHandler(browser, caps, options, false, config, 'jasmine', true, false, accessibilityOpts)
467+
jasmineHandler['_sessionId'] = 'session123'
468+
await jasmineHandler.beforeHook({ title: '"before each" hook' } as any, {}, 'hook-uuid-3')
469+
expect(jasmineHandler['_currentHookRunUuid']).toBeNull()
470+
})
471+
})
472+
436473
describe('beforeTest', () => {
437474
let executeAsyncSpy: any
438475
let executeSpy: any

0 commit comments

Comments
 (0)