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");
});
});
});