From 3bd01095eb0809f69bf495d19e37378a025daf81 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 5 May 2026 22:45:14 +0300 Subject: [PATCH] test(app): cover remaining Tauri integrations --- src/app/automation/bridge/file-handlers.ts | 17 ++-- src/app/shell/menu/files.ts | 4 +- src/app/shell/theme.ts | 4 +- tests/engine/tauri-fig-export.test.ts | 17 ++++ .../tauri-remaining-integrations.test.ts | 89 +++++++++++++++++++ tests/helpers/tauri-fig-export-fixture.ts | 33 +++++++ 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 tests/engine/tauri-fig-export.test.ts create mode 100644 tests/engine/tauri-remaining-integrations.test.ts create mode 100644 tests/helpers/tauri-fig-export-fixture.ts diff --git a/src/app/automation/bridge/file-handlers.ts b/src/app/automation/bridge/file-handlers.ts index 29a68fb5c..0bf5a5a79 100644 --- a/src/app/automation/bridge/file-handlers.ts +++ b/src/app/automation/bridge/file-handlers.ts @@ -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 { return { ok: true } } +export async function ensureTauriParentDirectory(path: string): Promise { + 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 { 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 { 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) diff --git a/src/app/shell/menu/files.ts b/src/app/shell/menu/files.ts index 754367edf..1a1235a9e 100644 --- a/src/app/shell/menu/files.ts +++ b/src/app/shell/menu/files.ts @@ -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 } + globalThis.window as Window & { __OPEN_PENCIL_OPEN_FILE__?: (path: string) => Promise } ).__OPEN_PENCIL_OPEN_FILE__ = async (path: string) => { const response = await fetch(path) const blob = await response.blob() diff --git a/src/app/shell/theme.ts b/src/app/shell/theme.ts index 8044ed311..f5f5e539f 100644 --- a/src/app/shell/theme.ts +++ b/src/app/shell/theme.ts @@ -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 diff --git a/tests/engine/tauri-fig-export.test.ts b/tests/engine/tauri-fig-export.test.ts new file mode 100644 index 000000000..dadeac3a8 --- /dev/null +++ b/tests/engine/tauri-fig-export.test.ts @@ -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: '' }) + }) +}) diff --git a/tests/engine/tauri-remaining-integrations.test.ts b/tests/engine/tauri-remaining-integrations.test.ts new file mode 100644 index 000000000..ecada7585 --- /dev/null +++ b/tests/engine/tauri-remaining-integrations.test.ts @@ -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 } }) + }) +}) diff --git a/tests/helpers/tauri-fig-export-fixture.ts b/tests/helpers/tauri-fig-export-fixture.ts new file mode 100644 index 000000000..ab762cffd --- /dev/null +++ b/tests/helpers/tauri-fig-export-fixture.ts @@ -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(',')}`) +}