UNPKG

@zeix/cause-effect

Version:

Cause & Effect - reactive state management with signals.

199 lines (171 loc) 4.44 kB
import { describe, test, expect, mock } from 'bun:test' import { state, computed, effect, UNSET } from '../' /* === Utility Functions === */ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) /* === Tests === */ describe('Effect', function () { test('should be triggered after a state change', function() { const cause = state('foo') let count = 0 cause.tap(() => { count++ }) expect(count).toBe(1) cause.set('bar') expect(count).toBe(2) }) test('should be triggered after computed async signals resolve without waterfalls', async function() { const a = computed(async () => { await wait(100) return 10 }) const b = computed(async () => { await wait(100) return 20 }) let result = 0 let count = 0 effect({ signals: [a, b], ok: (aValue, bValue) => { result = aValue + bValue count++ } }) expect(result).toBe(0) expect(count).toBe(0) await wait(110) expect(result).toBe(30) expect(count).toBe(1) }) test('should be triggered repeatedly after repeated state change', async function() { const cause = state(0) let result = 0 let count = 0 cause.tap(res => { result = res count++ }) for (let i = 0; i < 10; i++) { cause.set(i) expect(result).toBe(i) expect(count).toBe(i + 1); // + 1 for effect initialization } }) test('should handle errors in effects', function() { const a = state(1) const b = a.map(v => { if (v > 5) throw new Error('Value too high') return v * 2 }) let normalCallCount = 0 let errorCallCount = 0 b.tap({ ok: () => { // console.log('Normal effect:', value) normalCallCount++ }, err: error => { // console.log('Error effect:', error) errorCallCount++ expect(error.message).toBe('Value too high') } }) // Normal case a.set(2) expect(normalCallCount).toBe(2) expect(errorCallCount).toBe(0) // Error case a.set(6) expect(normalCallCount).toBe(2) expect(errorCallCount).toBe(1) // Back to normal a.set(3) expect(normalCallCount).toBe(3) expect(errorCallCount).toBe(1) }) test('should handle UNSET values in effects', async function() { const a = computed(async () => { await wait(100) return 42 }) let normalCallCount = 0 let nilCount = 0 a.tap({ ok: aValue => { normalCallCount++ expect(aValue).toBe(42) }, nil: () => { nilCount++ } }) expect(normalCallCount).toBe(0) expect(nilCount).toBe(1) expect(a.get()).toBe(UNSET) await wait(110) expect(normalCallCount).toBe(1) expect(nilCount).toBe(1) expect(a.get()).toBe(42) }) test('should log error to console when error is not handled', () => { // Mock console.error const originalConsoleError = console.error const mockConsoleError = mock(() => {}) console.error = mockConsoleError try { const a = state(1) const b = a.map(v => { if (v > 5) throw new Error('Value too high') return v * 2 }) // Create an effect without explicit error handling b.tap(() => {}) // This should trigger the error a.set(6) // Check if console.error was called with the error expect(mockConsoleError).toHaveBeenCalledWith( expect.any(Error) ) // Check the error message const error = (mockConsoleError as ReturnType<typeof mock>).mock.calls[0][0] as Error expect(error.message).toBe('Value too high') } finally { // Restore the original console.error console.error = originalConsoleError } }) test('should clean up subscriptions when disposed', () => { const count = state(42) let received = 0 const cleanup = count.tap(value => { received = value }) count.set(43) expect(received).toBe(43) cleanup() count.set(44) expect(received).toBe(43) // Should not update after dispose }) test('should detect and throw error for circular dependencies in effects', () => { let okCount = 0 let errCount = 0 const count = state(0) count.tap({ ok: () => { okCount++ // This effect updates the signal it depends on, creating a circular dependency count.update(v => ++v) }, err: e => { errCount++ expect(e).toBeInstanceOf(Error) expect(e.message).toBe('Circular dependency in effect detected') } }) // Verify that the count was changed only once due to the circular dependency error expect(count.get()).toBe(1) expect(okCount).toBe(1) expect(errCount).toBe(1) }) })