From 1674a68b17860bbf80b6c7fcca394b3301f6605d Mon Sep 17 00:00:00 2001 From: Jeff Repanich Date: Sat, 15 Aug 2026 19:27:01 -0400 Subject: [PATCH 1/5] test: reproduce command palette keyboard gap --- .github/workflows/ci.yml | 3 ++ .github/workflows/deploy.yml | 3 ++ .gitignore | 2 +- package-lock.json | 73 +++++++++++++++++++++++++++ package.json | 5 +- tests/browser/docs-search.test.tsx | 81 ++++++++++++++++++++++++++++++ vitest.test.browser.config.ts | 32 ++++++++++++ 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 tests/browser/docs-search.test.tsx create mode 100644 vitest.test.browser.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10d76be..2766332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Install browser engine + run: npx playwright install --with-deps chromium + - name: Format run: npm run fmt:check diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index fccccb1..4b80593 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -39,6 +39,9 @@ jobs: - name: Install dependencies run: npm ci + - name: Install browser engine + run: npx playwright install --with-deps chromium + - name: Validate source run: npm run fmt:check && npm run lint && npm run typecheck diff --git a/.gitignore b/.gitignore index dbd76ba..bd0b3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,9 +53,9 @@ Thumbs.db screenshots/ **/screenshots/ .codex-screenshots/ +.vitest-attachments/ # Environment .env .env.* !.env.example - diff --git a/package-lock.json b/package-lock.json index e97a982..5baf07d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,9 @@ "@askrjs/vite": "0.0.13", "@types/node": "^26.2.0", "@typescript/native": "npm:typescript@^7.0.2", + "@vitest/browser-playwright": "4.1.10", "monaco-editor": "^0.56.0", + "playwright": "^1.62.1", "tsx": "4.23.11", "typescript": "npm:@typescript/typescript6@^6.0.2", "vite-plus": "^0.2.8" @@ -2544,6 +2546,30 @@ "vitest": "4.1.10" } }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.10.tgz", + "integrity": "sha512-nMoXGEiRpT7m3W7NsbvrM2aKNwiNHZf+zEpUCvMteGjZFvfT96Q9fh7QyB98dvDWXiKvrLxA7bJ1mCOOv+JQPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.10", + "@vitest/mocker": "4.1.10", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, "node_modules/@vitest/browser-preview": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/browser-preview/-/browser-preview-4.1.10.tgz", @@ -4597,6 +4623,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", diff --git a/package.json b/package.json index 4e73a62..253580b 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "fmt:check": "vp fmt . --check", "test:unit": "vp test run --exclude tests/generated-output-contract.test.ts", "test:static": "vp test run tests/generated-output-contract.test.ts", - "test": "npm run test:unit", + "test": "npm run test:unit && npm run test:browser", + "test:browser": "vp test run -c vitest.test.browser.config.ts --mode production", "typecheck": "tsc --noEmit", "check": "npm run fmt:check && npm run lint && npm run typecheck && npm test && npm run build", "preview": "vp preview --outDir dist" @@ -46,6 +47,8 @@ "@askrjs/testing": "0.0.6", "@askrjs/vite": "0.0.13", "@types/node": "^26.2.0", + "@vitest/browser-playwright": "4.1.10", + "playwright": "^1.62.1", "@typescript/native": "npm:typescript@^7.0.2", "monaco-editor": "^0.56.0", "tsx": "4.23.11", diff --git a/tests/browser/docs-search.test.tsx b/tests/browser/docs-search.test.tsx new file mode 100644 index 0000000..5aca3ca --- /dev/null +++ b/tests/browser/docs-search.test.tsx @@ -0,0 +1,81 @@ +import { page, userEvent } from '@vitest/browser/context'; +import { cleanupApp, createSPA } from '@askrjs/askr/boot'; +import { createRouteRegistry, route } from '@askrjs/askr/router'; +import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'; +import { DocsSearch } from '../../src/pages/docs/search'; +import { searchDocs } from '../../src/pages/docs/search-index'; + +const query = 'createSPA'; +const destination = searchDocs(query, 1)[0]; + +if (!destination) { + throw new Error(`Expected a documentation search result for ${query}`); +} + +const registry = createRouteRegistry(() => { + route('/search-test', DocsSearch); + route(destination.route, () =>

Destination reached

); +}); + +async function settle(): Promise { + await Promise.resolve(); + await Promise.resolve(); + await new Promise((resolve) => requestAnimationFrame(resolve)); +} + +async function waitForElement(read: () => T | null): Promise { + const deadline = performance.now() + 5_000; + do { + const element = read(); + if (element) return element; + await settle(); + await new Promise((resolve) => setTimeout(resolve, 25)); + } while (performance.now() < deadline); + throw new Error('Timed out waiting for the documentation search interaction'); +} + +describe('documentation command-palette search', () => { + let container: HTMLDivElement | undefined; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + window.history.replaceState({}, '', '/search-test'); + }); + + afterEach(async () => { + if (container) { + cleanupApp(container); + container.remove(); + container = undefined; + } + await page.viewport(1280, 900); + }); + + it('moves a real keyboard highlight from the input and navigates on Enter', async () => { + await createSPA({ root: container!, registry }); + await userEvent.click(container!.querySelector('button')!); + + const input = await waitForElement( + () => document.querySelector('[data-docs-search-input]') as HTMLInputElement | null + ); + await userEvent.fill(input, query); + const firstResult = await waitForElement( + () => document.querySelector('[data-slot="command-item"]') as HTMLAnchorElement | null + ); + + expect(document.activeElement).toBe(input); + expect(input.getAttribute('aria-activedescendant')).toBeNull(); + + await userEvent.keyboard('{ArrowDown}'); + + expect(document.activeElement).toBe(input); + expect(input.getAttribute('aria-activedescendant')).toBe(firstResult.id); + expect(firstResult.getAttribute('data-active')).toBe('true'); + + await userEvent.keyboard('{Enter}'); + await waitForElement(() => document.querySelector('[data-search-destination]')); + + expect(window.location.pathname).toBe(destination.route); + }); +}); diff --git a/vitest.test.browser.config.ts b/vitest.test.browser.config.ts new file mode 100644 index 0000000..a80bf47 --- /dev/null +++ b/vitest.test.browser.config.ts @@ -0,0 +1,32 @@ +import { askr } from '@askrjs/vite'; +import { playwright } from 'vite-plus/test/browser-playwright'; +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + plugins: [askr()], + test: { + api: { + host: '127.0.0.1', + }, + globals: true, + browser: { + api: { + host: '127.0.0.1', + port: 0, + }, + enabled: true, + headless: true, + provider: playwright(), + instances: [{ browser: 'chromium' }], + }, + include: ['tests/browser/**/*.test.tsx'], + }, + oxc: { + jsx: { + runtime: 'automatic', + importSource: '@askrjs/askr', + }, + jsxInject: + "import { jsx, jsxs, Fragment } from '@askrjs/askr/jsx-runtime';", + }, +}); From 71de82c330fae134201051b577b1519481d68431 Mon Sep 17 00:00:00 2001 From: Jeff Repanich Date: Sat, 15 Aug 2026 19:28:47 -0400 Subject: [PATCH 2/5] fix: add command palette keyboard navigation --- .gitignore | 1 + package.json | 4 +- src/pages/docs/search.tsx | 100 +++++++++++++++++++++++++---- src/styles.css | 3 +- tests/browser/docs-search.test.tsx | 33 ++++++++-- 5 files changed, 121 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index bd0b3d2..6d0b757 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ Thumbs.db # Screenshots screenshots/ **/screenshots/ +**/__screenshots__/ .codex-screenshots/ .vitest-attachments/ diff --git a/package.json b/package.json index 253580b..16e6521 100644 --- a/package.json +++ b/package.json @@ -47,10 +47,10 @@ "@askrjs/testing": "0.0.6", "@askrjs/vite": "0.0.13", "@types/node": "^26.2.0", - "@vitest/browser-playwright": "4.1.10", - "playwright": "^1.62.1", "@typescript/native": "npm:typescript@^7.0.2", + "@vitest/browser-playwright": "4.1.10", "monaco-editor": "^0.56.0", + "playwright": "^1.62.1", "tsx": "4.23.11", "typescript": "npm:@typescript/typescript6@^6.0.2", "vite-plus": "^0.2.8" diff --git a/src/pages/docs/search.tsx b/src/pages/docs/search.tsx index 3ea8a9f..90f4593 100644 --- a/src/pages/docs/search.tsx +++ b/src/pages/docs/search.tsx @@ -25,7 +25,9 @@ function loadSearchIndex(): Promise { } function DocsSearchInput(props: { + activeResultId: () => string | undefined; close: () => void; + onKeyDown: (event: KeyboardEvent) => void; runSearch: (value: string | undefined) => Promise; }) { return ( @@ -33,8 +35,14 @@ function DocsSearchInput(props: {