@zeix/cause-effect
Version:
Cause & Effect - reactive state management with signals.
199 lines (171 loc) • 4.44 kB
text/typescript
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)
})
})