import { describe, it, beforeEach, afterEach } from "node:test"; import assert from "node:assert/strict"; import "/js/components/animated-sidebar.js"; describe("animated-sidebar", () => { const originalMatchMedia = window.matchMedia; // Media query list that supports `change` listener dispatch so tests can // simulate a viewport resize crossing the mobile breakpoint. function createMediaQueryList(matches, media) { const listeners = new Set(); return { get matches() { return this._matches; }, _matches: matches, media, onchange: null, addListener: (fn) => listeners.add(fn), removeListener: (fn) => listeners.delete(fn), addEventListener: (_type, fn) => listeners.add(fn), removeEventListener: (_type, fn) => listeners.delete(fn), dispatchEvent: () => {}, _set(newMatches) { this._matches = newMatches; for (const fn of listeners) { fn({ matches: newMatches, media }); } }, }; } let mobileQueryList; beforeEach(() => { document.body.innerHTML = ""; // Default to mobile viewport so open() goes through showModal(). // Desktop-specific tests override this. mobileQueryList = createMediaQueryList(true, "(max-width: 799px)"); window.matchMedia = (query) => { if (query === "(max-width: 799px)") { return mobileQueryList; } return createMediaQueryList(false, query); }; }); afterEach(() => { window.matchMedia = originalMatchMedia; }); function connectElement(element) { const container = document.createElement("div"); container.className = "page-visible"; // needed for scroll lock container.appendChild(element); document.body.appendChild(container); } describe("AnimatedSidebar - rendering", () => { it("should render sidebar dialog element", () => { const element = document.createElement("animated-sidebar"); connectElement(element); const sidebar = element.querySelector("dialog.sidebar"); assert(sidebar !== null); }); it("should set data-dialog-wrapper on the host element", () => { const element = document.createElement("animated-sidebar"); connectElement(element); assert(element.hasAttribute("data-dialog-wrapper")); }); it("should render sidebar-content", () => { const element = document.createElement("animated-sidebar"); connectElement(element); const content = element.querySelector(".sidebar-content"); assert(content !== null); }); it("should preserve children in sidebar-content", () => { const element = document.createElement("animated-sidebar"); element.innerHTML = "Test Content"; connectElement(element); const child = element.querySelector(".sidebar-content .test-child"); assert(child !== null); assert.deepEqual(child.textContent, "Test Content"); }); }); describe("AnimatedSidebar - initial state", () => { it("should start with isOpen set to false", () => { const element = document.createElement("animated-sidebar"); connectElement(element); assert.deepEqual(element.isOpen, false); }); it("should not have open attribute on sidebar initially", () => { const element = document.createElement("animated-sidebar"); connectElement(element); const sidebar = element.querySelector("dialog.sidebar"); assert(!sidebar.hasAttribute("open")); }); }); describe("AnimatedSidebar - open method", () => { it("should set isOpen to true when open() is called", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); assert.deepEqual(element.isOpen, true); }); it("should set open attribute on sidebar when open() is called", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); const sidebar = element.querySelector("dialog.sidebar"); assert(sidebar.hasAttribute("open")); }); it("should be findable via dialog[open] selector when opened", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); const openDialog = document.querySelector("dialog[open]"); assert(openDialog !== null); assert(openDialog.closest("[data-dialog-wrapper]") === element); }); it("should be a no-op on desktop viewports", () => { mobileQueryList._set(false); const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); assert.deepEqual(element.isOpen, false); const sidebar = element.querySelector("dialog.sidebar"); assert(!sidebar.hasAttribute("open")); }); }); describe("AnimatedSidebar - viewport resize", () => { it("should close when resizing from mobile to desktop while open", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); assert.deepEqual(element.isOpen, true); mobileQueryList._set(false); assert.deepEqual(element.isOpen, false); const sidebar = element.querySelector("dialog.sidebar"); assert(!sidebar.hasAttribute("open")); }); it("should not re-open or error when resizing back to mobile", () => { const element = document.createElement("animated-sidebar"); connectElement(element); mobileQueryList._set(false); mobileQueryList._set(true); assert.deepEqual(element.isOpen, false); }); it("should still respond to viewport changes after reconnecting", () => { const element = document.createElement("animated-sidebar"); connectElement(element); const container = element.parentElement; container.remove(); document.body.appendChild(container); element.open(); assert.deepEqual(element.isOpen, true); mobileQueryList._set(false); assert.deepEqual(element.isOpen, false); }); }); describe("AnimatedSidebar - close method", () => { it("should set isOpen to false when close() is called", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); element.close(); assert.deepEqual(element.isOpen, false); }); it("should remove open attribute from sidebar when close() is called", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); element.close(); const sidebar = element.querySelector("dialog.sidebar"); assert(!sidebar.hasAttribute("open")); }); it("should close immediately when animate: false", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); const sidebar = element.querySelector("dialog.sidebar"); sidebar.style.transitionDuration = "1s"; sidebar.getAnimations = () => []; element.close({ animate: false }); assert.deepEqual(element.isOpen, false); assert(!sidebar.hasAttribute("open")); }); }); describe("AnimatedSidebar - dismissal", () => { it("should close when cancel event fires on the dialog (ESC key)", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); const sidebar = element.querySelector("dialog.sidebar"); sidebar.dispatchEvent(new Event("cancel")); assert.deepEqual(element.isOpen, false); }); it("should close when the dialog itself is clicked (backdrop click)", () => { const element = document.createElement("animated-sidebar"); connectElement(element); element.open(); const sidebar = element.querySelector("dialog.sidebar"); sidebar.click(); assert.deepEqual(element.isOpen, false); }); it("should not close when a child of the dialog is clicked", () => { const element = document.createElement("animated-sidebar"); element.innerHTML = ""; connectElement(element); element.open(); const button = element.querySelector(".test-btn"); button.click(); assert.deepEqual(element.isOpen, true); }); }); describe("AnimatedSidebar - reinitialization protection", () => { it("should not reinitialize when connectedCallback is called multiple times", () => { const element = document.createElement("animated-sidebar"); element.innerHTML = "Original"; connectElement(element); // Manually trigger connectedCallback again (simulates re-attachment) element.connectedCallback(); // Should still have the original child preserved const child = element.querySelector(".sidebar-content .test-child"); assert(child !== null); assert.deepEqual(child.textContent, "Original"); }); }); });