fix: stabilize smoke vault harness
This commit is contained in:
@@ -24,11 +24,18 @@ if (typeof window !== 'undefined') {
|
||||
window.__mockHandlers = mockHandlers
|
||||
}
|
||||
|
||||
function resolveMockHandler(command: string) {
|
||||
if (typeof window !== 'undefined' && window.__mockHandlers?.[command]) {
|
||||
return window.__mockHandlers[command]
|
||||
}
|
||||
return mockHandlers[command]
|
||||
}
|
||||
|
||||
export async function mockInvoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
|
||||
const vaultResult = await tryVaultApi<T>(cmd, args)
|
||||
if (vaultResult !== undefined) return vaultResult
|
||||
|
||||
const handler = mockHandlers[cmd]
|
||||
const handler = resolveMockHandler(cmd)
|
||||
if (handler) {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
return handler(args) as T
|
||||
|
||||
@@ -103,11 +103,17 @@ let mockSettings: Settings = {
|
||||
default_ai_agent: 'claude_code',
|
||||
}
|
||||
|
||||
let mockLastVaultPath: string | null = null
|
||||
const DEFAULT_MOCK_VAULT_PATH = '/Users/mock/demo-vault-v2'
|
||||
const DEFAULT_MOCK_VAULT = {
|
||||
label: 'demo-vault-v2',
|
||||
path: DEFAULT_MOCK_VAULT_PATH,
|
||||
}
|
||||
|
||||
let mockLastVaultPath: string | null = DEFAULT_MOCK_VAULT_PATH
|
||||
|
||||
let mockVaultList: { vaults: Array<{ label: string; path: string }>; active_vault: string | null } = {
|
||||
vaults: [],
|
||||
active_vault: null,
|
||||
vaults: [DEFAULT_MOCK_VAULT],
|
||||
active_vault: DEFAULT_MOCK_VAULT_PATH,
|
||||
}
|
||||
|
||||
let mockVaultAiGuidanceStatus = {
|
||||
|
||||
61
src/mock-tauri/vault-api.test.ts
Normal file
61
src/mock-tauri/vault-api.test.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const originalFetch = globalThis.fetch
|
||||
|
||||
function jsonResponse(body: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
describe('tryVaultApi', () => {
|
||||
afterEach(() => {
|
||||
vi.resetModules()
|
||||
vi.restoreAllMocks()
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('retries vault API discovery after an unavailable response', async () => {
|
||||
let vaultApiOnline = false
|
||||
const fetchMock = vi.fn(async (input: string | URL) => {
|
||||
const url = String(input)
|
||||
if (url === '/api/vault/ping') {
|
||||
return jsonResponse({ ok: vaultApiOnline }, vaultApiOnline ? 200 : 503)
|
||||
}
|
||||
if (url === '/api/vault/list?path=%2Ffixture') {
|
||||
return jsonResponse([{ title: 'Alpha Project' }])
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`)
|
||||
})
|
||||
globalThis.fetch = fetchMock as typeof fetch
|
||||
|
||||
const { tryVaultApi } = await import('./vault-api')
|
||||
|
||||
await expect(tryVaultApi('list_vault', { path: '/fixture' })).resolves.toBeUndefined()
|
||||
|
||||
vaultApiOnline = true
|
||||
|
||||
await expect(tryVaultApi('list_vault', { path: '/fixture' })).resolves.toEqual([{ title: 'Alpha Project' }])
|
||||
expect(fetchMock.mock.calls.filter(([url]) => String(url) === '/api/vault/ping')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('unwraps note content responses from the vault API', async () => {
|
||||
const fetchMock = vi.fn(async (input: string | URL) => {
|
||||
const url = String(input)
|
||||
if (url === '/api/vault/ping') {
|
||||
return jsonResponse({ ok: true })
|
||||
}
|
||||
if (url === '/api/vault/content?path=%2Ffixture%2Falpha.md') {
|
||||
return jsonResponse({ content: '# Alpha Project' })
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`)
|
||||
})
|
||||
globalThis.fetch = fetchMock as typeof fetch
|
||||
|
||||
const { tryVaultApi } = await import('./vault-api')
|
||||
|
||||
await expect(tryVaultApi('get_note_content', { path: '/fixture/alpha.md' })).resolves.toBe('# Alpha Project')
|
||||
expect(fetchMock.mock.calls.filter(([url]) => String(url) === '/api/vault/ping')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -6,16 +6,22 @@
|
||||
|
||||
let vaultApiAvailable: boolean | null = null
|
||||
|
||||
async function checkVaultApi(): Promise<boolean> {
|
||||
if (vaultApiAvailable !== null) return vaultApiAvailable
|
||||
async function detectVaultApiAvailability(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch('/api/vault/ping', { signal: AbortSignal.timeout(500) })
|
||||
vaultApiAvailable = res.ok
|
||||
return res.ok
|
||||
} catch {
|
||||
vaultApiAvailable = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkVaultApi(): Promise<boolean> {
|
||||
if (vaultApiAvailable === true) return true
|
||||
|
||||
const available = await detectVaultApiAvailability()
|
||||
vaultApiAvailable = available
|
||||
console.info(`[mock-tauri] Vault API available: ${vaultApiAvailable}`)
|
||||
return vaultApiAvailable
|
||||
return available
|
||||
}
|
||||
|
||||
interface VaultApiRequest {
|
||||
@@ -65,25 +71,38 @@ const VAULT_API_COMMANDS: Record<string, (args: Record<string, unknown>) => Vaul
|
||||
},
|
||||
}
|
||||
|
||||
export async function tryVaultApi<T>(cmd: string, args?: Record<string, unknown>): Promise<T | undefined> {
|
||||
const available = await checkVaultApi()
|
||||
if (!available) return undefined
|
||||
|
||||
function buildVaultApiRequest(cmd: string, args?: Record<string, unknown>) {
|
||||
if (!args) return null
|
||||
const requestBuilder = VAULT_API_COMMANDS[cmd]
|
||||
if (!requestBuilder || !args) return undefined
|
||||
return requestBuilder?.(args) ?? null
|
||||
}
|
||||
|
||||
const request = requestBuilder(args)
|
||||
function buildFetchOptions(request: VaultApiRequest): RequestInit {
|
||||
if (!request.body) {
|
||||
return { method: request.method || 'GET' }
|
||||
}
|
||||
|
||||
return {
|
||||
method: request.method || 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(request.body),
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchVaultApiResponse(request: VaultApiRequest) {
|
||||
const res = await fetch(request.url, buildFetchOptions(request))
|
||||
if (!res.ok) return undefined
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function tryVaultApi<T>(cmd: string, args?: Record<string, unknown>): Promise<T | undefined> {
|
||||
const request = buildVaultApiRequest(cmd, args)
|
||||
if (!request) return undefined
|
||||
if (!await checkVaultApi()) return undefined
|
||||
|
||||
try {
|
||||
const fetchOpts: RequestInit = { method: request.method || 'GET' }
|
||||
if (request.body) {
|
||||
fetchOpts.headers = { 'Content-Type': 'application/json' }
|
||||
fetchOpts.body = JSON.stringify(request.body)
|
||||
}
|
||||
const res = await fetch(request.url, fetchOpts)
|
||||
if (!res.ok) return undefined
|
||||
const data = await res.json()
|
||||
const data = await fetchVaultApiResponse(request)
|
||||
if (data === undefined) return undefined
|
||||
return (cmd === 'get_note_content' ? data.content : data) as T
|
||||
} catch (err) {
|
||||
console.warn(`[mock-tauri] Vault API call failed for ${cmd}, falling back to mock:`, err)
|
||||
|
||||
@@ -2,20 +2,40 @@ import { expect, type Page } from '@playwright/test'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { installFixtureVaultDesktopBridgeInBrowser } from './fixtureVaultDesktopBridge'
|
||||
|
||||
const FIXTURE_VAULT = path.resolve('tests/fixtures/test-vault')
|
||||
const FIXTURE_VAULT_READY_TIMEOUT = 30_000
|
||||
const FIXTURE_VAULT_REMOVE_RETRIES = 10
|
||||
const FIXTURE_VAULT_REMOVE_RETRY_DELAY_MS = 100
|
||||
const CLAUDE_CODE_ONBOARDING_DISMISSED_KEY = 'tolaria:claude-code-onboarding-dismissed'
|
||||
type FixtureCommandArgs = Record<string, unknown> | undefined
|
||||
|
||||
function copyDirSync(src: string, dest: string): void {
|
||||
interface FixtureVaultPageArgs {
|
||||
page: Page
|
||||
vaultPath: string
|
||||
}
|
||||
|
||||
interface FixturePageArgs {
|
||||
page: Page
|
||||
}
|
||||
|
||||
interface CopyDirArgs {
|
||||
src: string
|
||||
dest: string
|
||||
}
|
||||
|
||||
interface RemoveFixtureVaultArgs {
|
||||
tempVaultDir: string
|
||||
}
|
||||
|
||||
function copyDirSync({ src, dest }: CopyDirArgs): void {
|
||||
fs.mkdirSync(dest, { recursive: true })
|
||||
for (const item of fs.readdirSync(src, { withFileTypes: true })) {
|
||||
const sourcePath = path.join(src, item.name)
|
||||
const destinationPath = path.join(dest, item.name)
|
||||
if (item.isDirectory()) {
|
||||
copyDirSync(sourcePath, destinationPath)
|
||||
copyDirSync({ src: sourcePath, dest: destinationPath })
|
||||
continue
|
||||
}
|
||||
fs.copyFileSync(sourcePath, destinationPath)
|
||||
@@ -24,12 +44,11 @@ function copyDirSync(src: string, dest: string): void {
|
||||
|
||||
export function createFixtureVaultCopy(): string {
|
||||
const tempVaultDir = fs.mkdtempSync(path.join(os.tmpdir(), 'laputa-test-vault-'))
|
||||
copyDirSync(FIXTURE_VAULT, tempVaultDir)
|
||||
copyDirSync({ src: FIXTURE_VAULT, dest: tempVaultDir })
|
||||
return tempVaultDir
|
||||
}
|
||||
|
||||
export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined): void {
|
||||
if (!tempVaultDir) return
|
||||
function removeFixtureVaultDirectory({ tempVaultDir }: RemoveFixtureVaultArgs): void {
|
||||
fs.rmSync(tempVaultDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
@@ -38,15 +57,21 @@ export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined):
|
||||
})
|
||||
}
|
||||
|
||||
export async function openFixtureVault(
|
||||
page: Page,
|
||||
vaultPath: string,
|
||||
): Promise<void> {
|
||||
export function removeFixtureVaultCopy(tempVaultDir: string | null | undefined): void {
|
||||
if (!tempVaultDir) return
|
||||
removeFixtureVaultDirectory({ tempVaultDir })
|
||||
}
|
||||
|
||||
async function installFixtureVaultInitScript({ page, vaultPath }: FixtureVaultPageArgs): Promise<void> {
|
||||
await page.addInitScript(({ dismissedKey, resolvedVaultPath }: { dismissedKey: string; resolvedVaultPath: string }) => {
|
||||
localStorage.clear()
|
||||
localStorage.setItem(dismissedKey, '1')
|
||||
|
||||
const jsonHeaders = { 'Content-Type': 'application/json' }
|
||||
const FRONTMATTER_OPEN = '---\n'
|
||||
const FRONTMATTER_CLOSE = '\n---\n'
|
||||
const nativeFetch = window.fetch.bind(window)
|
||||
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const requestUrl = typeof input === 'string'
|
||||
? input
|
||||
@@ -64,85 +89,21 @@ export async function openFixtureVault(
|
||||
return nativeFetch(input, init)
|
||||
}
|
||||
|
||||
const applyFixtureVaultOverrides = (
|
||||
handlers: Record<string, ((args?: unknown) => unknown)> | null | undefined,
|
||||
) => {
|
||||
if (!handlers) return handlers
|
||||
handlers.load_vault_list = () => ({
|
||||
vaults: [{ label: 'Test Vault', path: resolvedVaultPath }],
|
||||
active_vault: resolvedVaultPath,
|
||||
hidden_defaults: [],
|
||||
})
|
||||
handlers.check_vault_exists = () => true
|
||||
handlers.get_last_vault_path = () => resolvedVaultPath
|
||||
handlers.get_default_vault_path = () => resolvedVaultPath
|
||||
handlers.save_vault_list = () => null
|
||||
return handlers
|
||||
const readJson = async (url: string, init?: RequestInit) => {
|
||||
const response = await nativeFetch(url, init)
|
||||
if (!response.ok) {
|
||||
let message = `HTTP ${response.status}`
|
||||
try {
|
||||
const body = await response.json() as { error?: string }
|
||||
message = body.error ?? message
|
||||
} catch {
|
||||
// Preserve the HTTP status fallback when the body is not JSON.
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
let ref = applyFixtureVaultOverrides(
|
||||
(window.__mockHandlers as Record<string, ((args?: unknown) => unknown)> | undefined),
|
||||
) ?? null
|
||||
|
||||
Object.defineProperty(window, '__mockHandlers', {
|
||||
configurable: true,
|
||||
set(value) {
|
||||
ref = applyFixtureVaultOverrides(
|
||||
value as Record<string, ((args?: unknown) => unknown)> | undefined,
|
||||
) ?? null
|
||||
},
|
||||
get() {
|
||||
return applyFixtureVaultOverrides(ref) ?? ref
|
||||
},
|
||||
})
|
||||
}, { dismissedKey: CLAUDE_CODE_ONBOARDING_DISMISSED_KEY, resolvedVaultPath: vaultPath })
|
||||
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForFunction(() => Boolean(window.__mockHandlers))
|
||||
await page.evaluate((resolvedVaultPath: string) => {
|
||||
const handlers = window.__mockHandlers
|
||||
if (!handlers) {
|
||||
throw new Error('Mock handlers unavailable for fixture vault override')
|
||||
}
|
||||
|
||||
handlers.load_vault_list = () => ({
|
||||
vaults: [{ label: 'Test Vault', path: resolvedVaultPath }],
|
||||
active_vault: resolvedVaultPath,
|
||||
hidden_defaults: [],
|
||||
})
|
||||
handlers.check_vault_exists = () => true
|
||||
handlers.get_last_vault_path = () => resolvedVaultPath
|
||||
handlers.get_default_vault_path = () => resolvedVaultPath
|
||||
handlers.save_vault_list = () => null
|
||||
}, vaultPath)
|
||||
await page.reload({ waitUntil: 'domcontentloaded' })
|
||||
await page.locator('[data-testid="note-list-container"]').waitFor({ timeout: FIXTURE_VAULT_READY_TIMEOUT })
|
||||
await expect(page.getByText('Alpha Project', { exact: true }).first()).toBeVisible({
|
||||
timeout: FIXTURE_VAULT_READY_TIMEOUT,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
await openFixtureVault(page, vaultPath)
|
||||
await page.evaluate((resolvedVaultPath: string) => {
|
||||
const jsonHeaders = { 'Content-Type': 'application/json' }
|
||||
const nativeFetch = window.fetch.bind(window)
|
||||
|
||||
const FRONTMATTER_OPEN = '---\n'
|
||||
const FRONTMATTER_CLOSE = '\n---\n'
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
const splitFrontmatter = (content: string) => {
|
||||
if (!content.startsWith(FRONTMATTER_OPEN)) {
|
||||
return { frontmatter: null as string | null, body: content }
|
||||
@@ -206,16 +167,12 @@ export async function openFixtureVaultDesktopHarness(
|
||||
return `${FRONTMATTER_OPEN}${nextEntryLines.join('\n')}${FRONTMATTER_CLOSE}${body}`
|
||||
}
|
||||
|
||||
const entries = splitFrontmatterEntries(frontmatter).filter((entry) => entry.key !== '')
|
||||
const keyPattern = new RegExp(`^${escapeRegExp(key)}$`)
|
||||
let replaced = false
|
||||
const nextEntries = entries.map((entry) => {
|
||||
if (!keyPattern.test(entry.key)) return entry
|
||||
replaced = true
|
||||
return { key, lines: nextEntryLines }
|
||||
})
|
||||
const nextEntries = splitFrontmatterEntries(frontmatter)
|
||||
.filter((entry) => entry.key !== '')
|
||||
.map((entry) => (entry.key === key ? { key, lines: nextEntryLines } : entry))
|
||||
|
||||
if (!replaced) {
|
||||
const hasEntry = nextEntries.some((entry) => entry.key === key)
|
||||
if (!hasEntry) {
|
||||
nextEntries.push({ key, lines: nextEntryLines })
|
||||
}
|
||||
|
||||
@@ -226,9 +183,8 @@ export async function openFixtureVaultDesktopHarness(
|
||||
const { frontmatter, body } = splitFrontmatter(content)
|
||||
if (frontmatter === null) return content
|
||||
|
||||
const keyPattern = new RegExp(`^${escapeRegExp(key)}$`)
|
||||
const nextEntries = splitFrontmatterEntries(frontmatter)
|
||||
.filter((entry) => entry.key !== '' && !keyPattern.test(entry.key))
|
||||
.filter((entry) => entry.key !== '' && entry.key !== key)
|
||||
|
||||
if (nextEntries.length === 0) {
|
||||
return body
|
||||
@@ -237,32 +193,19 @@ export async function openFixtureVaultDesktopHarness(
|
||||
return `${FRONTMATTER_OPEN}${nextEntries.flatMap((entry) => entry.lines).join('\n')}${FRONTMATTER_CLOSE}${body}`
|
||||
}
|
||||
|
||||
const persistFrontmatterChange = async (path: string, transform: (content: string) => string) => {
|
||||
const current = await readJson(`/api/vault/content?path=${encodeURIComponent(path)}`) as { content: string }
|
||||
const persistFrontmatterChange = async (notePath: string, transform: (content: string) => string) => {
|
||||
const current = await readJson(
|
||||
`/api/vault/content?path=${encodeURIComponent(notePath)}`,
|
||||
) as { content: string }
|
||||
const updatedContent = transform(current.content)
|
||||
await readJson('/api/vault/save', {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({ path, content: updatedContent }),
|
||||
body: JSON.stringify({ path: notePath, content: updatedContent }),
|
||||
})
|
||||
return updatedContent
|
||||
}
|
||||
|
||||
const readJson = async (url: string, init?: RequestInit) => {
|
||||
const response = await nativeFetch(url, init)
|
||||
if (!response.ok) {
|
||||
let message = `HTTP ${response.status}`
|
||||
try {
|
||||
const body = await response.json() as { error?: string }
|
||||
message = body.error ?? message
|
||||
} catch {
|
||||
// Keep the HTTP status fallback when the body is not JSON.
|
||||
}
|
||||
throw new Error(message)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const activeVaultList = {
|
||||
vaults: [{ label: 'Test Vault', path: resolvedVaultPath }],
|
||||
active_vault: resolvedVaultPath,
|
||||
@@ -283,16 +226,16 @@ export async function openFixtureVaultDesktopHarness(
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
const commandHandlers: Record<string, (commandArgs?: Record<string, unknown>) => Promise<unknown> | unknown> = {
|
||||
trigger_menu_command: (commandArgs) => {
|
||||
const commandId = String(commandArgs?.id ?? '')
|
||||
const bridge = window.__laputaTest?.dispatchBrowserMenuCommand
|
||||
if (!bridge) throw new Error('Tolaria test bridge is missing dispatchBrowserMenuCommand')
|
||||
bridge(commandId)
|
||||
return null
|
||||
},
|
||||
const readCommandValue = (commandArgs: FixtureCommandArgs, key: string, fallback?: unknown) =>
|
||||
commandArgs?.[key] ?? fallback
|
||||
|
||||
const readCommandString = (commandArgs: FixtureCommandArgs, key: string, fallback = '') =>
|
||||
String(readCommandValue(commandArgs, key, fallback))
|
||||
|
||||
const buildFixtureStateHandlers = () => ({
|
||||
load_vault_list: () => activeVaultList,
|
||||
check_vault_exists: () => true,
|
||||
check_vault_exists: (commandArgs?: FixtureCommandArgs) =>
|
||||
readCommandString(commandArgs, 'path') === resolvedVaultPath,
|
||||
is_git_repo: () => true,
|
||||
get_last_vault_path: () => resolvedVaultPath,
|
||||
get_default_vault_path: () => resolvedVaultPath,
|
||||
@@ -309,97 +252,164 @@ export async function openFixtureVaultDesktopHarness(
|
||||
anonymous_id: null,
|
||||
release_channel: null,
|
||||
}),
|
||||
list_vault: (commandArgs) => readVaultList(commandArgs),
|
||||
reload_vault: (commandArgs) => readVaultList(commandArgs, true),
|
||||
})
|
||||
|
||||
const buildFixtureReadHandlers = () => ({
|
||||
list_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs),
|
||||
reload_vault: (commandArgs?: FixtureCommandArgs) => readVaultList(commandArgs, true),
|
||||
list_vault_folders: () => [],
|
||||
list_views: () => [],
|
||||
get_modified_files: () => [],
|
||||
detect_renames: () => [],
|
||||
reload_vault_entry: (commandArgs) =>
|
||||
readJson(`/api/vault/entry?path=${encodeURIComponent(String(commandArgs?.path ?? ''))}`),
|
||||
get_note_content: async (commandArgs) => {
|
||||
reload_vault_entry: (commandArgs?: FixtureCommandArgs) =>
|
||||
readJson(`/api/vault/entry?path=${encodeURIComponent(readCommandString(commandArgs, 'path'))}`),
|
||||
get_note_content: async (commandArgs?: FixtureCommandArgs) => {
|
||||
const data = await readJson(
|
||||
`/api/vault/content?path=${encodeURIComponent(String(commandArgs?.path ?? ''))}`,
|
||||
`/api/vault/content?path=${encodeURIComponent(readCommandString(commandArgs, 'path'))}`,
|
||||
) as { content: string }
|
||||
return data.content
|
||||
},
|
||||
get_all_content: (commandArgs) =>
|
||||
get_all_content: (commandArgs?: FixtureCommandArgs) =>
|
||||
readJson(
|
||||
`/api/vault/all-content?path=${encodeURIComponent(String(commandArgs?.path ?? resolvedVaultPath))}`,
|
||||
`/api/vault/all-content?path=${encodeURIComponent(readCommandString(commandArgs, 'path', resolvedVaultPath))}`,
|
||||
),
|
||||
save_note_content: (commandArgs) =>
|
||||
readJson('/api/vault/save', {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({ path: commandArgs?.path, content: commandArgs?.content }),
|
||||
}),
|
||||
update_frontmatter: (commandArgs) =>
|
||||
persistFrontmatterChange(
|
||||
String(commandArgs?.path ?? ''),
|
||||
(content) => replaceFrontmatterEntry(content, String(commandArgs?.key ?? ''), commandArgs?.value),
|
||||
),
|
||||
delete_frontmatter_property: (commandArgs) =>
|
||||
persistFrontmatterChange(
|
||||
String(commandArgs?.path ?? ''),
|
||||
(content) => removeFrontmatterEntry(content, String(commandArgs?.key ?? '')),
|
||||
),
|
||||
rename_note: (commandArgs) =>
|
||||
renameNoteRequest({
|
||||
vault_path: commandArgs?.vaultPath ?? resolvedVaultPath,
|
||||
old_path: commandArgs?.oldPath,
|
||||
new_title: commandArgs?.newTitle,
|
||||
old_title: commandArgs?.oldTitle ?? null,
|
||||
}),
|
||||
rename_note_filename: (commandArgs) =>
|
||||
readJson('/api/vault/rename-filename', {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({
|
||||
vault_path: commandArgs?.vaultPath ?? resolvedVaultPath,
|
||||
old_path: commandArgs?.oldPath,
|
||||
new_filename_stem: commandArgs?.newFilenameStem,
|
||||
}),
|
||||
}),
|
||||
search_vault: (commandArgs) => {
|
||||
const resolvedPath = String(commandArgs?.path ?? commandArgs?.vaultPath ?? resolvedVaultPath)
|
||||
const query = encodeURIComponent(String(commandArgs?.query ?? ''))
|
||||
const mode = encodeURIComponent(String(commandArgs?.mode ?? 'all'))
|
||||
search_vault: (commandArgs?: FixtureCommandArgs) => {
|
||||
const resolvedPath = readCommandString(
|
||||
commandArgs,
|
||||
'path',
|
||||
readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
|
||||
)
|
||||
const query = encodeURIComponent(readCommandString(commandArgs, 'query'))
|
||||
const mode = encodeURIComponent(readCommandString(commandArgs, 'mode', 'all'))
|
||||
return readJson(
|
||||
`/api/vault/search?vault_path=${encodeURIComponent(resolvedPath)}&query=${query}&mode=${mode}`,
|
||||
)
|
||||
},
|
||||
auto_rename_untitled: async (commandArgs) => {
|
||||
const notePath = String(commandArgs?.notePath ?? '')
|
||||
})
|
||||
|
||||
const buildFixtureWriteHandlers = () => ({
|
||||
save_note_content: (commandArgs?: FixtureCommandArgs) =>
|
||||
readJson('/api/vault/save', {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({
|
||||
path: readCommandValue(commandArgs, 'path'),
|
||||
content: readCommandValue(commandArgs, 'content'),
|
||||
}),
|
||||
}),
|
||||
update_frontmatter: (commandArgs?: FixtureCommandArgs) =>
|
||||
persistFrontmatterChange(
|
||||
readCommandString(commandArgs, 'path'),
|
||||
(content) => replaceFrontmatterEntry(
|
||||
content,
|
||||
readCommandString(commandArgs, 'key'),
|
||||
readCommandValue(commandArgs, 'value'),
|
||||
),
|
||||
),
|
||||
delete_frontmatter_property: (commandArgs?: FixtureCommandArgs) =>
|
||||
persistFrontmatterChange(
|
||||
readCommandString(commandArgs, 'path'),
|
||||
(content) => removeFrontmatterEntry(content, readCommandString(commandArgs, 'key')),
|
||||
),
|
||||
rename_note: (commandArgs?: FixtureCommandArgs) =>
|
||||
renameNoteRequest({
|
||||
vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
|
||||
old_path: readCommandValue(commandArgs, 'oldPath'),
|
||||
new_title: readCommandValue(commandArgs, 'newTitle'),
|
||||
old_title: readCommandValue(commandArgs, 'oldTitle', null),
|
||||
}),
|
||||
rename_note_filename: (commandArgs?: FixtureCommandArgs) =>
|
||||
readJson('/api/vault/rename-filename', {
|
||||
method: 'POST',
|
||||
headers: jsonHeaders,
|
||||
body: JSON.stringify({
|
||||
vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
|
||||
old_path: readCommandValue(commandArgs, 'oldPath'),
|
||||
new_filename_stem: readCommandValue(commandArgs, 'newFilenameStem'),
|
||||
}),
|
||||
}),
|
||||
auto_rename_untitled: async (commandArgs?: FixtureCommandArgs) => {
|
||||
const notePath = readCommandString(commandArgs, 'notePath')
|
||||
const contentData = await readJson(
|
||||
`/api/vault/content?path=${encodeURIComponent(notePath)}`,
|
||||
) as { content: string }
|
||||
const match = contentData.content.match(/^#\s+(.+)$/m)
|
||||
if (!match) return null
|
||||
return renameNoteRequest({
|
||||
vault_path: commandArgs?.vaultPath ?? resolvedVaultPath,
|
||||
vault_path: readCommandValue(commandArgs, 'vaultPath', resolvedVaultPath),
|
||||
old_path: notePath,
|
||||
new_title: match[1].trim(),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const applyFixtureVaultOverrides = (
|
||||
handlers: Record<string, ((args?: unknown) => unknown)> | null | undefined,
|
||||
) => {
|
||||
if (!handlers) return handlers
|
||||
Object.assign(
|
||||
handlers,
|
||||
buildFixtureStateHandlers(),
|
||||
buildFixtureReadHandlers(),
|
||||
buildFixtureWriteHandlers(),
|
||||
)
|
||||
return handlers
|
||||
}
|
||||
|
||||
const invoke = async (command: string, args?: Record<string, unknown>) => {
|
||||
const handler = commandHandlers[command] ?? window.__mockHandlers?.[command]
|
||||
if (!handler) throw new Error(`Unhandled invoke: ${command}`)
|
||||
return handler(args)
|
||||
}
|
||||
let ref = applyFixtureVaultOverrides(
|
||||
(window.__mockHandlers as Record<string, ((args?: unknown) => unknown)> | undefined),
|
||||
) ?? null
|
||||
|
||||
Object.defineProperty(window, '__TAURI__', {
|
||||
Object.defineProperty(window, '__mockHandlers', {
|
||||
configurable: true,
|
||||
value: {},
|
||||
set(value) {
|
||||
ref = applyFixtureVaultOverrides(
|
||||
value as Record<string, ((args?: unknown) => unknown)> | undefined,
|
||||
) ?? null
|
||||
},
|
||||
get() {
|
||||
return applyFixtureVaultOverrides(ref) ?? ref
|
||||
},
|
||||
})
|
||||
Object.defineProperty(window, '__TAURI_INTERNALS__', {
|
||||
configurable: true,
|
||||
value: { invoke },
|
||||
})
|
||||
}, vaultPath)
|
||||
}, { dismissedKey: CLAUDE_CODE_ONBOARDING_DISMISSED_KEY, resolvedVaultPath: vaultPath })
|
||||
}
|
||||
|
||||
async function waitForFixtureVaultReady({ page }: FixturePageArgs): Promise<void> {
|
||||
await page.goto('/', { waitUntil: 'domcontentloaded' })
|
||||
await page.waitForFunction(() => Boolean(window.__mockHandlers?.list_vault))
|
||||
await page.locator('[data-testid="note-list-container"]').waitFor({ timeout: FIXTURE_VAULT_READY_TIMEOUT })
|
||||
await expect(page.getByText('Alpha Project', { exact: true }).first()).toBeVisible({
|
||||
timeout: FIXTURE_VAULT_READY_TIMEOUT,
|
||||
})
|
||||
}
|
||||
|
||||
export async function openFixtureVault(
|
||||
page: Page,
|
||||
vaultPath: string,
|
||||
): Promise<void> {
|
||||
await installFixtureVaultInitScript({ page, vaultPath })
|
||||
await waitForFixtureVaultReady({ page })
|
||||
}
|
||||
|
||||
async function installFixtureVaultDesktopBridge({ page }: FixturePageArgs): Promise<void> {
|
||||
await page.evaluate(installFixtureVaultDesktopBridgeInBrowser)
|
||||
|
||||
await page.waitForFunction(() => Boolean(window.__TAURI_INTERNALS__))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
await openFixtureVault(page, vaultPath)
|
||||
await installFixtureVaultDesktopBridge({ page })
|
||||
}
|
||||
|
||||
export const openFixtureVaultTauri = openFixtureVaultDesktopHarness
|
||||
|
||||
27
tests/helpers/fixtureVaultDesktopBridge.ts
Normal file
27
tests/helpers/fixtureVaultDesktopBridge.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export function installFixtureVaultDesktopBridgeInBrowser(): void {
|
||||
const dispatchBrowserMenuCommand =
|
||||
window.__laputaTest?.dispatchBrowserMenuCommand
|
||||
?? (() => { throw new Error('Tolaria test bridge is missing dispatchBrowserMenuCommand') })
|
||||
|
||||
const specialHandlers: Record<string, (args?: Record<string, unknown>) => unknown> = {
|
||||
trigger_menu_command: (args) => {
|
||||
dispatchBrowserMenuCommand(String(args?.id ?? ''))
|
||||
return null
|
||||
},
|
||||
}
|
||||
|
||||
const invoke = (command: string, args?: Record<string, unknown>) => {
|
||||
const handler = specialHandlers[command] ?? window.__mockHandlers?.[command]
|
||||
if (!handler) throw new Error(`Unhandled invoke: ${command}`)
|
||||
return handler(args)
|
||||
}
|
||||
|
||||
Object.defineProperty(window, '__TAURI__', {
|
||||
configurable: true,
|
||||
value: {},
|
||||
})
|
||||
Object.defineProperty(window, '__TAURI_INTERNALS__', {
|
||||
configurable: true,
|
||||
value: { invoke },
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user