mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
Deeper nested properties guaranteed to be processed after shallower nested properties. Also added strict mode checks for duplicate nested properties.
1282 lines
28 KiB
Lua
1282 lines
28 KiB
Lua
local testkit = require("test/testkit")
|
|
local TEST, CASE, CHECK, FINISH = testkit.test()
|
|
|
|
local mock = require "test/mock"
|
|
local Instance, Signal = mock.Instance, mock.Signal
|
|
|
|
local vide = require "src/init"
|
|
|
|
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
|
|
|
|
TEST("graph", function()
|
|
local graph = require "src/graph"
|
|
local create = graph.create
|
|
local get = graph.get
|
|
local set = graph.set
|
|
local capture = graph.capture
|
|
local capture_and_link = graph.capture_and_link
|
|
local link = graph.link
|
|
local set_effect = graph.set_effect
|
|
|
|
do CASE "node creation"
|
|
local node = create(1)
|
|
CHECK(get(node) == 1)
|
|
end
|
|
|
|
do CASE "node value"
|
|
local node = create(0)
|
|
set(node, 1)
|
|
CHECK(get(node) == 1)
|
|
set(node, 2)
|
|
CHECK(get(node) == 2)
|
|
end
|
|
|
|
do CASE "capture nodes"
|
|
local node1 = create(nil)
|
|
local node2 = create(nil)
|
|
local nodes = capture(function()
|
|
return get(node1), get(node2)
|
|
end)
|
|
CHECK(nodes[1] == node1)
|
|
CHECK(nodes[2] == node2)
|
|
end
|
|
|
|
do CASE "linking nodes"
|
|
local parent = create(1)
|
|
local child = create(0)
|
|
|
|
link(parent, child, function()
|
|
return get(parent)
|
|
end)
|
|
|
|
set(parent, get(parent) + 1)
|
|
CHECK(get(child) == 2) -- child should automatically update
|
|
end
|
|
|
|
do CASE "capture and link nodes"
|
|
local parent = create(1)
|
|
local child = create()
|
|
|
|
child.cache = capture_and_link(child, function()
|
|
return tostring(get(parent))
|
|
end)
|
|
|
|
set(parent, 2)
|
|
CHECK(get(child) == "2")
|
|
end
|
|
|
|
do CASE "nodes garbage collection"
|
|
local wref = weak { create(1) }
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do CASE "node effect garbage collection"
|
|
do
|
|
local wref
|
|
|
|
do
|
|
local function factory(p) -- factory function to prevent closure caching
|
|
return function()
|
|
return get(p)
|
|
end
|
|
end
|
|
|
|
local node = create(1)
|
|
|
|
do
|
|
local effect1 = factory(node)
|
|
local effect2 = factory(node)
|
|
|
|
wref = weak { e1 = effect1, e2 = effect2, n = node}
|
|
|
|
set_effect(node, effect1, {})
|
|
set_effect(node, effect2, true)
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref.e1) -- effect1 should gc since nothing is referencing table `t`
|
|
CHECK(wref.e2) -- effect2 should not gc as `true` is not garbage collectable
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref.n and not wref.e2) -- node should now gc along with effect2
|
|
end
|
|
|
|
do
|
|
local wref
|
|
|
|
do -- same test but for multiple nodes referenced by watcher
|
|
local function factory(a, b)
|
|
return (function(c, d)
|
|
return function()
|
|
return get(c), get(d)
|
|
end
|
|
end)(a, b)
|
|
end
|
|
|
|
local node1 = create(1)
|
|
local node2 = create(1)
|
|
|
|
do
|
|
local effect = factory(node1, node2)
|
|
|
|
wref = weak { n1 = node1, n2 = node2, e = effect }
|
|
|
|
local t1 = {}
|
|
set_effect(node1, effect, t1)
|
|
set_effect(node2, effect, t1)
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref.e)
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref.n1)
|
|
CHECK(not wref.n2)
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("source()", function()
|
|
local source = vide.source
|
|
local watch = vide.watch
|
|
|
|
do CASE "create source"
|
|
local state = source(1)
|
|
CHECK(state() == 1)
|
|
end
|
|
|
|
do CASE "set and get source value"
|
|
local state = source(1)
|
|
state(2)
|
|
CHECK(state() == 2)
|
|
end
|
|
|
|
do CASE "does not update if same value"
|
|
local state = source(1)
|
|
|
|
local updates = -1
|
|
watch(function()
|
|
state()
|
|
updates += 1
|
|
end)
|
|
|
|
CHECK(updates == 0)
|
|
state(1)
|
|
CHECK(updates == 0)
|
|
state(2)
|
|
CHECK(updates == 1)
|
|
end
|
|
|
|
do CASE "does update if same value is table"
|
|
local state = source {}
|
|
|
|
local updates = -1
|
|
watch(function()
|
|
state()
|
|
updates += 1
|
|
end)
|
|
|
|
CHECK(updates == 0)
|
|
state(state())
|
|
CHECK(updates == 1)
|
|
end
|
|
|
|
do CASE "does not update if same value is frozen table"
|
|
local a = table.freeze {}
|
|
local b = table.freeze {}
|
|
|
|
local state = source(a)
|
|
|
|
local updates = -1
|
|
watch(function()
|
|
state()
|
|
updates += 1
|
|
end)
|
|
|
|
CHECK(updates == 0)
|
|
state(a)
|
|
CHECK(updates == 0)
|
|
state(b)
|
|
CHECK(updates == 1)
|
|
state(b)
|
|
CHECK(updates == 1)
|
|
end
|
|
end)
|
|
|
|
TEST("derive()", function()
|
|
local source = vide.source
|
|
local derive = vide.derive
|
|
|
|
do CASE "derive new value on source change"
|
|
local inputA = source(1)
|
|
local inputB = source(2)
|
|
|
|
local output = derive(function()
|
|
return tostring(inputA() + inputB())
|
|
end)
|
|
|
|
CHECK(output() == "3")
|
|
inputA(2)
|
|
CHECK(output() == "4")
|
|
end
|
|
|
|
do CASE "derive wrapped source"
|
|
local input = source(1)
|
|
|
|
local transform = function()
|
|
return tostring(input())
|
|
end
|
|
|
|
local output = derive(function()
|
|
return tonumber(transform())
|
|
end)
|
|
|
|
CHECK(output() == 1)
|
|
input(2)
|
|
CHECK(output() == 2)
|
|
end
|
|
|
|
do CASE "garbage collection"
|
|
do -- check that `b` does not allow gc of `a`
|
|
local wref, b
|
|
|
|
do
|
|
local a = source(1)
|
|
|
|
b = derive(function()
|
|
return a()
|
|
end)
|
|
|
|
wref = weak { a }
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
b()
|
|
end
|
|
|
|
do -- check that `a` allows gc of `b`
|
|
local a = source(1)
|
|
|
|
local wref
|
|
|
|
do
|
|
local b = derive(function()
|
|
return a()
|
|
end)
|
|
|
|
wref = weak { b }
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
end
|
|
|
|
do CASE "garbage collection 2"
|
|
-- creats a chain `a -> b -> c` where `a` is the source
|
|
local function setup()
|
|
local a = source(0)
|
|
|
|
local b = derive(function()
|
|
return a()
|
|
end)
|
|
|
|
local c = derive(function()
|
|
return b()
|
|
end)
|
|
|
|
return weak { a, b, c }, a, b, c
|
|
end
|
|
|
|
do -- check that `b` and `c` can gc if `a` is referenced
|
|
local wref, _a = setup()
|
|
|
|
gc()
|
|
CHECK(not wref[2])
|
|
CHECK(not wref[3])
|
|
end
|
|
|
|
do -- check that `a` and `b` wont gc if `c` is referenced
|
|
local weak, _a, _b, _c = setup()
|
|
|
|
_a, _b = nil :: any, nil :: any
|
|
|
|
gc()
|
|
CHECK(weak[1])
|
|
CHECK(weak[2])
|
|
end
|
|
|
|
do -- check that `b` wont gc if `a` and `c` are referenced
|
|
local weak, a, _b, c = setup()
|
|
|
|
_b = nil :: any
|
|
|
|
gc()
|
|
CHECK(weak[2])
|
|
|
|
a(2)
|
|
CHECK(c() == 2)
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("watch()", function()
|
|
local source = vide.source
|
|
local watch = vide.watch
|
|
local cleanup = vide.cleanup
|
|
|
|
do CASE "capture sourcess"
|
|
local a = source(1)
|
|
local b = source(1)
|
|
|
|
local runcount = -1
|
|
watch(function()
|
|
a()
|
|
b()
|
|
runcount += 1
|
|
end)
|
|
|
|
CHECK(runcount == 0)
|
|
a(2)
|
|
CHECK(runcount == 1)
|
|
b(2)
|
|
CHECK(runcount == 2)
|
|
end
|
|
|
|
do CASE "stop watch"
|
|
local a = source(1)
|
|
|
|
local runcount = -1
|
|
local unwatch = watch(function()
|
|
a()
|
|
runcount += 1
|
|
end)
|
|
|
|
unwatch()
|
|
a(2)
|
|
CHECK(runcount == 0)
|
|
end
|
|
|
|
do CASE "side-effect cleanup"
|
|
local state = source(1)
|
|
|
|
local effect_runcount = 0
|
|
local cleanup_runcount = 0
|
|
|
|
local unwatch = watch(function()
|
|
state()
|
|
effect_runcount += 1
|
|
cleanup(function() cleanup_runcount += 1 end)
|
|
end)
|
|
|
|
CHECK(effect_runcount == 1)
|
|
CHECK(cleanup_runcount == 0)
|
|
state(2)
|
|
CHECK(effect_runcount == 2)
|
|
CHECK(cleanup_runcount == 1)
|
|
|
|
unwatch()
|
|
unwatch = nil :: any
|
|
gc()
|
|
vide.step(0)
|
|
|
|
CHECK(effect_runcount == 2)
|
|
CHECK(cleanup_runcount == 2)
|
|
end
|
|
|
|
do CASE "garbage collection"
|
|
local function factory(p)
|
|
return function()
|
|
p()
|
|
end
|
|
end
|
|
|
|
do -- state prevents gc of watcher
|
|
local state = source(1)
|
|
|
|
local wref
|
|
|
|
do
|
|
local effect = factory(state)
|
|
watch(effect)
|
|
wref = { effect }
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
end
|
|
|
|
do -- watcher can gc if stopped
|
|
local state = source(1)
|
|
|
|
local wref, unwatch
|
|
|
|
do
|
|
local effect = factory(state)
|
|
unwatch = watch(effect)
|
|
wref = weak { effect }
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
|
|
unwatch()
|
|
unwatch = nil :: any -- unwatch holds ref to effect
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
|
|
end
|
|
|
|
do -- state can gc with watcher
|
|
local wref
|
|
|
|
do
|
|
local state = source(1)
|
|
local effect = factory(state)
|
|
watch(effect)
|
|
wref = weak { state }
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("cleanup()", function()
|
|
local source = vide.source
|
|
local watch = vide.watch
|
|
local cleanup = vide.cleanup
|
|
|
|
do CASE "cleanup runs for watcher"
|
|
local state = source(1)
|
|
|
|
local watched = 0
|
|
local cleaned = 0
|
|
|
|
local stop = watch(function()
|
|
state()
|
|
watched += 1
|
|
cleanup(function()
|
|
cleaned += 1
|
|
end)
|
|
end)
|
|
|
|
CHECK(watched == 1)
|
|
CHECK(cleaned == 0)
|
|
|
|
state(2)
|
|
|
|
CHECK(watched == 2)
|
|
CHECK(cleaned == 1)
|
|
|
|
stop()
|
|
|
|
do -- vide detects by iterating through and checking for gc'd refs
|
|
stop = nil :: any
|
|
gc()
|
|
vide.step(0)
|
|
end
|
|
|
|
CHECK(watched == 2)
|
|
CHECK(cleaned == 2)
|
|
end
|
|
|
|
do CASE "scoped"
|
|
local function setup()
|
|
local state = source(1)
|
|
local obj = { cleaned = 0 }
|
|
|
|
local _stop = watch(function()
|
|
state()
|
|
cleanup(function()
|
|
obj.cleaned += 1
|
|
end)
|
|
end)
|
|
|
|
return state, obj
|
|
end
|
|
|
|
local stateA, objA = setup()
|
|
local stateB, objB = setup()
|
|
|
|
CHECK(objA.cleaned == 0)
|
|
CHECK(objB.cleaned == 0)
|
|
|
|
stateA(2)
|
|
|
|
CHECK(objA.cleaned == 1)
|
|
CHECK(objB.cleaned == 0)
|
|
|
|
stateB(2)
|
|
|
|
CHECK(objA.cleaned == 1)
|
|
CHECK(objB.cleaned == 1)
|
|
|
|
do
|
|
stateA = nil :: any
|
|
stateB = nil :: any
|
|
gc()
|
|
vide.step(0)
|
|
end
|
|
|
|
CHECK(objA.cleaned == 2)
|
|
CHECK(objB.cleaned == 2)
|
|
end
|
|
|
|
do CASE "multiple cleanup"
|
|
local state = source(1)
|
|
|
|
local queue = {}
|
|
|
|
watch(function()
|
|
state()
|
|
cleanup(function() table.insert(queue, 1) end)
|
|
cleanup(function() table.insert(queue, 2) end)
|
|
end)
|
|
|
|
CHECK(testkit.seq(queue, {}))
|
|
state(2)
|
|
CHECK(testkit.seq(queue, { 1, 2 }))
|
|
state(3)
|
|
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
|
|
|
|
do
|
|
state = nil :: any
|
|
gc()
|
|
vide.step(0)
|
|
end
|
|
|
|
-- todo: guarantee call order when gc? (currently not)
|
|
--testkit.print2(queue)
|
|
--CHECK(testkit.seq(queue, { 1, 2, 1, 2, 1, 2 }))
|
|
end
|
|
end)
|
|
|
|
TEST("create()", function()
|
|
local create = vide.create
|
|
local source = vide.source
|
|
|
|
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" },
|
|
Group = { Text = "test" }
|
|
}
|
|
CHECK(text.Name == "Label")
|
|
CHECK(text.Text == "test")
|
|
end
|
|
|
|
do CASE "nested precedence"
|
|
local text = create "TextLabel" {
|
|
Group = {
|
|
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" }
|
|
}
|
|
},
|
|
Children = {
|
|
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 garbage collection"
|
|
do -- instance should gc when unparented
|
|
local state = source("Hi")
|
|
|
|
local wref = weak {
|
|
create "TextLabel" {
|
|
Text = state,
|
|
}
|
|
}
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do -- instance should not gc when parented
|
|
local state = source("Hi")
|
|
|
|
local parent = create "Frame" {}
|
|
|
|
local wref = weak {
|
|
create "TextLabel" {
|
|
Parent = parent,
|
|
Text = state,
|
|
}
|
|
}
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
|
|
wref[1].Parent = nil
|
|
wref[1].Parent = parent
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
|
|
wref[1]:Destroy()
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do -- instance does not allow gc of state
|
|
local label
|
|
local wref
|
|
|
|
do
|
|
local state = source("Hi")
|
|
label = create "TextLabel" {
|
|
Name = state,
|
|
}
|
|
wref = weak { state :: any, label }
|
|
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[2])
|
|
CHECK(wref[1])
|
|
end
|
|
|
|
do -- state and instance should gc once both exit scope
|
|
local wref
|
|
|
|
do
|
|
local text = source("Hi")
|
|
|
|
local box = create "TextLabel" {
|
|
Text = text,
|
|
}
|
|
|
|
wref = weak { text = text, box = box}
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref.text)
|
|
CHECK(not wref.box)
|
|
end
|
|
|
|
do -- binding should gc despite state still existing after instance is gc
|
|
local state = source("Hi")
|
|
|
|
local node = require "src/graph".capture(state)[1]
|
|
|
|
local wref
|
|
|
|
do
|
|
local instance = create "TextLabel" {
|
|
Text = state,
|
|
}
|
|
|
|
wref = weak {
|
|
instance = instance,
|
|
binding = next(node.effects)
|
|
}
|
|
end
|
|
|
|
CHECK(wref.binding)
|
|
|
|
gc()
|
|
CHECK(not wref.instance)
|
|
CHECK(not wref.binding)
|
|
end
|
|
end
|
|
|
|
do CASE "bind same state to multiple instance properties"
|
|
local state = source "1"
|
|
|
|
local text = create "TextBox" {
|
|
Name = state,
|
|
Text = state,
|
|
PlaceholderText = state
|
|
}
|
|
|
|
state "2"
|
|
|
|
CHECK(text.Name == "2")
|
|
CHECK(text.Text == "2")
|
|
CHECK(text.PlaceholderText == "2")
|
|
end
|
|
|
|
do CASE "bind children"
|
|
local state = source()
|
|
|
|
local a, b, c =
|
|
create "TextLabel" { Name = "A" },
|
|
create "TextLabel" { Name = "B" },
|
|
create "TextLabel" { Name = "C" }
|
|
|
|
local frame = create "Frame" {
|
|
state
|
|
}
|
|
|
|
state { a, b }
|
|
|
|
CHECK(frame:FindFirstChild "A")
|
|
CHECK(frame:FindFirstChild "B")
|
|
|
|
-- check that b is removed and c is added while a remains untouched
|
|
|
|
state { a, c }
|
|
|
|
CHECK(frame:FindFirstChild "A")
|
|
CHECK(frame:FindFirstChild "C")
|
|
CHECK(not frame:FindFirstChild "B")
|
|
|
|
state(nil)
|
|
|
|
CHECK(#frame:GetChildren() == 0)
|
|
end
|
|
|
|
--[[
|
|
do CASE "Parent set to nil by state does not allow gc"
|
|
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)
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
|
|
wref[1]:Destroy()
|
|
|
|
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("indexes()", function()
|
|
local source = vide.source
|
|
local indexes = vide.indexes
|
|
|
|
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 runcount = table.create(3, 0)
|
|
|
|
local output = indexes(input, function(v, i)
|
|
runcount[i] += 1
|
|
return v
|
|
end)
|
|
|
|
input { 1, 2, 4 }
|
|
|
|
CHECK(output()[1]() == 1)
|
|
CHECK(output()[2]() == 2)
|
|
CHECK(output()[3]() == 4)
|
|
|
|
CHECK(runcount[1] == 1)
|
|
CHECK(runcount[2] == 1)
|
|
CHECK(runcount[3] == 1)
|
|
end
|
|
|
|
do CASE "removal reflected"
|
|
local input = source { 1, 2, 3 }
|
|
|
|
local output = indexes(input, function(v, i)
|
|
return v
|
|
end)
|
|
|
|
input { 1, 2 }
|
|
|
|
local t = output()
|
|
|
|
CHECK(t[1]() == 1)
|
|
CHECK(t[2]() == 2)
|
|
CHECK(t[3] == nil)
|
|
end
|
|
|
|
do CASE "garbage collection"
|
|
do -- check that `output` does not allow gc of `input`
|
|
local input = source {}
|
|
|
|
local _derived = indexes(input, function(i, v)
|
|
return v
|
|
end)
|
|
|
|
local wref = weak { input }
|
|
|
|
input = nil :: any
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
end
|
|
|
|
do -- check that `input` allows gc of `output`
|
|
local input = source {}
|
|
|
|
local output = indexes(input, function(i, v)
|
|
return i, v
|
|
end)
|
|
|
|
local wref = weak { output }
|
|
|
|
output = nil :: any
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("values()", function()
|
|
local source = vide.source
|
|
local values = vide.values
|
|
|
|
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 runcount = table.create(3, 0)
|
|
|
|
local output = values(input, function(v, i)
|
|
runcount[v] += 1
|
|
return i
|
|
end)
|
|
|
|
input { 1, 3, 2 }
|
|
|
|
CHECK(output()[1]() == 1)
|
|
CHECK(output()[2]() == 3)
|
|
CHECK(output()[3]() == 2)
|
|
|
|
CHECK(runcount[1] == 1)
|
|
CHECK(runcount[2] == 1)
|
|
CHECK(runcount[3] == 1)
|
|
end
|
|
|
|
do CASE "removal reflected"
|
|
local input = source { 1, 2, 3 }
|
|
|
|
local output = values(input, function(v, i)
|
|
return { v = v, i = i }
|
|
end)
|
|
|
|
input { 1, 2 }
|
|
|
|
local t = output()
|
|
|
|
CHECK(t[1].v == 1)
|
|
CHECK(t[2].v == 2)
|
|
CHECK(t[3] == nil)
|
|
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
|
|
end)
|
|
|
|
TEST("spring()", function()
|
|
local create = vide.create
|
|
local source = vide.source
|
|
local spring = vide.spring
|
|
local watch = vide.watch
|
|
|
|
do CASE "update source (on next step)"
|
|
local value = source(10)
|
|
local springed = spring(value, 1, 1)
|
|
|
|
value(20)
|
|
CHECK(springed() == 10)
|
|
vide.step(1/60)
|
|
CHECK(springed() ~= 10)
|
|
CHECK(springed() > 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 :: any
|
|
|
|
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 :: any
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
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 :: any
|
|
|
|
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(6) -- spring finished, should be internally removed from queue
|
|
CHECK(output() == input()) -- check spring is at target
|
|
|
|
local count = -1
|
|
watch(function()
|
|
output()
|
|
count += 1
|
|
end)
|
|
|
|
vide.step(1) -- attempt to cause another spring update
|
|
CHECK(count == 1) -- 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 == 2) -- check spring was rescheduled correctly
|
|
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
|
|
|
|
create "Frame" {
|
|
action(function(self)
|
|
ran = true
|
|
end, 1)
|
|
}
|
|
|
|
CHECK(ran)
|
|
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("strict", function()
|
|
vide.strict = true
|
|
|
|
local create = vide.create
|
|
local source = vide.source
|
|
local derive = vide.derive
|
|
local watch = vide.watch
|
|
local indexes, values = vide.indexes, vide.values
|
|
|
|
do CASE "error on derived callback yield"
|
|
local state = source(1)
|
|
|
|
local ok = pcall(function()
|
|
local _derived = derive(function()
|
|
coroutine.yield()
|
|
return state()
|
|
end)
|
|
end)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "error on watcher callback yield"
|
|
local state = source(1)
|
|
|
|
local ok = pcall(function()
|
|
local _derived = watch(function()
|
|
coroutine.yield()
|
|
state()
|
|
end)
|
|
end)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "run derived callback twice"
|
|
local state = source(1)
|
|
local runcount = 0
|
|
|
|
local _ = derive(function()
|
|
runcount += 1
|
|
return state()
|
|
end)
|
|
|
|
CHECK(runcount == 2)
|
|
state(2)
|
|
CHECK(runcount == 4)
|
|
end
|
|
|
|
do CASE "run watcher callback twice"
|
|
local state = source(1)
|
|
local runcount = 0
|
|
|
|
watch(function()
|
|
runcount += 1
|
|
state()
|
|
end)
|
|
|
|
CHECK(runcount == 2)
|
|
state(2)
|
|
CHECK(runcount == 4)
|
|
end
|
|
|
|
do CASE "indexes() error if primitive"
|
|
local state = source { 1 }
|
|
|
|
local ok = pcall(function()
|
|
indexes(state, function() return 1 end)
|
|
end)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "values() error if duplicate"
|
|
local state = source { 1, 2, 1 }
|
|
|
|
local ok = pcall(function()
|
|
values(state, 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
|
|
end)
|
|
|
|
local ok = FINISH()
|
|
if not ok then error("Tests failed", 0) end
|
|
|
|
return nil
|