vide/test/tests.luau
2025-03-15 11:25:03 -03:00

2936 lines
65 KiB
Text

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 graph = require "../../vide/src/graph"
type Node<T> = graph.Node<T>
type Map<K, V> = { [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 & {}): 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_child_to_scope = graph.push_child_to_scope
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
local push_cleanup = graph.push_cleanup
local destroy = graph.destroy
local function node<T>(owner: Node<any>?, v: T?)
return create_node(owner or false, function(x) return not x end, v or false :: any)
end
local function scope(owner: Node<any>?)
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_child_to_scope(a)
push_child_to_scope(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_child_to_scope(a)
push_child_to_scope(b)
count += 1
return not x
end
c.effect = effect
push_scope(c)
effect(c.cache)
pop_scope()
CHECK(count == 1)
update_descendants(a, true)
CHECK(count == 2)
update_descendants(b, true)
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_child_to_scope(a); pop_scope()
push_scope(c); push_child_to_scope(a); pop_scope()
push_scope(d); push_child_to_scope(b); push_child_to_scope(c); pop_scope()
update_descendants(a, true)
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_child_to_scope(a)
push_child_to_scope(b)
return not x
end
push_scope(c); assert(type(c.effect) == "function" and c.effect)(NIL); pop_scope()
update_descendants(a, true)
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_child_to_scope(items_updated) -- should not
do push_scope(items_updated)
push_child_to_scope(items)
do push_scope(root)
do push_scope(scope1)
clean "scope1"
bind1 = node(scope1)
do push_scope(bind1)
clean "bind1"
push_child_to_scope(selected)
pop_scope() end
pop_scope() end
do push_scope(scope2)
clean "scope2"
bind2 = node(scope2)
do push_scope(bind2)
clean "bind2"
push_child_to_scope(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<any>))
-- CHECK(table.find(c, scope2 :: Node<any>))
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<any>))
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, true)
return not x
end
push_child(a, b); push_child(a, c)
push_child(d, e); push_child(d, f)
update_descendants(a, true)
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 "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 = 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 "recursive create"
local set_test_to_true = vide.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()
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 "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)
vide.cleanup(function() print "destroyed" end)
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 == 8)
branch = 2
weapon { id = "1", enchant = "poison" }
CHECK(count == 10)
weapon { id = "1", enchant = nil }
CHECK(count == 10)
branch = 1
weapon { id = "1", enchant = "fire" }
CHECK(count == 14)
weapon(nil)
branch = 2
weapon { id = "1", enchant = "poison" }
CHECK(count == 22)
vide.strict = false
end
do CASE "alt" -- todo: move test
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)
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("batch()", wrap_root(function()
local source = vide.source
local derive = vide.derive
local effect = vide.effect
local batch = vide.batch
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()
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 "push_child_to_scope 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()
local root = vide.root
local context = vide.context
local effect = vide.effect
local untrack = vide.untrack
local show = vide.show
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 vide = require "../../vide"
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
local root = vide.root
do CASE "brother requires older brother"
--[[
A
^ ^
B <-- C
]]
local count = { b = 0, c = 0 }
local a = source(0)
local b
local function upd_c() count.c += 1; return a()* (b and b() or 0) end
local c = derive(upd_c)
local function upd_b() count.b += 1; return a()*2 end
b = derive(upd_b)
a(2)
print('CAVALO', count.b, count.c)
CHECK(count.b == 2)
CHECK(count.c == 2)
end
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 == 2)
CHECK(e() == 4)
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"
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
local root = vide.root
local show = vide.show
local create = vide.create
local source = vide.source
local derive = vide.derive
local effect = vide.effect
local indexes, values = vide.indexes, vide.values
local untrack = vide.untrack
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
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)
vide.cleanup(function() count += 1 end)
return {}
end)
end)
local ok = pcall(function()
src(true)
end)
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()
vide.cleanup(function() count_1 += 1 end)
src {}
vide.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()
vide.cleanup(function() count_1 += 1 end)
src {}
vide.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