Files
Genarrative/apps/ai-game-creator-shell/tests/tauriEventFake.ts
T
k88936 594063a1e0 测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
2026-10-03 17:41:30 +08:00

240 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 与 tauri 2.11.x 等价的 Tauri 事件替身,用于回归「注销竞态」这一类 bug。
*
* 现有各套件的替身(`window.__TAURI__ = { core: { invoke }, event: { listen } }`)
* 里的 `listen` 返回的注销函数不读任何注册表,所以**结构上抓不到**
* `undefined is not an object (evaluating 'listeners[eventId].handlerId')`
* (tauri-apps/tauri#15799)。这里按 tauri 2.11.3 的真实实现建三层:
*
* - `window.__TAURI_INTERNALS__`:core 注入的底层桥(`invoke` / `transformCallback`
* / `unregisterCallback`),AGC 的 `subscribeTauriEvent` 走这一层。
* - `window.__TAURI__`:`withGlobalTauri` 的全局桥,`event.listen` 按库内实现返回
* 「先读注册表条目再摘回调」的注销函数(就是会抛的那条路径)。
* - `window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener`:库内注销脚本本体,
* 条目缺失时抛 `TypeError`,并在 `racyUnregisterViolations` 里留痕,便于断言。
*
* 注册表条目由 `plugin:event|listen` 之后**另一次** eval 写入:替身把它排进
* `pendingRegistrationEvals`,只有在 `flushRegistrationEvals()` 之后才可见。
*/
export type TauriEventFakeEvent = {
event: string;
id: number;
payload: unknown;
};
type FakeListener = {
event: string;
eventId: number;
handlerId: number;
target: unknown;
};
export type TauriEventFake = {
/** 已登记在「后端」的事件订阅(模拟 `Listeners::js_event_listeners`)。 */
listeners: FakeListener[];
/** 库内注销脚本读不到注册表条目时的记录(每次都是一次竞态命中)。 */
racyUnregisterViolations: string[];
/** 库内注销脚本被调用的次数。 */
racyUnregisterCalls: number;
/** 全局桥 `event.listen` 被调用的次数。 */
bridgeListenCalls: number;
/** 每个事件的 `plugin:event|unlisten` 调用次数。 */
unlistenInvokeCounts: Map<string, number>;
/** 每个事件的 `plugin:event|listen` 调用次数。 */
listenInvokeCounts: Map<string, number>;
/** 还没落地的注册 eval 数量(模拟 listen 之后那次独立 eval)。 */
pendingRegistrationEvals(): number;
/** 让注册 eval 落地:之后 `emit` 才能命中新登记的事件 ID。 */
flushRegistrationEvals(): void;
/** 仍在 `__TAURI_INTERNALS__.callbacks` 里的 handler 数量。 */
jsCallbackCount(): number;
/** 直接投递事件,语义与 `event_initialization_script` 一致(条目缺失则跳过)。 */
emit(event: string, payload: unknown): void;
install(): void;
restore(): void;
};
type FakeOptions = {
/** 非 event 插件的命令交给它(通常是套件自己的 invoke 替身)。 */
invoke?: (
command: string,
args?: Record<string, unknown>,
) => Promise<unknown>;
};
const INTERNALS_KEY = '__TAURI_INTERNALS__';
const EVENT_INTERNALS_KEY = '__TAURI_EVENT_PLUGIN_INTERNALS__';
const GLOBAL_KEY = '__TAURI__';
type MutableWindow = Window & Record<string, unknown>;
export function createTauriEventFake(
options: FakeOptions = {},
): TauriEventFake {
const listeners: FakeListener[] = [];
const racyUnregisterViolations: string[] = [];
const unlistenInvokeCounts = new Map<string, number>();
const listenInvokeCounts = new Map<string, number>();
const jsCallbacks = new Map<number, (eventData: unknown) => void>();
const registrations: FakeListener[] = [];
let nextEventId = 1;
let nextHandlerId = 1;
let racyUnregisterCalls = 0;
let bridgeListenCalls = 0;
let installedGlobals: Array<{ key: string; had: boolean; value: unknown }> =
[];
const registry: Record<string, Record<number, { handlerId: number }>> = {};
const fake: TauriEventFake = {
listeners,
racyUnregisterViolations,
get racyUnregisterCalls() {
return racyUnregisterCalls;
},
get bridgeListenCalls() {
return bridgeListenCalls;
},
unlistenInvokeCounts,
listenInvokeCounts,
pendingRegistrationEvals: () => registrations.length,
flushRegistrationEvals() {
const pending = registrations.splice(0, registrations.length);
for (const listener of pending) {
const eventRegistry = (registry[listener.event] ??= {});
eventRegistry[listener.eventId] = {
handlerId: listener.handlerId,
};
}
},
jsCallbackCount: () => jsCallbacks.size,
emit(event, payload) {
for (const listener of listeners) {
if (listener.event !== event) continue;
const entry = registry[event]?.[listener.eventId];
if (!entry) continue;
jsCallbacks.get(entry.handlerId)?.({
event,
id: listener.eventId,
payload,
});
}
},
install() {
const target = window as unknown as MutableWindow;
const internals = {
invoke: async (command: string, args?: Record<string, unknown>) => {
if (command === 'plugin:event|listen') {
const event = String(args?.event ?? '');
const eventId = nextEventId++;
listenInvokeCounts.set(
event,
(listenInvokeCounts.get(event) ?? 0) + 1,
);
const listener: FakeListener = {
event,
eventId,
handlerId: Number(args?.handler ?? 0),
target: args?.target,
};
listeners.push(listener);
// 注册表条目由 listen 之后一次独立 eval 写入,这里只排队。
registrations.push(listener);
return eventId;
}
if (command === 'plugin:event|unlisten') {
const event = String(args?.event ?? '');
const eventId = Number(args?.eventId ?? 0);
unlistenInvokeCounts.set(
event,
(unlistenInvokeCounts.get(event) ?? 0) + 1,
);
const index = listeners.findIndex(
(item) => item.event === event && item.eventId === eventId,
);
if (index >= 0) listeners.splice(index, 1);
return null;
}
if (options.invoke) return options.invoke(command, args);
return null;
},
transformCallback: (callback: (eventData: unknown) => void) => {
const handlerId = nextHandlerId++;
jsCallbacks.set(handlerId, callback);
return handlerId;
},
unregisterCallback: (handlerId: number) => {
jsCallbacks.delete(handlerId);
},
};
const racyUnregisterListener = (event: string, eventId: number) => {
racyUnregisterCalls += 1;
const eventListeners = registry[event];
if (!eventListeners) return;
const entry = eventListeners[eventId];
if (!entry) {
racyUnregisterViolations.push(`${event}#${eventId}`);
// tauri 2.11 unlisten_js_script 的真实抛错点。
throw new TypeError(
"undefined is not an object (evaluating 'listeners[eventId].handlerId')",
);
}
jsCallbacks.delete(entry.handlerId);
};
// 全局桥(withGlobalTauri):按库内实现返回会读注册表的注销函数。
const bridgeListen = async (
event: string,
handler: (eventData: unknown) => void,
) => {
bridgeListenCalls += 1;
const handlerId = internals.transformCallback(handler);
const eventId = Number(
(await internals.invoke('plugin:event|listen', {
event,
target: { kind: 'Any' },
handler: handlerId,
})) as number,
);
return () => {
racyUnregisterListener(event, eventId);
void internals.invoke('plugin:event|unlisten', { event, eventId });
};
};
const globals: Array<[string, unknown]> = [
[INTERNALS_KEY, internals],
[EVENT_INTERNALS_KEY, { unregisterListener: racyUnregisterListener }],
[
GLOBAL_KEY,
{
core: { invoke: internals.invoke },
event: {
listen: (event: string, handler: (eventData: unknown) => void) =>
bridgeListen(event, handler),
},
},
],
];
installedGlobals = globals.map(([key]) => ({
key,
had: key in target,
value: target[key],
}));
for (const [key, value] of globals) target[key] = value;
},
restore() {
const target = window as unknown as MutableWindow;
for (const entry of installedGlobals) {
if (entry.had) target[entry.key] = entry.value;
else delete target[entry.key];
}
installedGlobals = [];
},
};
return fake;
}