add gs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Generated by Rotriever. Format subject to change in future releases.
|
||||
name = "roblox/state-table"
|
||||
version = "0.1.0"
|
||||
commit = "aa0641127efcb15fcabb26f5ba46e44815f42d4a"
|
||||
source = "url+https://github.com/roblox/state-table"
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
local StateTable = {}
|
||||
|
||||
StateTable.__index = StateTable
|
||||
|
||||
local function validateStateTableItem(item, qualifier)
|
||||
local type = typeof(item)
|
||||
local isValid = type == "string" or type == "userdata"
|
||||
assert(isValid, string.format("%s '%s' must be a string or userdata but is a %s", qualifier, tostring(item), type))
|
||||
end
|
||||
|
||||
--[[
|
||||
This class method creates a new StateTable instance that you can use to control complicated
|
||||
logic that is based upon your state machine design. Ex:
|
||||
|
||||
self.stateTable = StateTable.new(name, initialState, initialContext, {
|
||||
InitialState = {
|
||||
EventName1 = { nextState = "StateOne", action = self.actionDoSomething },
|
||||
EventName2 = { nextState = "FinalState" } -- actions are optional
|
||||
},
|
||||
StateOne = {
|
||||
EventName1 = { nextState = "FinalState", action = self.actionDoSomethingAtLast },
|
||||
EventName3 = { action = self.actionDoSomethingElse } -- will maintain current state
|
||||
},
|
||||
FinalState = {} -- transitions are optional
|
||||
})
|
||||
|
||||
Arguments:
|
||||
name - A debug name for this StateTable. (String)
|
||||
initialState - Name of the beginning state for this StateTable. (String)
|
||||
initialContext - A reference to an existing table where you hold all sidecar contextual data
|
||||
that needs to be manipulated by this StateTable's actions. (Table)
|
||||
transitionTable - Description of the state machine structure. (Table)
|
||||
|
||||
The outermost keys in "transitionTable" represent individual states in your design, each of which
|
||||
contains a description of the events that can be called while in that state. Calling an event
|
||||
triggers a transition to a new state while also (optionally) running an action functor.
|
||||
|
||||
(All states and events must be simple strings or userdata.)
|
||||
(If using userdata for states and events, implement a tostring metamethod for ease of debugging.)
|
||||
|
||||
Named events in your state table will be converted into functions that you can call directly.
|
||||
Calling these event functions will transition the StateTable to the appropriate nextState and
|
||||
call the registered action handler, if any. You may pass arguments to your actions through the
|
||||
event function by passing them as a table. Ex:
|
||||
|
||||
self.stateTable.events.EventName1(args)
|
||||
|
||||
The combination of named states and events in StateTable make up the control flow portion of your
|
||||
state machine. To run business logic, you need to implement Actions.
|
||||
|
||||
Each action functor accepts four arguments: the current state, the next state, event arguments,
|
||||
and the contextual data table that you passed in when you called the event function. Your action
|
||||
functor should return a table containing the keys that need to be updated from currentContext.
|
||||
The returned table will be merged into currentContext and passed back to your onStateChange
|
||||
callback. Ex:
|
||||
|
||||
function actionDoSomething(currentState, nextState, args, currentContext)
|
||||
local contextDiff = doSomething(args, currentContext)
|
||||
return contextDiff
|
||||
end
|
||||
|
||||
Do NOT update your own copy of the StateTable's internal state variable or your context in actions.
|
||||
If this is an action-less transition, you'll fail to update it!
|
||||
|
||||
To update your context and own tracking of the current state at the same time, listen to changes
|
||||
via StateTable:onStateChange. See the documentation of that method for more details.
|
||||
]]
|
||||
function StateTable.new(name, initialState, initialContext, transitionTable)
|
||||
assert(typeof(name) == "string", "name must be a string")
|
||||
assert(#name > 0, "name must not be an empty string")
|
||||
|
||||
validateStateTableItem(initialState, "initialState")
|
||||
assert(initialContext == nil or typeof(initialContext) == "table", "initialContext must be a table or nil")
|
||||
|
||||
assert(typeof(transitionTable) == "table", "transitionTable must be a table")
|
||||
assert(typeof(transitionTable[initialState]) == "table", "initialState must be present in transitionTable")
|
||||
|
||||
local self = {}
|
||||
setmetatable(self, StateTable)
|
||||
|
||||
self.name = name
|
||||
self.currentState = initialState
|
||||
self.currentContext = initialContext or {}
|
||||
self.transitionTable = {}
|
||||
self.events = {}
|
||||
|
||||
for state, eventTable in pairs(transitionTable) do
|
||||
validateStateTableItem(state, "state")
|
||||
assert(typeof(eventTable) == "table", string.format("state '%s' must map to a table", tostring(state)))
|
||||
|
||||
local parsedEventTable = {}
|
||||
for event, eventData in pairs(eventTable) do
|
||||
validateStateTableItem(event, "event")
|
||||
assert(typeof(eventData) == "table", string.format("event '%s' must map to a table", tostring(event)))
|
||||
|
||||
local nextState = eventData.nextState
|
||||
local action = eventData.action
|
||||
|
||||
if nextState ~= nil then
|
||||
validateStateTableItem(nextState, "nextState")
|
||||
|
||||
-- Check that the transition lands on a known state
|
||||
assert(transitionTable[nextState] ~= nil,
|
||||
string.format("nextState '%s' does not exist in transitionTable", tostring(nextState)))
|
||||
end
|
||||
|
||||
assert(action == nil or typeof(action) == "function", "action must be a function")
|
||||
|
||||
parsedEventTable[event] = eventData
|
||||
|
||||
-- Create a function to make it easy to call this event
|
||||
if self.events[event] == nil then
|
||||
self.events[event] = function(args)
|
||||
return self:handleEvent(event, args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.transitionTable[state] = parsedEventTable
|
||||
end
|
||||
|
||||
-- catch calls to invalid events earlier
|
||||
setmetatable(self.events, {
|
||||
__index = function(_, event)
|
||||
error(string.format("'%s' is not a valid event in StateTable '%s'",
|
||||
tostring(event), self.name), 2)
|
||||
end
|
||||
})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
It is recommended that you use the auto-generated event functions instead
|
||||
of calling this method; see StateTable.new.
|
||||
|
||||
Process an event through this StateTable instance. Pass in the name
|
||||
of the event, and optional arguments. The arguments will be passed to the
|
||||
registered action handler for the state/event transition, if any.
|
||||
|
||||
This function does not return anything. Listen to changes using
|
||||
StateTable:onStateChange if you need to store the current state or use the
|
||||
results of an action.
|
||||
]]
|
||||
function StateTable:handleEvent(event, args)
|
||||
validateStateTableItem(event, "event")
|
||||
assert(args == nil or typeof(args) == "table", "args must be nil or valid table")
|
||||
|
||||
local currentState = self.currentState
|
||||
local eventMap = self.transitionTable[currentState]
|
||||
|
||||
assert(eventMap ~= nil, "no transition events for current state")
|
||||
|
||||
if eventMap[event] ~= nil then
|
||||
local eventData = eventMap[event]
|
||||
local nextState = eventData.nextState or currentState
|
||||
local action = eventData.action
|
||||
|
||||
local updatedContext = self.currentContext
|
||||
if action ~= nil then
|
||||
local contextDiff = action(currentState, nextState, args, self.currentContext) or {}
|
||||
|
||||
-- Immutable merge dictionaries (without Cryo)
|
||||
updatedContext = {}
|
||||
for key, value in pairs(self.currentContext) do
|
||||
updatedContext[key] = value
|
||||
end
|
||||
|
||||
for key, value in pairs(contextDiff) do
|
||||
updatedContext[key] = value
|
||||
end
|
||||
|
||||
self.currentContext = updatedContext
|
||||
end
|
||||
|
||||
self.currentState = nextState
|
||||
|
||||
if self.stateChangeHandler ~= nil then
|
||||
self.stateChangeHandler(currentState, nextState, updatedContext)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Register a function to process changes in state. Your function should have
|
||||
the following signature and return nothing:
|
||||
|
||||
function handleStateChange(oldState, newState, updatedContext)
|
||||
self.currentState = newState
|
||||
self.currentContext = updatedContext
|
||||
end
|
||||
|
||||
The updatedContext parameter contains the table that was returned by the action
|
||||
handler associated with the event transition.
|
||||
]]
|
||||
function StateTable:onStateChange(stateChangeHandler)
|
||||
assert(stateChangeHandler == nil or typeof(stateChangeHandler) == "function",
|
||||
"stateChangeHandler must be nil or a function")
|
||||
self.stateChangeHandler = stateChangeHandler
|
||||
end
|
||||
|
||||
return StateTable
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
return function()
|
||||
local StateTable = require(script.Parent.StateTable)
|
||||
|
||||
local TEST_NAME = "test_state_table_name"
|
||||
local TEST_INITIAL_STATE = "Initial"
|
||||
local TEST_DATA = { foo = 1 }
|
||||
|
||||
local DUMMY_ACTION = function(_, _, data)
|
||||
return data
|
||||
end
|
||||
|
||||
local function FieldCount(t)
|
||||
local fieldCount = 0
|
||||
for _ in pairs(t) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
return fieldCount
|
||||
end
|
||||
|
||||
local function ShallowEqual(A, B)
|
||||
if not A or not B then
|
||||
return false
|
||||
elseif A == B then
|
||||
return true
|
||||
end
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= value then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(B) do
|
||||
if A[key] ~= value then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
describe("StateTable.new validation throw tests", function()
|
||||
it("should throw if name is nil", function()
|
||||
expect(function()
|
||||
StateTable.new(nil, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if name is not a string", function()
|
||||
expect(function()
|
||||
StateTable.new(5, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if name is empty", function()
|
||||
expect(function()
|
||||
StateTable.new("", TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if initialState is nil", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, nil, {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if initialState is not a string", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, 5, {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if initialState is empty", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, "", {}, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if initialContext is wrong type", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, 5, { Initial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if transitionTable is nil", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if empty transitionTable is provided", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-table transitionTable is provided", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, 1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if no arguments are provided", function()
|
||||
expect(function()
|
||||
StateTable.new()
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-string/userdata used for state name", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { [1] = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-table is used for state table", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = 1 })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-string/userdata is used for event name", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { [1] = {} } })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-table is used for event data table", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = 1 } })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-string/userdata used for nextState", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { nextState = 1 } } })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if non-function used for action", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = 1 } } })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if initial state not in transitionTable", function()
|
||||
expect(function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { NotInitial = {} })
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateTable.new validation success tests", function()
|
||||
it("should not throw if empty event table is provided", function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
end)
|
||||
|
||||
it("should not throw if empty event data table is provided", function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } })
|
||||
end)
|
||||
|
||||
it("should not throw if action is missing", function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Next" }
|
||||
},
|
||||
Next = {}
|
||||
})
|
||||
end)
|
||||
|
||||
it("should not throw if nextState is missing", function()
|
||||
StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = function() end } } })
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateTable.new event function creation tests", function()
|
||||
it("should create an event function for a single event", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } })
|
||||
expect(FieldCount(st.events)).to.equal(1)
|
||||
expect(typeof(st.events.Event1)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should create different event functions for multiple events", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { }, Event2 = { } } })
|
||||
expect(FieldCount(st.events)).to.equal(2)
|
||||
expect(typeof(st.events.Event1)).to.equal("function")
|
||||
expect(typeof(st.events.Event2)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should not create event functions without at least one event", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
expect(FieldCount(st.events)).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateTable onStateChange registration tests", function()
|
||||
it("should assert when non-function provided", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
expect(function()
|
||||
st:onStateChange(5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should not assert when function provided", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
st:onStateChange(function() end)
|
||||
end)
|
||||
|
||||
it("should not assert when nil provided", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = {} })
|
||||
st:onStateChange(nil)
|
||||
end)
|
||||
|
||||
it("should call onStateChange handler when transition occurs", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = {}
|
||||
}
|
||||
})
|
||||
|
||||
local called = false
|
||||
st:onStateChange(function()
|
||||
called = true
|
||||
end)
|
||||
|
||||
st.events.Event1(nil)
|
||||
expect(called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should not call onStateChange handler after it has been de-registered", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = {}
|
||||
}
|
||||
})
|
||||
|
||||
local called = false
|
||||
st:onStateChange(function()
|
||||
called = true
|
||||
end)
|
||||
|
||||
st:onStateChange(nil)
|
||||
|
||||
st.events.Event1(nil)
|
||||
expect(called).to.equal(false)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateTable event call tests", function()
|
||||
it("should change state when handleEvent is called", function()
|
||||
local action1Called = false
|
||||
local testAction1 = function() action1Called = true end
|
||||
|
||||
local action2Called = false
|
||||
local testAction2 = function() action2Called = true end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two", action = testAction1 }
|
||||
},
|
||||
Two = {
|
||||
Event2 = { action = testAction2 }
|
||||
}
|
||||
})
|
||||
|
||||
st:handleEvent("Event1", nil)
|
||||
expect(action1Called).to.equal(true)
|
||||
expect(action2Called).to.equal(false)
|
||||
|
||||
action1Called = false
|
||||
action2Called = false
|
||||
|
||||
st:handleEvent("Event2", nil)
|
||||
expect(action1Called).to.equal(false)
|
||||
expect(action2Called).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should call mapped action when handleEvent is called", function()
|
||||
local actionOldState, actionNewState, actionData
|
||||
local testAction = function(oldState, newState, data)
|
||||
actionOldState = oldState
|
||||
actionNewState = newState
|
||||
actionData = data
|
||||
end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two", action = testAction }
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
st:handleEvent("Event1", TEST_DATA)
|
||||
expect(actionOldState).to.equal("Initial")
|
||||
expect(actionNewState).to.equal("Two")
|
||||
expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return expected nextState and data in onStateChange callback when handleEvent is called", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two", action = DUMMY_ACTION }
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st:handleEvent("Event1", TEST_DATA)
|
||||
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Two")
|
||||
expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should call mapped action when event functor is called", function()
|
||||
local actionOldState, actionNewState, actionData
|
||||
local testAction = function(oldState, newState, data)
|
||||
actionOldState = oldState
|
||||
actionNewState = newState
|
||||
actionData = data
|
||||
end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two", action = testAction }
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
st.events.Event1(TEST_DATA)
|
||||
expect(actionOldState).to.equal("Initial")
|
||||
expect(actionNewState).to.equal("Two")
|
||||
expect(ShallowEqual(actionData, TEST_DATA)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return expected nextState and data in onStateChange callback when event functor is called", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two", action = DUMMY_ACTION }
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(TEST_DATA)
|
||||
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Two")
|
||||
expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should still return nextState in onStateChange callback when no action handler is provided", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { nextState = "Two" }
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(TEST_DATA)
|
||||
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Two")
|
||||
expect(typeof(updatedContext)).to.equal("table")
|
||||
expect(FieldCount(updatedContext)).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should return current state and empty data if new state and action handler are not specified", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { } } })
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(TEST_DATA)
|
||||
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Initial")
|
||||
expect(FieldCount(updatedContext)).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should return current state and matching data if only action handler is provided", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = { action = DUMMY_ACTION } } })
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(TEST_DATA)
|
||||
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Initial")
|
||||
expect(ShallowEqual(updatedContext, TEST_DATA)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return empty data in onStateChange callback when nil data is provided to no-action event", function()
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, { Initial = { Event1 = {} } })
|
||||
|
||||
local updatedContext
|
||||
st:onStateChange(function(_, _, uc)
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(nil)
|
||||
|
||||
expect(FieldCount(updatedContext)).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should merge context updates with old context", function()
|
||||
local initialContext = { foo = 1 }
|
||||
|
||||
local function action1()
|
||||
return { bar = 2 }
|
||||
end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, initialContext, {
|
||||
Initial = {
|
||||
Event1 = { action = action1 }
|
||||
}
|
||||
})
|
||||
|
||||
local updatedContext
|
||||
st:onStateChange(function(_, _, uc)
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1()
|
||||
|
||||
expect(ShallowEqual(updatedContext, { foo = 1, bar = 2 })).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should pass args to actions", function()
|
||||
local passedArgs
|
||||
local function action1(_, _, args)
|
||||
passedArgs = args
|
||||
end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = { Event1 = { action = action1 } }
|
||||
})
|
||||
|
||||
local theArgs = { argsAreHere = true }
|
||||
st.events.Event1(theArgs)
|
||||
|
||||
expect(ShallowEqual(theArgs, passedArgs)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should call actions independently for different events", function()
|
||||
local testData1 = TEST_DATA
|
||||
local testData2 = { foo = 2 }
|
||||
|
||||
local action1OldState, action1NewState, action1Data
|
||||
local testAction1 = function(oldState, newState, data)
|
||||
action1OldState = oldState
|
||||
action1NewState = newState
|
||||
action1Data = data
|
||||
return data
|
||||
end
|
||||
|
||||
local action2OldState, action2NewState, action2Data
|
||||
local testAction2 = function(oldState, newState, data)
|
||||
action2OldState = oldState
|
||||
action2NewState = newState
|
||||
action2Data = data
|
||||
return data
|
||||
end
|
||||
|
||||
local st = StateTable.new(TEST_NAME, TEST_INITIAL_STATE, {}, {
|
||||
Initial = {
|
||||
Event1 = { action = testAction1 },
|
||||
Event2 = { nextState = "Two", action = testAction2 },
|
||||
},
|
||||
Two = {}
|
||||
})
|
||||
|
||||
local oldState, newState, updatedContext
|
||||
st:onStateChange(function(os, ns, uc)
|
||||
oldState = os
|
||||
newState = ns
|
||||
updatedContext = uc
|
||||
end)
|
||||
|
||||
st.events.Event1(testData1)
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Initial")
|
||||
expect(ShallowEqual(updatedContext, testData1)).to.equal(true)
|
||||
|
||||
st.events.Event2(testData2)
|
||||
expect(oldState).to.equal("Initial")
|
||||
expect(newState).to.equal("Two")
|
||||
expect(ShallowEqual(updatedContext, testData2)).to.equal(true)
|
||||
|
||||
expect(action1OldState).to.equal("Initial")
|
||||
expect(action2OldState).to.equal("Initial")
|
||||
expect(action1NewState).to.equal("Initial")
|
||||
expect(action2NewState).to.equal("Two")
|
||||
|
||||
expect(ShallowEqual(action1Data, testData1)).to.equal(true)
|
||||
expect(ShallowEqual(action2Data, testData2)).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- Generator information:
|
||||
-- Human name: State Table
|
||||
-- Variable name: StateTable
|
||||
-- Repo name: state-table
|
||||
|
||||
local StateTable = require(script.StateTable)
|
||||
|
||||
return StateTable
|
||||
|
||||
Reference in New Issue
Block a user