@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
233 lines • 10.7 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { createComponent, initializeShadeRoot, Shade } from '@furystack/shades';
import { ObservableValue, sleepAsync, usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ThemeProviderService } from '../services/theme-provider-service.js';
import { CircularProgress } from './circular-progress.js';
const CircularWrapper = Shade({
customElementName: 'test-circular-progress-wrapper',
render: ({ props, useObservable }) => {
const [value] = useObservable('value', props.obs);
return createComponent(CircularProgress, { variant: "determinate", value: value });
},
});
describe('CircularProgress', () => {
let originalAnimate;
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
originalAnimate = Element.prototype.animate;
Element.prototype.animate = vi.fn((_keyframes, _options) => {
const mockAnimation = {
onfinish: null,
oncancel: null,
cancel: vi.fn(),
play: vi.fn(),
pause: vi.fn(),
finish: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
};
return mockAnimation;
});
});
afterEach(() => {
document.body.innerHTML = '';
Element.prototype.animate = originalAnimate;
vi.restoreAllMocks();
});
it('should render as custom element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el).not.toBeNull();
});
});
it('should render an SVG element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const svg = document.querySelector('shade-circular-progress svg');
expect(svg).not.toBeNull();
});
});
it('should render track and progress circles', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const track = document.querySelector('shade-circular-progress .progress-track');
const circle = document.querySelector('shade-circular-progress .progress-circle');
expect(track).not.toBeNull();
expect(circle).not.toBeNull();
});
});
it('should set role="progressbar"', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el.getAttribute('role')).toBe('progressbar');
});
});
describe('size', () => {
it('should use default size of 40px', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const svg = document.querySelector('shade-circular-progress svg');
expect(svg.getAttribute('width')).toBe('40');
expect(svg.getAttribute('height')).toBe('40');
});
});
it('should accept custom size', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, { size: 60 }),
});
await sleepAsync(50);
const svg = document.querySelector('shade-circular-progress svg');
expect(svg.getAttribute('width')).toBe('60');
expect(svg.getAttribute('height')).toBe('60');
});
});
});
describe('determinate variant', () => {
it('should set aria-valuenow for determinate variant', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, { variant: "determinate", value: 50 }),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el.getAttribute('aria-valuenow')).toBe('50');
expect(el.getAttribute('aria-valuemin')).toBe('0');
expect(el.getAttribute('aria-valuemax')).toBe('100');
});
});
it('should clamp value to 0-100 range', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, { variant: "determinate", value: 150 }),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el.getAttribute('aria-valuenow')).toBe('100');
});
});
it('should update stroke-dashoffset when value prop changes', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const obs = new ObservableValue(0);
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularWrapper, { obs: obs }),
});
await sleepAsync(50);
const circle = document.querySelector('shade-circular-progress .progress-circle');
const initialOffset = circle.style.strokeDashoffset;
obs.setValue(75);
await sleepAsync(50);
const updatedOffset = circle.style.strokeDashoffset;
expect(updatedOffset).not.toBe(initialOffset);
});
});
it('should update aria-valuenow when value prop changes', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const obs = new ObservableValue(20);
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularWrapper, { obs: obs }),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el.getAttribute('aria-valuenow')).toBe('20');
obs.setValue(85);
await sleepAsync(50);
expect(el.getAttribute('aria-valuenow')).toBe('85');
});
});
});
describe('indeterminate variant', () => {
it('should default to indeterminate variant', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
expect(el.hasAttribute('aria-valuenow')).toBe(false);
});
});
});
describe('theme integration', () => {
it('should set CSS color variable from theme', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, null),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
const themeService = injector.get(ThemeProviderService);
expect(el.style.getPropertyValue('--circular-progress-color')).toBe(themeService.theme.palette.primary.main);
});
});
it('should use custom color from color prop', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(CircularProgress, { color: "secondary" }),
});
await sleepAsync(50);
const el = document.querySelector('shade-circular-progress');
const themeService = injector.get(ThemeProviderService);
expect(el.style.getPropertyValue('--circular-progress-color')).toBe(themeService.theme.palette.secondary.main);
});
});
});
});
//# sourceMappingURL=circular-progress.spec.js.map