add gs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/rodux"
|
||||
version = "1.0.0"
|
||||
commit = "416e8042bd6eac7a385cb0955079501fd44f11e1"
|
||||
source = "url+https://github.com/roblox/rodux"
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
--[[
|
||||
Calls a function and throws an error if it attempts to yield.
|
||||
|
||||
Pass any number of arguments to the function after the callback.
|
||||
|
||||
This function supports multiple return; all results returned from the
|
||||
given function will be returned.
|
||||
]]
|
||||
|
||||
local function resultHandler(co, ok, ...)
|
||||
if not ok then
|
||||
local message = (...)
|
||||
error(debug.traceback(co, message), 2)
|
||||
end
|
||||
|
||||
if coroutine.status(co) ~= "dead" then
|
||||
error(debug.traceback(co, "Attempted to yield inside changed event!"), 2)
|
||||
end
|
||||
|
||||
return ...
|
||||
end
|
||||
|
||||
local function NoYield(callback, ...)
|
||||
local co = coroutine.create(callback)
|
||||
|
||||
return resultHandler(co, coroutine.resume(co, ...))
|
||||
end
|
||||
|
||||
return NoYield
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
return function()
|
||||
local NoYield = require(script.Parent.NoYield)
|
||||
|
||||
it("should call functions normally", function()
|
||||
local callCount = 0
|
||||
|
||||
local function test(a, b)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
callCount = callCount + 1
|
||||
|
||||
return 11, "hello"
|
||||
end
|
||||
|
||||
local a, b = NoYield(test, 5, 6)
|
||||
|
||||
expect(a).to.equal(11)
|
||||
expect(b).to.equal("hello")
|
||||
end)
|
||||
|
||||
it("should throw on yield", function()
|
||||
local preCount = 0
|
||||
local postCount = 0
|
||||
|
||||
local function testMethod()
|
||||
preCount = preCount + 1
|
||||
wait()
|
||||
postCount = postCount + 1
|
||||
end
|
||||
|
||||
local ok, err = pcall(NoYield, testMethod)
|
||||
|
||||
expect(preCount).to.equal(1)
|
||||
expect(postCount).to.equal(0)
|
||||
|
||||
expect(ok).to.equal(false)
|
||||
expect(err:find("Attempted to yield inside changed event!")).to.be.ok()
|
||||
expect(err:find("NoYield.spec")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should propagate error messages", function()
|
||||
local count = 0
|
||||
|
||||
local function test()
|
||||
count = count + 1
|
||||
error("foo")
|
||||
end
|
||||
|
||||
local ok, err = pcall(NoYield, test)
|
||||
|
||||
expect(ok).to.equal(false)
|
||||
expect(err:find("foo")).to.be.ok()
|
||||
expect(err:find("NoYield.spec")).to.be.ok()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
--[[
|
||||
A limited, simple implementation of a Signal.
|
||||
|
||||
Handlers are fired in order, and (dis)connections are properly handled when
|
||||
executing an event.
|
||||
]]
|
||||
|
||||
local function immutableAppend(list, ...)
|
||||
local new = {}
|
||||
local len = #list
|
||||
|
||||
for key = 1, len do
|
||||
new[key] = list[key]
|
||||
end
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
new[len + i] = select(i, ...)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local function immutableRemoveValue(list, removeValue)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if list[i] ~= removeValue then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
local Signal = {}
|
||||
|
||||
Signal.__index = Signal
|
||||
|
||||
function Signal.new()
|
||||
local self = {
|
||||
_listeners = {}
|
||||
}
|
||||
|
||||
setmetatable(self, Signal)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Signal:connect(callback)
|
||||
local listener = {
|
||||
callback = callback,
|
||||
disconnected = false,
|
||||
}
|
||||
|
||||
self._listeners = immutableAppend(self._listeners, listener)
|
||||
|
||||
local function disconnect()
|
||||
listener.disconnected = true
|
||||
self._listeners = immutableRemoveValue(self._listeners, listener)
|
||||
end
|
||||
|
||||
return {
|
||||
disconnect = disconnect
|
||||
}
|
||||
end
|
||||
|
||||
function Signal:fire(...)
|
||||
for _, listener in ipairs(self._listeners) do
|
||||
if not listener.disconnected then
|
||||
listener.callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Signal
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
return function()
|
||||
local Signal = require(script.Parent.Signal)
|
||||
|
||||
it("should construct from nothing", function()
|
||||
local signal = Signal.new()
|
||||
|
||||
expect(signal).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fire connected callbacks", function()
|
||||
local callCount = 0
|
||||
local value1 = "Hello World"
|
||||
local value2 = 7
|
||||
|
||||
local callback = function(arg1, arg2)
|
||||
expect(arg1).to.equal(value1)
|
||||
expect(arg2).to.equal(value2)
|
||||
callCount = callCount + 1
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
connection:disconnect()
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should disconnect handlers", function()
|
||||
local callback = function()
|
||||
error("Callback was called after disconnect!")
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
connection:disconnect()
|
||||
|
||||
signal:fire()
|
||||
end)
|
||||
|
||||
it("should fire handlers in order", function()
|
||||
local signal = Signal.new()
|
||||
local x = 0
|
||||
local y = 0
|
||||
|
||||
local callback1 = function()
|
||||
expect(x).to.equal(0)
|
||||
expect(y).to.equal(0)
|
||||
x = x + 1
|
||||
end
|
||||
|
||||
local callback2 = function()
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(0)
|
||||
y = y + 1
|
||||
end
|
||||
|
||||
signal:connect(callback1)
|
||||
signal:connect(callback2)
|
||||
signal:fire()
|
||||
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should continue firing despite mid-event disconnection", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionA
|
||||
connectionA = signal:connect(function()
|
||||
connectionA:disconnect()
|
||||
countA = countA + 1
|
||||
end)
|
||||
|
||||
signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should skip listeners that were disconnected during event evaluation", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionB
|
||||
|
||||
signal:connect(function()
|
||||
countA = countA + 1
|
||||
connectionB:disconnect()
|
||||
end)
|
||||
|
||||
connectionB = signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(0)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,131 @@
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
local Signal = require(script.Parent.Signal)
|
||||
local NoYield = require(script.Parent.NoYield)
|
||||
|
||||
local Store = {}
|
||||
|
||||
-- This value is exposed as a private value so that the test code can stay in
|
||||
-- sync with what event we listen to for dispatching the Changed event.
|
||||
-- It may not be Heartbeat in the future.
|
||||
Store._flushEvent = RunService.Heartbeat
|
||||
|
||||
Store.__index = Store
|
||||
|
||||
--[[
|
||||
Create a new Store whose state is transformed by the given reducer function.
|
||||
|
||||
Each time an action is dispatched to the store, the new state of the store
|
||||
is given by:
|
||||
|
||||
state = reducer(state, action)
|
||||
|
||||
Reducers do not mutate the state object, so the original state is still
|
||||
valid.
|
||||
]]
|
||||
function Store.new(reducer, initialState, middlewares)
|
||||
assert(typeof(reducer) == "function", "Bad argument #1 to Store.new, expected function.")
|
||||
assert(middlewares == nil or typeof(middlewares) == "table", "Bad argument #3 to Store.new, expected nil or table.")
|
||||
|
||||
local self = {}
|
||||
|
||||
self._reducer = reducer
|
||||
self._state = reducer(initialState, {
|
||||
type = "@@INIT",
|
||||
})
|
||||
self._lastState = self._state
|
||||
|
||||
self._mutatedSinceFlush = false
|
||||
self._connections = {}
|
||||
|
||||
self.changed = Signal.new()
|
||||
|
||||
setmetatable(self, Store)
|
||||
|
||||
local connection = self._flushEvent:Connect(function()
|
||||
self:flush()
|
||||
end)
|
||||
table.insert(self._connections, connection)
|
||||
|
||||
if middlewares then
|
||||
local unboundDispatch = self.dispatch
|
||||
local dispatch = function(...)
|
||||
return unboundDispatch(self, ...)
|
||||
end
|
||||
|
||||
for i = #middlewares, 1, -1 do
|
||||
local middleware = middlewares[i]
|
||||
dispatch = middleware(dispatch, self)
|
||||
end
|
||||
|
||||
self.dispatch = function(self, ...)
|
||||
return dispatch(...)
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Get the current state of the Store. Do not mutate this!
|
||||
]]
|
||||
function Store:getState()
|
||||
return self._state
|
||||
end
|
||||
|
||||
--[[
|
||||
Dispatch an action to the store. This allows the store's reducer to mutate
|
||||
the state of the application by creating a new copy of the state.
|
||||
|
||||
Listeners on the changed event of the store are notified when the state
|
||||
changes, but not necessarily on every Dispatch.
|
||||
]]
|
||||
function Store:dispatch(action)
|
||||
if typeof(action) == "table" then
|
||||
if action.type == nil then
|
||||
error("action does not have a type field", 2)
|
||||
end
|
||||
|
||||
self._state = self._reducer(self._state, action)
|
||||
self._mutatedSinceFlush = true
|
||||
else
|
||||
error(("actions of type %q are not permitted"):format(typeof(action)), 2)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Marks the store as deleted, disconnecting any outstanding connections.
|
||||
]]
|
||||
function Store:destruct()
|
||||
for _, connection in ipairs(self._connections) do
|
||||
connection:Disconnect()
|
||||
end
|
||||
|
||||
self._connections = nil
|
||||
end
|
||||
|
||||
--[[
|
||||
Flush all pending actions since the last change event was dispatched.
|
||||
]]
|
||||
function Store:flush()
|
||||
if not self._mutatedSinceFlush then
|
||||
return
|
||||
end
|
||||
|
||||
self._mutatedSinceFlush = false
|
||||
|
||||
-- On self.changed:fire(), further actions may be immediately dispatched, in
|
||||
-- which case self._lastState will be set to the most recent self._state,
|
||||
-- unless we cache this value first
|
||||
local state = self._state
|
||||
|
||||
-- If a changed listener yields, *very* surprising bugs can ensue.
|
||||
-- Because of that, changed listeners cannot yield.
|
||||
NoYield(function()
|
||||
self.changed:fire(state, self._lastState)
|
||||
end)
|
||||
|
||||
self._lastState = state
|
||||
end
|
||||
|
||||
return Store
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
return function()
|
||||
local Store = require(script.Parent.Store)
|
||||
|
||||
describe("new", function()
|
||||
it("should instantiate with a reducer", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return "hello, world"
|
||||
end)
|
||||
|
||||
expect(store).to.be.ok()
|
||||
expect(store:getState()).to.equal("hello, world")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should instantiate with a reducer and an initial state", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return state
|
||||
end, "initial state")
|
||||
|
||||
expect(store).to.be.ok()
|
||||
expect(store:getState()).to.equal("initial state")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should instantiate with a reducer, initial state, and middlewares", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return state
|
||||
end, "initial state", {})
|
||||
|
||||
expect(store).to.be.ok()
|
||||
expect(store:getState()).to.equal("initial state")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should modify the dispatch method when middlewares are passed", function()
|
||||
local middlewareInstantiateCount = 0
|
||||
local middlewareInvokeCount = 0
|
||||
local passedDispatch
|
||||
local passedStore
|
||||
local passedAction
|
||||
|
||||
local function reducer(state, action)
|
||||
if action.type == "test" then
|
||||
return "test state"
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
local function testMiddleware(nextDispatch, store)
|
||||
middlewareInstantiateCount = middlewareInstantiateCount + 1
|
||||
passedDispatch = nextDispatch
|
||||
passedStore = store
|
||||
|
||||
return function(action)
|
||||
middlewareInvokeCount = middlewareInvokeCount + 1
|
||||
passedAction = action
|
||||
|
||||
nextDispatch(action)
|
||||
end
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, "initial state", { testMiddleware })
|
||||
|
||||
expect(middlewareInstantiateCount).to.equal(1)
|
||||
expect(middlewareInvokeCount).to.equal(0)
|
||||
expect(passedDispatch).to.be.a("function")
|
||||
expect(passedStore).to.equal(store)
|
||||
|
||||
store:dispatch({
|
||||
type = "test",
|
||||
})
|
||||
|
||||
expect(middlewareInstantiateCount).to.equal(1)
|
||||
expect(middlewareInvokeCount).to.equal(1)
|
||||
expect(passedAction.type).to.equal("test")
|
||||
|
||||
store:flush()
|
||||
|
||||
expect(store:getState()).to.equal("test state")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should execute middleware left-to-right", function()
|
||||
local events = {}
|
||||
|
||||
local function reducer(state)
|
||||
return state
|
||||
end
|
||||
|
||||
local function middlewareA(nextDispatch, store)
|
||||
table.insert(events, "instantiate a")
|
||||
return function(action)
|
||||
table.insert(events, "execute a")
|
||||
return nextDispatch(action)
|
||||
end
|
||||
end
|
||||
|
||||
local function middlewareB(nextDispatch, store)
|
||||
table.insert(events, "instantiate b")
|
||||
return function(action)
|
||||
table.insert(events, "execute b")
|
||||
return nextDispatch(action)
|
||||
end
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, 5, { middlewareA, middlewareB })
|
||||
|
||||
expect(#events).to.equal(2)
|
||||
expect(events[1]).to.equal("instantiate b")
|
||||
expect(events[2]).to.equal("instantiate a")
|
||||
|
||||
store:dispatch({
|
||||
type = "test",
|
||||
})
|
||||
|
||||
expect(#events).to.equal(4)
|
||||
expect(events[3]).to.equal("execute a")
|
||||
expect(events[4]).to.equal("execute b")
|
||||
end)
|
||||
|
||||
it("should send an initial action with a 'type' field", function()
|
||||
local lastAction
|
||||
local callCount = 0
|
||||
|
||||
local store = Store.new(function(state, action)
|
||||
lastAction = action
|
||||
callCount = callCount + 1
|
||||
|
||||
return state
|
||||
end)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(lastAction).to.be.a("table")
|
||||
expect(lastAction.type).to.be.ok()
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getState", function()
|
||||
it("should get the current state", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return "foo"
|
||||
end)
|
||||
|
||||
local state = store:getState()
|
||||
|
||||
expect(state).to.equal("foo")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("dispatch", function()
|
||||
it("should be sent through the reducer", function()
|
||||
local store = Store.new(function(state, action)
|
||||
state = state or "foo"
|
||||
|
||||
if action.type == "act" then
|
||||
return "bar"
|
||||
end
|
||||
|
||||
return state
|
||||
end)
|
||||
|
||||
expect(store).to.be.ok()
|
||||
expect(store:getState()).to.equal("foo")
|
||||
|
||||
store:dispatch({
|
||||
type = "act",
|
||||
})
|
||||
|
||||
store:flush()
|
||||
|
||||
expect(store:getState()).to.equal("bar")
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should trigger the changed event after a flush", function()
|
||||
local store = Store.new(function(state, action)
|
||||
state = state or 0
|
||||
|
||||
if action.type == "increment" then
|
||||
return state + 1
|
||||
end
|
||||
|
||||
return state
|
||||
end)
|
||||
|
||||
local callCount = 0
|
||||
|
||||
store.changed:connect(function(state, oldState)
|
||||
expect(oldState).to.equal(0)
|
||||
expect(state).to.equal(1)
|
||||
|
||||
callCount = callCount + 1
|
||||
end)
|
||||
|
||||
store:dispatch({
|
||||
type = "increment",
|
||||
})
|
||||
|
||||
store:flush()
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should handle actions dispatched within the changed event", function()
|
||||
local store = Store.new(function(state, action)
|
||||
state = state or {
|
||||
value = 0,
|
||||
}
|
||||
|
||||
if action.type == "increment" then
|
||||
return {
|
||||
value = state.value + 1,
|
||||
}
|
||||
elseif action.type == "decrement" then
|
||||
return {
|
||||
value = state.value - 1,
|
||||
}
|
||||
end
|
||||
|
||||
return state
|
||||
end)
|
||||
|
||||
local changeCount = 0
|
||||
|
||||
store.changed:connect(function(state, oldState)
|
||||
expect(state).never.to.equal(oldState)
|
||||
|
||||
if state.value > 0 then
|
||||
store:dispatch({
|
||||
type = "decrement",
|
||||
})
|
||||
end
|
||||
|
||||
changeCount = changeCount + 1
|
||||
end)
|
||||
|
||||
store:dispatch({
|
||||
type = "increment",
|
||||
})
|
||||
store:flush()
|
||||
store:flush()
|
||||
|
||||
expect(changeCount).to.equal(2)
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should prevent yielding from changed handler", function()
|
||||
local preCount = 0
|
||||
local postCount = 0
|
||||
|
||||
local store = Store.new(function(state, action)
|
||||
state = state or 0
|
||||
return state + 1
|
||||
end)
|
||||
|
||||
store.changed:connect(function(state, oldState)
|
||||
preCount = preCount + 1
|
||||
wait()
|
||||
postCount = postCount + 1
|
||||
end)
|
||||
|
||||
store:dispatch({
|
||||
type = "increment",
|
||||
})
|
||||
|
||||
expect(function()
|
||||
store:flush()
|
||||
end).to.throw()
|
||||
|
||||
expect(preCount).to.equal(1)
|
||||
expect(postCount).to.equal(0)
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should throw if an action is dispatched without a type field", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return state
|
||||
end)
|
||||
|
||||
expect(function()
|
||||
store:dispatch({})
|
||||
end).to.throw()
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
|
||||
it("should throw if the action is not a function or table", function()
|
||||
local store = Store.new(function(state, action)
|
||||
return state
|
||||
end)
|
||||
|
||||
expect(function()
|
||||
store:dispatch(1)
|
||||
end).to.throw()
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("flush", function()
|
||||
it("should not fire a changed event if there were no dispatches", function()
|
||||
local store = Store.new(function()
|
||||
end)
|
||||
|
||||
local count = 0
|
||||
store.changed:connect(function()
|
||||
count = count + 1
|
||||
end)
|
||||
|
||||
store:flush()
|
||||
|
||||
expect(count).to.equal(0)
|
||||
|
||||
store:dispatch({
|
||||
type = "increment",
|
||||
})
|
||||
store:flush()
|
||||
|
||||
expect(count).to.equal(1)
|
||||
|
||||
store:flush()
|
||||
|
||||
expect(count).to.equal(1)
|
||||
|
||||
store:destruct()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
--[[
|
||||
Create a composite reducer from a map of keys and sub-reducers.
|
||||
]]
|
||||
local function combineReducers(map)
|
||||
return function(state, action)
|
||||
-- If state is nil, substitute it with a blank table.
|
||||
if state == nil then
|
||||
state = {}
|
||||
end
|
||||
|
||||
local newState = {}
|
||||
|
||||
for key, reducer in pairs(map) do
|
||||
-- Each reducer gets its own state, not the entire state table
|
||||
newState[key] = reducer(state[key], action)
|
||||
end
|
||||
|
||||
return newState
|
||||
end
|
||||
end
|
||||
|
||||
return combineReducers
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
return function()
|
||||
local combineReducers = require(script.Parent.combineReducers)
|
||||
|
||||
it("should invoke each sub-reducer for every action", function()
|
||||
local aCount = 0
|
||||
local bCount = 0
|
||||
|
||||
local reducer = combineReducers({
|
||||
a = function(state, action)
|
||||
aCount = aCount + 1
|
||||
end,
|
||||
b = function(state, action)
|
||||
bCount = bCount + 1
|
||||
end,
|
||||
})
|
||||
|
||||
-- Mock reducer invocation
|
||||
reducer({}, {})
|
||||
expect(aCount).to.equal(1)
|
||||
expect(bCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should assign each sub-reducer's value to the new state", function()
|
||||
local reducer = combineReducers({
|
||||
a = function(state, action)
|
||||
return (state or 0) + 1
|
||||
end,
|
||||
b = function(state, action)
|
||||
return (state or 0) + 3
|
||||
end,
|
||||
})
|
||||
|
||||
local newState = reducer({}, {})
|
||||
expect(newState.a).to.equal(1)
|
||||
expect(newState.b).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should not throw when state is nil", function()
|
||||
local reducer = combineReducers({
|
||||
a = function(state, action)
|
||||
return (state or 0) + 1
|
||||
end,
|
||||
b = function(state, action)
|
||||
return (state or 0) + 3
|
||||
end,
|
||||
})
|
||||
|
||||
expect(function()
|
||||
reducer(nil, {})
|
||||
end).to.never.throw()
|
||||
end)
|
||||
end
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
return function(initialState, handlers)
|
||||
return function(state, action)
|
||||
if state == nil then
|
||||
state = initialState
|
||||
end
|
||||
|
||||
local handler = handlers[action.type]
|
||||
|
||||
if handler then
|
||||
return handler(state, action)
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
end
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
return function()
|
||||
local createReducer = require(script.Parent.createReducer)
|
||||
|
||||
it("should handle actions", function()
|
||||
local reducer = createReducer({
|
||||
a = 0,
|
||||
b = 0,
|
||||
}, {
|
||||
a = function(state, action)
|
||||
return {
|
||||
a = state.a + 1,
|
||||
b = state.b,
|
||||
}
|
||||
end,
|
||||
b = function(state, action)
|
||||
return {
|
||||
a = state.a,
|
||||
b = state.b + 2,
|
||||
}
|
||||
end,
|
||||
})
|
||||
|
||||
local newState = reducer({
|
||||
a = 0,
|
||||
b = 0,
|
||||
}, {
|
||||
type = "a",
|
||||
})
|
||||
|
||||
expect(newState.a).to.equal(1)
|
||||
|
||||
newState = reducer(newState, {
|
||||
type = "b",
|
||||
})
|
||||
|
||||
expect(newState.b).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should return the initial state if the state is nil", function()
|
||||
local reducer = createReducer({
|
||||
a = 0,
|
||||
b = 0,
|
||||
-- We don't care about the actions here
|
||||
}, {})
|
||||
|
||||
local newState = reducer(nil, {})
|
||||
expect(newState).to.be.ok()
|
||||
expect(newState.a).to.equal(0)
|
||||
expect(newState.b).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should still run action handlers if the state is nil", function()
|
||||
local callCount = 0
|
||||
|
||||
local reducer = createReducer(0, {
|
||||
foo = function(state, action)
|
||||
callCount = callCount + 1
|
||||
return nil
|
||||
end
|
||||
})
|
||||
|
||||
expect(callCount).to.equal(0)
|
||||
|
||||
local newState = reducer(nil, {
|
||||
type = "foo",
|
||||
})
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
expect(newState).to.equal(nil)
|
||||
|
||||
newState = reducer(newState, {
|
||||
type = "foo",
|
||||
})
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
expect(newState).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return the same state if the action is not handled", function()
|
||||
local initialState = {
|
||||
a = 0,
|
||||
b = 0,
|
||||
}
|
||||
|
||||
local reducer = createReducer(initialState, {
|
||||
a = function(state, action)
|
||||
return {
|
||||
a = state.a + 1,
|
||||
b = state.b,
|
||||
}
|
||||
end,
|
||||
b = function(state, action)
|
||||
return {
|
||||
a = state.a,
|
||||
b = state.b + 2,
|
||||
}
|
||||
end,
|
||||
})
|
||||
|
||||
local newState = reducer(initialState, {
|
||||
type = "c",
|
||||
})
|
||||
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
local Store = require(script.Store)
|
||||
local createReducer = require(script.createReducer)
|
||||
local combineReducers = require(script.combineReducers)
|
||||
local loggerMiddleware = require(script.loggerMiddleware)
|
||||
local thunkMiddleware = require(script.thunkMiddleware)
|
||||
|
||||
return {
|
||||
Store = Store,
|
||||
createReducer = createReducer,
|
||||
combineReducers = combineReducers,
|
||||
loggerMiddleware = loggerMiddleware.middleware,
|
||||
thunkMiddleware = thunkMiddleware,
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
return function()
|
||||
describe("Rodux", function()
|
||||
it("should load", function()
|
||||
local Rodux = require(script.Parent)
|
||||
|
||||
expect(Rodux.Store).to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
local indent = " "
|
||||
|
||||
local function prettyPrint(value, indentLevel)
|
||||
indentLevel = indentLevel or 0
|
||||
local output = {}
|
||||
|
||||
if typeof(value) == "table" then
|
||||
table.insert(output, "{\n")
|
||||
|
||||
for key, value in pairs(value) do
|
||||
table.insert(output, indent:rep(indentLevel + 1))
|
||||
table.insert(output, tostring(key))
|
||||
table.insert(output, " = ")
|
||||
|
||||
table.insert(output, prettyPrint(value, indentLevel + 1))
|
||||
table.insert(output, "\n")
|
||||
end
|
||||
|
||||
table.insert(output, indent:rep(indentLevel))
|
||||
table.insert(output, "}")
|
||||
elseif typeof(value) == "string" then
|
||||
table.insert(output, string.format("%q", value))
|
||||
table.insert(output, " (string)")
|
||||
else
|
||||
table.insert(output, tostring(value))
|
||||
table.insert(output, " (")
|
||||
table.insert(output, typeof(value))
|
||||
table.insert(output, ")")
|
||||
end
|
||||
|
||||
return table.concat(output, "")
|
||||
end
|
||||
|
||||
-- We want to be able to override outputFunction in tests, so the shape of this
|
||||
-- module is kind of unconventional.
|
||||
--
|
||||
-- We fix it this weird shape in init.lua.
|
||||
local loggerMiddleware = {
|
||||
outputFunction = print,
|
||||
}
|
||||
|
||||
function loggerMiddleware.middleware(nextDispatch, store)
|
||||
return function(action)
|
||||
local result = nextDispatch(action)
|
||||
|
||||
loggerMiddleware.outputFunction(("Action dispatched: %s\nState changed to: %s"):format(
|
||||
prettyPrint(action),
|
||||
prettyPrint(store:getState())
|
||||
))
|
||||
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
return loggerMiddleware
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
return function()
|
||||
local Store = require(script.Parent.Store)
|
||||
local loggerMiddleware = require(script.Parent.loggerMiddleware)
|
||||
|
||||
it("should print whenever an action is dispatched", function()
|
||||
local outputCount = 0
|
||||
local outputMessage
|
||||
|
||||
local function reducer(state, action)
|
||||
return state
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, {
|
||||
fooValue = 12345,
|
||||
barValue = {
|
||||
bazValue = "hiBaz",
|
||||
},
|
||||
}, { loggerMiddleware.middleware })
|
||||
|
||||
loggerMiddleware.outputFunction = function(message)
|
||||
outputCount = outputCount + 1
|
||||
outputMessage = message
|
||||
end
|
||||
|
||||
store:dispatch({
|
||||
type = "testActionType",
|
||||
})
|
||||
|
||||
expect(outputCount).to.equal(1)
|
||||
expect(outputMessage:find("testActionType")).to.be.ok()
|
||||
expect(outputMessage:find("fooValue")).to.be.ok()
|
||||
expect(outputMessage:find("12345")).to.be.ok()
|
||||
expect(outputMessage:find("barValue")).to.be.ok()
|
||||
expect(outputMessage:find("bazValue")).to.be.ok()
|
||||
expect(outputMessage:find("hiBaz")).to.be.ok()
|
||||
|
||||
loggerMiddleware.outputFunction = print
|
||||
end)
|
||||
end
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
--[[
|
||||
A middleware that allows for functions to be dispatched.
|
||||
Functions will receive a single argument, the store itself.
|
||||
This middleware consumes the function; middleware further down the chain
|
||||
will not receive it.
|
||||
]]
|
||||
local function thunkMiddleware(nextDispatch, store)
|
||||
return function(action)
|
||||
if typeof(action) == "function" then
|
||||
return action(store)
|
||||
else
|
||||
return nextDispatch(action)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return thunkMiddleware
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
return function()
|
||||
local Store = require(script.Parent.Store)
|
||||
local thunkMiddleware = require(script.Parent.thunkMiddleware)
|
||||
|
||||
it("should dispatch thunks", function()
|
||||
local function reducer(state, action)
|
||||
return state
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, {}, { thunkMiddleware })
|
||||
local thunkCount = 0
|
||||
|
||||
local function thunk(store)
|
||||
thunkCount = thunkCount + 1
|
||||
end
|
||||
|
||||
store:dispatch(thunk)
|
||||
|
||||
expect(thunkCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should allow normal actions to pass through", function()
|
||||
local reducerCount = 0
|
||||
|
||||
local function reducer(state, action)
|
||||
reducerCount = reducerCount + 1
|
||||
return state
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, {}, { thunkMiddleware })
|
||||
|
||||
store:dispatch({
|
||||
type = "test",
|
||||
})
|
||||
|
||||
-- Reducer will be invoked twice:
|
||||
-- Once when creating the store (@@INIT action)
|
||||
-- Once when the test action is dispatched
|
||||
expect(reducerCount).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should return the value from the thunk", function()
|
||||
local function reducer(state, action)
|
||||
return state
|
||||
end
|
||||
|
||||
local store = Store.new(reducer, {}, { thunkMiddleware })
|
||||
local thunkValue = "test"
|
||||
|
||||
local function thunk(store)
|
||||
return thunkValue
|
||||
end
|
||||
|
||||
local result = store:dispatch(thunk)
|
||||
|
||||
expect(result).to.equal(thunkValue)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user