Skip to content

Commit 740698d

Browse files
committed
feat: Allow disabling namespace checks
Messenger namespace checks can now be disabled by using the `DISABLE_NAMESPACE` namespace constructor parameter, and setting the `Namespace` type parameter to `string`. This makes it much easier to mock messengers in unit tests.
1 parent e470404 commit 740698d

File tree

4 files changed

+207
-7
lines changed

4 files changed

+207
-7
lines changed

packages/messenger/src/Messenger.test.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Patch } from 'immer';
22
import sinon from 'sinon';
33

4-
import { Messenger } from './Messenger';
4+
import { Messenger, DISABLE_NAMESPACE } from './Messenger';
55

66
describe('Messenger', () => {
77
afterEach(() => {
@@ -27,6 +27,24 @@ describe('Messenger', () => {
2727
expect(count).toBe(1);
2828
});
2929

30+
it('allows registering and calling an action handler for a different namespace using DISABLE_NAMESPACE', () => {
31+
type CountAction = {
32+
type: 'Fixture:count';
33+
handler: (increment: number) => void;
34+
};
35+
const messenger = new Messenger<string, CountAction, never>({
36+
namespace: DISABLE_NAMESPACE,
37+
});
38+
39+
let count = 0;
40+
messenger.registerActionHandler('Fixture:count', (increment: number) => {
41+
count += increment;
42+
});
43+
messenger.call('Fixture:count', 1);
44+
45+
expect(count).toBe(1);
46+
});
47+
3048
it('automatically delegates actions to parent upon registration', () => {
3149
type CountAction = {
3250
type: 'Fixture:count';
@@ -168,6 +186,67 @@ describe('Messenger', () => {
168186
}).toThrow('A handler for Fixture:ping has not been registered');
169187
});
170188

189+
it('throws when registering an action handler for a different namespace', () => {
190+
type CountAction = {
191+
type: 'Fixture:count';
192+
handler: (increment: number) => void;
193+
};
194+
const messenger = new Messenger<'Different', CountAction, never>({
195+
namespace: 'Different',
196+
});
197+
198+
expect(() =>
199+
// @ts-expect-error Intentionally invalid parameter
200+
messenger.registerActionHandler('Fixture:count', jest.fn()),
201+
).toThrow(
202+
`Only allowed registering action handlers prefixed by 'Different:'`,
203+
);
204+
});
205+
206+
it('throws when unregistering an action handler for a different namespace', () => {
207+
type CountAction = {
208+
type: 'Source:count';
209+
handler: (increment: number) => void;
210+
};
211+
const sourceMessenger = new Messenger<'Source', CountAction, never>({
212+
namespace: 'Source',
213+
});
214+
const messenger = new Messenger<'Destination', CountAction, never>({
215+
namespace: 'Destination',
216+
});
217+
sourceMessenger.delegate({ actions: ['Source:count'], messenger });
218+
219+
expect(() =>
220+
// @ts-expect-error Intentionally invalid parameter
221+
messenger.unregisterActionHandler('Source:count'),
222+
).toThrow(
223+
`Only allowed unregistering action handlers prefixed by 'Destination:'`,
224+
);
225+
});
226+
227+
it('throws when calling an action from a different namespace that has been unregistered using DISABLE_NAMESPACE', () => {
228+
type PingAction = { type: 'Fixture:ping'; handler: () => void };
229+
const messenger = new Messenger<string, PingAction, never>({
230+
namespace: DISABLE_NAMESPACE,
231+
});
232+
233+
expect(() => {
234+
messenger.call('Fixture:ping');
235+
}).toThrow('A handler for Fixture:ping has not been registered');
236+
237+
let pingCount = 0;
238+
messenger.registerActionHandler('Fixture:ping', () => {
239+
pingCount += 1;
240+
});
241+
242+
messenger.unregisterActionHandler('Fixture:ping');
243+
244+
expect(() => {
245+
messenger.call('Fixture:ping');
246+
}).toThrow('A handler for Fixture:ping has not been registered');
247+
expect(pingCount).toBe(0);
248+
});
249+
171250
it('throws when calling an action that has been unregistered', () => {
172251
type PingAction = { type: 'Fixture:ping'; handler: () => void };
173252
const messenger = new Messenger<'Fixture', PingAction, never>({
@@ -259,6 +338,20 @@ describe('Messenger', () => {
259338
expect(handler.callCount).toBe(1);
260339
});
261340

341+
it('publishes event from different namespace using DISABLE_NAMESPACE', () => {
342+
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
343+
const messenger = new Messenger<string, never, MessageEvent>({
344+
namespace: DISABLE_NAMESPACE,
345+
});
346+
347+
const handler = sinon.stub();
348+
messenger.subscribe('Fixture:message', handler);
349+
messenger.publish('Fixture:message', 'hello');
350+
351+
expect(handler.calledWithExactly('hello')).toBe(true);
352+
expect(handler.callCount).toBe(1);
353+
});
354+
262355
it('automatically delegates events to parent upon first publish', () => {
263356
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
264357
const parentMessenger = new Messenger<'Parent', never, MessageEvent>({
@@ -428,6 +521,66 @@ describe('Messenger', () => {
428521
});
429522
});
430523

524+
describe('on first state change with an initial payload function from another namespace registered (using DISABLE_NAMESPACE)', () => {
525+
it('publishes event if selected payload differs', () => {
526+
const state = {
527+
propA: 1,
528+
propB: 1,
529+
};
530+
type MessageEvent = {
531+
type: 'Fixture:complexMessage';
532+
payload: [typeof state];
533+
};
534+
const messenger = new Messenger<string, never, MessageEvent>({
535+
namespace: DISABLE_NAMESPACE,
536+
});
537+
messenger.registerInitialEventPayload({
538+
eventType: 'Fixture:complexMessage',
539+
getPayload: () => [state],
540+
});
541+
const handler = sinon.stub();
542+
messenger.subscribe(
543+
'Fixture:complexMessage',
544+
handler,
545+
(obj) => obj.propA,
546+
);
547+
548+
state.propA += 1;
549+
messenger.publish('Fixture:complexMessage', state);
550+
551+
expect(handler.getCall(0)?.args).toStrictEqual([2, 1]);
552+
expect(handler.callCount).toBe(1);
553+
});
554+
555+
it('does not publish event if selected payload is the same', () => {
556+
const state = {
557+
propA: 1,
558+
propB: 1,
559+
};
560+
type MessageEvent = {
561+
type: 'Fixture:complexMessage';
562+
payload: [typeof state];
563+
};
564+
const messenger = new Messenger<string, never, MessageEvent>({
565+
namespace: DISABLE_NAMESPACE,
566+
});
567+
messenger.registerInitialEventPayload({
568+
eventType: 'Fixture:complexMessage',
569+
getPayload: () => [state],
570+
});
571+
const handler = sinon.stub();
572+
messenger.subscribe(
573+
'Fixture:complexMessage',
574+
handler,
575+
(obj) => obj.propA,
576+
);
577+
578+
messenger.publish('Fixture:complexMessage', state);
579+
580+
expect(handler.callCount).toBe(0);
581+
});
582+
});
583+
431584
describe('on first state change without an initial payload function registered', () => {
432585
it('publishes event if selected payload differs', () => {
433586
const state = {
@@ -727,6 +880,42 @@ describe('Messenger', () => {
727880
expect(stub.callCount).toBe(0);
728881
});
729882

883+
it('throws when publishing an event from another namespace', () => {
884+
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
885+
const messenger = new Messenger<'Other', never, MessageEvent>({
886+
namespace: 'Other',
887+
});
888+
const handler = jest.fn();
889+
messenger.subscribe('Fixture:message', handler);
890+
891+
// @ts-expect-error Intentionally invalid parameter
892+
expect(() => messenger.publish('Fixture:message', 'hello')).toThrow(
893+
`Only allowed publishing events prefixed by 'Other:'`,
894+
);
895+
expect(handler).not.toHaveBeenCalled();
896+
});
897+
898+
it('throws when registering an initial event payload from another namespace', () => {
899+
type MessageEvent = {
900+
type: 'Fixture:complexMessage';
901+
payload: [null];
902+
};
903+
const messenger = new Messenger<'Other', never, MessageEvent>({
904+
namespace: 'Other',
905+
});
906+
907+
expect(() =>
908+
messenger.registerInitialEventPayload({
909+
// @ts-expect-error Intentionally invalid parameter
910+
eventType: 'Fixture:complexMessage',
911+
// @ts-expect-error Intentionally invalid parameter
912+
getPayload: () => [null],
913+
}),
914+
).toThrow(
915+
`Only allowed registering initial payloads for events prefixed by 'Other:'`,
916+
);
917+
});
918+
730919
it('throws when unsubscribing when there are no subscriptions', () => {
731920
type MessageEvent = { type: 'Fixture:message'; payload: [string] };
732921
const messenger = new Messenger<'Fixture', never, MessageEvent>({

packages/messenger/src/Messenger.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ export type MessengerEvents<
9494
? Event
9595
: never;
9696

97+
/**
98+
* Messenger namespace checks can be disabled by using this as the `namespace` constructor
99+
* parameter, and using `string` as the Namespace type parameter.
100+
*
101+
* This is useful for mocking a variety of different actions/events in unit tests. Please do not
102+
* use this in production code.
103+
*/
104+
export const DISABLE_NAMESPACE = 'DISABLE_NAMESPACE';
105+
97106
/**
98107
* Metadata for a single event subscription.
99108
*
@@ -278,7 +287,6 @@ export class Messenger<
278287
registerActionHandler<
279288
ActionType extends Action['type'] & NamespacedName<Namespace>,
280289
>(actionType: ActionType, handler: ActionHandler<Action, ActionType>) {
281-
/* istanbul ignore if */ // Branch unreachable with valid types
282290
if (!this.#isInCurrentNamespace(actionType)) {
283291
throw new Error(
284292
`Only allowed registering action handlers prefixed by '${
@@ -341,7 +349,6 @@ export class Messenger<
341349
unregisterActionHandler<
342350
ActionType extends Action['type'] & NamespacedName<Namespace>,
343351
>(actionType: ActionType) {
344-
/* istanbul ignore if */ // Branch unreachable with valid types
345352
if (!this.#isInCurrentNamespace(actionType)) {
346353
throw new Error(
347354
`Only allowed unregistering action handlers prefixed by '${
@@ -419,7 +426,6 @@ export class Messenger<
419426
eventType: EventType;
420427
getPayload: () => ExtractEventPayload<Event, EventType>;
421428
}) {
422-
/* istanbul ignore if */ // Branch unreachable with valid types
423429
if (!this.#isInCurrentNamespace(eventType)) {
424430
throw new Error(
425431
`Only allowed registering initial payloads for events prefixed by '${
@@ -478,7 +484,6 @@ export class Messenger<
478484
eventType: EventType & NamespacedName<Namespace>,
479485
...payload: ExtractEventPayload<Event, EventType>
480486
) {
481-
/* istanbul ignore if */ // Branch unreachable with valid types
482487
if (!this.#isInCurrentNamespace(eventType)) {
483488
throw new Error(
484489
`Only allowed publishing events prefixed by '${this.#namespace}:'`,
@@ -985,10 +990,15 @@ export class Messenger<
985990
/**
986991
* Determine whether the given name is within the current namespace.
987992
*
993+
* If the current namespace is DISABLE_NAMESPACE, this check always returns true.
994+
*
988995
* @param name - The name to check
989996
* @returns Whether the name is within the current namespace
990997
*/
991998
#isInCurrentNamespace(name: string): name is NamespacedName<Namespace> {
992-
return name.startsWith(`${this.#namespace}:`);
999+
return (
1000+
this.#namespace === DISABLE_NAMESPACE ||
1001+
name.startsWith(`${this.#namespace}:`)
1002+
);
9931003
}
9941004
}

packages/messenger/src/index.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ describe('@metamask/messenger', () => {
44
it('has expected JavaScript exports', () => {
55
expect(Object.keys(allExports)).toMatchInlineSnapshot(`
66
Array [
7+
"DISABLE_NAMESPACE",
78
"Messenger",
89
]
910
`);

packages/messenger/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export type {
1414
NotNamespacedBy,
1515
NamespacedName,
1616
} from './Messenger';
17-
export { Messenger } from './Messenger';
17+
export { DISABLE_NAMESPACE, Messenger } from './Messenger';

0 commit comments

Comments
 (0)