diff --git a/src/runtime/utils.ts b/src/runtime/utils.ts index ccdbc13f4..8914b8d91 100644 --- a/src/runtime/utils.ts +++ b/src/runtime/utils.ts @@ -1,4 +1,5 @@ import { OwlError } from "../common/owl_error"; +import { ComponentNode, getCurrent } from "./component_node"; export type Callback = () => void; /** @@ -81,10 +82,52 @@ export function validateTarget(target: HTMLElement | ShadowRoot) { } export class EventBus extends EventTarget { + constructor(events?: string[]) { + if (events) { + let node: ComponentNode | null = null; + try { + node = getCurrent(); + } catch {} + if (node?.app?.dev) { + return new DebugEventBus(events); + } + } + super(); + } trigger(name: string, payload?: any) { this.dispatchEvent(new CustomEvent(name, { detail: payload })); } } +class DebugEventBus extends EventBus { + private events: Set; + constructor(events: string[]) { + super(); + this.events = new Set(events); + } + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions + ): void { + if (!this.events.has(type)) { + throw new OwlError(`EventBus: subscribing to unknown event '${type}'`); + } + super.addEventListener(type, listener, options); + } + trigger(name: string, payload?: any) { + if (!this.events.has(name)) { + throw new OwlError(`EventBus: triggering unknown event '${name}'`); + } + super.trigger(name, payload); + } + + dispatchEvent(event: Event): boolean { + if (!this.events.has(event.type)) { + throw new OwlError(`EventBus: dispatching unknown event '${event.type}'`); + } + return super.dispatchEvent(event); + } +} export function whenReady(fn?: any): Promise { return new Promise(function (resolve) { diff --git a/tests/utils.test.ts b/tests/utils.test.ts index 44466d6b7..2a4b6054b 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -1,5 +1,7 @@ import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils"; -import { nextMicroTick } from "./helpers"; +import { makeTestFixture, nextMicroTick } from "./helpers"; +import { getCurrent } from "../src/runtime/component_node"; +import { Component, mount, xml } from "../src"; describe("event bus behaviour", () => { test("can subscribe and be notified", () => { @@ -33,6 +35,66 @@ describe("event bus behaviour", () => { bus.addEventListener("event", (ev: any) => expect(ev.detail).toBe("hello world")); bus.trigger("event", "hello world"); }); + + test("events are not validated if the bus is created outside of dev mode", async () => { + let bus_empty: EventBus | null = null; + class Root extends Component { + static template = xml`
`; + + setup() { + getCurrent(); // checks that we're in a component context + + bus_empty = new EventBus([]); + } + } + await mount(Root, makeTestFixture()); + + bus_empty!.addEventListener("a", () => {}); + bus_empty!.trigger("a"); + bus_empty!.dispatchEvent(new CustomEvent("a")); + }); + test("events are validated if the bus is created in dev mode & events are provided", async () => { + let bus: EventBus | null = null; + let bus_empty: EventBus | null = null; + let bbus_no_validation: EventBus | null = null; + class Root extends Component { + static template = xml`
`; + + setup() { + getCurrent(); // checks that we're in a component context + + bus = new EventBus(["a", "b"]); + bus_empty = new EventBus([]); + bbus_no_validation = new EventBus(); + } + } + + await mount(Root, makeTestFixture(), { test: true }); + + bbus_no_validation!.addEventListener("c", () => {}); + bbus_no_validation!.trigger("c"); + bbus_no_validation!.dispatchEvent(new CustomEvent("c")); + + bus!.addEventListener("a", () => {}); + bus!.trigger("a"); + bus!.dispatchEvent(new CustomEvent("a")); + + expect(() => bus!.addEventListener("c", () => {})).toThrow( + "EventBus: subscribing to unknown event 'c'" + ); + expect(() => bus!.trigger("c")).toThrow("EventBus: triggering unknown event 'c'"); + expect(() => bus!.dispatchEvent(new CustomEvent("c"))).toThrow( + "EventBus: dispatching unknown event 'c'" + ); + + expect(() => bus_empty!.addEventListener("a", () => {})).toThrow( + "EventBus: subscribing to unknown event 'a'" + ); + expect(() => bus_empty!.trigger("a")).toThrow("EventBus: triggering unknown event 'a'"); + expect(() => bus_empty!.dispatchEvent(new CustomEvent("a"))).toThrow( + "EventBus: dispatching unknown event 'a'" + ); + }); }); describe("batched", () => {