feat(app-a11y): scan the WDIO config-level before() window, and report it as a hook - #168
feat(app-a11y): scan the WDIO config-level before() window, and report it as a hook#168kamal-kaur04 wants to merge 18 commits into
Conversation
…n TRA can resolve Rebuilt on top of the reloadSession fix and its review corrections, so this branch is now the before() work alone — the earlier parked version predated those and still carried the reverted alias config-capture. Driver commands issued from WDIO's config-level before()/beforeSession() were never scanned. Those hooks are not test-framework hooks, so neither onHookStart nor onBeforeTest had run and the scan gate had no entry for the session at all — while the rake still counted those commands as expected coverage. The gate now opens at driver creation, unconditionally when autoScanning is on, matching what onHookStart already does for Mocha hooks. Scans from that window also had no parent: no thTestRunUuid (no test exists yet) and no thHookRunUuid, so App-A11y's lookup of the scan's parent in BTCER found nothing. The window is now reported as a BEFORE_ALL hook run through the existing framework path, so the framework mints the uuid, the binary emits HookRunStarted/Finished, and onHookStart stamps it onto the scans. Opened ON DEMAND, from the scan path only. WDIO gives a service no event for a user's config-level before() — ConfigParser folds the user's config hooks and every service's hooks into one array the runner fires with Promise.all — so there is nothing to detect, and opening it from the lifecycle would report a hook that never ran on every build in the fleet. hookInstrumentation logs the start and finish of every session-scoped config hook, which is how that window becomes observable at all. Logging only, no events. SDK-7422
The predicate added in cec234c landed between the decorator and the method, so the decoration moved to it: onReload stopped emitting its SDK_HOOK measurement, and the predicate started reporting one under hookType 'onReload' on every call — including from the before hook, so every session reported an onReload measurement whether or not it ever reloaded. Behaviour was unaffected (PerformanceTester.measure returns the raw value on the synchronous path), which is why nothing surfaced it: valid TypeScript, lint has no opinion, and the unit suites mock Measure into a pass-through so the binding has no observable behaviour to assert. Hence a structural test over the decorated files instead — verified to fail with the mistake reintroduced and pass without it. Doc comment corrected too: the two accessibility paths are NOT strictly exclusive. The module is registered on startBinResponse.accessibility?.success alone, independent of provider, so under a non-BrowserStack provider it can hold real state while the classic handler's before() has also run. What the predicate decides is which one owns the CLASSIC handler's state. SDK-7422
Found by running the case rather than reasoning about it: with a config-level before() that
throws, WDIO leaves the exit code at 0 (executeHooksWithArgs resolves WITH the error instead of
rejecting — deliberately, so a throwing config hook cannot fail the run), every reporter shows
green, and before this commit the hook run this branch emits said result=passed.
That is worse than the pre-existing silence. Unreported means the dashboard omits the failure;
reporting `passed` means it asserts something false about it.
The instrumentation wrapper is the only thing in the process that sees the rejection, so it now
records the message per hook name, and closePreTestHookRun reads it and reports
{ passed: false, error: { message } }.
Measured on the same bench, one line apart in the same log:
before: HookRunFinished BEFORE_ALL result=passed uuid=1bcd33f4
after: HookRunFinished BEFORE_ALL result=failed uuid=d9c3ccb7
and the TRA build verdict moves from passed {passed: 2} to failed {passed: 1, failed: 1}.
SDK-7422
Keying the failure lookup on the literal 'before' was too narrow. `beforeSuite` is a config hook
with a live driver that the Mocha adapter registers as a root before-all
(`this._runner.suite.beforeAll(this.wrapHook('beforeSuite'))`), so it runs after the config-level
before and still ahead of the first test — inside the same window this hook run covers. A
beforeSuite that threw was reported as a passed window.
PRE_TEST_WINDOW_HOOKS names the hooks that can run there, and the message is prefixed with the
hook that failed. Mocha's own hooks stay excluded: each has its own hook run with its own result,
so folding them in would report the same failure twice.
Instrumenting a config now also clears the record for those hooks. The map is process state for
the worker's lifetime, so without it a failure could be reported against a later config.
Verified on device with a clean before() and a throwing beforeSuite (build
eq3p9ktfb1uiiq5mtxqj670cu42o6vf82z6nzcdx):
[hook-window] before#0 finished in 2695ms
[hook-window] beforeSuite#0 rejected: BOOM: beforeSuite failed in 4132ms
HookRunFinished BEFORE_ALL result=failed uuid=187517a8
SDK-7422
The label was fixed at 'wdio config-level before hook', which names the wrong hook whenever the
window opens somewhere else — and it can: beforeSuite runs in the same window, so a suite whose
before() touches no driver gets its window opened from beforeSuite instead.
The instrumentation already wraps every session-scoped hook, so it now also keeps a stack of the
hooks currently executing and exposes the innermost one. The name is captured once, when the
window opens, because the binary takes a hook's name from the record pushed at PRE and by POST
time a different hook is running.
Verified on device, two shapes:
before() does the first scannable command -> 'wdio before() hook (pre-test window)'
before() touches nothing, beforeSuite does -> 'wdio beforeSuite() hook (pre-test window)'
(build eestbqwlk8mvjhzqkijfl0lc4gnzqxredgofblrw)
Note the name says where the window OPENED, not where it failed — those differ, and the failure
message carries the failing hook's name, so both facts survive.
SDK-7422
…dow suffix
`wdio "before" hook` / `wdio "beforeSuite" hook` instead of
`wdio before() hook (pre-test window)`.
Quoting matches how Mocha names its own hooks ("before all" hook: ...), so the dashboard reads
consistently across both kinds, and the suffix was internal vocabulary that meant nothing to
someone looking at a hook in a build.
Verified on device:
wdio "before" hook result=passed 3a0c7942 (build mu2hqmwrk4ygyh1yp8queidnxfdeeju7dk4ubbdx)
wdio "beforeSuite" hook result=failed 1184570a (build xprccsrzaofseerwsevv5pgcec55h9jufvjzzf2s)
Scans in that window are unchanged — 7 in the config-level hook on the normal bench.
SDK-7422
Only four of the eight session-scoped hooks had ever been seen firing in a device run, because
earlier benches did not define the rest. A bench that defines all eight confirms every one is
instrumented and logs its start and finish:
instrumenting before: 1 · beforeSuite: 1 · beforeHook: 1 · beforeTest: 3
afterTest: 2 · afterHook: 1 · afterSuite: 1 · after: 2
start/finish lines: before 2 · beforeSuite 2 · beforeHook 8 · beforeTest 6
afterTest 4 · afterHook 8 · afterSuite 2 · after 4
(beforeHook/afterHook fire four times each — once per Mocha hook. The handler counts above 1 are
expect-webdriverio's snapshot and soft-assert services sharing the array, which the index in the
label distinguishes.)
The test asserts it over BROWSER_CONTEXT_HOOKS itself, so adding a name to that list without the
wrapping actually reaching it fails here rather than showing up as a silence in a device log.
SDK-7422
Debug-log coverage across the patched set — verified, then pinned (
|
| hook | handlers wrapped at construction | start/finish lines |
|---|---|---|
before |
1 | 2 |
beforeSuite |
1 | 2 |
beforeHook |
1 | 8 |
beforeTest |
3 | 6 |
afterTest |
2 | 4 |
afterHook |
1 | 8 |
afterSuite |
1 | 2 |
after |
2 | 4 |
Two things in that table are worth reading rather than skimming:
beforeHook/afterHooklog 8 lines each — they fire once per Mocha hook, and this spec has four (before all,before each,after each,after all). That's 4 invocations × start+finish.- The handler counts above 1 are not the user's.
beforeTest: 3,afterTest: 2,after: 2includeexpect-webdriverio's snapshot and soft-assert services, which share those arrays. The index in the label (beforeTest#0,#1,#2) is what separates them; "registered at construction" means the user's hooks plus any service constructed before ours, never our own.
Pinned with a test that iterates BROWSER_CONTEXT_HOOKS itself and asserts a started and a finished line for each, so adding a name to that list without the wrapping reaching it fails in CI rather than showing up as a silence in a device log.
Suite: 1,153 passing, 75 pre-existing failures, lint clean.
…tion unable to break a hook Comments cut to the file's density: hookInstrumentation 192 -> 147 lines with no code removed beyond `getHookFailure`, which only tests used. Comment share of the accessibilityModule diff drops from 47/106 lines to 20/79. Exception safety, which matters because this wrapper runs inside the user's hook: - every log and every bookkeeping step goes through `quietly()`, so a throwing logger or a hostile object cannot surface as the customer's hook failing - the entry point's own catch-block log is contained too — that one could escape the function - `isThenable` tolerates a throwing `.then` getter - one unwrappable handler is handed back untouched instead of costing the rest their wrapping The handler's own error is never routed through any of that: it propagates unchanged, verified by asserting identity of the thrown object. Four tests for it, two of which found real holes while being written — the unprotected catch-block log, and a wrong premise about Object.freeze (freezing a handler does not prevent wrapping it, since defineProperty runs on the new wrapper). SDK-7422
The gate fix and the instrumentation were both mocha/CLI-shaped. Two gaps closed.
Non-CLI flow: the classic AccessibilityHandler had the same defect the CLI module did — nothing
registered the session before the first test, so commands from a config-level hook went unscanned
while still counting as expected coverage. `before()` now opens the gate when autoScanning is on;
beforeTest/beforeScenario/beforeHook each recompute it. This is the only path cucumber and jasmine
ever take (CLISupportedFrameworks is ['mocha']), and the path mocha takes under multiremote.
Cucumber lifecycle: beforeFeature/beforeScenario/beforeStep/afterStep/afterScenario/afterFeature
were not instrumented at all, so a cucumber suite's hook windows were invisible. Added, and
beforeFeature joins PRE_TEST_WINDOW_HOOKS since it precedes the first scenario.
Measured, cucumber on device (Direct flow by construction):
prod 9.35.0 config-level before() window: 0 scans (kfribnppwbyt98ob8xc9lftkjvbm3xeroffaqjy1)
this branch config-level before() window: 1 scan (4ouu5t3ci52ouoo3pmxcczehbzxatuln4fqhxv08)
all 7 cucumber hooks + before/after logged start/finish
a throwing before(): exit 0, "1 passed", and the only record is ours —
[hook-window] before#0 rejected: BOOM: cucumber config-level before failed in 3050ms
(tupaf5ev3rr3qclpvq2sx9628hs7ot6lvzev2phd)
Remaining asymmetry, deliberately not papered over: the hook RUN is still reported only on the
CLI path. Direct flow reports hooks through insights-handler, whose hook state is shared with the
real hooks (setCurrentHook drives log parenting), so injecting a synthetic hook there needs its
own design and testing rather than a rider on this change.
SDK-7422
Both flows, both frameworks — implemented and run (
|
| case | build | result |
|---|---|---|
| cucumber, prod 9.35.0 | kfribnppwbyt98ob8xc9lftkjvbm3xeroffaqjy1 |
config-level before() window: 0 scans |
| cucumber, this branch | 4ouu5t3ci52ouoo3pmxcczehbzxatuln4fqhxv08 |
window: 1 scan; all 7 cucumber hooks + before/after logged start/finish |
cucumber, failing before() |
tupaf5ev3rr3qclpvq2sx9628hs7ot6lvzev2phd |
exit 0, 1 passed — and the only record of the failure is ours |
| mocha CLI, all 8 hooks defined | (earlier) | every hook instrumented, beforeHook/afterHook 4× each |
mocha CLI, failing before() / beforeSuite |
qkfnla45… / eq3p9ktf… |
hook run result=failed, TRA verdict failed |
The cucumber failing path, in full:
ERROR @wdio/utils:shim: Error: BOOM: cucumber config-level before failed
[hook-window] before#0 (bound before) rejected: BOOM: cucumber config-level before failed in 3050ms
Spec Files: 1 passed, 1 total ← WDIO swallows it, exit code 0
The one asymmetry left, stated plainly
| CLI (mocha) | non-CLI (cucumber, jasmine, mocha-multiremote) | |
|---|---|---|
| config-level hook window scanned | ✅ 7 scans | ✅ 1 scan (was 0) |
| window bounded in logs | ✅ | ✅ |
| failure recorded | ✅ | ✅ |
| hook run reported to TRA | ✅ wdio "before" hook, passed/failed |
❌ not implemented |
Direct flow reports hooks through insights-handler, and its hook state is shared with the real hooks — setCurrentHook is what parents subsequent logs. Injecting a synthetic hook there could mis-parent a customer's logs, so it needs its own design and test pass rather than riding on this change. Scans in that window therefore stay unattributed on the non-CLI path, exactly as they are on main today; what changes is that they now happen.
Unit: 1,160 passing (75 pre-existing failures), lint clean. New coverage includes the classic gate opening in before(), staying closed when autoScanning is off, and being recomputed by a filtered test.
…ion' into SDK-7422-before-hook
Per the repo convention that constants live in one place: BROWSER_CONTEXT_HOOKS, PRE_TEST_WINDOW_HOOKS, the [hook-window] log tag and the reported hook title now come from constants.ts, with hookInstrumentation and accessibilityModule importing them. That also removes the last string literals duplicated between the two — the 'wdio' title prefix appeared in both a template and its fallback. Tests import BROWSER_CONTEXT_HOOKS from constants too, so the list has a single definition. Re-verified on device after the move, both flows unchanged: mocha CLI config-level before() window: 7 scans (7wtmrijn2s3fchavf5bs6p0uprfd0uxjrboenkeg) cucumber non-CLI config-level before() window: 1 scan (7tzkeeepk1x80zwtczn8gqu67kv2vcpqbvqvmbec) SDK-7422
Closes the CLI/non-CLI asymmetry for the framework that always takes the non-CLI path. Jasmine
and mocha-multiremote are deliberately left out — neither reports hooks on a path this can reuse,
so they would need their own design.
Direct flow reports cucumber hooks through insights-handler, so that is where the pre-test window
now goes: reportPreTestHookStarted/Finished build the same TestMeta shape the real cucumber hooks
use and go out via listener.hookStarted/hookFinished. Two small things made it fit —
getHookRunDataForCucumber now prefers a stored `name` over deriving one from hookType, so a
synthesised hook can carry its own label, and the run is stashed under a fixed key so its finish
can find its start.
The accessibility handler drives it, on demand from the scan path and closed at the first
test/scenario, exactly as the CLI module does. It takes the reporter by injection rather than
reaching for insights-handler itself, so it stays unaware of which framework it is serving and
the service decides who gets one.
Measured on device, cucumber:
happy [BEFORE_ALL] wdio "before" hook status=passed build zzppjaz9vfv1tsexw1nkdbuguqyqhfxsyzopgyrm
failing [BEFORE_ALL] wdio "before" hook status=failed build czrlfmfadwt32xjgdtsmzuup46wmutqqftujmje7
build verdict follows: passed {passed: 2} vs failed {passed: 1, failed: 1}
the window's scan still lands (1x back), and the scenario itself still reports passed —
WDIO swallows the hook error either way, which is why the hook row is the only place it shows
SDK-7422
Cucumber now reports the hook run too — the asymmetry is closed (
|
| case | hook on the dashboard | build verdict | build |
|---|---|---|---|
| happy | [BEFORE_ALL] wdio "before" hook — passed |
passed {passed: 2} |
zzppjaz9vfv1tsexw1nkdbuguqyqhfxsyzopgyrm |
before() throws |
[BEFORE_ALL] wdio "before" hook — failed |
failed {passed: 1, failed: 1} |
czrlfmfadwt32xjgdtsmzuup46wmutqqftujmje7 |
The scenario itself reports passed in both — WDIO swallows the hook error regardless — so that hook row is the only place the failure is visible. Which is the whole point of reporting it. The window's scan still lands (1x back) either way.
Parity, updated
| CLI (mocha) | cucumber | jasmine / mocha-multiremote | |
|---|---|---|---|
| window scanned | ✅ | ✅ | ✅ |
| window bounded in logs | ✅ | ✅ | ✅ |
| failure recorded | ✅ | ✅ | ✅ |
| hook run reported | ✅ | ✅ new | ❌ by decision |
Jasmine and mocha-multiremote are left out deliberately: neither reports hooks on a path this can reuse, so each needs its own design rather than a guess. The scanning fix covers them regardless, since it lives in the classic handler's before().
Unit: 1,164 passing (75 pre-existing failures), lint clean. Four new tests for the cucumber lifecycle — opens once, closes at the first scenario, never closes what it did not open, and reports nothing when no reporter is installed.
…ng jasmine untouched Both the gate and the hook instrumentation were framework-agnostic, so a jasmine or multiremote run would have picked them up. Now gated on PRE_TEST_SCAN_FRAMEWORKS (mocha, cucumber) and not-multiremote, at the two places that act: the classic handler's before() and the service's call to instrumentBrowserContextHooks. Checked against the code rather than taking the premise on trust, and it contradicts it twice: TEST_HOOK_FRAMEWORKS is ['mocha', 'jasmine'] with a comment that WDIO's jasmine adapter emits the same service hooks as mocha (SDK-7190), and util.ts states plainly that every WDIO framework (mocha/jasmine/cucumber) supports App Automate. There is no framework gate on App-A11y anywhere, and no multiremote gate outside the launcher's CLI check. So "unsupported" is a product position, not something the code enforces — worth knowing, because it means jasmine is only unscanned today by accident of provisioning rather than by design. Measured on device, same jasmine bench with a driver-touching config-level before(): prod 9.35.0 0 scans, whole session (jhiej0xnkfxdigpk29psmzykmldy96mvysjzxirs) this branch 0 scans, whole session (ca39y63f7h2qmuck5tylhcaajlxj2fxhtlkv1vya) 0 [hook-window] lines and 0 hook runs in the jasmine build segment test passes identically on both So jasmine is byte-for-byte unchanged, which is the requirement. SDK-7422
Jasmine and multiremote scoped out — and the premise checked against the code (
|
| scans, whole session | [hook-window] lines |
hook runs | test | |
|---|---|---|---|---|
prod 9.35.0 — jhiej0xnkfxdigpk29psmzykmldy96mvysjzxirs |
0 | 0 | 0 | passes |
this branch — ca39y63f7h2qmuck5tylhcaajlxj2fxhtlkv1vya |
0 | 0 | 0 | passes |
Byte-for-byte unchanged, which is the requirement. Bench kept at examples/repro-jasmine/ so it stays a standing control.
But the code does not say what the premise says
Worth recording, because it changes how the exclusion should be read. I checked rather than trusting it, and it contradicts the premise in two places:
// accessibility-handler.ts
/**
* WDIO's jasmine adapter emits the same service hooks as mocha (SDK-7190);
* cucumber goes through beforeScenario/afterScenario instead.
*/
private static readonly TEST_HOOK_FRAMEWORKS = ['mocha', 'jasmine']// util.ts
* handles the three edge cases, and clears the app on conflict [...] There is NO Tier-2 branch:
* every WebdriverIO test framework (mocha/jasmine/cucumber) supports App Automate.There is no framework gate on App-A11y anywhere in the service, and no multiremote gate outside the launcher's CLI check. beforeTest admits jasmine and sets the scan gate; commandWrapper has no framework check at all.
So "App-A11y is unsupported on jasmine" is a product position, not something the code enforces. The reason jasmine shows 0 scans on prod today is not a guard — it is that nothing provisions the wrapped-command set on that path. That is a meaningfully different situation from being switched off, and if the unsupported status is meant to hold, an explicit guard is the thing that would make it true.
I have not added such a guard: it would change jasmine's behaviour, which is the opposite of the instruction here. Flagging it as its own decision.
Unit: 1,167 passing (75 pre-existing failures), lint clean. Three new tests — jasmine gets no gate, multiremote gets no gate, and jasmine opens no hook run even with a reporter installed.
A multiremote browser has no sessionId — MultiRemoteDriver defines none and the element path deletes it, which is why wdio-runner reads ids per instance instead. The service passes browser.sessionId straight to the handler, so it receives undefined, and the classic scan gate is keyed on session id: commandWrapper's `this._sessionId && map[this._sessionId]` can never match. So multiremote was never scanning by construction, not by policy. Recording it as a test because the reason is three files away from the code that depends on it, and because the pre-test gate would otherwise have written an 'undefined' key into the shared map. SDK-7422
Correction to my earlier comment: multiremote was never scanning, and not by policyI wrote above that there is "no multiremote gate outside the launcher's CLI check". That is true of explicit guards and misleading as a conclusion — there is a structural one, and it is stronger than a policy flag. A multiremote browser has no // webdriverio/src/multiremote.ts — MultiRemoteDriver
export class MultiRemoteDriver {
instances: string[]
isMultiremote = true as const
__propertiesObject__: Record<string, PropertyDescriptor>
// ...no sessionId// same file, element path
// @ts-expect-error ToDo(Christian): remove eventually
delete client.sessionIdWhich is exactly why prev[multiRemoteBrowser.getInstance(browserName).sessionId] = ...
caps[browserName].sessionId = multiRemoteBrowser.getInstance(browserName).sessionIdThe service passes this._sessionId && AccessibilityHandler._a11yScanSessionMap[this._sessionId] && ...
Two consequences worth stating:
Pinned as a test ( Contrast with jasmine, which is worth keeping distinct: jasmine has a session id and is in |
…sic path The cucumber hook run was reaching TRA while the scans in its window still went out with no parent at all — thTestRunUuid ABSENT and thHookRunUuid ABSENT — so the join the hook run exists for could not happen. Caught by reading a real payload rather than trusting the previous commit's claim that the asymmetry was closed. The mechanism was already there: commandWrapper passes this._currentHookRunUuid to performA11yScan as thHookRunUuid, exactly as beforeHook/afterHook use it for framework hooks. The pre-test window just never set it. Set on open, cleared on close so a test-body scan is never stamped as a hook scan. SDK-7422
…an open The previous commit stamped the hook uuid onto the scan but nothing changed on device: the window's scan still went out with thHookRunUuid ABSENT. The reason was ordering, not the stamp — the reporter was installed after InsightsHandler was constructed, and WDIO runs the user's config-level before() concurrently with ours, so its first command reached the scan path while this method was still setting up. ensurePreTestHookRun found no reporter and did nothing. Installed before AccessibilityHandler.before(), and the callbacks now read _insightsHandler at CALL time instead of capturing it, so being wired before it exists is fine. Measured, cucumber with two commands in the hook (fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01): scan 1 hookRun=9384b3f5 <- the window, now parented scan 2 hookRun=9384b3f5 scan 3..5 testRun=0032ebfd, hookRun ABSENT <- scenario body, correctly not hook scans Worth noting what the window's scans still lack: thTestRunUuid is ABSENT there, because on the Direct path that env var is only set when a test actually starts. The hook uuid is the join, which is what we agreed was sufficient. SDK-7422
A scan fired from a WDIO config-level hook belongs to no test, so it must not carry one. TEST_ANALYTICS_ID in that window holds a uuid mocha minted at instance creation — a test that has not started — and sending it attributed the scan to a test it did not come from. The hook run uuid is the parent there, which is the whole reason it exists. _getParamsForAppAccessibility takes an isGlobalHook flag; both scan paths thread it from an explicit "pre-test window active" flag, set when the gate opens at driver creation and cleared at the first test. Cleared unconditionally on the classic path: the window is over whether or not a hook run was opened, and leaving it set would strip the uuid from every later test-body scan. Two existing tests asserted the LAST argument of performA11yScan, so appending a parameter made them silently follow it — now asserted by position. Two more asserted an exact 3-arg call that is now 4; corrected to expect the forwarded undefined rather than pretending the arity is unchanged. SDK-7422
Device-verified: global-hook scans no longer carry a test run uuid (
|
| build | window scans | test-body scans | |
|---|---|---|---|
| pre-fix | fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01 |
no test uuid | stamped |
| post-fix | yldiivrvnkhf0xi7ttvzekksvxoafxulcrx66gac |
no test uuid | stamped |
Identical, because the defect was CLI-flow-only: mocha mints TEST_ANALYTICS_ID at instance creation, so the window had a uuid to leak, while the classic path sets it at Listener.testStarted and never had one. The guard is wired on both paths anyway so the classic path cannot regress if that ever moves earlier.
What is this about?
Driver commands issued from WDIO's config-level
before()were never scanned for App-A11y, and the rake counted them as expected coverage anyway. On the account that prompted SDK-7422 that window is app launch, login and account selection — the screens a customer most expects covered.Those hooks are not test-framework hooks, so when they run neither
onHookStartnoronBeforeTesthas fired and the scan gate has no entry for the session at all. The gate now opens at driver creation, unconditionally whenautoScanningis on — the same policyonHookStartalready applies to Mocha hooks. Per-test include/exclude filters need a test to evaluate and there is none yet; the followingonBeforeTestrecomputes the gate for the test proper.The scans needed a parent
Scans from that window went out with neither
thTestRunUuid(no test exists yet) northHookRunUuid, so App-A11y's lookup of the scan's parent in BTCER found nothing — the scan reached the hub and then belonged to nothing:The window is now reported as a
BEFORE_ALLhook run through the existing framework path (framework.trackEvent), so the framework mints the uuid, the binary emitsHookRunStarted/HookRunFinished, and this module's ownonHookStartobserver stamps it onto the scans. No new plumbing, one source of truth for the uuid.Two things fell out better than designed:
resolveInstancealready creates an instance for aBEFORE_*PRE when none exists (it has to — suite-levelbefore allfires before anyINIT_TEST), and the first test then reuses that instance, so the hook lands with a realtest_run_idrather than orphaned.It only fires when the user actually has such a hook
Opening this from the WDIO lifecycle would report a hook that never ran on every build in the fleet. And it cannot be detected from config:
ConfigParser.addServicefolds the user's config hooks and every service's hooks into oneconfig.beforearray (eachhook.bind(service), so even name inspection fails) and the runner fires them together withPromise.all. A service is a sibling of the user's hook, not an observer of it.So the hook run is opened on demand from the scan path — the exact condition that matters, since the only reason it exists is to parent a scan.
before()—axzkajmr2odsx8dvyx2wfxw6mxtec3d9sbpmp08tlpbvftwaslksugogditfsvc1pspzd4fwgtneyxnhA failed hook is reported as failed
WDIO swallows a throwing config hook —
executeHooksWithArgsresolves with the error rather than rejecting, deliberately ("so in case of a rejection it won't cause the hook to fail"). Run one that throws and the exit code stays 0, the spec reporter says1 passing, Allure is green, and the TRA build readspassed. The failure exists in exactly one place: alog.erroron stdout.An early version of this branch made that worse — it reported the hook run as
passed. Unreported means the dashboard omits a failure;passedmeans it asserts something false about it. The instrumentation wrapper is the only thing in the process that sees the rejection, so it records the message and the hook run reports the truth:TRA build verdict moves with it —
passed {passed: 2}→failed {passed: 1, failed: 1}(qkfnla45gogeddlwlfbszpft6ff6estskur5yqoq).beforeSuitecounts too, and is easy to miss: the Mocha adapter registers it as a root before-all (this._runner.suite.beforeAll(this.wrapHook('beforeSuite'))), so it runs inside the same window. Keyed only on'before', a throwingbeforeSuitereported a passed window (eq3p9ktfb1uiiq5mtxqj670cu42o6vf82z6nzcdx). Mocha's own hooks stay excluded — each has its own hook run with its own result, so folding them in would report one failure twice.The hook is named after the hook
The name comes from the live hook stack, not a fixed label, because the window does not always open in
before():before()wdio "before" hookmu2hqmwrk4ygyh1yp8queidnxfdeeju7dk4ubbdxbeforeSuite(before() touches no driver)wdio "beforeSuite" hookeestbqwlk8mvjhzqkijfl0lc4gnzqxredgofblrwCaptured once, when the window opens: the binary takes a hook's name from the record pushed at PRE, and by POST a different hook is running. So the name says where the window opened, not where it failed — the failure message carries the failing hook's name, so both facts survive.
Verification
Bench:
test-samples/app_automate-wdio_mocha-android/examples/repro-triple/— phase boundaries annotated into the session log withbrowserstack_executor, so every scan is attributed by position rather than inferred from its command type. Verdicts read from the App Automate session-logs API.before()execute×2,executeScript×2,click×3before allbefore each#1hookInstrumentationis what makes the window observable at all —[hook-window] before#0 (bound before) finished in 37970ms— and it is logging only, no events.Unit: 4 new files/suites worth — instrumentation lifecycle (incl. sync-stays-sync and unwinding on throw), the on-demand hook run (opens once, never while a framework hook is the parent, closes at the first test, never closes what it did not open), dynamic naming, and failure reporting. Full suite at parity — 75 pre-existing failures before and after.
npm run lintclean.Global-hook scans carry no test run uuid (
178c541)The scan is a
browserstack_executor: {"action":"appAllyScan", arguments: {...}}payload, so the session log carries the exact params sent.work/SDK-7422-app-a11y-fixes/uuid-verdict.py <build> <session>prints a per-scan census — phase,thTestRunUuid,thHookRunUuid, command.rpgacbl0dyjcmmg34kwm3y5xui1bdirosz16ncll5a6695f7c00b047ca90b7d367d580f5f3058bc8f22ec974c, test 1's ownr89xojas5cvljamjpwrj8x7zcf3pc3umt8qyoptk72d8f60ebfd6485d670021fb68f43c4163ee6897,e60022566902b3f72651c4784448e2d39ff73302fyiiwkwdl8deghsyanxtq7e0shsrlkdc59xrmg01dc355a966c7539ccb73ae42e8af69b75c1495b0dyldiivrvnkhf0xi7ttvzekksvxoafxulcrx66gacd316f0f4dd32466cbe16b444a959abccd6242328Pre-fix, mocha's two
before allscans ran before any test existed and still carried the uuid the test body later reported under. Post-fix they carry only their hook uuid, and all 16 post-reload scans still land, so the base branch's reload fix is unaffected.Cucumber is identical either side because the defect was CLI-flow-only: mocha mints
TEST_ANALYTICS_IDat instance creation, so the window had a uuid to leak, while the classic path sets it atListener.testStartedand never had one. The guard is wired on both paths so the classic path cannot regress if that ever moves earlier.Known limits, stated rather than implied
08:03:57.817, wrapping done08:03:57.823). Not closable from inside a service — no driver object exists to wrap earlier. Any prior driver call absorbs it.hook:start/hook:endcome from@wdio/mocha-framework; the runner emits nothing for config hooks, so Allure/spec/junit cannot see the window. A reviewer checking the Allure report will find no trace of this change.before eachBrowserstack wdio service code #1 loses its test uuid — it falls inside the pre-test window, while chore: update package and lock files devDependencies #2/Updated README. #3 keep theirs. Those scans keep their own hook run uuid, and hook-run→test parentage is untouched here; only scan params changed.Related Jira task/s
https://browserstack.atlassian.net/browse/SDK-7422
Release (mandatory for every PR — required for the
ready-for-reviewlabel)Version bump: (required — tick exactly one)
Release notes type: (optional)
Release notes (customer-facing): (optional but encouraged)
before()hook, so screens visited during setup are covered.before()was reported nowhere.Release notes (internal): (required — engineer-facing; what actually changed / why)
accessibilityModule: open the scan gate atonBeforeExecute(driver creation) so the pre-test window — WDIO config-levelbefore/beforeSession— is scanned instead of silently uncovered while still counting as expected coverage.accessibilityModule: report that window as aBEFORE_ALLhook run via the existingframework.trackEventpath, opened ON DEMAND from the scan path so suites without such a hook report nothing extra. Named from the live hook stack;'idle' → 'attempted' → 'open' → 'closed'so a failed PRE is neither retried per command nor closed by a POST pairing with nothing.hookInstrumentation.ts: wrap the handlers of every session-scoped config hook to log start/finish, record failures, and expose the executing hook. Sync handlers stay sync so a sync throw is not converted into a rejection. Record resets per instrumented config.PRE_TEST_WINDOW_HOOKS(before,beforeSuite) — the latter runs in the same window because the Mocha adapter registers it as a root before-all.Checklist
PR Validations
Run Tests: Comment RUN_TESTS to trigger sanity tests.