@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
379 lines • 20.2 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { createComponent, flushUpdates, initializeShadeRoot, Shade } from '@furystack/shades';
import { ObservableValue, usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DataGridHeader, OrderButton } from './header.js';
const OrderButtonWrapper = Shade({
customElementName: 'test-order-button-wrapper',
render: ({ props, useObservable }) => {
const [findOptions, setFindOptions] = useObservable('findOptions', props.obs);
return createComponent(OrderButton, { field: props.field, findOptions: findOptions, onFindOptionsChange: setFindOptions });
},
});
const HeaderWrapper = Shade({
customElementName: 'test-header-wrapper',
render: ({ props, useObservable }) => {
const [findOptions, setFindOptions] = useObservable('findOptions', props.obs);
return (createComponent(DataGridHeader, { field: props.field, findOptions: findOptions, onFindOptionsChange: setFindOptions, filterConfig: props.filterConfig }));
},
});
describe('DataGridHeader', () => {
let originalAnimate;
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
originalAnimate = Element.prototype.animate;
Element.prototype.animate = vi.fn(() => {
const mockAnimation = {
onfinish: null,
oncancel: null,
cancel: vi.fn(),
play: vi.fn(),
pause: vi.fn(),
finish: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
};
setTimeout(() => {
mockAnimation.onfinish?.({});
}, 0);
return mockAnimation;
});
});
afterEach(() => {
document.body.innerHTML = '';
Element.prototype.animate = originalAnimate;
});
describe('rendering', () => {
it('should render with custom element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const header = document.querySelector('data-grid-header');
expect(header).not.toBeNull();
});
});
it('should render field name', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const header = document.querySelector('data-grid-header');
const fieldName = header?.querySelector('.header-field-name');
expect(fieldName?.textContent).toBe('name');
});
});
it('should render order button', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const orderButton = document.querySelector('data-grid-order-button');
expect(orderButton).not.toBeNull();
});
});
it('should not render filter button when no filterConfig is provided', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button');
expect(filterButton).toBeNull();
});
});
it('should render filter button when filterConfig is provided', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'string' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button');
expect(filterButton).not.toBeNull();
});
});
});
describe('OrderButton', () => {
it('should show neutral icon when no order is set', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(OrderButton, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const button = document.querySelector('data-grid-order-button');
expect(button?.querySelector('shade-icon')).not.toBeNull();
});
});
it('should call onFindOptionsChange with ASC when clicking on unsorted field', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(OrderButton, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange }),
});
await flushUpdates();
const button = document.querySelector('data-grid-order-button')?.querySelector('button');
button?.click();
await flushUpdates();
expect(onFindOptionsChange).toHaveBeenCalledWith(expect.objectContaining({ order: { name: 'ASC' } }));
});
});
it('should call onFindOptionsChange with DESC when clicking on ASC-sorted field', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(OrderButton, { field: "name", findOptions: { order: { name: 'ASC' } }, onFindOptionsChange: onFindOptionsChange })),
});
await flushUpdates();
const button = document.querySelector('data-grid-order-button')?.querySelector('button');
button?.click();
await flushUpdates();
expect(onFindOptionsChange).toHaveBeenCalledWith(expect.objectContaining({ order: { name: 'DESC' } }));
});
});
it('should call onFindOptionsChange with ASC when clicking on DESC-sorted field', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(OrderButton, { field: "name", findOptions: { order: { name: 'DESC' } }, onFindOptionsChange: onFindOptionsChange })),
});
await flushUpdates();
const button = document.querySelector('data-grid-order-button')?.querySelector('button');
button?.click();
await flushUpdates();
expect(onFindOptionsChange).toHaveBeenCalledWith(expect.objectContaining({ order: { name: 'ASC' } }));
});
});
it('should react to external findOptions changes', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const obs = new ObservableValue({});
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(OrderButtonWrapper, { obs: obs, field: "name" }),
});
await flushUpdates();
let button = document.querySelector('data-grid-order-button');
expect(button?.querySelector('shade-icon')).not.toBeNull();
obs.setValue({ order: { name: 'ASC' } });
await flushUpdates();
button = document.querySelector('data-grid-order-button');
expect(button?.querySelector('shade-icon')).not.toBeNull();
});
});
});
describe('FilterButton', () => {
it('should show inactive state when no filter is set', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'string' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button');
expect(filterButton?.querySelector('shade-icon')).not.toBeNull();
});
});
it('should show active state when filter is set for field', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: { filter: { name: { $regex: 'test' } } }, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'string' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button');
expect(filterButton?.querySelector('shade-icon')).not.toBeNull();
});
});
it('should transition from active to inactive when filter is externally cleared', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const obs = new ObservableValue({
filter: { name: { $regex: 'test' } },
});
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(HeaderWrapper, { obs: obs, field: "name", filterConfig: { type: 'string' } }),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button button');
expect(filterButton?.hasAttribute('data-selected')).toBe(true);
obs.setValue({ filter: {} });
await flushUpdates();
const updatedButton = document.querySelector('data-grid-filter-button button');
expect(updatedButton?.hasAttribute('data-selected')).toBe(false);
});
});
it('should open filter dropdown when clicked', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'string' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const dropdown = document.querySelector('data-grid-filter-dropdown');
expect(dropdown).not.toBeNull();
});
});
});
describe('filter type routing', () => {
it('should render StringFilter for string filterConfig', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'string' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const stringFilter = document.querySelector('data-grid-string-filter');
expect(stringFilter).not.toBeNull();
});
});
it('should render NumberFilter for number filterConfig', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "id", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'number' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const numberFilter = document.querySelector('data-grid-number-filter');
expect(numberFilter).not.toBeNull();
});
});
it('should render BooleanFilter for boolean filterConfig', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'boolean' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const booleanFilter = document.querySelector('data-grid-boolean-filter');
expect(booleanFilter).not.toBeNull();
});
});
it('should render EnumFilter for enum filterConfig', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'enum', values: [{ label: 'A', value: 'a' }] } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const enumFilter = document.querySelector('data-grid-enum-filter');
expect(enumFilter).not.toBeNull();
});
});
it('should render DateFilter for date filterConfig', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onFindOptionsChange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(DataGridHeader, { field: "name", findOptions: {}, onFindOptionsChange: onFindOptionsChange, filterConfig: { type: 'date' } })),
});
await flushUpdates();
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const dateFilter = document.querySelector('data-grid-date-filter');
expect(dateFilter).not.toBeNull();
});
});
});
describe('integration', () => {
it('should support both sorting and filtering simultaneously', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const obs = new ObservableValue({});
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(HeaderWrapper, { obs: obs, field: "name", filterConfig: { type: 'string' } }),
});
await flushUpdates();
const orderButton = document.querySelector('data-grid-order-button')?.querySelector('button');
orderButton?.click();
await flushUpdates();
expect(obs.getValue().order).toEqual({ name: 'ASC' });
const filterButton = document.querySelector('data-grid-filter-button')?.querySelector('button');
filterButton?.click();
await flushUpdates();
const dropdown = document.querySelector('data-grid-filter-dropdown');
expect(dropdown).not.toBeNull();
});
});
});
});
//# sourceMappingURL=header.spec.js.map