import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils"; 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", () => { const bus = new EventBus(); let notified = false; bus.addEventListener("event", () => { notified = true; }); expect(notified).toBe(false); bus.trigger("event"); expect(notified).toBe(true); }); test("can unsubscribe", () => { const bus = new EventBus(); let n = 0; let cb = () => n++; bus.addEventListener("event", cb); expect(n).toBe(0); bus.trigger("event"); expect(n).toBe(1); bus.removeEventListener("event", cb); expect(n).toBe(1); bus.trigger("event"); expect(n).toBe(1); }); test("arguments are properly propagated", () => { expect.assertions(1); const bus = new EventBus(); 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", () => { test("callback is called only once after operations", async () => { let n = 0; let fn = batched(() => n++); expect(n).toBe(0); fn(); fn(); expect(n).toBe(0); await nextMicroTick(); expect(n).toBe(1); await nextMicroTick(); expect(n).toBe(1); }); test("calling batched function from within the callback is not treated as part of the original batch", async () => { let n = 0; let fn = batched(() => { n++; if (n === 1) { fn(); } }); expect(n).toBe(0); fn(); expect(n).toBe(0); await nextMicroTick(); // First batch expect(n).toBe(1); await nextMicroTick(); // Second batch initiated from within the callback expect(n).toBe(2); await nextMicroTick(); expect(n).toBe(2); }); }); const Markup = markup("").constructor; describe("markup", () => { test("string is flagged as safe", () => { const html = markup(""); expect(html).toBeInstanceOf(Markup); }); describe("htmlEscape", () => { test("htmlEscape escapes text", () => { const res = htmlEscape("test
"); expect(res.toString()).toBe("<p>test</p>"); expect(res).toBeInstanceOf(Markup); }); test("htmlEscape keeps html markup", () => { const res = htmlEscape(markup("test
")); expect(res.toString()).toBe("test
"); expect(res).toBeInstanceOf(Markup); }); test("htmlEscape produces empty string on undefined", () => { const res = htmlEscape(undefined); expect(res.toString()).toBe(""); expect(res).toBeInstanceOf(Markup); }); test("htmlEscape produces string from number", () => { const res = htmlEscape(10); expect(res.toString()).toBe("10"); expect(res).toBeInstanceOf(Markup); }); test("htmlEscape produces string from boolean", () => { const res = htmlEscape(false); expect(res.toString()).toBe("false"); expect(res).toBeInstanceOf(Markup); }); test("htmlEscape correctly escapes various links", () => { expect(htmlEscape("this is a link").toString()).toBe( "<a>this is a link</a>" ); expect(htmlEscape(`odoo`).toString()).toBe( `<a href="https://www.odoo.com">odoo<a>` ); expect(htmlEscape(`odoo`).toString()).toBe( `<a href='https://www.odoo.com'>odoo<a>` ); expect(htmlEscape("Odoo`s website").toString()).toBe( `<a href='https://www.odoo.com'>Odoo`s website<a>` ); }); test("htmlEscape doesn't escape already escaped content", () => { const res = htmlEscape("test
"); expect(res.toString()).toBe("<p>test</p>"); expect(res).toBeInstanceOf(Markup); const res2 = htmlEscape(res); expect(res2.toString()).toBe("<p>test</p>"); expect(res2).toBeInstanceOf(Markup); expect(res2).toBe(res); }); test("htmlEscape returns markup even for only-safe text", () => { const res = htmlEscape("safe"); expect(res.toString()).toBe("safe"); expect(res).toBeInstanceOf(Markup); }); }); describe("tag function", () => { test("interpolated values are escaped", () => { const maliciousInput = ""; const html = markup`${maliciousInput}`; expect(html.toString()).toBe("<script>alert('💥💥')</script>"); expect(html).toBeInstanceOf(Markup); }); test("interpolated markups aren't escaped", () => { const shouldBeEscaped = ""; const shouldnt = markup("this is safe"); const html = markup`test
"); expect(res.toString()).toBe("<p>test</p>"); const html = markup`${res}`; expect(html.toString()).toBe("<p>test</p>"); }); }); });