@zeix/cause-effect
Version:
Cause & Effect - reactive state management primitives library for TypeScript.
293 lines (249 loc) • 6.59 kB
text/typescript
import { describe, expect, test } from 'bun:test'
import {
createEffect,
createScope,
createState,
unown,
} from '../index.ts'
describe('createScope with root option', () => {
test('root scope is not registered on enclosing scope', () => {
let rootCleanupRan = false
const outerDispose = createScope(() => {
createScope(() => {
return () => { rootCleanupRan = true }
}, { root: true })
})
outerDispose()
expect(rootCleanupRan).toBe(false)
})
test('root scope is not registered on enclosing effect', () => {
const trigger = createState(0)
let rootCleanupRuns = 0
const outerDispose = createScope(() => {
createEffect((): undefined => {
trigger.get()
createScope(() => {
return () => { rootCleanupRuns++ }
}, { root: true })
})
})
expect(rootCleanupRuns).toBe(0)
trigger.set(1)
expect(rootCleanupRuns).toBe(0)
trigger.set(2)
expect(rootCleanupRuns).toBe(0)
outerDispose()
})
test('effects inside a root survive outer effect re-runs (Le Truc regression)', () => {
const listChange = createState(0)
let componentEffectRuns = 0
let componentCleanupRuns = 0
const connectComponent = () =>
createScope(() => {
createEffect((): undefined => {
componentEffectRuns++
})
return () => { componentCleanupRuns++ }
}, { root: true })
const outerDispose = createScope(() => {
createEffect((): undefined => {
listChange.get()
if (listChange.get() === 0) connectComponent()
})
})
expect(componentEffectRuns).toBe(1)
expect(componentCleanupRuns).toBe(0)
listChange.set(1)
expect(componentEffectRuns).toBe(1)
expect(componentCleanupRuns).toBe(0)
outerDispose()
})
test('dispose returned from root scope still works', () => {
let cleanupRan = false
const dispose = createScope(() => {
return () => { cleanupRan = true }
}, { root: true })
expect(cleanupRan).toBe(false)
dispose()
expect(cleanupRan).toBe(true)
})
test('effects inside root still run reactively', () => {
const source = createState('a')
let effectRuns = 0
const dispose = createScope(() => {
createEffect((): undefined => {
source.get()
effectRuns++
})
}, { root: true })
expect(effectRuns).toBe(1)
source.set('b')
expect(effectRuns).toBe(2)
dispose()
source.set('c')
expect(effectRuns).toBe(2)
})
test('activeOwner is restored correctly even when fn throws', () => {
let postCleanupRan = false
const outerDispose = createScope(() => {
try {
createScope(() => { throw new Error('boom') }, { root: true })
} catch {
// swallow
}
createScope(() => {
return () => { postCleanupRan = true }
})
})
outerDispose()
expect(postCleanupRan).toBe(true)
})
})
describe('unown', () => {
test('should return the value of the callback', () => {
const result = unown(() => 42)
expect(result).toBe(42)
})
test('should run the callback immediately and synchronously', () => {
let ran = false
unown(() => { ran = true })
expect(ran).toBe(true)
})
test('scope created inside unown is not registered on the enclosing scope', () => {
let innerCleanupRan = false
const outerDispose = createScope(() => {
unown(() => {
createScope(() => {
return () => { innerCleanupRan = true }
})
})
})
outerDispose()
expect(innerCleanupRan).toBe(false)
})
test('scope created inside unown is not registered on the enclosing effect', () => {
const trigger = createState(0)
let innerCleanupRuns = 0
const outerDispose = createScope(() => {
createEffect((): undefined => {
trigger.get()
unown(() => {
createScope(() => {
return () => { innerCleanupRuns++ }
})
})
})
})
expect(innerCleanupRuns).toBe(0)
trigger.set(1)
expect(innerCleanupRuns).toBe(0)
trigger.set(2)
expect(innerCleanupRuns).toBe(0)
outerDispose()
})
test('effects inside an unowned scope survive effect re-runs (ownership bug regression)', () => {
const listChange = createState(0)
let componentEffectRuns = 0
let componentCleanupRuns = 0
const connectComponent = () => unown(() =>
createScope(() => {
createEffect((): undefined => {
componentEffectRuns++
})
return () => { componentCleanupRuns++ }
})
)
const outerDispose = createScope(() => {
createEffect((): undefined => {
listChange.get()
if (listChange.get() === 0) connectComponent()
})
})
expect(componentEffectRuns).toBe(1)
expect(componentCleanupRuns).toBe(0)
listChange.set(1)
expect(componentEffectRuns).toBe(1)
expect(componentCleanupRuns).toBe(0)
outerDispose()
})
test('effects inside an unowned scope still run reactively', () => {
const source = createState('a')
let effectRuns = 0
const dispose = unown(() =>
createScope(() => {
createEffect((): undefined => {
source.get()
effectRuns++
})
})
)
expect(effectRuns).toBe(1)
source.set('b')
expect(effectRuns).toBe(2)
dispose()
source.set('c')
expect(effectRuns).toBe(2)
})
test('dispose returned from an unowned scope still works', () => {
let cleanupRan = false
const dispose = unown(() =>
createScope(() => {
return () => { cleanupRan = true }
})
)
expect(cleanupRan).toBe(false)
dispose()
expect(cleanupRan).toBe(true)
})
test('nested unown calls work correctly', () => {
let innerCleanupRan = false
const outerDispose = createScope(() => {
unown(() => {
unown(() => {
createScope(() => {
return () => { innerCleanupRan = true }
})
})
})
})
outerDispose()
expect(innerCleanupRan).toBe(false)
})
test('restores the active owner after the callback completes', () => {
let postCleanupRan = false
const outerDispose = createScope(() => {
unown(() => { /* some unowned work */ })
createScope(() => {
return () => { postCleanupRan = true }
})
})
outerDispose()
expect(postCleanupRan).toBe(true)
})
test('restores the active owner even if the callback throws', () => {
let postCleanupRan = false
const outerDispose = createScope(() => {
try {
unown(() => { throw new Error('boom') })
} catch {
// swallow
}
createScope(() => {
return () => { postCleanupRan = true }
})
})
outerDispose()
expect(postCleanupRan).toBe(true)
})
test('works correctly when called outside any scope or effect', () => {
let ran = false
const dispose = unown(() => {
ran = true
return createScope(() => {
return () => {}
})
})
expect(ran).toBe(true)
expect(typeof dispose).toBe('function')
})
})