diff --git a/apps/desktop/src/main/__tests__/real-window-smoke-contract.test.ts b/apps/desktop/src/main/__tests__/real-window-smoke-contract.test.ts index 90b04fffa2..2c4c80dda1 100644 --- a/apps/desktop/src/main/__tests__/real-window-smoke-contract.test.ts +++ b/apps/desktop/src/main/__tests__/real-window-smoke-contract.test.ts @@ -81,8 +81,8 @@ describe('real Electron window smoke gate (PR-DESKTOP-SMOKE-0)', () => { ); assert.match( src, - /maka-search-modal-input/, - 'programmatic focus check must accept the actual search input focus target, not only the close button', + /activeElementInSearchModal/, + 'programmatic focus check must verify focus is trapped structurally inside the search modal (any interactive control), not by a brittle class on the focused element', ); assert.match(src, /Window diagnostics/, 'real-window smoke report must include BrowserWindow/renderer diagnostics when available'); assert.match(src, /PROGRAMMATIC_SMOKE_CHECKS/, 'real-window smoke must include an accessibility-independent programmatic BrowserWindow/renderer layer'); diff --git a/apps/desktop/src/main/__tests__/smoke-diagnostic-contract.test.ts b/apps/desktop/src/main/__tests__/smoke-diagnostic-contract.test.ts new file mode 100644 index 0000000000..8f75598ee6 --- /dev/null +++ b/apps/desktop/src/main/__tests__/smoke-diagnostic-contract.test.ts @@ -0,0 +1,65 @@ +/** + * Contract between the real-window smoke diagnostic (main.ts, injected into + * the renderer) and the live modal DOM. + * + * The diagnostic detects the search modal's backdrop and focus state by + * querying the renderer. Those selectors silently drifted once the modal + * migrated to the Base UI `Dialog` primitives: the backdrop stopped carrying + * `.maka-search-modal-backdrop`, and the focused input lost the + * `maka-search-modal-input` class to InputGroup's own utility classes — so + * the `programmatic-search-modal-open` and `programmatic-focus-target` smoke + * checks failed on a clean tree (reproduced identically on `main`). + * + * This gate binds the diagnostic to a STABLE hook so it can't drift again: + * - the shared DialogBackdrop carries `maka-dialog-backdrop` (style-free), + * - the diagnostic queries that hook (not the dead search-modal-backdrop), + * - focus-trap is detected structurally via closest('.maka-search-modal'), + * not via a brittle class on the focused element. + */ + +import { strict as assert } from 'node:assert'; +import { readFile } from 'node:fs/promises'; +import { describe, it } from 'node:test'; +import { join } from 'node:path'; + +const MAIN_PATH = join(process.cwd(), 'src', 'main', 'main.ts'); +const UI_PATH = join(process.cwd(), '..', '..', 'packages', 'ui', 'src', 'ui.tsx'); + +describe('real-window smoke diagnostic ↔ modal DOM contract', () => { + it('the shared dialog backdrop carries the stable maka-dialog-backdrop hook', async () => { + const ui = await readFile(UI_PATH, 'utf8'); + assert.match( + ui, + /maka-dialog-backdrop/, + 'DialogBackdrop must carry the style-free `maka-dialog-backdrop` hook so the smoke diagnostic can select the backdrop. Base UI otherwise renders only drifting Tailwind utility classes.', + ); + }); + + it('the diagnostic queries the live backdrop hook, not the dead search-modal-backdrop class', async () => { + const main = await readFile(MAIN_PATH, 'utf8'); + assert.match( + main, + /querySelector\('\.maka-dialog-backdrop'\)/, + 'the smoke diagnostic must detect the modal backdrop via `.maka-dialog-backdrop`', + ); + assert.doesNotMatch( + main, + /maka-search-modal-backdrop/, + 'the dead `.maka-search-modal-backdrop` selector (no element carries it) must be gone', + ); + }); + + it('the diagnostic reports whether focus is trapped inside the search modal', async () => { + const main = await readFile(MAIN_PATH, 'utf8'); + assert.match( + main, + /activeElementInSearchModal/, + 'the diagnostic must expose `activeElementInSearchModal` so focus-target is checked structurally', + ); + assert.match( + main, + /closest\('\.maka-search-modal'\)/, + 'focus-trap detection must use closest(.maka-search-modal), not a class on the focused element', + ); + }); +}); diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index 5999bfff17..42f7897cca 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -1562,8 +1562,9 @@ function emitRealWindowSmokeDiagnostic(stage: string): void { title: document.title, appFramePresent: Boolean(document.querySelector('.appFrame')), searchModalPresent: Boolean(document.querySelector('.maka-search-modal')), - searchModalBackdropPresent: Boolean(document.querySelector('.maka-search-modal-backdrop')), + searchModalBackdropPresent: Boolean(document.querySelector('.maka-dialog-backdrop')), errorBoundaryPresent: Boolean(document.querySelector('.maka-error-surface')), + activeElementInSearchModal: Boolean(document.activeElement && document.activeElement.closest && document.activeElement.closest('.maka-search-modal')), activeElement: document.activeElement ? { tagName: document.activeElement.tagName, className: typeof document.activeElement.className === 'string' ? document.activeElement.className : '', diff --git a/apps/desktop/src/renderer/styles.css b/apps/desktop/src/renderer/styles.css index 9e7de1d004..4032c9f484 100644 --- a/apps/desktop/src/renderer/styles.css +++ b/apps/desktop/src/renderer/styles.css @@ -418,12 +418,9 @@ button:active { } /* Search modal: input + local thread-search results, backed by - * `window.maka.search.thread()` through the renderer shell. */ -.maka-search-modal-backdrop { - /* Reuse the existing modal backdrop tokens. The presentational - * `.maka-modal-backdrop` class above this block handles dimming - * and centering. */ -} + * `window.maka.search.thread()` through the renderer shell. The dialog + * backdrop is rendered by the shared Dialog primitive (.maka-dialog-backdrop + * + .maka-modal-backdrop tokens), not a search-modal-specific element. */ .maka-search-modal { width: min(560px, 92vw); max-height: 64vh; diff --git a/packages/ui/src/ui.tsx b/packages/ui/src/ui.tsx index 82311047cb..ac718b57a0 100644 --- a/packages/ui/src/ui.tsx +++ b/packages/ui/src/ui.tsx @@ -221,7 +221,11 @@ export const DialogBackdrop = forwardRef ); diff --git a/scripts/desktop-real-window-smoke.mjs b/scripts/desktop-real-window-smoke.mjs index d41eb12a8d..1d20f9665a 100644 --- a/scripts/desktop-real-window-smoke.mjs +++ b/scripts/desktop-real-window-smoke.mjs @@ -382,16 +382,14 @@ function buildProgrammaticResults(args, diagnostics) { }, { check: PROGRAMMATIC_SMOKE_CHECKS[4], + // Focus must land on a control trapped inside the search modal. Check + // this structurally (activeElement is inside `.maka-search-modal`) rather + // than by a class on the focused element — the search input's class is + // owned by InputGroup's utility classes and isn't stable. ok: - ( - renderer.activeElement?.tagName === 'INPUT' && - renderer.activeElement?.className?.includes('maka-search-modal-input') - ) || - ( - renderer.activeElement?.tagName === 'BUTTON' && - renderer.activeElement?.className?.includes('maka-search-modal-close') - ), - note: `activeElement=${JSON.stringify(renderer.activeElement ?? null)}`, + renderer.activeElementInSearchModal === true && + (renderer.activeElement?.tagName === 'INPUT' || renderer.activeElement?.tagName === 'BUTTON'), + note: `activeElementInSearchModal=${renderer.activeElementInSearchModal ?? 'unknown'} activeElement=${JSON.stringify(renderer.activeElement ?? null)}`, }, { check: PROGRAMMATIC_SMOKE_CHECKS[5],