refactor: make shortcut QA modes explicit
This commit is contained in:
@@ -729,12 +729,15 @@ Selection-dependent note actions are wired through both the command palette and
|
||||
|
||||
Shortcut routing is explicit:
|
||||
|
||||
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs and modifier rules
|
||||
- `appCommandCatalog.ts` is the shared shortcut manifest for command IDs, modifier rules, and deterministic QA metadata
|
||||
- `useAppKeyboard` is the primary execution path for real shortcut keypresses, including Tauri runs
|
||||
- macOS browser-reserved chords such as `Cmd+Shift+L` are unblocked at webview init via `tauri-plugin-prevent-default`, then continue through the same renderer-first command path
|
||||
- `menu.rs` and `useMenuEvents` emit the same command IDs for native menu clicks and accelerators
|
||||
- `appCommandDispatcher.ts` suppresses the paired native-menu/renderer echo from a single shortcut so the command runs once
|
||||
- Deterministic QA uses both real key events in a Tauri-like environment and `trigger_menu_command` to prove the keyboard path and the native menu path without relying on flaky macOS key synthesis
|
||||
- Deterministic QA uses two explicit proof paths from the shared manifest:
|
||||
- renderer shortcut-event proof through `window.__laputaTest.triggerShortcutCommand()`
|
||||
- native menu-command proof through `trigger_menu_command`
|
||||
- The browser harness is only a deterministic desktop command bridge; exact native accelerator delivery still requires real Tauri QA for commands flagged as manual-native-critical
|
||||
|
||||
## Auto-Release & In-App Updates
|
||||
|
||||
|
||||
@@ -282,11 +282,16 @@ type SidebarSelection =
|
||||
|
||||
### Command Registry
|
||||
|
||||
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Shortcut combos live in `appCommandCatalog.ts`; real keypresses always flow through `useAppKeyboard`, native menu clicks emit the same command IDs through `useMenuEvents`, and `appCommandDispatcher.ts` suppresses the duplicate native/renderer echo from a single shortcut. On macOS, any browser-reserved chord that WKWebView swallows before that path must also be added to the narrow `tauri-plugin-prevent-default` registration in `src-tauri/src/lib.rs`.
|
||||
`useCommandRegistry` + `useAppCommands` build a centralized command registry. Commands are registered with labels, shortcuts, and handlers. The `CommandPalette` (Cmd+K) fuzzy-searches this registry. Shortcut combos live in `appCommandCatalog.ts`; real keypresses always flow through `useAppKeyboard`, native menu clicks emit the same command IDs through `useMenuEvents`, and `appCommandDispatcher.ts` suppresses the duplicate native/renderer echo from a single shortcut. On macOS, any browser-reserved chord that WKWebView swallows before that path must also be added to the narrow `tauri-plugin-prevent-default` registration in `src-tauri/src/lib.rs`. The same shortcut manifest also declares the deterministic QA mode for each shortcut-capable command.
|
||||
|
||||
Commands whose availability depends on the current note or Git state must also flow through `update_menu_state` so the native menu stays in sync with the command palette. The deleted-note restore action in Changes view is the reference example: the row opens a deleted diff preview, the command palette exposes "Restore Deleted Note", and the Note menu enables the same action only while that preview is active.
|
||||
|
||||
For automated QA, prefer real key events in a Tauri-like environment for shortcut behavior and `window.__laputaTest.triggerMenuCommand()` for the native menu click path. For macOS browser-reserved chords, also perform native QA in the real Tauri app because the webview-init prevent-default layer is only active there. Do not treat flaky synthesized macOS keystrokes as proof that a shortcut works unless you also confirm the visible app behavior.
|
||||
For automated shortcut QA, use the explicit proof path from `appCommandCatalog.ts`:
|
||||
|
||||
- `window.__laputaTest.triggerShortcutCommand()` for deterministic renderer shortcut-event coverage
|
||||
- `window.__laputaTest.triggerMenuCommand()` for deterministic native menu-command coverage
|
||||
|
||||
That browser harness is a deterministic desktop command bridge, not real native accelerator QA. For macOS browser-reserved chords, still perform native QA in the real Tauri app because the webview-init prevent-default layer is only active there. Do not treat flaky synthesized macOS keystrokes as proof that a shortcut works unless you also confirm the visible app behavior.
|
||||
|
||||
## Running Tests
|
||||
|
||||
@@ -341,7 +346,7 @@ BASE_URL="http://localhost:5173" npx playwright test tests/smoke/<slug>.spec.ts
|
||||
|
||||
1. Register the command in `useAppCommands.ts` via the command registry
|
||||
2. Add a corresponding menu bar item in `menu.rs` for discoverability
|
||||
3. If it has a keyboard shortcut, register it in `appCommandCatalog.ts` with the canonical command ID and modifier rule, then wire the matching native menu item in `menu.rs` if it should also appear in the menu bar
|
||||
3. If it has a keyboard shortcut, register it in `appCommandCatalog.ts` with the canonical command ID, modifier rule, and deterministic QA mode, then wire the matching native menu item in `menu.rs` if it should also appear in the menu bar
|
||||
4. If its enabled state depends on runtime selection (active note, deleted preview, Git status, etc.), thread that flag through `useMenuEvents.ts` and `update_menu_state` so the native menu enables/disables correctly
|
||||
|
||||
### Modify styling
|
||||
|
||||
35
docs/adr/0054-deterministic-shortcut-qa-matrix.md
Normal file
35
docs/adr/0054-deterministic-shortcut-qa-matrix.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
type: ADR
|
||||
id: "0054"
|
||||
title: "Deterministic shortcut QA matrix"
|
||||
status: active
|
||||
date: 2026-04-11
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0052 made renderer-first shortcut execution the primary runtime path, and ADR 0053 added a narrow macOS webview-init prevent-default layer for browser-reserved chords such as `Cmd+Shift+L`. Those decisions improved behavior, but the automated QA story was still muddy:
|
||||
|
||||
- browser smoke tests were describing a mocked desktop harness as if it were native Tauri QA
|
||||
- some tests used `page.keyboard.press()` for commands whose real desktop accelerators are intercepted or reserved by the browser shell
|
||||
- native menu command coverage existed, but the catalog did not declare which deterministic proof path each shortcut should use
|
||||
|
||||
That made it too easy to ship a shortcut with passing automation while overstating what the automation had actually proven.
|
||||
|
||||
## Decision
|
||||
|
||||
**Laputa will treat shortcut QA as an explicit part of the shared command manifest. Every shortcut-capable command must have a deterministic automated proof path, and the test harness must distinguish renderer shortcut-event proof from native menu-command proof instead of calling the browser harness “native Tauri QA”.**
|
||||
|
||||
## Options considered
|
||||
|
||||
- **Option A** (chosen): Add a deterministic shortcut QA matrix to the shared command catalog. Renderer shortcut handling can be exercised through synthetic `keydown` events generated from the manifest, while native menu commands are exercised through `trigger_menu_command`. Pros: deterministic, explicit, and honest about what is being proved. Cons: still requires real native QA for exact accelerator delivery on macOS.
|
||||
- **Option B**: Keep using ad hoc Playwright key presses and browser-side menu shims. Lower change cost, but still allows false claims about native coverage and still depends on browser-reserved shortcuts behaving nicely.
|
||||
- **Option C**: Block all shortcut work until full native Tauri automation exists. Strongest eventual guarantee, but it would leave the keyboard-first app without a usable deterministic QA strategy today.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `appCommandCatalog.ts` now owns not just command IDs and modifier rules, but also the deterministic QA mode for each shortcut-capable command.
|
||||
- Browser harness smoke tests must describe themselves as a desktop command bridge, not native app QA.
|
||||
- Renderer shortcut behavior can be verified deterministically without depending on browser chrome or flaky AppleScript key synthesis.
|
||||
- Native menu-command behavior can be verified deterministically through the Tauri command bridge.
|
||||
- Exact desktop accelerator delivery still requires real Tauri QA for commands flagged as needing manual native verification, especially browser-reserved macOS chords.
|
||||
@@ -109,3 +109,4 @@ proposed → active → superseded
|
||||
| [0051](0051-shared-shortcut-manifest-for-testable-routing.md) | Shared shortcut manifest for testable routing | superseded → [0052](0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md) |
|
||||
| [0052](0052-renderer-first-shortcut-execution-with-native-menu-dedupe.md) | Renderer-first shortcut execution with native-menu dedupe | active |
|
||||
| [0053](0053-webview-init-prevention-for-browser-reserved-shortcuts.md) | Webview-init prevention for browser-reserved shortcuts | active |
|
||||
| [0054](0054-deterministic-shortcut-qa-matrix.md) | Deterministic shortcut QA matrix | active |
|
||||
|
||||
@@ -51,8 +51,27 @@ export type AppCommandShortcutCombo =
|
||||
| 'command-or-ctrl'
|
||||
| 'command-or-ctrl-shift'
|
||||
| 'command-shift'
|
||||
export type AppCommandDeterministicQaMode =
|
||||
| 'renderer-shortcut-event'
|
||||
| 'native-menu-command'
|
||||
type ShortcutEventLike = Pick<KeyboardEvent, 'altKey' | 'ctrlKey' | 'metaKey' | 'shiftKey' | 'key' | 'code'>
|
||||
|
||||
export interface AppCommandDeterministicQaDefinition {
|
||||
preferredMode: AppCommandDeterministicQaMode
|
||||
supportsRendererShortcutEvent: boolean
|
||||
supportsNativeMenuCommand: boolean
|
||||
requiresManualNativeAcceleratorQa: boolean
|
||||
}
|
||||
|
||||
export interface AppCommandShortcutEventOptions {
|
||||
preferControl?: boolean
|
||||
}
|
||||
|
||||
export type AppCommandShortcutEventInit = Pick<
|
||||
KeyboardEventInit,
|
||||
'altKey' | 'bubbles' | 'cancelable' | 'code' | 'ctrlKey' | 'key' | 'metaKey' | 'shiftKey'
|
||||
>
|
||||
|
||||
type SimpleHandlerKey =
|
||||
| 'onOpenSettings'
|
||||
| 'onCheckForUpdates'
|
||||
@@ -312,6 +331,20 @@ const NATIVE_MENU_COMMAND_SET = new Set<string>(
|
||||
.map(([id]) => id),
|
||||
)
|
||||
|
||||
const MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET = new Set<AppCommandId>([
|
||||
APP_COMMAND_IDS.appSettings,
|
||||
APP_COMMAND_IDS.fileNewNote,
|
||||
APP_COMMAND_IDS.fileDailyNote,
|
||||
APP_COMMAND_IDS.fileQuickOpen,
|
||||
APP_COMMAND_IDS.fileSave,
|
||||
APP_COMMAND_IDS.editFindInVault,
|
||||
APP_COMMAND_IDS.viewToggleProperties,
|
||||
APP_COMMAND_IDS.viewToggleAiChat,
|
||||
APP_COMMAND_IDS.viewCommandPalette,
|
||||
APP_COMMAND_IDS.noteToggleOrganized,
|
||||
APP_COMMAND_IDS.noteToggleFavorite,
|
||||
])
|
||||
|
||||
const shortcutKeyMaps = {
|
||||
'command-or-ctrl': new Map<string, AppCommandId>(),
|
||||
'command-or-ctrl-shift': new Map<string, AppCommandId>(),
|
||||
@@ -353,6 +386,41 @@ export function isNativeMenuCommandId(value: string): value is AppCommandId {
|
||||
return NATIVE_MENU_COMMAND_SET.has(value)
|
||||
}
|
||||
|
||||
export function getDeterministicShortcutQaDefinition(
|
||||
id: AppCommandId,
|
||||
): AppCommandDeterministicQaDefinition | null {
|
||||
const definition = APP_COMMAND_DEFINITIONS[id]
|
||||
if (!definition.shortcut) return null
|
||||
|
||||
return {
|
||||
preferredMode: definition.menuOwned ? 'native-menu-command' : 'renderer-shortcut-event',
|
||||
supportsRendererShortcutEvent: true,
|
||||
supportsNativeMenuCommand: definition.menuOwned,
|
||||
requiresManualNativeAcceleratorQa: MANUAL_NATIVE_ACCELERATOR_QA_COMMAND_SET.has(id),
|
||||
}
|
||||
}
|
||||
|
||||
export function getShortcutEventInit(
|
||||
id: AppCommandId,
|
||||
options: AppCommandShortcutEventOptions = {},
|
||||
): AppCommandShortcutEventInit | null {
|
||||
const shortcut = APP_COMMAND_DEFINITIONS[id].shortcut
|
||||
if (!shortcut) return null
|
||||
|
||||
const useControl = options.preferControl ?? false
|
||||
|
||||
return {
|
||||
key: shortcut.key,
|
||||
code: shortcut.code,
|
||||
altKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
ctrlKey: useControl,
|
||||
metaKey: !useControl,
|
||||
shiftKey: shortcut.combo !== 'command-or-ctrl',
|
||||
}
|
||||
}
|
||||
|
||||
export function shortcutCombosForEvent({
|
||||
altKey,
|
||||
ctrlKey,
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
resetAppCommandDispatchStateForTests,
|
||||
type AppCommandHandlers,
|
||||
} from './appCommandDispatcher'
|
||||
import {
|
||||
getDeterministicShortcutQaDefinition,
|
||||
getShortcutEventInit,
|
||||
} from './appCommandCatalog'
|
||||
|
||||
function makeHandlers(): AppCommandHandlers {
|
||||
return {
|
||||
@@ -73,6 +77,39 @@ describe('appCommandDispatcher', () => {
|
||||
expect(findShortcutCommandId('command-shift', '¬', 'KeyL')).toBe(APP_COMMAND_IDS.viewToggleAiChat)
|
||||
})
|
||||
|
||||
it('gives every shortcut command an explicit deterministic QA strategy', () => {
|
||||
expect(getDeterministicShortcutQaDefinition(APP_COMMAND_IDS.fileNewNote)).toMatchObject({
|
||||
preferredMode: 'native-menu-command',
|
||||
supportsRendererShortcutEvent: true,
|
||||
supportsNativeMenuCommand: true,
|
||||
requiresManualNativeAcceleratorQa: true,
|
||||
})
|
||||
expect(getDeterministicShortcutQaDefinition(APP_COMMAND_IDS.noteToggleFavorite)).toMatchObject({
|
||||
preferredMode: 'renderer-shortcut-event',
|
||||
supportsRendererShortcutEvent: true,
|
||||
supportsNativeMenuCommand: false,
|
||||
requiresManualNativeAcceleratorQa: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('builds deterministic keyboard events from the shared shortcut manifest', () => {
|
||||
expect(getShortcutEventInit(APP_COMMAND_IDS.viewToggleAiChat)).toMatchObject({
|
||||
key: 'l',
|
||||
code: 'KeyL',
|
||||
metaKey: true,
|
||||
ctrlKey: false,
|
||||
shiftKey: true,
|
||||
})
|
||||
expect(getShortcutEventInit(APP_COMMAND_IDS.viewToggleAiChat, { preferControl: true })).toMatchObject({
|
||||
key: 'l',
|
||||
code: 'KeyL',
|
||||
metaKey: false,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
})
|
||||
expect(getShortcutEventInit(APP_COMMAND_IDS.appCheckForUpdates)).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves event modifiers through the shared shortcut catalog', () => {
|
||||
expect(
|
||||
findShortcutCommandIdForEvent({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { isTauri } from '../mock-tauri'
|
||||
import type { AppCommandShortcutEventInit, AppCommandShortcutEventOptions } from './appCommandCatalog'
|
||||
import {
|
||||
APP_COMMAND_EVENT_NAME,
|
||||
executeAppCommand,
|
||||
@@ -11,8 +12,10 @@ declare global {
|
||||
interface Window {
|
||||
__laputaTest?: {
|
||||
dispatchAppCommand?: (id: string) => void
|
||||
dispatchShortcutEvent?: (init: AppCommandShortcutEventInit) => void
|
||||
dispatchBrowserMenuCommand?: (id: string) => void
|
||||
triggerMenuCommand?: (id: string) => Promise<unknown>
|
||||
triggerShortcutCommand?: (id: string, options?: AppCommandShortcutEventOptions) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main.tsx
31
src/main.tsx
@@ -8,13 +8,20 @@ import {
|
||||
isAppCommandId,
|
||||
isNativeMenuCommandId,
|
||||
} from './hooks/appCommandDispatcher'
|
||||
import {
|
||||
getShortcutEventInit,
|
||||
type AppCommandShortcutEventInit,
|
||||
type AppCommandShortcutEventOptions,
|
||||
} from './hooks/appCommandCatalog'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__laputaTest?: {
|
||||
dispatchAppCommand?: (id: string) => void
|
||||
dispatchShortcutEvent?: (init: AppCommandShortcutEventInit) => void
|
||||
dispatchBrowserMenuCommand?: (id: string) => void
|
||||
triggerMenuCommand?: (id: string) => Promise<unknown>
|
||||
triggerShortcutCommand?: (id: string, options?: AppCommandShortcutEventOptions) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +34,15 @@ if ('__TAURI__' in window || '__TAURI_INTERNALS__' in window) {
|
||||
document.addEventListener('contextmenu', (e) => e.preventDefault(), true)
|
||||
}
|
||||
|
||||
function dispatchDeterministicShortcutEvent(init: AppCommandShortcutEventInit) {
|
||||
const target =
|
||||
document.activeElement instanceof HTMLElement
|
||||
? document.activeElement
|
||||
: document.body ?? window
|
||||
|
||||
target.dispatchEvent(new KeyboardEvent('keydown', init))
|
||||
}
|
||||
|
||||
window.__laputaTest = {
|
||||
dispatchAppCommand(id: string) {
|
||||
if (!isAppCommandId(id)) {
|
||||
@@ -34,6 +50,9 @@ window.__laputaTest = {
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(APP_COMMAND_EVENT_NAME, { detail: id }))
|
||||
},
|
||||
dispatchShortcutEvent(init: AppCommandShortcutEventInit) {
|
||||
dispatchDeterministicShortcutEvent(init)
|
||||
},
|
||||
async triggerMenuCommand(id: string) {
|
||||
if (!isNativeMenuCommandId(id)) {
|
||||
throw new Error(`Unknown native menu command: ${id}`)
|
||||
@@ -51,6 +70,18 @@ window.__laputaTest = {
|
||||
window.__laputaTest.dispatchBrowserMenuCommand(id)
|
||||
return undefined
|
||||
},
|
||||
triggerShortcutCommand(id: string, options?: AppCommandShortcutEventOptions) {
|
||||
if (!isAppCommandId(id)) {
|
||||
throw new Error(`Unknown app command: ${id}`)
|
||||
}
|
||||
|
||||
const init = getShortcutEventInit(id, options)
|
||||
if (!init) {
|
||||
throw new Error(`Command ${id} does not define a keyboard shortcut`)
|
||||
}
|
||||
|
||||
dispatchDeterministicShortcutEvent(init)
|
||||
},
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
|
||||
@@ -120,7 +120,14 @@ export async function openFixtureVault(
|
||||
})
|
||||
}
|
||||
|
||||
export async function openFixtureVaultTauri(
|
||||
/**
|
||||
* Browser harness for desktop command-routing tests.
|
||||
*
|
||||
* This stubs the Tauri invoke bridge inside Playwright so tests can exercise
|
||||
* renderer shortcut dispatch and desktop menu-command dispatch without a native
|
||||
* shell. It is deterministic, but it is not a substitute for real native QA.
|
||||
*/
|
||||
export async function openFixtureVaultDesktopHarness(
|
||||
page: Page,
|
||||
vaultPath: string,
|
||||
): Promise<void> {
|
||||
@@ -388,3 +395,5 @@ export async function openFixtureVaultTauri(
|
||||
|
||||
await page.waitForFunction(() => Boolean(window.__TAURI_INTERNALS__))
|
||||
}
|
||||
|
||||
export const openFixtureVaultTauri = openFixtureVaultDesktopHarness
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { triggerMenuCommand } from './testBridge'
|
||||
import { createFixtureVaultCopy, openFixtureVaultTauri, removeFixtureVaultCopy } from '../helpers/fixtureVault'
|
||||
import { APP_COMMAND_IDS } from '../../src/hooks/appCommandCatalog'
|
||||
import {
|
||||
dispatchShortcutEvent,
|
||||
triggerMenuCommand,
|
||||
triggerShortcutCommand,
|
||||
} from './testBridge'
|
||||
import {
|
||||
createFixtureVaultCopy,
|
||||
openFixtureVaultDesktopHarness,
|
||||
removeFixtureVaultCopy,
|
||||
} from '../helpers/fixtureVault'
|
||||
|
||||
let tempVaultDir: string
|
||||
|
||||
@@ -13,72 +22,79 @@ test.describe('keyboard command routing', () => {
|
||||
removeFixtureVaultCopy(tempVaultDir)
|
||||
})
|
||||
|
||||
test('native menu trigger creates a note through the shared command path @smoke', async ({ page }) => {
|
||||
test('desktop menu-command bridge creates a note through the shared command path @smoke', async ({ page }) => {
|
||||
const errors: string[] = []
|
||||
page.on('pageerror', (error) => errors.push(error.message))
|
||||
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
await triggerMenuCommand(page, 'file-new-note')
|
||||
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 })
|
||||
expect(errors).toEqual([])
|
||||
})
|
||||
|
||||
test('Meta+Shift+I toggles the properties panel in Tauri mode through the shared keyboard path @smoke', async ({ page }) => {
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
test('desktop menu-command bridge toggles the properties panel through the shared command path @smoke', async ({ page }) => {
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir)
|
||||
await page.getByText('Alpha Project', { exact: true }).first().click()
|
||||
await page.locator('.bn-editor').click()
|
||||
|
||||
await page.keyboard.press('Meta+Shift+I')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleProperties)
|
||||
await expect(page.getByTitle('Close Properties (⌘⇧I)')).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await page.keyboard.press('Meta+Shift+I')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleProperties)
|
||||
await expect(page.getByTitle('Properties (⌘⇧I)')).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
test('native menu trigger toggles organized state through the shared command path @smoke', async ({ page }) => {
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
test('desktop menu-command bridge toggles organized state through the shared command path @smoke', async ({ page }) => {
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir)
|
||||
await page.getByText('Alpha Project', { exact: true }).first().click()
|
||||
await page.locator('.bn-editor').click()
|
||||
|
||||
await expect(page.getByTitle('Mark as organized (remove from Inbox) (Cmd+E)')).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
// Chromium reserves Meta+E for the browser chrome, so the exact keystroke
|
||||
// needs native-app QA. The smoke suite proves the shared command path here.
|
||||
await triggerMenuCommand(page, 'note-toggle-organized')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.noteToggleOrganized)
|
||||
await expect(page.getByTitle('Mark as unorganized (back to Inbox) (Cmd+E)')).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await triggerMenuCommand(page, 'note-toggle-organized')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.noteToggleOrganized)
|
||||
await expect(page.getByTitle('Mark as organized (remove from Inbox) (Cmd+E)')).toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
|
||||
test('Meta+Backslash toggles the raw editor in Tauri mode through the shared keyboard path @smoke', async ({ page }) => {
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
test('renderer shortcut bridge toggles the raw editor through the shared keyboard handler @smoke', async ({ page }) => {
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir)
|
||||
await page.getByText('Alpha Project', { exact: true }).first().click()
|
||||
await page.locator('.bn-editor').click()
|
||||
|
||||
await page.keyboard.press('Meta+Backslash')
|
||||
await triggerShortcutCommand(page, APP_COMMAND_IDS.editToggleRawEditor)
|
||||
await expect(page.getByTestId('raw-editor-codemirror')).toBeVisible({ timeout: 5_000 })
|
||||
|
||||
await page.keyboard.press('Meta+Backslash')
|
||||
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 })
|
||||
})
|
||||
|
||||
test('Meta+Shift+L toggles the AI panel in Tauri mode, while Ctrl+Shift+L does not @smoke', async ({ page }) => {
|
||||
await openFixtureVaultTauri(page, tempVaultDir)
|
||||
test('desktop menu-command bridge toggles the AI panel, while the wrong modifier event does not @smoke', async ({ page }) => {
|
||||
await openFixtureVaultDesktopHarness(page, tempVaultDir)
|
||||
await page.getByText('Alpha Project', { exact: true }).first().click()
|
||||
await page.locator('.bn-editor').click()
|
||||
|
||||
await page.keyboard.press('Control+Shift+L')
|
||||
await dispatchShortcutEvent(page, {
|
||||
key: 'l',
|
||||
code: 'KeyL',
|
||||
ctrlKey: true,
|
||||
metaKey: false,
|
||||
shiftKey: true,
|
||||
altKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
await page.waitForTimeout(200)
|
||||
await expect(page.getByTestId('ai-panel')).not.toBeVisible()
|
||||
|
||||
await page.keyboard.press('Meta+Shift+L')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleAiChat)
|
||||
await expect(page.getByTestId('ai-panel')).toBeVisible({ timeout: 5_000 })
|
||||
await expect(page.getByTitle('Close AI panel')).toBeVisible()
|
||||
|
||||
await page.keyboard.press('Meta+Shift+L')
|
||||
await triggerMenuCommand(page, APP_COMMAND_IDS.viewToggleAiChat)
|
||||
await expect(page.getByTestId('ai-panel')).not.toBeVisible({ timeout: 5_000 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type Page } from '@playwright/test'
|
||||
import type {
|
||||
AppCommandId,
|
||||
AppCommandShortcutEventInit,
|
||||
AppCommandShortcutEventOptions,
|
||||
} from '../../src/hooks/appCommandCatalog'
|
||||
|
||||
export async function triggerMenuCommand(page: Page, id: string): Promise<void> {
|
||||
await page.evaluate(async (commandId) => {
|
||||
@@ -32,3 +37,30 @@ export async function triggerMenuCommand(page: Page, id: string): Promise<void>
|
||||
throw new Error('Laputa test bridge is missing dispatchBrowserMenuCommand')
|
||||
}, id)
|
||||
}
|
||||
|
||||
export async function dispatchShortcutEvent(
|
||||
page: Page,
|
||||
init: AppCommandShortcutEventInit,
|
||||
): Promise<void> {
|
||||
await page.evaluate((eventInit) => {
|
||||
const bridge = window.__laputaTest?.dispatchShortcutEvent
|
||||
if (typeof bridge !== 'function') {
|
||||
throw new Error('Laputa test bridge is missing dispatchShortcutEvent')
|
||||
}
|
||||
bridge(eventInit)
|
||||
}, init)
|
||||
}
|
||||
|
||||
export async function triggerShortcutCommand(
|
||||
page: Page,
|
||||
id: AppCommandId,
|
||||
options?: AppCommandShortcutEventOptions,
|
||||
): Promise<void> {
|
||||
await page.evaluate((payload) => {
|
||||
const bridge = window.__laputaTest?.triggerShortcutCommand
|
||||
if (typeof bridge !== 'function') {
|
||||
throw new Error('Laputa test bridge is missing triggerShortcutCommand')
|
||||
}
|
||||
bridge(payload.id, payload.options)
|
||||
}, { id, options })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user