测试:抽出全仓共享的 Tauri 运行时替身模块

- 新增 tests/tauriRuntimeFake.ts,作为唯一的 window.__TAURI__ 安装点与类型转换点,并提供 createTauriEventBridge 与 resetTauriRuntime
- appSurface/harness.ts 改为从该模块导入并转出 installTauriRuntime / createTauriEventBridge,既有 ./harness 导入路径不变
- pickProjectFromLauncher 不再直接写 window.__TAURI__,改走 installTauriRuntime 保留原有 core/event
This commit is contained in:
2026-10-03 19:41:21 +08:00
parent 55cca04971
commit a77d58b901
2 changed files with 68 additions and 48 deletions
@@ -15,6 +15,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resetLlmModelCatalogCacheForTest } from '../../src/services/llmModelCatalog';
import { useLauncherHomeDraftStore } from '../../src/view/home/useHomeDraftStore';
import {
createTauriEventBridge,
installTauriRuntime,
} from '../tauriRuntimeFake';
const nativeClipboardMock = vi.hoisted(() => ({
text: '',
@@ -200,7 +204,7 @@ function pickProjectFromLauncher(
if (!tauri || !invoke) {
throw new Error('Tauri invoke is not configured');
}
window.__TAURI__ = {
installTauriRuntime({
...tauri,
core: {
...tauri.core,
@@ -210,7 +214,7 @@ function pickProjectFromLauncher(
? projectPath
: invoke(command, args)) as unknown as typeof invoke,
},
};
});
fireEvent.click(screen.getByRole('button', { name: action }));
}
@@ -938,53 +942,10 @@ function createProjectChatRuntimeHarness({
/**
* 安装测试用 Tauri 替身。
*
* `vi.fn` 与各夹具的 invoke / listen 是固定签名,装不进 `window.__TAURI__` 声明的泛型签名
* (两者都是泛型函数),这里把唯一的装入点收在 harness,整体显式转换,行为与直接赋值一致。
*
* 参数用「可调用」下界而不是 `unknown`:非函数的误装(字符串、undefined)会在编译期被挡下,
* 同时不与各夹具更具体的 mock 签名冲突;`core` / `event` 都可缺省,只装 invoke 的用例
* 不需要伪造 event。
* 定义已收进 `tests/tauriRuntimeFake.ts`(全仓唯一安装点),这里只转出,供 appSurface 各
* suite 沿用既有的 `./harness` 导入路径。
*/
export function installTauriRuntime(runtime: {
core?: {
invoke?: (
command: string,
args?: Record<string, unknown>,
) => Promise<unknown>;
};
event?: {
listen?: (
event: string,
handler: (event: { payload: unknown }) => void,
) => Promise<() => void>;
};
}) {
window.__TAURI__ = runtime as unknown as Window['__TAURI__'];
}
/**
* 测试用 Tauri 事件注册表:只登记 `event.listen` 的回调并按事件名派发。
*
* 不碰 `window.__TAURI__`、不做类型转换;安装由 `installTauriRuntime` 负责,需要事件桥的
* 用例把 `bridge.listen` 交给它即可。
*/
export function createTauriEventBridge() {
const listeners = new Map<string, (event: { payload: unknown }) => void>();
return {
listen: async (
event: string,
handler: (event: { payload: unknown }) => void,
) => {
listeners.set(event, handler);
return () => {
listeners.delete(event);
};
},
emit: (event: string, payload: unknown) => {
listeners.get(event)?.({ payload });
},
};
}
export { createTauriEventBridge, installTauriRuntime };
export function installResizeObserverStub() {
let observerCount = 0;
@@ -0,0 +1,59 @@
/**
* 测试用 Tauri 全局桥替身:全仓唯一的 `window.__TAURI__` 安装点与类型转换点。
*
* `Window['__TAURI__']` 声明的是泛型签名(`core.invoke` / `event.listen` 都是泛型函数),
* 而各套件的替身是 `vi.fn` 或具体签名的 mock,二者结构上不兼容。若在每个用例里直接赋值,
* 就得反复写 `as never` / `as unknown as`,类型检查形同虚设;这里把转换收敛到一处,
* 调用方只声明自己真正装了哪些能力。
*
* 参数用「可调用」下界而不是 `unknown`:非函数的误装(字符串、`undefined`)会在编译期被挡下,
* 同时不与各夹具更具体的 mock 签名冲突;`core` / `event` 都可缺省。
*/
export type TauriRuntimeInstall = {
core?: {
invoke?: (
command: string,
args?: Record<string, unknown>,
) => Promise<unknown>;
};
event?: {
listen?: (
event: string,
handler: (event: { payload: unknown }) => void,
) => Promise<() => void>;
};
};
export function installTauriRuntime(runtime: TauriRuntimeInstall) {
window.__TAURI__ = runtime as unknown as Window['__TAURI__'];
}
/** 清掉替身,供 `afterEach` / `beforeEach` 复位,等价于历史里的 `window.__TAURI__ = undefined`。 */
export function resetTauriRuntime() {
window.__TAURI__ = undefined;
}
/**
* 测试用 Tauri 事件注册表:只登记 `event.listen` 的回调并按事件名派发。
*
* 不碰 `window.__TAURI__`、不做类型转换;安装由 `installTauriRuntime` 负责,需要事件桥的
* 用例把 `bridge.listen` 交给它即可。
*/
export function createTauriEventBridge() {
const listeners = new Map<string, (event: { payload: unknown }) => void>();
return {
listen: async (
event: string,
handler: (event: { payload: unknown }) => void,
) => {
listeners.set(event, handler);
return () => {
listeners.delete(event);
};
},
emit: (event: string, payload: unknown) => {
listeners.get(event)?.({ payload });
},
};
}