local testkit = require("test/testkit") local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local mock = require "test/mock" local Instance, Signal = mock.Instance, mock.Signal local Vector2, UDim2 = mock.Vector2, mock.UDim2 local vide = require "src/init" local graph = require "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 --SKIP "graph edge cases" TEST("graph", function() local create_node = graph.create_node local track = graph.track local update = graph.update local add_child = graph.add_child local get_scope = graph.get_scope local open_scope = graph.open_scope local close_scope = graph.close_scope local set_owner = graph.set_owner local get_children = graph.get_children local add_cleanup = graph.add_cleanup local destroy = graph.destroy local function node(v: T?) return create_node(v or false, function(x) return not x end) end local function scope() return create_node(false, false) end local function cleanup(fn: () -> ()) local node = assert(get_scope()) add_cleanup(node, fn) end do CASE "link nodes" local a = node() local b = node() local c = node() open_scope(c) track(a) track(b) close_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() local c = node() set_owner(b, root) set_owner(c, root) local count = 0 local function effect(x) track(a) track(b) count += 1 return not x end c.effect = effect open_scope(c) effect(c.cache) close_scope() CHECK(count == 1) update(a) CHECK(count == 2) update(b) CHECK(count == 3) end do CASE "diamond graph" -- a -> b -> d -- -> c local root = node() local a, b, c, d = node(), node(), node(), node() set_owner(b, root) set_owner(c, root) set_owner(d, 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 open_scope(b); track(a); close_scope() open_scope(c); track(a); close_scope() open_scope(d); track(b); track(c); close_scope() update(a) CHECK(b_cnt == 1) CHECK(c_cnt == 1) CHECK(d_cnt == 1) end do CASE "diamond graph 2" -- todo: include cached value from parent nodes to confirm update order -- a -> b -> c -> e -- -> d local root = node() local a, b, c, d, e = node(), node(), node(), node(), node() set_owner(b, root) set_owner(c, root) set_owner(d, root) set_owner(e, root) local b_cnt, c_cnt, d_cnt, e_cnt = 0, 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 function e.effect(x) e_cnt += 1; return not x end open_scope(b); track(a); close_scope() open_scope(c); track(b); close_scope() open_scope(d); track(a); close_scope() open_scope(e); track(c); track(d); close_scope() update(a) CHECK(b_cnt == 1) CHECK(c_cnt == 1) CHECK(d_cnt == 1) CHECK(e_cnt == 1) end do CASE "duplicate child on rerun" local root = node() local a, b, c = node(), node(), node() set_owner(a, root) set_owner(b, root) set_owner(c, root) function c.effect(x) track(a) track(b) return not x end open_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); close_scope() update(a) CHECK(#get_children(a) == 1) CHECK(#get_children(b) == 1) end do CASE "case 1" -- construct graph local items = node { "a", "b" } local selected = node "a" local root = scope() local scope1 = scope() local scope2 = scope() local items_updated local bind1 local bind2 local cleaned = {} :: { [any]: any } local function clean(s) cleanup(function() cleaned[s] = true end) end do open_scope(root) clean "root" items_updated = node() track(items_updated) -- should not set_owner(items_updated, root) do open_scope(items_updated) track(items) do open_scope(root) set_owner(scope1, root) do open_scope(scope1) clean "scope1" bind1 = node() set_owner(bind1, scope1) do open_scope(bind1) clean "bind1" track(selected) close_scope() end close_scope() end set_owner(scope2, root) do open_scope(scope2) clean "scope2" bind2 = node() set_owner(bind2, scope2) do open_scope(bind2) clean "bind2" track(selected) close_scope() end close_scope() end close_scope() end close_scope() end close_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)) CHECK(table.find(c, bind2)) 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(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 nothing ^ ]] local a, b, c, d, e, f = node(), node(), node(), node(), node(), node() local root = node() set_owner(a, root) set_owner(b, root) set_owner(c, root) set_owner(d, root) set_owner(e, root) set_owner(f, root) function b.effect(x) update(d) return not x end add_child(a, b); add_child(a, c) add_child(d, e); add_child(d, f) update(a) CHECK(true) end end) TEST("mount()", function() local mount = vide.mount local create = vide.create local source = vide.source local cleanup = vide.cleanup 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 root = vide.root local cleanup = vide.cleanup local count = 0 root(function(destroy) cleanup(function() count += 1 end) destroy() end) CHECK(count == 1) end) TEST("source()", wrap_root(function() local source = vide.source local effect = vide.effect 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() local source = vide.source local derive = vide.derive local effect = vide.effect local cleanup = vide.cleanup 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 = vide.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() local source = vide.source local effect = vide.effect local derive = vide.derive 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() local root = vide.root local source = vide.source local effect = vide.effect local cleanup = vide.cleanup do CASE "root cleanup" local count = 0 local destroy = vide.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() local create = vide.create local source = vide.source local cleanup = vide.cleanup 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 "aggregate construction" local template = create "TextLabel" { AnchorPoint = Vector2.new(), Position = UDim2.new() } local text = create(template) { AnchorPoint = { 1, 2 }, Position = { 3, 4 } } CHECK(text.AnchorPoint == Vector2.new(1, 2)) CHECK(text.Position == UDim2.new(3, 4)) end do CASE "nested precedence" local text = create "TextLabel" { { Text = "1", { Text = "2" } } } CHECK(text.Text == "2") 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 "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 = vide.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 = vide.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 "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() local untrack = vide.untrack local cleanup = vide.cleanup local source = vide.source local effect = vide.effect local show = vide.show local root = vide.root do CASE "main" -- uses switch() internally, more extensive testing of scoping not needed local value = source("truey" :: unknown) local function one() return 1 end local function two() return 2 end local output = show(value, one, two) CHECK(output() == 1) value(nil) CHECK(output() == 2) end do CASE "alt" local visible = vide.source(true) local count = vide.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) return nil 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 end)) TEST("switch()", wrap_root(function() local source = vide.source local switch = vide.switch local effect = vide.effect local cleanup = vide.cleanup 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 end)) TEST("indexes()", wrap_root(function() local create = vide.create local source = vide.source local effect = vide.effect local indexes = vide.indexes local cleanup = vide.cleanup 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 = vide.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 output = indexes(input, function(v, i) return v, i end) 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 end)) TEST("values()", wrap_root(function() local create = vide.create local source = vide.source local values = vide.values local cleanup = vide.cleanup 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 end)) TEST("spring()", wrap_root(function() local create = vide.create local source = vide.source local spring = vide.spring local effect = vide.effect 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) vide.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) vide.step(0.05) CHECK(output() ~= input()) -- check spring is moving vide.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) vide.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 vide.step(0) -- process spring queue CHECK(count == 1) -- check spring was rescheduled correctly end end)) TEST("untrack()", wrap_root(function() local source = vide.source local effect = vide.effect local derive = vide.derive local untrack = vide.untrack local cleanup = vide.cleanup local root = vide.root 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 create = vide.create 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() local create = vide.create local action = vide.action 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() local root = vide.root local create = vide.create local source = vide.source local changed = vide.changed 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("read()", wrap_root(function() local source = vide.source local effect = vide.effect local read = vide.read :: any -- todo 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 "track source" local src = source(0) local count = 0 effect(function() read(src) count += 1 end) src(1) CHECK(count == 2) end end)) TEST("nested effects cases", function() local vide = require "src/init" local source = vide.source local effect = vide.effect local untrack = vide.untrack local cleanup = vide.cleanup local root = vide.root 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() local source = vide.source local derive = vide.derive local effect = vide.effect 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() print "ran b"; return (a() % 2 == 0) and 1 or 0 end) local c = derive(function() print "ran c"; return a() * 2 end) local d = derive(function() print "ran d"; return c() * 2 end) local e = derive(function() print "ran e"; return b() + c() 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() d(); count.e += 1 end) a(1) print(e()) CHECK(count.b == 2) CHECK(count.c == 2) CHECK(count.d == 2) CHECK(count.e == 2) CHECK(e() == 4) -- todo: solve e evaluating before d a(3) CHECK(count.b == 2) CHECK(count.c == 3) CHECK(count.d == 3) CHECK(count.e == 3) CHECK(e() == 12) end do CASE "repeated read" end end)) TEST("strict", wrap_root(function() vide.strict = true local create = vide.create local source = vide.source local derive = vide.derive local effect = vide.effect local indexes, values = vide.indexes, vide.values 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 "indexes() error if primitive" local src = source { 1 } local ok = pcall(function() indexes(src, function() return 1 end) end) CHECK(not ok) 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 end)) local ok = FINISH() if not ok then error("Tests failed", 0) end return nil