UNPKG

@furystack/shades

Version:

A lightweight UI framework for FuryStack with JSX support

381 lines 16.8 kB
import { Injector } from '@furystack/inject'; import { ObservableValue, usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { initializeShadeRoot } from './initialize.js'; import { createComponent } from './shade-component.js'; import { flushUpdates, Shade } from './shade.js'; describe('useHostProps integration tests', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; }); it('should set data attributes on the host element', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const ExampleComponent = Shade({ customElementName: 'host-props-data-attr-test', render: ({ props, useHostProps }) => { useHostProps({ 'data-variant': props.variant, }); return createComponent("div", null, "content"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, { variant: "primary" }), }); await flushUpdates(); const el = document.querySelector('host-props-data-attr-test'); expect(el).toBeTruthy(); expect(el.getAttribute('data-variant')).toBe('primary'); }); }); it('should set aria attributes on the host element', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const ExampleComponent = Shade({ customElementName: 'host-props-aria-test', render: ({ useHostProps }) => { useHostProps({ role: 'progressbar', 'aria-valuenow': '50', 'aria-valuemin': '0', 'aria-valuemax': '100', }); return createComponent("div", null, "progress"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-aria-test'); expect(el.getAttribute('role')).toBe('progressbar'); expect(el.getAttribute('aria-valuenow')).toBe('50'); expect(el.getAttribute('aria-valuemin')).toBe('0'); expect(el.getAttribute('aria-valuemax')).toBe('100'); }); }); it('should apply CSS custom properties via style', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const ExampleComponent = Shade({ customElementName: 'host-props-css-vars-test', render: ({ useHostProps }) => { useHostProps({ style: { '--my-color': 'red', '--my-size': '16px', }, }); return createComponent("div", null, "styled"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-css-vars-test'); expect(el.style.getPropertyValue('--my-color')).toBe('red'); expect(el.style.getPropertyValue('--my-size')).toBe('16px'); }); }); it('should apply standard inline styles', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const ExampleComponent = Shade({ customElementName: 'host-props-inline-style-test', render: ({ useHostProps }) => { useHostProps({ style: { display: 'flex', gap: '8px', }, }); return createComponent("div", null, "styled"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-inline-style-test'); expect(el.style.display).toBe('flex'); expect(el.style.gap).toBe('8px'); }); }); it('should merge multiple useHostProps calls', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const ExampleComponent = Shade({ customElementName: 'host-props-merge-test', render: ({ useHostProps }) => { useHostProps({ 'data-first': 'one', style: { '--color-a': 'red' }, }); useHostProps({ 'data-second': 'two', style: { '--color-b': 'blue' }, }); return createComponent("div", null, "merged"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-merge-test'); expect(el.getAttribute('data-first')).toBe('one'); expect(el.getAttribute('data-second')).toBe('two'); expect(el.style.getPropertyValue('--color-a')).toBe('red'); expect(el.style.getPropertyValue('--color-b')).toBe('blue'); }); }); it('should remove attributes when they are no longer set on re-render', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const showExtra = new ObservableValue(true); const ExampleComponent = Shade({ customElementName: 'host-props-remove-attr-test', render: ({ useHostProps, useObservable }) => { const [show] = useObservable('showExtra', showExtra); useHostProps({ 'data-always': 'yes', ...(show ? { 'data-extra': 'present' } : {}), }); return createComponent("div", null, "content"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-remove-attr-test'); expect(el.getAttribute('data-always')).toBe('yes'); expect(el.getAttribute('data-extra')).toBe('present'); showExtra.setValue(false); await flushUpdates(); expect(el.getAttribute('data-always')).toBe('yes'); expect(el.getAttribute('data-extra')).toBeNull(); }); }); it('should remove CSS custom properties when they are no longer set', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const showColor = new ObservableValue(true); const ExampleComponent = Shade({ customElementName: 'host-props-remove-css-var-test', render: ({ useHostProps, useObservable }) => { const [show] = useObservable('showColor', showColor); useHostProps({ style: { display: 'block', ...(show ? { '--my-color': 'red' } : {}), }, }); return createComponent("div", null, "content"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-remove-css-var-test'); expect(el.style.getPropertyValue('--my-color')).toBe('red'); expect(el.style.display).toBe('block'); showColor.setValue(false); await flushUpdates(); expect(el.style.getPropertyValue('--my-color')).toBe(''); expect(el.style.display).toBe('block'); }); }); it('should set event handlers on the host element', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); let clicked = false; const ExampleComponent = Shade({ customElementName: 'host-props-event-test', render: ({ useHostProps }) => { useHostProps({ onclick: () => { clicked = true; }, }); return createComponent("div", null, "clickable"); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const el = document.querySelector('host-props-event-test'); el.click(); expect(clicked).toBe(true); }); }); }); describe('useRef integration tests', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; }); it('should set ref.current to the mounted element', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); let capturedRef; const ExampleComponent = Shade({ customElementName: 'use-ref-basic-test', render: ({ useRef }) => { const divRef = useRef('myDiv'); capturedRef = divRef; return (createComponent("div", { ref: divRef, id: "target" }, "hello")); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); expect(capturedRef).toBeTruthy(); expect(capturedRef.current).toBeTruthy(); expect(capturedRef.current).toBe(document.getElementById('target')); expect(capturedRef.current?.textContent).toBe('hello'); }); }); it('should return the same ref object across re-renders', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const counter = new ObservableValue(0); const capturedRefs = []; const ExampleComponent = Shade({ customElementName: 'use-ref-stable-test', render: ({ useRef, useObservable }) => { const [count] = useObservable('counter', counter); const divRef = useRef('myDiv'); capturedRefs.push(divRef); return createComponent("div", { ref: divRef }, count); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); counter.setValue(1); await flushUpdates(); counter.setValue(2); await flushUpdates(); expect(capturedRefs.length).toBe(3); expect(capturedRefs[0]).toBe(capturedRefs[1]); expect(capturedRefs[1]).toBe(capturedRefs[2]); }); }); it('should set ref.current on nested child elements', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); let capturedInputRef; const ExampleComponent = Shade({ customElementName: 'use-ref-nested-test', render: ({ useRef }) => { const inputRef = useRef('input'); capturedInputRef = inputRef; return (createComponent("div", null, createComponent("label", null, createComponent("input", { ref: inputRef, type: "text", id: "my-input" })))); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); expect(capturedInputRef).toBeTruthy(); expect(capturedInputRef.current).toBeTruthy(); expect(capturedInputRef.current).toBe(document.getElementById('my-input')); expect(capturedInputRef.current?.type).toBe('text'); }); }); it('should clear ref.current when element is unmounted', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const showChild = new ObservableValue(true); let capturedRef; const ExampleComponent = Shade({ customElementName: 'use-ref-unmount-test', render: ({ useRef, useObservable }) => { const [show] = useObservable('showChild', showChild); const spanRef = useRef('span'); capturedRef = spanRef; return createComponent("div", null, show ? createComponent("span", { ref: spanRef }, "visible") : null); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); expect(capturedRef.current).toBeTruthy(); expect(capturedRef.current?.textContent).toBe('visible'); showChild.setValue(false); await flushUpdates(); expect(capturedRef.current).toBeNull(); }); }); it('should work with useRef in onChange callbacks', async () => { await usingAsync(new Injector(), async (injector) => { const rootElement = document.getElementById('root'); const counter = new ObservableValue(0); const ExampleComponent = Shade({ customElementName: 'use-ref-onchange-test', render: ({ useRef, useObservable }) => { const spanRef = useRef('counterSpan'); useObservable('counter', counter, { onChange: (value) => { if (spanRef.current) { spanRef.current.textContent = String(value); } }, }); return (createComponent("span", { ref: spanRef, id: "counter-span" }, "0")); }, }); initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(ExampleComponent, null), }); await flushUpdates(); const span = document.getElementById('counter-span'); expect(span?.textContent).toBe('0'); counter.setValue(42); await flushUpdates(); expect(span?.textContent).toBe('42'); }); }); }); //# sourceMappingURL=shade-host-props-ref.integration.spec.js.map