测试:appSurface 的 Tauri 替身统一走单一安装点

- harness 新增 createTauriEventBridge,只负责事件登记与派发,不碰 window、不做类型转换
- home.suite 44 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,并删除 asTauriInvoke / asTauriListen 两个适配器
- auth.suite 删除 installTauriBridge 的 as never,改为 createTauriEventBridge + installTauriRuntime 组合
This commit is contained in:
2026-10-03 19:34:08 +08:00
parent 6efab10743
commit 179983b823
3 changed files with 113 additions and 144 deletions
@@ -10,8 +10,10 @@ import { installUnhandledRejectionBridge } from '../unhandledRejectionBridge';
import {
act,
AuthenticatedClient,
createTauriEventBridge,
expect,
fireEvent,
installTauriRuntime,
it,
React,
render,
@@ -21,52 +23,22 @@ import {
waitFor,
} from './harness';
type AuthEventBridge = {
listeners: Map<string, (event: { payload: unknown }) => void>;
emit: (event: string, payload: unknown) => void;
};
/**
* 注入 Tauri 事件替身。
*
* jsdom 没有 `__TAURI_INTERNALS__`,`subscribeTauriEvent` 会退回
* `window.__TAURI__.event.listen`,所以这里按同一入口登记回调。
* `window.__TAURI__.event.listen`,所以按同一入口登记回调:安装走
* harness.installTauriRuntime,事件注册 / 派发由 harness.createTauriEventBridge 负责。
*/
function installTauriBridge(
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
options: { withEvents?: boolean } = {},
): AuthEventBridge {
const listeners = new Map<string, (event: { payload: unknown }) => void>();
window.__TAURI__ = {
core: { invoke },
...(options.withEvents
? {
event: {
listen: async (
event: string,
handler: (payload: { payload: unknown }) => void,
) => {
listeners.set(event, handler);
return () => listeners.delete(event);
},
},
}
: {}),
} as never;
return {
listeners,
emit: (event, payload) => {
const handler = listeners.get(event);
if (handler) handler({ payload });
},
};
}
function renderAuthSurface(
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
options: { withEvents?: boolean } = {},
) {
const bridge = installTauriBridge(invoke, options);
const bridge = createTauriEventBridge();
installTauriRuntime({
core: { invoke },
...(options.withEvents ? { event: { listen: bridge.listen } } : {}),
});
render(
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改传进 props。
React.createElement(AuthenticatedClient, {
@@ -962,6 +962,30 @@ export function installTauriRuntime(runtime: {
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 function installResizeObserverStub() {
let observerCount = 0;
let observerDisconnected = false;
@@ -11,6 +11,7 @@ import {
findResourceSelectButton,
fireEvent,
getResourceSelectButton,
installTauriRuntime,
it,
nativeClipboardMock,
pickProjectFromLauncher,
@@ -28,34 +29,6 @@ import {
within,
} from './harness';
type TauriInvoke = NonNullable<
NonNullable<NonNullable<Window['__TAURI__']>['core']>['invoke']
>;
type TauriListen = NonNullable<
NonNullable<NonNullable<Window['__TAURI__']>['event']>['listen']
>;
// Tauri 的 invoke / listen 都是泛型签名,而测试里的 vi.fn mock 只有具体返回类型与
// 事件负载,非泛型函数无法直接满足泛型签名。下面两个 helper 只做类型收敛,运行时
// 原样返回同一个 mock,调用方仍持有原引用(mock.calls 等断言不受影响)。
function asTauriInvoke(
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
): TauriInvoke {
// 泛型 invoke 与具体的非泛型 mock 结构上不可兼容,只能用 as unknown as 收敛。
return invoke as unknown as TauriInvoke;
}
function asTauriListen(
listen: (
event: string,
handler: (event: { payload: unknown }) => void,
) => Promise<() => void>,
): TauriListen {
// 泛型 listen 与携带具体负载的 mock 结构上不可兼容,只能用 as unknown as 收敛。
return listen as unknown as TauriListen;
}
// App 的 props 参数带默认值,`React.ComponentProps<typeof App>` 会带上 undefined,
// createElement 因此推断不出 props;这里收窄成非空组件类型,运行时仍是同一个组件。
const TestApp = App as React.ComponentType<
@@ -160,7 +133,7 @@ export function registerClientHomeTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
// 挂载即加载目录,触发按钮直接落在默认模型上,不出现「选择模型」空态。
@@ -208,7 +181,7 @@ export function registerClientHomeTests() {
if (command === 'read_game_creator_app_config') return { config: {} };
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('get_game_template_library_access'),
@@ -290,7 +263,7 @@ export function registerClientHomeTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
// 首页推荐位只展示封面、标题、运行时与已下载徽标,本机灵感图库已随模板库上线删除。
@@ -432,7 +405,7 @@ export function registerClientHomeTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
render(
React.createElement(WorkspaceLauncherShell, {
currentUser: testAuthUser,
@@ -574,10 +547,10 @@ export function registerClientHomeTests() {
return runtimeHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(runtimeHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: runtimeHarness.listen },
});
render(
React.createElement(WorkspaceLauncherShell, {
currentUser: testAuthUser,
@@ -747,10 +720,10 @@ export function registerClientHomeTests() {
return runtimeHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(runtimeHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: runtimeHarness.listen },
});
render(
React.createElement(WorkspaceLauncherShell, {
currentUser: testAuthUser,
@@ -854,7 +827,7 @@ export function registerClientHomeTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
expect(screen.getByLabelText('GameAgent 客户端')).not.toBeNull();
@@ -1051,7 +1024,7 @@ export function registerClientHomeTests() {
return undefined;
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '充值' }));
@@ -1151,7 +1124,7 @@ export function registerClientHomeTests() {
}
return undefined;
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '我的' }));
@@ -1200,9 +1173,9 @@ export function registerClientHomeTests() {
export function registerHomeProjectCreationTests() {
it('scopes the 策划补全 option to the game entry and clears it on creation-type switch', () => {
window.__TAURI__ = {
core: { invoke: asTauriInvoke(vi.fn(async () => null)) },
};
installTauriRuntime({
core: { invoke: vi.fn(async () => null) },
});
renderLauncherAt('/?launcher', 'home', true);
const creationTypes = screen.getByRole('group', { name: '创作类型' });
@@ -1259,10 +1232,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
fireEvent.click(screen.getByRole('checkbox', { name: '策划补全' }));
@@ -1315,7 +1288,7 @@ export function registerHomeProjectCreationTests() {
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/authorized-game']),
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
expect(await screen.findByText('authorized-game')).not.toBeNull();
@@ -1365,7 +1338,7 @@ export function registerHomeProjectCreationTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
pickProjectFromLauncher('/tmp/missing-game');
@@ -1407,7 +1380,7 @@ export function registerHomeProjectCreationTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
@@ -1460,7 +1433,7 @@ export function registerHomeProjectCreationTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
@@ -1493,7 +1466,7 @@ export function registerHomeProjectCreationTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
@@ -1513,7 +1486,7 @@ export function registerHomeProjectCreationTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
const openButton = screen.getByRole('button', { name: '打开项目' });
@@ -1572,10 +1545,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
const creationTypes = screen.getByRole('group', { name: '创作类型' });
@@ -1719,10 +1692,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
const fileInput =
@@ -1832,7 +1805,7 @@ export function registerHomeProjectCreationTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher', 'home', true);
const promptInput = screen.getByLabelText('创作想法');
@@ -1904,7 +1877,7 @@ export function registerHomeProjectCreationTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher', 'home', true);
await act(async () => {
await vi.advanceTimersByTimeAsync(0);
@@ -1998,10 +1971,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
// 做方案走立项链路:建项之后还要写策划运行时,这一步失败时项目目录已经存在。
@@ -2065,10 +2038,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
if (modeLabel === '做方案') {
@@ -2162,10 +2135,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
fireEvent.click(screen.getByRole('button', { name: '做方案' }));
@@ -2300,10 +2273,10 @@ export function registerHomeProjectCreationTests() {
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(chatHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: chatHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
fireEvent.click(screen.getByRole('button', { name: '做方案' }));
@@ -2366,10 +2339,10 @@ export function registerHomeProjectCreationTests() {
return runtimeHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(runtimeHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: runtimeHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
const promptInput = screen.getByLabelText('创作想法');
@@ -2542,7 +2515,7 @@ export function registerHomeProjectCreationTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
render(
React.createElement(TestApp, {
@@ -2626,7 +2599,7 @@ export function registerHomeProjectCreationTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
render(
React.createElement(TestApp, {
@@ -2737,7 +2710,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(paths),
@@ -2808,7 +2781,7 @@ export function registerRecentProjectsTests() {
return undefined;
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
expect(screen.getByLabelText('最近项目')).not.toBeNull();
@@ -2898,7 +2871,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify([projectPath]),
@@ -2946,7 +2919,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify([projectPath]),
@@ -2995,7 +2968,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/recent-one']),
@@ -3034,7 +3007,7 @@ export function registerRecentProjectsTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
pickProjectFromLauncher('/tmp/open-failed-game');
@@ -3084,7 +3057,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify([
@@ -3231,7 +3204,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/refreshable-game']),
@@ -3296,7 +3269,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherAt('/?launcher');
expect(screen.getByLabelText('最近项目')).not.toBeNull();
@@ -3344,7 +3317,7 @@ export function registerRecentProjectsTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
@@ -3374,7 +3347,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
@@ -3415,7 +3388,7 @@ export function registerRecentProjectsTests() {
},
);
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
@@ -3461,7 +3434,7 @@ export function registerRecentProjectsTests() {
},
);
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
@@ -3514,7 +3487,7 @@ export function registerRecentProjectsTests() {
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
@@ -3541,7 +3514,7 @@ export function registerRecentProjectsTests() {
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
installTauriRuntime({ core: { invoke } });
renderLauncherProjectsAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
@@ -3608,10 +3581,10 @@ export function registerRecentProjectsTests() {
return supervisorHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: asTauriInvoke(invoke) },
event: { listen: asTauriListen(supervisorHarness.listen) },
};
installTauriRuntime({
core: { invoke },
event: { listen: supervisorHarness.listen },
});
renderLauncherAt('/?launcher', 'home', true);
// 设置 → 工作区:默认位置就是不选目录,仍然落在 AGC 管理的应用数据目录。