From 0401bc36ea201e786fe0821e7fe00b17c237e587 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 10 Oct 2026 21:55:58 +0200 Subject: [PATCH] test(a11y): trace drawer focus before visible transition --- .../p5-15/p5-15-04/mobile-drawer/README.md | 9 +++++++++ .../e2e/responsive-accessibility-audit.spec.ts | 16 +++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/redesign/phase-5/p5-15/p5-15-04/mobile-drawer/README.md diff --git a/docs/redesign/phase-5/p5-15/p5-15-04/mobile-drawer/README.md b/docs/redesign/phase-5/p5-15/p5-15-04/mobile-drawer/README.md new file mode 100644 index 0000000..9f6325a --- /dev/null +++ b/docs/redesign/phase-5/p5-15/p5-15-04/mobile-drawer/README.md @@ -0,0 +1,9 @@ +# P5-15-04-A11Y-02 — Mobile navigation focus lifecycle + +## Reproduction and demonstrated cause + +The unchanged native reduced-motion drawer case still fails. A nonmutating test diagnostic records the actual focus attempt: MUI tries to focus the dialog while both dialog and modal ancestor have `visibility:hidden`. Focus remains on Open navigation. By the settled observation the dialog is visible, but the application's `onEntered` callback only selects a navigation item if the dialog already has focus. That condition cannot recover the failed initial attempt. Immediate Escape therefore originates outside the modal, and Tab recovery can give the focus trap a different restore target. + +The instrument calls the original HTMLElement focus implementation without changing its arguments/behaviour. It records only synthetic fixture DOM states. The existing hard/soft assertions are unchanged; no sleep, skip or expected-failure marker is added. This is transition/entry handling, not evidence that all navigation permanently traps the user. + +PRES-E07 remains FAIL at this RED checkpoint. A minimal successor must focus from the visible entering phase, preserve a user's already-selected internal focus, and leave MUI containment/Escape/restoration protection enabled. Human screen-reader/device acceptance remains incomplete; production remains NO-GO. diff --git a/job-tracker-ui/e2e/responsive-accessibility-audit.spec.ts b/job-tracker-ui/e2e/responsive-accessibility-audit.spec.ts index 60869bf..4a0f473 100644 --- a/job-tracker-ui/e2e/responsive-accessibility-audit.spec.ts +++ b/job-tracker-ui/e2e/responsive-accessibility-audit.spec.ts @@ -183,6 +183,16 @@ test('[P5-15-02 COLLECTION] thirteen areas retain native rendered entry across t test('[P5-15-02] reduced-motion mobile drawer enters focus and Escape returns it', async ({ page, owner }, info) => { test.setTimeout(60_000); + await page.addInitScript(() => { + const original = HTMLElement.prototype.focus; + const state = window as Window & { drawerFocusAttempts?: unknown[] }; + state.drawerFocusAttempts = []; + HTMLElement.prototype.focus = function (options) { + if (this.getAttribute('role') === 'dialog') state.drawerFocusAttempts!.push({ visibility: getComputedStyle(this).visibility, + parentVisibility: this.closest('.MuiModal-root') ? getComputedStyle(this.closest('.MuiModal-root')!).visibility : null }); + original.call(this, options); + }; + }); const t = translations.en; const sample: Sample = { area: 'shell', path: '/jobs', width: 390, language: 'en', theme: 'light' }; await enter(page, owner, sample); @@ -190,7 +200,11 @@ test('[P5-15-02] reduced-motion mobile drawer enters focus and Escape returns it await navigation.focus(); await navigation.press('Enter'); const drawer = page.getByRole('dialog', { name: t.navigation, exact: true }); await expect.soft(drawer.getByRole('button', { name: t.dashboard, exact: true })).toBeFocused(); - const focus = () => page.evaluate(() => ({ active: document.activeElement?.outerHTML.slice(0, 500), focusedInDialog: !!document.activeElement?.closest('[role=dialog]') })); + const focus = () => page.evaluate(() => ({ active: document.activeElement?.outerHTML.slice(0, 500), focusedInDialog: !!document.activeElement?.closest('[role=dialog]'), + attempts: (window as Window & { drawerFocusAttempts?: unknown[] }).drawerFocusAttempts, + dialogs: Array.from(document.querySelectorAll('[role=dialog]')).map(node => ({ html: node.outerHTML.slice(0, 400), visibility: getComputedStyle(node).visibility, + modalVisibility: node.closest('.MuiModal-root') ? getComputedStyle(node.closest('.MuiModal-root')!).visibility : null, + focusable: node.getAttribute('tabindex'), bounds: node.getBoundingClientRect().toJSON() })) })); const beforeScan = await focus(); await collect(page, info, sample, 'navigation-open'); const afterScan = await focus();