local testkit = require "./testkit" local TEST, CASE, CHECK, FINISH = testkit.test() local mock = require "./mock" local Instance, Signal = mock.Instance, mock.Signal local Vector2, UDim2 = mock.Vector2, mock.UDim2 local vide = require "../../vide" local root = vide.root local mount = vide.mount local create = vide.create local source = vide.source local effect = vide.effect local derive = vide.derive local switch = vide.switch local show = vide.show local indexes = vide.indexes local values = vide.values local cleanup = vide.cleanup local untrack = vide.untrack local read = vide.read local batch = vide.batch local context = vide.context local spring = vide.spring local action = vide.action local changed = vide.changed local apply = vide.apply local step = vide.step local graph = require "../../vide/src/graph" type Node = graph.Node type Map = { [K] : V } local function gc(n: number?) for i = 1, n or 3 do (collectgarbage :: any)("collect") end end local function weak(t: T & {}): T setmetatable(t :: {}, { __mode = "kv" }) return t end local function wrap_root(fn: () -> ()) return function() local destroy = vide.mount(fn :: any) destroy() end end local NIL = nil :: any vide.strict = false TEST("graph", function() local create_node = graph.create_node local push_scope_as_child_of = graph.push_scope_as_child_of local update_descendants = graph.update_descendants local push_child = graph.push_child local get_scope = graph.get_scope local push_scope = graph.push_scope local pop_scope = graph.pop_scope local get_children = graph.get_children_as_array local push_cleanup = graph.push_cleanup local destroy = graph.destroy local function node(owner: Node?, v: T?) return create_node(owner or false, function(x) return not x end, v or false :: any) end local function scope(owner: Node?) return create_node(owner or false, false, false) end local function cleanup(fn: () -> ()) local node = assert(get_scope()) push_cleanup(node, fn) end do CASE "link nodes" local a = node() local b = node() local c = node() push_scope(c) push_scope_as_child_of(a) push_scope_as_child_of(b) pop_scope() CHECK(get_children(a)[1] == c) CHECK(get_children(b)[1] == c) end do CASE "rerun linked nodes" local root = node() local a = node() local b = node(root) local c = node(root) local count = 0 local function effect(x) push_scope_as_child_of(a) push_scope_as_child_of(b) count += 1 return not x end c.effect = effect push_scope(c) effect(c.cache) pop_scope() CHECK(count == 1) update_descendants(a) CHECK(count == 2) update_descendants(b) CHECK(count == 3) end do CASE "diamond graph" -- a -> b -> d -- -> c local root = node() local a, b, c, d = node(), node(root), node(root), node(root) local b_cnt, c_cnt, d_cnt = 0, 0, 0 function b.effect(x) b_cnt += 1; return not x end function c.effect(x) c_cnt += 1; return not x end function d.effect(x) d_cnt += 1; return not x end push_scope(b); push_scope_as_child_of(a); pop_scope() push_scope(c); push_scope_as_child_of(a); pop_scope() push_scope(d); push_scope_as_child_of(b); push_scope_as_child_of(c); pop_scope() update_descendants(a) CHECK(b_cnt == 1) CHECK(c_cnt == 1) CHECK(d_cnt == 1) end do CASE "duplicate child on rerun" local root = node() local a, b, c = node(root), node(root), node(root) function c.effect(x) push_scope_as_child_of(a) push_scope_as_child_of(b) return not x end push_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); pop_scope() update_descendants(a) CHECK(#get_children(a) == 1) CHECK(#get_children(b) == 1) end do CASE "case 1" -- construct graph local items = node(nil, { "a", "b" }) local selected = node(nil, "a") local root = scope() local scope1 = scope(root) local scope2 = scope(root) local items_updated local bind1 local bind2 local cleaned = {} :: { [any]: any } local function clean(s) cleanup(function() cleaned[s] = true end) end do push_scope(root) clean "root" items_updated = node(root) push_scope_as_child_of(items_updated) -- should not do push_scope(items_updated) push_scope_as_child_of(items) do push_scope(root) do push_scope(scope1) clean "scope1" bind1 = node(scope1) do push_scope(bind1) clean "bind1" push_scope_as_child_of(selected) pop_scope() end pop_scope() end do push_scope(scope2) clean "scope2" bind2 = node(scope2) do push_scope(bind2) clean "bind2" push_scope_as_child_of(selected) pop_scope() end pop_scope() end pop_scope() end pop_scope() end pop_scope() end -- verify graph do local c = get_children(items_updated) CHECK(#c == 0) end do local c = get_children(root) CHECK(#c == 0) -- CHECK(table.find(c, items_updated)) -- CHECK(table.find(c, scope1 :: Node)) -- CHECK(table.find(c, scope2 :: Node)) end do local c = get_children(selected) CHECK(#c == 2) CHECK(table.find(c, bind1 :: any)) CHECK(table.find(c, bind2 :: any)) end do local c = get_children(scope1) CHECK(#c == 0) --CHECK(table.find(c, bind1)) end do local c = get_children(scope2) CHECK(#c == 0) --CHECK(table.find(c, bind2)) end -- destroy --CHECK(table.find(get_children(root), scope1 :: Node)) destroy(scope1) CHECK(cleaned.scope1) CHECK(cleaned.bind1) scope1 = NIL bind1 = NIL bind2 = NIL gc() CHECK(#get_children(root) == 0) CHECK(#get_children(selected) == 1) end do CASE "nodes garbage collection" local wref = weak { node(nil, 1) } destroy(wref[1]) gc() CHECK(not wref[1]) end do CASE "recursive update" --[[ A -> B + C D -> E + F B updates D depth=1 B, C ^ depth=2 E, F ^ depth=2 _, F ^ depth=1 _, _ <- attempt to update_descendants nothing ^ ]] local root = node() local a, b, c, d, e, f = node(root), node(root), node(root), node(root), node(root), node(root) function b.effect(x) update_descendants(d) return not x end push_child(a, b); push_child(a, c) push_child(d, e); push_child(d, f) update_descendants(a) CHECK(true) end end) TEST("mount()", function() local screen = create "ScreenGui" {} local text = source "foo" local count = 0 local unmount = mount(function() cleanup(function() count += 1 end) return create "TextLabel" { Name = "TextLabel", Text = text } end, screen) local label = screen:FindFirstChild "TextLabel" :: TextLabel CHECK(label) CHECK(label.Text == "foo") text "bar" CHECK(label.Text == "bar") CHECK(count == 0) unmount() CHECK(count == 1) end) TEST("root()", function() local count = 0 root(function(destroy) cleanup(function() count += 1 end) destroy() end) CHECK(count == 1) end) TEST("source()", wrap_root(function() do CASE "create source" local src = source(1) CHECK(src() == 1) end do CASE "set and get source value" local src = source(1) src(2) CHECK(src() == 2) end do CASE "does not update if same value" local src = source(1) local count = 0 effect(function() src() count += 1 end) CHECK(count == 1) src(1) CHECK(count == 1) src(2) CHECK(count == 2) end do CASE "does update if same value is mutable table" local src = source {} local count = 0 effect(function() src() count += 1 end) CHECK(count == 1) src(src()) CHECK(count == 2) end do CASE "does not update if same value is frozen table" local a = table.freeze {} local b = table.freeze {} local src = source(a) local count = 0 effect(function() src() count += 1 end) CHECK(count == 1) src(a) CHECK(count == 1) src(b) CHECK(count == 2) src(b) CHECK(count == 2) end end)) TEST("derive()", wrap_root(function() do CASE "derive new value on source change" local a = source(1) local b = source(2) local c = derive(function() return tostring(a() + b()) end) CHECK(c() == "3") a(2) CHECK(c() == "4") end do CASE "derive wrapped source" local a = source(1) local b = function() return tostring(a()) end local c = derive(function() return tonumber(b()) end) CHECK(c() == 1) a(2) CHECK(c() == 2) end do CASE "does not update if same value" local num = source(0) local is_even = derive(function() return bit32.band(num(), 0b01) == 0 end) local count = 0 effect(function() is_even() count += 1 end) num(1) -- odd CHECK(count == 2) num(2) -- even CHECK(count == 3) num(4) -- even CHECK(count == 3) num(5) -- odd CHECK(count == 4) end do CASE "conditional derive" local a = source(false) local b = source(false) local c = derive(function() return if a() then "a" elseif b() then "b" else "never" end) local count = 0 effect(function() c(); count += 1 end) b(true) CHECK(c() == "b") CHECK(count == 2) a(true) CHECK(c() == "a") CHECK(count == 3) b(false) CHECK(count == 3) b(true) CHECK(count == 3) a(false) CHECK(c() == "b") CHECK(count == 4) end do CASE "owner not disconnected" local count = 0 local a = source(0) local destroy = mount(function() local _b = derive(function() cleanup(function() count += 1 end) return a() end) end) CHECK(count == 0) a(1) -- b clears parents (should not clear owner) CHECK(count == 1) destroy() CHECK(count == 2) end -- do CASE "behavior of effect within an effect" -- local num = source(1) -- local ran = table.create(100, 0) -- local cleaned = table.create(100, 0) -- local destroy = vide.mount(function() -- local owner = derive(function() -- local i = num() -- return untrack(function() -- return derive(function() -- ran[i] += 1 -- cleanup(function() -- cleaned[i] += 1 -- end) -- return i -- end) -- end) -- end) -- local child1 = owner() -- num(2) -- CHECK(cleaned[1] == 1) -- local child2 = owner() -- CHECK(child1() == 1) -- CHECK(child2() == 2) -- end) -- destroy() -- CHECK(ran[1] == 1) -- CHECK(ran[2] == 1) -- CHECK(cleaned[1] == 1) -- CHECK(cleaned[2] == 1) -- end do CASE "garbage collection" -- check that `b` does not allow gc of `a` local a = source(1) local _b = derive(function() return a() end) _b = NIL local wref = weak { a } gc() CHECK(wref[1]) end end)) TEST("effect()", wrap_root(function() do CASE "rerun on source change" local a = source(1) local b = source(1) local count = 0 effect(function() a() b() count += 1 end) CHECK(count == 1) a(2) CHECK(count == 2) b(2) CHECK(count == 3) end do CASE "rerun on derived source change" local num = source(0) local text = derive(function() return tostring(num()) end) local count = 0 effect(function() text() count += 1 end) num(1) CHECK(count == 2) end do CASE "cache" local num = source(0) local count effect(function(x: number) num() count = x + 1 return x + 1 end, 0) num(1) CHECK(count == 2) end end)) TEST("cleanup()", wrap_root(function() do CASE "root cleanup" local count = 0 local destroy = mount(function() cleanup(function() count += 1 end) end) CHECK(count == 0) destroy() CHECK(count == 1) end do CASE "cleanup on rerun" local src = source(1) local effected = 0 local cleaned = 0 effect(function() src() effected += 1 cleanup(function() cleaned += 1 end) end) CHECK(effected == 1) CHECK(cleaned == 0) src(2) CHECK(effected == 2) CHECK(cleaned == 1) end do CASE "multiple cleanup" local src = source(1) local queue = {} effect(function() src() cleanup(function() table.insert(queue, 1) end) cleanup(function() table.insert(queue, 2) end) end) CHECK(testkit.seq(queue, {})) src(2) CHECK(testkit.seq(queue, { 1, 2 })) src(3) CHECK(testkit.seq(queue, { 1, 2, 1, 2 })) end do CASE "cleanup objects" local ran = {} root(function(destroy) effect(function() cleanup { disconnect = function() ran.disconnect = true end } cleanup { Disconnect = function() ran.Disconnect = true end } cleanup { destroy = function() ran.destroy = true end } cleanup { Destroy = function() ran.Destroy = true end } destroy() end) end) CHECK(ran.disconnect) CHECK(ran.Disconnect) CHECK(ran.destroy) CHECK(ran.Destroy) end end)) TEST("create()", wrap_root(function() do CASE "create(\"ClassName\", props) syntax" local frame = create("Frame", { BackgroundTransparency = 0.5, Name = "Foo" }) CHECK(frame.BackgroundTransparency == 0.5) CHECK(frame.Name == "Foo") end do CASE "create(Instance, props) syntax" local frame0 = create("Frame", { BackgroundTransparency = 0.5, Name = "Foo" }) local frame = create(frame0, { BackgroundTransparency = 1 }) CHECK(frame.BackgroundTransparency == 1) CHECK(frame.Name == "Foo") end do CASE "apply default properties" local defaults = require "../src/defaults" local frame = create "Frame" {} :: Instance & { BorderSizePixel: any, BorderColor3: any } CHECK(frame.BorderSizePixel == defaults.Frame.BorderSizePixel) CHECK(frame.BorderColor3 == defaults.Frame.BorderColor3) end do CASE "set properties" local text = create "TextLabel" { Name = "Label", Text = "test" } CHECK(text.Name == "Label") CHECK(text.Text == "test") end do CASE "set nested properties" local text = create "TextLabel" { { Name = "Label" }, { Text = "test" } } CHECK(text.Name == "Label") CHECK(text.Text == "test") end do CASE "set nested parent" local frame = create "Frame" {} local text = create "TextLavel" { { Parent = frame } } CHECK(frame:GetChildren()[1] == text) CHECK(text.Parent == frame) end do CASE "nested deferred" local text = create "TextLabel" { { { Text = "2" }, Text = "1", } } CHECK(text.Text == "2") end do CASE "nested not deferred" vide.defer_nested_properties = false local t = {} create "TextLabel" { { { function() table.insert(t, 1) end } :: any, function() table.insert(t, 2) end, } } CHECK(t[1] == 1) CHECK(t[2] == 2) vide.defer_nested_properties = true end do CASE "independent" local frame = create "Frame" CHECK(frame {} ~= frame {}) end do CASE "set children" local frame = create "Frame" { create "TextLabel" { Name = "A" }, create "TextLabel" { Name = "B" }, { create "TextLabel" { Name = "C" } :: any, create "TextLabel" { Name = "D" }, { create "TextLabel" { Name = "E" } } }, { create "TextLabel" { Name = "F" } :: any, { create "TextLabel" { Name = "G" } } } } CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "B") CHECK(frame:FindFirstChild "C") CHECK(frame:FindFirstChild "D") CHECK(frame:FindFirstChild "E") CHECK(frame:FindFirstChild "F") CHECK(frame:FindFirstChild "G") end do CASE "set false as child" create "Frame" { false } create "Frame" { function() return false end } create "Frame" { function() return { false } end } end do CASE "binding properties to source" local name = source("Hi") local text = source("Bye") local label = create "TextLabel" { Name = name, Text = text } CHECK(label.Name == "Hi") CHECK(label.Text == "Bye") name "Foo" text "Bar" CHECK(label.Name == "Foo") CHECK(label.Text == "Bar") end do CASE "binding destroy" local count = 0 local destroy = mount(function() local src = source(0) return create "TextLabel" { Text = function() cleanup(function() count += 1 end) return src() end } end) CHECK(count == 0) destroy() CHECK(count == 1) end do CASE "bind same source to multiple instance properties" local src = source "1" local text = create "TextBox" { Name = src, Text = src, PlaceholderText = src } src "2" CHECK(text.Name == "2") CHECK(text.Text == "2") CHECK(text.PlaceholderText == "2") end do CASE "bind children" local children = source() local a, b, c = create "TextLabel" { Name = "A" }, create "TextLabel" { Name = "B" }, create "TextLabel" { Name = "C" } local frame = create "Frame" { children } children { a, b } CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "B") -- check that b is removed and c is added while a remains untouched children { a, c } CHECK(frame:FindFirstChild "A") CHECK(frame:FindFirstChild "C") CHECK(not frame:FindFirstChild "B") children(nil) CHECK(#frame:GetChildren() == 0) end do CASE "parent bound to source" local _, wref, destroy = root(function(destroy) local frame = create "Frame" { Name = "Parent" } local parent = source(frame :: Frame?) local wref = weak { create "TextLabel" { Parent = parent, Name = "Child" } } gc() CHECK(wref[1]) parent(nil) return wref, destroy end) gc() CHECK(wref[1]) destroy() destroy = NIL gc() CHECK(not wref[1]) end do CASE "recursive create" local set_test_to_true = action(function(self) (self :: any).test = true end) local f2 local to_apply = { { a = 1 }, set_test_to_true, b = function() f2 = create "Frame" { a = 2 } end, } :: { [number|string]: unknown } -- do -- confirm iteration order -- local t = {} -- for i in to_apply do -- table.insert(t, i) -- end -- assert(t[1] == "a") -- end local f = create "Frame" (to_apply) CHECK((f :: any).a == 1) CHECK((f :: any).test == true ) CHECK((f2 :: any).a == 2) end do CASE "nested children effect" local a = create "Frame" { Name = "a" } local b = create "Frame" { Name = "b" } local c = create "Frame" { Name = "c" } local d = create "Frame" { Name = "d" } local e = create "Frame" { Name = "e" } local children = source { a, { b, c, { d } }, { { e } } } local obj = create "Frame" { children } CHECK(obj:FindFirstChild("a")) CHECK(obj:FindFirstChild("b")) CHECK(obj:FindFirstChild("c")) CHECK(obj:FindFirstChild("d")) CHECK(obj:FindFirstChild("e")) children { b, { c, a }, { { d } } } CHECK(obj:FindFirstChild("a")) CHECK(obj:FindFirstChild("b")) CHECK(obj:FindFirstChild("c")) CHECK(obj:FindFirstChild("d")) CHECK(not obj:FindFirstChild("e")) end do CASE "nested children source effect" local a = create "Frame" { Name = "a" } :: Instance local b = create "Frame" { Name = "b" } :: Instance local c = create "Frame" { Name = "c" } :: Instance local nested_children = source { b, c } local children = source { a :: Instance | () -> { Instance }, nested_children } local parent = create "Frame" { Name = "parent", children } CHECK(parent:FindFirstChild "a") CHECK(parent:FindFirstChild "b") CHECK(parent:FindFirstChild "c") nested_children {} CHECK(parent:FindFirstChild "a") CHECK(not parent:FindFirstChild "b") CHECK(not parent:FindFirstChild "c") nested_children { b } CHECK(parent:FindFirstChild "a") CHECK(parent:FindFirstChild "b") CHECK(not parent:FindFirstChild "c") children { a } CHECK(parent:FindFirstChild "a") CHECK(not parent:FindFirstChild "b") CHECK(not parent:FindFirstChild "c") nested_children { b, c } CHECK(parent:FindFirstChild "a") CHECK(not parent:FindFirstChild "b") CHECK(not parent:FindFirstChild "c") children { a :: Instance | () -> { Instance }, nested_children } CHECK(parent:FindFirstChild "a") CHECK(parent:FindFirstChild "b") CHECK(parent:FindFirstChild "c") nested_children { c } CHECK(parent:FindFirstChild "a") CHECK(not parent:FindFirstChild "b") CHECK(parent:FindFirstChild "c") end do CASE "garbage collection test" local wref do local data = setmetatable({}, {}) local proxy = setmetatable({}, { __mode = "v" }) local ref = setmetatable({}, { __mode = "v" }) --proxy.data = data --? (this line should not affect outcome) -- `data` strongly references `proxy` data.connection = proxy -- `ref` strongly references `data` ref[data] = proxy -- although `ref` is weak to values, `data` keeps `proxy` alive -- this forms a sort of cyclic reference that the luau gc is unable to detect wref = { data = data, proxy = proxy } end gc() CHECK(wref.data and wref.proxy) end end)) TEST("show()", wrap_root(function() do CASE "show component" local input = source(true) local function one() return 1 end local output = show(input, one) CHECK(output() == 1) input(false) CHECK(output() == nil) end do CASE "fallback component" local input = source(true) local function one() return 1 end local function two() return 2 end local output = show(input, one, two) CHECK(output() == 1) input(false) CHECK(output() == 2) end do CASE "updating truth to truthy does not rerun" local input = source(1) local count = 0 local function component() count += 1 return 1 end local output = show(input, component) CHECK(count == 1) CHECK(output() == 1) input(2) CHECK(count == 1) end do CASE "updating source passed to component" local input = source(1 :: number?) local count = 0 show(input :: () -> number?, function(value: () -> number) effect(function() local v = value() count += 1 CHECK(v == count) if v ~= count then error(count) end end) return true end) input(2) CHECK(count == 2) input(3) CHECK(count == 3) input(nil) CHECK(count == 3) end do CASE "special strict case" type Weapon = { id: string, enchant: string? } vide.strict = true local count = 0 local branch = 0 local weapon = source(nil :: Weapon?) effect(function() weapon() end) effect(function() weapon() end) show(weapon, function(weapon: () -> Weapon) local enchant = function() return weapon().enchant end show(enchant, function(enchant: () -> string) effect(function() local e = enchant() count += 1 CHECK(e ~= nil) if branch == 1 then CHECK(e == "fire") elseif branch == 2 then CHECK(e == "poison") end end) return {} end) return {} end) effect(function() weapon() end) effect(function() weapon() end) branch = 1 weapon { id = "1", enchant = "fire" } CHECK(count == 2) branch = 2 weapon { id = "1", enchant = "poison" } CHECK(count == 4) weapon { id = "1", enchant = nil } CHECK(count == 4) branch = 1 weapon { id = "1", enchant = "fire" } CHECK(count == 6) weapon(nil) branch = 2 weapon { id = "1", enchant = "poison" } CHECK(count == 8) vide.strict = false end do CASE "alt" -- todo: move test local visible = source(true) local count = source(0) local outer = 0 local inner = 0 local destroyed = 0 root(function() effect(function() visible() outer += 1 untrack(function() effect(function() count() inner += 1 cleanup(function() destroyed += 1 end) end) end) end) end) CHECK(outer == 1) CHECK(inner == 1) CHECK(destroyed == 0) count(count() + 1) CHECK(outer == 1) CHECK(inner == 2) CHECK(destroyed == 1) visible(false) CHECK(outer == 2) CHECK(inner == 3) CHECK(destroyed == 2) count(count() + 1) CHECK(outer == 2) CHECK(inner == 4) CHECK(destroyed == 3) end do CASE "delay (destruction)" local input = source(false) local obj = {} local value_upval local present_upval local cleaned = false local output = show(input, function(value, present) value_upval = value present_upval = present cleanup(function() cleaned = true end) CHECK(present() == false) return obj, 1 end) CHECK(output() == nil) input(true) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) input(false) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == false) CHECK(not cleaned) step(0.5) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == false) CHECK(not cleaned) step(0.5 + 0.01) CHECK(output() == nil) CHECK(value_upval() == true) CHECK(present_upval() == false) CHECK(cleaned) end do CASE "delay (reactivate before destruction)" local input = source(false) local obj = {} local value_upval local present_upval local cleaned = false local output = show(input, function(value, present) value_upval = value present_upval = present cleanup(function() cleaned = true end) CHECK(present() == false) return obj, 1 end) CHECK(output() == nil) input(true) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) input(false) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == false) CHECK(not cleaned) step(0.5) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == false) CHECK(not cleaned) input(true) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) step(0.5 + 0.01) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) end do CASE "delay (with fallback)" local input = source(false) local obj = {} local value_upval local present_upval local cleaned = false local obj_fallback = {} local present_fallback_upval local cleaned_fallback = false local output = show(input, function(value, present) value_upval = value present_upval = present cleanup(function() cleaned = true end) CHECK(present() == false) return obj, 1 end, function(present) present_fallback_upval = present cleanup(function() cleaned_fallback = true end) CHECK(present() == false) return obj_fallback, 1 end) CHECK(output() == obj_fallback) CHECK(value_upval == nil) CHECK(present_upval == nil) CHECK(not cleaned) CHECK(present_fallback_upval() == true) CHECK(not cleaned_fallback) input(true) CHECK(type(output() == "table") and table.find(output(), obj) and table.find(output(), obj_fallback)) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) CHECK(present_fallback_upval() == false) CHECK(not cleaned_fallback) step(1 + 0.01) CHECK(output() == obj) CHECK(value_upval() == true) CHECK(present_upval() == true) CHECK(not cleaned) CHECK(present_fallback_upval() == false) CHECK(cleaned_fallback) end end)) TEST("switch()", wrap_root(function() do CASE "update on source change" local input = source(true) local output = switch(input) { [true] = function() return 1 end, [false] = function() return 0 end } local count = 0 effect(function() output(); count += 1 end) CHECK(count == 1) CHECK(output() == 1) input(false) CHECK(output() == 0) CHECK(count == 2) input(false) CHECK(output() == 0) CHECK(count == 2) input(NIL) CHECK(output() == nil) end -- do CASE "same component different map" -- local input = source(0) -- local function component() -- return {} -- end -- local output = switch(input) { -- [1] = component, -- [2] = component -- } -- CHECK(output() == nil) -- input(1) -- local instance = output() -- CHECK(instance) -- input(2) -- CHECK(output() == instance) -- end do CASE "scoped switch" local input = source(true) local owner_count = 0 local switch0_count = 0 local switch1_count = 0 cleanup(function() owner_count += 1 end) local output = switch(input) { [true] = function() cleanup(function() switch1_count += 1 end) return 1 end, [false] = function() cleanup(function() switch0_count += 1 end) return 0 end } CHECK(output() == 1) input(false) CHECK(switch1_count == 1) CHECK(switch0_count == 0) input(true) CHECK(switch1_count == 1) CHECK(switch0_count == 1) input(NIL) CHECK(switch1_count == 2) CHECK(switch0_count == 1) CHECK(owner_count == 0) end do CASE "reactive stack resets after error" local scopes = require "../src/graph".scopes local input = source(1) local n0 = scopes.n local ok = pcall(function() switch(input) { error :: any } end) CHECK(not ok) local n1 = scopes.n CHECK(n0 == n1) end do CASE "strict" vide.strict = true local input = source(0) local output = switch(input) { [0] = function() return 0 end, [1] = function() return 1 end, } CHECK(output() == 0) input(1) CHECK(output() == 1) vide.strict = false end do CASE "delay" -- probably unneeded because show() uses switch() internally end end)) TEST("indexes()", wrap_root(function() do CASE "use source" local input = source { 1, 2, 3 } local output = indexes(input, function(v, k) return tostring(v()) end) CHECK("" .. input()[1] == output()[1]) CHECK("" .. input()[2] == output()[2]) CHECK("" .. input()[3] == output()[3]) end do CASE "cache result" local input = source { 1, 2, 3 } local count = table.create(3, 0) local _, output = root(function() local output = indexes(input, function(v, i) count[i] += 1 return v end) return output end) input { 1, 2, 4 } CHECK(output()[1]() == 1) CHECK(output()[2]() == 2) CHECK(output()[3]() == 4) CHECK(count[1] == 1) CHECK(count[2] == 1) CHECK(count[3] == 1) end do CASE "removal reflected" local input = source { 1, 2, 3 } local destroyed = false local output = indexes(input, function(v, i) local text = create "TextLabel" { Text = function() return tostring(v()) end } cleanup(function() destroyed = true end) return text end) input { 1, 2 } local t = output() CHECK(t[1].Text == "1") CHECK(t[2].Text == "2") CHECK(t[3] == NIL) CHECK(destroyed == true) end do CASE "garbage collection" do -- check that `output` does not allow gc of `input` local input = source {} local _derived = indexes(input, function(v, i) return v end) local wref = weak { input } input = NIL gc() CHECK(wref[1]) end do -- check that `input` allows gc of `output` local input = source {} local destroy, output = root(function() return indexes(input, function(v, i) return v, i end) end) destroy() local wref = weak { output } output = NIL gc() CHECK(not wref[1]) end end do CASE "cleanup" local input = source { 1, 2, 3 } local count = table.create(3, 0) local output = indexes(input, function(v, i) cleanup(function() count[i] += 1 end) return {} end) output() CHECK(count[1] == 0) CHECK(count[2] == 0) CHECK(count[3] == 0) end do CASE "reactive stack resets after error" local scopes = require "../src/graph".scopes local input = source { 1 } local n0 = scopes.n local ok = pcall(function() indexes(input, function() error("") return NIL end) end) CHECK(not ok) local n1 = scopes.n CHECK(n0 == n1) end -- practical example based on the graph - recursive update test do CASE "recursive update" local items = source { 1 } local updated = table.create(100, 0) indexes(items, function(item) effect(function() item() updated[1] += 1 end) effect(function() item() updated[2] += 1 end) return {} end) effect(function() items() updated[3] += 1 end) effect(function() items() updated[4] += 1 end) items { 2 } CHECK(updated[1] == 2) CHECK(updated[2] == 2) CHECK(updated[3] == 2) CHECK(updated[4] == 2) end do CASE "strict" vide.strict = true local input = source{1} local output = indexes(input, function(v) return { v } end) CHECK(output()[1][1]() == 1) input{2} CHECK(output()[1][1]() == 2) vide.strict = false end do CASE "delay" local input = source {} local cleaned_counts = {} :: Map local output = indexes(input, function(v, i, present) cleanup(function() cleaned_counts[i] = (cleaned_counts[i] or 0) + 1 end) return { value = v, index = i, present = present }, 1 end) local function mapped() local map = {} local objects = output() if objects then for _, object in objects do map[object.index] = { value = object.value, present = object.present } end end return map end ------------------------------------------------------------------------ do CHECK(mapped()[1] == nil) end input { 1, 2 } do CHECK(mapped()[1].value() == 1) CHECK(mapped()[1].present()) CHECK(mapped()[2].value() == 2) CHECK(mapped()[2].present()) end input { 2 } step(0.5) do CHECK(mapped()[1].value() == 2) CHECK(mapped()[1].present()) CHECK(mapped()[2].value() == 2) CHECK(not mapped()[2].present()) end input { 1, 2 } step(0.5 + 0.01) do CHECK(mapped()[1].value() == 1) CHECK(mapped()[1].present()) CHECK(mapped()[2].value() == 2) CHECK(mapped()[2].present()) end input { 3 } step(1 + 0.01) do CHECK(mapped()[1].value() == 3) CHECK(mapped()[1].present()) CHECK(not mapped()[2]) CHECK(cleaned_counts[1] == nil) CHECK(cleaned_counts[2] == 1) end end end)) TEST("values()", wrap_root(function() do CASE "use source" local input = source { 1, 2, 3 } local output = values(input, function(v, k) return tostring(v) end) CHECK("" .. input()[1] == output()[1]) CHECK("" .. input()[2] == output()[2]) CHECK("" .. input()[3] == output()[3]) end do CASE "cache result" local input = source { 1, 2, 3 } local count = table.create(3, 0) local output = values(input, function(v, i) count[v] += 1 return i end) input { 1, 3, 2 } CHECK(output()[1]() == 1) CHECK(output()[2]() == 3) CHECK(output()[3]() == 2) CHECK(count[1] == 1) CHECK(count[2] == 1) CHECK(count[3] == 1) end do CASE "removal reflected" local input = source { 1, 2, 3 } local destroyed = false local output = values(input, function(v, i) local text = create "TextLabel" { Text = tostring(v) } cleanup(function() destroyed = true end) return text end) input { 1, 2 } local t = output() CHECK(t[1].Text == "1") CHECK(t[2].Text == "2") CHECK(t[3] == NIL) CHECK(destroyed == true) end do CASE "removal reflected 2" local input = source { 1 } local output = values(input, function(v, i) return { v = v, i = i } end) input { 2, 1 } input { 1 } local t = output() CHECK(t[1].v == 1) CHECK(t[1].i() == 1) CHECK(t[2] == nil) CHECK(t[3] == nil) end do CASE "cleanup" local input = source { 1, 2, 3 } local count = table.create(3, 0) local output = values(input, function(v, i) cleanup(function() count[i()] += 1 end) return {} end) output() CHECK(count[1] == 0) CHECK(count[2] == 0) CHECK(count[3] == 0) end do CASE "reactive stack resets after error" local scopes = require "../src/graph".scopes local input = source { 1 } local n0 = scopes.n local ok = pcall(function() values(input, function() error("") return NIL end) end) CHECK(not ok) local n1 = scopes.n CHECK(n0 == n1) end do CASE "delay" local input = source {} local cleaned_counts = {} :: Map local output = values(input, function(v, i, present) cleanup(function() cleaned_counts[v] = (cleaned_counts[v] or 0) + 1 end) return { value = v, index = i, present = present }, 1 end) local function mapped() local map = {} local objects = output() if objects then for _, object in objects do map[object.value] = { index = object.index, present = object.present } end end return map end ------------------------------------------------------------------------ do CHECK(mapped()[1] == nil) end input { 1 } do CHECK(mapped()[1].index() == 1) CHECK(mapped()[1].present()) end input { 2 } step(0.5) do CHECK(mapped()[1].index() == 1) CHECK(not mapped()[1].present()) CHECK(mapped()[2].index() == 1) CHECK(mapped()[2].present()) end input { 1, 2 } step(0.5 + 0.01) do CHECK(mapped()[1].index() == 1) CHECK(mapped()[1].present()) CHECK(mapped()[2].index() == 2) CHECK(mapped()[2].present()) end input { 3 } step(1 + 0.01) do CHECK(not mapped()[1]) CHECK(not mapped()[2]) CHECK(mapped()[3].index() == 1) CHECK(mapped()[3].present()) CHECK(cleaned_counts[1] == 1) CHECK(cleaned_counts[2] == 1) CHECK(cleaned_counts[3] == nil) end end do CASE "delayed destruction deferred" local input = source {} local output = values(input, function() return {}, 1 end) local count = 0 effect(function() output(); count += 1 end) input { 1, 2, 3 } CHECK(count == 2) input {} CHECK(count == 2) step(1 + 0.01) --CHECK(count == 3) CHECK(count == 5) end end)) TEST("spring()", wrap_root(function() do CASE "update source (on next step)" local value = source(10) local sprung = spring(value, 1, 1) CHECK(sprung() == 10) value(20) CHECK(sprung() == 10) step(1/60) CHECK(sprung() ~= 10) CHECK(sprung() > 10) end do CASE "garbage collection" --[[ do -- `output` should not allow gc of `input` local input = source(10) local _output = spring(input) local wref = weak { input } input = NIL gc() CHECK(wref[1]) end ]] do -- `input` should allow gc of `output` local input = source(10) local output = spring(input) local wref = weak { output } output = NIL gc() CHECK(not wref[1]) end -- do -- spring data gc -- local input = source(10) -- local wref do -- local output, data = (spring :: any)(input) -- input(input() + 1) -- schedule spring calculation -- local output_node = unpack(capture(output)) -- wref = weak { output_node, data } -- end -- gc() -- CHECK(not wref[1]) -- CHECK(not wref[2]) -- end end do CASE "garbage collection (binded)" local input = source(10) local output = spring(input, 1, 1) local _label = create "TextLabel" { Text = output } local wref = { output } output = NIL gc() CHECK(wref[1]) -- `output` should not gc end do CASE "spring finished" local input = source(0) local output = spring(input) input(1) step(0.05) CHECK(output() ~= input()) -- check spring is moving step(10) -- spring finished, should be internally removed from queue CHECK(output() == input()) -- check spring is at target local count = -1 effect(function() output() count += 1 end) step(1) -- attempt to cause another spring update CHECK(count == 0) -- check no update occurs as spring is finished -- gc() -- perform full gc input(2) -- spring should be re-added to spring queue step(0) -- process spring queue CHECK(count == 1) -- check spring was rescheduled correctly end do CASE "spring control" local input = source(1) local output, control = spring(input) local value = input() local count = 0 effect(function() value = output() count += 1 end) CHECK(count == 1) CHECK(value == 1) control { impulse = 1 } CHECK(count == 1) CHECK(value == 1) step(1/120 + 0.001) CHECK(count == 2) CHECK(value > 1) end end)) TEST("untrack()", wrap_root(function() do CASE "does not register dependency" local a = source(0) local b = source(0) local count = 0 effect(function() count += 1 untrack(a) b() end) b(1) CHECK(count == 2) a(1) CHECK(count == 2) CHECK(a() == untrack(a)) end do CASE "derived source" local a = source(0) local b = source(0) local c = source(0) local d = function() return a() + b() end local count = 0 effect(function() count += 1 untrack(d) c() end) c(1) CHECK(count == 2) a(1) b(1) CHECK(count == 2) end do CASE "outer scope" local outer_count = 0 local inner_count = 0 local cleaned_count = 0 local input = source(0) local _, output, destroy = root(function(destroy) local output = derive(function() outer_count += 1 return untrack(function() return derive(function() inner_count += 1 cleanup(function() cleaned_count += 1 end) return tostring(input()) end) end) end) return output, destroy end) CHECK(outer_count == 1) CHECK(inner_count == 1) CHECK(cleaned_count == 0) local output2 = output() CHECK(output2() == "0") input(1) CHECK(outer_count == 1) CHECK(inner_count == 2) CHECK(cleaned_count == 1) local output3 = output() CHECK(output2() == "1") CHECK(output3() == "1") CHECK(output2 == output3) destroy() CHECK(cleaned_count == 2) end end)) TEST("events", function() local function Thing(props) local instance = Instance.new("Thing") instance.Signal = Signal.new() local clone = create(instance)(props) return clone end do CASE "connect event" local connected = false local val = Thing { Signal = function(newval) connected = true CHECK(newval == 1) end } CHECK(not connected) val.Value = 1; Signal.fire(val.Signal, val.Value) CHECK(connected) end end) TEST("actions", function() do CASE "run action" local ran = false local frame_ref local frame = create "Frame" { action(function(self) frame_ref = self ran = true end, 1) } CHECK(ran) CHECK(frame_ref == frame) end do CASE "priorities" local queue = {} create "Frame" { action(function(self) table.insert(queue, 2) end, 2), action(function(self) table.insert(queue, 1) end, 1) } CHECK(testkit.seq(queue, { 1, 2 })) end end) TEST("changed()", wrap_root(function() do CASE "outputs" local output = source(nil) local text = create "TextLabel" { Text = "a", changed("Text", output) } CHECK(output() == "a") text.Text = "b" CHECK(output() == "b") end do CASE "connection disconnected" local _, text, destroy = root(function(destroy) local output = source(nil) return create "TextLabel" { Text = "a", changed("Text", output) }, destroy end) destroy() -- changed() should of disconnect connection -- check if instance can gc local wref = weak { text } text = NIL gc() CHECK(not wref[1]) end end)) TEST("batch()", wrap_root(function() do CASE "evaluation deferred" local a = source(0) local count = { b = 0, b2 = 0, c = 0 } local b = derive(function() count.b += 1 return a() + 1 end) local b2 = derive(function() count.b2 += 1 return a() + 2 end) local c = derive(function() count.c += 1 return b() + b2() end) batch(function() a(1) CHECK(count.b == 1) CHECK(count.b2 == 1) CHECK(count.c == 1) end) CHECK(count.b == 2) CHECK(count.b2 == 2) CHECK(count.c == 2) CHECK(b() == 2) CHECK(c() == 5) end do CASE "recursive call" local a1 = source(0) local a2 = source(0) local a3 = source(0) local count = { b1 = 0, b2 = 0, b3 = 0 } local b1 = derive(function() count.b1 += 1 return a1() + 1 end) local b2 = derive(function() count.b2 += 1 return a2() + 1 end) local b3 = derive(function() count.b3 += 1 return a3() + 1 end) batch(function() a1(1) batch(function() a2(2) end) a3(3) CHECK(count.b1 == 1) CHECK(count.b2 == 1) CHECK(count.b3 == 1) end) CHECK(count.b1 == 2) CHECK(count.b2 == 2) CHECK(count.b3 == 2) CHECK(b1() == 2) CHECK(b2() == 3) CHECK(b3() == 4) end do CASE "subsequent updates do not batch" local a = source(0) local b = source(0) local c = source(0) local d_n = 0 effect(function() b() c() d_n += 1 end) effect(function() b(a()) c(a()) end) batch(function() a(1) end) CHECK(d_n == 3) end do CASE "recursive queue flush diamond A,B,C,D" --[[ a > b > d > c > ]] local a = source(0) local b = source(0) local c = source(0) local d = source(0) local count = { b = 0, c = 0, d = 0 } effect(function() batch(function() b(a() % 2 == 0 and 1 or 0) c(a() * 2) end) count.b += 1 count.c += 1 end) effect(function() batch(function() d(b() + c()) end) count.d += 1 end) a(1) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(d() == 2) a(3) CHECK(count.b == 3) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(d() == 6) end do CASE "recursive queue flush diamond A,B,C,D,E" --[[ where b and c batches d a > b > e > c > d > ]] local a = source(0) local b = source(0) local c = source(0) local d = source(0) local e = source(0) local count = { b = 0, c = 0, d = 0, e = 0 } effect(function() batch(function() b(a() % 2 == 0 and 1 or 0) c(a() * 2) end) count.b += 1 count.c += 1 end) effect(function() batch(function() d(c() * 2) end) count.d += 1 end) effect(function() batch(function() e(b() + d()) end) count.e += 1 end) CHECK(e() == 1) a(1) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(count.e == 3) CHECK(e() == 4) a(3) CHECK(count.b == 3) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(count.e == 4) CHECK(e() == 12) end do CASE "recursive queue flush diamond A,B,C,D,E,F,G" --[[ a > b > d > E > G > c ^ > F ]] local a = source(0) local b = source(0) local c = source(0) local d = source(0) local e = source(0) local f = source(0) local g = source(0) local count = { b = 0, c = 0, d = 0, e = 0, f = 0, g = 0 } effect(function() batch(function() b(a() % 2 == 0 and 1 or 0) c(a() * 2) end) count.b += 1 count.c += 1 end) effect(function() batch(function() d(b() + c()) end) count.d += 1 end) effect(function() batch(function() e(d() % 2 == 0 and 1 or 0) f(d() * 2) end) count.e += 1 count.f += 1 end) effect(function() batch(function() g(e() + f()) end) count.g += 1 end) a(1) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(count.e == 2) CHECK(count.f == 2) CHECK(count.g == 2) CHECK(d() == 2) CHECK(g() == 5) a(3) CHECK(count.b == 3) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(count.e == 3) CHECK(count.f == 3) CHECK(count.g == 3) CHECK(d() == 6) CHECK(g() == 13) end end)) TEST("read()", wrap_root(function() do CASE "read primitive" CHECK(read(1) == 1) end do CASE "read source" local src = source(1) :: () -> number CHECK(read(src) == 1) end do CASE "push_scope_as_child_of source" local src = source(0) local count = 0 effect(function() read(src) count += 1 end) src(1) CHECK(count == 2) end end)) TEST("context()", function() do CASE "set context" local ctx = context() root(function() ctx(1, function() CHECK(ctx() == 1) effect(function() CHECK(ctx() == 1) end) end) end) end do CASE "set context outside of scope" local ctx = context() local ok = pcall(function() ctx(1, function() end) end) CHECK(not ok) end do CASE "get default context" local ctx = context(1) CHECK(ctx() == 1) root(function() local v = ctx(2, function() CHECK(ctx() == 2) return ctx() end) CHECK(v == 2) CHECK(ctx() == 1) end) end do CASE "context cascade" local ctx = context(1) local ctx2 = context() root(function() ctx(2, function() ctx2(true, function() show(function() return true end, function() ctx(3, function() effect(function() CHECK(ctx() == 3) untrack(function() effect(function() CHECK(ctx() == 3) end) CHECK(ctx2() == true) return {} end) end) CHECK(ctx() == 3) end) CHECK(ctx() == 2) return {} end) end) CHECK(ctx() == 2) end) CHECK(ctx() == 1) end) end do CASE "nil context" local ctx = context(nil) root(function() CHECK(ctx() == nil) ctx(nil, function() CHECK(ctx() == nil) ctx(true :: any, function() CHECK(ctx() == true) ctx(nil, function() effect(function() untrack(function() effect(function() CHECK(ctx() == nil) end) return {} end) end) end) end) end) end) end end) TEST("nested effects cases", function() local ran = 0 local cleaned = 0 local function Count() local count = source(0) effect(function() count() ran += 1 cleanup(function() cleaned += 1 end) end) return nil end local function App(destroy) local name = source "a" effect(function() name() untrack(Count) end) CHECK(ran == 1) CHECK(cleaned == 0) name "b" CHECK(ran == 2) CHECK(cleaned == 1) destroy() CHECK(ran == 2) CHECK(cleaned == 2) end root(App) end) TEST("graph edge cases", wrap_root(function() do CASE "diamond A,B,C,D" --[[ a > b > d > c > ]] local a = source(0) local b = derive(function() return (a() % 2 == 0) and 1 or 0 end) local c = derive(function() return a() * 2 end) local d = derive(function() return b() + c() end) local count = { b = 0, c = 0, d = 0 } effect(function() b(); count.b += 1 end) effect(function() c(); count.c += 1 end) effect(function() d(); count.d += 1 end) a(1) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(d() == 2) a(3) CHECK(count.b == 2) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(d() == 6) end do CASE "diamond A,B,C,D,E" --[[ a > b > e > c > d > ]] local a = source(0) local b = derive(function() return (a() % 2 == 0) and 1 or 0 end) local c = derive(function() return a() * 2 end) local d = derive(function() return c() * 2 end) local e = derive(function() return b() + d() end) local count = { b = 0, c = 0, d = 0, e = 0 } effect(function() b(); count.b += 1 end) effect(function() c(); count.c += 1 end) effect(function() d(); count.d += 1 end) effect(function() e(); count.e += 1 end) CHECK(e() == 1) a(1) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(count.e == 3) -- todo: redundant re-eval CHECK(e() == 4) a(3) CHECK(count.b == 2) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(count.e == 4) CHECK(e() == 12) end do CASE "repeated read" local a = source(0) local b = derive(function() return a() + a() end) local count = 0 effect(function() b(); count += 1 end) a(1) CHECK(b() == 2) CHECK(count == 2) end do CASE "do not destroy children" local parent = source(0) local _, destroy, parent_to_destroy, update_parent_to_destroy = root(function(destroy) local src = source(0) return destroy, derive(function() return src() end), src end) local count = 0 effect(function() count += 1 parent() parent_to_destroy() end) parent(parent() + 1) CHECK(count == 2) update_parent_to_destroy(1) CHECK(count == 3) destroy() update_parent_to_destroy(2) CHECK(count == 3) parent(parent() + 1) CHECK(count == 4) end do CASE "double destroy" -- issue: -- parent evaluates -- child A queued -- child B queued -- child A destroys child B -- child B reevaluates due to already being queued -- parent destroys, destroys child B - uh oh local _, destroy_parent, parent, update_parent = root(function(destroy) local src = source(0) return destroy, derive(function() return src() end), src end) local _, destroy_child, _child_B = nil, function() end, nil local count_A = 0 -- child_A effect(function() count_A += 1 parent() destroy_child() end) local count_B = 0 destroy_child, _child_B = root(function(destroy) return destroy, derive(function() count_B += 1; return parent() end) end) update_parent(parent() + 1) CHECK(count_A == 2) CHECK(count_B == 1) -- child B should not run again destroy_parent() -- should not error CHECK(true) end end)) TEST("strict", wrap_root(function() vide.strict = true do CASE "error on derived callback yield" local src = source(1) local ok = pcall(function() local _derived = derive(function() coroutine.yield() return src() end) end) CHECK(not ok) end do CASE "error on effecter callback yield" local src = source(1) local ok = pcall(function() effect(function() coroutine.yield() src() end) end) CHECK(not ok) end do CASE "run derived callback twice" local src = source(1) local count = 0 local _ = derive(function() count += 1 return src() end) CHECK(count == 2) src(2) CHECK(count == 4) end do CASE "run effect callback twice" local src = source(1) local count = 0 effect(function() count += 1 src() end) CHECK(count == 2) src(2) CHECK(count == 4) end do CASE "values() error if duplicate" local src = source { 1, 2, 1 } local ok = pcall(function() values(src, function() return {} end) end) CHECK(not ok) end do CASE "duplicate properties" local ok = pcall(function() create "TextLabel" { { Name = "foo" }, { Name = "bar" } } end) CHECK(not ok) ok = pcall(function() create "TextLabel" { { Name = "foo", { Name = "bar" } } } end) CHECK(ok) end do CASE "effect counter" local src = source(true) local count = 0 effect(function(x: number) src() count = x + 1 return count end, count) CHECK(count == 2) src(not src()) CHECK(count == 4) end do CASE "effect using derived source" local input = source(true) local output = derive(function() return input() end) local count = 0 effect(function() output() count += 1 end) CHECK(count == 2) input(false) CHECK(count == 4) end do CASE "destruction of active scope" local src = source(false) local count = 0 root(function() show(src, function() src(false) cleanup(function() count += 1 end) return {} end) end) local ok = pcall(function() src(true) end) CHECK(count == 0) CHECK(not ok) end do CASE "destruction of active scope in indexes" local src = source {} local count_1 = 0 local count_2 = 0 root(function() effect(function() untrack(function() indexes(src, function() cleanup(function() count_1 += 1 end) src {} cleanup(function() count_2 += 1 end) return {} end) end) end) end) local ok = pcall(function() src { 1 } end) CHECK(not ok) end do CASE "destruction of active scope in values" local src = source {} local count_1 = 0 local count_2 = 0 root(function() effect(function() untrack(function() values(src, function() cleanup(function() count_1 += 1 end) src {} cleanup(function() count_2 += 1 end) return {} end) end) end) end) local ok = pcall(function() src { {} } end) CHECK(not ok) end end)) local ok = FINISH() if not ok then error("Tests failed", 0) end return nil