import { test, expect, type Page } from '@playwright/test' import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog' import { RUNTIME_STYLE_NONCE } from '../../src/lib/runtimeStyleNonce' import { dispatchShortcutEvent, triggerMenuCommand, triggerShortcutCommand, } from './testBridge' import { createFixtureVaultCopy, openFixtureVaultDesktopHarness, removeFixtureVaultCopy, } from '../helpers/fixtureVault' let tempVaultDir: string const GLOBAL_SEARCH_INPUT = 'input[placeholder="Search in all notes..."]' const GLOBAL_SEARCH_RESULTS = [ { title: 'First Search Result', path: '/vault/first-search-result.md', snippet: 'first', score: 1, note_type: null }, { title: 'Second Search Result', path: '/vault/second-search-result.md', snippet: 'second', score: 0.9, note_type: null }, { title: 'Third Search Result', path: '/vault/third-search-result.md', snippet: 'third', score: 0.8, note_type: null }, ] async function openAlphaProjectInEditor(page: Page) { await openFixtureVaultDesktopHarness(page, tempVaultDir) await page.getByText('Alpha Project', { exact: true }).first().click() await page.locator('.bn-editor').click() } function collectRuntimeStyleCspSignals(page: Page): string[] { const messages: string[] = [] page.on('pageerror', (error) => { messages.push(error.message) }) page.on('console', (message) => { const text = message.text() if ( /Content Security Policy|Refused to apply a stylesheet|Failed to insert placeholder CSS rule|style-src|insertRule/i .test(text) ) { messages.push(text) } }) return messages } async function expectRuntimeStyleNonce(page: Page): Promise { await expect.poll(async () => page.evaluate((nonce) => { const styles = Array.from(document.querySelectorAll('style')) as HTMLStyleElement[] return styles.some((style) => style.nonce === nonce) }, RUNTIME_STYLE_NONCE), { timeout: 5_000 }).toBe(true) } async function expectPropertiesPanelToggle(page: Page, toggle: () => Promise) { const propertiesButton = page.getByRole('button', { name: 'Open the properties panel' }) await expect(propertiesButton).toBeVisible({ timeout: 5_000 }) await toggle() await expect(propertiesButton).toHaveCount(0) await toggle() await expect(page.getByRole('button', { name: 'Open the properties panel' })).toBeVisible({ timeout: 5_000 }) } async function dispatchAppCommand(page: Page, id: string): Promise { await page.evaluate((commandId) => { const bridge = window.__laputaTest?.dispatchAppCommand if (typeof bridge !== 'function') { throw new Error('Tolaria test bridge is missing dispatchAppCommand') } bridge(commandId) }, id) } async function openQuickOpenFromKeyboard(page: Page): Promise { await dispatchShortcutEvent(page, { key: 'p', code: 'KeyP', ctrlKey: false, metaKey: true, shiftKey: false, altKey: false, bubbles: true, cancelable: true, }) await expect(page.getByTestId('quick-open-palette')).toBeVisible({ timeout: 5_000 }) } function selectedQuickOpenTitle(page: Page) { return page.getByTestId('quick-open-palette').locator('div.bg-accent > button:has([data-testid="note-search-item-icon"])').first() } function quickOpenTitleAt(page: Page, index: number) { return page.getByTestId('quick-open-palette').locator('button:has([data-testid="note-search-item-icon"])').nth(index) } async function installGlobalSearchResultsHarness(page: Page): Promise { await page.waitForFunction(() => Boolean(window.__mockHandlers?.search_vault)) await page.evaluate((results) => { type Handler = (args?: Record) => unknown const handlers = window.__mockHandlers as Record Object.defineProperty(window, '__mockHandlers', { configurable: true, get: () => handlers, set: (nextHandlers) => Object.assign(handlers, nextHandlers), }) handlers.search_vault = () => ({ results, elapsed_ms: 1 }) }, GLOBAL_SEARCH_RESULTS) } function searchResultRow(page: Page, title: string) { return page.locator('[role="option"]').filter({ hasText: title }).first() } async function expectSelectedSearchResult(page: Page, title: string): Promise { await expect(searchResultRow(page, title)).toHaveClass(/bg-accent/) } test.describe('keyboard command routing', () => { test.beforeEach(() => { tempVaultDir = createFixtureVaultCopy() }) test.afterEach(() => { removeFixtureVaultCopy(tempVaultDir) }) test('desktop menu-command bridge creates a note through the shared command path @smoke', async ({ page }) => { const runtimeStyleCspSignals = collectRuntimeStyleCspSignals(page) await openFixtureVaultDesktopHarness(page, tempVaultDir) await triggerMenuCommand(page, APP_COMMAND_IDS.fileNewNote) await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText(/untitled-note-\d+/i, { timeout: 5_000 }) await expectRuntimeStyleNonce(page) expect(runtimeStyleCspSignals).toEqual([]) }) test('desktop menu-command bridge toggles the properties panel through the shared command path', async ({ page }) => { await openAlphaProjectInEditor(page) await expectPropertiesPanelToggle(page, async () => { await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleProperties) }) }) test('desktop keyboard shortcut toggles the properties panel through the renderer shortcut path', async ({ page }) => { await openAlphaProjectInEditor(page) await expectPropertiesPanelToggle(page, async () => { await page.keyboard.press(process.platform === 'darwin' ? 'Meta+Shift+I' : 'Control+Shift+I') }) }) test('desktop shortcut bridge opens quick open through both Cmd+P and Cmd+O @smoke', async ({ page }) => { await openFixtureVaultDesktopHarness(page, tempVaultDir) await openQuickOpenFromKeyboard(page) await expect(page.locator('input[placeholder="Search notes..."]')).toBeFocused() await page.keyboard.press('Escape') await expect(page.getByTestId('quick-open-palette')).not.toBeVisible({ timeout: 5_000 }) await dispatchShortcutEvent(page, { key: 'o', code: 'KeyO', ctrlKey: false, metaKey: true, shiftKey: false, altKey: false, bubbles: true, cancelable: true, }) await expect(page.getByTestId('quick-open-palette')).toBeVisible({ timeout: 5_000 }) await expect(page.locator('input[placeholder="Search notes..."]')).toBeFocused() }) test('quick open ignores a stationary pointer until it moves', async ({ page }) => { await openFixtureVaultDesktopHarness(page, tempVaultDir) await openQuickOpenFromKeyboard(page) const initialTitle = await selectedQuickOpenTitle(page).textContent() const secondTitle = quickOpenTitleAt(page, 1) const targetTitle = await secondTitle.textContent() const secondTitleBox = await secondTitle.boundingBox() if (!initialTitle) throw new Error('Quick open did not select an initial note') if (!targetTitle) throw new Error('Quick open did not render a second note title') if (!secondTitleBox) throw new Error('Quick open did not render a second note box') await page.keyboard.press('Escape') await expect(page.getByTestId('quick-open-palette')).not.toBeVisible({ timeout: 5_000 }) await page.mouse.move( secondTitleBox.x + secondTitleBox.width / 2, secondTitleBox.y + secondTitleBox.height / 2, ) await openQuickOpenFromKeyboard(page) await expect(selectedQuickOpenTitle(page)).toHaveText(initialTitle) await page.keyboard.press('ArrowDown') await expect(selectedQuickOpenTitle(page)).toHaveText(targetTitle) }) test('global search arrow keys move one result at a time @smoke', async ({ page }) => { await openFixtureVaultDesktopHarness(page, tempVaultDir) await installGlobalSearchResultsHarness(page) await page.locator('body').click() await dispatchShortcutEvent(page, { key: 'f', code: 'KeyF', ctrlKey: false, metaKey: true, shiftKey: true, altKey: false, bubbles: true, cancelable: true, }) const input = page.locator(GLOBAL_SEARCH_INPUT) await expect(input).toBeVisible({ timeout: 5_000 }) await input.fill('search') await expect(searchResultRow(page, 'Third Search Result')).toBeVisible({ timeout: 5_000 }) await expectSelectedSearchResult(page, 'First Search Result') await page.keyboard.press('ArrowDown') await expectSelectedSearchResult(page, 'Second Search Result') await page.waitForTimeout(550) await page.keyboard.press('ArrowDown') await expectSelectedSearchResult(page, 'Third Search Result') await page.waitForTimeout(550) await page.keyboard.press('ArrowUp') await expectSelectedSearchResult(page, 'Second Search Result') }) test('desktop menu-command bridge toggles organized state through the shared command path', async ({ page }) => { await openAlphaProjectInEditor(page) await expect(page.getByRole('button', { name: 'Set note as organized' })).toBeVisible({ timeout: 5_000 }) await triggerMenuCommand(page, APP_COMMAND_IDS.noteToggleOrganized) await expect(page.getByRole('button', { name: 'Set note as not organized' })).toBeVisible({ timeout: 5_000 }) await triggerMenuCommand(page, APP_COMMAND_IDS.noteToggleOrganized) await expect(page.getByRole('button', { name: 'Set note as organized' })).toBeVisible({ timeout: 5_000 }) }) test('app command bridge undoes and redoes organized state through action history', async ({ page }) => { await openAlphaProjectInEditor(page) await triggerMenuCommand(page, APP_COMMAND_IDS.noteToggleOrganized) await expect(page.getByRole('button', { name: 'Set note as not organized' })).toBeVisible({ timeout: 5_000 }) await dispatchAppCommand(page, APP_COMMAND_IDS.editUndo) await expect(page.getByRole('button', { name: 'Set note as organized' })).toBeVisible({ timeout: 5_000 }) await dispatchAppCommand(page, APP_COMMAND_IDS.editRedo) await expect(page.getByRole('button', { name: 'Set note as not organized' })).toBeVisible({ timeout: 5_000 }) }) test('renderer shortcut bridge toggles the raw editor through the shared keyboard handler', async ({ page }) => { const runtimeStyleCspSignals = collectRuntimeStyleCspSignals(page) await openAlphaProjectInEditor(page) await dispatchShortcutEvent(page, { key: 'ยง', code: 'Backslash', ctrlKey: false, metaKey: true, shiftKey: false, altKey: false, bubbles: true, cancelable: true, }) await expect(page.getByTestId('raw-editor-codemirror')).toBeVisible({ timeout: 5_000 }) await expectRuntimeStyleNonce(page) await triggerShortcutCommand(page, APP_COMMAND_IDS.editToggleRawEditor) await expect(page.getByTestId('raw-editor-codemirror')).not.toBeVisible({ timeout: 5_000 }) await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 }) expect(runtimeStyleCspSignals).toEqual([]) }) test('desktop menu-command bridge toggles the AI panel, while the wrong modifier event does not', async ({ page }) => { await openAlphaProjectInEditor(page) await dispatchShortcutEvent(page, { key: 'l', code: 'KeyL', ctrlKey: false, metaKey: false, shiftKey: true, altKey: true, bubbles: true, cancelable: true, }) await page.waitForTimeout(200) await expect(page.getByTestId('ai-panel')).not.toBeVisible() await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleAiChat) await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 5_000 }) await expect(page.getByTitle('Close AI workspace')).toBeVisible() await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleAiChat) await expect(page.getByTestId('ai-panel')).not.toBeVisible({ timeout: 5_000 }) }) })