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 = '
' }) 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') as HTMLDivElement const ExampleComponent = Shade<{ variant: string }>({ customElementName: 'host-props-data-attr-test', render: ({ props, useHostProps }) => { useHostProps({ 'data-variant': props.variant, }) return
content
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-data-attr-test') as HTMLElement 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') as HTMLDivElement const ExampleComponent = Shade({ customElementName: 'host-props-aria-test', render: ({ useHostProps }) => { useHostProps({ role: 'progressbar', 'aria-valuenow': '50', 'aria-valuemin': '0', 'aria-valuemax': '100', }) return
progress
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-aria-test') as HTMLElement 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') as HTMLDivElement const ExampleComponent = Shade({ customElementName: 'host-props-css-vars-test', render: ({ useHostProps }) => { useHostProps({ style: { '--my-color': 'red', '--my-size': '16px', }, }) return
styled
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-css-vars-test') as HTMLElement 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') as HTMLDivElement const ExampleComponent = Shade({ customElementName: 'host-props-inline-style-test', render: ({ useHostProps }) => { useHostProps({ style: { display: 'flex', gap: '8px', }, }) return
styled
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-inline-style-test') as HTMLElement 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') as HTMLDivElement 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
merged
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-merge-test') as HTMLElement 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') as HTMLDivElement 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
content
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-remove-attr-test') as HTMLElement 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') as HTMLDivElement 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
content
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-remove-css-var-test') as HTMLElement 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') as HTMLDivElement let clicked = false const ExampleComponent = Shade({ customElementName: 'host-props-event-test', render: ({ useHostProps }) => { useHostProps({ onclick: () => { clicked = true }, }) return
clickable
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const el = document.querySelector('host-props-event-test') as HTMLElement el.click() expect(clicked).toBe(true) }) }) }) describe('useRef integration tests', () => { beforeEach(() => { document.body.innerHTML = '
' }) 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') as HTMLDivElement let capturedRef: { readonly current: HTMLDivElement | null } | undefined const ExampleComponent = Shade({ customElementName: 'use-ref-basic-test', render: ({ useRef }) => { const divRef = useRef('myDiv') capturedRef = divRef return (
hello
) }, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) 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') as HTMLDivElement const counter = new ObservableValue(0) const capturedRefs: Array<{ readonly current: Element | null }> = [] const ExampleComponent = Shade({ customElementName: 'use-ref-stable-test', render: ({ useRef, useObservable }) => { const [count] = useObservable('counter', counter) const divRef = useRef('myDiv') capturedRefs.push(divRef) return
{count}
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) 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') as HTMLDivElement let capturedInputRef: { readonly current: HTMLInputElement | null } | undefined const ExampleComponent = Shade({ customElementName: 'use-ref-nested-test', render: ({ useRef }) => { const inputRef = useRef('input') capturedInputRef = inputRef return (
) }, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) 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') as HTMLDivElement const showChild = new ObservableValue(true) let capturedRef: { readonly current: HTMLSpanElement | null } | undefined const ExampleComponent = Shade({ customElementName: 'use-ref-unmount-test', render: ({ useRef, useObservable }) => { const [show] = useObservable('showChild', showChild) const spanRef = useRef('span') capturedRef = spanRef return
{show ? visible : null}
}, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) 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') as HTMLDivElement 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 ( 0 ) }, }) initializeShadeRoot({ injector, rootElement, jsxElement: , }) await flushUpdates() const span = document.getElementById('counter-span') expect(span?.textContent).toBe('0') counter.setValue(42) await flushUpdates() expect(span?.textContent).toBe('42') }) }) })