mirror of
https://github.com/centau/vide.git
synced 2026-08-20 14:41:37 +00:00
1212 lines
28 KiB
Lua
1212 lines
28 KiB
Lua
----------------------------------------------------------------------------------------------------------------------
|
|
-- unit.lua
|
|
----------------------------------------------------------------------------------------------------------------------
|
|
|
|
local TEST, CASE, CHECK, FINISH, SKIP = require("test/testkit").test()
|
|
|
|
local mock = require "test/mock"
|
|
local Signal = require "test/goodsignal"
|
|
local Instance, Vector3, Color3, Vector2, UDim2 = mock.Instance, mock.Vector3, mock.Color3, mock.Vector2, mock.UDim2
|
|
|
|
local vide = require "src/init"
|
|
|
|
-- force run garbage collector cycles
|
|
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
|
|
|
|
-- weak reference table used for gc tests
|
|
--local wref = setmetatable({}, { __mode = "kv" }) :: any
|
|
|
|
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 "Create"
|
|
local node = create(1)
|
|
CHECK(get(node) == 1)
|
|
end
|
|
|
|
do CASE "Read/write"
|
|
local node = create(0)
|
|
set(node, 1)
|
|
CHECK(get(node) == 1)
|
|
set(node, 2)
|
|
CHECK(get(node) == 2)
|
|
end
|
|
|
|
do CASE "Capture"
|
|
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 "Link"
|
|
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)
|
|
end
|
|
|
|
do CASE "Capture and link"
|
|
local parent = create(1)
|
|
local child = create(nil :: any)
|
|
|
|
capture_and_link(child, function()
|
|
return tostring(get(parent))
|
|
end)
|
|
|
|
set(parent, get(parent) + 1)
|
|
CHECK(get(child) == "2")
|
|
end
|
|
|
|
--[[
|
|
do CASE "Scoped captures"
|
|
-- table.find but uses `rawequal` since nodes have overloaded __eq metamethod
|
|
local function rawfind(t, x)
|
|
for i, v in ipairs(t) do
|
|
if rawequal(x, v) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local a = graph.create(1)
|
|
local b = graph.create(1)
|
|
|
|
graph.link(a, b, function() return get(a) end)
|
|
set(a, get(a) + 1) -- mark `b` for recomputation
|
|
|
|
-- `a` and `b` should be referenced
|
|
local captures = graph.capture(function(from) return from(b) end)
|
|
|
|
-- check that only `b` was referenced
|
|
CHECK(not rawfind(captures, a))
|
|
CHECK(rawfind(captures, b))
|
|
|
|
-- repeat for `capture_and_link`
|
|
local c = graph.create(1)
|
|
set(a, get(a) + 1) -- mark `b` for recomputation again
|
|
capture_and_link(c, function(from) return from(b) end)
|
|
-- check that only `b` was linked
|
|
CHECK(not rawfind(assert(a.__children), c))
|
|
CHECK(rawfind(assert(b.__children), c))
|
|
end
|
|
]]
|
|
|
|
do CASE "Nodes garbage collection"
|
|
local wref
|
|
do
|
|
wref = weak { create(1) }
|
|
end
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do CASE "Node effect garbage collection"
|
|
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 { effect1, effect2, node :: any }
|
|
|
|
set_effect(node, effect1, {})
|
|
set_effect(node, effect2, true)
|
|
end
|
|
|
|
gc()
|
|
|
|
CHECK(not wref[1]) -- effect1 should gc since nothing is referencing table `t`
|
|
CHECK(wref[2]) -- effect2 should not gc as `true` is not garbage collectable
|
|
end
|
|
|
|
gc()
|
|
CHECK(not wref[3] and not wref[2]) -- node should now gc along with effect2
|
|
|
|
do -- same test but for multiple nodes referenced by watcher
|
|
local function factory(a, b)
|
|
return function()
|
|
return get(a), get(b)
|
|
end
|
|
end
|
|
|
|
local node1 = create(1)
|
|
local node2 = graph.create(1)
|
|
local effect = factory(node1, node2)
|
|
wref.node1 = node1
|
|
wref.node2 = node2
|
|
wref.effect = effect
|
|
|
|
local t1 = {}
|
|
set_effect(node1, effect, t1)
|
|
set_effect(node2, effect, t1)
|
|
|
|
t1 = nil :: any
|
|
effect = nil :: any
|
|
|
|
gc()
|
|
|
|
CHECK(not wref.effect)
|
|
end
|
|
|
|
gc()
|
|
|
|
CHECK(not wref.node1)
|
|
CHECK(not wref.node2)
|
|
end
|
|
end)
|
|
|
|
TEST("wrap()", function()
|
|
local wrap = vide.wrap
|
|
local watch = vide.watch
|
|
|
|
do CASE "Wrap value"
|
|
local state = wrap(1)
|
|
CHECK(state() == 1)
|
|
end
|
|
|
|
do CASE "Setter"
|
|
local state = wrap(1)
|
|
state(2) -- set directly
|
|
CHECK(state() == 2)
|
|
end
|
|
|
|
do CASE "Does not update if same value"
|
|
local state = wrap(1)
|
|
|
|
local updates = -1
|
|
watch(function()
|
|
state()
|
|
updates += 1
|
|
end)
|
|
state(1)
|
|
CHECK(updates == 0)
|
|
state(2)
|
|
CHECK(updates == 1)
|
|
end
|
|
end)
|
|
|
|
TEST("derive()", function()
|
|
local wrap = vide.wrap
|
|
local derive = vide.derive
|
|
|
|
do CASE "Derive new value on state change"
|
|
local state = wrap(1)
|
|
|
|
local derived = derive(function()
|
|
return tostring(state())
|
|
end)
|
|
|
|
CHECK(derived() == "1") -- check initial run during detection
|
|
state(state() + 1)
|
|
CHECK(derived() == "2") -- check updates
|
|
end
|
|
|
|
do CASE "Derive from updated"
|
|
do
|
|
local a = wrap(1)
|
|
|
|
local b = derive(function()
|
|
return a() + 1
|
|
end)
|
|
|
|
a(2)
|
|
|
|
local c = derive(function()
|
|
return b() + 1
|
|
end)
|
|
|
|
CHECK(c() == 4)
|
|
end
|
|
end
|
|
|
|
--[[
|
|
do CASE "Cleanup"
|
|
local count, set = wrap(1)
|
|
|
|
local derived = derive(function(from)
|
|
return { Value = from(count), Destroyed = false }
|
|
end, function(v)
|
|
v.Destroyed = true
|
|
end)
|
|
|
|
local first = derived()
|
|
CHECK(first.Destroyed == false)
|
|
set(2)
|
|
local _ = derived() -- trigger recalc
|
|
CHECK(first.Destroyed == true)
|
|
end
|
|
]]
|
|
|
|
do CASE "Garbage collection"
|
|
do -- check that `b` does not allow gc of `a`
|
|
local a = wrap(1)
|
|
|
|
local _b = derive(function()
|
|
return a()
|
|
end)
|
|
|
|
local wref = weak { a }
|
|
a = nil :: any
|
|
|
|
gc()
|
|
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do -- check that `a` allows gc of `b`
|
|
local a = wrap(1)
|
|
|
|
local b = derive(function()
|
|
return a()
|
|
end)
|
|
|
|
local wref = weak { b }
|
|
b = nil :: any
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
end
|
|
|
|
do CASE "Garbage collection 2"
|
|
-- creats a chain `a -> b -> c` where `a` is the root
|
|
local function setup()
|
|
local a = wrap(1)
|
|
|
|
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()
|
|
|
|
a(2)
|
|
|
|
CHECK(weak[2])
|
|
CHECK(c() == 2)
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("watch()", function()
|
|
local wrap = vide.wrap
|
|
local watch = vide.watch
|
|
|
|
do CASE "Capture states"
|
|
local a = wrap(1)
|
|
local b = wrap(1)
|
|
local runcount = 0
|
|
|
|
watch(function()
|
|
a()
|
|
b()
|
|
runcount += 1
|
|
end)
|
|
|
|
CHECK(runcount == 1) -- immediate callback execution
|
|
a(2)
|
|
CHECK(runcount == 2)
|
|
b(2)
|
|
CHECK(runcount == 3)
|
|
end
|
|
|
|
do CASE "Stop watch"
|
|
local a = wrap(1)
|
|
local b = wrap(1)
|
|
local runcount = 0
|
|
|
|
local unwatch = watch(function()
|
|
a()
|
|
runcount += 1
|
|
end)
|
|
|
|
CHECK(runcount == 1)
|
|
unwatch()
|
|
a(2)
|
|
CHECK(runcount == 1)
|
|
end
|
|
|
|
do CASE "Side-effect cleanup"
|
|
local state = wrap(1)
|
|
|
|
local effect_runcount = 0
|
|
local cleanup_runcount = 0
|
|
|
|
local unwatch = watch(function()
|
|
state()
|
|
effect_runcount += 1
|
|
return 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()
|
|
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 = wrap(1)
|
|
|
|
local wref
|
|
|
|
do
|
|
local effect = factory(state)
|
|
watch(effect)
|
|
wref = { effect }
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[1]) -- should still exist
|
|
|
|
end
|
|
|
|
do -- watcher can gc if stopped
|
|
local state = wrap(1)
|
|
local effect = factory(state)
|
|
|
|
local unwatch = watch(effect)
|
|
|
|
local wref = weak { effect }
|
|
effect = nil :: any
|
|
|
|
gc()
|
|
CHECK(wref[1]) -- should still exist
|
|
unwatch(); unwatch = nil :: any
|
|
gc()
|
|
CHECK(not wref[1]) -- should gc
|
|
|
|
end
|
|
|
|
do -- state can gc with watcher
|
|
local wref
|
|
|
|
do
|
|
local state = wrap(1)
|
|
local effect = factory(state)
|
|
watch(effect)
|
|
wref = weak { state }
|
|
end
|
|
|
|
gc()
|
|
|
|
CHECK(not wref[1]) -- should gc
|
|
end
|
|
|
|
do -- watcher can gc if no state captured
|
|
local effect = factory(function() end)
|
|
|
|
watch(effect)
|
|
|
|
local wref = weak { effect }
|
|
effect = nil :: any
|
|
|
|
gc()
|
|
CHECK(not wref[1]) -- should gc
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("create()", function()
|
|
local create = vide.create
|
|
local wrap = vide.wrap
|
|
|
|
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 "Assign custom properties"
|
|
local text = create "TextLabel" {
|
|
Name = "Label",
|
|
Text = "test"
|
|
}
|
|
CHECK(text.Name == "Label")
|
|
CHECK(text.Text == "test")
|
|
end
|
|
|
|
do CASE "Independent"
|
|
local frame = create "Frame"
|
|
CHECK(frame {} ~= frame {})
|
|
end
|
|
|
|
do CASE "Assign children"
|
|
local frame = create "Frame" {
|
|
create "TextLabel" { Name = "A" } :: any,
|
|
create "TextLabel" { Name = "B" },
|
|
{
|
|
create "TextLabel" { Name = "C" } :: any,
|
|
create "TextLabel" { Name = "D" },
|
|
{
|
|
create "TextLabel" { Name = "E" }
|
|
}
|
|
}
|
|
}
|
|
CHECK(frame:FindFirstChild "A")
|
|
CHECK(frame:FindFirstChild "B")
|
|
CHECK(frame:FindFirstChild "C")
|
|
CHECK(frame:FindFirstChild "D")
|
|
CHECK(frame:FindFirstChild "E")
|
|
|
|
local image = create "ImageLabel" {
|
|
create "TextLabel" { Name = "A" }
|
|
}
|
|
CHECK(image:FindFirstChild "A")
|
|
end
|
|
|
|
do CASE "Binding properties to state"
|
|
local name = wrap("Hi")
|
|
local text = wrap("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 despite property bound to state
|
|
local state = wrap("Hi")
|
|
|
|
local wref = weak {
|
|
create "TextLabel" {
|
|
Text = state,
|
|
}
|
|
}
|
|
|
|
gc()
|
|
CHECK(not wref[1])
|
|
end
|
|
|
|
do -- instance should NOT gc despite property bound to state when parented
|
|
local state = wrap("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 -- state should not gc once exits scope while instance still exists
|
|
local _label
|
|
local wref
|
|
|
|
do
|
|
local state = wrap("Hi")
|
|
wref = weak { state }
|
|
_label = create "TextLabel" {
|
|
Name = state,
|
|
}
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref[1])
|
|
end
|
|
|
|
do -- state and instance should gc once both exit scope
|
|
local wref
|
|
|
|
do
|
|
local text = wrap("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 = wrap("Hi")
|
|
|
|
do
|
|
local instance = create "TextLabel" {
|
|
Text = state,
|
|
}
|
|
|
|
local wref = {
|
|
instance = instance,
|
|
brinding = next((state :: any).effects
|
|
}
|
|
|
|
wref.instance = instance
|
|
wref.binding = )
|
|
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 = wrap "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 = wrap({} :: {}?)
|
|
|
|
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"
|
|
-- this is technically a bug but we test for this anyways to confirm behavior
|
|
local parent = create "Frame" { Name = "Parent" }
|
|
local state = wrap(parent :: Frame?)
|
|
|
|
do
|
|
wref.child = create "Frame" { Parent = state, Name = "Child" } :: Frame?
|
|
end
|
|
|
|
gc()
|
|
CHECK(wref.child)
|
|
|
|
set(nil)
|
|
|
|
gc()
|
|
CHECK(wref.child)
|
|
|
|
wref.child:Destroy()
|
|
|
|
gc()
|
|
CHECK(not wref.child)
|
|
end
|
|
]]
|
|
|
|
do CASE "GC test"
|
|
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
|
|
wref.proxy = proxy
|
|
data = nil :: any
|
|
proxy = nil :: any
|
|
|
|
gc()
|
|
|
|
CHECK(wref.data and wref.proxy)
|
|
end
|
|
end)
|
|
|
|
TEST("map()", function()
|
|
local wrap = vide.wrap
|
|
local unwrap = vide.unwrap
|
|
local map = vide.map
|
|
|
|
do CASE "Use integer"
|
|
local n = 5
|
|
|
|
local t = map(n, function(i)
|
|
return tostring(i)
|
|
end)
|
|
|
|
for i = 1, n do
|
|
CHECK(tostring(i) == t[i])
|
|
end
|
|
end
|
|
|
|
do CASE "Use table"
|
|
local t = { 1, 2, 3, 4, 5 }
|
|
|
|
local t2 = map(t, function(_, v)
|
|
return tostring(v)
|
|
end)
|
|
|
|
for i, v in t do
|
|
CHECK(tostring(v) == t2[i])
|
|
end
|
|
end
|
|
|
|
do CASE "Use state"
|
|
local state = wrap { 1, 2, 3 }
|
|
|
|
local derived = map(state, function(_, v)
|
|
return tostring(v)
|
|
end)
|
|
|
|
local t = derived()
|
|
|
|
for i, v in next, state() do
|
|
CHECK(tostring(v) == t[i])
|
|
end
|
|
end
|
|
|
|
do CASE "Cache result"
|
|
local state, set = wrap { 1, 2, 3 }
|
|
|
|
local runcount = table.create(3, 0)
|
|
|
|
local derived = map(state, function(i, v)
|
|
runcount[i] += 1
|
|
return tostring(v)
|
|
end)
|
|
|
|
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
|
|
set { 1, 2, 4 }
|
|
|
|
local t = derived()
|
|
|
|
CHECK(t[1] == "1")
|
|
CHECK(t[2] == "2")
|
|
CHECK(t[3] == "4")
|
|
|
|
CHECK(runcount[1] == 1)
|
|
CHECK(runcount[2] == 1)
|
|
CHECK(runcount[3] == 2)
|
|
end
|
|
|
|
do CASE "Removal reflected"
|
|
local state, set = wrap { 1, 2, 3 }
|
|
|
|
local derived = map(state, function(i, v)
|
|
return tostring(v)
|
|
end)
|
|
|
|
local _ = derived() -- trigger evaluation (so the next set is forced to be re-calculated)
|
|
set { 1, 2 }
|
|
|
|
local t = derived()
|
|
|
|
CHECK(t[1] == "1")
|
|
CHECK(t[2] == "2")
|
|
CHECK(t[3] == nil)
|
|
end
|
|
|
|
local create = vide.create
|
|
local Children = vide.Children
|
|
|
|
do CASE "Bind children"
|
|
local state, set = wrap { "A", "B", "C" }
|
|
|
|
local derived = map(state, function(i, v)
|
|
return create "TextLabel" {
|
|
Name = v,
|
|
Text = tostring(i)
|
|
}
|
|
end)
|
|
|
|
local frame = create "Frame" {
|
|
Name = "21",
|
|
[Children] = derived
|
|
}
|
|
|
|
local function find(childname: string): TextLabel
|
|
return frame:FindFirstChild(childname) :: TextLabel
|
|
end
|
|
|
|
CHECK(find "A".Text == "1")
|
|
CHECK(find "B".Text == "2")
|
|
CHECK(find "C".Text == "3")
|
|
|
|
set { "A", "C", "D" }
|
|
|
|
CHECK(find "A".Text == "1")
|
|
CHECK(not find "B")
|
|
CHECK(find "C".Text == "2")
|
|
CHECK(find "D".Text == "3")
|
|
end
|
|
|
|
do CASE "Use optional destructor"
|
|
local state, set = wrap { 1, 2, 3 }
|
|
local derived = map(state, function(i, v)
|
|
return { Value = v, Destroyed = false }
|
|
end, function(v)
|
|
v.Destroyed = true
|
|
end)
|
|
|
|
local first = derived()
|
|
|
|
CHECK(first[1].Destroyed == false)
|
|
|
|
set { 1, 2, 4 }
|
|
|
|
local _ = derived()
|
|
|
|
CHECK(first[1].Destroyed == false)
|
|
CHECK(first[2].Destroyed == false)
|
|
CHECK(first[3].Destroyed == true)
|
|
end
|
|
|
|
do CASE "Garbage collection"
|
|
do -- check that `derived` does not allow gc of `state`
|
|
local state = wrap {}
|
|
|
|
local derived = map(state, function(i, v)
|
|
return v
|
|
end)
|
|
|
|
wref.state, state = state, nil :: any
|
|
wref.derived = derived
|
|
|
|
gc()
|
|
CHECK(wref.state)
|
|
end
|
|
|
|
do -- check that `state` allows gc of `derived`
|
|
local state = wrap {}
|
|
|
|
local derived = map(state, function(i, v)
|
|
return i, v
|
|
end) :: State?
|
|
|
|
wref.state = state
|
|
wref.derived, derived = derived, nil
|
|
|
|
gc()
|
|
CHECK(not wref.derived)
|
|
end
|
|
end
|
|
end)
|
|
|
|
TEST("apply()", function()
|
|
local apply = vide.apply
|
|
|
|
-- uses same application method as `create()` internally, further testing unnecessary
|
|
do CASE "Apply properties"
|
|
local part = Instance.new("Part") :: Part
|
|
|
|
apply(part) {
|
|
Position = Vector3.new(1, 1, 1),
|
|
Color = Color3.new(1, 0, 0)
|
|
}
|
|
|
|
CHECK(part.Position == Vector3.new(1, 1, 1))
|
|
CHECK(part.Color == Color3.new(1, 0, 0))
|
|
end
|
|
end)
|
|
|
|
TEST("spring()", function()
|
|
local wrap = vide.wrap
|
|
local unwrap = vide.unwrap
|
|
local spring = vide.spring
|
|
|
|
do CASE "Update state (on next hearbeat resumption cycle)"
|
|
local number, set = wrap(10)
|
|
local springed = spring(number, 1, 1)
|
|
|
|
set(20)
|
|
CHECK(springed() == 10)
|
|
vide.step(1/60)
|
|
CHECK(springed() ~= 10)
|
|
CHECK(springed() > 10)
|
|
end
|
|
|
|
do CASE "Garbage collection"
|
|
do -- `spring` should not allow gc of `state`
|
|
local state = wrap(10)
|
|
local _springed = spring(state, 1, 1)
|
|
|
|
wref.state, state = state, nil :: any
|
|
|
|
gc()
|
|
CHECK(wref.state)
|
|
|
|
end
|
|
|
|
do -- `number` should allow gc of `spring`
|
|
local number = wrap(10)
|
|
local springed = spring(number, 1, 1) :: State?
|
|
|
|
wref.springed, springed = springed, nil
|
|
|
|
gc()
|
|
CHECK(not wref.springed)
|
|
|
|
end
|
|
end
|
|
|
|
local create = vide.create
|
|
|
|
do CASE "Garbage collection (binded)"
|
|
local number = wrap(10)
|
|
local springed = spring(number, 1, 1) :: State?
|
|
|
|
local _label = create "TextLabel" {
|
|
Text = springed
|
|
}
|
|
|
|
wref.springed, springed = springed, nil
|
|
|
|
gc()
|
|
CHECK(wref.springed) -- `springed` should not gc
|
|
end
|
|
end)
|
|
|
|
TEST("Event", function()
|
|
local create = vide.create
|
|
local Event = vide.Event
|
|
local wrap = vide.wrap
|
|
|
|
do CASE "Connect event"
|
|
local connected = false
|
|
|
|
local event = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any }
|
|
|
|
local val = create "IntValue" {
|
|
Changed = event,
|
|
[Event.Changed] = function(newval)
|
|
connected = true
|
|
CHECK(newval == 1)
|
|
end
|
|
} :: IntValue
|
|
|
|
CHECK(not connected)
|
|
val.Value = 1; event:Fire(val.Value)
|
|
vide.step(1/60)
|
|
CHECK(connected)
|
|
end
|
|
|
|
do CASE "Bind connection to state"
|
|
local countA = 0
|
|
local countB = 0
|
|
local listener, set = wrap(function() countA += 1 end :: () -> ()?)
|
|
|
|
local event = (Signal.new() :: any) :: RBXScriptSignal & { Fire: any }
|
|
|
|
local int = create "IntValue" {
|
|
Changed = event,
|
|
[Event.Changed] = listener
|
|
} :: IntValue
|
|
|
|
int.Value = 1; event:Fire(int.Value)
|
|
int.Value = 2; event:Fire(int.Value)
|
|
CHECK(countA == 2)
|
|
set(function() return function() countB += 1 end end)
|
|
int.Value = 3; event:Fire(int.Value)
|
|
int.Value = 4; event:Fire(int.Value)
|
|
CHECK(countA == 2)
|
|
CHECK(countB == 2)
|
|
set(nil)
|
|
int.Value = 5; event:Fire(int.Value)
|
|
CHECK(countA == 2)
|
|
CHECK(countB == 2)
|
|
end
|
|
|
|
do CASE "Always return same object for a given index"
|
|
CHECK(Event.Test == Event.Test)
|
|
end
|
|
end)
|
|
|
|
TEST("Changed", function()
|
|
local create = vide.create
|
|
local Changed = vide.Changed
|
|
local wrap = vide.wrap
|
|
|
|
do CASE "Connects event"
|
|
local connected = false
|
|
local label = create "TextLabel" {
|
|
[Changed.Text] = function(text)
|
|
CHECK(text == "hi")
|
|
connected = true
|
|
end
|
|
}
|
|
CHECK(not connected)
|
|
label.Text = "hi"
|
|
CHECK(connected)
|
|
end
|
|
|
|
do CASE "Bind connection to state"
|
|
local countA = 0
|
|
local countB = 0
|
|
local listener, set = wrap(function() countA += 1 end :: () -> ()?)
|
|
|
|
|
|
local label = create "TextLabel" {
|
|
[Changed.Text] = listener
|
|
}
|
|
|
|
label.Text = "1"
|
|
label.Text = "2"
|
|
CHECK(countA == 2)
|
|
set(function() return function() countB += 1 end end)
|
|
label.Text = "3"
|
|
label.Text = "4"
|
|
CHECK(countA == 2)
|
|
CHECK(countB == 2)
|
|
set(nil)
|
|
label.Text = "5"
|
|
CHECK(countA == 2)
|
|
CHECK(countB == 2)
|
|
end
|
|
|
|
do CASE "Always return same object for a given index"
|
|
CHECK(Changed.Test == Changed.Test)
|
|
end
|
|
end)
|
|
|
|
--[[
|
|
TEST("Created", function()
|
|
local create = vide.create
|
|
local Created = vide.Created
|
|
|
|
do CASE "Run after instance creation"
|
|
local ran = false
|
|
|
|
create "Frame" {
|
|
A = true,
|
|
B = true,
|
|
C = true,
|
|
[Created] = function(self)
|
|
ran = true
|
|
CHECK(self.A and self.B and self.C)
|
|
end
|
|
}
|
|
|
|
CHECK(ran)
|
|
end
|
|
end)]]
|
|
|
|
TEST("strict", function()
|
|
vide.strict = true
|
|
|
|
local wrap = vide.wrap
|
|
local unwrap = vide.unwrap
|
|
local derive = vide.derive
|
|
local watch = vide.watch
|
|
|
|
do CASE "Error on derived callback yield"
|
|
local state = wrap(1)
|
|
|
|
local ok = pcall(function()
|
|
local _derived = derive(function(from)
|
|
coroutine.yield()
|
|
return from(state)
|
|
end)
|
|
end)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "Error on watcher callback yield"
|
|
local state = wrap(1)
|
|
|
|
local ok = pcall(function()
|
|
local _derived = watch(function(from)
|
|
coroutine.yield()
|
|
local _ = from(state)
|
|
end)
|
|
end)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "Run derived callback twice"
|
|
local state, set = wrap(1)
|
|
local runcount = 0
|
|
|
|
local derived = derive(function(from)
|
|
runcount += 1
|
|
return from(state)
|
|
end)
|
|
|
|
CHECK(runcount == 2)
|
|
set(2)
|
|
local _ = derived()
|
|
CHECK(runcount == 4)
|
|
end
|
|
|
|
do CASE "Run watcher callback twice"
|
|
local state, set = wrap(1)
|
|
local runcount = 0
|
|
|
|
watch(function(from)
|
|
runcount += 1
|
|
local _ = from(state)
|
|
end)
|
|
|
|
CHECK(runcount == 2)
|
|
set(2)
|
|
vide.step(1/60)
|
|
CHECK(runcount == 4)
|
|
end
|
|
|
|
do CASE "Does not allow non-layout properties"
|
|
local ok = pcall(function()
|
|
vide.create "Frame" {
|
|
[vide.Layout] = {
|
|
AnchorPoint = Vector2.new(0, 0.5),
|
|
BackgroundColor3 = Color3.new(0, 0, 0)
|
|
}
|
|
}
|
|
end)
|
|
CHECK(not ok)
|
|
end
|
|
|
|
do CASE "Does not allow same table set"
|
|
local _, set = wrap()
|
|
|
|
local t = {}
|
|
|
|
set(t)
|
|
|
|
local ok = pcall(set, t)
|
|
|
|
CHECK(not ok)
|
|
end
|
|
|
|
-- todo: add case for strict mode bindings
|
|
end)
|
|
|
|
local ok = FINISH()
|
|
if not ok then error("Tests failed", 0) end
|
|
|
|
return nil
|