feat: initial commit

This commit is contained in:
Richard 2024-02-25 03:41:09 -08:00
commit 18723c1377
50 changed files with 3238 additions and 0 deletions

View file

@ -0,0 +1,85 @@
local atom = require(script.Parent.Parent.atom)
return function()
it("updates state", function()
local source = atom(1)
source(2)
expect(source()).to.equal(2)
source(function(x)
return x + 1
end)
expect(source()).to.equal(3)
end)
it("calls listeners", function()
local source = atom(1)
local value
local function listener(state)
value = state
end
source.listeners[listener] = true
source(2)
expect(value).to.equal(2)
end)
it("skips pending listeners", function()
local source = atom(1)
local outer = 0
local inner = 0
local function listener()
outer += 1
local function listener()
inner += 1
end
source.listeners[listener] = true
end
source.listeners[listener] = true
source(2)
expect(outer).to.equal(1)
expect(inner).to.equal(0)
source(3)
expect(outer).to.equal(2)
expect(inner).to.equal(1)
end)
it("can be readonly", function()
local readonly = atom(1):readonly()
expect(function()
(readonly :: any)(2)
end).to.throw()
local writable = readonly:writable()
writable(2)
expect(writable()).to.equal(2)
end)
it("calls the setter", function()
local source = atom(1):writable(function(_, value: number)
return value + 1
end)
source(1)
expect(source()).to.equal(2)
end)
it("cancels if setter returns nil", function()
local source = atom(1):writable(function(value: number)
return if value == 2 then nil else value
end)
source(2)
expect(source()).to.equal(1)
source(3)
expect(source()).to.equal(3)
end)
it("can be memoized", function()
local source = atom(1):memo(function(prev, next)
return next < prev
end)
source(2)
expect(source()).to.equal(2)
source(1)
expect(source()).to.equal(2)
end)
end

View file

@ -0,0 +1,92 @@
local atom = require(script.Parent.Parent.atom)
local derive = require(script.Parent.Parent.derive)
return function()
it("derives from dependencies", function()
local a = atom(1)
local b = atom(2)
local sum = derive(a, b, function(a: number, b: number)
return a + b
end)
expect(sum()).to.equal(3)
a(2)
expect(sum()).to.equal(4)
b(3)
expect(sum()).to.equal(5)
expect(function()
(sum :: any)(2)
end).to.throw()
end)
it("memoizes the result", function()
local calls = 0
local source = atom(1)
local double = derive(source, function(value: number)
calls += 1
return value * 2
end)
expect(calls).to.equal(1)
source(1)
expect(calls).to.equal(1)
source(2)
expect(calls).to.equal(2)
double()
expect(calls).to.equal(2)
end)
it("can be a dependency", function()
local source = atom(1)
local double = derive(source, function(value: number)
return value * 2
end)
local quadruple = derive(double, function(value: number)
return value * 2
end)
expect(quadruple()).to.equal(4)
source(2)
expect(quadruple()).to.equal(8)
end)
it("allows custom setter", function()
local source = atom(1)
local double = derive(source, function(value: number)
return value * 2
end):writable(function(update: number)
source(update / 2)
end)
double(4)
expect(source()).to.equal(2)
expect(double()).to.equal(4)
end)
it("garbage collects", function()
-- large values coerce the garbage collector to run
local source = atom(string.rep("x", 1e5))
local max = 0
local function listenersDecreased()
local count = 0
for _ in source.listeners do
count += 1
end
max = math.max(max, count)
return count < max
end
for index = 1, 500 do
derive(source, function(value)
return string.upper(value)
end)
if index % 50 == 0 then
-- derived atoms check if they are collected when they are updated
source(source() .. "x")
task.wait()
end
if listenersDecreased() then
return
end
end
error("None of the derived atoms were collected")
end)
end

View file

@ -0,0 +1,7 @@
local reference = require(script.Parent.Parent.utils.reference)
return function()
afterEach(function()
reference.clear()
end)
end

View file

@ -0,0 +1,33 @@
local atom = require(script.Parent.Parent.atom)
local observe = require(script.Parent.Parent.observe)
return function()
it("observes lifetime", function()
local itemsAtom = atom({ "a", "b", "c" })
local state = {}
local cleanup, flush = observe(itemsAtom, function(item, key)
state[key] = item
return function()
state[key] = nil
end
end)
for key = 1, 3 do
expect(state[key]).to.equal(itemsAtom:get()[key])
end
itemsAtom:set({ [4] = "d", [5] = "e", [6] = "f" })
flush()
for key = 1, 6 do
expect(state[key]).to.equal(itemsAtom:get()[key])
end
cleanup()
for key = 1, 6 do
expect(state[key]).to.equal(nil)
end
end)
end

View file

@ -0,0 +1,42 @@
_G.__ROACT_17_MOCK_SCHEDULER__ = true
local Package = script:FindFirstAncestor("__tests__").Parent
local React = require(Package.modules.React)
local ReactRoblox = require(Package.modules.ReactRoblox)
type RenderedHook<Props, Result> = {
result: Result,
rerender: (Props?) -> (),
unmount: () -> (),
}
local function renderHook<Props, Result>(callback: (Props) -> Result, props: Props?): RenderedHook<Props, Result>
local root = ReactRoblox.createLegacyRoot(Instance.new("Folder"))
local hook = {}
local function Component(props)
hook.result = callback(props)
end
function hook.rerender(newProps)
props = newProps or props
ReactRoblox.act(function()
root:render(React.createElement(Component, props))
end)
end
function hook.unmount()
ReactRoblox.act(function()
root:unmount()
end)
end
ReactRoblox.act(function()
hook.rerender(props)
end)
return hook
end
return renderHook

View file

@ -0,0 +1,27 @@
local atom = require(script.Parent.Parent.Parent.atom)
local useAtomState = require(script.Parent.Parent.Parent.react.useAtomState)
local renderHook = require(script.Parent.helpers.renderHook)
return function()
it("tracks atom state", function()
local source = atom(0)
local hook = renderHook(function()
return useAtomState(source)
end)
expect(hook.result).to.equal(0)
source(1)
expect(hook.result).to.equal(1)
end)
it("receives a selector", function()
local source = atom(0)
local hook = renderHook(function()
return useAtomState(source, function(value)
return 2 * value
end)
end)
expect(hook.result).to.equal(0)
source(1)
expect(hook.result).to.equal(2)
end)
end

View file

@ -0,0 +1,34 @@
local atom = require(script.Parent.Parent.Parent.atom)
local useSetAtom = require(script.Parent.Parent.Parent.react.useSetAtom)
local renderHook = require(script.Parent.helpers.renderHook)
return function()
it("sets atom state", function()
local source = atom(0)
local hook = renderHook(function()
return useSetAtom(source)
end)
expect(hook.result).to.be.a("function")
hook.result(1)
expect(source()).to.equal(1)
end)
it("returns a stable function", function()
local source = atom(0)
local hook = renderHook(function()
return useSetAtom(source)
end)
local fn = hook.result
hook.rerender()
expect(hook.result).to.equal(fn)
end)
it("memoizes the function", function()
local hook = renderHook(function(source)
return useSetAtom(source)
end, atom(0))
local fn = hook.result
hook.rerender(atom(1))
expect(hook.result).to.never.equal(fn)
end)
end

View file

@ -0,0 +1,128 @@
local atom = require(script.Parent.Parent.atom)
local isAtom = require(script.Parent.Parent.isAtom)
local observe = require(script.Parent.Parent.observe)
local split = require(script.Parent.Parent.split)
return function()
it("returns split atoms", function()
local itemsAtom = atom({ "a", "b", "c" })
local itemAtoms = split(itemsAtom)
local updates = 0
itemAtoms.listeners[function()
updates += 1
end] = true
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
assert(isAtom(itemAtom), `itemAtom {key} is not an atom; got {typeof(itemAtom)}`)
expect(itemAtom()).to.equal(itemsAtom()[key])
end
local prevAtoms = itemAtoms()
itemsAtom({ "d", "e", "f" })
expect(updates).to.equal(1)
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
assert(itemAtom == prevAtoms[key] :: any, `itemAtom {key} is not the same atom; got {typeof(itemAtom)}`)
expect(itemAtom()).to.equal(itemsAtom()[key])
end
itemsAtom({ [1] = "d", [3] = "f" })
expect(itemAtoms()[2]).to.never.be.ok()
expect(updates).to.equal(2)
end)
it("has writable items", function()
local itemsAtom = atom({ "a", "b", "c" })
local itemAtoms = split(itemsAtom)
local updates = 0
itemAtoms.listeners[function()
updates += 1
end] = true
itemAtoms()[2]("x")
expect(updates).to.equal(1)
expect(itemsAtom()[2]).to.equal("x")
expect(itemAtoms()[2]()).to.equal("x")
end)
it("uses key extractor", function()
local itemsAtom = atom({ { key = 1 }, { key = 2 }, { key = 3 } })
local itemAtoms = split(itemsAtom, function(item)
return item.key
end)
for key = 1, 3 do
local itemAtom = itemAtoms()[key]
expect(itemAtom()).to.equal(itemsAtom()[key])
end
local prevAtoms = itemAtoms()
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
expect(itemAtoms()[1]).to.equal(prevAtoms[1])
expect(itemAtoms()[1]()).to.equal(itemsAtom()[1])
expect(itemAtoms()[2]).to.never.be.ok()
expect(itemAtoms()[3]).to.equal(prevAtoms[3])
expect(itemAtoms()[3]()).to.equal(itemsAtom()[2])
end)
it("allows observe()", function()
local itemsAtom = atom({
{ key = 1, value = "a" },
{ key = 2, value = "b" },
{ key = 3, value = "c" },
})
local itemAtoms = split(itemsAtom, function(item)
return item.key
end)
local added = 0
local updated = 0
local deleted = 0
local _, flush = observe(itemAtoms, function(item, key)
expect(item()).to.equal(itemsAtom()[key])
item.listeners[function(value)
expect(value).to.equal(itemsAtom()[key])
updated += 1
end] = true
added += 1
return function()
expect(itemAtoms()[key]).to.never.be.ok()
deleted += 1
end
end)
expect(added).to.equal(3)
expect(updated).to.equal(0)
expect(deleted).to.equal(0)
itemsAtom({ itemsAtom()[1], itemsAtom()[3] })
flush()
expect(added).to.equal(3)
expect(updated).to.equal(0)
expect(deleted).to.equal(1)
itemAtoms()[1]({ key = 1, value = "x" })
flush()
expect(added).to.equal(3)
expect(updated).to.equal(1)
expect(deleted).to.equal(1)
itemsAtom({})
flush()
expect(added).to.equal(3)
expect(updated).to.equal(1)
expect(deleted).to.equal(3)
end)
end

View file

@ -0,0 +1,92 @@
local atom = require(script.Parent.Parent.atom)
local derive = require(script.Parent.Parent.derive)
local subscribe = require(script.Parent.Parent.subscribe)
return function()
it("runs on state change", function()
local current, previous
local source = atom(0)
local unsubscribe, flush = subscribe(source, function(...)
current, previous = ...
end)
for index = 1, 3 do
source(index)
flush()
expect(current).to.equal(index)
expect(previous).to.equal(index - 1)
end
unsubscribe()
source(4)
flush()
expect(current).to.equal(3)
expect(previous).to.equal(2)
end)
it("batches updates", function()
local current, previous
local source = atom(0)
local _, flush = subscribe(source, function(...)
current, previous = ...
end)
source(1)
source(2)
flush()
expect(current).to.equal(2)
expect(previous).to.equal(0)
end)
it("checks equality", function()
local current, previous
local source = atom(0):memo(function(previous, current)
return current < previous
end)
local _, flush = subscribe(source, function(...)
current, previous = ...
end)
source(1)
flush()
expect(current).to.equal(1)
expect(previous).to.equal(0)
source(0)
flush()
expect(current).to.equal(1)
expect(previous).to.equal(0)
end)
it("prevents garbage collect", function()
local source = atom(1)
local updates = 0
for _ = 1, 1000 do
-- The idea here is to test whether subscribe() prevents every derived
-- atom in a chain from being garbage collected. If any are GCed, then
-- the subscription will stop updating and the test will fail.
subscribe(
derive(
derive(source, function(x)
return x
end),
function(x)
return 2 * x
end
),
function()
updates += 1
end
)
end
source(source() + 1)
task.wait()
expect(updates).to.equal(1000)
end)
end