@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
316 lines • 16.1 kB
JavaScript
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