/** @jsxImportSource preact */
import {afterEach, describe, expect, it} from 'vitest';
import {SidebarWidget} from './sidebar-widget';
import type {WidgetPanel} from './widget-containers';
const panel: WidgetPanel = {
id: 'settings',
title: 'Settings',
content:
settings content
};
afterEach(() => {
document.body.innerHTML = '';
});
describe('SidebarWidget', () => {
it('starts open by default when uncontrolled', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-default-open',
panel,
side: 'right'
});
widget.onRenderHTML(root);
expect(root.textContent).toContain('settings content');
expect(root.querySelector('[role="dialog"]')).toBeTruthy();
expect(root.querySelector('header')).toBeNull();
expect(widget.placement).toBe('top-right');
expect(root.style.top).toBe('var(--widget-margin, 12px)');
expect(root.style.bottom).toBe('var(--widget-margin, 12px)');
expect(root.style.left).toBe('var(--widget-margin, 12px)');
expect(root.style.right).toBe('-1px');
expect(root.style.width).toBe('auto');
expect(root.style.height).toBe('auto');
});
it('renders a built-in icon trigger when button is enabled', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar',
panel,
button: true,
icon: 'icon'
});
widget.onRenderHTML(root);
const triggerButton = root.querySelector('[data-sidebar-handle-button]');
expect(triggerButton).toBeTruthy();
expect(triggerButton?.textContent).toContain('›');
});
it('opens the sidebar panel when the built-in icon trigger is clicked', async () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-open',
panel,
button: true,
icon: 'icon',
title: 'Visualization settings',
side: 'right'
});
widget.onRenderHTML(root);
const triggerButton = root.querySelector('[data-sidebar-handle-button]');
triggerButton?.click();
await Promise.resolve();
expect(root.textContent).toContain('settings content');
expect(root.querySelector('[role="dialog"]')).toBeTruthy();
});
it('respects controlled closed state even though defaultOpen is true', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-controlled-closed',
panel,
open: false,
title: 'Visualization settings',
side: 'right'
});
widget.onRenderHTML(root);
const dialog = root.querySelector('[role="dialog"]');
expect(dialog).toBeTruthy();
expect(dialog?.getAttribute('aria-hidden')).toBe('true');
});
it('top-aligns the sidebar handle with the panel shell', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-top-aligned-handle',
panel,
button: true,
icon: 'icon',
title: 'Visualization settings',
side: 'right'
});
widget.onRenderHTML(root);
const shell = root.querySelector('[data-sidebar-shell]');
const handle = root.querySelector('[data-sidebar-handle]');
const handleButton = root.querySelector('[data-sidebar-handle-button]');
expect(shell?.style.alignItems).toBe('flex-start');
expect(shell?.style.position).toBe('absolute');
expect(shell?.style.pointerEvents).toBe('auto');
expect(handle?.style.alignItems).toBe('flex-start');
expect(shell?.style.gap).toBe('8px');
expect(handleButton?.style.width).toBe('36px');
expect(handleButton?.textContent).toContain('›');
});
it('reparents the sidebar root into the full widget container by default', () => {
const overlayRoot = document.createElement('div');
const placementRoot = document.createElement('div');
const widgetRoot = document.createElement('div');
overlayRoot.appendChild(placementRoot);
placementRoot.appendChild(widgetRoot);
document.body.appendChild(overlayRoot);
const widget = new SidebarWidget({
id: 'settings-sidebar-overlay-parent',
panel
});
widget.onRenderHTML(widgetRoot);
expect(widgetRoot.parentElement).toBe(overlayRoot);
expect(widgetRoot.style.zIndex).toBe('35');
});
it('keeps the same overlay parent after open state updates', () => {
const overlayRoot = document.createElement('div');
const placementRoot = document.createElement('div');
const widgetRoot = document.createElement('div');
overlayRoot.appendChild(placementRoot);
placementRoot.appendChild(widgetRoot);
document.body.appendChild(overlayRoot);
const widget = new SidebarWidget({
id: 'settings-sidebar-stable-overlay-parent',
panel,
open: false
});
widget.onRenderHTML(widgetRoot);
expect(widgetRoot.parentElement).toBe(overlayRoot);
widget.setProps({open: true});
expect(widgetRoot.parentElement).toBe(overlayRoot);
});
it('keeps the same shell mounted and only updates transform when open changes', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-animated-shell',
panel,
button: true,
open: false,
side: 'right'
});
widget.onRenderHTML(root);
const closedShell = root.querySelector('[data-sidebar-shell]');
expect(closedShell?.style.transform).toBe('translateX(360px)');
expect(closedShell?.style.transition).toContain('transform 320ms');
widget.setProps({open: true});
const openShell = root.querySelector('[data-sidebar-shell]');
expect(openShell).toBe(closedShell);
expect(openShell?.style.transform).toBe('translateX(0px)');
expect(openShell?.style.transition).toContain('transform 320ms');
});
it('stops mouse move events from leaking past the sidebar shell', () => {
const root = document.createElement('div');
document.body.appendChild(root);
const widget = new SidebarWidget({
id: 'settings-sidebar-stop-mousemove',
panel,
open: true,
side: 'right'
});
let bodyMouseMoveCount = 0;
document.body.addEventListener('mousemove', () => {
bodyMouseMoveCount += 1;
});
widget.onRenderHTML(root);
const shell = root.querySelector('[data-sidebar-shell]');
shell?.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));
expect(bodyMouseMoveCount).toBe(0);
});
});