Skip to content

Commit 9639146

Browse files
feat(mock): add mock for listen and emit (#13783)
* feat(mock): add mock for listen and emit * feat(mock): add mock for listen and emit * feat(mock): add mock for listen and emit * Add change file * correctly clear unregisterListener * format with prettier * build project * opt-in to mocking events * Use a minor bump
1 parent c0a654b commit 9639146

File tree

4 files changed

+108
-2
lines changed

4 files changed

+108
-2
lines changed

.changes/mock-emit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tauri-apps/api": "minor:enhance"
3+
---
4+
5+
Allow events emitted with `emit` to be handled correctly by `listen` callbacks when in a mocked environment

crates/tauri/scripts/bundle.global.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/api/src/global.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ declare global {
3434
}
3535
}
3636
}
37+
__TAURI_EVENT_PLUGIN_INTERNALS__: {
38+
unregisterListener: (event: string, eventId: number) => void
39+
}
3740
}
3841
}
3942

packages/api/src/mocks.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,25 @@
33
// SPDX-License-Identifier: MIT
44

55
import type { InvokeArgs, InvokeOptions } from './core'
6+
import { EventName } from './event'
67

78
function mockInternals() {
89
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
925
}
1026

1127
/**
@@ -59,19 +75,89 @@ function mockInternals() {
5975
* })
6076
* ```
6177
*
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+
*
62102
* @since 1.0.0
63103
*/
64104
export function mockIPC(
65-
cb: (cmd: string, payload?: InvokeArgs) => unknown
105+
cb: (cmd: string, payload?: InvokeArgs) => unknown,
106+
options?: MockIPCOptions
66107
): void {
67108
mockInternals()
68109

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+
69151
// eslint-disable-next-line @typescript-eslint/require-await
70152
async function invoke<T>(
71153
cmd: string,
72154
args?: InvokeArgs,
73155
_options?: InvokeOptions
74156
): Promise<T> {
157+
if (options?.shouldMockEvents && isEventPluginInvoke(cmd)) {
158+
return handleEventPlugin(cmd, args) as T
159+
}
160+
75161
return cb(cmd, args) as T
76162
}
77163

@@ -107,11 +193,17 @@ export function mockIPC(
107193
}
108194
}
109195

196+
function unregisterListener(event: EventName, id: number) {
197+
unregisterCallback(id)
198+
}
199+
110200
window.__TAURI_INTERNALS__.invoke = invoke
111201
window.__TAURI_INTERNALS__.transformCallback = registerCallback
112202
window.__TAURI_INTERNALS__.unregisterCallback = unregisterCallback
113203
window.__TAURI_INTERNALS__.runCallback = runCallback
114204
window.__TAURI_INTERNALS__.callbacks = callbacks
205+
window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener =
206+
unregisterListener
115207
}
116208

117209
/**
@@ -240,4 +332,10 @@ export function clearMocks(): void {
240332
delete window.__TAURI_INTERNALS__.convertFileSrc
241333
// @ts-expect-error "The operand of a 'delete' operator must be optional." does not matter in this case
242334
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
243341
}

0 commit comments

Comments
 (0)