You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/components/timeline/timeline-provider.svelte.te...

180 lines
6.2 KiB

// @vitest-environment jsdom
import { tick } from 'svelte';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createActiveDom } from '$adom';
import { state } from '$libs/reactive';
import type { Morfo } from '$uix/morfo';
import { Soma } from '$soma/core/soma.svelte';
import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte';
import { TimelineItemProvider, TimelineProvider } from './timeline-provider.svelte';
function withEffectRoot<T>(fn: () => T): { result: T; cleanup: () => void } {
let result!: T;
const cleanup = $effect.root(() => {
result = fn();
});
return { result, cleanup };
}
function installSomaHarness() {
const dom = createActiveDom();
const soma = {
dom,
langs: {
ts: vi.fn(() => 'Timeline')
},
runtime: (morfo: Morfo, sources: Omit<SomaRuntimeSources, 'dom' | 'eventEngine'>) =>
createSomaRuntime(morfo, {
dom,
translate: (key) => key,
...sources
})
} as unknown as Soma;
vi.spyOn(Soma, 'require').mockReturnValue(soma);
vi.spyOn(TimelineProvider.ctx, 'set').mockImplementation((value) => value);
vi.spyOn(TimelineItemProvider.ctx, 'set').mockImplementation((value) => value);
return { dom };
}
function timelineOpts(active: number | null, live = false, root = document.createElement('ol')) {
return {
id: state('timeline-root'),
ref: state<HTMLElement | null>(root),
active: state<number | null>(active),
live: state(live),
ariaLabel: state<string | undefined>(undefined)
};
}
function itemOpts(index: number | undefined, opts?: { status?: 'done' | 'current' | 'upcoming' }) {
return {
id: state(`timeline-item-${index ?? 'auto'}`),
ref: state<HTMLElement | null>(document.createElement('li')),
index: state<number | undefined>(index),
status: state<'done' | 'current' | 'upcoming' | undefined>(opts?.status),
intent: state<'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss' | undefined>(undefined)
};
}
describe('TimelineProvider', () => {
afterEach(() => {
vi.restoreAllMocks();
document.body.innerHTML = '';
});
it('derives item status from the active index and projects runtime attrs', async () => {
const { dom } = installSomaHarness();
const root = document.createElement('ol');
const firstEl = document.createElement('li');
const currentEl = document.createElement('li');
const lastEl = document.createElement('li');
document.body.append(root, firstEl, currentEl, lastEl);
const { result, cleanup } = withEffectRoot(() => {
const provider = TimelineProvider.create(timelineOpts(1, false, root));
vi.spyOn(TimelineProvider, 'require').mockReturnValue(provider);
const first = TimelineItemProvider.create({ ...itemOpts(0), ref: state<HTMLElement | null>(firstEl) });
const current = TimelineItemProvider.create({ ...itemOpts(1), ref: state<HTMLElement | null>(currentEl) });
const last = TimelineItemProvider.create({ ...itemOpts(2), ref: state<HTMLElement | null>(lastEl) });
return { provider, first, current, last };
});
expect(result.first.effectiveStatus).toBe('done');
expect(result.current.effectiveStatus).toBe('current');
expect(result.last.effectiveStatus).toBe('upcoming');
expect(result.current.isCurrent).toBe(true);
// renderProps resolves the morfo attrs into the props object the wrapper
// spreads onto the element; assert the object (no wrapper in this harness).
expect(result.provider.props).toMatchObject({
id: 'timeline-root',
'data-timeline': '',
role: 'list',
'aria-label': 'Timeline'
});
expect(result.first.props).toMatchObject({ 'data-timeline-item': '', 'data-status': 'done' });
expect(result.current.props).toMatchObject({
'data-status': 'current',
'aria-current': 'step'
});
expect(result.last.props).toMatchObject({ 'data-status': 'upcoming' });
expect(result.first.props['aria-current']).toBeUndefined();
await tick();
cleanup();
dom.dispose();
});
it('lets an explicit status override the derived value and tints with intent', async () => {
const { dom } = installSomaHarness();
const root = document.createElement('ol');
const el = document.createElement('li');
document.body.append(root, el);
const { result, cleanup } = withEffectRoot(() => {
const provider = TimelineProvider.create(timelineOpts(2, false, root));
vi.spyOn(TimelineProvider, 'require').mockReturnValue(provider);
// index 0 would derive 'done', but the explicit status wins.
const item = TimelineItemProvider.create({
...itemOpts(0, { status: 'current' }),
ref: state<HTMLElement | null>(el),
intent: state<'neutral' | 'affirm' | 'fulfill' | 'risk' | 'threat' | 'loss' | undefined>('threat')
});
return { provider, item };
});
expect(result.item.effectiveStatus).toBe('current');
expect(result.item.props).toMatchObject({
'data-status': 'current',
'data-intent': 'threat',
'aria-current': 'step'
});
cleanup();
dom.dispose();
});
it('auto-indexes items by registration order when no index is passed', () => {
const { dom } = installSomaHarness();
const root = document.createElement('ol');
const { result, cleanup } = withEffectRoot(() => {
const provider = TimelineProvider.create(timelineOpts(1, false, root));
vi.spyOn(TimelineProvider, 'require').mockReturnValue(provider);
const a = TimelineItemProvider.create(itemOpts(undefined));
const b = TimelineItemProvider.create(itemOpts(undefined));
const c = TimelineItemProvider.create(itemOpts(undefined));
return { a, b, c };
});
expect([result.a.index, result.b.index, result.c.index]).toEqual([0, 1, 2]);
expect(result.a.effectiveStatus).toBe('done');
expect(result.b.effectiveStatus).toBe('current');
expect(result.c.effectiveStatus).toBe('upcoming');
cleanup();
dom.dispose();
});
it('leaves status undefined when neither active nor explicit status is set', () => {
const { dom } = installSomaHarness();
const { result, cleanup } = withEffectRoot(() => {
const provider = TimelineProvider.create(timelineOpts(null));
vi.spyOn(TimelineProvider, 'require').mockReturnValue(provider);
const item = TimelineItemProvider.create(itemOpts(0));
return { item };
});
expect(result.item.effectiveStatus).toBeUndefined();
cleanup();
dom.dispose();
});
});

Powered by TurnKey Linux.