UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

316 lines 16.1 kB
import { createInjector } from '@furystack/inject'; import { initializeShadeRoot, createComponent, Shade, flushUpdates, SpatialNavigationService } from '@furystack/shades'; import { usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Modal } from './modal.js'; describe('Modal', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; }); describe('visibility', () => { it('should render when isVisible is true', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); expect(document.body.innerHTML).toContain('shade-backdrop'); expect(document.body.innerHTML).toContain('modal-content'); }); }); it('should not render when isVisible is false', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: false }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); expect(document.body.innerHTML).not.toContain('shade-backdrop'); expect(document.body.innerHTML).not.toContain('modal-content'); }); }); it('should show modal when isVisible changes from false to true', async () => { let setVisible; const Wrapper = Shade({ customElementName: 'modal-visibility-test-show', render: ({ useState }) => { const [visible, setter] = useState('visible', false); setVisible = setter; return (createComponent(Modal, { isVisible: visible }, createComponent("div", { id: "modal-content" }, "Modal Content"))); }, }); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Wrapper, null) }); await flushUpdates(); expect(document.body.innerHTML).not.toContain('modal-content'); setVisible(true); await flushUpdates(); expect(document.body.innerHTML).toContain('shade-backdrop'); expect(document.body.innerHTML).toContain('modal-content'); }); }); it('should hide modal when isVisible changes from true to false', async () => { let setVisible; const Wrapper = Shade({ customElementName: 'modal-visibility-test-hide', render: ({ useState }) => { const [visible, setter] = useState('visible', true); setVisible = setter; return (createComponent(Modal, { isVisible: visible }, createComponent("div", { id: "modal-content" }, "Modal Content"))); }, }); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Wrapper, null) }); await flushUpdates(); expect(document.body.innerHTML).toContain('modal-content'); setVisible(false); await flushUpdates(); expect(document.body.innerHTML).not.toContain('shade-backdrop'); expect(document.body.innerHTML).not.toContain('modal-content'); }); }); }); describe('backdrop click', () => { it('should call onClose when backdrop is clicked', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const onClose = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, onClose: onClose }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); expect(backdrop).not.toBeNull(); backdrop.click(); await flushUpdates(); expect(onClose).toHaveBeenCalledTimes(1); }); }); it('should call hideAnimation before onClose when backdrop is clicked', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const callOrder = []; const hideAnimation = vi.fn(async () => { callOrder.push('hideAnimation'); }); const onClose = vi.fn(() => { callOrder.push('onClose'); }); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, onClose: onClose, hideAnimation: hideAnimation }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); backdrop.click(); await flushUpdates(); expect(hideAnimation).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1); expect(callOrder).toEqual(['hideAnimation', 'onClose']); }); }); }); describe('animations', () => { it('should call showAnimation when modal becomes visible', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const showAnimation = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, showAnimation: showAnimation }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); expect(showAnimation).toHaveBeenCalled(); }); }); it('should call showAnimation with element when visibility changes to true', async () => { let setVisible; const showAnimation = vi.fn(); const Wrapper = Shade({ customElementName: 'modal-show-animation-test', render: ({ useState }) => { const [visible, setter] = useState('visible', false); setVisible = setter; return (createComponent(Modal, { isVisible: visible, showAnimation: showAnimation }, createComponent("div", { id: "modal-content" }, "Modal Content"))); }, }); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Wrapper, null) }); await flushUpdates(); expect(showAnimation).not.toHaveBeenCalled(); setVisible(true); await flushUpdates(); expect(showAnimation).toHaveBeenCalled(); expect(showAnimation.mock.calls[0][0]).toBeInstanceOf(Element); }); }); it('should call hideAnimation with element on backdrop click', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const hideAnimation = vi.fn(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, hideAnimation: hideAnimation }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); backdrop.click(); await flushUpdates(); expect(hideAnimation).toHaveBeenCalled(); expect(hideAnimation.mock.calls[0][0]).toBeInstanceOf(Element); }); }); }); describe('styling', () => { it('should apply custom backdropStyle', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const backdropStyle = { backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '1000', }; initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, backdropStyle: backdropStyle }, createComponent("div", { id: "modal-content" }, "Modal Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); expect(backdrop).not.toBeNull(); expect(backdrop.style.backgroundColor).toBe('rgba(0, 0, 0, 0.5)'); expect(backdrop.style.zIndex).toBe('1000'); }); }); }); describe('children', () => { it('should render children inside the backdrop', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true }, createComponent("div", { id: "child-1" }, "First Child"), createComponent("div", { id: "child-2" }, "Second Child"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); expect(backdrop).not.toBeNull(); expect(backdrop.innerHTML).toContain('child-1'); expect(backdrop.innerHTML).toContain('child-2'); }); }); }); describe('spatial navigation', () => { it('should render with data-nav-section attribute when visible', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true }, createComponent("div", null, "Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); expect(backdrop?.getAttribute('data-nav-section')).toBeTruthy(); }); }); it('should render with custom navSection name', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, navSection: "my-modal" }, createComponent("div", null, "Content"))), }); await flushUpdates(); const backdrop = document.querySelector('.shade-backdrop'); expect(backdrop?.getAttribute('data-nav-section')).toBe('my-modal'); }); }); it('should push focus trap when trapFocus is true and service is active', async () => { await usingAsync(createInjector(), async (injector) => { const spatialNav = injector.get(SpatialNavigationService); const pushSpy = vi.spyOn(spatialNav, 'pushFocusTrap'); const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, trapFocus: true, navSection: "trapped-modal" }, createComponent("div", null, "Content"))), }); await flushUpdates(); expect(pushSpy).toHaveBeenCalledWith('trapped-modal'); expect(spatialNav.activeSection.getValue()).toBe('trapped-modal'); }); }); it('should not push focus trap when trapFocus is false', async () => { await usingAsync(createInjector(), async (injector) => { const spatialNav = injector.get(SpatialNavigationService); const pushSpy = vi.spyOn(spatialNav, 'pushFocusTrap'); const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(Modal, { isVisible: true, trapFocus: false }, createComponent("div", null, "Content"))), }); await flushUpdates(); expect(pushSpy).not.toHaveBeenCalled(); spatialNav.activeSection.setValue('other-section'); expect(spatialNav.activeSection.getValue()).toBe('other-section'); }); }); it('should pop focus trap when visibility changes from true to false', async () => { let setVisible; const Wrapper = Shade({ customElementName: 'modal-trap-visibility-test', render: ({ useState }) => { const [visible, setter] = useState('visible', true); setVisible = setter; return (createComponent(Modal, { isVisible: visible, trapFocus: true, navSection: "trap-test" }, createComponent("div", null, "Content"))); }, }); await usingAsync(createInjector(), async (injector) => { const spatialNav = injector.get(SpatialNavigationService); const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Wrapper, null) }); await flushUpdates(); expect(spatialNav.activeSection.getValue()).toBe('trap-test'); setVisible(false); await flushUpdates(); expect(spatialNav.activeSection.getValue()).not.toBe('trap-test'); }); }); }); }); //# sourceMappingURL=modal.spec.js.map