/**
* Tests for ArsTabs and ArsTabPanel
* @vi-environment jsdom
*/
import { beforeEach, describe, expect, it } from "vitest";
import { ArsTabs, ArsTabPanel } from "./ars-tabs.js";
describe("ArsTabs", () => {
let element: ArsTabs;
beforeEach(() => {
document.body.innerHTML = "";
element = document.createElement("ars-tabs") as ArsTabs;
});
/** Helper: creates tabs with panels */
function createTabsWithPanels(): ArsTabs {
element.innerHTML = `
Content One
Content Two
Content Three
`;
document.body.appendChild(element);
return element;
}
// --- Registration ---
it("registers the ars-tabs custom element", () => {
expect(customElements.get("ars-tabs")).toBe(ArsTabs);
});
it("registers the ars-tab-panel custom element", () => {
expect(customElements.get("ars-tab-panel")).toBe(ArsTabPanel);
});
it("creates a shadow root on construction", () => {
expect(element.shadowRoot).toBeTruthy();
});
// --- Tab rendering ---
it("renders tab buttons for each panel", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
expect(tabs.length).toBe(3);
});
it("renders a tablist with correct role", () => {
createTabsWithPanels();
const tablist = element.shadowRoot!.querySelector('[role="tablist"]');
expect(tablist).toBeTruthy();
});
it("renders tab labels from panel label attribute", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
expect(tabs[0].textContent).toContain("Tab One");
expect(tabs[1].textContent).toContain("Tab Two");
});
// --- Active tab ---
it("auto-selects the first tab if none specified", () => {
createTabsWithPanels();
expect(element.activeTab).toBe("one");
});
it("marks the active tab with aria-selected=true", () => {
createTabsWithPanels();
const activeTab = element.shadowRoot!.querySelector('[aria-selected="true"]');
expect(activeTab).toBeTruthy();
expect(activeTab!.getAttribute("data-tab-id")).toBe("one");
});
it("shows only the active panel", () => {
createTabsWithPanels();
const panels = element.querySelectorAll("ars-tab-panel");
expect((panels[0] as HTMLElement).style.display).toBe("flex");
expect((panels[0] as HTMLElement).style.flex).toBe("1 1 0%");
expect((panels[1] as HTMLElement).style.display).toBe("none");
expect((panels[2] as HTMLElement).style.display).toBe("none");
});
it("changes active tab via property setter", () => {
createTabsWithPanels();
element.activeTab = "two";
const panels = element.querySelectorAll("ars-tab-panel");
expect((panels[0] as HTMLElement).style.display).toBe("none");
expect((panels[1] as HTMLElement).style.display).toBe("flex");
expect((panels[1] as HTMLElement).style.flex).toBe("1 1 0%");
});
it("changes active tab via attribute", () => {
createTabsWithPanels();
element.setAttribute("active-tab", "three");
const panels = element.querySelectorAll("ars-tab-panel");
expect((panels[2] as HTMLElement).style.display).toBe("flex");
expect((panels[2] as HTMLElement).style.flex).toBe("1 1 0%");
});
// --- Tab click ---
it("switches tab on click", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[1] as HTMLElement).click();
expect(element.activeTab).toBe("two");
});
// --- Events ---
it("emits ars-tabs:change with activeTab and previousTab", () => {
createTabsWithPanels();
const events: any[] = [];
element.addEventListener("ars-tabs:change", (e) => {
events.push((e as CustomEvent).detail);
});
element.activeTab = "two";
expect(events.length).toBe(1);
expect(events[0].activeTab).toBe("two");
expect(events[0].previousTab).toBe("one");
});
it("does not emit change when setting the same tab", () => {
createTabsWithPanels();
const events: any[] = [];
element.addEventListener("ars-tabs:change", (e) => events.push(e));
element.activeTab = "one";
expect(events.length).toBe(0);
});
// --- Disabled tabs ---
it("does not switch to a disabled tab on click", () => {
element.innerHTML = `
A
B
`;
document.body.appendChild(element);
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[1] as HTMLElement).click();
expect(element.activeTab).toBe("a"); // Should not change
});
it("marks disabled tabs with disabled attribute", () => {
element.innerHTML = `
A
B
`;
document.body.appendChild(element);
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
expect((tabs[1] as HTMLButtonElement).disabled).toBe(true);
});
// --- Placement ---
it("defaults to top placement", () => {
createTabsWithPanels();
expect(element.placement).toBe("top");
});
it("supports placement attribute", () => {
element.setAttribute("placement", "start");
createTabsWithPanels();
expect(element.placement).toBe("start");
});
it("renders vertical tablist for start/end placement", () => {
element.setAttribute("placement", "start");
createTabsWithPanels();
const tablist = element.shadowRoot!.querySelector('[role="tablist"]');
expect(tablist!.getAttribute("aria-orientation")).toBe("vertical");
});
it("renders horizontal tablist for top/bottom placement", () => {
createTabsWithPanels();
const tablist = element.shadowRoot!.querySelector('[role="tablist"]');
expect(tablist!.getAttribute("aria-orientation")).toBe("horizontal");
});
// --- Keyboard navigation ---
it("moves focus and activates next tab on ArrowRight", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[0] as HTMLElement).dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }),
);
expect(element.activeTab).toBe("two");
});
it("wraps around on ArrowRight from last tab", () => {
createTabsWithPanels();
element.activeTab = "three";
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[2] as HTMLElement).dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true }),
);
expect(element.activeTab).toBe("one");
});
it("jumps to first tab on Home key", () => {
createTabsWithPanels();
element.activeTab = "three";
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[2] as HTMLElement).dispatchEvent(
new KeyboardEvent("keydown", { key: "Home", bubbles: true }),
);
expect(element.activeTab).toBe("one");
});
it("jumps to last tab on End key", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
(tabs[0] as HTMLElement).dispatchEvent(
new KeyboardEvent("keydown", { key: "End", bubbles: true }),
);
expect(element.activeTab).toBe("three");
});
// --- ARIA ---
it("sets aria-controls on tabs pointing to panels", () => {
createTabsWithPanels();
const tabs = element.shadowRoot!.querySelectorAll('[role="tab"]');
expect(tabs[0].getAttribute("aria-controls")).toBe("panel-one");
});
it("sets role=tabpanel on panel elements", () => {
createTabsWithPanels();
const panels = element.querySelectorAll("ars-tab-panel");
panels.forEach((p) => expect(p.getAttribute("role")).toBe("tabpanel"));
});
// --- Observed attributes ---
it("observes the expected attributes", () => {
expect(ArsTabs.observedAttributes).toContain("active-tab");
expect(ArsTabs.observedAttributes).toContain("placement");
});
// --- ArsTabPanel properties ---
it("ArsTabPanel reflects tabId property", () => {
const panel = document.createElement("ars-tab-panel") as ArsTabPanel;
panel.tabId = "test";
expect(panel.getAttribute("tab-id")).toBe("test");
});
it("ArsTabPanel reflects label property", () => {
const panel = document.createElement("ars-tab-panel") as ArsTabPanel;
panel.label = "Test Label";
expect(panel.getAttribute("label")).toBe("Test Label");
});
it("ArsTabPanel reflects disabled property", () => {
const panel = document.createElement("ars-tab-panel") as ArsTabPanel;
panel.disabled = true;
expect(panel.hasAttribute("disabled")).toBe(true);
});
// --- CSS tokens ---
it("uses design token variables in styles", () => {
createTabsWithPanels();
const styles = element.shadowRoot!.querySelector("style")?.textContent ?? "";
expect(styles).toContain("--arswc-color-accent");
expect(styles).toContain("--arswc-color-border");
expect(styles).toContain("--arswc-color-muted");
expect(styles).toContain("--arswc-focus-ring");
expect(styles).toContain("--arswc-transition-duration");
});
// --- Slot ---
it("provides a default slot for panels", () => {
createTabsWithPanels();
expect(element.shadowRoot!.querySelector("slot")).toBeTruthy();
});
});