@zeix/cause-effect
Version:
Cause & Effect - reactive state management with signals.
223 lines (176 loc) • 5.77 kB
text/typescript
import { describe, test, expect } from 'bun:test'
import { isComputed, isState, state, UNSET } from '../'
/* === Utility Functions === */
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
/* === Tests === */
describe('State', function () {
describe("State type guard", () => {
test("isState identifies state signals", () => {
const count = state(42)
expect(isState(count)).toBe(true)
expect(isComputed(count)).toBe(false)
})
})
describe('Boolean cause', function () {
test('should be boolean', function () {
const cause = state(false)
expect(typeof cause.get()).toBe('boolean')
})
test('should set initial value to false', function () {
const cause = state(false)
expect(cause.get()).toBe(false)
})
test('should set initial value to true', function () {
const cause = state(true)
expect(cause.get()).toBe(true)
})
test('should set new value with .set(true)', function () {
const cause = state(false)
cause.set(true)
expect(cause.get()).toBe(true)
})
test('should toggle initial value with .set(v => !v)', function () {
const cause = state(false)
cause.update((v) => !v)
expect(cause.get()).toBe(true)
})
})
describe('Number cause', function () {
test('should be number', function () {
const cause = state(0)
expect(typeof cause.get()).toBe('number')
})
test('should set initial value to 0', function () {
const cause = state(0)
expect(cause.get()).toBe(0)
})
test('should set new value with .set(42)', function () {
const cause = state(0)
cause.set(42)
expect(cause.get()).toBe(42)
})
test('should increment value with .set(v => ++v)', function () {
const cause = state(0)
cause.update(v => ++v)
expect(cause.get()).toBe(1)
})
})
describe('String cause', function () {
test('should be string', function () {
const cause = state('foo')
expect(typeof cause.get()).toBe('string')
})
test('should set initial value to "foo"', function () {
const cause = state('foo')
expect(cause.get()).toBe('foo')
})
test('should set new value with .set("bar")', function () {
const cause = state('foo')
cause.set('bar')
expect(cause.get()).toBe('bar')
})
test('should upper case value with .set(v => v.toUpperCase())', function () {
const cause = state('foo')
cause.update(v => v ? v.toUpperCase() : '')
expect(cause.get()).toBe("FOO")
})
})
describe('Array cause', function () {
test('should be array', function () {
const cause = state([1, 2, 3])
expect(Array.isArray(cause.get())).toBe(true)
})
test('should set initial value to [1, 2, 3]', function () {
const cause = state([1, 2, 3])
expect(cause.get()).toEqual([1, 2, 3])
})
test('should set new value with .set([4, 5, 6])', function () {
const cause = state([1, 2, 3])
cause.set([4, 5, 6])
expect(cause.get()).toEqual([4, 5, 6])
})
test('should reflect current value of array after modification', function () {
const array = [1, 2, 3]
const cause = state(array)
array.push(4); // don't do this! the result will be correct, but we can't trigger effects
expect(cause.get()).toEqual([1, 2, 3, 4])
})
test('should set new value with .set([...array, 4])', function () {
const array = [1, 2, 3]
const cause = state(array)
cause.set([...array, 4]); // use destructuring instead!
expect(cause.get()).toEqual([1, 2, 3, 4])
})
})
describe('Object cause', function () {
test('should be object', function () {
const cause = state({ a: 'a', b: 1 })
expect(typeof cause.get()).toBe('object')
})
test('should set initial value to { a: "a", b: 1 }', function () {
const cause = state({ a: 'a', b: 1 })
expect(cause.get()).toEqual({ a: 'a', b: 1 })
})
test('should set new value with .set({ c: true })', function () {
const cause = state<Record<string, any>>({ a: 'a', b: 1 })
cause.set({ c: true })
expect(cause.get()).toEqual({ c: true })
})
test('should reflect current value of object after modification', function () {
const obj = { a: 'a', b: 1 }
const cause = state<Record<string, any>>(obj)
// @ts-expect-error
obj.c = true; // don't do this! the result will be correct, but we can't trigger effects
expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
})
test('should set new value with .set({...obj, c: true})', function () {
const obj = { a: 'a', b: 1 }
const cause = state<Record<string, any>>(obj)
cause.set({...obj, c: true}); // use destructuring instead!
expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
})
})
describe('Map method', function () {
test('should return a computed signal', function() {
const cause = state(42)
const double = cause.map(v => v * 2)
expect(isComputed(double)).toBe(true)
expect(double.get()).toBe(84)
})
test('should return a computed signal for an async function', async function() {
const cause = state(42)
const asyncDouble = cause.map(async value => {
await wait(100)
return value * 2
})
expect(isComputed(asyncDouble)).toBe(true)
expect(asyncDouble.get()).toBe(UNSET)
await wait(110)
expect(asyncDouble.get()).toBe(84)
})
})
describe('Tap method', function () {
test('should create an effect that reacts on signal changes', function() {
const cause = state(42)
let okCount = 0
let nilCount = 0
let result = 0
cause.tap({
ok: v => {
result = v
okCount++
},
nil: () => {
nilCount++
}
})
cause.set(43)
expect(okCount).toBe(2); // + 1 for effect initialization
expect(nilCount).toBe(0)
expect(result).toBe(43)
cause.set(UNSET)
expect(okCount).toBe(2)
expect(nilCount).toBe(1)
})
})
});