UI Component Testing Guide

How to Test Cmd+K Command Palette with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing command palettes (Cmd+K / Ctrl+K) with Playwright. Fuzzy search matching, keyboard shortcut triggers, focus management, nested command groups, recent items tracking, and reliable action execution assertions.

89%

In a 2024 survey by the Baymard Institute, 89% of desktop SaaS users reported using keyboard shortcuts at least once per session, with command palettes being the second most used shortcut after copy/paste.

Baymard Institute UX Research, 2024

0Scenarios covered
0msTypical debounce delay
0Keyboard interactions tested
0%Fewer lines with Assrt

Command Palette Interaction Flow

UserBrowserCommand PaletteSearch EngineApp RouterCmd+K keydownOpen overlay, trap focusType search queryFuzzy match (debounced)Filtered + ranked resultsArrow keys + EnterExecute selected actionNavigate or perform action

1. Why Testing Command Palettes Is Harder Than It Looks

A command palette looks simple on the surface: press Cmd+K, type a query, pick a result. But underneath that minimal UI sits a dense web of interacting concerns that make automated testing surprisingly difficult. The first challenge is the keyboard shortcut itself. Playwright's page.keyboard.press method sends key events, but whether Meta+k or Control+k triggers the palette depends on the operating system the test runs on. macOS expects Meta (Cmd), Linux and Windows expect Control. Your test must detect the platform or accept both, and it must avoid firing the shortcut while focus is inside an input field where the browser might intercept it for its own purposes.

The second challenge is fuzzy search. Most command palettes use libraries like fuse.js, cmdk, or match-sorterto rank results by relevance rather than exact match. A query like “usr sett” should surface “User Settings” at the top, but the ranking algorithm might change between versions, reorder results, or introduce new heuristics that break assertions based on positional indexes. Your tests need to assert on the content of results, not their exact ordering.

Third, focus management is a minefield. A well built command palette traps focus inside the overlay, returns focus to the previously active element when closed, and handles edge cases like Tab wrapping, Shift+Tab reverse navigation, and Escape dismissal. If any of these behaviors break, keyboard users lose access to the rest of the application. Testing focus state requires querying document.activeElement after each interaction, which is inherently async and timing-sensitive.

Fourth, nested command groups add a navigation layer. Selecting a group like “Settings” replaces the result list with that group's children and shows a breadcrumb trail. The test must verify the transition, the breadcrumb content, the back-navigation behavior (Backspace on empty input), and that the search scope narrows correctly within the group. Fifth, recent items introduce persistent state. The palette remembers what the user selected last session and surfaces those items at the top. Your test needs to execute an action, reopen the palette, and verify the recent items list updated, often relying on localStorage or a backend store that you need to seed or clear between runs.

Command Palette Internal Flow

🌐

Shortcut Pressed

Cmd+K / Ctrl+K

🔒

Overlay Opens

Focus trapped in dialog

⚙️

User Types Query

Input debounced 100-200ms

⚙️

Fuzzy Match

Filter + rank commands

Results Rendered

Keyboard navigable list

↪️

Action Executed

Navigate, toggle, or run

2. Setting Up a Reliable Test Environment

Before writing any command palette tests, you need a Playwright project configured to handle the timing sensitivities of keyboard-driven UI. The palette opens and closes with animations, search results appear after a debounce delay, and focus shifts happen asynchronously. A few configuration choices prevent the majority of flaky failures.

Project Setup
playwright.config.ts

Helper Utilities

Create a shared helper file that encapsulates platform detection and common palette interactions. This prevents duplicating the Meta vs Control logic across every test file and gives you a single place to adjust timing constants when the palette's debounce delay changes.

tests/helpers/palette.ts

Test Environment Setup Steps

🌐

Install Playwright

npm init playwright@latest

⚙️

Configure Projects

Multi-platform shortcuts

Create Helpers

openPalette, searchPalette

⚙️

Seed Test Data

Clear localStorage state

Run Suite

npx playwright test

3

Opening and Closing with Keyboard Shortcuts

Straightforward

The most fundamental test verifies that the palette opens on Cmd+K (or Ctrl+K on Linux/Windows), that focus moves to the search input immediately, and that pressing Escape closes the overlay and restores focus to the element that was active before. This scenario also covers the edge case where pressing Cmd+K while the palette is already open should close it (toggle behavior), which many implementations support.

tests/command-palette/open-close.spec.ts
Test Run: Open/Close
4

Fuzzy Search Matching and Result Ranking

Complex

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started
5

Keyboard Navigation and Action Execution

Moderate

After search, keyboard navigation is the most critical interaction pattern. Users expect to press Arrow Down to move through results, Arrow Up to go back, and Enter to execute the selected command. Many palettes also support Home and End to jump to the first and last results. The test must verify that the visual highlight moves correctly, that aria-activedescendant or aria-selected updates on the active option, and that pressing Enter on a navigation command actually routes the user to the correct page.

The trickiest part of keyboard navigation testing is verifying that the selection wraps correctly. When the user presses Arrow Down on the last result, some palettes wrap to the first result; others stop at the bottom. When the user presses Arrow Up on the first result, the focus might wrap to the last item or return to the search input. Your test must cover both boundaries to prevent regressions when the result count changes.

tests/command-palette/keyboard-nav.spec.ts

Navigation Execution: Playwright vs Assrt

test('Enter navigates to User Settings', async ({ page }) => {
  const modifier = await page.evaluate(
    () => navigator.platform.includes('Mac') ? 'Meta' : 'Control'
  );
  await page.keyboard.press(`${modifier}+k`);
  await expect(
    page.getByRole('dialog', { name: /command/i })
  ).toBeVisible();
  const input = page.getByRole('dialog').getByRole('combobox');
  await input.fill('User Settings');
  await page.waitForTimeout(200);
  await page.keyboard.press('ArrowDown');
  await page.keyboard.press('Enter');
  await expect(page).toHaveURL(/\/settings/);
});
57% fewer lines
6

Nested Command Groups and Breadcrumb Navigation

Complex

Many production command palettes organize commands into groups or categories. Selecting a group replaces the result list with that group's children and shows a breadcrumb trail (for example, “Home > Settings”). This pattern is used by VS Code, Linear, Notion, and most modern SaaS apps. It introduces a mini navigation state machine inside the palette that your tests must exercise thoroughly.

The key behaviors to verify: entering a group updates the breadcrumb, the search scope narrows to only commands within that group, pressing Backspace on an empty input navigates back one level, clicking a breadcrumb segment jumps directly to that level, and the palette remembers its position when the user types and clears the search within a group.

tests/command-palette/nested-groups.spec.ts
7

Recent Items and Persistent State

Moderate

Most command palettes track recently used commands and surface them at the top of the default result list. This is powered by localStorage, IndexedDB, or a backend API. Testing this feature requires executing a command, reopening the palette, and verifying the recent items section updated. The test also needs to handle the initial state where no recent items exist, clear state between tests to prevent cross-contamination, and verify that the recent items list has a maximum cap.

tests/command-palette/recent-items.spec.ts
8

Focus Trap and Accessibility

Complex

Accessibility is not optional for command palettes. The palette overlay must implement a proper focus trap: Tab and Shift+Tab cycle through focusable elements within the dialog without letting focus escape to the page behind it. The dialog must have the correct ARIA role (dialog or combobox with listbox), the search input must be connected to the result list via aria-controls, and the active result must be announced via aria-activedescendant. Screen reader users rely on these attributes to understand the palette's state.

The focus restore behavior is equally important. When the palette closes, focus must return to the element that was focused before the palette opened. If focus restore fails, focus drops to the document body, and keyboard users have to Tab through the entire page to find their place again. This is a common regression introduced when developers refactor the palette mount/unmount lifecycle.

tests/command-palette/accessibility.spec.ts

Accessibility Assertions Checklist

  • Dialog has role='dialog' with an accessible name
  • Search input has role='combobox' with aria-controls
  • Result list has role='listbox' with aria-label
  • Each result has role='option' with aria-selected
  • aria-activedescendant updates on keyboard navigation
  • Focus trapped inside dialog on Tab/Shift+Tab
  • Focus restores to trigger element on Escape
  • Overlay announced to screen readers on open

9. Common Pitfalls That Break Command Palette Test Suites

After running command palette test suites in CI for hundreds of projects, these are the recurring failure patterns that cause the most debugging time. Each one stems from a real issue reported in the cmdk, kbar, or Ninja Keys GitHub issue trackers.

Pitfall 1: Hardcoding Meta+K Instead of Detecting Platform

Tests that use page.keyboard.press('Meta+k') pass on macOS but silently do nothing on Linux CI runners. The Meta key is the Windows key on Linux, and most command palettes listen for Control+K on non-Mac platforms. The fix is the platform detection helper shown in Section 2. Without it, your entire suite passes locally but fails in CI, and the error is a timeout waiting for the dialog to appear rather than a clear keyboard error.

Pitfall 2: Not Waiting for the Debounce Delay

Typing into the search field and immediately asserting on results races against the debounce timer. The reliable pattern is to assert on visible result text rather than using waitForTimeout. Playwright's auto-retrying expect will poll until the debounce fires and results render. However, if you use count() immediately after typing, you get the pre-debounce count. Always use expect(locator).toHaveCount() or toContainText(), which auto-retry.

Pitfall 3: Testing Result Order Instead of Result Presence

Fuzzy search ranking is not deterministic across versions. A test that asserts “the second result is exactly ‘Billing’” will break when a new command is added that scores higher for the same query. Instead, assert that the expected result exists somewhere in the list: allTextContents().some(t => t.includes('Billing')). If order matters for a specific test (like “recently used appears first”), assert only on the first result.

Pitfall 4: Forgetting to Clear localStorage Between Tests

Recent items stored in localStorage bleed between tests unless you explicitly clear them. Test A executes “Dark Mode”, and Test B opens the palette expecting the default state but sees “Dark Mode” at the top of the recent section. Use page.evaluate(() => localStorage.removeItem('command-palette-recent')) in beforeEach. Even better, use Playwright's storageState to start each test with a clean browser context.

Pitfall 5: Animation Timing Causes Flaky Close Assertions

Palettes that animate open and close (opacity fade, scale transform) may report the dialog as “visible” during the exit animation. Asserting toBeHidden() immediately after Escape can fail if the CSS transition takes 300ms. The fix is to assert on the absence of the dialog from the DOM entirely, or wait for the transitionend event. Alternatively, disable CSS animations in your Playwright config with reducedMotion: 'reduce', which most palette libraries respect.

Common Anti-Patterns

  • Hardcoding Meta+K without platform detection
  • Asserting on result count immediately after typing (before debounce)
  • Testing exact result order instead of result presence
  • Not clearing localStorage recent items between tests
  • Ignoring CSS animation timing on close assertions
  • Using getByText for results that may have highlighted/split text nodes
  • Not testing the empty query state (default results)
  • Assuming aria-selected is the only selection indicator
Flaky Test Debug Session

10. Writing These Scenarios in Plain English with Assrt

Every scenario in this guide translates to 10 to 20 lines of Playwright TypeScript. The platform detection logic, debounce handling, ARIA attribute checks, and focus restoration verification add up fast. Assrt lets you describe the same scenarios in plain English, and it compiles them into the Playwright code you saw in Sections 3 through 8.

Here is the fuzzy search scenario from Section 4, the keyboard navigation test from Section 5, and the focus management test from Section 8, compiled into a single .assrt file. Assrt handles the platform modifier detection, debounce waiting, and ARIA assertions automatically based on the semantic intent of each step.

command-palette.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When the palette library updates its DOM structure (for example, cmdk v1 changed from div to semantic dialog elements), Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.

Start with the open/close shortcut test. Once it passes in CI, add the fuzzy search scenario, then keyboard navigation, then nested groups, then focus management, then recent items. In a single afternoon you can have complete command palette coverage that most applications never manage to achieve by hand.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk