// @vitest-environment happy-dom
import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest';
import { Tabs } from './tabs.js';
// Helper utility for async delays
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
describe('Tabs Plugin', () => {
const fixture = `
Test 1
Test 2
Test 3
Test 4
Test 5
Test 6
Test 7
Test 8
`;
let container: HTMLDivElement;
beforeEach(() => {
// Mount fixture
container = document.createElement('div');
container.innerHTML = fixture;
document.body.appendChild(container);
const normalTabs = document.querySelector('.tabs.normal')!;
Tabs.init(normalTabs, { duration: 0 });
window.location.hash = '';
// Force selection of test2 as active tab
Tabs.getInstance(normalTabs)?.select('test2');
});
afterEach(() => {
const normalTabs = document.querySelector('.tabs.normal');
if (normalTabs) {
Tabs.getInstance(normalTabs)?.destroy();
}
document.body.innerHTML = '';
});
describe('Tabs', () => {
it('should open to active tab', () => {
const normalTabs = document.querySelector('.tabs.normal')!;
const activeTab = normalTabs.querySelector('.active')!;
const activeTabHash = activeTab.getAttribute('href')!;
const tabLinks = normalTabs.querySelectorAll('.tab a');
tabLinks.forEach((tabLink) => {
const tabHash = tabLink.getAttribute('href')!;
const contentEl = document.querySelector(tabHash)!;
if (tabHash === activeTabHash) {
expect(contentEl.style.display).not.toBe('none');
} else {
expect(contentEl.style.display).toBe('none');
}
});
const indicator = normalTabs.querySelector('.indicator');
expect(indicator).not.toBeNull();
});
it('should switch to clicked tab', async () => {
const normalTabs = document.querySelector('.tabs.normal')!;
const activeTab = normalTabs.querySelector('.active')!;
const activeTabHash = activeTab.getAttribute('href')!;
const disabledTab = normalTabs.querySelector('.disabled a')!;
const disabledTabHash = disabledTab.getAttribute('href')!;
const firstTab = normalTabs.querySelector('.tab a')!;
const firstTabHash = firstTab.getAttribute('href')!;
const indicator = normalTabs.querySelector('.indicator')!;
expect(indicator).not.toBeNull();
// Click disabled tab
disabledTab.click();
await delay(10);
const activeContent = document.querySelector(activeTabHash)!;
const disabledContent = document.querySelector(disabledTabHash)!;
expect(activeContent.style.display).not.toBe('none');
expect(disabledContent.style.display).toBe('none');
// Click first tab
firstTab.click();
await delay(10);
const firstContent = document.querySelector(firstTabHash)!;
expect(activeContent.style.display).toBe('none');
expect(firstContent.style.display).not.toBe('none');
expect(indicator.offsetLeft).toBe(firstTab.offsetLeft);
});
it("shouldn't hide active tab if clicked while active", async () => {
const normalTabs = document.querySelector('.tabs.normal')!;
const activeTab = normalTabs.querySelector('.active')!;
const activeTabHash = activeTab.getAttribute('href')!;
const indicator = normalTabs.querySelector('.indicator');
expect(indicator).not.toBeNull();
activeTab.click();
await delay(10);
const activeContent = document.querySelector(activeTabHash)!;
expect(activeContent.style.display).not.toBe('none');
});
it('should horizontally scroll when too many tabs', () => {
const normalTabs = document.querySelector('.tabs.normal')!;
const tabsInstance = Tabs.getInstance(normalTabs);
// Mock container visible width (400px) and total scroll width (800px)
vi.spyOn(normalTabs, 'getBoundingClientRect').mockReturnValue({
width: 400,
height: 50,
top: 0,
left: 0,
bottom: 50,
right: 400,
x: 0,
y: 0,
toJSON: () => {}
});
Object.defineProperty(normalTabs, 'scrollWidth', {
configurable: true,
value: 800
});
// Trigger recalculation
tabsInstance._setTabsAndTabWidth();
// Verify internal width state
expect(normalTabs.scrollWidth).toBeGreaterThan(tabsInstance._tabsWidth);
expect(tabsInstance._tabWidth).toBe(100); // 800px scrollWidth / 8 tabs
});
it('should programmatically switch tabs', async () => {
const normalTabs = document.querySelector('.tabs.normal')!;
const activeTab = normalTabs.querySelector('.active')!;
const activeTabHash = activeTab.getAttribute('href')!;
const firstTab = normalTabs.querySelector('li a')!;
const firstTabHash = firstTab.getAttribute('href')!;
const indicator = normalTabs.querySelector('.indicator')!;
const tabs = normalTabs.querySelectorAll('.tab a');
tabs.forEach((tab) => {
const tabHash = tab.getAttribute('href')!;
const contentEl = document.querySelector(tabHash)!;
if (tabHash === activeTabHash) {
expect(contentEl.style.display).not.toBe('none');
} else {
expect(contentEl.style.display).toBe('none');
}
});
Tabs.getInstance(normalTabs)?.select('test1');
await delay(10);
const activeContent = document.querySelector(activeTabHash)!;
const firstContent = document.querySelector(firstTabHash)!;
expect(activeContent.style.display).toBe('none');
expect(firstContent.style.display).not.toBe('none');
expect(indicator.offsetLeft).toBe(firstTab.offsetLeft);
});
it("shouldn't error if tab has no associated content", async () => {
document.querySelector('#test8')?.remove();
const tabNoContent = document.querySelector('[href="#test8"]')!;
expect(tabNoContent.classList.contains('active')).toBe(false);
tabNoContent.click();
await delay(10);
expect(tabNoContent.classList.contains('active')).toBe(true);
});
});
});