test(app): cover remaining Tauri integrations

This commit is contained in:
Danila Poyarkov 2026-05-05 22:45:14 +03:00
parent 057b3a02b7
commit 3bd01095eb
6 changed files with 153 additions and 11 deletions

View file

@ -1,6 +1,6 @@
import { openFileFromPath } from '@/app/shell/menu/use'
import { isTauri } from '@/app/tauri/env'
import { createTab, openFileInNewTab } from '@/app/tabs'
import { IS_TAURI } from '@open-pencil/core/constants'
import type { EditorStore } from '@/app/editor/active-store'
@ -9,16 +9,19 @@ export async function handleSaveFile(store: EditorStore): Promise<unknown> {
return { ok: true }
}
export async function ensureTauriParentDirectory(path: string): Promise<void> {
if (!isTauri()) return
const { mkdir } = await import('@tauri-apps/plugin-fs')
const dir = path.replace(/[\\/][^\\/]+$/, '')
await mkdir(dir, { recursive: true })
}
export async function handleNewDocument(_store: EditorStore, args: unknown): Promise<unknown> {
const path = (args as { path?: string }).path
const tab = createTab()
if (path) {
tab.store.setPlannedFilePath(path)
if (IS_TAURI) {
const { mkdir } = await import('@tauri-apps/plugin-fs')
const dir = path.replace(/[\\/][^\\/]+$/, '')
await mkdir(dir, { recursive: true })
}
await ensureTauriParentDirectory(path)
await tab.store.saveFigFile()
tab.store.startWatchingCurrentFile()
}
@ -28,7 +31,7 @@ export async function handleNewDocument(_store: EditorStore, args: unknown): Pro
export async function handleOpenFile(_store: EditorStore, args: unknown): Promise<unknown> {
const path = (args as { path?: string }).path
if (!path) throw new Error('Missing "path" in args')
if (IS_TAURI) {
if (isTauri()) {
await openFileFromPath(path)
} else {
const response = await fetch(path)

View file

@ -11,9 +11,9 @@ fileDialog.onChange((files) => {
if (file) void openFileInNewTab(file)
})
if (IS_BROWSER) {
if (IS_BROWSER && 'window' in globalThis) {
;(
window as Window & { __OPEN_PENCIL_OPEN_FILE__?: (path: string) => Promise<void> }
globalThis.window as Window & { __OPEN_PENCIL_OPEN_FILE__?: (path: string) => Promise<void> }
).__OPEN_PENCIL_OPEN_FILE__ = async (path: string) => {
const response = await fetch(path)
const blob = await response.blob()

View file

@ -30,7 +30,7 @@ function cssColorToRgba(value: string) {
}
function readRulerTheme(): RulerTheme | null {
if (!IS_BROWSER) return null
if (!IS_BROWSER || !('document' in globalThis)) return null
const style = getComputedStyle(document.documentElement)
return {
background: cssColorToRgba(style.getPropertyValue('--color-ruler-bg')),
@ -53,7 +53,7 @@ function updateCanvasTheme(): void {
}
function applyTheme(value: 'dark' | 'light', setting: AppTheme): void {
if (!IS_BROWSER) return
if (!IS_BROWSER || !('document' in globalThis)) return
document.documentElement.dataset.theme = value
document.documentElement.dataset.themeSetting = setting
document.documentElement.style.colorScheme = value

View file

@ -0,0 +1,17 @@
import { describe, expect, test } from 'bun:test'
describe('Tauri fig export', () => {
test('delegates fig archive construction to the Tauri Rust command', async () => {
const proc = Bun.spawn(['bun', 'tests/helpers/tauri-fig-export-fixture.ts'], {
stdout: 'pipe',
stderr: 'pipe'
})
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited
])
expect({ exitCode, stdout, stderr }).toEqual({ exitCode: 0, stdout: '', stderr: '' })
})
})

View file

@ -0,0 +1,89 @@
import { afterEach, describe, expect, test, vi } from 'bun:test'
import { createACPTransport } from '@/app/ai/chat/transports'
import { ensureTauriParentDirectory } from '@/app/automation/bridge/file-handlers'
import { spawnMCPIfNeeded } from '@/app/automation/mcp/spawn'
import { clearTauriMocks, installTauriMockWindow, mockTauriIPC } from '../helpers/tauri-mocks'
import type { ACPChatTransport } from '@/app/ai/acp/transport'
afterEach(async () => {
await clearTauriMocks()
vi.restoreAllMocks()
// @ts-expect-error test cleanup
delete globalThis.window
// @ts-expect-error test cleanup
delete globalThis.navigator
// @ts-expect-error test cleanup
delete globalThis.location
})
describe('remaining Tauri integrations', () => {
test('uses Tauri home directory for ACP transport cwd', async () => {
await mockTauriIPC((cmd, args) => {
expect(cmd).toBe('plugin:path|resolve_directory')
expect(args).toEqual({ directory: 21 })
return '/Users/tester'
})
const transport = (await createACPTransport('acp:claude-code')) as ACPChatTransport & { cwd: string }
expect(transport.cwd).toBe('/Users/tester')
})
test('creates parent directory for automation new-document paths in Tauri', async () => {
await mockTauriIPC((cmd, args) => {
expect(cmd).toBe('plugin:fs|mkdir')
expect(args).toEqual({ path: '/tmp/open-pencil/nested', options: { recursive: true } })
return null
})
await ensureTauriParentDirectory('/tmp/open-pencil/nested/file.fig')
})
test('spawns MCP server with shell plugin when health check is missing', async () => {
installTauriMockWindow()
Object.assign(globalThis.window, { location: { origin: 'tauri://localhost' } })
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: { platform: 'MacIntel' }
})
let healthChecks = 0
vi.spyOn(globalThis, 'fetch').mockImplementation(async () => {
healthChecks += 1
if (healthChecks === 1) return new Response('', { status: 404 })
return new Response(JSON.stringify({ status: 'ok', token: 'server-token' }), { status: 200 })
})
let onEvent: ((event: unknown) => void) | null = null
const calls: Array<{ cmd: string; args: unknown }> = []
await mockTauriIPC((cmd, args) => {
calls.push({ cmd, args })
if (cmd === 'plugin:shell|spawn') {
onEvent = (args as { onEvent: { onmessage: (event: unknown) => void } }).onEvent.onmessage
return 77
}
return null
})
const handle = await spawnMCPIfNeeded()
onEvent?.({ event: 'Stderr', payload: [119, 97, 114, 110] })
handle?.disconnect()
await Promise.resolve()
expect(handle?.authToken).toBe('server-token')
expect(calls[0]?.cmd).toBe('plugin:shell|spawn')
expect(calls[0]?.args).toMatchObject({
program: 'openpencil-mcp-http',
args: [],
options: {
env: {
OPENPENCIL_MCP_AUTH_TOKEN: expect.any(String),
OPENPENCIL_MCP_CORS_ORIGIN: 'tauri://localhost'
}
}
})
expect(calls.at(-1)).toEqual({ cmd: 'plugin:shell|kill', args: { cmd: 'killChild', pid: 77 } })
})
})

View file

@ -0,0 +1,33 @@
import { mockIPC } from '@tauri-apps/api/mocks'
const windowLike = globalThis as typeof globalThis & {
__TAURI_INTERNALS__?: unknown
__TAURI_EVENT_PLUGIN_INTERNALS__?: unknown
}
Object.assign(globalThis, { window: windowLike })
mockIPC((cmd, args) => {
if (cmd !== 'build_fig_file') throw new Error(`Unexpected command: ${cmd}`)
const payload = args as {
schemaDeflated: number[]
kiwiData: number[]
thumbnailPng: number[]
metaJson: string
images: Array<{ name: string; data: number[] }>
}
if (payload.schemaDeflated.length === 0) throw new Error('schemaDeflated is empty')
if (payload.kiwiData.length === 0) throw new Error('kiwiData is empty')
if (payload.thumbnailPng.length === 0) throw new Error('thumbnailPng is empty')
if (payload.images.length !== 0) throw new Error('images should be empty')
JSON.parse(payload.metaJson)
return [7, 8, 9]
})
const [{ exportFigFile }, { SceneGraph }] = await Promise.all([
import('@open-pencil/core/io/formats/fig/export'),
import('@open-pencil/core/scene-graph')
])
const bytes = await exportFigFile(new SceneGraph())
if (bytes.length !== 3 || bytes[0] !== 7 || bytes[1] !== 8 || bytes[2] !== 9) {
throw new Error(`Unexpected export bytes: ${Array.from(bytes).join(',')}`)
}