import { describe, it, expect, vi, beforeEach } from "vitest"; import { screen } from "@testing-library/dom"; import { userEvent } from "@testing-library/user-event"; import { ElementWithUtils, qsr, onDOMReady, __testing, } from "../../src/ts/utils/dom"; const resetReady = __testing.resetReady; describe("dom", () => { describe("ElementWithUtils", () => { describe("onChild", () => { const handler = vi.fn(); function registerOnChild( event: string, selector: string, options?: { parent?: ElementWithUtils; }, ): void { const parent = options?.parent ?? qsr("#parent"); parent?.onChild(event, selector, (e) => handler({ target: e.target, childTarget: e.childTarget, currentTarget: e.currentTarget, }), ); } beforeEach(() => { handler.mockClear(); document.body.innerHTML = ""; const root = document.createElement("div"); root.innerHTML = `
test
test
test
test
`; document.body.appendChild(root); }); it("should not fire when parent element is clicked", async () => { //GIVEN registerOnChild("click", "div"); //WHEN await userEvent.click(screen.getByTestId("parent")); //THEN expect(handler).not.toHaveBeenCalled(); }); it("should not fire when selector doesnt match", async () => { //GIVEN const buttonEl = qsr("#button"); registerOnChild("click", "div", { parent: buttonEl }); //WHEN await userEvent.click(screen.getByTestId("icon")); //THEN expect(handler).not.toHaveBeenCalled(); }); it("should fire when selector is clicked", async () => { //GIVEN registerOnChild("click", "div"); //WHEN const clickTarget = screen.getByTestId("mid1"); await userEvent.click(clickTarget); //THEN expect(handler).toHaveBeenCalledWith( expect.objectContaining({ target: clickTarget, childTarget: clickTarget, currentTarget: screen.getByTestId("parent"), }), ); }); it("should fire when child of selector is clicked", async () => { //GIVEN registerOnChild("click", "div.middle"); //WHEN const selectorTarget = screen.getByTestId("mid1"); const clickTarget = screen.getByTestId("button"); await userEvent.click(clickTarget); //THEN expect(handler).toHaveBeenCalledWith( expect.objectContaining({ target: clickTarget, childTarget: selectorTarget, currentTarget: screen.getByTestId("parent"), }), ); }); it("should fire on each element matching the selector from the child up to the parent", async () => { //GIVEN registerOnChild("click", "div.middle, div.inner"); //WHEN let clickTarget = screen.getByTestId("button"); await userEvent.click(clickTarget); //THEN expect(handler).toHaveBeenCalledTimes(2); //First call is for childTarget inner2 (grand child of parent) expect(handler).toHaveBeenNthCalledWith( 1, expect.objectContaining({ target: clickTarget, childTarget: screen.getByTestId("inner2"), currentTarget: screen.getByTestId("parent"), }), ); //Second call is for childTarget mid1 (child of parent) expect(handler).toHaveBeenNthCalledWith( 2, expect.objectContaining({ target: clickTarget, childTarget: screen.getByTestId("mid1"), currentTarget: screen.getByTestId("parent"), }), ); //WHEN click on mid1 handler is only called one time handler.mockClear(); clickTarget = screen.getByTestId("mid1"); await userEvent.click(clickTarget); //THEN expect(handler).toHaveBeenCalledTimes(1); expect(handler).toHaveBeenCalledWith( expect.objectContaining({ target: clickTarget, childTarget: clickTarget, currentTarget: screen.getByTestId("parent"), }), ); }); }); }); describe("onDOMReady", () => { beforeEach(() => { document.body.innerHTML = ""; resetReady(); vi.useFakeTimers(); }); function dispatchEvent(event: "DOMContextLoaded" | "load"): void { if (event === "DOMContextLoaded") { document.dispatchEvent(new Event("DOMContentLoaded")); } else { window.dispatchEvent(new Event("load")); } vi.runAllTimers(); } it("executes callbacks when DOMContentLoaded fires", () => { const spy = vi.fn(); onDOMReady(spy); expect(spy).not.toHaveBeenCalled(); dispatchEvent("DOMContextLoaded"); expect(spy).toHaveBeenCalledOnce(); }); it("executes callbacks added before ready in order", () => { const calls: number[] = []; onDOMReady(() => calls.push(1)); onDOMReady(() => calls.push(2)); dispatchEvent("DOMContextLoaded"); expect(calls).toEqual([1, 2]); }); it("executes callbacks asynchronously when DOM is already ready", () => { const spy = vi.fn(); Object.defineProperty(document, "readyState", { value: "complete", configurable: true, }); onDOMReady(spy); expect(spy).not.toHaveBeenCalled(); vi.runAllTimers(); expect(spy).toHaveBeenCalledOnce(); }); it("executes callbacks added after ready asynchronously", () => { const calls: string[] = []; onDOMReady(() => calls.push("ready")); dispatchEvent("DOMContextLoaded"); onDOMReady(() => calls.push("late")); expect(calls).toEqual(["ready"]); vi.runAllTimers(); expect(calls).toEqual(["ready", "late"]); }); it("executes callbacks added during ready execution", () => { const calls: number[] = []; onDOMReady(() => { calls.push(1); onDOMReady(() => calls.push(3)); }); onDOMReady(() => calls.push(2)); dispatchEvent("DOMContextLoaded"); expect(calls).toEqual([1, 2, 3]); }); it("does not execute ready callbacks more than once", () => { const spy = vi.fn(); onDOMReady(spy); dispatchEvent("DOMContextLoaded"); dispatchEvent("load"); expect(spy).toHaveBeenCalledOnce(); }); it("falls back to window load event if DOMContentLoaded does not fire", () => { const spy = vi.fn(); onDOMReady(spy); dispatchEvent("load"); expect(spy).toHaveBeenCalledOnce(); }); }); });