mirror of
https://github.com/littensy/charm.git
synced 2026-10-20 17:51:37 +00:00
674 lines
14 KiB
Text
674 lines
14 KiB
Text
local Charm = require("../src")
|
|
local test = require("../../../tests/test")
|
|
|
|
local computed = Charm.computed
|
|
local effect = Charm.effect
|
|
local effectScope = Charm.effectScope
|
|
local endBatch = Charm.endBatch
|
|
local onCleanup = Charm.onCleanup
|
|
local setActiveSub = Charm.setActiveSub
|
|
local signal = Charm.signal
|
|
local startBatch = Charm.startBatch
|
|
|
|
test("should clear subscriptions when untracked by all subscribers", function()
|
|
local bRunTimes = 0
|
|
|
|
local a, setA = signal(1)
|
|
local b = computed(function()
|
|
bRunTimes += 1
|
|
return a() * 2
|
|
end)
|
|
local stopEffect = effect(function()
|
|
b()
|
|
end)
|
|
|
|
assert(bRunTimes == 1, "should run initially")
|
|
setA(2)
|
|
assert(bRunTimes == 2, "should run after change")
|
|
stopEffect()
|
|
setA(3)
|
|
assert(bRunTimes == 2, "should not run after stop")
|
|
end)
|
|
|
|
test("should not run untracked inner effect", function()
|
|
local a, setA = signal(3)
|
|
local b = computed(function()
|
|
return a() > 0
|
|
end)
|
|
|
|
effect(function()
|
|
if b() then
|
|
effect(function()
|
|
if a() == 0 then
|
|
error("bad")
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
|
|
setA(2)
|
|
setA(1)
|
|
setA(0)
|
|
end)
|
|
|
|
test("should run outer effect first", function()
|
|
local a, setA = signal(1)
|
|
local b, setB = signal(1)
|
|
|
|
effect(function()
|
|
if a() > 0 then
|
|
effect(function()
|
|
b()
|
|
if a() == 0 then
|
|
error("bad")
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
|
|
startBatch()
|
|
setB(0)
|
|
setA(0)
|
|
endBatch()
|
|
end)
|
|
|
|
test("should not trigger inner effect when resolve maybe dirty", function()
|
|
local a, setA = signal(0)
|
|
local b = computed(function()
|
|
return a() % 2
|
|
end)
|
|
|
|
local innerTriggerTimes = 0
|
|
|
|
effect(function()
|
|
effect(function()
|
|
b()
|
|
innerTriggerTimes += 1
|
|
if innerTriggerTimes >= 2 then
|
|
error("bad")
|
|
end
|
|
end)
|
|
end)
|
|
|
|
setA(2)
|
|
end)
|
|
|
|
test("should notify inner effects in the same order as non-inner effects", function()
|
|
local getA, setA = signal(0)
|
|
local getB, setB = signal(0)
|
|
local c = computed(function()
|
|
return getA() - getB()
|
|
end)
|
|
local order1 = {}
|
|
local order2 = {}
|
|
local order3 = {}
|
|
|
|
effect(function()
|
|
table.insert(order1, "effect1")
|
|
getA()
|
|
end)
|
|
effect(function()
|
|
table.insert(order1, "effect2")
|
|
getA()
|
|
getB()
|
|
end)
|
|
|
|
effect(function()
|
|
c()
|
|
effect(function()
|
|
table.insert(order2, "effect1")
|
|
getA()
|
|
end)
|
|
effect(function()
|
|
table.insert(order2, "effect2")
|
|
getA()
|
|
getB()
|
|
end)
|
|
end)
|
|
|
|
effectScope(function()
|
|
effect(function()
|
|
table.insert(order3, "effect1")
|
|
getA()
|
|
end)
|
|
effect(function()
|
|
table.insert(order3, "effect2")
|
|
getA()
|
|
getB()
|
|
end)
|
|
end)
|
|
|
|
table.clear(order1)
|
|
table.clear(order2)
|
|
table.clear(order3)
|
|
|
|
startBatch()
|
|
setB(1)
|
|
setA(1)
|
|
endBatch()
|
|
|
|
assert(table.concat(order1, ", ") == "effect2, effect1", "wrong order 1")
|
|
assert(table.concat(order2, ", ") == "effect2, effect1", "wrong order 2")
|
|
assert(table.concat(order3, ", ") == "effect2, effect1", "wrong order 3")
|
|
end)
|
|
|
|
test("should custom effect support batch", function()
|
|
local function batchEffect(fn: () -> ())
|
|
return effect(function()
|
|
startBatch()
|
|
local success, result: unknown = pcall(fn)
|
|
endBatch()
|
|
if not success then
|
|
error(result)
|
|
end
|
|
end)
|
|
end
|
|
|
|
local logs: { string } = {}
|
|
local a = signal(0)
|
|
local b, setB = signal(0)
|
|
|
|
local aa = computed(function()
|
|
table.insert(logs, "aa-0")
|
|
if a() == 0 then
|
|
setB(1)
|
|
end
|
|
table.insert(logs, "aa-1")
|
|
return nil
|
|
end)
|
|
|
|
local bb = computed(function()
|
|
table.insert(logs, "bb")
|
|
return b()
|
|
end)
|
|
|
|
batchEffect(function()
|
|
bb()
|
|
end)
|
|
batchEffect(function()
|
|
aa()
|
|
end)
|
|
|
|
assert(table.concat(logs, ", ") == "bb, aa-0, aa-1, bb", `wrong order: {table.concat(logs, ", ")}`)
|
|
end)
|
|
|
|
test("should duplicate subscribers do not affect the notify order", function()
|
|
local src1, setSrc1 = signal(0)
|
|
local src2, setSrc2 = signal(0)
|
|
local order: { string } = {}
|
|
|
|
effect(function()
|
|
table.insert(order, "a")
|
|
local currentSub = setActiveSub(nil)
|
|
local isOne = src2() == 1
|
|
setActiveSub(currentSub)
|
|
if isOne then
|
|
src1()
|
|
end
|
|
src2()
|
|
src1()
|
|
end)
|
|
effect(function()
|
|
table.insert(order, "b")
|
|
src1()
|
|
end)
|
|
setSrc2(1) -- src1.subs: a -> b -> a
|
|
|
|
table.clear(order)
|
|
setSrc1(src1() + 1)
|
|
|
|
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
|
end)
|
|
|
|
test("should handle side effect with inner effects", function()
|
|
local a, setA = signal(0)
|
|
local b, setB = signal(0)
|
|
local order: { string } = {}
|
|
|
|
effect(function()
|
|
effect(function()
|
|
a()
|
|
table.insert(order, "a")
|
|
end)
|
|
effect(function()
|
|
b()
|
|
table.insert(order, "b")
|
|
end)
|
|
assert(table.concat(order) == "ab", `wrong order: {table.concat(order)}`)
|
|
|
|
table.clear(order)
|
|
setB(1)
|
|
setA(1)
|
|
end)
|
|
|
|
-- DEVIATION: This assertion was moved outside of the effect so that it
|
|
-- runs after the batch updates have been processed
|
|
assert(table.concat(order) == "ba", `wrong order: {table.concat(order)}`)
|
|
end)
|
|
|
|
test("should handle flags are indirectly updated during checkDirty", function()
|
|
local a, setA = signal(false)
|
|
local b = computed(function()
|
|
return a()
|
|
end)
|
|
local c = computed(function()
|
|
b()
|
|
return 0
|
|
end)
|
|
local d = computed(function()
|
|
c()
|
|
return b()
|
|
end)
|
|
|
|
local triggers = 0
|
|
|
|
effect(function()
|
|
d()
|
|
triggers += 1
|
|
end)
|
|
assert(triggers == 1, "did not run initially")
|
|
setA(true)
|
|
assert(triggers == 2, "did not update indirectly")
|
|
end)
|
|
|
|
test("should not execute skipped effects from previous failed flush when updating unrelated signal", function()
|
|
local a, setA = signal(0)
|
|
local b, setB = signal(0)
|
|
local c, setC = signal(0)
|
|
local d = computed(function()
|
|
c()
|
|
return 0
|
|
end)
|
|
|
|
local effect3Executed = false
|
|
|
|
effect(function()
|
|
a()
|
|
end)
|
|
effect(function()
|
|
if a() == 2 then
|
|
error("Error in effect 2")
|
|
end
|
|
end)
|
|
effect(function()
|
|
a()
|
|
d()
|
|
effect3Executed = true
|
|
end)
|
|
effect(function()
|
|
b()
|
|
end)
|
|
|
|
setA(1)
|
|
|
|
effect3Executed = false
|
|
local success, err = pcall(setA, 2)
|
|
assert(not success and string.find(tostring(err), "Error in effect 2"), `bad error: {err}`)
|
|
|
|
assert(not effect3Executed, "effect 3 should not have executed after failed flush")
|
|
setB(1)
|
|
assert(not effect3Executed, "effect 3 should not have executed after updating unrelated signal b")
|
|
setC(1)
|
|
assert(effect3Executed, "effect 3 should have executed after updating signal c")
|
|
end)
|
|
|
|
test("should allow finite recursion", function()
|
|
local src, setSrc = signal(0)
|
|
local order = {}
|
|
|
|
effect(function()
|
|
table.insert(order, src())
|
|
setSrc(math.min(src() + 1, 5))
|
|
end)
|
|
assert(table.concat(order) == "012345", `bad order: {table.concat(order)}`)
|
|
end)
|
|
|
|
test("should run cleanups in sequence", function()
|
|
local order = {}
|
|
|
|
local a, setA = signal(false)
|
|
local stopEffect = effect(function()
|
|
a()
|
|
onCleanup(function()
|
|
table.insert(order, "a")
|
|
end)
|
|
onCleanup(function()
|
|
table.insert(order, "b")
|
|
end)
|
|
end)
|
|
|
|
assert(table.concat(order) == "", "should not cleanup immediately")
|
|
|
|
setA(true)
|
|
assert(table.concat(order) == "ab", `bad update: {table.concat(order)}`)
|
|
|
|
table.clear(order)
|
|
stopEffect()
|
|
assert(table.concat(order) == "ab", `bad dispose: {table.concat(order)}`)
|
|
|
|
table.clear(order)
|
|
setA(false)
|
|
assert(table.concat(order) == "", "should not cleanup after update")
|
|
end)
|
|
|
|
test("should run inner effect cleanup", function()
|
|
local cleanups = 0
|
|
|
|
local src, setSrc = signal(false)
|
|
local stopEffect = effect(function()
|
|
src()
|
|
effect(function()
|
|
onCleanup(function()
|
|
cleanups += 1
|
|
end)
|
|
end)
|
|
effect(function()
|
|
onCleanup(function()
|
|
cleanups += 1
|
|
end)
|
|
end)
|
|
end)
|
|
|
|
assert(cleanups == 0, "should not cleanup immediately")
|
|
setSrc(true)
|
|
assert(cleanups == 2, "should cleanup after outer update")
|
|
stopEffect()
|
|
assert(cleanups == 4, "should cleanup after outer dispose")
|
|
setSrc(false)
|
|
assert(cleanups == 4, "should not cleanup after update")
|
|
end)
|
|
|
|
test("should run cleanups outside any evaluation context", function()
|
|
local calls = 0
|
|
local a, setA = signal(0)
|
|
local b, setB = signal(0)
|
|
local c = computed(function()
|
|
if a() == 0 then
|
|
effect(function()
|
|
onCleanup(function()
|
|
b()
|
|
end)
|
|
end)
|
|
end
|
|
return a()
|
|
end)
|
|
|
|
effect(function()
|
|
calls += 1
|
|
c()
|
|
end)
|
|
assert(calls == 1, "should run once")
|
|
|
|
calls = 0
|
|
setA(1)
|
|
assert(calls == 1, "should run once after a update")
|
|
|
|
calls = 0
|
|
setB(1)
|
|
assert(calls == 0, "should not run after b update")
|
|
end)
|
|
|
|
test("should not retrigger the effect if the cleanup modifies one of the dependencies", function()
|
|
local a, setA = signal(0)
|
|
local calls = {}
|
|
|
|
effect(function()
|
|
table.insert(calls, a())
|
|
return function()
|
|
setA(2)
|
|
end
|
|
end)
|
|
assert(#calls == 1, "should have run once initially")
|
|
table.clear(calls)
|
|
|
|
setA(1)
|
|
assert(#calls == 1, "should run once after an update")
|
|
assert(calls[1] == 2, `bad call: {calls[1]}`)
|
|
end)
|
|
|
|
test("should run the cleanup if the effect disposes itself", function()
|
|
local a, setA = signal(0)
|
|
local calls = 0
|
|
local dispose: () -> ()
|
|
|
|
dispose = effect(function()
|
|
if a() > 0 then
|
|
dispose()
|
|
return function()
|
|
calls += 1
|
|
end
|
|
end
|
|
end)
|
|
assert(calls == 0, "should not have called cleanup initially")
|
|
setA(1)
|
|
assert(calls == 1, "should have called cleanup once after first update")
|
|
setA(2)
|
|
assert(calls == 1, "should not call cleanup again after second update")
|
|
end)
|
|
|
|
test("should not run the effect if the cleanup function disposes it", function()
|
|
local a, setA = signal(0)
|
|
local calls = 0
|
|
local dispose: () -> ()
|
|
|
|
dispose = effect(function()
|
|
a()
|
|
calls += 1
|
|
return function()
|
|
dispose()
|
|
end
|
|
end)
|
|
assert(calls == 1, "should have run effect once initially")
|
|
setA(1)
|
|
assert(calls == 1, "should not run effect again after update")
|
|
end)
|
|
|
|
test("should reset the cleanup if the effect throws", function()
|
|
local a, setA = signal(0)
|
|
local calls = 0
|
|
|
|
effect(function()
|
|
if a() == 0 then
|
|
return function()
|
|
calls += 1
|
|
end
|
|
else
|
|
error("hello")
|
|
end
|
|
end)
|
|
assert(calls == 0, "should not have called cleanup initially")
|
|
local success, err = pcall(setA, 1)
|
|
assert(not success and string.find(tostring(err), "hello"), `bad error: {err}`)
|
|
assert(calls == 1, "should have called cleanup once after throwing")
|
|
setA(0)
|
|
assert(calls == 1, "should not call cleanup again after resetting value")
|
|
end)
|
|
|
|
test("recursive effects should run and cleanup in order", function()
|
|
local a, setA = signal(0)
|
|
local order: { string } = {}
|
|
|
|
effect(function()
|
|
local value = a()
|
|
table.insert(order, `+{value}`)
|
|
if value < 3 then
|
|
setA(value + 1)
|
|
end
|
|
return function()
|
|
table.insert(order, `-{value}`)
|
|
end
|
|
end)
|
|
assert(table.concat(order) == "+0-0+1-1+2-2+3", `bad order: {table.concat(order)}`)
|
|
table.clear(order)
|
|
setA(100)
|
|
assert(table.concat(order) == "-3+100", `bad order: {table.concat(order)}`)
|
|
end)
|
|
|
|
test("should effects dispose in order", function()
|
|
local order: { number } = {}
|
|
|
|
local src, setSrc = signal(false)
|
|
local stopEffect = effect(function()
|
|
src()
|
|
for i = 1, 3 do
|
|
effect(function()
|
|
return function()
|
|
table.insert(order, i)
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
|
|
assert(#order == 0, "should not cleanup immediately")
|
|
setSrc(true)
|
|
assert(table.concat(order) == "321", `bad rerun order: {table.concat(order)}`)
|
|
table.clear(order)
|
|
stopEffect()
|
|
assert(table.concat(order) == "321", `bad dispose order: {table.concat(order)}`)
|
|
table.clear(order)
|
|
setSrc(false)
|
|
assert(#order == 0, "should not cleanup after dispose")
|
|
end)
|
|
|
|
test("should cleanup nested effects deepest first", function()
|
|
local order: { string } = {}
|
|
|
|
local stopEffect = effect(function()
|
|
effect(function()
|
|
effect(function()
|
|
return function()
|
|
table.insert(order, "grandchild")
|
|
end
|
|
end)
|
|
return function()
|
|
table.insert(order, "child")
|
|
end
|
|
end)
|
|
return function()
|
|
table.insert(order, "outer")
|
|
end
|
|
end)
|
|
|
|
stopEffect()
|
|
assert(table.concat(order, ", ") == "grandchild, child, outer", `bad dispose order: {table.concat(order, ", ")}`)
|
|
end)
|
|
|
|
test("should cleanup computed child effects in reverse when unwatched", function()
|
|
local order: { number } = {}
|
|
local c = computed(function()
|
|
for i = 1, 3 do
|
|
effect(function()
|
|
return function()
|
|
table.insert(order, i)
|
|
end
|
|
end)
|
|
end
|
|
return 0
|
|
end)
|
|
|
|
local stopEffect = effect(function()
|
|
c()
|
|
end)
|
|
|
|
stopEffect()
|
|
assert(table.concat(order) == "321", `bad dispose order: {table.concat(order)}`)
|
|
end)
|
|
|
|
test("should handle inner effect clearing outer effect's deps on rerun", function()
|
|
local src, setSrc = signal(false)
|
|
local stopEffect: () -> ()
|
|
stopEffect = effect(function()
|
|
src()
|
|
effect(function()
|
|
return function()
|
|
stopEffect()
|
|
end
|
|
end)
|
|
effect(function() end)
|
|
end)
|
|
setSrc(true)
|
|
end)
|
|
|
|
test("should handle inner effect cleanup disposing outer effect", function()
|
|
local order: { number } = {}
|
|
|
|
local src, setSrc = signal(false)
|
|
local stopEffect: () -> ()
|
|
stopEffect = effect(function()
|
|
src()
|
|
table.insert(order, 0)
|
|
for i = 1, 4 do
|
|
effect(function()
|
|
return function()
|
|
table.insert(order, i)
|
|
stopEffect()
|
|
end
|
|
end)
|
|
end
|
|
end)
|
|
assert(table.concat(order) == "0", "should not cleanup immediately")
|
|
|
|
table.clear(order)
|
|
setSrc(true)
|
|
assert(table.concat(order) == "4321", `bad rerun order: {table.concat(order)}`)
|
|
|
|
table.clear(order)
|
|
setSrc(false)
|
|
assert(#order == 0, "should not cleanup after dispose")
|
|
end)
|
|
|
|
test("should cleanup child before parent on rerun", function()
|
|
local order: { string } = {}
|
|
local src, setSrc = signal(false)
|
|
effect(function()
|
|
src()
|
|
table.insert(order, "outer run")
|
|
effect(function()
|
|
table.insert(order, "inner run")
|
|
return function()
|
|
table.insert(order, "inner cleanup")
|
|
end
|
|
end)
|
|
return function()
|
|
table.insert(order, "outer cleanup")
|
|
end
|
|
end)
|
|
|
|
table.clear(order)
|
|
setSrc(true)
|
|
assert(
|
|
table.concat(order, ", ") == "inner cleanup, outer cleanup, outer run, inner run",
|
|
`bad rerun order: {table.concat(order, ", ")}`
|
|
)
|
|
end)
|
|
|
|
test("should preserve child tracking after an inner-only rerun", function()
|
|
local order: { string } = {}
|
|
local outerSrc, setOuterSrc = signal(false)
|
|
local innerSrc, setInnerSrc = signal(false)
|
|
|
|
effect(function()
|
|
outerSrc()
|
|
table.insert(order, "outer run")
|
|
effect(function()
|
|
innerSrc()
|
|
table.insert(order, "inner run")
|
|
return function()
|
|
table.insert(order, "inner cleanup")
|
|
end
|
|
end)
|
|
return function()
|
|
table.insert(order, "outer cleanup")
|
|
end
|
|
end)
|
|
|
|
setInnerSrc(true)
|
|
table.clear(order)
|
|
setOuterSrc(true)
|
|
assert(
|
|
table.concat(order, ", ") == "inner cleanup, outer cleanup, outer run, inner run",
|
|
`bad rerun order: {table.concat(order, ", ")}`
|
|
)
|
|
end)
|
|
|
|
return {}
|