@furystack/shades
Version:
A lightweight UI framework for FuryStack with JSX support
381 lines • 16.8 kB
JavaScript
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