|
3 | 3 | // SPDX-License-Identifier: MIT
|
4 | 4 |
|
5 | 5 | import type { InvokeArgs, InvokeOptions } from './core'
|
| 6 | +import { EventName } from './event' |
6 | 7 |
|
7 | 8 | function mockInternals() {
|
8 | 9 | window.__TAURI_INTERNALS__ = window.__TAURI_INTERNALS__ ?? {}
|
| 10 | + window.__TAURI_EVENT_PLUGIN_INTERNALS__ = |
| 11 | + window.__TAURI_EVENT_PLUGIN_INTERNALS__ ?? {} |
| 12 | +} |
| 13 | + |
| 14 | +/** |
| 15 | + * Options for `mockIPC`. |
| 16 | + * |
| 17 | + * # Options |
| 18 | + * `shouldMockEvents`: If true, the `listen` and `emit` functions will be mocked, allowing you to test event handling without a real backend. |
| 19 | + * **This will consume any events emitted with the `plugin:event` prefix.** |
| 20 | + * |
| 21 | + * @since 2.7.0 |
| 22 | + */ |
| 23 | +export interface MockIPCOptions { |
| 24 | + shouldMockEvents?: boolean |
9 | 25 | }
|
10 | 26 |
|
11 | 27 | /**
|
@@ -59,19 +75,89 @@ function mockInternals() {
|
59 | 75 | * })
|
60 | 76 | * ```
|
61 | 77 | *
|
| 78 | + * `listen` can also be mocked with direct calls to the `emit` function. This functionality is opt-in via the `shouldMockEvents` option: |
| 79 | + * ```js |
| 80 | + * import { mockIPC, clearMocks } from "@tauri-apps/api/mocks" |
| 81 | + * import { emit, listen } from "@tauri-apps/api/event" |
| 82 | + * |
| 83 | + * afterEach(() => { |
| 84 | + * clearMocks() |
| 85 | + * }) |
| 86 | + * |
| 87 | + * test("mocked event", () => { |
| 88 | + * mockIPC(() => {}, { shouldMockEvents: true }); // enable event mocking |
| 89 | + * |
| 90 | + * const eventHandler = vi.fn(); |
| 91 | + * listen('test-event', eventHandler); // typically in component setup or similar |
| 92 | + * |
| 93 | + * emit('test-event', { foo: 'bar' }); |
| 94 | + * expect(eventHandler).toHaveBeenCalledWith({ |
| 95 | + * event: 'test-event', |
| 96 | + * payload: { foo: 'bar' } |
| 97 | + * }); |
| 98 | + * }) |
| 99 | + * ``` |
| 100 | + * `emitTo` is currently **not** supported by this mock implementation. |
| 101 | + * |
62 | 102 | * @since 1.0.0
|
63 | 103 | */
|
64 | 104 | export function mockIPC(
|
65 |
| - cb: (cmd: string, payload?: InvokeArgs) => unknown |
| 105 | + cb: (cmd: string, payload?: InvokeArgs) => unknown, |
| 106 | + options?: MockIPCOptions |
66 | 107 | ): void {
|
67 | 108 | mockInternals()
|
68 | 109 |
|
| 110 | + function isEventPluginInvoke(cmd: string): boolean { |
| 111 | + return cmd.startsWith('plugin:event|') |
| 112 | + } |
| 113 | + |
| 114 | + function handleEventPlugin(cmd: string, args?: InvokeArgs): unknown { |
| 115 | + switch (cmd) { |
| 116 | + case 'plugin:event|listen': |
| 117 | + return handleListen(args as { event: EventName; handler: number }) |
| 118 | + case 'plugin:event|emit': |
| 119 | + return handleEmit(args as { event: EventName; payload?: unknown }) |
| 120 | + case 'plugin:event|unlisten': |
| 121 | + return handleRemoveListener(args as { event: EventName; id: number }) |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + const listeners = new Map<string, number[]>() |
| 126 | + function handleListen(args: { event: EventName; handler: number }) { |
| 127 | + if (!listeners.has(args.event)) { |
| 128 | + listeners.set(args.event, []) |
| 129 | + } |
| 130 | + listeners.get(args.event)!.push(args.handler) |
| 131 | + return args.handler |
| 132 | + } |
| 133 | + |
| 134 | + function handleEmit(args: { event: EventName; payload?: unknown }) { |
| 135 | + const eventListeners = listeners.get(args.event) || [] |
| 136 | + for (const handler of eventListeners) { |
| 137 | + runCallback(handler, args) |
| 138 | + } |
| 139 | + return null |
| 140 | + } |
| 141 | + function handleRemoveListener(args: { event: EventName; id: number }) { |
| 142 | + const eventListeners = listeners.get(args.event) |
| 143 | + if (eventListeners) { |
| 144 | + const index = eventListeners.indexOf(args.id) |
| 145 | + if (index !== -1) { |
| 146 | + eventListeners.splice(index, 1) |
| 147 | + } |
| 148 | + } |
| 149 | + } |
| 150 | + |
69 | 151 | // eslint-disable-next-line @typescript-eslint/require-await
|
70 | 152 | async function invoke<T>(
|
71 | 153 | cmd: string,
|
72 | 154 | args?: InvokeArgs,
|
73 | 155 | _options?: InvokeOptions
|
74 | 156 | ): Promise<T> {
|
| 157 | + if (options?.shouldMockEvents && isEventPluginInvoke(cmd)) { |
| 158 | + return handleEventPlugin(cmd, args) as T |
| 159 | + } |
| 160 | + |
75 | 161 | return cb(cmd, args) as T
|
76 | 162 | }
|
77 | 163 |
|
@@ -107,11 +193,17 @@ export function mockIPC(
|
107 | 193 | }
|
108 | 194 | }
|
109 | 195 |
|
| 196 | + function unregisterListener(event: EventName, id: number) { |
| 197 | + unregisterCallback(id) |
| 198 | + } |
| 199 | + |
110 | 200 | window.__TAURI_INTERNALS__.invoke = invoke
|
111 | 201 | window.__TAURI_INTERNALS__.transformCallback = registerCallback
|
112 | 202 | window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback
|
113 | 203 | window.__TAURI_INTERNALS__.runCallback = runCallback
|
114 | 204 | window.__TAURI_INTERNALS__.callbacks = callbacks
|
| 205 | + window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener = |
| 206 | + unregisterListener |
115 | 207 | }
|
116 | 208 |
|
117 | 209 | /**
|
@@ -240,4 +332,10 @@ export function clearMocks(): void {
|
240 | 332 | delete window.__TAURI_INTERNALS__.convertFileSrc
|
241 | 333 | // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
|
242 | 334 | delete window.__TAURI_INTERNALS__.metadata
|
| 335 | + |
| 336 | + if (typeof window.__TAURI_EVENT_PLUGIN_INTERNALS__ !== 'object') { |
| 337 | + return |
| 338 | + } |
| 339 | + // @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case |
| 340 | + delete window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener |
243 | 341 | }
|
0 commit comments