Skip to content

Commit 76792d4

Browse files
committed
[ADD] runtime/utils: optional validation to EventBus
Currently the event bus allows sending and listening to arbitrary events, I got got by that when I pushed a fix using `addEventListener` on a bus across an events renaming, and on the other side the fix did nothing anymore. Entirely my fault, but if the list of events sent on a bus is known and documented (e.g. a jsdoc has `@emits` tags) it would make sense for both the listening and the dispatching to also be validated. This proposal performs validation only when the eventbus is created: - in dev mode (which also requires being in a component context) - if an iterable of events is passed to the ctor The dev-mode check might be overkill but it seems like a good idea at least for an initial version, as the validation does have a cost however low, and validation errors can occur essentially anywhere.
1 parent c2728c9 commit 76792d4

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

src/runtime/utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OwlError } from "../common/owl_error";
2+
import { ComponentNode, getCurrent } from "./component_node";
23
export type Callback = () => void;
34

45
/**
@@ -81,10 +82,46 @@ export function validateTarget(target: HTMLElement | ShadowRoot) {
8182
}
8283

8384
export class EventBus extends EventTarget {
85+
constructor(events?: string[]) {
86+
if (events) {
87+
let node: ComponentNode | null = null;
88+
try { node = getCurrent() } catch {}
89+
if (node?.app?.dev) {
90+
return new DebugEventBus(events);
91+
}
92+
}
93+
super()
94+
}
8495
trigger(name: string, payload?: any) {
8596
this.dispatchEvent(new CustomEvent(name, { detail: payload }));
8697
}
8798
}
99+
class DebugEventBus extends EventBus {
100+
private events: Set<string>;
101+
constructor(events: string[]) {
102+
super();
103+
this.events = new Set(events);
104+
}
105+
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void {
106+
if (!this.events.has(type)) {
107+
throw new OwlError(`EventBus: subscribing to unknown event '${type}'`);
108+
}
109+
super.addEventListener(type, listener, options);
110+
}
111+
trigger(name: string, payload?: any) {
112+
if (!this.events.has(name)) {
113+
throw new OwlError(`EventBus: triggering unknown event '${name}'`);
114+
}
115+
super.trigger(name, payload);
116+
}
117+
118+
dispatchEvent(event: Event): boolean {
119+
if (!this.events.has(event.type)) {
120+
throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`);
121+
}
122+
return super.dispatchEvent(event);
123+
}
124+
}
88125

89126
export function whenReady(fn?: any): Promise<void> {
90127
return new Promise(function (resolve) {

tests/utils.test.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
2-
import { nextMicroTick } from "./helpers";
2+
import { makeTestFixture, nextMicroTick } from "./helpers";
3+
import { getCurrent } from "../src/runtime/component_node";
4+
import { Component, mount, xml } from "../src";
35

46
describe("event bus behaviour", () => {
57
test("can subscribe and be notified", () => {
@@ -33,6 +35,72 @@ describe("event bus behaviour", () => {
3335
bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world"));
3436
bus.trigger("event", "hello world");
3537
});
38+
39+
test(
40+
"events are not validated if the bus is created outside of dev mode",
41+
async () => {
42+
let bus_empty: EventBus | null = null;
43+
class Root extends Component {
44+
static template = xml`<div/>`;
45+
46+
setup() {
47+
getCurrent(); // checks that we're in a component context
48+
49+
bus_empty = new EventBus([]);
50+
}
51+
}
52+
await mount(Root, makeTestFixture());
53+
54+
bus_empty!.addEventListener("a", () => {});
55+
bus_empty!.trigger("a");
56+
bus_empty!.dispatchEvent(new CustomEvent("a"));
57+
}
58+
)
59+
test(
60+
"events are validated if the bus is created in dev mode & events are provided",
61+
async () => {
62+
let bus: EventBus | null = null;
63+
let bus_empty: EventBus | null = null;
64+
let bbus_no_validation: EventBus | null = null;
65+
class Root extends Component {
66+
static template = xml`<div/>`;
67+
68+
setup() {
69+
getCurrent(); // checks that we're in a component context
70+
71+
bus = new EventBus(["a", "b"]);
72+
bus_empty = new EventBus([]);
73+
bbus_no_validation = new EventBus();
74+
}
75+
}
76+
77+
await mount(Root, makeTestFixture(), { test: true });
78+
79+
bbus_no_validation!.addEventListener("c", () => {});
80+
bbus_no_validation!.trigger("c");
81+
bbus_no_validation!.dispatchEvent(new CustomEvent("c"));
82+
83+
bus!.addEventListener("a", () => {});
84+
bus!.trigger("a");
85+
bus!.dispatchEvent(new CustomEvent("a"));
86+
87+
expect(() => bus!.addEventListener("c", () => {})).toThrow(
88+
"EventBus: subscribing to unknown event 'c'"
89+
);
90+
expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'");
91+
expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow(
92+
"EventBus: dispatching unknown event 'c'"
93+
);
94+
95+
expect(() => bus_empty!.addEventListener("a", () => {})).toThrow(
96+
"EventBus: subscribing to unknown event 'a'"
97+
);
98+
expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'");
99+
expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow(
100+
"EventBus: dispatching unknown event 'a'"
101+
);
102+
}
103+
);
36104
});
37105

38106
describe("batched", () => {

0 commit comments

Comments
 (0)