import { describe, it, expect, beforeEach, afterEach } from 'vitest' import { clearAllWidgetStores, createWidgetStore, } from './widget-store-registry' import type { Transform, WidgetStoreApi } from './types' const t = ( over: Partial & { id: string; fn: Transform['fn']; order: number }, ): Transform => ({ type: 'data', enabled: true, ...over, }) describe('pipeline-middleware', () => { let store: WidgetStoreApi beforeEach(() => { store = createWidgetStore('pm-test', { data: 1 }) }) afterEach(() => { clearAllWidgetStores() }) it('runs the data pipeline when rawData changes', () => { store.setState({ dataTransforms: [ t({ id: 'd', type: 'data', order: 1, fn: (n) => (n as number) * 2 }), ], }) store.setState({ rawData: 5 }) expect(store.getState().data).toBe(10) }) it('skips the pipeline when neither rawData nor dataTransforms change', () => { let runs = 0 store.setState({ dataTransforms: [ t({ id: 'd', type: 'data', order: 1, fn: (n) => { runs++ return n }, }), ], }) const baseline = runs store.setState({ isLoading: true }) expect(runs).toBe(baseline) }) it('sets the error field when a transform throws', () => { const bad = t({ id: 'bad', type: 'data', order: 1, fn: () => { throw new Error('pipeline-throw') }, }) store.setState({ dataTransforms: [bad], rawData: 1 }) expect(store.getState().error).toBeInstanceOf(Error) expect((store.getState().error as Error).message).toBe('pipeline-throw') }) it('clears a stale error once a clean pipeline run completes', () => { // First run throws and stamps `error` into state. const bad = t({ id: 'bad', type: 'data', order: 1, fn: () => { throw new Error('one-shot-throw') }, }) store.setState({ dataTransforms: [bad], rawData: 1 }) expect(store.getState().error).toBeInstanceOf(Error) // Next pipeline run completes cleanly — the prior error must be cleared. const ok = t({ id: 'ok', type: 'data', order: 1, fn: (n) => n, }) store.setState({ dataTransforms: [ok], rawData: 2 }) expect(store.getState().error).toBeUndefined() }) it('preserves a consumer-set error when a clean pipeline runs (e.g. action mount adds dataTransforms)', () => { // Regression: an action like StackToggle mounts and registers a // dataTransforms entry; that triggers a pipeline run with no throw. // The consumer-supplied error (Provider's `error` prop) must survive. const consumerError = new Error('fetch failed') store.setState({ error: consumerError }) // Action mount: a new dataTransforms array reference flows in. store.setState({ dataTransforms: [ t({ id: 'noop', type: 'data', order: 1, fn: (n) => n, }), ], }) expect(store.getState().error).toBe(consumerError) }) it('clears a consumer-set error when the consumer explicitly writes a new one mid-pipeline-run', () => { const initial = new Error('first') const next = new Error('second') store.setState({ error: initial }) // Consumer writes `error` AND triggers the pipeline in the same call. store.setState({ error: next, rawData: 99, }) expect(store.getState().error).toBe(next) }) it('preserves the first error when multiple transforms throw', () => { const a = t({ id: 'a', type: 'data', order: 1, fn: () => { throw new Error('first') }, }) const b = t({ id: 'b', type: 'data', order: 2, fn: () => { throw new Error('second') }, }) store.setState({ dataTransforms: [a, b], rawData: 1 }) expect((store.getState().error as Error).message).toBe('first') }) it('passes incoming non-pipeline updates through unchanged', () => { store.setState({ isFetching: true, isLoading: true }) expect(store.getState().isFetching).toBe(true) expect(store.getState().isLoading).toBe(true) }) it('runs the data pipeline when dataTransforms reference changes', () => { const id1 = t({ id: 'a', type: 'data', order: 1, fn: (n) => (n as number) + 1, }) store.setState({ dataTransforms: [id1] }) expect(store.getState().data).toBe(2) const id2 = t({ id: 'a', type: 'data', order: 1, fn: (n) => (n as number) + 100, }) store.setState({ dataTransforms: [id2] }) expect(store.getState().data).toBe(101) }) it('one set call → one render — atomic raw + derived', () => { let renders = 0 const unsub = store.subscribe(() => { renders++ }) store.setState({ rawData: 5, dataTransforms: [ t({ id: 'd', type: 'data', order: 1, fn: (n) => (n as number) * 2 }), ], }) expect(renders).toBe(1) expect(store.getState().data).toBe(10) unsub() }) })