add gs
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local NONE_TOKEN = NavigationSymbol("NONE")
|
||||
local INITIAL_ROUTE_TOKEN = NavigationSymbol("INITIAL_ROUTE")
|
||||
local ORDER_TOKEN = NavigationSymbol("ORDER")
|
||||
local HISTORY_TOKEN = NavigationSymbol("HISTORY")
|
||||
|
||||
--[[
|
||||
BackBehavior provides shared constants that are used to configure back
|
||||
action styles for different navigators. Note that not all routers support
|
||||
all BackBehaviors and they will fall back to appropriate defaults for
|
||||
those cases.
|
||||
]]
|
||||
local BackBehavior = {
|
||||
None = NONE_TOKEN,
|
||||
InitialRoute = INITIAL_ROUTE_TOKEN,
|
||||
Order = ORDER_TOKEN,
|
||||
History = HISTORY_TOKEN,
|
||||
}
|
||||
|
||||
-- we are using this metatable to error when BackBehavior is indexed
|
||||
-- with an unexpected key.
|
||||
setmetatable(BackBehavior, {
|
||||
__index = function(self, key)
|
||||
error(("%q is not a valid member of BackBehavior"):format(tostring(key)), 2)
|
||||
end,
|
||||
})
|
||||
|
||||
return BackBehavior
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local WILL_FOCUS_TOKEN = NavigationSymbol("WILL_FOCUS")
|
||||
local DID_FOCUS_TOKEN = NavigationSymbol("DID_FOCUS")
|
||||
local WILL_BLUR_TOKEN = NavigationSymbol("WILL_BLUR")
|
||||
local DID_BLUR_TOKEN = NavigationSymbol("DID_BLUR")
|
||||
local ACTION_TOKEN = NavigationSymbol("ACTION")
|
||||
local REFOCUS_TOKEN = NavigationSymbol("REFOCUS")
|
||||
|
||||
--[[
|
||||
Events provides shared constants that are used to register
|
||||
listeners for different RoactNavigation UI state changes.
|
||||
]]
|
||||
return {
|
||||
WillFocus = WILL_FOCUS_TOKEN,
|
||||
DidFocus = DID_FOCUS_TOKEN,
|
||||
WillBlur = WILL_BLUR_TOKEN,
|
||||
DidBlur = DID_BLUR_TOKEN,
|
||||
Action = ACTION_TOKEN,
|
||||
Refocus = REFOCUS_TOKEN,
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/NavigationActions.ts
|
||||
|
||||
local NavigationSymbol = require(script.Parent.NavigationSymbol)
|
||||
|
||||
local BACK_TOKEN = NavigationSymbol("BACK")
|
||||
local INIT_TOKEN = NavigationSymbol("INIT")
|
||||
local NAVIGATE_TOKEN = NavigationSymbol("NAVIGATE")
|
||||
local SET_PARAMS_TOKEN = NavigationSymbol("SET_PARAMS")
|
||||
|
||||
--[[
|
||||
NavigationActions provides shared constants and methods to construct
|
||||
actions that are dispatched to routers to cause a change in the route.
|
||||
]]
|
||||
local NavigationActions = {
|
||||
Back = BACK_TOKEN,
|
||||
Init = INIT_TOKEN,
|
||||
Navigate = NAVIGATE_TOKEN,
|
||||
SetParams = SET_PARAMS_TOKEN,
|
||||
}
|
||||
|
||||
-- deviation: we using this metatable to error when NavigationActions is indexed
|
||||
-- with an unexpected key.
|
||||
setmetatable(NavigationActions, {
|
||||
__index = function(self, key)
|
||||
error(("%q is not a valid member of NavigationActions"):format(tostring(key)), 2)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Navigate back in the history (temporally).
|
||||
function NavigationActions.back(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = BACK_TOKEN,
|
||||
key = data.key,
|
||||
immediate = data.immediate,
|
||||
}
|
||||
end
|
||||
|
||||
-- Initialize the navigation history if not already defined.
|
||||
function NavigationActions.init(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = INIT_TOKEN,
|
||||
params = data.params,
|
||||
}
|
||||
end
|
||||
|
||||
-- Navigate to an existing or new route.
|
||||
function NavigationActions.navigate(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = NAVIGATE_TOKEN,
|
||||
routeName = data.routeName,
|
||||
params = data.params,
|
||||
action = data.action,
|
||||
key = data.key,
|
||||
}
|
||||
end
|
||||
|
||||
-- Swap out the params for an existing route, matched by the given key.
|
||||
function NavigationActions.setParams(payload)
|
||||
local data = payload or {}
|
||||
return {
|
||||
type = SET_PARAMS_TOKEN,
|
||||
preserveFocus = true,
|
||||
key = data.key,
|
||||
params = data.params,
|
||||
}
|
||||
end
|
||||
|
||||
return NavigationActions
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
-- Taken from Roact.Symbol and modified to produce exact string names
|
||||
-- to allow for serialization/pathing.
|
||||
|
||||
return function (name)
|
||||
assert(type(name) == "string", "Symbols must be created using a string name!")
|
||||
|
||||
local self = newproxy(true)
|
||||
|
||||
-- Unlike Symbols in Roact, we need the exact names.
|
||||
getmetatable(self).__tostring = function()
|
||||
return name
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/StateUtils.js
|
||||
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
|
||||
--[[
|
||||
StateUtils provides utilities to read and write standard route data.
|
||||
Routes have the following general structure:
|
||||
{
|
||||
index = <required integer: current active route in list>,
|
||||
routes = [
|
||||
{
|
||||
routeName = <required string: navigation name for route>,
|
||||
key = <unique string: identifier for this route>,
|
||||
params = <optional dictionary: params for screen>,
|
||||
action = <optional dictionary: sub-action to be run by child routers>,
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
This structure is independent of the notion of stack, tab, drawer, or any
|
||||
other kind of navigation. It simply represents a list of pages and their
|
||||
parameters. Different kinds of routers can treat the data in their own way.
|
||||
]]
|
||||
local StateUtils = {}
|
||||
|
||||
-- Get the route matching the given key. Returns nil if no match is found.
|
||||
function StateUtils.get(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for _, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return route
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Get the index of the route matching the given key. Returns nil if no match is found.
|
||||
function StateUtils.indexOf(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for index, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return index
|
||||
end
|
||||
end
|
||||
|
||||
-- deviation: returning nil instead of -1
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Returns true if a route exists matching the given key, false otherwise.
|
||||
function StateUtils.has(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
for _, route in ipairs(state.routes) do
|
||||
if route.key == key then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
-- Push a new route into the navigation state. Makes the pushed route active.
|
||||
function StateUtils.push(state, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
assert(StateUtils.indexOf(state, route.key) == nil,
|
||||
("should not push route with duplicated key %s"):format(route.key))
|
||||
|
||||
local routes = Cryo.List.join(state.routes, { route })
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = #routes,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Pop the top-most route from the navigation state (NOT the active route).
|
||||
-- Makes the new top-most route active.
|
||||
function StateUtils.pop(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
if state.index <= 1 then
|
||||
-- [Note]: Over-popping does not throw error. Instead, it will be no-op.
|
||||
return state
|
||||
end
|
||||
|
||||
local routes = Cryo.List.removeIndex(state.routes, #state.routes)
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = #routes,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Sets the active route to match the given index.
|
||||
function StateUtils.jumpToIndex(state, index)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(index) == "number", "index must be a number")
|
||||
|
||||
if index == state.index then
|
||||
return state
|
||||
end
|
||||
|
||||
assert(state.routes[index] ~= nil, ("invalid index %d to jump to"):format(index))
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
})
|
||||
end
|
||||
|
||||
-- Sets the active route to match the given key.
|
||||
function StateUtils.jumpTo(state, key)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
assert(index ~= nil, ('attempt to jump to unknown key "%s"'):format(key))
|
||||
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Sets the active route to the previous route in the list.
|
||||
function StateUtils.back(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
local index = state.index - 1
|
||||
if not state.routes[index] then
|
||||
return state
|
||||
end
|
||||
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Sets the active route to the next route in the list.
|
||||
function StateUtils.forward(state)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
|
||||
local index = state.index + 1
|
||||
if not state.routes[index] then
|
||||
return state
|
||||
end
|
||||
|
||||
return StateUtils.jumpToIndex(state, index)
|
||||
end
|
||||
|
||||
-- Replace the route matching the given key. Sets the active route to the
|
||||
-- newly replaced entry. Prunes the old entries that follow the replaced one.
|
||||
function StateUtils.replaceAndPrune(state, key, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
local replaced = StateUtils.replaceAtIndex(state, index, route)
|
||||
|
||||
return Cryo.Dictionary.join(replaced, {
|
||||
routes = { unpack(replaced.routes, 1, index) }
|
||||
})
|
||||
end
|
||||
|
||||
-- Replace the route matching the given key without pruning the following routes.
|
||||
-- The active route will be updated to match the newly replaced one unless
|
||||
-- preserveIndex is true.
|
||||
function StateUtils.replaceAt(state, key, route, preserveIndex)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(key) == "string", "key must be a string")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
assert(preserveIndex == nil or type(preserveIndex) == "boolean",
|
||||
"preserveIndex must be nil or a boolean")
|
||||
|
||||
local index = StateUtils.indexOf(state, key)
|
||||
local nextIndex = preserveIndex and state.index or index
|
||||
local nextState = StateUtils.replaceAtIndex(state, index, route)
|
||||
nextState.index = nextIndex
|
||||
return nextState
|
||||
end
|
||||
|
||||
-- Replace the route at the given index. Updates the active route to point to
|
||||
-- the replaced entry.
|
||||
function StateUtils.replaceAtIndex(state, index, route)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(index) == "number", "index must be a number")
|
||||
assert(type(route) == "table", "route must be a table")
|
||||
|
||||
assert(state.routes[index] ~= nil,
|
||||
("invalid index %d for replacing route %s"):format(index, route.key))
|
||||
|
||||
if state.routes[index] == route and index == state.index then
|
||||
return state
|
||||
end
|
||||
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[index] = route
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
-- Wipe away the existing routes and replace them with new routes.
|
||||
-- Sets the active route to the provided index (if provided), otherwise
|
||||
-- sets the active route to the last one in the list.
|
||||
function StateUtils.reset(state, routes, index)
|
||||
assert(type(state) == "table", "state must be a table")
|
||||
assert(type(routes) == "table" and #routes > 0, "invalid routes to replace")
|
||||
assert(index == nil or type(index) == "number", "index must be a number or nil")
|
||||
|
||||
local nextIndex = not index and #routes or index
|
||||
|
||||
-- Bail out without replacing IFF index and routes all match
|
||||
if #state.routes == #routes and state.index == nextIndex then
|
||||
local routesAreEqual = true
|
||||
for i = 1, #routes, 1 do
|
||||
if state.routes[i] ~= routes[i] then
|
||||
routesAreEqual = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if routesAreEqual then
|
||||
return state
|
||||
end
|
||||
end
|
||||
|
||||
assert(routes[nextIndex] ~= nil, ("invalid index %d to reset"):format(nextIndex))
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
index = nextIndex,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
return StateUtils
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
return function()
|
||||
local BackBehavior = require(script.Parent.Parent.BackBehavior)
|
||||
|
||||
describe("BackBehavior token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(BackBehavior.None).to.equal(BackBehavior.None)
|
||||
expect(BackBehavior.InitialRoute).to.equal(BackBehavior.InitialRoute)
|
||||
expect(BackBehavior.Order).to.equal(BackBehavior.Order)
|
||||
expect(BackBehavior.History).to.equal(BackBehavior.History)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(BackBehavior.None)).to.equal("NONE")
|
||||
expect(tostring(BackBehavior.InitialRoute)).to.equal("INITIAL_ROUTE")
|
||||
expect(tostring(BackBehavior.Order)).to.equal("ORDER")
|
||||
expect(tostring(BackBehavior.History)).to.equal("HISTORY")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
return function()
|
||||
local Events = require(script.Parent.Parent.Events)
|
||||
|
||||
describe("Events token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(Events.WillFocus).to.equal(Events.WillFocus)
|
||||
expect(Events.DidFocus).to.equal(Events.DidFocus)
|
||||
expect(Events.WillBlur).to.equal(Events.WillBlur)
|
||||
expect(Events.DidBlur).to.equal(Events.DidBlur)
|
||||
expect(Events.Action).to.equal(Events.Action)
|
||||
expect(Events.Refocus).to.equal(Events.Refocus)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(Events.WillFocus)).to.equal("WILL_FOCUS")
|
||||
expect(tostring(Events.DidFocus)).to.equal("DID_FOCUS")
|
||||
expect(tostring(Events.WillBlur)).to.equal("WILL_BLUR")
|
||||
expect(tostring(Events.DidBlur)).to.equal("DID_BLUR")
|
||||
expect(tostring(Events.Action)).to.equal("ACTION")
|
||||
expect(tostring(Events.Refocus)).to.equal("REFOCUS")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
return function()
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
|
||||
it("throws when indexing an unknown field", function()
|
||||
expect(function()
|
||||
return NavigationActions.foo
|
||||
end).to.throw("\"foo\" is not a valid member of NavigationActions")
|
||||
end)
|
||||
|
||||
describe("NavigationActions token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(NavigationActions.Back).to.equal(NavigationActions.Back)
|
||||
expect(NavigationActions.Init).to.equal(NavigationActions.Init)
|
||||
expect(NavigationActions.Navigate).to.equal(NavigationActions.Navigate)
|
||||
expect(NavigationActions.SetParams).to.equal(NavigationActions.SetParams)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(NavigationActions.Back)).to.equal("BACK")
|
||||
expect(tostring(NavigationActions.Init)).to.equal("INIT")
|
||||
expect(tostring(NavigationActions.Navigate)).to.equal("NAVIGATE")
|
||||
expect(tostring(NavigationActions.SetParams)).to.equal("SET_PARAMS")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("NavigationActions function tests", function()
|
||||
it("should return a back action with matching data for a call to back()", function()
|
||||
local backTable = NavigationActions.back({
|
||||
key = "the_key",
|
||||
immediate = true,
|
||||
})
|
||||
|
||||
expect(backTable.type).to.equal(NavigationActions.Back)
|
||||
expect(backTable.key).to.equal("the_key")
|
||||
expect(backTable.immediate).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return an init action with matching data for call to init()", function()
|
||||
local initTable = NavigationActions.init({
|
||||
params = "foo",
|
||||
})
|
||||
|
||||
expect(initTable.type).to.equal(NavigationActions.Init)
|
||||
expect(initTable.params).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should return a navigate action with matching data for call to navigate()", function()
|
||||
local navigateTable = NavigationActions.navigate({
|
||||
routeName = "routeName",
|
||||
params = "foo",
|
||||
action = "action",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(navigateTable.type).to.equal(NavigationActions.Navigate)
|
||||
expect(navigateTable.routeName).to.equal("routeName")
|
||||
expect(navigateTable.params).to.equal("foo")
|
||||
expect(navigateTable.action).to.equal("action")
|
||||
expect(navigateTable.key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should return a set params action with matching data for call to setParams()", function()
|
||||
local setParamsTable = NavigationActions.setParams({
|
||||
key = "key",
|
||||
params = "foo",
|
||||
})
|
||||
|
||||
expect(setParamsTable.type).to.equal(NavigationActions.SetParams)
|
||||
expect(setParamsTable.key).to.equal("key")
|
||||
expect(setParamsTable.params).to.equal("foo")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/__tests__/NavigationActions.test.js
|
||||
|
||||
return function()
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
|
||||
local expectDeepEqual = require(script.Parent.Parent.utils.expectDeepEqual)
|
||||
|
||||
describe("generic navigation actions", function()
|
||||
local params = { foo = "bar" }
|
||||
local navigateAction = NavigationActions.navigate({ routeName = "another" })
|
||||
|
||||
it("exports back action and type", function()
|
||||
expectDeepEqual(NavigationActions.back(), { type = NavigationActions.Back })
|
||||
expectDeepEqual(
|
||||
NavigationActions.back({ key = "test" }),
|
||||
{
|
||||
type = NavigationActions.Back,
|
||||
key = "test",
|
||||
}
|
||||
)
|
||||
end)
|
||||
|
||||
it("exports init action and type", function()
|
||||
expectDeepEqual(NavigationActions.init(), { type = NavigationActions.Init })
|
||||
expectDeepEqual(
|
||||
NavigationActions.init({ params = params }),
|
||||
{
|
||||
type = NavigationActions.Init,
|
||||
params = params,
|
||||
}
|
||||
)
|
||||
end)
|
||||
|
||||
it("exports navigate action and type", function()
|
||||
expectDeepEqual(
|
||||
NavigationActions.navigate({ routeName = "test" }),
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "test",
|
||||
}
|
||||
)
|
||||
expectDeepEqual(
|
||||
NavigationActions.navigate({
|
||||
routeName = "test",
|
||||
params = params,
|
||||
action = navigateAction,
|
||||
}),
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "test",
|
||||
params = params,
|
||||
action = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "another",
|
||||
},
|
||||
}
|
||||
)
|
||||
end)
|
||||
|
||||
it("exports setParams action and type", function()
|
||||
expectDeepEqual(
|
||||
NavigationActions.setParams({
|
||||
key = "test",
|
||||
params = params,
|
||||
}),
|
||||
{
|
||||
type = NavigationActions.SetParams,
|
||||
key = "test",
|
||||
preserveFocus = true,
|
||||
params = params,
|
||||
}
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/__tests__/NavigationFocusEvents.test.js
|
||||
local RunService = game:GetService("RunService")
|
||||
|
||||
return function()
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
|
||||
local TabRouter = require(root.routers.TabRouter)
|
||||
local createAppContainer = require(root.createAppContainer)
|
||||
local createNavigator = require(root.navigators.createNavigator)
|
||||
local Events = require(root.Events)
|
||||
local NavigationActions = require(root.NavigationActions)
|
||||
local createSpy = require(root.utils.createSpy)
|
||||
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
|
||||
-- deviation: utility function moved out of test scope because
|
||||
-- it is shared across both tests
|
||||
local function createTestNavigator(routeConfigMap, config)
|
||||
config = config or {}
|
||||
local router = TabRouter(routeConfigMap, config)
|
||||
|
||||
return createNavigator(
|
||||
function(props)
|
||||
local navigation = props.navigation
|
||||
local descriptors = props.descriptors
|
||||
|
||||
local children = Cryo.List.foldLeft(navigation.state.routes, function(acc, route)
|
||||
local Comp = descriptors[route.key].getComponent()
|
||||
acc[route.key] = Roact.createElement(Comp, {
|
||||
key = route.key,
|
||||
navigation = descriptors[route.key].navigation
|
||||
})
|
||||
return acc
|
||||
end, {})
|
||||
|
||||
return Roact.createFragment(children)
|
||||
end,
|
||||
router,
|
||||
config
|
||||
)
|
||||
end
|
||||
|
||||
local function waitUntil(predicate, timeout)
|
||||
timeout = timeout or 1
|
||||
local waitedTime = 0
|
||||
while waitedTime < timeout and not predicate() do
|
||||
waitedTime = waitedTime + RunService.Heartbeat:Wait()
|
||||
end
|
||||
end
|
||||
|
||||
-- deviation: utility function moved out of test scope because
|
||||
-- it is shared across both tests
|
||||
local function createComponent(focusCallback, blurCallback)
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
|
||||
function TestComponent:didMount()
|
||||
local navigation = self.props.navigation
|
||||
|
||||
self.focusSub = navigation.addListener(Events.WillFocus, focusCallback)
|
||||
self.blurSub = navigation.addListener(Events.WillBlur, blurCallback)
|
||||
end
|
||||
|
||||
function TestComponent:willUnmount()
|
||||
self.focusSub.remove()
|
||||
self.blurSub.remove()
|
||||
end
|
||||
|
||||
function TestComponent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
return TestComponent
|
||||
end
|
||||
|
||||
it("fires focus and blur events in root navigator", function()
|
||||
local firstFocusCallback = createSpy()
|
||||
local firstBlurCallback = createSpy()
|
||||
|
||||
local secondFocusCallback = createSpy()
|
||||
local secondBlurCallback = createSpy()
|
||||
|
||||
local thirdFocusCallback = createSpy()
|
||||
local thirdBlurCallback = createSpy()
|
||||
|
||||
local fourthFocusCallback = createSpy()
|
||||
local fourthBlurCallback = createSpy()
|
||||
|
||||
local Navigator = createAppContainer(
|
||||
createTestNavigator({
|
||||
{ first = createComponent(firstFocusCallback.value, firstBlurCallback.value) },
|
||||
{ second = createComponent(secondFocusCallback.value, secondBlurCallback.value) },
|
||||
{ third = createComponent(thirdFocusCallback.value, thirdBlurCallback.value) },
|
||||
{ fourth = createComponent(fourthFocusCallback.value, fourthBlurCallback.value) },
|
||||
})
|
||||
)
|
||||
|
||||
local dispatch
|
||||
local element = Roact.createElement(Navigator, {
|
||||
externalDispatchConnector = function(currentDispatch)
|
||||
dispatch = currentDispatch
|
||||
return function ()
|
||||
dispatch = nil
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
Roact.mount(element)
|
||||
|
||||
waitUntil(function()
|
||||
return firstFocusCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(firstFocusCallback.callCount).to.equal(1)
|
||||
expect(firstBlurCallback.callCount).to.equal(0)
|
||||
expect(secondFocusCallback.callCount).to.equal(0)
|
||||
expect(secondBlurCallback.callCount).to.equal(0)
|
||||
expect(thirdFocusCallback.callCount).to.equal(0)
|
||||
expect(thirdBlurCallback.callCount).to.equal(0)
|
||||
expect(fourthFocusCallback.callCount).to.equal(0)
|
||||
expect(fourthBlurCallback.callCount).to.equal(0)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'second' }))
|
||||
|
||||
waitUntil(function()
|
||||
return firstBlurCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(firstBlurCallback.callCount).to.equal(1)
|
||||
expect(secondFocusCallback.callCount).to.equal(1)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'fourth' }))
|
||||
|
||||
waitUntil(function()
|
||||
return secondBlurCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(firstFocusCallback.callCount).to.equal(1)
|
||||
expect(firstBlurCallback.callCount).to.equal(1)
|
||||
expect(secondFocusCallback.callCount).to.equal(1)
|
||||
expect(secondBlurCallback.callCount).to.equal(1)
|
||||
expect(thirdFocusCallback.callCount).to.equal(0)
|
||||
expect(thirdBlurCallback.callCount).to.equal(0)
|
||||
expect(fourthFocusCallback.callCount).to.equal(1)
|
||||
expect(fourthBlurCallback.callCount).to.equal(0)
|
||||
end)
|
||||
|
||||
it('fires focus and blur events in nested navigator', function()
|
||||
local firstFocusCallback = createSpy()
|
||||
local firstBlurCallback = createSpy()
|
||||
|
||||
local secondFocusCallback = createSpy()
|
||||
local secondBlurCallback = createSpy()
|
||||
|
||||
local thirdFocusCallback = createSpy()
|
||||
local thirdBlurCallback = createSpy()
|
||||
|
||||
local fourthFocusCallback = createSpy()
|
||||
local fourthBlurCallback = createSpy()
|
||||
|
||||
local Navigator = createAppContainer(
|
||||
createTestNavigator({
|
||||
{ first = createComponent(firstFocusCallback.value, firstBlurCallback.value) },
|
||||
{ second = createComponent(secondFocusCallback.value, secondBlurCallback.value) },
|
||||
{
|
||||
nested = createTestNavigator({
|
||||
{ third = createComponent(thirdFocusCallback.value, thirdBlurCallback.value) },
|
||||
{ fourth = createComponent(fourthFocusCallback.value, fourthBlurCallback.value) },
|
||||
})
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
local dispatch
|
||||
local element = Roact.createElement(Navigator, {
|
||||
externalDispatchConnector = function(currentDispatch)
|
||||
dispatch = currentDispatch
|
||||
return function ()
|
||||
dispatch = nil
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
Roact.mount(element)
|
||||
|
||||
waitUntil(function()
|
||||
return firstFocusCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(thirdFocusCallback.callCount).to.equal(0)
|
||||
expect(firstFocusCallback.callCount).to.equal(1)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'nested' }))
|
||||
|
||||
waitUntil(function()
|
||||
return thirdFocusCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(firstFocusCallback.callCount).to.equal(1)
|
||||
expect(fourthFocusCallback.callCount).to.equal(0)
|
||||
expect(thirdFocusCallback.callCount).to.equal(1)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'second' }))
|
||||
|
||||
waitUntil(function()
|
||||
return secondFocusCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(thirdFocusCallback.callCount).to.equal(1)
|
||||
expect(secondFocusCallback.callCount).to.equal(1)
|
||||
expect(fourthBlurCallback.callCount).to.equal(0)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'nested' }))
|
||||
|
||||
waitUntil(function()
|
||||
return thirdFocusCallback.callCount > 1
|
||||
end)
|
||||
|
||||
expect(firstBlurCallback.callCount).to.equal(1)
|
||||
expect(secondBlurCallback.callCount).to.equal(1)
|
||||
expect(thirdFocusCallback.callCount).to.equal(2)
|
||||
expect(fourthFocusCallback.callCount).to.equal(0)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'third' }))
|
||||
|
||||
expect(fourthBlurCallback.callCount).to.equal(0)
|
||||
expect(thirdFocusCallback.callCount).to.equal(2)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'first' }))
|
||||
|
||||
waitUntil(function()
|
||||
return firstFocusCallback.callCount > 1
|
||||
end)
|
||||
|
||||
expect(firstFocusCallback.callCount).to.equal(2)
|
||||
expect(thirdBlurCallback.callCount).to.equal(2)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'fourth' }))
|
||||
|
||||
waitUntil(function()
|
||||
return fourthFocusCallback.callCount > 0
|
||||
end)
|
||||
|
||||
expect(fourthFocusCallback.callCount).to.equal(1)
|
||||
expect(thirdBlurCallback.callCount).to.equal(2)
|
||||
expect(firstBlurCallback.callCount).to.equal(2)
|
||||
|
||||
dispatch(NavigationActions.navigate({ routeName = 'third' }))
|
||||
|
||||
waitUntil(function()
|
||||
return thirdFocusCallback.callCount > 2
|
||||
end)
|
||||
|
||||
expect(thirdFocusCallback.callCount).to.equal(3)
|
||||
expect(fourthBlurCallback.callCount).to.equal(1)
|
||||
|
||||
-- Make sure nothing else has changed
|
||||
expect(firstFocusCallback.callCount).to.equal(2)
|
||||
expect(firstBlurCallback.callCount).to.equal(2)
|
||||
|
||||
expect(secondFocusCallback.callCount).to.equal(1)
|
||||
expect(secondBlurCallback.callCount).to.equal(1)
|
||||
|
||||
expect(thirdFocusCallback.callCount).to.equal(3)
|
||||
expect(thirdBlurCallback.callCount).to.equal(2)
|
||||
|
||||
expect(fourthFocusCallback.callCount).to.equal(1)
|
||||
expect(fourthBlurCallback.callCount).to.equal(1)
|
||||
end)
|
||||
end
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
return function()
|
||||
local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol)
|
||||
|
||||
it("should give an opaque object", function()
|
||||
local symbol = NavigationSymbol("foo")
|
||||
|
||||
expect(symbol).to.be.a("userdata")
|
||||
end)
|
||||
|
||||
it("should coerce to the given name", function()
|
||||
local symbol = NavigationSymbol("foo")
|
||||
|
||||
expect(tostring(symbol)).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should be unique when constructed", function()
|
||||
local symbolA = NavigationSymbol("abc")
|
||||
local symbolB = NavigationSymbol("abc")
|
||||
|
||||
expect(symbolA).never.to.equal(symbolB)
|
||||
end)
|
||||
end
|
||||
+630
@@ -0,0 +1,630 @@
|
||||
return function()
|
||||
local StateUtils = require(script.Parent.Parent.StateUtils)
|
||||
|
||||
describe("StateUtils.get tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.get(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.get({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if key is not found in routes", function()
|
||||
local result = StateUtils.get({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
},
|
||||
},
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return route if key is found in routes", function()
|
||||
local result = StateUtils.get({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
},
|
||||
}, "foo-1")
|
||||
|
||||
expect(result.routeName).to.equal("foo")
|
||||
expect(result.key).to.equal("foo-1")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.indexOf tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.indexOf(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.indexOf({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return nil if key is not found in routes", function()
|
||||
local result = StateUtils.indexOf({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
},
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return index if key is found in routes", function()
|
||||
local result = StateUtils.indexOf({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
},
|
||||
{
|
||||
routeName = "foo2",
|
||||
key = "foo-2",
|
||||
}
|
||||
},
|
||||
}, "foo-2")
|
||||
|
||||
expect(result).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.has tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.has(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.has({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return false if key is not in routes", function()
|
||||
local result = StateUtils.has({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, "key")
|
||||
|
||||
expect(result).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return true if key is found in routes", function()
|
||||
local result = StateUtils.has({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, "foo-1")
|
||||
|
||||
expect(result).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.push tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.push(nil, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.push({}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route.key is already present", function()
|
||||
expect(function()
|
||||
StateUtils.push({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
}
|
||||
}
|
||||
}, {
|
||||
routeName = "foo",
|
||||
key = "foo-1",
|
||||
})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should insert new route if it doesn't exist", function()
|
||||
local newState = StateUtils.push({
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
routeName = "first",
|
||||
key = "foo-1",
|
||||
},
|
||||
},
|
||||
}, {
|
||||
routeName = "second",
|
||||
key = "foo-2",
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[newState.index].key).to.equal("foo-2")
|
||||
expect(newState.routes[newState.index].routeName).to.equal("second")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.pop tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.pop(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return existing state if routes is empty", function()
|
||||
local initialState = {
|
||||
index = 0,
|
||||
routes = {},
|
||||
}
|
||||
|
||||
local newState = StateUtils.pop(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should remove top route if popping with more than one route", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "route-1", },
|
||||
{ routeName = "route", key = "route-2", },
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.pop(initialState)
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].key).to.equal("route-1")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.jumpToIndex tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex(nil, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex({}, "foo")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index does not match a route", function()
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex({
|
||||
index = 1,
|
||||
routes = { { routeName = "first", key = "first-1" } }
|
||||
}, 5)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if index matches current", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { { routeName = "one", key = "1" } }
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpToIndex(initialState, 1)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should return updated state if index differs", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "route-1" },
|
||||
{ routeName = "route", key = "route-2" },
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpToIndex(initialState, 2)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.jumpTo tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.jumpTo(nil, "key")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.jumpTo({}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if key is already active route", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpTo(initialState, "key-1")
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should return state with new active route if key is not active", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.jumpTo(initialState, "key-2")
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.back tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.back(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if route for new index does not exist", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.back(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should remove top state if there is somewhere to go", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.back(initialState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.forward tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.forward(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should not walk off the end of the route list", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.forward(initialState)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should move to next route if available", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.forward(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAndPrune tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune(nil, "key", {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune({}, 0, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAndPrune({}, "key", 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should replace matching route and prune following routes", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAndPrune(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAt tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt(nil, "key", {}, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if key is not a string", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, 0, {}, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, "key", 0, false)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if preserveIndex is not a boolean", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAt({}, "key", {}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should replace matching route, not prune, and update index", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAt(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
}, false)
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
|
||||
it("should replace matching route, not prune, and preserve existing index", function()
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ routeName = "route", key = "key-1" },
|
||||
{ routeName = "route", key = "key-2" },
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAt(initialState, "key-1", {
|
||||
routeName = "newRoute", key = "key-3"
|
||||
}, true)
|
||||
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.routes[1].routeName).to.equal("newRoute")
|
||||
expect(newState.routes[1].key).to.equal("key-3")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.replaceAtIndex tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex(nil, 0, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({}, nil, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if route is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({}, 5, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index does not exist", function()
|
||||
expect(function()
|
||||
StateUtils.replaceAtIndex({
|
||||
index = 0,
|
||||
routes = {}
|
||||
}, 5, { routeName = "name", key = "key" })
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if inputs are same", function()
|
||||
local testRoute = { routeName = "name", key = "key" }
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { testRoute },
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 1, testRoute)
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should replace route at index if route is not equal", function()
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "name", key = "key" }
|
||||
},
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 1, {
|
||||
routeName = "newName",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(#newState.routes).to.equal(1)
|
||||
expect(newState.routes[1].routeName).to.equal("newName")
|
||||
expect(newState.routes[1].key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should update index, if new index differs but route does not", function()
|
||||
local testRoute = { routeName = "name", key = "key-2" }
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ routeName = "name", key = "key-1" },
|
||||
testRoute,
|
||||
}
|
||||
}
|
||||
|
||||
local newState = StateUtils.replaceAtIndex(initialState, 2, testRoute)
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StateUtils.reset tests", function()
|
||||
it("should assert if state is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.reset(nil, {}, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if routes is not a table", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, nil, 0)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should assert if index is not a number", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, {}, "foo")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
-- the test does not seem to match with the name
|
||||
it("should NOT assert if index is nil", function()
|
||||
expect(function()
|
||||
StateUtils.reset({}, {})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should return original state if index matches and all routes are same objects", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
route2,
|
||||
}, 2)
|
||||
|
||||
expect(newState).to.equal(initialState)
|
||||
end)
|
||||
|
||||
it("should update state if index is not specified and old index is not last route", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
route2,
|
||||
})
|
||||
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(newState.index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should update state if index matches but routes differ", function()
|
||||
local route1 = { routeName = "route1", key = "route-1" }
|
||||
local route2 = { routeName = "route2", key = "route-2" }
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
routes = { route1, route2 },
|
||||
}
|
||||
|
||||
local newState = StateUtils.reset(initialState, {
|
||||
route1,
|
||||
{ routeName = "route3", key = "route-3" },
|
||||
}, 1)
|
||||
|
||||
expect(newState).never.to.equal(initialState)
|
||||
expect(#newState.routes).to.equal(2)
|
||||
expect(newState.index).to.equal(1)
|
||||
expect(newState.routes[2].routeName).to.equal("route3")
|
||||
expect(newState.routes[2].key).to.equal("route-3")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+500
@@ -0,0 +1,500 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/__tests__/NavigationStateUtils.test.js
|
||||
|
||||
return function()
|
||||
local StateUtils = require(script.Parent.Parent.StateUtils)
|
||||
|
||||
local utils = script.Parent.Parent.utils
|
||||
local expectDeepEqual = require(utils.expectDeepEqual)
|
||||
|
||||
local routeName = "Anything"
|
||||
|
||||
describe("StateUtils", function()
|
||||
describe("get", function()
|
||||
it("gets route", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
key = "a",
|
||||
routeName = routeName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.get(state, "a"),
|
||||
{
|
||||
key = "a",
|
||||
routeName = routeName,
|
||||
}
|
||||
)
|
||||
end)
|
||||
|
||||
it("returns null when getting an unknown route", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
key = "a",
|
||||
routeName = routeName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expect(StateUtils.get(state, "b")).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("indexOf", function()
|
||||
it("gets route index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expect(StateUtils.indexOf(state, "a")).to.equal(1)
|
||||
expect(StateUtils.indexOf(state, "b")).to.equal(2)
|
||||
end)
|
||||
|
||||
-- deviation(will not fix): it is preferable to return `nil` as it's
|
||||
-- more common so there is less chance to surprise the consumer of
|
||||
-- that function
|
||||
itSKIP("returns -1 when getting an unknown route index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expect(StateUtils.indexOf(state, "b")).to.equal(-1)
|
||||
end)
|
||||
end)
|
||||
|
||||
it("has a route", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expect(StateUtils.has(state, "b")).to.equal(true)
|
||||
expect(StateUtils.has(state, "c")).to.equal(false)
|
||||
end)
|
||||
|
||||
describe("push", function()
|
||||
it("pushes a route", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {{ key = "a", routeName = routeName }},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
isTransitioning = false,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.push(state, { key = "b", routeName = routeName }),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("does not push duplicated route", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {{ key = "a", routeName = routeName }},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expect(function()
|
||||
StateUtils.push(state, { key = "a", routeName = routeName })
|
||||
end).to.throw("should not push route with duplicated key a")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("pop", function()
|
||||
it("pops route", function()
|
||||
local state = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 1,
|
||||
routes = {{ key = "a", routeName = routeName }},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(StateUtils.pop(state), newState)
|
||||
end)
|
||||
|
||||
it("does not pop route if not applicable with single route config", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {{ key = "a", routeName = routeName }},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.pop(state),
|
||||
state
|
||||
)
|
||||
end)
|
||||
|
||||
it("does not pop route if not applicable with multiple route config", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
};
|
||||
expectDeepEqual(StateUtils.pop(state), state)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("jumpToIndex", function()
|
||||
it("jumps to new index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.jumpToIndex(state, 1),
|
||||
state
|
||||
)
|
||||
expectDeepEqual(
|
||||
StateUtils.jumpToIndex(state, 2),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("throws if jumps to invalid index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expect(function()
|
||||
StateUtils.jumpToIndex(state, 3)
|
||||
end).to.throw("invalid index 3 to jump to")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("jumpTo", function()
|
||||
it("jumps to the current key", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expectDeepEqual(StateUtils.jumpTo(state, "a"), state)
|
||||
end)
|
||||
|
||||
it("jumps to new key", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.jumpTo(state, "b"),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("throws if jumps to invalid key", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expect(function()
|
||||
StateUtils.jumpTo(state, "c")
|
||||
end).to.throw("attempt to jump to unknown key \"c\"")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("back", function()
|
||||
it("move backwards", function()
|
||||
local state = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(StateUtils.back(state), newState)
|
||||
end)
|
||||
|
||||
it("does not move backwards when the active route is the first", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expect(StateUtils.back(state)).to.equal(state)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("forward", function()
|
||||
it("move forwards", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expectDeepEqual(StateUtils.forward(state), newState)
|
||||
end)
|
||||
|
||||
it("does not move forward when active route is already the top-most", function()
|
||||
local state = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(StateUtils.forward(state), state)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("replace", function()
|
||||
it("Replaces by key", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "c", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.replaceAt(state, "b", { key = "c", routeName = routeName }),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("Replaces by index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "c", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.replaceAtIndex(state, 2, { key = "c", routeName = routeName }),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("Returns the state with updated index if route is unchanged but index changes", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.replaceAtIndex(state, 2, state.routes[2]),
|
||||
{
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reset", function()
|
||||
it("Resets routes", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "x", routeName = routeName },
|
||||
{ key = "y", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.reset(state, {
|
||||
{ key = "x", routeName = routeName },
|
||||
{ key = "y", routeName = routeName },
|
||||
}),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("throws when attempting to set empty state", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expect(function()
|
||||
StateUtils.reset(state, {})
|
||||
end).to.throw("invalid routes to replace")
|
||||
end)
|
||||
|
||||
it("Resets routes with index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
local newState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "x", routeName = routeName },
|
||||
{ key = "y", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
|
||||
expectDeepEqual(
|
||||
StateUtils.reset(state, {
|
||||
{ key = "x", routeName = routeName },
|
||||
{ key = "y", routeName = routeName },
|
||||
}, 1),
|
||||
newState
|
||||
)
|
||||
end)
|
||||
|
||||
it("throws when attempting to set an out of range route index", function()
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = routeName },
|
||||
{ key = "b", routeName = routeName },
|
||||
},
|
||||
isTransitioning = false,
|
||||
}
|
||||
expect(function()
|
||||
StateUtils.reset(state, {
|
||||
{ key = "x", routeName = routeName },
|
||||
{ key = "y", routeName = routeName },
|
||||
}, 100)
|
||||
end).to.throw("invalid index 100 to reset")
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local createAppContainer = require(script.Parent.Parent.createAppContainer)
|
||||
local createRobloxSwitchNavigator = require(script.Parent.Parent.navigators.createRobloxSwitchNavigator)
|
||||
|
||||
it("should be a function", function()
|
||||
expect(type(createAppContainer)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a valid component when mounting a switch navigator", function()
|
||||
local TestNavigator = createRobloxSwitchNavigator({
|
||||
{ Foo = function() end },
|
||||
})
|
||||
|
||||
local TestApp = createAppContainer(TestNavigator)
|
||||
local element = Roact.createElement(TestApp)
|
||||
local instance = Roact.mount(element)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should throw when navigator has both navigation and container props", function()
|
||||
local TestAppComponent = Roact.Component:extend("TestAppComponent")
|
||||
TestAppComponent.router = {}
|
||||
function TestAppComponent:render() end
|
||||
|
||||
local element = Roact.createElement(createAppContainer(TestAppComponent), {
|
||||
navigation = {},
|
||||
somePropThatShouldNotBeHere = true,
|
||||
})
|
||||
|
||||
local status, err = pcall(function()
|
||||
Roact.mount(element)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "This navigator has both 'navigation' and container props.")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when not passed a table for AppComponent", function()
|
||||
local TestAppComponent = 5
|
||||
|
||||
local status, err = pcall(function()
|
||||
createAppContainer(TestAppComponent)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " ..
|
||||
"component with a 'router' field")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw when passed a stateful component without router field", function()
|
||||
local TestAppComponent = Roact.Component:extend("TestAppComponent")
|
||||
|
||||
local status, err = pcall(function()
|
||||
createAppContainer(TestAppComponent)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "AppComponent must be a navigator or a stateful Roact " ..
|
||||
"component with a 'router' field")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should accept actions from externalDispatchConnector", function()
|
||||
local TestNavigator = createRobloxSwitchNavigator({
|
||||
{ Foo = function() end },
|
||||
})
|
||||
|
||||
local registeredCallback = nil
|
||||
local externalDispatchConnector = function(rnCallback)
|
||||
registeredCallback = rnCallback
|
||||
return function()
|
||||
registeredCallback = nil
|
||||
end
|
||||
end
|
||||
|
||||
local element = Roact.createElement(createAppContainer(TestNavigator), {
|
||||
externalDispatchConnector = externalDispatchConnector,
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
expect(type(registeredCallback)).to.equal("function")
|
||||
|
||||
-- Make sure it processes action
|
||||
local result = registeredCallback(NavigationActions.navigate({
|
||||
routeName = "Foo",
|
||||
}))
|
||||
expect(result).to.equal(true)
|
||||
|
||||
local failResult = registeredCallback(NavigationActions.navigate({
|
||||
routeName = "Bar", -- should fail because not a valid route
|
||||
}))
|
||||
expect(failResult).to.equal(false)
|
||||
|
||||
Roact.unmount(instance)
|
||||
expect(registeredCallback).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should correctly pass screenProps to pages", function()
|
||||
local passedScreenProps = nil
|
||||
local extractedValue1 = nil
|
||||
local extractedMissingValue1 = nil
|
||||
local extractedMissingValue2 = nil
|
||||
|
||||
local testScreenProps = {
|
||||
MyKey1 = "MyValue1",
|
||||
}
|
||||
|
||||
local TestNavigator = createRobloxSwitchNavigator({
|
||||
{
|
||||
Foo = function(props)
|
||||
-- doing this in render is an abuse, but it's just a test
|
||||
passedScreenProps = props.navigation.getScreenProps()
|
||||
extractedValue1 = props.navigation.getScreenProps("MyKey1")
|
||||
extractedMissingValue1 = props.navigation.getScreenProps("MyMissingKey", 5)
|
||||
extractedMissingValue2 = props.navigation.getScreenProps("MyMissingKey")
|
||||
end,
|
||||
},
|
||||
})
|
||||
|
||||
local TestApp = createAppContainer(TestNavigator)
|
||||
local element = Roact.createElement(TestApp, {
|
||||
screenProps = testScreenProps,
|
||||
})
|
||||
local instance = Roact.mount(element)
|
||||
|
||||
expect(passedScreenProps).to.equal(testScreenProps)
|
||||
expect(extractedValue1).to.equal("MyValue1")
|
||||
expect(extractedMissingValue1).to.equal(5)
|
||||
expect(extractedMissingValue2).to.equal(nil)
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
return function()
|
||||
local getChildNavigation = require(script.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return nil if there is no route matching requested key", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "invalid_child", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return cached child if its state is a top-level route", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
},
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
testNavigation._childrenNavigation = {
|
||||
a = {
|
||||
state = testNavigation.state.routes[1]
|
||||
}
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation.a)
|
||||
end)
|
||||
|
||||
it("should update cache and return new data when child's state has changed", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
router = {
|
||||
getComponentForRouteName = function(routeName)
|
||||
return function() end
|
||||
end,
|
||||
getActionCreators = function() end,
|
||||
},
|
||||
}
|
||||
|
||||
local oldStateA = {
|
||||
isFirstRouteInParent = function()
|
||||
return true
|
||||
end,
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 2,
|
||||
},
|
||||
}
|
||||
|
||||
testNavigation._childrenNavigation = {
|
||||
a = oldStateA,
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation["a"])
|
||||
expect(childNav.state).to.equal(testNavigation.state.routes[1])
|
||||
expect(type(childNav.getParam)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should create a new entry if cached child does not exist yet", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "a", routeName = "a", params = { a = 1 } },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
index = 1,
|
||||
},
|
||||
router = {
|
||||
getComponentForRouteName = function(routeName)
|
||||
return function() end
|
||||
end,
|
||||
getActionCreators = function() end,
|
||||
},
|
||||
addListener = function()
|
||||
return {
|
||||
remove = function() end
|
||||
}
|
||||
end,
|
||||
isFocused = function()
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local childNav = getChildNavigation(testNavigation, "a", function()
|
||||
return testNavigation
|
||||
end)
|
||||
|
||||
expect(testNavigation._childrenNavigation["a"]).to.never.equal(nil)
|
||||
expect(childNav).to.equal(testNavigation._childrenNavigation["a"])
|
||||
expect(childNav.isFocused()).to.equal(true)
|
||||
|
||||
expect(childNav.getParam("a", 0)).to.equal(1)
|
||||
expect(childNav.getParam("b", 0)).to.equal(0)
|
||||
end)
|
||||
end
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
return function()
|
||||
local getChildRouter = require(script.Parent.Parent.getChildRouter)
|
||||
|
||||
it("should throw if router is not a table", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter(5, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "router must be a table")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw if routeName is not a string", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter({}, 5)
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "routeName must be a string")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should return child router if found", function()
|
||||
local childRouter = {}
|
||||
local result = getChildRouter({
|
||||
childRouters = {
|
||||
myRoute = childRouter,
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(childRouter)
|
||||
end)
|
||||
|
||||
it("should look up component router if no child router is found", function()
|
||||
local component = { router = {} }
|
||||
|
||||
local result = getChildRouter({
|
||||
getComponentForRouteName = function(routeName)
|
||||
if routeName == "myRoute" then
|
||||
return component
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(component.router)
|
||||
end)
|
||||
|
||||
it("should throw if no child routers are specified and getComponentForRouteName is not a function", function()
|
||||
local status, err = pcall(function()
|
||||
getChildRouter({
|
||||
getComponentForRouteName = 5
|
||||
}, "myRoute")
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "router.getComponentForRouteName must be a function if no child routers are specified")
|
||||
).to.never.equal(nil)
|
||||
end)
|
||||
end
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/f10543f9fcc0f347c9d23aeb57616fd0f21cd4e3/packages/core/src/__tests__/getChildrenNavigationCache.test.js
|
||||
return function()
|
||||
local getChildrenNavigationCache = require(script.Parent.Parent.getChildrenNavigationCache)
|
||||
|
||||
it("should return empty table if navigation arg not provided", function()
|
||||
expect(getChildrenNavigationCache()._childrenNavigation).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should populate navigation._childrenNavigation as a side-effect", function()
|
||||
local navigation = { state = {} }
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result).to.never.equal(nil)
|
||||
expect(navigation._childrenNavigation).to.equal(result)
|
||||
end)
|
||||
|
||||
it("should delete children cache keys that are no longer valid", function()
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "one" },
|
||||
{ key = "two" },
|
||||
{ key = "three" },
|
||||
}
|
||||
},
|
||||
_childrenNavigation = {
|
||||
one = {},
|
||||
two = {},
|
||||
three = {},
|
||||
four = {},
|
||||
}
|
||||
}
|
||||
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result.one).to.never.equal(nil)
|
||||
expect(result.two).to.never.equal(nil)
|
||||
expect(result.three).to.never.equal(nil)
|
||||
expect(result.four).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should not delete children cache keys if in transitioning state", function()
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "one" },
|
||||
{ key = "two" },
|
||||
{ key = "three" },
|
||||
},
|
||||
isTransitioning = true,
|
||||
},
|
||||
_childrenNavigation = {
|
||||
one = {},
|
||||
two = {},
|
||||
three = {},
|
||||
four = {},
|
||||
}
|
||||
}
|
||||
|
||||
local result = getChildrenNavigationCache(navigation)
|
||||
expect(result.one).to.never.equal(nil)
|
||||
expect(result.two).to.never.equal(nil)
|
||||
expect(result.three).to.never.equal(nil)
|
||||
expect(result.four).to.never.equal(nil)
|
||||
end)
|
||||
end
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/f10543f9fcc0f347c9d23aeb57616fd0f21cd4e3/packages/core/src/__tests__/getEventManager.test.js
|
||||
return function()
|
||||
local root = script.Parent.Parent
|
||||
local getEventManager = require(root.getEventManager)
|
||||
local Events = require(root.Events)
|
||||
local createSpy = require(root.utils.createSpy)
|
||||
|
||||
local TARGET = "target"
|
||||
|
||||
it("calls listeners to emitted event", function()
|
||||
local eventManager = getEventManager(TARGET)
|
||||
local callback = createSpy()
|
||||
eventManager.addListener(Events.DidFocus, callback.value)
|
||||
|
||||
eventManager.emit(Events.DidFocus)
|
||||
|
||||
expect(callback.callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("does not call listeners connected to a different event", function()
|
||||
local eventManager = getEventManager(TARGET)
|
||||
local callback = createSpy()
|
||||
eventManager.addListener(Events.DidFocus, callback.value)
|
||||
|
||||
eventManager.emit("didBlur")
|
||||
|
||||
expect(callback.callCount).to.equal(0)
|
||||
end)
|
||||
|
||||
it("does not call removed listeners", function()
|
||||
local eventManager = getEventManager(TARGET)
|
||||
local callback = createSpy()
|
||||
local remove = eventManager.addListener(Events.DidFocus, callback.value).remove
|
||||
|
||||
eventManager.emit(Events.DidFocus)
|
||||
expect(callback.callCount).to.equal(1)
|
||||
|
||||
remove()
|
||||
|
||||
eventManager.emit(Events.DidFocus)
|
||||
expect(callback.callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("calls the listeners with the given payload", function()
|
||||
local eventManager = getEventManager(TARGET)
|
||||
local callback = createSpy()
|
||||
eventManager.addListener(Events.DidFocus, callback.value)
|
||||
|
||||
local payload = { foo = 0 }
|
||||
eventManager.emit(Events.DidFocus, payload)
|
||||
|
||||
callback:assertCalledWithDeepEqual(payload)
|
||||
end)
|
||||
end
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
return function()
|
||||
local Events = require(script.Parent.Parent.Events)
|
||||
local getNavigation = require(script.Parent.Parent.getNavigation)
|
||||
|
||||
local function makeTestBundle(testState)
|
||||
testState = testState or {
|
||||
routes = {
|
||||
{ key = "a" }
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local testActions = {}
|
||||
local bundle = {
|
||||
testActions = testActions,
|
||||
testState = testState,
|
||||
testRouter = {
|
||||
getActionCreators = function()
|
||||
return testActions
|
||||
end
|
||||
},
|
||||
testDispatch = function() end,
|
||||
testActionSubscribers = {},
|
||||
testGetScreenProps = function() end,
|
||||
}
|
||||
|
||||
function bundle.testGetCurrentNavigation()
|
||||
return bundle.navigation
|
||||
end
|
||||
|
||||
bundle.navigation = getNavigation(
|
||||
bundle.testRouter,
|
||||
bundle.testState,
|
||||
bundle.testDispatch,
|
||||
bundle.testActionSubscribers,
|
||||
bundle.testGetScreenProps,
|
||||
bundle.testGetCurrentNavigation
|
||||
)
|
||||
|
||||
return bundle
|
||||
end
|
||||
|
||||
it("should build out correct public props", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
expect(bundle.navigation.actions).to.equal(bundle.testActions)
|
||||
expect(bundle.navigation.router).to.equal(bundle.testRouter)
|
||||
expect(bundle.navigation.state).to.equal(bundle.testState)
|
||||
expect(bundle.navigation.dispatch).to.equal(bundle.testDispatch)
|
||||
expect(bundle.navigation.getScreenProps).to.equal(bundle.testGetScreenProps)
|
||||
expect(#bundle.navigation._childrenNavigation).to.equal(0)
|
||||
end)
|
||||
|
||||
describe("isFocused tests", function()
|
||||
it("should return focused=true for child key matching index", function()
|
||||
local bundle = makeTestBundle()
|
||||
expect(bundle.navigation.isFocused("a")).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return focused=false for child key not matching index", function()
|
||||
local bundle = makeTestBundle({
|
||||
routes = {
|
||||
{ key = "a" },
|
||||
{ key = "b" },
|
||||
},
|
||||
index = 2,
|
||||
})
|
||||
expect(bundle.navigation.isFocused("a")).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should return focused=true if no child key provided (parent always focused)", function()
|
||||
local bundle = makeTestBundle()
|
||||
expect(bundle.navigation.isFocused()).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("addListener tests", function()
|
||||
it("should short-circuit subscriptions for non-Action events", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
local testHandler = function() end
|
||||
bundle.navigation.addListener(Events.WillFocus, testHandler)
|
||||
expect(bundle.testActionSubscribers[testHandler]).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should add Action event handlers to actionSubscribers set", function()
|
||||
local bundle = makeTestBundle()
|
||||
|
||||
local testHandler = function() end
|
||||
bundle.navigation.addListener(Events.Action, testHandler)
|
||||
expect(bundle.testActionSubscribers[testHandler]).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/__tests__/getNavigation.test.js
|
||||
|
||||
return function()
|
||||
local root = script.Parent.Parent
|
||||
local getNavigation = require(root.getNavigation)
|
||||
local NavigationActions = require(root.NavigationActions)
|
||||
|
||||
local createSpy = require(root.utils.createSpy)
|
||||
|
||||
it("getNavigation provides default action helpers", function()
|
||||
local router = {
|
||||
getActionCreators = function()
|
||||
return {}
|
||||
end,
|
||||
getStateForAction = function(action, lastState)
|
||||
return lastState or {}
|
||||
end,
|
||||
}
|
||||
|
||||
local dispatchSpy = createSpy()
|
||||
|
||||
local topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatchSpy.value,
|
||||
{},
|
||||
function()
|
||||
return {}
|
||||
end,
|
||||
function() end
|
||||
)
|
||||
|
||||
topNav.navigate("GreatRoute")
|
||||
|
||||
expect(dispatchSpy.callCount).to.equal(1)
|
||||
expect(dispatchSpy.values[1].type).to.equal(NavigationActions.Navigate)
|
||||
expect(dispatchSpy.values[1].routeName).to.equal("GreatRoute")
|
||||
end)
|
||||
|
||||
it("getNavigation provides router action helpers", function()
|
||||
local router = {
|
||||
getActionCreators = function()
|
||||
return {
|
||||
foo = function(bar)
|
||||
return { type = "FooBarAction", bar = bar }
|
||||
end,
|
||||
}
|
||||
end,
|
||||
getStateForAction = function(action, lastState)
|
||||
return lastState or {}
|
||||
end,
|
||||
}
|
||||
|
||||
local dispatchSpy = createSpy()
|
||||
|
||||
local topNav = nil
|
||||
topNav = getNavigation(
|
||||
router,
|
||||
{},
|
||||
dispatchSpy.value,
|
||||
{},
|
||||
function()
|
||||
return {}
|
||||
end,
|
||||
function()
|
||||
return topNav
|
||||
end
|
||||
)
|
||||
|
||||
topNav.foo("Great")
|
||||
|
||||
expect(dispatchSpy.callCount).to.equal(1)
|
||||
expect(dispatchSpy.values[1].type).to.equal("FooBarAction")
|
||||
expect(dispatchSpy.values[1].bar).to.equal("Great")
|
||||
end)
|
||||
|
||||
it("getNavigation get child navigation with router", function()
|
||||
local actionSubscribers = {}
|
||||
local navigation = nil
|
||||
|
||||
local routerA = {
|
||||
getActionCreators = function()
|
||||
return {}
|
||||
end,
|
||||
getStateForAction = function(action, lastState)
|
||||
return lastState or {}
|
||||
end,
|
||||
}
|
||||
local router = {
|
||||
childRouters = {
|
||||
RouteA = routerA,
|
||||
},
|
||||
getActionCreators = function()
|
||||
return {}
|
||||
end,
|
||||
getStateForAction = function(action, lastState)
|
||||
return lastState or {}
|
||||
end,
|
||||
}
|
||||
|
||||
local initState = {
|
||||
index = 0,
|
||||
routes = {
|
||||
{
|
||||
key = "a",
|
||||
routeName = "RouteA",
|
||||
routes = {{ key = "c", routeName = "RouteC" }},
|
||||
index = 0,
|
||||
},
|
||||
{ key = "b", routeName = "RouteB" },
|
||||
},
|
||||
}
|
||||
|
||||
local topNav = getNavigation(
|
||||
router,
|
||||
initState,
|
||||
function() end,
|
||||
actionSubscribers,
|
||||
function()
|
||||
return {}
|
||||
end,
|
||||
function()
|
||||
return navigation
|
||||
end
|
||||
)
|
||||
|
||||
local childNavA = topNav.getChildNavigation("a")
|
||||
|
||||
expect(childNavA.router).to.equal(routerA)
|
||||
end)
|
||||
end
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
return function()
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local RoactNavigation = require(root)
|
||||
local Roact = require(Packages.Roact)
|
||||
local StackPresentationStyle = require(root.views.RobloxStackView.StackPresentationStyle)
|
||||
|
||||
it("should return a function for createAppContainer", function()
|
||||
expect(type(RoactNavigation.createAppContainer)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getNavigation", function()
|
||||
expect(type(RoactNavigation.getNavigation)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return an appropriate table for Context", function()
|
||||
expect(type(RoactNavigation.Context)).to.equal("table")
|
||||
expect(type(RoactNavigation.Context.Provider)).to.equal("table")
|
||||
expect(type(RoactNavigation.Context.Consumer)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a Component for Provider", function()
|
||||
expect(type(RoactNavigation.Provider)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a Component for Consumer", function()
|
||||
expect(type(RoactNavigation.Consumer)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a function for withNavigation", function()
|
||||
expect(type(RoactNavigation.withNavigation)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for withNavigationFocus", function()
|
||||
expect(type(RoactNavigation.withNavigationFocus)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createRobloxSwitchNavigator", function()
|
||||
expect(type(RoactNavigation.createRobloxSwitchNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createRobloxStackNavigator", function()
|
||||
expect(type(RoactNavigation.createRobloxStackNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for createNavigator", function()
|
||||
expect(type(RoactNavigation.createNavigator)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for StackRouter", function()
|
||||
expect(type(RoactNavigation.StackRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for SwitchRouter", function()
|
||||
expect(type(RoactNavigation.SwitchRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for TabRouter", function()
|
||||
expect(type(RoactNavigation.TabRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a table for Actions", function()
|
||||
expect(type(RoactNavigation.Actions)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for StackActions", function()
|
||||
expect(type(RoactNavigation.StackActions)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for SwitchActions", function()
|
||||
expect(type(RoactNavigation.SwitchActions)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for BackBehavior", function()
|
||||
expect(type(RoactNavigation.BackBehavior)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a table for Events", function()
|
||||
expect(type(RoactNavigation.Events)).to.equal("table")
|
||||
end)
|
||||
|
||||
it("should return a valid component for NavigationEvents", function()
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.Provider, {
|
||||
value = {
|
||||
addListener = function()
|
||||
return { remove = function() end }
|
||||
end
|
||||
}
|
||||
}, {
|
||||
Events = Roact.createElement(RoactNavigation.NavigationEvents),
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return StackPresentationStyle", function()
|
||||
expect(RoactNavigation.StackPresentationStyle).to.equal(StackPresentationStyle)
|
||||
end)
|
||||
|
||||
it("should return a valid component for SceneView", function()
|
||||
expect(RoactNavigation.SceneView.render).never.to.equal(nil)
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.SceneView, {
|
||||
navigation = {},
|
||||
component = function() end,
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return a valid component for RobloxSwitchView", function()
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo", }
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
}
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(RoactNavigation.RobloxSwitchView, {
|
||||
descriptors = {
|
||||
Foo = {
|
||||
getComponent = function()
|
||||
return function() end
|
||||
end,
|
||||
navigation = testNavigation,
|
||||
}
|
||||
},
|
||||
navigation = testNavigation,
|
||||
}))
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
|
||||
it("should return a function for createConfigGetter", function()
|
||||
expect(type(RoactNavigation.createConfigGetter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getScreenForRouteName", function()
|
||||
expect(type(RoactNavigation.getScreenForRouteName)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for validateRouteConfigMap", function()
|
||||
expect(type(RoactNavigation.validateRouteConfigMap)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should return a function for getActiveChildNavigationOptions", function()
|
||||
expect(type(RoactNavigation.getActiveChildNavigationOptions)).to.equal("function")
|
||||
end)
|
||||
end
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
local Roact = require(script.Parent.Parent.Roact)
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local NavigationActions = require(script.Parent.NavigationActions)
|
||||
local Events = require(script.Parent.Events)
|
||||
local NavigationContext = require(script.Parent.views.NavigationContext)
|
||||
local getNavigation = require(script.Parent.getNavigation)
|
||||
local validate = require(script.Parent.utils.validate)
|
||||
|
||||
local function validateProps(props)
|
||||
if not props.navigation then
|
||||
return
|
||||
end
|
||||
|
||||
local errStr =
|
||||
"This navigator has both 'navigation' and container props. " ..
|
||||
"It is unclear if it should own its own state. Remove the " ..
|
||||
"container props or don't pass a 'navigation' prop."
|
||||
|
||||
for key in pairs(props) do
|
||||
validate(key == "screenProps" or key == "navigation", errStr)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Construct a container Roact component that will host the navigation hierarchy
|
||||
specified by your main AppComponent. AppComponent must be a navigator created by
|
||||
a Roact-Navigation helper function, or a stateful Roact component
|
||||
|
||||
If you are using a custom stateful Roact component, make sure to set the 'router'
|
||||
field so that it can be hooked into the navigation system. You must also pass your
|
||||
'navigation' prop to any child navigators.
|
||||
|
||||
Additional props:
|
||||
renderLoading - Roact component to render while the app is loading.
|
||||
externalDispatchConnector - Function that Roact Navigation can use to connect to
|
||||
externally triggered navigation Actions. This is useful
|
||||
for external UI or handling of the Android back button.
|
||||
|
||||
Ex:
|
||||
local connector = function(rnDispatch)
|
||||
-- You store rnDispatch and call it when you want to inject
|
||||
-- an event from outside RN.
|
||||
return function()
|
||||
-- You disconnect rnDispatch when RN calls this.
|
||||
end
|
||||
end
|
||||
|
||||
...
|
||||
Roact.createElement(MyRNAppContainer, {
|
||||
externalDispatchConnector = connector,
|
||||
})
|
||||
]]
|
||||
return function(AppComponent)
|
||||
validate(type(AppComponent) == "table" and AppComponent.router ~= nil,
|
||||
"AppComponent must be a navigator or a stateful Roact component with a 'router' field")
|
||||
|
||||
local containerName = string.format("NavigationContainer(%s)", tostring(AppComponent))
|
||||
local NavigationContainer = Roact.Component:extend(containerName)
|
||||
|
||||
function NavigationContainer.getDerivedStateFromProps(nextProps)
|
||||
validateProps(nextProps)
|
||||
return nil
|
||||
end
|
||||
|
||||
function NavigationContainer:init()
|
||||
validateProps(self.props)
|
||||
|
||||
self._actionEventSubscribers = {}
|
||||
self._initialAction = NavigationActions.init()
|
||||
|
||||
local initialNav = nil
|
||||
local containerIsStateful = self:_isStateful()
|
||||
if containerIsStateful and not self.props.persistenceKey then
|
||||
initialNav = AppComponent.router.getStateForAction(self._initialAction)
|
||||
end
|
||||
|
||||
self.state = {
|
||||
nav = initialNav,
|
||||
}
|
||||
end
|
||||
|
||||
function NavigationContainer:_updateExternalDispatchConnector()
|
||||
local externalDispatchConnector = self.props.externalDispatchConnector
|
||||
if self._subs then
|
||||
self._subs()
|
||||
self._subs = nil
|
||||
end
|
||||
|
||||
if externalDispatchConnector ~= nil then
|
||||
self._subs = externalDispatchConnector(function(...)
|
||||
if self._isMounted then
|
||||
return self:dispatch(...)
|
||||
end
|
||||
|
||||
-- External dispatch while we're not mounted gets dropped on floor.
|
||||
return false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_renderLoading()
|
||||
local renderLoading = self.props.renderLoading
|
||||
if renderLoading then
|
||||
return renderLoading()
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:render()
|
||||
local navigation = self.props.navigation
|
||||
|
||||
if self:_isStateful() then
|
||||
local navState = self.state.nav
|
||||
if not navState then
|
||||
return self:_renderLoading()
|
||||
end
|
||||
|
||||
if not self._navigation or self._navigation.state ~= navState then
|
||||
self._navigation = getNavigation(
|
||||
AppComponent.router,
|
||||
navState,
|
||||
function(...)
|
||||
return self:dispatch(...)
|
||||
end,
|
||||
self._actionEventSubscribers,
|
||||
function(...)
|
||||
return self:_getScreenProps(...)
|
||||
end,
|
||||
function()
|
||||
return self._navigation
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
navigation = self._navigation
|
||||
end
|
||||
|
||||
validate(navigation ~= nil, "failed to get navigation")
|
||||
|
||||
return Roact.createElement(NavigationContext.Provider, {
|
||||
value = navigation,
|
||||
}, {
|
||||
-- Provide navigation prop for top-level component so it doesn't have to connect.
|
||||
AppComponent = Roact.createElement(AppComponent, Cryo.Dictionary.join(self.props, {
|
||||
navigation = navigation,
|
||||
}))
|
||||
})
|
||||
end
|
||||
|
||||
function NavigationContainer:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
self:_updateExternalDispatchConnector()
|
||||
|
||||
if not self:_isStateful() then
|
||||
return
|
||||
end
|
||||
|
||||
local action = self._initialAction
|
||||
local startupState = self.state.nav
|
||||
|
||||
if not startupState then
|
||||
startupState = AppComponent.router.getStateForAction(action)
|
||||
end
|
||||
|
||||
local function dispatchActionEvents()
|
||||
-- _actionEventSubscribers is a table(handler, true), e.g. a Set container
|
||||
for subscriber in pairs(self._actionEventSubscribers) do
|
||||
subscriber({
|
||||
type = Events.Action,
|
||||
action = action,
|
||||
state = self.state.nav,
|
||||
-- there is no lastState for initial mounting
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if startupState ~= self.state.nav then
|
||||
self:setState({
|
||||
nav = startupState
|
||||
})
|
||||
end
|
||||
|
||||
-- This must be spawned until we get async setState callback handler in Roact
|
||||
spawn(dispatchActionEvents)
|
||||
end
|
||||
|
||||
function NavigationContainer:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
-- TODO: Disconnect from from URL listener once implemented
|
||||
|
||||
if self._subs then
|
||||
self._subs()
|
||||
self._subs = nil
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:didUpdate(oldProps)
|
||||
-- Clear cached _navState every time we update.
|
||||
if self._navState == self.state.nav then
|
||||
self._navState = nil
|
||||
end
|
||||
|
||||
if self.props.externalDispatchConnector ~= oldProps.externalDispatchConnector then
|
||||
self:_updateExternalDispatchConnector()
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_isStateful()
|
||||
return not self.props.navigation
|
||||
end
|
||||
|
||||
-- NOTE: Not implementing _validateProps; it is duplicate
|
||||
|
||||
-- NOTE: Not implementing _handleOpenURL; app should have a component
|
||||
-- that transforms URLs into paths for AppContainer instead.
|
||||
|
||||
function NavigationContainer:_onNavigationStateChange(prevNav, nextNav, action)
|
||||
local onNavigationStateChange = self.props.onNavigationStateChange
|
||||
|
||||
if type(onNavigationStateChange) == "function" then
|
||||
onNavigationStateChange(prevNav, nextNav, action)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationContainer:_getScreenProps(propKey, defaultValue)
|
||||
local screenProps = self.props.screenProps or {}
|
||||
if propKey ~= nil then
|
||||
return screenProps[propKey] or defaultValue
|
||||
end
|
||||
|
||||
-- Legacy: return original table if no args provided
|
||||
return screenProps
|
||||
end
|
||||
|
||||
function NavigationContainer:dispatch(action)
|
||||
if self.props.navigation then
|
||||
return self.props.navigation.dispatch(action)
|
||||
end
|
||||
|
||||
self._navState = self._navState or self.state.nav
|
||||
|
||||
local lastNavState = self._navState
|
||||
validate(lastNavState ~= nil, "navState should be set in constructor if stateful")
|
||||
|
||||
local reducedState = AppComponent.router.getStateForAction(action, lastNavState)
|
||||
local navState = reducedState
|
||||
if not navState then
|
||||
navState = lastNavState
|
||||
end
|
||||
|
||||
local function dispatchActionEvents()
|
||||
-- _actionEventSubscribers is a table(handler, true), e.g. a Set container
|
||||
for subscriber in pairs(self._actionEventSubscribers) do
|
||||
subscriber({
|
||||
type = Events.Action,
|
||||
action = action,
|
||||
state = navState,
|
||||
lastState = lastNavState,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if reducedState == nil then
|
||||
-- Router returns nil when action has been handled and there is no state change.
|
||||
-- dispatch() must return true whenever something has been handled.
|
||||
dispatchActionEvents()
|
||||
return true
|
||||
end
|
||||
|
||||
if navState ~= lastNavState then
|
||||
-- Update cache to ensure that subsequent calls do not discard this change
|
||||
self._navState = navState
|
||||
|
||||
-- TODO: We have to dispatch events before or after setState (which mounts/unmounts components)
|
||||
-- based upon the specific event type, to ensure that pages get them in the correct order...
|
||||
|
||||
self:setState({
|
||||
nav = navState
|
||||
})
|
||||
|
||||
-- Must be spawned until we get async setState callback handler in Roact.
|
||||
spawn(function()
|
||||
self:_onNavigationStateChange(lastNavState, navState, action)
|
||||
dispatchActionEvents()
|
||||
-- TODO: Add call to persist navigation state here, if we ever implement it.
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
spawn(dispatchActionEvents)
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return NavigationContainer
|
||||
end
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/getChildNavigation.js
|
||||
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local getEventManager = require(script.Parent.getEventManager)
|
||||
local getChildRouter = require(script.Parent.getChildRouter)
|
||||
local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators)
|
||||
local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache)
|
||||
|
||||
local function createParamGetter(route)
|
||||
return function(paramName, defaultValue)
|
||||
local params = route.params
|
||||
|
||||
if params and params[paramName] ~= nil then
|
||||
return params[paramName]
|
||||
else
|
||||
return defaultValue
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function getChildNavigation(navigation, childKey, getCurrentParentNavigation)
|
||||
local children = getChildrenNavigationCache(navigation)
|
||||
|
||||
local childRouteIndex = Cryo.List.findWhere(navigation.state.routes, function(route)
|
||||
return route.key == childKey
|
||||
end)
|
||||
|
||||
if not childRouteIndex then
|
||||
return nil
|
||||
end
|
||||
local childRoute = navigation.state.routes[childRouteIndex]
|
||||
|
||||
local requestedChild = children[childKey]
|
||||
|
||||
if requestedChild and requestedChild.state == childRoute then
|
||||
return requestedChild
|
||||
end
|
||||
|
||||
local childRouter = getChildRouter(navigation.router, childRoute.routeName)
|
||||
|
||||
-- If the route has children that match our routes schema then get a reference
|
||||
-- to the focused grandchild so we can pass the correct action creators to the
|
||||
-- child router so that any action that depends on the child route will behave
|
||||
-- as expected.
|
||||
local focusedGrandChildRoute = nil
|
||||
if childRoute.routes and type(childRoute.index) == "number" then
|
||||
focusedGrandChildRoute = childRoute.routes[childRoute.index]
|
||||
end
|
||||
|
||||
local childRouterActionCreators = childRouter and
|
||||
childRouter.getActionCreators(focusedGrandChildRoute, childRoute.key) or {}
|
||||
|
||||
local actionCreators = Cryo.Dictionary.join(
|
||||
navigation.actions or {},
|
||||
navigation.router.getActionCreators(childRoute, navigation.state.key) or {},
|
||||
childRouterActionCreators or {},
|
||||
getNavigationActionCreators(childRoute) or {}
|
||||
)
|
||||
|
||||
local actionHelpers = {}
|
||||
for actionName, actionCreator in pairs(actionCreators) do
|
||||
actionHelpers[actionName] = function(...)
|
||||
local action = actionCreator(...)
|
||||
return navigation.dispatch(action)
|
||||
end
|
||||
end
|
||||
|
||||
local isFirstRouteInParent = true;
|
||||
|
||||
local parentNavigation = getCurrentParentNavigation();
|
||||
|
||||
if parentNavigation then
|
||||
isFirstRouteInParent = Cryo.List.find(parentNavigation.state.routes, childRoute) == 1;
|
||||
end
|
||||
|
||||
if requestedChild and requestedChild.isFirstRouteInParent() == isFirstRouteInParent then
|
||||
-- Update cache value for requestedChild because child's state has changed
|
||||
children[childKey] = Cryo.Dictionary.join(requestedChild, actionHelpers, {
|
||||
state = childRoute,
|
||||
router = childRouter,
|
||||
actions = actionCreators,
|
||||
getParam = createParamGetter(childRoute),
|
||||
})
|
||||
|
||||
return children[childKey]
|
||||
else
|
||||
-- No cached value for requestedChild. Create a new entry.
|
||||
local childSubscriber = getEventManager(childKey)
|
||||
|
||||
children[childKey] = Cryo.Dictionary.join(actionHelpers, {
|
||||
state = childRoute,
|
||||
router = childRouter,
|
||||
actions = actionCreators,
|
||||
getParam = createParamGetter(childRoute),
|
||||
getChildNavigation = function(grandChildKey)
|
||||
return getChildNavigation(children[childKey], grandChildKey, function()
|
||||
local nav = getCurrentParentNavigation()
|
||||
return nav and nav.getChildNavigation(childKey) or nil
|
||||
end)
|
||||
end,
|
||||
isFocused = function()
|
||||
local currentNavigation = getCurrentParentNavigation()
|
||||
if not currentNavigation then
|
||||
return false
|
||||
end
|
||||
|
||||
if not currentNavigation.isFocused() then
|
||||
return false
|
||||
end
|
||||
|
||||
local state = currentNavigation.state
|
||||
local routes = state.routes
|
||||
local index = state.index
|
||||
|
||||
if routes[index].key == childKey then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end,
|
||||
isFirstRouteInParent = function()
|
||||
return isFirstRouteInParent
|
||||
end,
|
||||
dispatch = navigation.dispatch,
|
||||
getScreenProps = navigation.getScreenProps,
|
||||
-- deviation: `dangerouslyGetParent` is renamed as private because
|
||||
-- it is deprecated in latest react navigation
|
||||
_dangerouslyGetParent = getCurrentParentNavigation,
|
||||
addListener = childSubscriber.addListener,
|
||||
emit = childSubscriber.emit,
|
||||
})
|
||||
|
||||
return children[childKey]
|
||||
end
|
||||
end
|
||||
|
||||
return getChildNavigation
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/getChildRouter.ts
|
||||
local validate = require(script.Parent.utils.validate)
|
||||
|
||||
return function(router, routeName)
|
||||
validate(type(router) == "table", "router must be a table")
|
||||
validate(type(routeName) == "string", "routeName must be a string")
|
||||
|
||||
if router.childRouters and router.childRouters[routeName] then
|
||||
return router.childRouters[routeName]
|
||||
end
|
||||
|
||||
validate(type(router.getComponentForRouteName) == "function",
|
||||
"router.getComponentForRouteName must be a function if no child routers are specified")
|
||||
local component = router.getComponentForRouteName(routeName)
|
||||
|
||||
-- deviation: functional components cannot be indexed in Lua
|
||||
if type(component) == "table" then
|
||||
return component.router
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/views/withNavigation.js
|
||||
|
||||
return function(navigation)
|
||||
if not navigation then
|
||||
return {}
|
||||
end
|
||||
|
||||
if not navigation._childrenNavigation then
|
||||
navigation._childrenNavigation = {}
|
||||
end
|
||||
|
||||
local childrenNavigationCache = navigation._childrenNavigation
|
||||
|
||||
local childKeys = {}
|
||||
for _, route in ipairs(navigation.state.routes or {}) do
|
||||
childKeys[route.key] = true
|
||||
end
|
||||
|
||||
if not navigation.state.isTransitioning then
|
||||
for cacheKey, _ in pairs(childrenNavigationCache) do
|
||||
if not childKeys[cacheKey] then
|
||||
childrenNavigationCache[cacheKey] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return navigation._childrenNavigation
|
||||
end
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/getEventManager.js
|
||||
|
||||
local root = script.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
|
||||
local function getEventManager(target)
|
||||
local listeners = {}
|
||||
|
||||
local function removeListener(type, callback)
|
||||
local callbacks = listeners[type] and listeners[type][target]
|
||||
|
||||
if not callbacks then
|
||||
return
|
||||
end
|
||||
|
||||
local index = table.find(callbacks, callback)
|
||||
table.remove(callbacks, index)
|
||||
end
|
||||
|
||||
local function addListener(type, callback)
|
||||
listeners[type] = listeners[type] or {}
|
||||
listeners[type][target] = listeners[type][target] or {}
|
||||
|
||||
table.insert(listeners[type][target], callback)
|
||||
|
||||
return {
|
||||
remove = function()
|
||||
removeListener(type, callback)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
addListener = addListener,
|
||||
emit = function(type, data)
|
||||
local items = listeners[type] or {}
|
||||
local callbacks = items[target] and Cryo.List.join({}, items[target])
|
||||
|
||||
if callbacks then
|
||||
for _, callback in ipairs(callbacks) do
|
||||
callback(data)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return getEventManager
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/getNavigation.js
|
||||
|
||||
local Cryo = require(script.Parent.Parent.Cryo)
|
||||
local Events = require(script.Parent.Events)
|
||||
local getNavigationActionCreators = require(script.Parent.routers.getNavigationActionCreators)
|
||||
local getChildNavigation = require(script.Parent.getChildNavigation)
|
||||
local getChildrenNavigationCache = require(script.Parent.getChildrenNavigationCache)
|
||||
|
||||
return function(router, state, dispatch, actionSubscribers, getScreenProps, getCurrentNavigation)
|
||||
local actions = router.getActionCreators(state, nil)
|
||||
|
||||
local navigation = {
|
||||
actions = actions,
|
||||
router = router,
|
||||
state = state,
|
||||
dispatch = dispatch,
|
||||
getScreenProps = getScreenProps,
|
||||
-- deviation: `dangerouslyGetParent` is renamed as private because
|
||||
-- it is deprecated in latest react navigation
|
||||
_dangerouslyGetParent = function()
|
||||
return nil
|
||||
end,
|
||||
isFirstRouteInParent = function()
|
||||
return true
|
||||
end,
|
||||
_childrenNavigation = getChildrenNavigationCache(getCurrentNavigation()),
|
||||
}
|
||||
|
||||
function navigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(navigation, childKey, getCurrentNavigation)
|
||||
end
|
||||
|
||||
function navigation.isFocused(childKey)
|
||||
local currentState = getCurrentNavigation().state
|
||||
local routes = currentState.routes
|
||||
local index = currentState.index
|
||||
|
||||
return childKey == nil or routes[index].key == childKey
|
||||
end
|
||||
|
||||
function navigation.addListener(event, handler)
|
||||
if event ~= Events.Action then
|
||||
return { remove = function() end }
|
||||
else
|
||||
actionSubscribers[handler] = true
|
||||
return {
|
||||
remove = function()
|
||||
actionSubscribers[handler] = nil
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local actionCreators = Cryo.Dictionary.join(getNavigationActionCreators(navigation.state), actions)
|
||||
|
||||
for actionName, _ in pairs(actionCreators) do
|
||||
navigation[actionName] = function(...)
|
||||
navigation.dispatch(actionCreators[actionName](...))
|
||||
end
|
||||
end
|
||||
|
||||
return navigation
|
||||
end
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
-- Generator information:
|
||||
-- Human name: Roact Navigation
|
||||
-- Variable name: RoactNavigation
|
||||
-- Repo name: roact-navigation
|
||||
|
||||
local NavigationContext = require(script.views.NavigationContext)
|
||||
|
||||
return {
|
||||
-- Navigation container construction
|
||||
createAppContainer = require(script.createAppContainer),
|
||||
getNavigation = require(script.getNavigation),
|
||||
|
||||
-- Context Access
|
||||
Context = NavigationContext,
|
||||
Provider = NavigationContext.Provider,
|
||||
Consumer = NavigationContext.Consumer,
|
||||
|
||||
withNavigation = require(script.views.withNavigation),
|
||||
withNavigationFocus = require(script.views.withNavigationFocus),
|
||||
|
||||
-- Navigators
|
||||
createRobloxStackNavigator = require(script.navigators.createRobloxStackNavigator),
|
||||
createRobloxSwitchNavigator = require(script.navigators.createRobloxSwitchNavigator),
|
||||
createNavigator = require(script.navigators.createNavigator),
|
||||
|
||||
-- Routers
|
||||
StackRouter = require(script.routers.StackRouter),
|
||||
SwitchRouter = require(script.routers.SwitchRouter),
|
||||
TabRouter = require(script.routers.TabRouter),
|
||||
|
||||
-- Navigation Actions
|
||||
Actions = require(script.NavigationActions),
|
||||
StackActions = require(script.routers.StackActions),
|
||||
SwitchActions = require(script.routers.SwitchActions),
|
||||
BackBehavior = require(script.BackBehavior),
|
||||
|
||||
-- Navigation Events
|
||||
Events = require(script.Events),
|
||||
NavigationEvents = require(script.views.NavigationEvents),
|
||||
|
||||
-- Additional Types
|
||||
StackPresentationStyle = require(script.views.RobloxStackView.StackPresentationStyle),
|
||||
|
||||
-- Screen Views
|
||||
SceneView = require(script.views.SceneView),
|
||||
RobloxSwitchView = require(script.views.RobloxSwitchView),
|
||||
RobloxStackView = require(script.views.RobloxStackView.StackView),
|
||||
|
||||
-- Utilities
|
||||
createConfigGetter = require(script.routers.createConfigGetter),
|
||||
getScreenForRouteName = require(script.routers.getScreenForRouteName),
|
||||
validateRouteConfigMap = require(script.routers.validateRouteConfigMap),
|
||||
getActiveChildNavigationOptions = require(script.utils.getActiveChildNavigationOptions),
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createNavigator = require(script.Parent.Parent.createNavigator)
|
||||
|
||||
local testRouter = {
|
||||
getScreenOptions = function() return nil end,
|
||||
}
|
||||
|
||||
it("should return a Roact component that exposes navigator fields", function()
|
||||
local testComponentMounted = nil
|
||||
local TestViewComponent = Roact.Component:extend("TestViewComponent")
|
||||
function TestViewComponent:render() end
|
||||
function TestViewComponent:didMount() testComponentMounted = true end
|
||||
function TestViewComponent:willUnmount() testComponentMounted = false end
|
||||
|
||||
local testNavOptions = {}
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = testNavOptions,
|
||||
})
|
||||
|
||||
expect(navigator.render).to.be.a("function")
|
||||
expect(navigator.router).to.equal(testRouter)
|
||||
expect(navigator.navigationOptions).to.equal(testNavOptions)
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
getChildNavigation = function() return nil end, -- stub
|
||||
addListener = function() end,
|
||||
}
|
||||
|
||||
-- Try to mount it
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
expect(testComponentMounted).to.equal(true)
|
||||
Roact.unmount(instance)
|
||||
expect(testComponentMounted).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should throw when trying to mount without navigation prop", function()
|
||||
local TestViewComponent = function() end
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = {}
|
||||
})
|
||||
|
||||
expect(function()
|
||||
Roact.mount(Roact.createElement(navigator))
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when trying to mount without routes", function()
|
||||
local TestViewComponent = function() end
|
||||
|
||||
local navigator = createNavigator(TestViewComponent, testRouter, {
|
||||
navigationOptions = {}
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
index = 1
|
||||
},
|
||||
getChildNavigation = function() return nil end, -- stub
|
||||
}
|
||||
|
||||
expect(function()
|
||||
Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createRobloxStackNavigator = require(script.Parent.Parent.createRobloxStackNavigator)
|
||||
local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return a mountable Roact component", function()
|
||||
local navigator = createRobloxStackNavigator({
|
||||
{ Foo = function() end },
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
router = navigator.router
|
||||
}
|
||||
|
||||
function testNavigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(testNavigation, childKey, function()
|
||||
return testNavigation
|
||||
end)
|
||||
end
|
||||
|
||||
function testNavigation.addListener(symbol, callback)
|
||||
return {
|
||||
remove = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createRobloxSwitchNavigator = require(script.Parent.Parent.createRobloxSwitchNavigator)
|
||||
local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return a mountable Roact component", function()
|
||||
local navigator = createRobloxSwitchNavigator({
|
||||
{ Foo = function() end },
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
router = navigator.router
|
||||
}
|
||||
|
||||
function testNavigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(testNavigation, childKey, function()
|
||||
return testNavigation
|
||||
end)
|
||||
end
|
||||
|
||||
function testNavigation.addListener(symbol, callback)
|
||||
return {
|
||||
remove = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local createSwitchNavigator = require(script.Parent.Parent.createSwitchNavigator)
|
||||
local getChildNavigation = require(script.Parent.Parent.Parent.getChildNavigation)
|
||||
|
||||
it("should return a mountable Roact component", function()
|
||||
local navigator = createSwitchNavigator({
|
||||
{ Foo = function() end },
|
||||
})
|
||||
|
||||
local testNavigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ routeName = "Foo", key = "Foo" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
router = navigator.router
|
||||
}
|
||||
|
||||
function testNavigation.getChildNavigation(childKey)
|
||||
return getChildNavigation(testNavigation, childKey, function()
|
||||
return testNavigation
|
||||
end)
|
||||
end
|
||||
|
||||
function testNavigation.addListener(symbol, callback)
|
||||
return {
|
||||
remove = function() end
|
||||
}
|
||||
end
|
||||
|
||||
local instance = Roact.mount(Roact.createElement(navigator, {
|
||||
navigation = testNavigation
|
||||
}))
|
||||
|
||||
Roact.unmount(instance)
|
||||
end)
|
||||
end
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/navigators/createNavigator.js
|
||||
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Roact = require(Packages.Roact)
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local validate = require(root.utils.validate)
|
||||
local NavigationFocusEvents = require(root.views.NavigationFocusEvents)
|
||||
|
||||
return function(navigatorViewComponent, router, navigationConfig)
|
||||
local Navigator = Roact.Component:extend("Navigator")
|
||||
|
||||
-- These statics need to be accessible to routers
|
||||
Navigator.router = router
|
||||
Navigator.navigationOptions = navigationConfig.navigationOptions
|
||||
-- deviation: no theme support
|
||||
|
||||
function Navigator:init()
|
||||
local screenProps = self.props.screenProps
|
||||
|
||||
self.state = {
|
||||
descriptors = {},
|
||||
screenProps = screenProps,
|
||||
-- deviation: no theme support
|
||||
}
|
||||
end
|
||||
|
||||
function Navigator.getDerivedStateFromProps(nextProps, prevState)
|
||||
local prevDescriptors = prevState.descriptors
|
||||
local navigation = nextProps.navigation
|
||||
local screenProps = nextProps.screenProps
|
||||
|
||||
validate(navigation ~= nil, "The navigation prop is missing for this navigator. " ..
|
||||
"In react-navigation v3 and v4 you must set up your app container directly. " ..
|
||||
"More info: https://reactnavigation.org/docs/en/app-containers.html")
|
||||
|
||||
local routes = navigation.state.routes
|
||||
|
||||
validate(type(routes) == "table", 'No "routes" found in navigation state. ' ..
|
||||
"Did you try to pass the navigation prop of a React component to a Navigator child? " ..
|
||||
"See https://reactnavigation.org/docs/en/custom-navigators.html#navigator-navigation-prop")
|
||||
|
||||
local descriptors = Cryo.List.foldLeft(routes, function(descriptors, route)
|
||||
if
|
||||
prevDescriptors and
|
||||
prevDescriptors[route.key] and
|
||||
route == prevDescriptors[route.key].state and
|
||||
screenProps == prevState.screenProps
|
||||
-- deviation: no theme support
|
||||
then
|
||||
descriptors[route.key] = prevDescriptors[route.key]
|
||||
else
|
||||
local getComponent = function()
|
||||
return router.getComponentForRouteName(route.routeName)
|
||||
end
|
||||
|
||||
local childNavigation = navigation.getChildNavigation(route.key)
|
||||
local options = router.getScreenOptions(childNavigation, screenProps)
|
||||
|
||||
descriptors[route.key] = {
|
||||
key = route.key,
|
||||
getComponent = getComponent,
|
||||
options = options,
|
||||
state = route,
|
||||
navigation = childNavigation,
|
||||
}
|
||||
end
|
||||
|
||||
return descriptors
|
||||
end, {})
|
||||
|
||||
return {
|
||||
descriptors = descriptors,
|
||||
screenProps = screenProps,
|
||||
-- deviation: no theme support
|
||||
}
|
||||
end
|
||||
|
||||
-- deviation: no `componentDidUpdate` because no theme support
|
||||
|
||||
function Navigator:render()
|
||||
local navigation = self.props.navigation
|
||||
local screenProps = self.state.screenProps
|
||||
local descriptors = self.state.descriptors
|
||||
|
||||
return Roact.createFragment({
|
||||
Events = Roact.createElement(NavigationFocusEvents, {
|
||||
navigation = navigation,
|
||||
onEvent = function(target, type, data)
|
||||
if descriptors[target] then
|
||||
descriptors[target].navigation.emit(type, data);
|
||||
end
|
||||
end,
|
||||
}),
|
||||
View = Roact.createElement(navigatorViewComponent, Cryo.Dictionary.join(self.props, {
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
navigationConfig = navigationConfig,
|
||||
descriptors = descriptors,
|
||||
}))
|
||||
})
|
||||
end
|
||||
|
||||
return Navigator
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local root = script.Parent.Parent
|
||||
local createNavigator = require(script.Parent.createNavigator)
|
||||
local StackRouter = require(root.routers.StackRouter)
|
||||
local StackView = require(root.views.RobloxStackView.StackView)
|
||||
|
||||
return function(routeArray, stackConfig)
|
||||
local router = StackRouter(routeArray, stackConfig)
|
||||
|
||||
return createNavigator(StackView, router, stackConfig or {})
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
local root = script.Parent.Parent
|
||||
local createNavigator = require(script.Parent.createNavigator)
|
||||
local SwitchRouter = require(root.routers.SwitchRouter)
|
||||
local RobloxSwitchView = require(root.views.RobloxSwitchView)
|
||||
|
||||
--[[
|
||||
Creates a navigator component that provides simple screen "switcher" behavior.
|
||||
Each page is mutually exclusive and no transition animation is used.
|
||||
|
||||
Additional config options:
|
||||
order : List<string>
|
||||
Specifies the index order for page components, e.g. for use with tab bars.
|
||||
keepVisitedScreensMounted : Boolean (false)
|
||||
Set to true if you want to keep previously visited screens mounted for better performance.
|
||||
resetOnBlur : Boolean (true)
|
||||
Set to false if you want to preserve existing state for child navigators.
|
||||
backBehavior : BackBehavior (None)
|
||||
Set to BackBehavior.InitialRoute to allow a goBack() operation to return to the
|
||||
initial route name. By default, the SwitchNavigator will not do anything on a back action.
|
||||
]]
|
||||
return function(routeArray, switchConfig)
|
||||
local router = SwitchRouter(routeArray, switchConfig)
|
||||
|
||||
return createNavigator(RobloxSwitchView, router, switchConfig or {})
|
||||
end
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/1f5000e86bef5e4c8ee6fbeb25e3ca3eb8873ad0/packages/core/src/navigators/createSwitchNavigator.js
|
||||
local root = script.Parent.Parent
|
||||
local createNavigator = require(script.Parent.createNavigator)
|
||||
local SwitchRouter = require(root.routers.SwitchRouter)
|
||||
local SwitchView = require(root.views.SwitchView.SwitchView)
|
||||
|
||||
return function(routeArray, switchConfig)
|
||||
if switchConfig == nil then
|
||||
switchConfig = {}
|
||||
end
|
||||
local router = SwitchRouter(routeArray, switchConfig)
|
||||
|
||||
return createNavigator(SwitchView, router, switchConfig)
|
||||
end
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/StackActions.js
|
||||
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local NavigationSymbol = require(root.NavigationSymbol)
|
||||
|
||||
local POP_TOKEN = NavigationSymbol("POP")
|
||||
local POP_TO_TOP_TOKEN = NavigationSymbol("POP_TO_TOP")
|
||||
local PUSH_TOKEN = NavigationSymbol("PUSH")
|
||||
local RESET_TOKEN = NavigationSymbol("RESET")
|
||||
local REPLACE_TOKEN = NavigationSymbol("REPLACE")
|
||||
local COMPLETE_TRANSITION_TOKEN = NavigationSymbol("COMPLETE_TRANSITION")
|
||||
|
||||
--[[
|
||||
StackActions provides shared constants and methods to construct
|
||||
actions that are dispatched to routers to cause a change in the route.
|
||||
These actions are specific to Stack navigation. See NavigationActions
|
||||
if you need to use more general APIs.
|
||||
]]
|
||||
local StackActions = {
|
||||
Pop = POP_TOKEN,
|
||||
PopToTop = POP_TO_TOP_TOKEN,
|
||||
Push = PUSH_TOKEN,
|
||||
Reset = RESET_TOKEN,
|
||||
Replace = REPLACE_TOKEN,
|
||||
CompleteTransition = COMPLETE_TRANSITION_TOKEN,
|
||||
}
|
||||
|
||||
-- deviation: we using this metatable to error when StackActions is indexed
|
||||
-- with an unexpected key.
|
||||
setmetatable(StackActions, {
|
||||
__index = function(self, key)
|
||||
error(("%q is not a valid member of StackActions"):format(tostring(key)), 2)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Pop the top-most item off the route stack, if any.
|
||||
function StackActions.pop(payload)
|
||||
return Cryo.Dictionary.join({ type = POP_TOKEN }, payload or {})
|
||||
end
|
||||
|
||||
-- Pop all the items except the last one off the route stack.
|
||||
function StackActions.popToTop(payload)
|
||||
return Cryo.Dictionary.join({ type = POP_TO_TOP_TOKEN }, payload or {})
|
||||
end
|
||||
|
||||
-- Push a new item onto the route stack.
|
||||
function StackActions.push(payload)
|
||||
return Cryo.Dictionary.join({ type = PUSH_TOKEN }, payload or {})
|
||||
end
|
||||
|
||||
-- Reset the route stack and replace it with a new stack,
|
||||
-- specified by a list of actions to be applied.
|
||||
function StackActions.reset(payload)
|
||||
return Cryo.Dictionary.join({ type = RESET_TOKEN }, payload or {})
|
||||
end
|
||||
|
||||
-- Replace the route for the given key with a new route.
|
||||
function StackActions.replace(payload)
|
||||
return Cryo.Dictionary.join(
|
||||
{ type = REPLACE_TOKEN, preserveFocus = true },
|
||||
payload or {}
|
||||
)
|
||||
end
|
||||
|
||||
function StackActions.completeTransition(payload)
|
||||
return Cryo.Dictionary.join(
|
||||
{ type = COMPLETE_TRANSITION_TOKEN, preserveFocus = true },
|
||||
payload or {}
|
||||
)
|
||||
end
|
||||
|
||||
return StackActions
|
||||
+666
@@ -0,0 +1,666 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/TabRouter.js
|
||||
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local NavigationActions = require(root.NavigationActions)
|
||||
local StackActions = require(script.Parent.StackActions)
|
||||
local KeyGenerator = require(root.utils.KeyGenerator)
|
||||
local StateUtils = require(root.StateUtils)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local createConfigGetter = require(script.Parent.createConfigGetter)
|
||||
local validateRouteConfigArray = require(script.Parent.validateRouteConfigArray)
|
||||
local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap)
|
||||
local validate = require(root.utils.validate)
|
||||
local NavigationSymbol = require(root.NavigationSymbol)
|
||||
|
||||
local STACK_ROUTER_ROOT_KEY = "StackRouterRoot"
|
||||
-- This symbol is used to differentiate if a router has a child router
|
||||
-- or if is a regular Roact component. React-navigation does it by using
|
||||
-- undefined vs. null (that's why we need)
|
||||
local CHILD_IS_SCREEN = NavigationSymbol("CHILD_IS_SCREEN")
|
||||
|
||||
local defaultActionCreators = function() return {} end
|
||||
|
||||
local function behavesLikePushAction(action)
|
||||
return action.type == NavigationActions.Navigate or
|
||||
action.type == StackActions.Push
|
||||
end
|
||||
|
||||
local function isResetToRootStack(action)
|
||||
return action.type == StackActions.Reset and action.key == nil
|
||||
end
|
||||
|
||||
local function mapToRouteName(element)
|
||||
local routeName = next(element)
|
||||
return routeName
|
||||
end
|
||||
|
||||
local function foldToRoutes(routes, element, index)
|
||||
local routeName, value = next(element)
|
||||
routes[routeName] = value
|
||||
return routes
|
||||
end
|
||||
|
||||
return function(routeArray, config)
|
||||
validateRouteConfigArray(routeArray)
|
||||
config = config or {}
|
||||
local routeConfigs = validateRouteConfigMap(
|
||||
Cryo.List.foldLeft(routeArray, foldToRoutes, {})
|
||||
)
|
||||
|
||||
local routeNames = config.order or Cryo.List.map(routeArray, mapToRouteName)
|
||||
|
||||
-- Loop through routes and find child routers
|
||||
local childRouters = {}
|
||||
for _, routeName in ipairs(routeNames) do
|
||||
-- We're not using `getScreenForRouteName` here to preserve the lazy loading
|
||||
-- behaviour of routes. This means that routes with child routers must be
|
||||
-- defined using a component directly or with an object with a screen prop.
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
local screen = (type(routeConfig) == "table" and routeConfig.screen)
|
||||
and routeConfig.screen or routeConfig
|
||||
if type(screen) == "table" and screen.router then
|
||||
-- If it has a router it's a navigator.
|
||||
childRouters[routeName] = screen.router
|
||||
else
|
||||
-- TODO: This is a hack to make this code behave like React-Navigation's usage of
|
||||
-- null and undefined values for childRouters. We should come up with a better approach.
|
||||
childRouters[routeName] = CHILD_IS_SCREEN
|
||||
end
|
||||
end
|
||||
|
||||
local initialRouteParams = config.initialRouteParams
|
||||
local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators
|
||||
|
||||
local initialRouteName = config.initialRouteName or routeNames[1]
|
||||
|
||||
local initialChildRouter = childRouters[initialRouteName]
|
||||
|
||||
local initialRouteIndex = Cryo.List.find(routeNames, initialRouteName)
|
||||
-- dump an error if initialRouteName is not in routes.
|
||||
if initialRouteIndex == nil then
|
||||
local availableRouteStr = ""
|
||||
for _, name in ipairs(routeNames) do
|
||||
availableRouteStr = availableRouteStr .. name .. ","
|
||||
end
|
||||
|
||||
error(string.format("Invalid initialRouteName '%s'. Must be one of [%s]", initialRouteName, availableRouteStr), 2)
|
||||
end
|
||||
|
||||
local function getInitialState(action)
|
||||
local route = {}
|
||||
local childRouter = childRouters[action.routeName]
|
||||
|
||||
-- This is a push-like action, and childRouter will be a router or null
|
||||
-- if we are responsible for this routeName
|
||||
if behavesLikePushAction(action) and childRouter ~= nil then
|
||||
local childState = {}
|
||||
|
||||
-- The router is 'CHILD_IS_SCREEN' for normal leaf routes
|
||||
if childRouter ~= CHILD_IS_SCREEN then
|
||||
local childAction = action.action
|
||||
or NavigationActions.init({ params = action.params })
|
||||
childState = childRouter.getStateForAction(childAction)
|
||||
end
|
||||
|
||||
return {
|
||||
key = STACK_ROUTER_ROOT_KEY,
|
||||
isTransitioning = false,
|
||||
index = 1,
|
||||
routes = {
|
||||
Cryo.Dictionary.join({ params = action.params }, childState, {
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
routeName = action.routeName,
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if initialChildRouter ~= nil and initialChildRouter ~= CHILD_IS_SCREEN then
|
||||
route = initialChildRouter.getStateForAction(NavigationActions.navigate({
|
||||
routeName = initialRouteName,
|
||||
params = initialRouteParams,
|
||||
}))
|
||||
end
|
||||
|
||||
local initialRouteConfig = routeConfigs[initialRouteName]
|
||||
-- we need to check if the routeConfig is a table because functions can't be
|
||||
-- indexed in Lua.
|
||||
local initialRouteConfigParams = type(initialRouteConfig) == "table" and initialRouteConfig.params
|
||||
|
||||
local params = (initialRouteConfigParams or route.params or action.params or initialRouteParams)
|
||||
and Cryo.Dictionary.join(
|
||||
initialRouteConfigParams or {}, -- params set in routes table!
|
||||
route.params or {},
|
||||
action.params or {},
|
||||
initialRouteParams or {} -- params provided at top level
|
||||
)
|
||||
|
||||
local initialRouteKey = config.initialRouteKey
|
||||
route = Cryo.Dictionary.join(route, {
|
||||
params = params,
|
||||
routeName = initialRouteName,
|
||||
key = action.key or initialRouteKey or KeyGenerator.generateKey()
|
||||
})
|
||||
|
||||
return {
|
||||
key = STACK_ROUTER_ROOT_KEY,
|
||||
isTransitioning = false,
|
||||
index = 1,
|
||||
routes = { route }
|
||||
}
|
||||
end
|
||||
|
||||
local function getParamsForRouteAndAction(routeName, action)
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
-- we need to check if the routeConfig is a table because functions can't be
|
||||
-- indexed in Lua.
|
||||
if type(routeConfig) == "table" and routeConfig.params then
|
||||
return Cryo.Dictionary.join(routeConfig.params, action.params)
|
||||
else
|
||||
return action.params
|
||||
end
|
||||
end
|
||||
|
||||
-- Strip out the CHILD_IS_SCREEN hacked elements before exposing publicly.
|
||||
local strippedChildRouters = {}
|
||||
for routerName, router in pairs(childRouters) do
|
||||
if router ~= CHILD_IS_SCREEN then
|
||||
strippedChildRouters[routerName] = router
|
||||
end
|
||||
end
|
||||
|
||||
local StackRouter = {
|
||||
childRouters = strippedChildRouters,
|
||||
getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions),
|
||||
_CHILD_IS_SCREEN = CHILD_IS_SCREEN, -- expose symbol for testing purposes
|
||||
}
|
||||
|
||||
function StackRouter.getComponentForState(state)
|
||||
local activeChildRoute = state.routes[state.index] or {}
|
||||
local routeName = activeChildRoute.routeName
|
||||
validate(routeName, "There is no route defined for index '%d'. " ..
|
||||
"Make sure that you passed in a navigation state with a " ..
|
||||
"valid stack index.", state.index)
|
||||
|
||||
local childRouter = childRouters[routeName]
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
return childRouters[routeName].getComponentForState(activeChildRoute)
|
||||
end
|
||||
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
function StackRouter.getComponentForRouteName(routeName)
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
function StackRouter.getActionCreators(route, navStateKey)
|
||||
return Cryo.Dictionary.join(getCustomActionCreators(route, navStateKey), {
|
||||
pop = function(n, params)
|
||||
return StackActions.pop(Cryo.Dictionary.join({
|
||||
n = n,
|
||||
}, params or {}))
|
||||
end,
|
||||
popToTop = function(params)
|
||||
return StackActions.popToTop(params)
|
||||
end,
|
||||
push = function(routeName, params, action)
|
||||
return StackActions.push({
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
action = action,
|
||||
})
|
||||
end,
|
||||
replace = function(replaceWith, params, action, newKey)
|
||||
if type(replaceWith) == "string" then
|
||||
return StackActions.replace({
|
||||
routeName = replaceWith,
|
||||
params = params,
|
||||
action = action,
|
||||
key = route.key,
|
||||
newKey = newKey,
|
||||
})
|
||||
end
|
||||
|
||||
validate(type(replaceWith) == "table", "replaceWith must be a table or string")
|
||||
validate(params == nil, "params cannot be provided to .replace() when specifying a table")
|
||||
validate(action == nil, "Child action cannot be provided to .replace() when specifying a table")
|
||||
validate(newKey == nil, "newKey cannot be provided to .replace() when specifying a table")
|
||||
|
||||
return StackActions.replace(replaceWith)
|
||||
end,
|
||||
reset = function(actions, index)
|
||||
local resetIndex = index
|
||||
if index == nil then
|
||||
resetIndex = #actions
|
||||
end
|
||||
|
||||
return StackActions.reset({
|
||||
actions = actions,
|
||||
index = resetIndex,
|
||||
key = navStateKey,
|
||||
})
|
||||
end,
|
||||
dismiss = function()
|
||||
return NavigationActions.back({
|
||||
key = navStateKey,
|
||||
})
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function StackRouter.getStateForAction(action, state)
|
||||
-- Set up initial state if needed
|
||||
if not state then
|
||||
return getInitialState(action)
|
||||
end
|
||||
|
||||
local activeChildRoute = state.routes[state.index]
|
||||
|
||||
if not isResetToRootStack(action) and action.type ~= NavigationActions.Navigate then
|
||||
-- Let the active child router handle the action
|
||||
local activeChildRouter = childRouters[activeChildRoute.routeName]
|
||||
if activeChildRouter ~= nil and activeChildRouter ~= CHILD_IS_SCREEN then
|
||||
local route = activeChildRouter.getStateForAction(action, activeChildRoute)
|
||||
if route ~= nil and route ~= activeChildRoute then
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
activeChildRoute.key,
|
||||
route,
|
||||
-- the following tells replaceAt to NOT change the index to this
|
||||
-- route for the setParam action, because people don't expect
|
||||
-- param-setting actions to switch the active route
|
||||
action.type == NavigationActions.SetParams
|
||||
)
|
||||
end
|
||||
end
|
||||
elseif action.type == NavigationActions.Navigate then
|
||||
-- Traverse routes from the top of the stack to the bottom, so the
|
||||
-- active route has the first opportunity, then the one before it, etc.
|
||||
for i = #state.routes, 1, -1 do
|
||||
local childRoute = state.routes[i]
|
||||
local childRouter = childRouters[childRoute.routeName]
|
||||
local childAction = action
|
||||
if action.routeName == childRoute.routeName and action.action then
|
||||
childAction = action.action
|
||||
end
|
||||
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local nextRouteState = childRouter.getStateForAction(childAction, childRoute)
|
||||
if nextRouteState == nil or nextRouteState ~= childRoute then
|
||||
local newState = StateUtils.replaceAndPrune(
|
||||
state,
|
||||
nextRouteState and nextRouteState.key or childRoute.key,
|
||||
nextRouteState and nextRouteState or childRoute
|
||||
)
|
||||
|
||||
local newTransitioning = state.isTransitioning
|
||||
if state.index ~= newState.index then
|
||||
newTransitioning = action.immediate ~= true
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(newState, {
|
||||
isTransitioning = newTransitioning,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle push and navigation actions. This must happen after focused child router
|
||||
-- has had its chance to handle the action.
|
||||
-- If a router equals `nil` it means that it is not a childRouter or a screen.
|
||||
if behavesLikePushAction(action) and childRouters[action.routeName] ~= nil then
|
||||
local childRouter = childRouters[action.routeName]
|
||||
validate(action.type ~= StackActions.Push or action.key == nil,
|
||||
"StackRouter does not support key on the push action")
|
||||
|
||||
-- Before pushing a new route we first try to find one in the existing route stack
|
||||
-- More information on this: https://github.com/react-navigation/rfcs/blob/master/text/0004-less-pushy-navigate.md
|
||||
local findRoute = function(route)
|
||||
return route.routeName == action.routeName
|
||||
end
|
||||
if action.key then
|
||||
findRoute = function(route)
|
||||
return route.key == action.key
|
||||
end
|
||||
end
|
||||
local lastRouteIndex = Cryo.List.findWhere(state.routes, findRoute)
|
||||
|
||||
-- An instance of this route exists already and we're dealing with a navigate action.
|
||||
if action.type ~= StackActions.Push and lastRouteIndex ~= nil then
|
||||
-- If index is unchanged and params are not being set, leave state identity intact
|
||||
if state.index == lastRouteIndex and not action.params then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Remove the now unused routes at the tail of the routes array
|
||||
local routes = Cryo.List.getRange(state.routes, 1, lastRouteIndex)
|
||||
|
||||
-- Apply params if provided, otherwise leave route identity intact
|
||||
if action.params then
|
||||
local route = state.routes[lastRouteIndex]
|
||||
routes[lastRouteIndex] = Cryo.Dictionary.join(route, {
|
||||
params = Cryo.Dictionary.join(route.params or {}, action.params)
|
||||
})
|
||||
end
|
||||
|
||||
-- Return state with new index, changing isTransitioning only if index has changed
|
||||
local newIsTransitioning = state.isTransitioning
|
||||
if state.index ~= lastRouteIndex then
|
||||
newIsTransitioning = action.immediate ~= true
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = newIsTransitioning,
|
||||
index = lastRouteIndex,
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
|
||||
local route
|
||||
if childRouter ~= CHILD_IS_SCREEN then
|
||||
-- Delegate to the child router with the given action, or init it
|
||||
local childAction = action.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(action.routeName, action)
|
||||
})
|
||||
|
||||
route = Cryo.Dictionary.join({
|
||||
-- does it make sense to wipe out the params here? or even to
|
||||
-- add params at all? need more info about what this solves
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
}, childRouter.getStateForAction(childAction), {
|
||||
routeName = action.routeName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
})
|
||||
else
|
||||
-- Create the route from scratch
|
||||
route = {
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
routeName = action.routeName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
}
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(StateUtils.push(state, route), {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
})
|
||||
elseif action.type == StackActions.Push and childRouters[action.routeName] == nil then
|
||||
-- Return the state identity to bubble the action up
|
||||
return state
|
||||
end
|
||||
|
||||
-- Handle navigation to other child routers that are not yet pushed
|
||||
if behavesLikePushAction(action) then
|
||||
local childRouterNames = Cryo.Dictionary.keys(childRouters)
|
||||
for _, childRouterName in ipairs(childRouterNames) do
|
||||
local childRouter = childRouters[childRouterName]
|
||||
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
-- For each child router, start with a blank state
|
||||
local initChildRoute = childRouter.getStateForAction(NavigationActions.init())
|
||||
|
||||
-- Then check to see if the router handles our navigate action
|
||||
local navigatedChildRoute = childRouter.getStateForAction(action, initChildRoute)
|
||||
|
||||
local routeToPush = nil
|
||||
if navigatedChildRoute == nil then
|
||||
-- Push the route if the router has 'handled' the action and returned null
|
||||
routeToPush = initChildRoute
|
||||
elseif navigatedChildRoute ~= initChildRoute then
|
||||
-- Push the route if the state has changed in response to this navigation
|
||||
routeToPush = navigatedChildRoute
|
||||
end
|
||||
|
||||
if routeToPush then
|
||||
local route = Cryo.Dictionary.join(routeToPush, {
|
||||
routeName = childRouterName,
|
||||
key = action.key or KeyGenerator.generateKey(),
|
||||
})
|
||||
|
||||
return Cryo.Dictionary.join(StateUtils.push(state, route), {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle pop-to-top behavior. Make sure this happens after children have had a
|
||||
-- chance to handle the action, so that the inner stack pops to top first.
|
||||
if action.type == StackActions.PopToTop then
|
||||
-- Refuse to handle pop to top if a key is given that does not correspond
|
||||
-- to this router
|
||||
if action.key and state.key ~= action.key then
|
||||
return state
|
||||
end
|
||||
|
||||
-- If we're already at the top, then we return the state with a new
|
||||
-- identity so that the action is handled by this router.
|
||||
if state.index > 1 then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = action.immediate ~= true,
|
||||
index = 1,
|
||||
routes = { state.routes[1] }
|
||||
})
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
if action.type == StackActions.Replace then
|
||||
local routeIndex = nil -- luacheck: ignore routeIndex
|
||||
|
||||
-- If the key param is undefined, set the index to the last route in the stack
|
||||
if action.key == nil and #state.routes > 0 then
|
||||
routeIndex = #state.routes
|
||||
else
|
||||
routeIndex = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.key == action.key
|
||||
end)
|
||||
end
|
||||
|
||||
if routeIndex then
|
||||
local childRouter = childRouters[action.routeName]
|
||||
local childState = {}
|
||||
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local childAction = action.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(action.routeName, action)
|
||||
})
|
||||
|
||||
childState = childRouter.getStateForAction(childAction)
|
||||
end
|
||||
|
||||
local routes = Cryo.List.replaceIndex(
|
||||
state.routes,
|
||||
routeIndex,
|
||||
Cryo.Dictionary.join({
|
||||
params = getParamsForRouteAndAction(action.routeName, action),
|
||||
}, childState, {
|
||||
routeName = action.routeName,
|
||||
key = action.newKey or KeyGenerator.generateKey(),
|
||||
})
|
||||
)
|
||||
|
||||
return Cryo.Dictionary.join(state, { routes = routes })
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == StackActions.CompleteTransition and
|
||||
(action.key == nil or action.key == state.key) and
|
||||
action.toChildKey == state.routes[state.index].key and
|
||||
state.isTransitioning
|
||||
then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
isTransitioning = false,
|
||||
})
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.SetParams then
|
||||
local key = action.key
|
||||
local lastRouteIndex = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.key == key
|
||||
end)
|
||||
|
||||
if lastRouteIndex then
|
||||
local lastRoute = state.routes[lastRouteIndex]
|
||||
local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params or {})
|
||||
local routes = Cryo.List.replaceIndex(
|
||||
state.routes,
|
||||
lastRouteIndex,
|
||||
Cryo.Dictionary.join(lastRoute, { params = params })
|
||||
)
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == StackActions.Reset then
|
||||
-- Only handle reset actions that are unspecified or match this state key
|
||||
if action.key ~= nil and action.key ~= state.key then
|
||||
return state
|
||||
end
|
||||
|
||||
local newStackActions = action.actions or {}
|
||||
local newRoutes = Cryo.List.map(newStackActions, function(newStackAction)
|
||||
local router = childRouters[newStackAction.routeName]
|
||||
|
||||
local childState = {}
|
||||
-- we need to check if initialChildRouter is not CHILD_IS_SCREEN because
|
||||
-- of the divergence with react-navigation.
|
||||
if router ~= nil and router ~= CHILD_IS_SCREEN then
|
||||
local childAction = newStackAction.action or NavigationActions.init({
|
||||
params = getParamsForRouteAndAction(
|
||||
newStackAction.routeName,
|
||||
newStackAction
|
||||
),
|
||||
})
|
||||
|
||||
childState = router.getStateForAction(childAction)
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join({
|
||||
params = getParamsForRouteAndAction(newStackAction.routeName, newStackAction)
|
||||
}, childState, {
|
||||
routeName = newStackAction.routeName,
|
||||
key = newStackAction.key or KeyGenerator.generateKey(),
|
||||
})
|
||||
end)
|
||||
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = newRoutes,
|
||||
index = action.index,
|
||||
})
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.Back or action.type == StackActions.Pop then
|
||||
local key = action.key
|
||||
local n = action.n
|
||||
local immediate = action.immediate
|
||||
local prune = action.prune
|
||||
|
||||
if action.type == StackActions.Pop and prune == false and key then
|
||||
local index = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.key == key
|
||||
end)
|
||||
|
||||
if index ~= nil then
|
||||
local count = math.max(index - (n or 1), 1)
|
||||
local routes = Cryo.List.join(
|
||||
Cryo.List.getRange(state.routes, 1, count),
|
||||
Cryo.List.getRange(state.routes, index + 1, math.huge)
|
||||
)
|
||||
|
||||
if #routes > 0 then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
index = #routes,
|
||||
isTransitioning = immediate ~= true,
|
||||
})
|
||||
end
|
||||
end
|
||||
else
|
||||
local backRouteIndex = state.index
|
||||
|
||||
if action.type == StackActions.Pop and n ~= nil then
|
||||
-- determine the index to go back *from*. In this case, n=1 means to go
|
||||
-- back from state.index, as if it were a normal "BACK" action
|
||||
backRouteIndex = math.max(2, state.index - n + 1)
|
||||
elseif key then
|
||||
backRouteIndex = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.key == key
|
||||
end)
|
||||
end
|
||||
|
||||
if backRouteIndex and backRouteIndex > 1 then
|
||||
return Cryo.Dictionary.join(state, {
|
||||
routes = Cryo.List.getRange(state.routes, 1, backRouteIndex - 1),
|
||||
index = backRouteIndex - 1,
|
||||
isTransitioning = immediate ~= true,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- By this point in the router's state handling logic, we have handled the behavior
|
||||
-- of the active route, and handled any stack actions. If we haven't returned by
|
||||
-- now, we should allow non-active child routers to handle this action, and switch
|
||||
-- to that index if the child state (route) does change..
|
||||
|
||||
local keyIndex = action.key and StateUtils.indexOf(state, action.key) or nil
|
||||
|
||||
-- Traverse routes from the top of the stack to the bottom, so the
|
||||
-- active route has the first opportunity, then the one before it, etc.
|
||||
for i = #state.routes, 1, -1 do
|
||||
local childRoute = state.routes[i]
|
||||
-- skip over the active child because we let it attempt to handle the action
|
||||
-- earlier.
|
||||
-- If a key is provided and in routes state then let's use that
|
||||
-- knowledge to skip extra getStateForAction calls on other child
|
||||
-- routers
|
||||
if (childRoute.key ~= activeChildRoute.key) and
|
||||
(keyIndex == 1 or childRoute.key == action.key)
|
||||
then
|
||||
local childRouter = childRouters[childRoute.routeName]
|
||||
if childRouter ~= nil and childRouter ~= CHILD_IS_SCREEN then
|
||||
local route = childRouter.getStateForAction(action, childRoute)
|
||||
|
||||
if route == nil then
|
||||
return state
|
||||
elseif route ~= childRoute then
|
||||
return StateUtils.replaceAt(
|
||||
state,
|
||||
childRoute.key,
|
||||
route,
|
||||
-- People don't expect these actions to switch the active route
|
||||
action.preserveFocus
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
-- TODO: Implement StackRouter.getPathAndParamsForState after we add path expression support
|
||||
-- TODO: Implement StackRouter.getActionForPathAndParams after we add path expression support
|
||||
|
||||
return StackRouter
|
||||
end
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local NavigationSymbol = require(script.Parent.Parent.NavigationSymbol)
|
||||
|
||||
local JUMP_TO_TOKEN = NavigationSymbol("JUMP_TO")
|
||||
|
||||
local SwitchActions = {
|
||||
JumpTo = JUMP_TO_TOKEN,
|
||||
}
|
||||
|
||||
-- deviation: we using this metatable to error when SwitchActions is indexed
|
||||
-- with an unexpected key.
|
||||
setmetatable(SwitchActions, {
|
||||
__index = function(self, key)
|
||||
error(("%q is not a valid member of SwitchActions"):format(tostring(key)), 2)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Pop the top-most item off the route stack, if any.
|
||||
function SwitchActions.jumpTo(payload)
|
||||
return Cryo.Dictionary.join(
|
||||
{ type = JUMP_TO_TOKEN, preserveFocus = true },
|
||||
payload or {}
|
||||
)
|
||||
end
|
||||
|
||||
return SwitchActions
|
||||
+445
@@ -0,0 +1,445 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/SwitchRouter.js
|
||||
local Root = script.Parent.Parent
|
||||
local Packages = Root.Parent
|
||||
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
local BackBehavior = require(Root.BackBehavior)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local createConfigGetter = require(script.Parent.createConfigGetter)
|
||||
local validateRouteConfigMap = require(script.Parent.validateRouteConfigMap)
|
||||
local validateRouteConfigArray = require(script.Parent.validateRouteConfigArray)
|
||||
local validate = require(Root.utils.validate)
|
||||
local StackActions = require(Root.routers.StackActions)
|
||||
local SwitchActions = require(script.Parent.SwitchActions)
|
||||
|
||||
local function defaultActionCreators()
|
||||
return {}
|
||||
end
|
||||
|
||||
local function mapToRouteName(element)
|
||||
local routeName = next(element)
|
||||
return routeName
|
||||
end
|
||||
|
||||
local function foldToRoutes(routes, element)
|
||||
local routeName, value = next(element)
|
||||
routes[routeName] = value
|
||||
return routes
|
||||
end
|
||||
|
||||
return function(routeArray, config)
|
||||
validateRouteConfigArray(routeArray)
|
||||
config = config or {}
|
||||
local routeConfigs = validateRouteConfigMap(
|
||||
Cryo.List.foldLeft(routeArray, foldToRoutes, {})
|
||||
)
|
||||
|
||||
-- Order is how we map the active index into the list of possible routes.
|
||||
-- Lua does not guarantee any sense of order of table keys in dictionaries, so
|
||||
-- we have to deviate from react-navigation SwitchRouter API. Instead of using a
|
||||
-- map for the routeConfigs, we wrap each key-value pair into its own table so that
|
||||
-- routeConfigs becomes an array (i.e. { { Foo = Screen }, { Bar = Screen } }).
|
||||
local order = config.order or Cryo.List.map(routeArray, mapToRouteName)
|
||||
|
||||
local getCustomActionCreators = config.getCustomActionCreators or defaultActionCreators
|
||||
|
||||
local initialRouteParams = config.initialRouteParams
|
||||
local initialRouteName = config.initialRouteName or order[1]
|
||||
local backBehavior = config.backBehavior or BackBehavior.None
|
||||
|
||||
local resetOnBlur = true
|
||||
if config.resetOnBlur ~= nil then
|
||||
resetOnBlur = config.resetOnBlur
|
||||
end
|
||||
|
||||
local initialRouteIndex = Cryo.List.find(order, initialRouteName)
|
||||
if initialRouteIndex == nil then
|
||||
local availableRouteNames = table.concat(
|
||||
Cryo.List.map(order, function(routeName)
|
||||
return ('"%s"'):format(routeName)
|
||||
end),
|
||||
", "
|
||||
)
|
||||
|
||||
error(("Invalid initialRouteName '%s'. Should be one of %s"):format(
|
||||
initialRouteName,
|
||||
availableRouteNames
|
||||
), 2)
|
||||
end
|
||||
|
||||
local childRouters = {}
|
||||
for _, routeName in ipairs(order) do
|
||||
childRouters[routeName] = false
|
||||
local screen = getScreenForRouteName(routeConfigs, routeName)
|
||||
if type(screen) == "table" and screen.router then
|
||||
childRouters[routeName] = screen.router
|
||||
end
|
||||
end
|
||||
|
||||
local function getParamsForRoute(routeName, params)
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
if type(routeConfig) == "table" and routeConfig.params then
|
||||
return Cryo.Dictionary.join(routeConfig.params, params or {})
|
||||
else
|
||||
return params
|
||||
end
|
||||
end
|
||||
|
||||
local function resetChildRoute(routeName)
|
||||
local initialParams = routeName == initialRouteName and initialRouteParams or nil
|
||||
-- note(brentvatne): merging initialRouteParams *on top* of default params
|
||||
-- on the route seems incorrect but it's consistent with existing behavior
|
||||
-- in stackrouter
|
||||
local params = getParamsForRoute(routeName, initialParams)
|
||||
local childRouter = childRouters[routeName]
|
||||
if childRouter then
|
||||
local childAction = NavigationActions.init()
|
||||
return Cryo.Dictionary.join(childRouter.getStateForAction(childAction), {
|
||||
key = routeName,
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
})
|
||||
end
|
||||
|
||||
return {
|
||||
key = routeName,
|
||||
routeName = routeName,
|
||||
params = params,
|
||||
}
|
||||
end
|
||||
|
||||
local function getNextState(action, prevState, possibleNextState)
|
||||
local nextState = possibleNextState
|
||||
|
||||
if prevState and possibleNextState and
|
||||
prevState.index ~= possibleNextState.index and
|
||||
resetOnBlur
|
||||
then
|
||||
local prevRouteName = prevState.routes[prevState.index].routeName
|
||||
local nextRoutes = Cryo.List.join(possibleNextState.routes)
|
||||
nextRoutes[prevState.index] = resetChildRoute(prevRouteName)
|
||||
nextState = Cryo.Dictionary.join(
|
||||
possibleNextState,
|
||||
{ routes = nextRoutes }
|
||||
)
|
||||
end
|
||||
|
||||
if backBehavior ~= BackBehavior.History or
|
||||
(prevState and nextState and nextState.index == prevState.index )
|
||||
then
|
||||
return nextState
|
||||
end
|
||||
|
||||
local nextRouteKeyHistory = prevState and prevState.routeKeyHistory or {}
|
||||
|
||||
if action.type == NavigationActions.Navigate then
|
||||
local keyToAdd = nextState.routes[nextState.index].key
|
||||
nextRouteKeyHistory = Cryo.List.filter(nextRouteKeyHistory, function(k)
|
||||
return k ~= keyToAdd
|
||||
end)
|
||||
table.insert(nextRouteKeyHistory, keyToAdd)
|
||||
|
||||
elseif action.type == NavigationActions.Back then
|
||||
nextRouteKeyHistory = Cryo.List.removeIndex(nextRouteKeyHistory, #nextRouteKeyHistory)
|
||||
end
|
||||
|
||||
return Cryo.Dictionary.join(
|
||||
nextState,
|
||||
{ routeKeyHistory = nextRouteKeyHistory }
|
||||
)
|
||||
end
|
||||
|
||||
local function getInitialState()
|
||||
local routes = Cryo.List.map(order, resetChildRoute)
|
||||
local initialState = {
|
||||
routes = routes,
|
||||
index = initialRouteIndex,
|
||||
}
|
||||
|
||||
if backBehavior == BackBehavior.History then
|
||||
local initialKey = routes[initialRouteIndex].key
|
||||
initialState.routeKeyHistory = { initialKey }
|
||||
end
|
||||
|
||||
return initialState
|
||||
end
|
||||
|
||||
local SwitchRouter = {
|
||||
childRouters = childRouters,
|
||||
getActionCreators = function(route, stateKey)
|
||||
return getCustomActionCreators(route, stateKey)
|
||||
end,
|
||||
getScreenOptions = createConfigGetter(routeConfigs, config.defaultNavigationOptions)
|
||||
}
|
||||
|
||||
function SwitchRouter.getStateForAction(action, inputState)
|
||||
local prevState = inputState and Cryo.Dictionary.join(inputState) or nil
|
||||
local state = inputState or getInitialState()
|
||||
local activeChildIndex = state.index
|
||||
|
||||
if action.type == NavigationActions.Init then
|
||||
-- TODO: React-Navigation has a comment that wonders why we merge params into child routes.
|
||||
-- Need to understand if we really want to do this.
|
||||
local params = action.params
|
||||
if params then
|
||||
state.routes = Cryo.List.map(state.routes, function(route)
|
||||
local initialParams = route.routeName == initialRouteName and initialRouteParams or {}
|
||||
return Cryo.Dictionary.join(route, {
|
||||
params = Cryo.Dictionary.join(route.params or {}, params, initialParams)
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == SwitchActions.JumpTo and
|
||||
(action.key == nil or action.key == state.key)
|
||||
then
|
||||
local params = action.params
|
||||
local index = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.routeName == action.routeName
|
||||
end)
|
||||
|
||||
if index == nil then
|
||||
error(
|
||||
("There is no route named '%s' in the navigator with the key '%s'.\n"):format(
|
||||
action.routeName,
|
||||
action.key
|
||||
) ..
|
||||
"Must be one of: " .. table.concat(
|
||||
Cryo.List.map(state.routes, function(route)
|
||||
return route.routeName
|
||||
end),
|
||||
","
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
local routes = state.routes
|
||||
if params then
|
||||
return state.routes.map(function(route, i)
|
||||
if i == index then
|
||||
return Cryo.Dictionary.join(route, {
|
||||
params = Cryo.Dictionary.join(route.params, params),
|
||||
})
|
||||
end
|
||||
|
||||
return route
|
||||
end)
|
||||
end
|
||||
|
||||
return getNextState(action, prevState, Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
index = index,
|
||||
}))
|
||||
end
|
||||
|
||||
-- Let the current child handle it
|
||||
local activeChildLastState = state.routes[state.index]
|
||||
local activeChildRouter = childRouters[order[state.index]]
|
||||
|
||||
if activeChildRouter then
|
||||
local activeChildState = activeChildRouter.getStateForAction(
|
||||
action,
|
||||
activeChildLastState
|
||||
)
|
||||
if not activeChildState and inputState then
|
||||
return nil
|
||||
end
|
||||
if activeChildState and activeChildState ~= activeChildLastState then
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[state.index] = activeChildState
|
||||
return getNextState(action, prevState, Cryo.Dictionary.join(
|
||||
state,
|
||||
{ routes = routes }
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle tab changing. Do this after letting the current tab try to
|
||||
-- handle the action, to allow inner children to change first
|
||||
local isBackEligible = action.key == nil or action.key == activeChildLastState.key
|
||||
|
||||
if action.type == NavigationActions.Back then
|
||||
if isBackEligible and backBehavior == BackBehavior.InitialRoute then
|
||||
activeChildIndex = initialRouteIndex
|
||||
elseif isBackEligible and backBehavior == BackBehavior.Order then
|
||||
activeChildIndex = math.max(1, activeChildIndex - 1)
|
||||
-- The history contains current route, so we can only go back
|
||||
-- if there is more than one item in the history
|
||||
elseif isBackEligible and
|
||||
backBehavior == BackBehavior.History and
|
||||
#state.routeKeyHistory > 1
|
||||
then
|
||||
local routeKey = state.routeKeyHistory[#state.routeKeyHistory - 1]
|
||||
activeChildIndex = Cryo.List.find(order, routeKey)
|
||||
end
|
||||
end
|
||||
|
||||
local didNavigate = false
|
||||
|
||||
if action.type == NavigationActions.Navigate then
|
||||
didNavigate = nil ~= Cryo.List.findWhere(order, function(childId, i)
|
||||
if childId == action.routeName then
|
||||
activeChildIndex = i
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
if didNavigate then
|
||||
local childState = state.routes[activeChildIndex]
|
||||
local childRouter = childRouters[action.routeName]
|
||||
local newChildState = childState
|
||||
|
||||
if action.action and childRouter then
|
||||
local childStateUpdate = childRouter.getStateForAction(
|
||||
action.action,
|
||||
childState
|
||||
)
|
||||
if childStateUpdate then
|
||||
newChildState = childStateUpdate
|
||||
end
|
||||
end
|
||||
|
||||
if action.params then
|
||||
newChildState = Cryo.Dictionary.join(newChildState, {
|
||||
params = Cryo.Dictionary.join(
|
||||
newChildState.params or {},
|
||||
action.params
|
||||
),
|
||||
})
|
||||
end
|
||||
|
||||
if newChildState ~= childState then
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
routes[activeChildIndex] = newChildState
|
||||
local nextState = Cryo.Dictionary.join(state, {
|
||||
routes = routes,
|
||||
index = activeChildIndex,
|
||||
})
|
||||
return getNextState(action, prevState, nextState)
|
||||
|
||||
elseif newChildState == childState and
|
||||
state.index == activeChildIndex and
|
||||
prevState
|
||||
then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if action.type == NavigationActions.SetParams then
|
||||
local key = action.key
|
||||
local lastRouteIndex = Cryo.List.findWhere(state.routes, function(route)
|
||||
return route.key == key
|
||||
end)
|
||||
|
||||
if lastRouteIndex ~= nil then
|
||||
local lastRoute = state.routes[lastRouteIndex]
|
||||
local params = Cryo.Dictionary.join(lastRoute.params or {}, action.params)
|
||||
local routes = Cryo.List.join(state.routes)
|
||||
|
||||
routes[lastRouteIndex] = Cryo.Dictionary.join(
|
||||
lastRoute,
|
||||
{ params = params }
|
||||
)
|
||||
|
||||
return getNextState(action, prevState, Cryo.Dictionary.join(
|
||||
state,
|
||||
{ routes = routes })
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if activeChildIndex ~= state.index then
|
||||
return getNextState(action, prevState, Cryo.Dictionary.join(
|
||||
state,
|
||||
{ index = activeChildIndex })
|
||||
)
|
||||
elseif didNavigate and not inputState then
|
||||
return state
|
||||
elseif didNavigate then
|
||||
return Cryo.List.join(state)
|
||||
end
|
||||
|
||||
|
||||
local isActionBackOrPop = action.type == NavigationActions.Back or
|
||||
action.type == StackActions.Pop or action.type == StackActions.PopToTop
|
||||
local sendActionToInactiveChildren = not isActionBackOrPop or
|
||||
(action.type == NavigationActions.Back and action.key ~= nil)
|
||||
|
||||
-- Let other children handle it and switch to the first child that returns a new state
|
||||
-- Do not do this for StackActions.Pop or NavigationActions.Back actions without a key:
|
||||
-- it would be unintuitive for these actions to switch to another tab just because that tab had a Stack that could accept a back action
|
||||
if sendActionToInactiveChildren then
|
||||
local index = state.index
|
||||
local routes = state.routes
|
||||
Cryo.List.findWhere(order, function(childId, i)
|
||||
local childRouter = childRouters[childId]
|
||||
if i == index then
|
||||
return false
|
||||
end
|
||||
|
||||
local childState = routes[i]
|
||||
if childRouter then
|
||||
childState = childRouter.getStateForAction(action, childState)
|
||||
end
|
||||
|
||||
if not childState then
|
||||
index = i
|
||||
return true
|
||||
end
|
||||
|
||||
if childState ~= routes[i] then
|
||||
routes = Cryo.List.join(routes)
|
||||
routes[i] = childState
|
||||
index = i
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end)
|
||||
|
||||
-- Nested routers can be updated after switching children with actions such as SetParams
|
||||
-- and CompleteTransition.
|
||||
if action.preserveFocus then
|
||||
index = state.index
|
||||
end
|
||||
|
||||
if index ~= state.index or routes ~= state.routes then
|
||||
return getNextState(action, prevState, Cryo.Dictionary.join(state, {
|
||||
index = index,
|
||||
routes = routes,
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
function SwitchRouter.getComponentForState(state)
|
||||
local activeRoute = state.routes[state.index] or {}
|
||||
local routeName = activeRoute.routeName
|
||||
validate(routeName, "There is no route defined for index '%d'. " ..
|
||||
"Check that you passed in a navigation state with a " ..
|
||||
"valid tab/screen index.", state.index)
|
||||
|
||||
local childRouter = childRouters[routeName]
|
||||
|
||||
if childRouter then
|
||||
return childRouter.getComponentForState(state.routes[state.index])
|
||||
end
|
||||
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
function SwitchRouter.getComponentForRouteName(routeName)
|
||||
return getScreenForRouteName(routeConfigs, routeName)
|
||||
end
|
||||
|
||||
-- TODO: Implement SwitchRouter.getPathAndParamsForState after we add path expression support
|
||||
-- TODO: Implement SwitchRouter.getActionForPathAndParams after we add path expression support
|
||||
|
||||
return SwitchRouter
|
||||
end
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/TabRouter.js
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local SwitchRouter = require(script.Parent.SwitchRouter)
|
||||
local BackBehavior = require(script.Parent.Parent.BackBehavior)
|
||||
|
||||
return function(routeArray, config)
|
||||
-- Provide defaults suitable for tab routing.
|
||||
local switchConfig = {
|
||||
resetOnBlur = false,
|
||||
backBehavior = BackBehavior.InitialRoute,
|
||||
}
|
||||
|
||||
if config then
|
||||
switchConfig = Cryo.Dictionary.join(switchConfig, config)
|
||||
end
|
||||
|
||||
return SwitchRouter(routeArray, switchConfig)
|
||||
end
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
-- upstream: https://github.com/react-navigation/react-navigation/blob/a57e47786c5654a3803582b2c4953547164f26a0/packages/core/src/routers/__tests__/Routers.test.js
|
||||
|
||||
return function()
|
||||
local Root = script.Parent.Parent.Parent
|
||||
local Packages = Root.Parent
|
||||
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
local StackRouter = require(script.Parent.Parent.StackRouter)
|
||||
local TabRouter = require(script.Parent.Parent.TabRouter)
|
||||
local SwitchRouter = require(script.Parent.Parent.SwitchRouter)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
local StackActions = require(Root.routers.StackActions)
|
||||
local KeyGenerator = require(Root.utils.KeyGenerator)
|
||||
local expectDeepEqual = require(Root.utils.expectDeepEqual)
|
||||
|
||||
beforeEach(function()
|
||||
KeyGenerator._TESTING_ONLY_normalize_keys()
|
||||
end)
|
||||
|
||||
local ROUTERS = {
|
||||
TabRouter = TabRouter,
|
||||
StackRouter = StackRouter,
|
||||
SwitchRouter = SwitchRouter,
|
||||
}
|
||||
|
||||
local function dummyEventSubscriber()
|
||||
return { remove = function() end }
|
||||
end
|
||||
|
||||
for routerName in pairs(ROUTERS) do
|
||||
local Router = ROUTERS[routerName]
|
||||
|
||||
describe(("General router features - %s"):format(routerName), function()
|
||||
it(("title is configurable using navigationOptions and getScreenOptions - %s"):format(routerName), function()
|
||||
local FooView = Roact.Component:extend("FooView")
|
||||
function FooView:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
local BarView = Roact.Component:extend("BarView")
|
||||
function BarView:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
BarView.navigationOptions = {
|
||||
title = "BarTitle",
|
||||
}
|
||||
|
||||
local BazView = Roact.Component:extend("BazView")
|
||||
function BazView:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
BazView.navigationOptions = function(options)
|
||||
local navigation = options.navigation
|
||||
|
||||
return { title = ("Baz-%s"):format(navigation.state.params.id) }
|
||||
end
|
||||
|
||||
local router = Router({
|
||||
{ Foo = { screen = FooView } },
|
||||
{ Bar = { screen = BarView } },
|
||||
{ Baz = { screen = BazView } },
|
||||
})
|
||||
local routes = {
|
||||
{ key = "A", routeName = "Foo" },
|
||||
{ key = "B", routeName = "Bar" },
|
||||
{ key = "A", routeName = "Baz", params = { id = "123" } },
|
||||
}
|
||||
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
{
|
||||
state = routes[1],
|
||||
dispatch = function() return false end,
|
||||
addListener = dummyEventSubscriber,
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).to.equal(nil)
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
{
|
||||
state = routes[2],
|
||||
dispatch = function() return false end,
|
||||
addListener = dummyEventSubscriber,
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).to.equal("BarTitle")
|
||||
expect(
|
||||
router.getScreenOptions(
|
||||
{
|
||||
state = routes[3],
|
||||
dispatch = function() return false end,
|
||||
addListener = dummyEventSubscriber,
|
||||
},
|
||||
{}
|
||||
).title
|
||||
).to.equal("Baz-123")
|
||||
end)
|
||||
|
||||
it(("set params works in %s"):format(routerName), function()
|
||||
local FooView = Roact.Component:extend("FooView")
|
||||
|
||||
function FooView:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
local router = Router({
|
||||
{ Foo = { screen = FooView } },
|
||||
{ Bar = { screen = FooView } },
|
||||
})
|
||||
local initState = router.getStateForAction(NavigationActions.init())
|
||||
local initRoute = initState.routes[initState.index]
|
||||
|
||||
expect(initRoute.params).to.equal(nil)
|
||||
|
||||
local state0 = router.getStateForAction(
|
||||
NavigationActions.setParams({ params = {foo = 42}, key = initRoute.key }),
|
||||
initState
|
||||
)
|
||||
|
||||
expect(state0.routes[state0.index].params.foo).to.equal(42)
|
||||
end)
|
||||
|
||||
it("merges existing params when set params on existing state", function()
|
||||
local function Screen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = Router({
|
||||
{ Foo = { screen = Screen, params = { a = 1 } } },
|
||||
})
|
||||
local key = "Foo"
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
key = key,
|
||||
})
|
||||
|
||||
expect(state.index).to.equal(1)
|
||||
expect(state.routes[1].key).to.equal(key)
|
||||
expectDeepEqual(state.routes[1].params, { a = 1 })
|
||||
|
||||
local newState = router.getStateForAction(
|
||||
NavigationActions.setParams({ key = key, params = { b = 2 } }),
|
||||
state
|
||||
)
|
||||
|
||||
expectDeepEqual(newState.routes[newState.index].params, { a = 1, b = 2 })
|
||||
end)
|
||||
|
||||
it("merges params when setting params during init", function()
|
||||
local function Screen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = Router({
|
||||
{ Foo = { screen = Screen, params = { a = 1 } } },
|
||||
})
|
||||
local newState = router.getStateForAction(
|
||||
NavigationActions.setParams({ key = "Foo", params = { b = 2 } })
|
||||
)
|
||||
|
||||
expectDeepEqual(
|
||||
newState.routes[newState.index].params,
|
||||
{ a = 1, b = 2 }
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
it("Nested navigate behavior test", function()
|
||||
local function Leaf()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local First = Roact.Component:extend("First")
|
||||
function First:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
First.router = StackRouter({
|
||||
{ First1 = Leaf },
|
||||
{ First2 = Leaf },
|
||||
})
|
||||
|
||||
local Second = Roact.Component:extend("Second")
|
||||
function Second:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
Second.router = StackRouter({
|
||||
{ Second1 = Leaf },
|
||||
{ Second2 = Leaf },
|
||||
})
|
||||
|
||||
local Main = Roact.Component:extend("Main")
|
||||
function Main:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
Main.router = StackRouter({
|
||||
{ First = First },
|
||||
{ Second = Second },
|
||||
})
|
||||
|
||||
local TestRouter = SwitchRouter({
|
||||
{ Login = Leaf },
|
||||
{ Main = Main },
|
||||
})
|
||||
local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "First" },
|
||||
state1
|
||||
)
|
||||
|
||||
expect(state2.index).to.equal(2)
|
||||
expect(state2.routes[2].index).to.equal(1)
|
||||
expect(state2.routes[2].routes[1].index).to.equal(1)
|
||||
|
||||
local state3 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Second2" },
|
||||
state2
|
||||
)
|
||||
|
||||
expect(state3.index).to.equal(2)
|
||||
expect(state3.routes[2].index).to.equal(2) -- second
|
||||
expect(state3.routes[2].routes[2].index).to.equal(2) -- second.second2
|
||||
|
||||
local state4 = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "First",
|
||||
action = { type = NavigationActions.Navigate, routeName = "First2" },
|
||||
},
|
||||
state3,
|
||||
true
|
||||
)
|
||||
|
||||
expect(state4.index).to.equal(2) -- main
|
||||
expect(state4.routes[2].index).to.equal(1) -- first
|
||||
expect(state4.routes[2].routes[1].index).to.equal(2) -- first2
|
||||
|
||||
local state5 = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "First",
|
||||
action = { type = NavigationActions.Navigate, routeName = "First1" },
|
||||
},
|
||||
state3 -- second.second2 is active on state3
|
||||
)
|
||||
|
||||
expect(state5.index).to.equal(2) -- main
|
||||
expect(state5.routes[2].index).to.equal(1) -- first
|
||||
expect(state5.routes[2].routes[1].index).to.equal(1) -- first.first1
|
||||
end)
|
||||
|
||||
it("Handles no-op actions with tabs within stack router", function()
|
||||
local function BarView()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local FooTabNavigator = Roact.Component:extend("FooTabNavigator")
|
||||
|
||||
function FooTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooTabNavigator.router = TabRouter({
|
||||
{ Zap = { screen = BarView } },
|
||||
{ Zoo = { screen = BarView } },
|
||||
})
|
||||
|
||||
local TestRouter = StackRouter({
|
||||
{ Foo = {screen = FooTabNavigator } },
|
||||
{ Bar = {screen = BarView } },
|
||||
})
|
||||
local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Qux"}
|
||||
)
|
||||
|
||||
expect(state1.routes[1].key).to.equal("id-0")
|
||||
expect(state2.routes[1].key).to.equal("id-1")
|
||||
|
||||
state1.routes[1].key = state2.routes[1].key
|
||||
|
||||
expectDeepEqual(state1, state2)
|
||||
|
||||
local state3 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Zap" },
|
||||
state2
|
||||
)
|
||||
|
||||
expect(state2, state3)
|
||||
end)
|
||||
|
||||
it("Handles deep action", function()
|
||||
local function BarView()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
local FooTabNavigator = Roact.Component:extend("FooTabNavigator")
|
||||
function FooTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooTabNavigator.router = TabRouter({
|
||||
{ Zap = {screen = BarView } },
|
||||
{ Zoo = {screen = BarView } },
|
||||
})
|
||||
|
||||
local TestRouter = StackRouter({
|
||||
{ Bar = {screen = BarView} },
|
||||
{ Foo = {screen = FooTabNavigator} },
|
||||
})
|
||||
local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local expectedState = {
|
||||
index = 1,
|
||||
isTransitioning = false,
|
||||
key = "StackRouterRoot",
|
||||
routes = {
|
||||
{ key = "id-0", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state1, expectedState)
|
||||
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Foo",
|
||||
immediate = true,
|
||||
action = { type = NavigationActions.Navigate, routeName = "Zoo" },
|
||||
},
|
||||
state1
|
||||
)
|
||||
|
||||
expect(state2 and state2.index).to.equal(2)
|
||||
expect(state2 and state2.routes[2].index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("Handles the navigate action with params", function()
|
||||
local FooTabNavigator = Roact.Component:extend("FooTabNavigator")
|
||||
function FooTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooTabNavigator.router = TabRouter({
|
||||
{ Baz = { screen = function() return Roact.createElement("Frame") end } },
|
||||
{ Boo = { screen = function() return Roact.createElement("Frame") end } },
|
||||
})
|
||||
|
||||
local TestRouter = StackRouter({
|
||||
{ Foo = { screen = function() return Roact.createElement("Frame") end } },
|
||||
{ Bar = { screen = FooTabNavigator } },
|
||||
})
|
||||
local state = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.Navigate,
|
||||
immediate = true,
|
||||
routeName = "Bar",
|
||||
params = { foo = "42" },
|
||||
},
|
||||
state
|
||||
)
|
||||
|
||||
expectDeepEqual(state2 and state2.routes[2].params, { foo = "42" })
|
||||
expectDeepEqual(state2 and state2.routes[2].routes, {
|
||||
{
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
params = { foo = "42" },
|
||||
},
|
||||
{
|
||||
key = "Boo",
|
||||
routeName = "Boo",
|
||||
params = { foo = "42" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Handles the setParams action", function()
|
||||
local FooTabNavigator = Roact.Component:extend("FooTabNavigator")
|
||||
function FooTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooTabNavigator.router = TabRouter({
|
||||
{ Baz = { screen = function() return Roact.createElement("Frame") end } },
|
||||
})
|
||||
|
||||
local TestRouter = StackRouter({
|
||||
{ Foo = { screen = FooTabNavigator } },
|
||||
{ Bar = { screen = function() return Roact.createElement("Frame") end } },
|
||||
})
|
||||
local state = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.SetParams,
|
||||
params = { name = "foobar" },
|
||||
key = "Baz",
|
||||
},
|
||||
state
|
||||
)
|
||||
|
||||
expect(state2 and state2.index).to.equal(1)
|
||||
expectDeepEqual(state2 and state2.routes[1].routes, {
|
||||
{
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
params = { name = "foobar" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Supports lazily-evaluated getScreen", function()
|
||||
local function BarView()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local FooTabNavigator = Roact.Component:extend("FooTabNavigator")
|
||||
function FooTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooTabNavigator.router = TabRouter({
|
||||
{ Zap = { screen = BarView } },
|
||||
{ Zoo = { screen = BarView } },
|
||||
})
|
||||
|
||||
local TestRouter = StackRouter({
|
||||
{ Foo = { screen = FooTabNavigator } },
|
||||
{ Bar = { getScreen = function() return BarView end } },
|
||||
})
|
||||
local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, immediate = true, routeName = "Qux" }
|
||||
)
|
||||
|
||||
expect(state1.routes[1].key).to.equal("id-0")
|
||||
expect(state2.routes[1].key).to.equal("id-1")
|
||||
|
||||
state1.routes[1].key = state2.routes[1].key
|
||||
|
||||
expectDeepEqual(state1, state2)
|
||||
|
||||
local state3 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, immediate = true, routeName = "Zap" },
|
||||
state2
|
||||
)
|
||||
|
||||
expectDeepEqual(state2, state3)
|
||||
end)
|
||||
|
||||
it("Does not switch tab index when TabRouter child handles COMPLETE_NAVIGATION or SET_PARAMS", function()
|
||||
local FooStackNavigator = Roact.Component:extend("FooStackNavigator")
|
||||
function FooStackNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function BarView()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
FooStackNavigator.router = StackRouter({
|
||||
{ Foo = { screen = BarView } },
|
||||
{ Bar = { screen = BarView } },
|
||||
})
|
||||
|
||||
local TestRouter = TabRouter({
|
||||
{ Zap = { screen = FooStackNavigator } },
|
||||
{ Zoo = { screen = FooStackNavigator } },
|
||||
})
|
||||
|
||||
local state1 = TestRouter.getStateForAction({ type = NavigationActions.Init })
|
||||
|
||||
-- Navigate to the second screen in the first tab
|
||||
local state2 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Bar" },
|
||||
state1
|
||||
)
|
||||
|
||||
-- Switch tabs
|
||||
local state3 = TestRouter.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Zoo" },
|
||||
state2
|
||||
)
|
||||
|
||||
local stateAfterCompleteTransition = TestRouter.getStateForAction(
|
||||
{
|
||||
type = StackActions.CompleteTransition,
|
||||
preserveFocus = true,
|
||||
key = state2.routes[1].key,
|
||||
},
|
||||
state3
|
||||
)
|
||||
local stateAfterSetParams = TestRouter.getStateForAction(
|
||||
{
|
||||
type = NavigationActions.SetParams,
|
||||
preserveFocus = true,
|
||||
key = state1.routes[1].routes[1].key,
|
||||
params = { key = "value" },
|
||||
},
|
||||
state3
|
||||
)
|
||||
|
||||
expect(stateAfterCompleteTransition.index).to.equal(2)
|
||||
expect(stateAfterSetParams.index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("Inner actions are only unpacked if the current tab matches", function()
|
||||
local function PlainScreen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local ScreenA = Roact.Component:extend("ScreenA")
|
||||
function ScreenA:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local ScreenB = Roact.Component:extend("ScreenB")
|
||||
function ScreenB:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ScreenB.router = StackRouter({
|
||||
{ Baz = { screen = PlainScreen } },
|
||||
{ Zoo = { screen = PlainScreen } },
|
||||
})
|
||||
ScreenA.router = StackRouter({
|
||||
{ Bar = { screen = PlainScreen } },
|
||||
{ Boo = { screen = ScreenB } },
|
||||
})
|
||||
|
||||
local TestRouter = TabRouter({
|
||||
{ Foo = { screen = ScreenA } },
|
||||
})
|
||||
local screenApreState = {
|
||||
index = 1,
|
||||
key = "Init",
|
||||
isTransitioning = false,
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{ key = "Init", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
local preState = {
|
||||
index = 1,
|
||||
isTransitioning = false,
|
||||
routes = { screenApreState },
|
||||
}
|
||||
|
||||
local function comparable(state)
|
||||
local result = {}
|
||||
|
||||
if typeof(state.routeName) == "string" then
|
||||
result = Cryo.Dictionary.join(result, {
|
||||
routeName = state.routeName
|
||||
})
|
||||
end
|
||||
|
||||
if typeof(state.routes) == "table" then
|
||||
result = Cryo.Dictionary.join(result, {
|
||||
routes = Cryo.List.map(state.routes, comparable)
|
||||
})
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local action = NavigationActions.navigate({
|
||||
routeName = "Boo",
|
||||
action = NavigationActions.navigate({ routeName = "Zoo" }),
|
||||
})
|
||||
|
||||
local expectedState = ScreenA.router.getStateForAction(action, screenApreState)
|
||||
local state = TestRouter.getStateForAction(action, preState)
|
||||
|
||||
local innerState = state and state.routes[1] or state
|
||||
|
||||
expectDeepEqual(
|
||||
expectedState and comparable(expectedState),
|
||||
innerState and comparable(innerState)
|
||||
)
|
||||
end)
|
||||
end
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
return function()
|
||||
local StackActions = require(script.Parent.Parent.StackActions)
|
||||
|
||||
it("throws when indexing an unknown field", function()
|
||||
expect(function()
|
||||
return StackActions.foo
|
||||
end).to.throw("\"foo\" is not a valid member of StackActions")
|
||||
end)
|
||||
|
||||
describe("StackActions token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(StackActions.Pop).to.equal(StackActions.Pop)
|
||||
expect(StackActions.PopToTop).to.equal(StackActions.PopToTop)
|
||||
expect(StackActions.Push).to.equal(StackActions.Push)
|
||||
expect(StackActions.Reset).to.equal(StackActions.Reset)
|
||||
expect(StackActions.Replace).to.equal(StackActions.Replace)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(StackActions.Pop)).to.equal("POP")
|
||||
expect(tostring(StackActions.PopToTop)).to.equal("POP_TO_TOP")
|
||||
expect(tostring(StackActions.Push)).to.equal("PUSH")
|
||||
expect(tostring(StackActions.Reset)).to.equal("RESET")
|
||||
expect(tostring(StackActions.Replace)).to.equal("REPLACE")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("StackActions function tests", function()
|
||||
it("should return a pop action for pop()", function()
|
||||
local popTable = StackActions.pop({
|
||||
n = "n",
|
||||
})
|
||||
|
||||
expect(popTable.type).to.equal(StackActions.Pop)
|
||||
expect(popTable.n).to.equal("n")
|
||||
end)
|
||||
|
||||
it("should return a pop to top action for popToTop()", function()
|
||||
local popToTopTable = StackActions.popToTop()
|
||||
|
||||
expect(popToTopTable.type).to.equal(StackActions.PopToTop)
|
||||
end)
|
||||
|
||||
it("should return a push action for push()", function()
|
||||
local pushTable = StackActions.push({
|
||||
routeName = "routeName",
|
||||
params = "params",
|
||||
action = "action",
|
||||
})
|
||||
|
||||
expect(pushTable.type).to.equal(StackActions.Push)
|
||||
expect(pushTable.routeName).to.equal("routeName")
|
||||
expect(pushTable.params).to.equal("params")
|
||||
expect(pushTable.action).to.equal("action")
|
||||
end)
|
||||
|
||||
it("should return a reset action for reset()", function()
|
||||
local resetTable = StackActions.reset({
|
||||
index = "index",
|
||||
actions = "actions",
|
||||
key = "key",
|
||||
})
|
||||
|
||||
expect(resetTable.type).to.equal(StackActions.Reset)
|
||||
expect(resetTable.index).to.equal("index")
|
||||
expect(resetTable.key).to.equal("key")
|
||||
end)
|
||||
|
||||
it("should return a replace action for replace()", function()
|
||||
local replaceTable = StackActions.replace({
|
||||
key = "key",
|
||||
newKey = "newKey",
|
||||
routeName = "routeName",
|
||||
params = "params",
|
||||
action = "action",
|
||||
immediate = "immediate",
|
||||
})
|
||||
|
||||
expect(replaceTable.type).to.equal(StackActions.Replace)
|
||||
expect(replaceTable.key).to.equal("key")
|
||||
expect(replaceTable.newKey).to.equal("newKey")
|
||||
expect(replaceTable.routeName).to.equal("routeName")
|
||||
expect(replaceTable.params).to.equal("params")
|
||||
expect(replaceTable.action).to.equal("action")
|
||||
expect(replaceTable.immediate).to.equal("immediate")
|
||||
end)
|
||||
|
||||
it("should return a complete transition action with matching data for call to completeTransition()", function()
|
||||
local completeTransitionTable = StackActions.completeTransition({
|
||||
key = "key",
|
||||
toChildKey = "toChildKey",
|
||||
})
|
||||
|
||||
expect(completeTransitionTable.type).to.equal(StackActions.CompleteTransition)
|
||||
expect(completeTransitionTable.key).to.equal("key")
|
||||
expect(completeTransitionTable.toChildKey).to.equal("toChildKey")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+1211
File diff suppressed because it is too large
Load Diff
+2032
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
||||
return function()
|
||||
local SwitchActions = require(script.Parent.Parent.SwitchActions)
|
||||
|
||||
it("throws when indexing an unknown field", function()
|
||||
expect(function()
|
||||
return SwitchActions.foo
|
||||
end).to.throw("\"foo\" is not a valid member of SwitchActions")
|
||||
end)
|
||||
|
||||
describe("token tests", function()
|
||||
it("returns same object for each token for multiple calls", function()
|
||||
expect(SwitchActions.JumpTo).to.equal(SwitchActions.JumpTo)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(SwitchActions.JumpTo)).to.equal("JUMP_TO")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("creators", function()
|
||||
it("returns a JumpTo action for jumpTo()", function()
|
||||
local popTable = SwitchActions.jumpTo({
|
||||
routeName = "foo",
|
||||
})
|
||||
|
||||
expect(popTable.type).to.equal(SwitchActions.JumpTo)
|
||||
expect(popTable.routeName).to.equal("foo")
|
||||
expect(popTable.preserveFocus).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+589
@@ -0,0 +1,589 @@
|
||||
return function()
|
||||
local Root = script.Parent.Parent.Parent
|
||||
local SwitchRouter = require(script.Parent.Parent.SwitchRouter)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
local BackBehavior = require(Root.BackBehavior)
|
||||
|
||||
it("should be a function", function()
|
||||
expect(type(SwitchRouter)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should throw when passed a non-table", function()
|
||||
expect(function()
|
||||
SwitchRouter(5)
|
||||
end).to.throw("routeConfigs must be an array table")
|
||||
end)
|
||||
|
||||
it("should throw if initialRouteName is not found in routes table", function()
|
||||
expect(function()
|
||||
SwitchRouter({
|
||||
{ Foo = function() end },
|
||||
{ Bar = function() end },
|
||||
}, {
|
||||
initialRouteName = "MyRoute",
|
||||
})
|
||||
end).to.throw("Invalid initialRouteName 'MyRoute'. Should be one of \"Foo\", \"Bar\"")
|
||||
end)
|
||||
|
||||
it("should expose childRouters as a member", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = "A",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Bar = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = "B",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(router.childRouters.Foo).to.equal("A")
|
||||
expect(router.childRouters.Bar).to.equal("B")
|
||||
end)
|
||||
|
||||
describe("getScreenOptions tests", function()
|
||||
it("should correctly configure default screen options", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
}
|
||||
}
|
||||
},
|
||||
}, {
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("FooTitle")
|
||||
end)
|
||||
|
||||
it("should correctly configure route-specified screen options", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
},
|
||||
navigationOptions = { title = "RouteFooTitle" },
|
||||
}
|
||||
},
|
||||
}, {
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("RouteFooTitle")
|
||||
end)
|
||||
|
||||
it("should correctly configure component-specified screen options", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentFooTitle" },
|
||||
},
|
||||
}
|
||||
},
|
||||
}, {
|
||||
defaultNavigationOptions = {
|
||||
title = "FooTitle",
|
||||
},
|
||||
})
|
||||
|
||||
local screenOptions = router.getScreenOptions({
|
||||
state = {
|
||||
routeName = "Foo",
|
||||
}
|
||||
})
|
||||
|
||||
expect(screenOptions.title).to.equal("ComponentFooTitle")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getActionCreators tests", function()
|
||||
it("should return empty action creators table if none are provided", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
})
|
||||
|
||||
local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key")
|
||||
|
||||
local fieldCount = 0
|
||||
for _ in pairs(actionCreators) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
|
||||
expect(fieldCount).to.equal(0)
|
||||
end)
|
||||
|
||||
it("should call custom action creators function if provided", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
}, {
|
||||
getCustomActionCreators = function()
|
||||
return { a = 1 }
|
||||
end,
|
||||
})
|
||||
|
||||
local actionCreators = router.getActionCreators({ routeName = "Foo" }, "key")
|
||||
expect(actionCreators.a).to.equal(1)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getComponentForState tests", function()
|
||||
it("should return component matching requested state", function()
|
||||
local testComponent = function() end
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = testComponent } },
|
||||
})
|
||||
|
||||
local component = router.getComponentForState({
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
},
|
||||
index = 1,
|
||||
})
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
|
||||
it("should throw if there is no route matching active index", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = function() end } },
|
||||
})
|
||||
|
||||
local message = "There is no route defined for index '2'. " ..
|
||||
"Check that you passed in a navigation state with a " ..
|
||||
"valid tab/screen index."
|
||||
expect(function()
|
||||
router.getComponentForState({
|
||||
routes = {
|
||||
Foo = { screen = function() end },
|
||||
},
|
||||
index = 2,
|
||||
})
|
||||
end).to.throw(message)
|
||||
end)
|
||||
|
||||
it("should descend child router for requested route", function()
|
||||
local testComponent = function() end
|
||||
local childRouter = SwitchRouter({
|
||||
{ Bar = { screen = testComponent } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local component = router.getComponentForState({
|
||||
routes = {
|
||||
{
|
||||
routeName = "Foo",
|
||||
routes = { -- Child router's routes
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1
|
||||
},
|
||||
},
|
||||
index = 1,
|
||||
})
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getComponentForRouteName tests", function()
|
||||
it("should return a component that matches the given route name", function()
|
||||
local testComponent = function() end
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = testComponent } },
|
||||
})
|
||||
|
||||
local component = router.getComponentForRouteName("Foo")
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("getStateForAction tests", function()
|
||||
it("should return initial state for init action", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = function() end } },
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(#state.routes).to.equal(2)
|
||||
expect(state.routes[state.index].routeName).to.equal("Foo")
|
||||
end)
|
||||
|
||||
it("should adjust initial state index to match initialRouteName's index", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = function() end } },
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state.routes[state.index].routeName).to.equal("Foo")
|
||||
|
||||
local router2 = SwitchRouter({
|
||||
{ Foo = { screen = function() end } },
|
||||
{ Bar = { screen = function() end } },
|
||||
}, {
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
|
||||
local state2 = router2.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state2.routes[state2.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should respect optional order property", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { screen = function() end } },
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
expect(state.routes[1].routeName).to.equal("Foo")
|
||||
expect(state.routes[2].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should incorporate child router state", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
{ City = { screen = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init(), nil)
|
||||
local activeState = state.routes[state.index]
|
||||
expect(activeState.routeName).to.equal("Foo") -- parent's tracking uses parent's route name
|
||||
expect(activeState.routes[activeState.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should let active child handle non-init action first", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{ Bar = { screen = function() end } },
|
||||
{ City = { screen = function() end } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
{ State = { render = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "City" }))
|
||||
expect(state.routes[1].index).to.equal(2)
|
||||
expect(state.index).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should go back to initial route index if BackBehavior.InitialRoute", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
}, {
|
||||
backBehavior = BackBehavior.InitialRoute,
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should not change state on back action if BackBehavior.None", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState).to.equal(prevState)
|
||||
end)
|
||||
|
||||
it("should change active route on navigate", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }))
|
||||
expect(newState.index).to.equal(2)
|
||||
expect(newState.routes[newState.index].routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
it("should pass sub-action to child router on navigate", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{ City = { screen = function() end } },
|
||||
{ State = { screen = function() end } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Bar",
|
||||
action = NavigationActions.navigate({ routeName = "State" }),
|
||||
}))
|
||||
|
||||
local activeRoute = newState.routes[newState.index]
|
||||
expect(activeRoute.routeName).to.equal("Bar")
|
||||
expect(activeRoute.routes[activeRoute.index].routeName).to.equal("State")
|
||||
end)
|
||||
|
||||
it("should return initial state if navigating to active child without previous state", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
{ City = { render = function() end } },
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Foo",
|
||||
}))
|
||||
|
||||
expect(newState.routes[newState.index].routeName).to.equal("Foo")
|
||||
end)
|
||||
|
||||
it("should reset state for deactivated route by default", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = { a = 1 } },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params).to.equal(nil) -- should be empty
|
||||
end)
|
||||
|
||||
it("should not reset state for deactivated route if resetOnBlur is false", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
}, {
|
||||
resetOnBlur = false,
|
||||
})
|
||||
|
||||
local testParams = { a = 1 }
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = testParams },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params).to.equal(testParams)
|
||||
end)
|
||||
|
||||
it("should set params on route for setParams action", function()
|
||||
local router = SwitchRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.setParams({
|
||||
key = "Foo", -- By default, key == routeName
|
||||
params = { a = 1 },
|
||||
}))
|
||||
|
||||
expect(newState.routes[newState.index].params.a).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should preserve route configured params for child router", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{
|
||||
Bar = {
|
||||
screen = function() end,
|
||||
params = { a = 2 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
{ City = { render = function() end } },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init())
|
||||
expect(state.routes[state.index].params.a).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should merge initialRouteParams with initial route's own params", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
Bar = {
|
||||
render = function() end,
|
||||
params = { a = 1 },
|
||||
},
|
||||
},
|
||||
}, {
|
||||
initialRouteParams = { a = 2, b = 3 },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init())
|
||||
expect(state.routes[1].params.a).to.equal(2)
|
||||
expect(state.routes[1].params.b).to.equal(3)
|
||||
expect(state.routes[2].params.a).to.equal(1)
|
||||
expect(state.routes[2].params.b).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("should merge init action params with initial route's own params and initialRouteParams", function()
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = { render = function() end, params = { a = 1 } }
|
||||
},
|
||||
}, {
|
||||
initialRouteParams = { c = 3 },
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.init({ params = { b = 2 } }))
|
||||
expect(state.routes[1].params.a).to.equal(1)
|
||||
expect(state.routes[1].params.b).to.equal(2)
|
||||
expect(state.routes[1].params.c).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should merge navigate action params for child router", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{
|
||||
Bar = {
|
||||
screen = function() end,
|
||||
params = { a = 2 },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({
|
||||
routeName = "Bar",
|
||||
params = { b = 3 },
|
||||
}))
|
||||
|
||||
expect(state.routes[1].routes[1].params.a).to.equal(2)
|
||||
expect(state.routes[1].routes[1].params.b).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should propagate a child router getStateForAction failure to caller", function()
|
||||
local childRouter = SwitchRouter({
|
||||
{ Bar = { screen = function() end } },
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{
|
||||
Foo = {
|
||||
render = function() end,
|
||||
router = childRouter,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- need to properly initialize state because we're being abusive of getStateForAction
|
||||
local initialState = router.getStateForAction(NavigationActions.init())
|
||||
|
||||
childRouter.getStateForAction = function() return nil end
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate("Bar"), initialState)
|
||||
expect(state).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/fcd7d83c4c33ad1fa508c8cfe687d2fa259bfc2c/packages/core/src/routers/__tests__/SwitchRouter.test.js
|
||||
|
||||
return function()
|
||||
local Root = script.Parent.Parent.Parent
|
||||
local Packages = Root.Parent
|
||||
local Roact = require(Packages.Roact)
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local StackRouter = require(script.Parent.Parent.StackRouter)
|
||||
local SwitchRouter = require(script.Parent.Parent.SwitchRouter)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
local BackBehavior = require(Root.BackBehavior)
|
||||
local expectDeepEqual = require(Root.utils.expectDeepEqual)
|
||||
local getRouterTestHelper = require(script.Parent.routerTestHelper)
|
||||
|
||||
local function getExampleRouter(config)
|
||||
config = config or {}
|
||||
|
||||
local function PlainScreen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local StackA = Roact.Component:extend("StackA")
|
||||
function StackA:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local StackB = Roact.Component:extend("StackB")
|
||||
function StackB:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local StackC = Roact.Component:extend("StackC")
|
||||
function StackC:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
StackA.router = StackRouter({
|
||||
{A1 = PlainScreen},
|
||||
{A2 = PlainScreen}
|
||||
})
|
||||
StackB.router = StackRouter({
|
||||
{B1 = PlainScreen},
|
||||
{B2 = PlainScreen}
|
||||
})
|
||||
StackC.router = StackRouter({
|
||||
{C1 = PlainScreen},
|
||||
{C2 = PlainScreen}
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{A = {screen = StackA, path = ""}},
|
||||
{B = {screen = StackB, path = "great/path"}},
|
||||
{C = {screen = StackC, path = "pathC"}}
|
||||
}, Cryo.Dictionary.join(
|
||||
{initialRouteName = "A"},
|
||||
config
|
||||
))
|
||||
|
||||
return router
|
||||
end
|
||||
|
||||
describe("SwitchRouter", function()
|
||||
it("resets the route when unfocusing a tab by default", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter())
|
||||
local navigateTo = helper.navigateTo
|
||||
local getState = helper.getState
|
||||
|
||||
navigateTo("A2")
|
||||
expect(getState().routes[1].index).to.equal(2)
|
||||
expect(#getState().routes[1].routes).to.equal(2)
|
||||
|
||||
navigateTo("B")
|
||||
expect(getState().routes[1].index).to.equal(1)
|
||||
expect(#getState().routes[1].routes).to.equal(1)
|
||||
end)
|
||||
|
||||
it("does not reset the route on unfocus if resetOnBlur is false", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({resetOnBlur = false}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local getState = helper.getState
|
||||
|
||||
navigateTo("A2")
|
||||
expect(getState().routes[1].index).to.equal(2)
|
||||
expect(#getState().routes[1].routes).to.equal(2)
|
||||
navigateTo("B")
|
||||
expect(getState().routes[1].index).to.equal(2)
|
||||
expect(#getState().routes[1].routes).to.equal(2)
|
||||
end)
|
||||
|
||||
it("ignores back by default", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter())
|
||||
local jumpTo = helper.jumpTo
|
||||
local back = helper.back
|
||||
local getState = helper.getState
|
||||
|
||||
jumpTo("B")
|
||||
expect(getState().index).to.equal(2)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("handles initialRoute backBehavior", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = BackBehavior.InitialRoute,
|
||||
initialRouteName = "B",
|
||||
}))
|
||||
local jumpTo = helper.jumpTo
|
||||
local back = helper.back
|
||||
local getState = helper.getState
|
||||
|
||||
expect(getState().routeKeyHistory).to.equal(nil)
|
||||
expect(getState().index).to.equal(2)
|
||||
|
||||
jumpTo("C")
|
||||
expect(getState().index).to.equal(3)
|
||||
|
||||
jumpTo("A")
|
||||
expect(getState().index).to.equal(1)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
end)
|
||||
|
||||
it("handles order backBehavior", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = BackBehavior.Order,
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local back = helper.back
|
||||
local getState = helper.getState
|
||||
|
||||
expect(getState().routeKeyHistory).to.equal(nil)
|
||||
|
||||
navigateTo("C")
|
||||
expect(getState().index).to.equal(3)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(1)
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(1)
|
||||
end)
|
||||
|
||||
it("handles history backBehavior", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = BackBehavior.History,
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local back = helper.back
|
||||
local getState = helper.getState
|
||||
|
||||
expectDeepEqual(getState().routeKeyHistory, {"A"})
|
||||
|
||||
navigateTo("B")
|
||||
expect(getState().index).to.equal(2)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"A", "B"})
|
||||
|
||||
navigateTo("A")
|
||||
expect(getState().index).to.equal(1)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B", "A"})
|
||||
|
||||
navigateTo("C")
|
||||
expect(getState().index).to.equal(3)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B", "A", "C"})
|
||||
|
||||
navigateTo("A")
|
||||
expect(getState().index).to.equal(1)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B", "C", "A"})
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(3)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B", "C"})
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B"})
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
expectDeepEqual(getState().routeKeyHistory, {"B"})
|
||||
end)
|
||||
|
||||
it("handles history backBehavior without popping routeKeyHistory when child handles action", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = BackBehavior.History,
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local back = helper.back
|
||||
local getState = helper.getState
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expectDeepEqual(getState().routeKeyHistory, {
|
||||
"A",
|
||||
})
|
||||
|
||||
navigateTo("B")
|
||||
expect(getState().index).to.equal(2)
|
||||
expectDeepEqual(getState().routeKeyHistory, {
|
||||
"A",
|
||||
"B",
|
||||
})
|
||||
|
||||
navigateTo("B2")
|
||||
expect(getState().index).to.equal(2)
|
||||
expectDeepEqual(getState().routeKeyHistory, {
|
||||
"A",
|
||||
"B",
|
||||
})
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
|
||||
back()
|
||||
expect(getState().index).to.equal(2)
|
||||
-- "B" should not be popped when the child handles the back action
|
||||
expectDeepEqual(getState().routeKeyHistory, {
|
||||
"A",
|
||||
"B",
|
||||
})
|
||||
expect(getSubState(2).routeName).to.equal("B1")
|
||||
end)
|
||||
|
||||
it("handles back and does not apply back action to inactive child", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = "initialRoute",
|
||||
resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local back = helper.back
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
navigateTo("B")
|
||||
navigateTo("B2")
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
|
||||
navigateTo("A")
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
-- The back action should not switch to B. It should stay on A
|
||||
back(nil)
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
end)
|
||||
|
||||
it("handles pop and does not apply pop action to inactive child", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = "initialRoute",
|
||||
resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local pop = helper.pop
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
navigateTo("B")
|
||||
navigateTo("B2")
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
|
||||
navigateTo("A")
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
-- The pop action should not switch to B. It should stay on A
|
||||
pop()
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
end)
|
||||
|
||||
it("handles popToTop and does not apply popToTop action to inactive child", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = "initialRoute",
|
||||
resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local popToTop = helper.popToTop
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
navigateTo("B")
|
||||
navigateTo("B2")
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
|
||||
navigateTo("A")
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
-- The popToTop action should not switch to B. It should stay on A
|
||||
popToTop()
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
end)
|
||||
|
||||
it("handles back and does switch to inactive child with matching key", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = "initialRoute",
|
||||
resetOnBlur = false, -- Don't erase the state of substack B when we switch back to A
|
||||
}))
|
||||
local navigateTo = helper.navigateTo
|
||||
local back = helper.back
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
navigateTo("B")
|
||||
navigateTo("B2")
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
|
||||
local b2Key = getSubState(2).key
|
||||
|
||||
navigateTo("A")
|
||||
expect(getSubState(1).routeName).to.equal("A")
|
||||
|
||||
-- The back action should switch to B and go back from B2 to B1
|
||||
back(b2Key)
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B1")
|
||||
end)
|
||||
|
||||
it("handles nested actions", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter())
|
||||
local navigateTo = helper.navigateTo
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
navigateTo("B", {
|
||||
action = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "B2",
|
||||
},
|
||||
})
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
end)
|
||||
|
||||
it("handles nested actions and params simultaneously", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter())
|
||||
local navigateTo = helper.navigateTo
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
local params1 = { foo = "bar" }
|
||||
local params2 = { bar = "baz" }
|
||||
|
||||
navigateTo("B", {
|
||||
params = params1,
|
||||
action = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "B2",
|
||||
params = params2,
|
||||
},
|
||||
})
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expectDeepEqual(getSubState(1).params, params1)
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
expectDeepEqual(getSubState(2).params, params2)
|
||||
end)
|
||||
|
||||
it("order of handling navigate action is correct for nested switchrouters", function()
|
||||
-- router = switch({ Nested: switch({ Foo, Bar }), Other: switch({ Foo }), Bar })
|
||||
-- if we are focused on Other and navigate to Bar, what should happen?
|
||||
|
||||
local function Screen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local NestedSwitch = Roact.Component:extend("NestedSwitch")
|
||||
function NestedSwitch:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local OtherNestedSwitch = Roact.Component:extend("OtherNestedSwitch")
|
||||
function OtherNestedSwitch:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
local nestedRouter = SwitchRouter({
|
||||
{Foo = Screen},
|
||||
{Bar = Screen},
|
||||
})
|
||||
local otherNestedRouter = SwitchRouter({
|
||||
{Foo = Screen},
|
||||
})
|
||||
|
||||
NestedSwitch.router = nestedRouter
|
||||
OtherNestedSwitch.router = otherNestedRouter
|
||||
|
||||
local router = SwitchRouter({
|
||||
{NestedSwitch = NestedSwitch},
|
||||
{OtherNestedSwitch = OtherNestedSwitch},
|
||||
{Bar = Screen}
|
||||
}, { initialRouteName = "OtherNestedSwitch" })
|
||||
|
||||
local helper = getRouterTestHelper(router)
|
||||
local navigateTo = helper.navigateTo
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("OtherNestedSwitch")
|
||||
|
||||
navigateTo("Bar")
|
||||
expect(getSubState(1).routeName).to.equal("Bar")
|
||||
|
||||
navigateTo("NestedSwitch")
|
||||
navigateTo("Bar")
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("NestedSwitch")
|
||||
expect(getSubState(2).routeName).to.equal("Bar")
|
||||
end)
|
||||
|
||||
-- https://github.com/react-navigation/react-navigation.github.io/issues/117#issuecomment-385597628
|
||||
it("order of handling navigate action is correct for nested stackrouters", function()
|
||||
local function Screen()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local MainStack = Roact.Component:extend("MainStack")
|
||||
function MainStack:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local LoginStack = Roact.Component:extend("LoginStack")
|
||||
function LoginStack:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
MainStack.router = StackRouter({
|
||||
{Home = Screen},
|
||||
{Profile = Screen}
|
||||
})
|
||||
LoginStack.router = StackRouter({
|
||||
{Form = Screen},
|
||||
{ForgotPassword = Screen}
|
||||
})
|
||||
|
||||
local router = SwitchRouter({
|
||||
{Home = Screen},
|
||||
{Login = LoginStack},
|
||||
{Main = MainStack}
|
||||
},{ initialRouteName = "Login" })
|
||||
|
||||
local helper = getRouterTestHelper(router)
|
||||
local navigateTo = helper.navigateTo
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
expect(getSubState(1).routeName).to.equal("Login")
|
||||
|
||||
navigateTo("Home")
|
||||
expect(getSubState(1).routeName).to.equal("Home")
|
||||
end)
|
||||
|
||||
it("does not error for a nested navigate action in an uninitialized history router", function()
|
||||
local helper = getRouterTestHelper(getExampleRouter({
|
||||
backBehavior = "history",
|
||||
}), {skipInitializeState = true})
|
||||
local navigateTo = helper.navigateTo
|
||||
local getSubState = helper.getSubState
|
||||
|
||||
navigateTo("B", {
|
||||
action = NavigationActions.navigate({ routeName = "B2" }),
|
||||
})
|
||||
expect(getSubState(1).routeName).to.equal("B")
|
||||
expect(getSubState(2).routeName).to.equal("B2")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
return function()
|
||||
local TabRouter = require(script.Parent.Parent.TabRouter)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
|
||||
-- NOTE: Most functional tests are covered by SwitchRouter.spec.lua
|
||||
-- We just check that we can mount a basic case, and check that our custom
|
||||
-- defaults for resetOnBlur and backBehavior work as expected.
|
||||
|
||||
it("should return a component that matches the given route name", function()
|
||||
local testComponent = function() end
|
||||
local router = TabRouter({
|
||||
{ Foo = { screen = testComponent } },
|
||||
})
|
||||
|
||||
local component = router.getComponentForRouteName("Foo")
|
||||
expect(component).to.equal(testComponent)
|
||||
end)
|
||||
|
||||
it("should not reset state for deactivated route", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local testParams = { a = 1 }
|
||||
|
||||
local initialState = {
|
||||
routes = {
|
||||
{ routeName = "Foo", params = testParams },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local state = router.getStateForAction(NavigationActions.navigate({ routeName = "Bar" }), initialState)
|
||||
expect(state.routes[1].params).to.equal(testParams)
|
||||
end)
|
||||
|
||||
it("should go back to initial route index", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = { render = function() end } },
|
||||
{ Bar = { render = function() end } },
|
||||
})
|
||||
|
||||
local prevState = {
|
||||
routes = {
|
||||
{ routeName = "Foo" },
|
||||
{ routeName = "Bar" },
|
||||
},
|
||||
index = 2,
|
||||
}
|
||||
|
||||
local newState = router.getStateForAction(NavigationActions.back(), prevState)
|
||||
expect(newState.index).to.equal(1)
|
||||
end)
|
||||
end
|
||||
+895
@@ -0,0 +1,895 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/__tests__/TabRouter.test.js
|
||||
|
||||
return function()
|
||||
local Root = script.Parent.Parent.Parent
|
||||
local Packages = Root.Parent
|
||||
|
||||
local Roact = require(Packages.Roact)
|
||||
local BackBehavior = require(Root.BackBehavior)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
local expectDeepEqual = require(Root.utils.expectDeepEqual)
|
||||
|
||||
local TabRouter = require(script.Parent.Parent.TabRouter)
|
||||
|
||||
local BareLeafRouteConfig = {
|
||||
screen = function()
|
||||
return Roact.createElement("Frame")
|
||||
end,
|
||||
}
|
||||
|
||||
local INIT_ACTION = { type = NavigationActions.Init }
|
||||
|
||||
describe("TabRouter", function()
|
||||
it("Handles basic tab logic", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ Foo = { screen = ScreenA } },
|
||||
{ Bar = { screen = ScreenB } },
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
})
|
||||
local expectedState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state, expectedState)
|
||||
|
||||
local state2 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
}, state)
|
||||
local expectedState2 = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state2, expectedState2)
|
||||
expect(router.getComponentForState(expectedState)).to.equal(ScreenA)
|
||||
expect(router.getComponentForState(expectedState2)).to.equal(ScreenB)
|
||||
|
||||
local state3 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
}, state2)
|
||||
|
||||
expect(state3).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("Handles getScreen", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ Foo = { getScreen = function() return ScreenA end } },
|
||||
{ Bar = { getScreen = function() return ScreenB end } },
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
})
|
||||
local expectedState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state, expectedState)
|
||||
|
||||
local state2 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
}, state)
|
||||
local expectedState2 = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state2, expectedState2)
|
||||
expect(router.getComponentForState(expectedState)).to.equal(ScreenA)
|
||||
expect(router.getComponentForState(expectedState2)).to.equal(ScreenB)
|
||||
|
||||
local state3 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
}, state2)
|
||||
|
||||
expect(state3).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("Can set the initial tab", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
}, {
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
local state = router.getStateForAction({ type = NavigationActions.Init })
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Can set the initial params", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
}, {
|
||||
initialRouteName = "Bar",
|
||||
initialRouteParams = { name = "Qux" },
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
})
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar", params = { name = "Qux" } },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Handles the SetParams action", function()
|
||||
local router = TabRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = function() return Roact.createElement("Frame") end,
|
||||
},
|
||||
},
|
||||
{
|
||||
Bar = {
|
||||
screen = function() return Roact.createElement("Frame") end,
|
||||
},
|
||||
},
|
||||
})
|
||||
local state2 = router.getStateForAction({
|
||||
type = NavigationActions.SetParams,
|
||||
params = { name = "Qux" },
|
||||
key = "Foo",
|
||||
})
|
||||
|
||||
expect(state2).to.be.a('table')
|
||||
expectDeepEqual(state2.routes[1].params, {
|
||||
name = "Qux",
|
||||
})
|
||||
end)
|
||||
|
||||
it("Handles the SetParams action for inactive routes", function()
|
||||
local router = TabRouter({
|
||||
{
|
||||
Foo = {
|
||||
screen = function() return Roact.createElement("Frame") end,
|
||||
},
|
||||
},
|
||||
{
|
||||
Bar = {
|
||||
screen = function() return Roact.createElement("Frame") end,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
local initialState = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{
|
||||
key = "RouteA",
|
||||
routeName = "Foo",
|
||||
params = { name = "InitialParam", other = "Unchanged" },
|
||||
},
|
||||
{ key = "RouteB", routeName = "Bar", params = {} },
|
||||
},
|
||||
}
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.SetParams,
|
||||
params = { name = "NewParam" },
|
||||
key = "RouteA",
|
||||
}, initialState)
|
||||
|
||||
expect(state.index).to.equal(2)
|
||||
expectDeepEqual(state.routes[1].params, {
|
||||
name = "NewParam",
|
||||
other = "Unchanged",
|
||||
})
|
||||
end)
|
||||
|
||||
it("getStateForAction returns null when navigating to same tab", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
}, {
|
||||
initialRouteName = "Bar",
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
})
|
||||
local state2 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
}, state)
|
||||
|
||||
expect(state2).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("getStateForAction returns initial navigate", function()
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Foo",
|
||||
})
|
||||
|
||||
expect(state and state.index).to.equal(1)
|
||||
end)
|
||||
|
||||
-- deviation: Router.getActionForPathAndParams not implemented yet.
|
||||
itSKIP("Handles nested tabs and nested actions", function()
|
||||
local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator")
|
||||
|
||||
function ChildTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ChildTabNavigator.router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Baz = { screen = ChildTabNavigator } },
|
||||
{ Boo = BareLeafRouteConfig },
|
||||
})
|
||||
local action = router.getActionForPathAndParams("Baz/Bar", { foo = "42" })
|
||||
local navAction = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Baz",
|
||||
params = { foo = "42" },
|
||||
action = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
params = { foo = "42" },
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(action, navAction)
|
||||
|
||||
local state = router.getStateForAction(navAction)
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{
|
||||
index = 2,
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
params = { foo = "42" },
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{
|
||||
key = "Bar",
|
||||
routeName = "Bar",
|
||||
params = { foo = "42" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Handles passing params to nested tabs", function()
|
||||
local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator")
|
||||
|
||||
function ChildTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ChildTabNavigator.router = TabRouter({
|
||||
{ Boo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Baz = { screen = ChildTabNavigator } },
|
||||
})
|
||||
local navAction = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Baz",
|
||||
}
|
||||
local state = router.getStateForAction(navAction)
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{
|
||||
index = 1,
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- Ensure that navigating back and forth doesn't overwrite
|
||||
state = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Bar" },
|
||||
state
|
||||
)
|
||||
state = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Boo" },
|
||||
state
|
||||
)
|
||||
|
||||
expectDeepEqual(state and state.routes[2], {
|
||||
index = 1,
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Handles initial deep linking into nested tabs", function()
|
||||
local ChildTabNavigator = Roact.Component:extend("ChildTabNavigator")
|
||||
|
||||
function ChildTabNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ChildTabNavigator.router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Bar = BareLeafRouteConfig },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ Foo = BareLeafRouteConfig },
|
||||
{ Baz = { screen = ChildTabNavigator } },
|
||||
{ Boo = BareLeafRouteConfig },
|
||||
})
|
||||
local state = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
})
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{
|
||||
index = 2,
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
},
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
},
|
||||
})
|
||||
|
||||
local state2 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Foo" },
|
||||
state
|
||||
)
|
||||
expectDeepEqual(state2, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{
|
||||
index = 1,
|
||||
key = "Baz",
|
||||
routeName = "Baz",
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
},
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
},
|
||||
})
|
||||
|
||||
local state3 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Foo" },
|
||||
state2
|
||||
)
|
||||
expect(state3).to.equal(nil)
|
||||
end)
|
||||
|
||||
it("Handles linking across of deeply nested tabs", function()
|
||||
local ChildNavigator0 = Roact.Component:extend("ChildNavigator0")
|
||||
function ChildNavigator0:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ChildNavigator0.router = TabRouter({
|
||||
{ Boo = BareLeafRouteConfig },
|
||||
{ Baz = BareLeafRouteConfig },
|
||||
})
|
||||
|
||||
local ChildNavigator1 = Roact.Component:extend("ChildNavigator1")
|
||||
function ChildNavigator1:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ChildNavigator1.router = TabRouter({
|
||||
{ Zoo = BareLeafRouteConfig },
|
||||
{ Zap = BareLeafRouteConfig },
|
||||
})
|
||||
|
||||
local MidNavigator = Roact.Component:extend("MidNavigator")
|
||||
function MidNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
MidNavigator.router = TabRouter({
|
||||
{ Fee = { screen = ChildNavigator0 } },
|
||||
{ Bar = { screen = ChildNavigator1 } },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ Foo = { screen = MidNavigator } },
|
||||
{ Gah = BareLeafRouteConfig },
|
||||
})
|
||||
local state = router.getStateForAction(INIT_ACTION)
|
||||
|
||||
expectDeepEqual(state, {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
index = 1,
|
||||
key = "Foo",
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{
|
||||
index = 1,
|
||||
key = "Fee",
|
||||
routeName = "Fee",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Baz", routeName = "Baz" },
|
||||
},
|
||||
},
|
||||
{
|
||||
index = 1,
|
||||
key = "Bar",
|
||||
routeName = "Bar",
|
||||
routes = {
|
||||
{ key = "Zoo", routeName = "Zoo" },
|
||||
{ key = "Zap", routeName = "Zap" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key = "Gah", routeName = "Gah" },
|
||||
},
|
||||
})
|
||||
|
||||
local state2 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Zap" },
|
||||
state
|
||||
)
|
||||
|
||||
expectDeepEqual(state2, {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
index = 2,
|
||||
key = "Foo",
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{
|
||||
index = 1,
|
||||
key = "Fee",
|
||||
routeName = "Fee",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Baz", routeName = "Baz" },
|
||||
},
|
||||
},
|
||||
{
|
||||
index = 2,
|
||||
key = "Bar",
|
||||
routeName = "Bar",
|
||||
routes = {
|
||||
{ key = "Zoo", routeName = "Zoo" },
|
||||
{ key = "Zap", routeName = "Zap" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key = "Gah", routeName = "Gah" },
|
||||
},
|
||||
})
|
||||
|
||||
local state3 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "Zap" },
|
||||
state2
|
||||
)
|
||||
|
||||
expect(state3).to.equal(nil)
|
||||
|
||||
local state4 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Foo",
|
||||
action = {
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "Bar",
|
||||
action = { type = NavigationActions.Navigate, routeName = "Zap" },
|
||||
},
|
||||
})
|
||||
|
||||
expectDeepEqual(state4, {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
index = 2,
|
||||
key = "Foo",
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{
|
||||
index = 1,
|
||||
key = "Fee",
|
||||
routeName = "Fee",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Baz", routeName = "Baz" },
|
||||
},
|
||||
},
|
||||
{
|
||||
index = 2,
|
||||
key = "Bar",
|
||||
routeName = "Bar",
|
||||
routes = {
|
||||
{ key = "Zoo", routeName = "Zoo" },
|
||||
{ key = "Zap", routeName = "Zap" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key = "Gah", routeName = "Gah" },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
-- deviation: Router.getActionForPathAndParams not implemented yet.
|
||||
itSKIP("Handles path configuration", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ Foo = { path = "f", screen = ScreenA } },
|
||||
{ Bar = { path = "b/:great", screen = ScreenB } },
|
||||
})
|
||||
local params = { foo = "42" }
|
||||
local action = router.getActionForPathAndParams("b/anything", params)
|
||||
local expectedAction = {
|
||||
params = {
|
||||
foo = "42",
|
||||
great = "anything",
|
||||
},
|
||||
routeName = "Bar",
|
||||
type = NavigationActions.Navigate,
|
||||
}
|
||||
|
||||
expectDeepEqual(action, expectedAction)
|
||||
|
||||
local state = router.getStateForAction({ type = NavigationActions.Init })
|
||||
local expectedState = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo" },
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
|
||||
expect(state).to.equal(expectedState)
|
||||
|
||||
local state2 = router.getStateForAction(expectedAction, state)
|
||||
local expectedState2 = {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "Foo", routeName = "Foo", params = nil },
|
||||
{
|
||||
key = "Bar",
|
||||
routeName = "Bar",
|
||||
params = { foo = "42", great = "anything" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectDeepEqual(state2, expectedState2)
|
||||
expect(router.getComponentForState(expectedState)).to.equal(ScreenA)
|
||||
expect(router.getComponentForState(expectedState2)).to.equal(ScreenB)
|
||||
expect(router.getPathAndParamsForState(expectedState).path).to.equal("f")
|
||||
expect(router.getPathAndParamsForState(expectedState2).path).to.equal("b/anything")
|
||||
end)
|
||||
|
||||
-- deviation: Router.getActionForPathAndParams not implemented yet.
|
||||
itSKIP("Handles default configuration", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ Foo = { path = "", screen = ScreenA } },
|
||||
{ Bar = { path = "b", screen = ScreenB } },
|
||||
})
|
||||
local action = router.getActionForPathAndParams("", { foo = "42" })
|
||||
|
||||
expectDeepEqual(action, {
|
||||
params = { foo = "42" },
|
||||
routeName = "Foo",
|
||||
type = NavigationActions.Navigate,
|
||||
})
|
||||
end)
|
||||
|
||||
-- deviation: Router.getActionForPathAndParams not implemented yet.
|
||||
itSKIP("Gets deep path", function()
|
||||
local ScreenA = Roact.Component:extend("ScreenA")
|
||||
function ScreenA:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ScreenA.router = TabRouter({
|
||||
{ Baz = { screen = ScreenB } },
|
||||
{ Boo = { screen = ScreenB } },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ Foo = { path = "f", screen = ScreenA } },
|
||||
{ Bar = { screen = ScreenB } },
|
||||
})
|
||||
local state = {
|
||||
index = 1,
|
||||
routes = {
|
||||
{
|
||||
index = 2,
|
||||
key = "Foo",
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{ key = "Boo", routeName = "Boo" },
|
||||
{ key = "Baz", routeName = "Baz" },
|
||||
},
|
||||
},
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
local path = router.getPathAndParamsForState(state).path
|
||||
|
||||
expect(path).to.equal("f/Baz")
|
||||
end)
|
||||
|
||||
it("Can navigate to other tab (no router) with params", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ a = { screen = ScreenA } },
|
||||
{ b = { screen = ScreenB } },
|
||||
})
|
||||
local state0 = router.getStateForAction(INIT_ACTION)
|
||||
|
||||
expectDeepEqual(state0, {
|
||||
index = 1,
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b" },
|
||||
},
|
||||
})
|
||||
|
||||
local params = { key = "value" }
|
||||
local state1 = router.getStateForAction({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = "b",
|
||||
params = params,
|
||||
}, state0)
|
||||
|
||||
expectDeepEqual(state1, {
|
||||
index = 2,
|
||||
routes = {
|
||||
{ key = "a", routeName = "a" },
|
||||
{ key = "b", routeName = "b", params = params },
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("Back actions are not propagated to inactive children", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenC()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local InnerNavigator = Roact.Component:extend("InnerNavigator")
|
||||
function InnerNavigator:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
InnerNavigator.router = TabRouter({
|
||||
{ a = { screen = ScreenA } },
|
||||
{ b = { screen = ScreenB } },
|
||||
})
|
||||
|
||||
local router = TabRouter({
|
||||
{ inner = { screen = InnerNavigator } },
|
||||
{ c = { screen = ScreenC } },
|
||||
}, {
|
||||
backBehavior = BackBehavior.None,
|
||||
})
|
||||
local state0 = router.getStateForAction(INIT_ACTION)
|
||||
local state1 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "b" },
|
||||
state0
|
||||
)
|
||||
local state2 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "c" },
|
||||
state1
|
||||
)
|
||||
local state3 = router.getStateForAction(
|
||||
{ type = NavigationActions.Back },
|
||||
state2
|
||||
)
|
||||
|
||||
expectDeepEqual(state3, state2)
|
||||
end)
|
||||
|
||||
it("Back behavior initialRoute works", function()
|
||||
local function ScreenA()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local function ScreenB()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local router = TabRouter({
|
||||
{ a = { screen = ScreenA } },
|
||||
{ b = { screen = ScreenB } },
|
||||
})
|
||||
local state0 = router.getStateForAction(INIT_ACTION)
|
||||
local state1 = router.getStateForAction(
|
||||
{ type = NavigationActions.Navigate, routeName = "b" },
|
||||
state0
|
||||
)
|
||||
local state2 = router.getStateForAction(
|
||||
{ type = NavigationActions.Back },
|
||||
state1
|
||||
)
|
||||
|
||||
expectDeepEqual(state2, state0)
|
||||
end)
|
||||
|
||||
it("Inner actions are only unpacked if the current tab matches", function()
|
||||
local PlainScreen = function()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local ScreenA = Roact.Component:extend("ScreenA")
|
||||
function ScreenA:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
local ScreenB = Roact.Component:extend("ScreenB")
|
||||
function ScreenB:render()
|
||||
return Roact.createElement("Frame")
|
||||
end
|
||||
|
||||
ScreenB.router = TabRouter({
|
||||
{ Baz = { screen = PlainScreen } },
|
||||
{ Zoo = { screen = PlainScreen } },
|
||||
})
|
||||
ScreenA.router = TabRouter({
|
||||
{ Bar = { screen = PlainScreen } },
|
||||
{ Boo = { screen = ScreenB } },
|
||||
})
|
||||
local router = TabRouter({
|
||||
{ Foo = { screen = ScreenA } },
|
||||
})
|
||||
|
||||
local screenApreState = {
|
||||
index = 1,
|
||||
key = "Foo",
|
||||
routeName = "Foo",
|
||||
routes = {
|
||||
{ key = "Bar", routeName = "Bar" },
|
||||
},
|
||||
}
|
||||
local preState = {
|
||||
index = 1,
|
||||
routes = { screenApreState },
|
||||
}
|
||||
|
||||
local function comparable(state)
|
||||
local result = {}
|
||||
|
||||
if typeof(state.routeName) == "string" then
|
||||
result.routeName = state.routeName
|
||||
end
|
||||
if typeof(state.routes) == 'table' then
|
||||
result.routes = {}
|
||||
for i=1, #state.routes do
|
||||
result.routes[i] = comparable(state.routes[i])
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local action = NavigationActions.navigate({
|
||||
routeName = "Boo",
|
||||
action = NavigationActions.navigate({ routeName = "Zoo" }),
|
||||
})
|
||||
local expectedState = ScreenA.router.getStateForAction(action, screenApreState)
|
||||
local state = router.getStateForAction(action, preState)
|
||||
local innerState = state and state.routes[1] or state
|
||||
|
||||
expect(innerState.routes[2].index).to.equal(2)
|
||||
expectDeepEqual(
|
||||
expectedState and comparable(expectedState),
|
||||
innerState and comparable(innerState)
|
||||
)
|
||||
|
||||
local noMatchAction = NavigationActions.navigate({
|
||||
routeName = "Qux",
|
||||
action = NavigationActions.navigate({ routeName = "Zoo" }),
|
||||
})
|
||||
local expectedState2 = ScreenA.router.getStateForAction(noMatchAction, screenApreState)
|
||||
local state2 = router.getStateForAction(noMatchAction, preState)
|
||||
local innerState2 = state2 and state2.routes[1] or state2
|
||||
|
||||
expect(innerState2.routes[2].index).to.equal(1)
|
||||
expectDeepEqual(
|
||||
expectedState2 and comparable(expectedState2),
|
||||
innerState2 and comparable(innerState2)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
return function()
|
||||
local createConfigGetter = require(script.Parent.Parent.createConfigGetter)
|
||||
|
||||
it("should return a function", function()
|
||||
local result = createConfigGetter({}, {})
|
||||
expect(result).to.be.a("function")
|
||||
end)
|
||||
|
||||
it("should override default config with component-specific config", function()
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentHome" },
|
||||
},
|
||||
},
|
||||
defaultNavigationOptions = { title = "DefaultTitle" },
|
||||
})
|
||||
|
||||
expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("ComponentHome")
|
||||
end)
|
||||
|
||||
it("should override component-specific config with route-specific config", function()
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = {
|
||||
screen = {
|
||||
render = function() end,
|
||||
navigationOptions = { title = "ComponentHome" },
|
||||
},
|
||||
navigationOptions = { title = "RouteHome" },
|
||||
},
|
||||
defaultNavigationOptions = { title = "DefaultTitle" },
|
||||
})
|
||||
|
||||
expect(getScreenOptions({ state = { routeName = "Home" } }).title).to.equal("RouteHome")
|
||||
end)
|
||||
end
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/__tests__/createConfigGetter.test.js
|
||||
|
||||
return function()
|
||||
local createConfigGetter = require(script.Parent.Parent.createConfigGetter)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
|
||||
it("should get config for screen", function()
|
||||
local HomeScreen = Roact.Component:extend("HomeScreen")
|
||||
HomeScreen.navigationOptions = function(props)
|
||||
local username = props.navigation.state.params and
|
||||
props.navigation.state.params.user or "anonymous"
|
||||
|
||||
return {
|
||||
title = string.format("Welcome %s", username),
|
||||
gesturesEnabled = true,
|
||||
}
|
||||
end
|
||||
function HomeScreen:render() return nil end
|
||||
|
||||
local SettingsScreen = Roact.Component:extend("SettingsScreen")
|
||||
SettingsScreen.navigationOptions = {
|
||||
title = "Settings!!!",
|
||||
gesturesEnabled = false,
|
||||
}
|
||||
function SettingsScreen:render() return nil end
|
||||
|
||||
local NotificationScreen = Roact.Component:extend("NotificationScreen")
|
||||
NotificationScreen.navigationOptions = function(props)
|
||||
local gesturesEnabled = true
|
||||
if props.navigation.state.params then
|
||||
gesturesEnabled = not props.navigation.state.params.fullscreen
|
||||
end
|
||||
|
||||
return {
|
||||
title = "42",
|
||||
gesturesEnabled = gesturesEnabled
|
||||
}
|
||||
end
|
||||
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = { screen = HomeScreen },
|
||||
Settings = { screen = SettingsScreen },
|
||||
Notifications = {
|
||||
screen = NotificationScreen,
|
||||
navigationOptions = {
|
||||
title = "10 new notifications",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
local routes = {
|
||||
{ key = "A", routeName = "Home", },
|
||||
{ key = "B", routeName = "Home", params = { user = "jane"} },
|
||||
{ key = "C", routeName = "Settings", },
|
||||
{ key = "D", routeName = "Notifications", },
|
||||
{ key = "E", routeName = "Notifications", params = { fullscreen = true } },
|
||||
}
|
||||
|
||||
expect(getScreenOptions({ state = routes[1] }, {}).title)
|
||||
.to.equal("Welcome anonymous")
|
||||
|
||||
expect(getScreenOptions({ state = routes[2] }, {}).title)
|
||||
.to.equal("Welcome jane")
|
||||
|
||||
expect(getScreenOptions({ state = routes[1] }, {}).gesturesEnabled)
|
||||
.to.equal(true)
|
||||
|
||||
expect(getScreenOptions({ state = routes[3] }, {}).title)
|
||||
.to.equal("Settings!!!")
|
||||
|
||||
expect(getScreenOptions({ state = routes[3] }, {}).gesturesEnabled)
|
||||
.to.equal(false)
|
||||
|
||||
expect(getScreenOptions({ state = routes[4] }, {}).title)
|
||||
.to.equal("10 new notifications")
|
||||
|
||||
expect(getScreenOptions({ state = routes[4] }, {}).gesturesEnabled)
|
||||
.to.equal(true)
|
||||
|
||||
expect(getScreenOptions({ state = routes[5] }, {}).gesturesEnabled)
|
||||
.to.equal(false)
|
||||
end)
|
||||
|
||||
it("should throw if the route does not exist", function()
|
||||
local HomeScreen = Roact.Component:extend("HomeScreen")
|
||||
|
||||
HomeScreen.navigationOptions = {
|
||||
title = "Home screen",
|
||||
gesturesEnabled = true,
|
||||
}
|
||||
|
||||
local getScreenOptions = createConfigGetter({
|
||||
Home = { screen = HomeScreen },
|
||||
})
|
||||
|
||||
local routes = {{ key = "B", routeName = "Settings" }}
|
||||
|
||||
expect(function()
|
||||
getScreenOptions({ state = routes[1] }, {})
|
||||
end).to.throw("There is no route defined for key Settings.\nMust be one of: 'Home'")
|
||||
end)
|
||||
end
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
return function()
|
||||
local getNavigationActionCreators = require(script.Parent.Parent.getNavigationActionCreators)
|
||||
local NavigationActions = require(script.Parent.Parent.Parent.NavigationActions)
|
||||
|
||||
local function expectError(functor, msg)
|
||||
local status, err = pcall(functor)
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, msg)).to.never.equal(nil)
|
||||
end
|
||||
|
||||
it("should return a table with correct functions when called", function()
|
||||
local result = getNavigationActionCreators()
|
||||
expect(type(result.goBack)).to.equal("function")
|
||||
expect(type(result.navigate)).to.equal("function")
|
||||
expect(type(result.setParams)).to.equal("function")
|
||||
end)
|
||||
|
||||
describe("goBack tests", function()
|
||||
it("should return a Back action when called", function()
|
||||
local result = getNavigationActionCreators().goBack("theKey")
|
||||
expect(result.type).to.equal(NavigationActions.Back)
|
||||
expect(result.key).to.equal("theKey")
|
||||
end)
|
||||
|
||||
it("should throw when route.key is not a string", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators({ key = 5 }).goBack()
|
||||
end, "%.goBack%(%): key should be a string")
|
||||
end)
|
||||
|
||||
it("should fall back to route.key if key is not provided", function()
|
||||
local result = getNavigationActionCreators({ key = "routeKey" }).goBack()
|
||||
expect(result.key).to.equal("routeKey")
|
||||
end)
|
||||
|
||||
it("should override route.key if key is provided", function()
|
||||
local result = getNavigationActionCreators({ key = "routeKey" }).goBack("theKey")
|
||||
expect(result.key).to.equal("theKey")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("navigate tests", function()
|
||||
it("should return a Navigate action when called", function()
|
||||
local theParams = {}
|
||||
local childAction = {}
|
||||
local result = getNavigationActionCreators().navigate("theRoute", theParams, childAction)
|
||||
expect(result.type).to.equal(NavigationActions.Navigate)
|
||||
expect(result.routeName).to.equal("theRoute")
|
||||
expect(result.params).to.equal(theParams)
|
||||
expect(result.action).to.equal(childAction)
|
||||
end)
|
||||
|
||||
it("should return a navigate action with matching properties when called with a table", function()
|
||||
local testNavigateTo = {
|
||||
routeName = "theRoute",
|
||||
params = {},
|
||||
action = {},
|
||||
}
|
||||
|
||||
local result = getNavigationActionCreators().navigate(testNavigateTo)
|
||||
expect(result.type).to.equal(NavigationActions.Navigate)
|
||||
expect(result.routeName).to.equal("theRoute")
|
||||
expect(result.params).to.equal(testNavigateTo.params)
|
||||
expect(result.action).to.equal(testNavigateTo.action)
|
||||
end)
|
||||
|
||||
it("should throw when navigateTo is not a valid type", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate(5)
|
||||
end, "%.navigate%(%): navigateTo must be a string or table")
|
||||
end)
|
||||
|
||||
it("should throw when params is provided with a table navigateTo", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate({}, {})
|
||||
end, "%.navigate%(%): params can only be provided with a string navigateTo value")
|
||||
end)
|
||||
|
||||
it("should throw when action is provided with a table navigateTo", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators().navigate({}, nil, {})
|
||||
end, "%.navigate%(%): child action can only be provided with a string navigateTo value")
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setParams tests", function()
|
||||
it("should return a SetParams action when called", function()
|
||||
local theParams = {}
|
||||
local result = getNavigationActionCreators({ key = "theKey" }).setParams(theParams)
|
||||
expect(result.type).to.equal(NavigationActions.SetParams)
|
||||
expect(result.key).to.equal("theKey")
|
||||
expect(result.params).to.equal(theParams)
|
||||
end)
|
||||
|
||||
it("should throw when called by a root navigator", function()
|
||||
expectError(function()
|
||||
getNavigationActionCreators({}).setParams({})
|
||||
end, "%.setParams%(%): cannot be called by the root navigator")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
return function()
|
||||
local getScreenForRouteName = require(script.Parent.Parent.getScreenForRouteName)
|
||||
|
||||
it("should throw for invalid arg types", function()
|
||||
expect(function()
|
||||
getScreenForRouteName("", "myRoute")
|
||||
end).to.throw("routeConfigs must be a table")
|
||||
|
||||
expect(function()
|
||||
getScreenForRouteName({}, 5)
|
||||
end).to.throw("routeName must be a string")
|
||||
end)
|
||||
|
||||
it("should throw if requested route is not present within table", function()
|
||||
local function shouldThrow()
|
||||
getScreenForRouteName({
|
||||
notMyRoute = function() return "foo" end
|
||||
}, "myRoute")
|
||||
end
|
||||
|
||||
expect(shouldThrow).to.throw(
|
||||
"There is no route defined for key myRoute.\nMust be one of: 'notMyRoute'"
|
||||
)
|
||||
end)
|
||||
|
||||
it("should return raw table if screen and getScreen are not props", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = screenComponent
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should return screen prop if it is set in route data table", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = {
|
||||
screen = screenComponent
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should return object returned by getScreen function if object is valid Roact element", function()
|
||||
local screenComponent = { render = function() return nil end }
|
||||
local result = getScreenForRouteName({
|
||||
myRoute = {
|
||||
getScreen = function() return screenComponent end
|
||||
}
|
||||
}, "myRoute")
|
||||
|
||||
expect(result).to.equal(screenComponent)
|
||||
end)
|
||||
|
||||
it("should throw if getScreen does not return a valid Roact element", function()
|
||||
local errorExpected = "The getScreen defined for route 'myRoute' didn't return a valid " ..
|
||||
"screen or navigator.\n\n"
|
||||
|
||||
expect(function()
|
||||
getScreenForRouteName({
|
||||
myRoute = {
|
||||
getScreen = function() return nil end
|
||||
}
|
||||
}, "myRoute")
|
||||
end).to.throw(errorExpected)
|
||||
end)
|
||||
|
||||
it("should throw if screen is not a valid Roact element", function()
|
||||
expect(function()
|
||||
getScreenForRouteName({
|
||||
myRoute = {
|
||||
screen = 5,
|
||||
}
|
||||
}, "myRoute")
|
||||
end).to.throw("screen for key 'myRoute' must be a valid Roact component.")
|
||||
end)
|
||||
end
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/fcd7d83c4c33ad1fa508c8cfe687d2fa259bfc2c/packages/core/src/routers/__tests__/routerTestHelper.js
|
||||
|
||||
local Root = script.Parent.Parent.Parent
|
||||
local Packages = Root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local StackActions = require(Root.routers.StackActions)
|
||||
local SwitchActions = require(Root.routers.SwitchActions)
|
||||
local NavigationActions = require(Root.NavigationActions)
|
||||
|
||||
local defaultOptions = { skipInitializeState = false }
|
||||
|
||||
local function getSubStateRecursive(state, level)
|
||||
level = level or 1
|
||||
if level == 0 then
|
||||
return state
|
||||
else
|
||||
local directSubState = state.routes[state.index]
|
||||
return getSubStateRecursive(directSubState, level -1 )
|
||||
end
|
||||
end
|
||||
|
||||
local function getRouterTestHelper(router, options)
|
||||
options = options or defaultOptions
|
||||
|
||||
local state = nil
|
||||
if (not options.skipInitializeState) then
|
||||
state = router.getStateForAction({
|
||||
type = NavigationActions.Init,
|
||||
})
|
||||
end
|
||||
|
||||
local function applyAction(action)
|
||||
state = router.getStateForAction(action, state)
|
||||
end
|
||||
|
||||
local function navigateTo(routeName, otherActionAttributes)
|
||||
otherActionAttributes = otherActionAttributes or {}
|
||||
|
||||
return applyAction(Cryo.Dictionary.join({
|
||||
type = NavigationActions.Navigate,
|
||||
routeName = routeName,
|
||||
}, otherActionAttributes))
|
||||
end
|
||||
|
||||
local function jumpTo(routeName, otherActionAttributes)
|
||||
otherActionAttributes = otherActionAttributes or {}
|
||||
|
||||
return applyAction(Cryo.Dictionary.join({
|
||||
type = SwitchActions.JumpTo,
|
||||
routeName = routeName,
|
||||
}, otherActionAttributes))
|
||||
end
|
||||
|
||||
local function back(key)
|
||||
return applyAction({
|
||||
type = NavigationActions.Back,
|
||||
key = key,
|
||||
})
|
||||
end
|
||||
|
||||
local function pop()
|
||||
return applyAction({
|
||||
type = StackActions.Pop,
|
||||
})
|
||||
end
|
||||
|
||||
local function popToTop()
|
||||
return applyAction({
|
||||
type = StackActions.PopToTop,
|
||||
})
|
||||
end
|
||||
|
||||
local function getState()
|
||||
return state
|
||||
end
|
||||
|
||||
local function getSubState(level)
|
||||
level = level or 1
|
||||
|
||||
return getSubStateRecursive(state, level)
|
||||
end
|
||||
|
||||
return{
|
||||
applyAction = applyAction,
|
||||
navigateTo = navigateTo,
|
||||
jumpTo = jumpTo,
|
||||
back = back,
|
||||
pop = pop,
|
||||
popToTop = popToTop,
|
||||
getState = getState,
|
||||
getSubState = getSubState,
|
||||
}
|
||||
end
|
||||
|
||||
return getRouterTestHelper
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local validateRouteConfigArray = require(script.Parent.Parent.validateRouteConfigArray)
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
it("should throw if routeConfigs is not a table", function()
|
||||
expect(function()
|
||||
validateRouteConfigArray(5)
|
||||
end).to.throw("routeConfigs must be an array table")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs is empty", function()
|
||||
expect(function()
|
||||
validateRouteConfigArray({})
|
||||
end).to.throw("Please specify at least one route when configuring a navigator")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs contains an invalid Roact element", function()
|
||||
local error = "The component for route 'myRoute' must be a Roact Function/Stateful"
|
||||
.. " component or table with 'getScreen'.getScreen function must return Roact"
|
||||
.. " Function/Stateful component"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ myRoute = 5 },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw if getScreen returns invalid Roact element", function()
|
||||
local error = "The component for route 'myRoute' must be a Roact Function/Stateful"
|
||||
.. " component or table with 'getScreen'.getScreen function must return Roact"
|
||||
.. " Function/Stateful component"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ myRoute = { getScreen = function() end } },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw when both screen and getScreen are provided for same component", function()
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{myRoute = {
|
||||
screen = "TheScreen",
|
||||
getScreen = function() return TestComponent end,
|
||||
}}
|
||||
})
|
||||
end).to.throw("Route 'myRoute' should provide 'screen' or 'getScreen', but not both")
|
||||
end)
|
||||
|
||||
it("should throw for a simple table where screen is not a Roact Function/Stateful component", function()
|
||||
local error = "The component for route 'myRoute' must be a Roact Function/Stateful"
|
||||
.. " component or table with 'getScreen'.getScreen function must return Roact"
|
||||
.. " Function/Stateful component"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ myRoute = { screen = {} } },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw for a non-function getScreen", function()
|
||||
local error = "The component for route 'myRoute' must be a Roact Function/Stateful"
|
||||
.. " component or table with 'getScreen'.getScreen function must return"
|
||||
.. " Roact Function/Stateful component"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ myRoute = { getScreen = 5 } },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw for a Host Component", function()
|
||||
local error = "The component for route 'myRoute' must be a Roact Function/Stateful"
|
||||
.. " component or table with 'getScreen'.getScreen function must return Roact"
|
||||
.. " Function/Stateful component"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ myRoute = { aFrame = "Frame" } },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw if routeConfig is a map", function()
|
||||
local key = "basicComponentRoute"
|
||||
local error = ("routeConfigs must be an array table (found non-number key %q of type %q"):format(
|
||||
key,
|
||||
type(key)
|
||||
)
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
[key] = TestComponent,
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should throw if there is more than one route in each array entry", function()
|
||||
local error = "only one route must be defined in each entry (found multiple at index 1)"
|
||||
expect(function()
|
||||
validateRouteConfigArray({
|
||||
{ aRouteName = TestComponent, anotherRoute = TestComponent },
|
||||
})
|
||||
end).to.throw(error)
|
||||
end)
|
||||
|
||||
it("should pass for valid basic routeConfigs", function()
|
||||
validateRouteConfigArray({
|
||||
{ basicComponentRoute = TestComponent },
|
||||
{ functionalComponentRoute = function() end },
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid screen prop type routeConfigs", function()
|
||||
validateRouteConfigArray({
|
||||
{ basicComponentRoute = { screen = TestComponent } },
|
||||
{ functionalComponentRoute = { screen = function() end } },
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid getScreen route configs", function()
|
||||
validateRouteConfigArray({
|
||||
{ getScreenRoute = { getScreen = function() return TestComponent end } },
|
||||
})
|
||||
end)
|
||||
end
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local validateRouteConfigMap = require(script.Parent.Parent.validateRouteConfigMap)
|
||||
|
||||
local TestComponent = Roact.Component:extend("TestComponent")
|
||||
function TestComponent:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
local INVALID_COMPONENT_MESSAGE = "The component for route 'myRoute' must be a Roact" ..
|
||||
" component or table with 'getScreen'."
|
||||
|
||||
it("should throw if routeConfigs is not a table", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap(5)
|
||||
end).to.throw("routeConfigs must be a table")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs is empty", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({})
|
||||
end).to.throw("Please specify at least one route when configuring a navigator.")
|
||||
end)
|
||||
|
||||
it("should throw if routeConfigs contains an invalid Roact element", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = 5,
|
||||
})
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when both screen and getScreen are provided for same component", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
screen = "TheScreen",
|
||||
getScreen = function() return TestComponent end,
|
||||
}
|
||||
})
|
||||
end).to.throw("Route 'myRoute' should declare a screen or a getScreen, not both.")
|
||||
end)
|
||||
|
||||
it("should throw for a simple table where screen is not a Roact component", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
screen = {},
|
||||
}
|
||||
})
|
||||
end).to.throw(INVALID_COMPONENT_MESSAGE)
|
||||
end)
|
||||
|
||||
it("should throw for a non-function getScreen", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
getScreen = 5
|
||||
}
|
||||
})
|
||||
end).to.throw(INVALID_COMPONENT_MESSAGE)
|
||||
end)
|
||||
|
||||
it("should throw for a Host Component", function()
|
||||
expect(function()
|
||||
validateRouteConfigMap({
|
||||
myRoute = {
|
||||
aFrame = "Frame"
|
||||
}
|
||||
})
|
||||
end).to.throw(INVALID_COMPONENT_MESSAGE)
|
||||
end)
|
||||
|
||||
it("should pass for valid basic routeConfigs", function()
|
||||
validateRouteConfigMap({
|
||||
basicComponentRoute = TestComponent,
|
||||
functionalComponentRoute = function() end,
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid screen prop type routeConfigs", function()
|
||||
validateRouteConfigMap({
|
||||
basicComponentRoute = {
|
||||
screen = TestComponent,
|
||||
},
|
||||
functionalComponentRoute = {
|
||||
screen = function() end,
|
||||
},
|
||||
})
|
||||
end)
|
||||
|
||||
it("should pass for valid getScreen route configs", function()
|
||||
validateRouteConfigMap({
|
||||
getScreenRoute = {
|
||||
getScreen = function() return TestComponent end,
|
||||
}
|
||||
})
|
||||
end)
|
||||
end
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
local validateScreenOptions = require(script.Parent.Parent.validateScreenOptions)
|
||||
|
||||
it("should not throw when there are no problems", function()
|
||||
validateScreenOptions({ title = "foo" }, { routeName = "foo" })
|
||||
end)
|
||||
|
||||
it("should throw error if no routeName is provided", function()
|
||||
local status, err = pcall(function()
|
||||
validateScreenOptions({ title = "bar" }, {})
|
||||
end)
|
||||
|
||||
expect(status).to.equal(false)
|
||||
expect(string.find(err, "route.routeName must be a string")).to.never.equal(nil)
|
||||
end)
|
||||
|
||||
it("should throw error for options with function for title", function()
|
||||
expect(function()
|
||||
validateScreenOptions({
|
||||
title = function() end,
|
||||
}, { routeName = "foo" })
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/createConfigGetter.js
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local getScreenForRouteName = require(script.Parent.getScreenForRouteName)
|
||||
local validateScreenOptions = require(script.Parent.validateScreenOptions)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
local function applyConfig(configurer, navigationOptions, configProps)
|
||||
navigationOptions = navigationOptions or {}
|
||||
|
||||
local configurerType = type(configurer)
|
||||
if configurerType == "function" then
|
||||
return Cryo.Dictionary.join(
|
||||
navigationOptions,
|
||||
configurer(Cryo.Dictionary.join(configProps, {
|
||||
navigationOptions = navigationOptions
|
||||
}))
|
||||
)
|
||||
elseif configurerType == "table" then
|
||||
return Cryo.Dictionary.join(navigationOptions, configurer)
|
||||
else
|
||||
return navigationOptions
|
||||
end
|
||||
end
|
||||
|
||||
return function(routeConfigs, navigatorScreenConfig)
|
||||
return function(navigation, screenProps)
|
||||
screenProps = screenProps or {}
|
||||
local route = navigation.state
|
||||
|
||||
validate(type(route) == "table", "navigation.state must be a table")
|
||||
validate(
|
||||
type(route.routeName == "string"),
|
||||
"Cannot get config because the route does not have a routeName."
|
||||
)
|
||||
|
||||
local component = getScreenForRouteName(routeConfigs, route.routeName)
|
||||
local routeConfig = routeConfigs[route.routeName]
|
||||
|
||||
local routeScreenConfig = nil
|
||||
if routeConfig ~= component then
|
||||
routeScreenConfig = routeConfig.navigationOptions
|
||||
end
|
||||
|
||||
-- deviation: check if the component is a table, because it could be a
|
||||
-- function and it can't be indexed in Lua.
|
||||
local componentScreenConfig = type(component) == "table"
|
||||
and component.navigationOptions or {}
|
||||
|
||||
local configOptions = {
|
||||
navigation = navigation,
|
||||
screenProps = screenProps,
|
||||
-- deviation: no theme support
|
||||
}
|
||||
|
||||
local outputConfig = applyConfig(navigatorScreenConfig, {}, configOptions)
|
||||
outputConfig = applyConfig(componentScreenConfig, outputConfig, configOptions)
|
||||
outputConfig = applyConfig(routeScreenConfig, outputConfig, configOptions)
|
||||
|
||||
validateScreenOptions(outputConfig, route)
|
||||
return outputConfig
|
||||
end
|
||||
end
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
local NavigationActions = require(script.Parent.Parent.NavigationActions)
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(route)
|
||||
local result = {}
|
||||
|
||||
-- Go back FROM screen identified by 'key', or the default for current route.
|
||||
function result.goBack(key)
|
||||
if key == nil and route.key then
|
||||
validate(type(route.key) == "string", ".goBack(): key should be a string")
|
||||
key = route.key
|
||||
end
|
||||
|
||||
return NavigationActions.back({ key = key })
|
||||
end
|
||||
|
||||
-- Navigate to a different screen, either by route name+params+action, or
|
||||
-- by passing a raw navigation table.
|
||||
function result.navigate(navigateTo, params, action)
|
||||
if type(navigateTo) == "string" then
|
||||
return NavigationActions.navigate({
|
||||
routeName = navigateTo,
|
||||
params = params,
|
||||
action = action,
|
||||
})
|
||||
else
|
||||
validate(type(navigateTo) == "table", ".navigate(): navigateTo must be a string or table")
|
||||
validate(params == nil, ".navigate(): params can only be provided with a string navigateTo value")
|
||||
validate(action == nil, ".navigate(): child action can only be provided with a string navigateTo value")
|
||||
|
||||
return NavigationActions.navigate(navigateTo)
|
||||
end
|
||||
end
|
||||
|
||||
-- Change navigation params for current route
|
||||
function result.setParams(params)
|
||||
validate(type(route.key) == "string", ".setParams(): cannot be called by the root navigator")
|
||||
return NavigationActions.setParams({ params = params, key = route.key })
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/62da341b672a83786b9c3a80c8a38f929964d7cc/packages/core/src/routers/SwitchRouter.js
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local validate = require(root.utils.validate)
|
||||
local isValidScreenComponent = require(root.utils.isValidScreenComponent)
|
||||
|
||||
-- Extract a single screen Roact component/navigator from
|
||||
-- a navigator's config.
|
||||
return function(routeConfigs, routeName)
|
||||
validate(type(routeConfigs) == "table", "routeConfigs must be a table")
|
||||
validate(type(routeName) == "string", "routeName must be a string")
|
||||
|
||||
local routeConfig = routeConfigs[routeName]
|
||||
|
||||
if routeConfig == nil then
|
||||
local possibleRoutes = Cryo.List.map(Cryo.Dictionary.keys(routeConfigs), function(name)
|
||||
return ("'%s'"):format(name)
|
||||
end)
|
||||
|
||||
local message = ("There is no route defined for key %s.\nMust be one of: %s"):format(
|
||||
routeName,
|
||||
table.concat(possibleRoutes, ",")
|
||||
)
|
||||
error(message, 2)
|
||||
end
|
||||
|
||||
local routeConfigType = type(routeConfig)
|
||||
|
||||
if routeConfigType == "table" then
|
||||
if routeConfig.screen ~= nil then
|
||||
validate(
|
||||
isValidScreenComponent(routeConfig.screen),
|
||||
"screen for key '%s' must be a valid Roact component.",
|
||||
routeName
|
||||
)
|
||||
return routeConfig.screen
|
||||
elseif type(routeConfig.getScreen) == "function" then
|
||||
local screen = routeConfig.getScreen()
|
||||
validate(
|
||||
isValidScreenComponent(screen),
|
||||
"The getScreen defined for route '%s' didn't return a valid " ..
|
||||
"screen or navigator.\n\n" ..
|
||||
"Please pass it like this:\n" ..
|
||||
"%s = {\n getScreen: function() return MyScreen end\n}",
|
||||
routeName,
|
||||
routeName
|
||||
)
|
||||
return screen
|
||||
end
|
||||
end
|
||||
|
||||
validate(
|
||||
isValidScreenComponent(routeConfig),
|
||||
"Value for key '%s' must be a route config table or a valid Roact component.",
|
||||
routeName
|
||||
)
|
||||
|
||||
return routeConfig
|
||||
end
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
local isValidScreenComponent = require(script.Parent.Parent.utils.isValidScreenComponent)
|
||||
|
||||
--[[
|
||||
This utility checks to make sure that configs passed to a
|
||||
router are in the correct format.
|
||||
|
||||
Example:
|
||||
routeConfigs = {
|
||||
{ routeNameEx1 = Roact.Function/Stateful_Component },
|
||||
{ routeNameEx2 = { screen = Roact.Function/Stateful_Component } },
|
||||
{
|
||||
routeNameEx3 = {
|
||||
getScreen = function() return Roact.Function/Stateful_Component end
|
||||
}
|
||||
}
|
||||
{ routeNameEx4 = AnotherRoactNavigator } -- this is a Stateful Component
|
||||
}
|
||||
]]
|
||||
return function(routeConfigs)
|
||||
validate(type(routeConfigs) == "table", "routeConfigs must be an array table")
|
||||
|
||||
for index, route in pairs(routeConfigs) do
|
||||
validate(
|
||||
type(index) == "number",
|
||||
("routeConfigs must be an array table (found non-number key %q of type %q)"):format(
|
||||
index,
|
||||
type(index)
|
||||
)
|
||||
)
|
||||
local routeName, routeConfig = next(route)
|
||||
validate(
|
||||
next(route, routeName) == nil,
|
||||
("only one route must be defined in each entry (found multiple at index %d)"):format(
|
||||
index
|
||||
)
|
||||
)
|
||||
local configIsTable = type(routeConfig) == "table" or false
|
||||
local screenConfig = configIsTable and routeConfig or {} -- easy index .screen/.getScreen
|
||||
local screenComponent = configIsTable and routeConfig.screen or routeConfig
|
||||
validate(isValidScreenComponent(screenComponent) or
|
||||
(type(screenConfig.getScreen) == "function" and isValidScreenComponent(screenConfig.getScreen())),
|
||||
"The component for route '%s' must be a Roact Function/Stateful component or table with 'getScreen'." ..
|
||||
"getScreen function must return Roact Function/Stateful component.",
|
||||
routeName)
|
||||
|
||||
validate(screenConfig.screen == nil or screenConfig.getScreen == nil,
|
||||
"Route '%s' should provide 'screen' or 'getScreen', but not both.", routeName)
|
||||
end
|
||||
|
||||
validate(#routeConfigs > 0, "Please specify at least one route when configuring a navigator.")
|
||||
|
||||
return routeConfigs
|
||||
end
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/routers/validateRouteConfigMap.js
|
||||
local root = script.Parent.Parent
|
||||
local validate = require(root.utils.validate)
|
||||
local isValidScreenComponent = require(root.utils.isValidScreenComponent)
|
||||
|
||||
local function getScreenComponent(routeConfig)
|
||||
if not routeConfig then
|
||||
return nil
|
||||
end
|
||||
|
||||
if type(routeConfig) == "table" and routeConfig.screen then
|
||||
return routeConfig.screen
|
||||
end
|
||||
|
||||
return routeConfig
|
||||
end
|
||||
|
||||
--[[
|
||||
This utility checks to make sure that configs passed to a
|
||||
router are in the correct format.
|
||||
|
||||
Example:
|
||||
routeConfigs = {
|
||||
routeNameEx1 = Roact.Function/Stateful_Component,
|
||||
routeNameEx2 = {
|
||||
screen = Roact.Function/Stateful_Component,
|
||||
},
|
||||
routeNameEx3 = {
|
||||
getScreen = function()
|
||||
return Roact.Function/Stateful_Component
|
||||
end
|
||||
}
|
||||
routeNameEx4 = AnotherRoactNavigator -- this is a Stateful Component
|
||||
}
|
||||
]]
|
||||
return function(routeConfigs)
|
||||
validate(type(routeConfigs) == "table", "routeConfigs must be a table")
|
||||
validate(next(routeConfigs) ~= nil, "Please specify at least one route when configuring a navigator.")
|
||||
|
||||
for routeName, routeConfig in pairs(routeConfigs) do
|
||||
local screenComponent = getScreenComponent(routeConfig)
|
||||
|
||||
local tableRouteConfig = type(routeConfig) == "table"
|
||||
validate(
|
||||
isValidScreenComponent(screenComponent) or
|
||||
(tableRouteConfig and type(routeConfig.getScreen) == "function"),
|
||||
"The component for route '%s' must be a Roact component or table with 'getScreen'." ..
|
||||
[[ For example:
|
||||
|
||||
local MyScreen = require(script.Parent.MyScreen)
|
||||
...
|
||||
%s = MyScreen,
|
||||
}
|
||||
|
||||
You can also use a navigator:
|
||||
|
||||
local MyNavigator = require(script.Parent.MyNavigator)
|
||||
...
|
||||
%s = MyNavigator,
|
||||
}]],
|
||||
routeName,
|
||||
routeName,
|
||||
routeName
|
||||
)
|
||||
|
||||
if tableRouteConfig then
|
||||
validate(
|
||||
routeConfig.screen == nil or routeConfig.getScreen == nil,
|
||||
"Route '%s' should declare a screen or a getScreen, not both.",
|
||||
routeName
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return routeConfigs
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local validate = require(script.Parent.Parent.utils.validate)
|
||||
|
||||
return function(screenOptions, route)
|
||||
validate(type(screenOptions) == "table", "screenOptions must be a table")
|
||||
validate(type(route) == "table", "route must be a table")
|
||||
validate(type(route.routeName) == "string", "route.routeName must be a string")
|
||||
validate(type(screenOptions.title) ~= "function",
|
||||
"title cannot be defined as a function in navigation options for screen '%s'",
|
||||
route.routeName)
|
||||
end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/72e8160537954af40f1b070aa91ef45fc02bba69/packages/core/src/routers/KeyGenerator.ts
|
||||
local uniqueBaseId = "id-" .. tostring(math.random(100000, 1000000))
|
||||
local uuidCount = 0
|
||||
|
||||
local KeyGenerator = {}
|
||||
|
||||
-- NOTE: FOR TESTING ONLY.
|
||||
-- Normalize keys so that tests can be consistent.
|
||||
function KeyGenerator._TESTING_ONLY_normalize_keys()
|
||||
uniqueBaseId = "id-"
|
||||
uuidCount = 0
|
||||
end
|
||||
|
||||
-- Get a string key that is unique for this session.
|
||||
function KeyGenerator.generateKey()
|
||||
local key = uniqueBaseId .. tostring(uuidCount)
|
||||
uuidCount = uuidCount + 1
|
||||
return key
|
||||
end
|
||||
|
||||
return KeyGenerator
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local validate = require(script.Parent.validate)
|
||||
|
||||
|
||||
local PageNavigationEvent = {}
|
||||
PageNavigationEvent.__index = PageNavigationEvent
|
||||
|
||||
function PageNavigationEvent.new(pageName, event)
|
||||
validate(typeof(pageName) == "string", "pageName should be string")
|
||||
validate(typeof(event) == "userdata", "event should be RoactNavigation.Event")
|
||||
local self = {
|
||||
event = event,
|
||||
pageName = pageName,
|
||||
}
|
||||
|
||||
setmetatable(self, PageNavigationEvent)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function PageNavigationEvent.isPageNavigationEvent(instance)
|
||||
return getmetatable(instance).__index == PageNavigationEvent
|
||||
end
|
||||
|
||||
|
||||
PageNavigationEvent.__tostring = function (pageNavigationEvent)
|
||||
return string.format( "%-15s - %s", tostring(pageNavigationEvent.event), pageNavigationEvent.pageName)
|
||||
end
|
||||
|
||||
|
||||
function PageNavigationEvent:equalTo(anotherPageNavigationEvent)
|
||||
validate(PageNavigationEvent.isPageNavigationEvent(anotherPageNavigationEvent), "should be PageNavigationEvent")
|
||||
return self.pageName == anotherPageNavigationEvent.pageName and self.event == anotherPageNavigationEvent.event
|
||||
end
|
||||
|
||||
|
||||
return PageNavigationEvent
|
||||
+320
@@ -0,0 +1,320 @@
|
||||
--[[
|
||||
Provides functions for comparing and printing lua tables.
|
||||
]]
|
||||
|
||||
local TableUtilities = {}
|
||||
local defaultIgnore = {}
|
||||
|
||||
local function makeKeyString(key)
|
||||
if type(key) == "string" then
|
||||
return string.format("%s", key)
|
||||
else
|
||||
return string.format("[%s]", tostring(key))
|
||||
end
|
||||
end
|
||||
|
||||
local function makeValueString(value)
|
||||
local valueType = type(value)
|
||||
if valueType == "string" then
|
||||
return string.format("%q", value)
|
||||
elseif valueType == "function" or valueType == "table" then
|
||||
return string.format("<%s>", tostring(value))
|
||||
else
|
||||
return string.format("%s", tostring(value))
|
||||
end
|
||||
end
|
||||
|
||||
local function printKeypair(key, value, indentStr, comment)
|
||||
local keyString = makeKeyString(key)
|
||||
local valueString = makeValueString(value)
|
||||
|
||||
local commentStr = comment and string.format(" -- %s", comment) or ""
|
||||
print(string.format("%s%s = %s,%s", indentStr, keyString, valueString, commentStr))
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns if they have the same key-value pairs
|
||||
Except ignored keys
|
||||
]]
|
||||
function TableUtilities.ShallowEqual(A, B, ignore)
|
||||
if not A or not B then
|
||||
return false
|
||||
elseif A == B then
|
||||
return true
|
||||
end
|
||||
|
||||
if not ignore then
|
||||
ignore = defaultIgnore
|
||||
end
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, value in pairs(B) do
|
||||
if A[key] ~= value and not ignore[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function formatDeepEqualMessage(message, level)
|
||||
if level ~= 0 then
|
||||
return message
|
||||
end
|
||||
|
||||
return message
|
||||
:gsub("{1}", "first")
|
||||
:gsub("{2}", "second")
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns if they have the same key-value pairs recursively
|
||||
]]
|
||||
function TableUtilities.DeepEqual(a, b, level)
|
||||
level = level or 0
|
||||
if a == b then
|
||||
return true
|
||||
end
|
||||
|
||||
if typeof(a) ~= typeof(b) then
|
||||
local message = ("{1} is of type %s, but {2} is of type %s"):format(
|
||||
typeof(a),
|
||||
typeof(b)
|
||||
)
|
||||
|
||||
return false, formatDeepEqualMessage(message, level)
|
||||
end
|
||||
|
||||
if typeof(a) == "table" then
|
||||
local visitedKeys = {}
|
||||
|
||||
for key, value in pairs(a) do
|
||||
visitedKeys[key] = true
|
||||
|
||||
local success, innerMessage = TableUtilities.DeepEqual(value, b[key], level + 1)
|
||||
if not success then
|
||||
local message = innerMessage
|
||||
:gsub("{1}", ("{1}[%s]"):format(tostring(key)))
|
||||
:gsub("{2}", ("{2}[%s]"):format(tostring(key)))
|
||||
|
||||
return false, formatDeepEqualMessage(message, level)
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in pairs(b) do
|
||||
if not visitedKeys[key] then
|
||||
local success, innerMessage = TableUtilities.DeepEqual(a[key], value, level + 1)
|
||||
|
||||
if not success then
|
||||
local message = innerMessage
|
||||
:gsub("{1}", ("{1}[%s]"):format(tostring(key)))
|
||||
:gsub("{2}", ("{2}[%s]"):format(tostring(key)))
|
||||
|
||||
return false, formatDeepEqualMessage(message, level)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local message = "{1} ~= {2}"
|
||||
return false, formatDeepEqualMessage(message, level)
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A, B and a key, returns if two tables have the same value at key
|
||||
]]
|
||||
function TableUtilities.EqualKey(A, B, key)
|
||||
if A and B and key and key ~= "" and A[key] and B[key] and A[key] == B[key] then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes two tables A and B, returns a new table with elements of A
|
||||
which are either not keys in B or have a different value in B
|
||||
]]
|
||||
function TableUtilities.TableDifference(A, B)
|
||||
local new = {}
|
||||
|
||||
for key, value in pairs(A) do
|
||||
if B[key] ~= A[key] then
|
||||
new[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a list and returns a table whose
|
||||
keys are elements of the list and whose
|
||||
values are all true
|
||||
]]
|
||||
local function membershipTable(list)
|
||||
local result = {}
|
||||
for i = 1, #list do
|
||||
result[list[i]] = true
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes a table and returns a list of keys in that table
|
||||
]]
|
||||
local function listOfKeys(t)
|
||||
local result = {}
|
||||
for key,_ in pairs(t) do
|
||||
table.insert(result, key)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
Takes two lists A and B, returns a new list of elements of A
|
||||
which are not in B
|
||||
]]
|
||||
function TableUtilities.ListDifference(A, B)
|
||||
return listOfKeys(TableUtilities.TableDifference(membershipTable(A), membershipTable(B)))
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging. Returns false if the given table has any of the following:
|
||||
- a key that is neither a number or a string
|
||||
- a mix of number and string keys
|
||||
- number keys which are not exactly 1..#t
|
||||
]]
|
||||
function TableUtilities.CheckListConsistency(t)
|
||||
local containsNumberKey = false
|
||||
local containsStringKey = false
|
||||
local numberConsistency = true
|
||||
|
||||
local index = 1
|
||||
for x, _ in pairs(t) do
|
||||
if type(x) == 'string' then
|
||||
containsStringKey = true
|
||||
elseif type(x) == 'number' then
|
||||
if index ~= x then
|
||||
numberConsistency = false
|
||||
end
|
||||
containsNumberKey = true
|
||||
else
|
||||
return false
|
||||
end
|
||||
|
||||
if containsStringKey and containsNumberKey then
|
||||
return false
|
||||
end
|
||||
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
if containsNumberKey then
|
||||
return numberConsistency
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
--[[
|
||||
For debugging, serializes the given table to a reasonable string that might even interpret as lua.
|
||||
]]
|
||||
function TableUtilities.RecursiveToString(t, indent)
|
||||
indent = indent or ''
|
||||
|
||||
if type(t) == 'table' then
|
||||
local result = ""
|
||||
if not TableUtilities.CheckListConsistency(t) then
|
||||
result = result .. "-- WARNING: this table fails the list consistency test\n"
|
||||
end
|
||||
result = result .. "{\n"
|
||||
for k,v in pairs(t) do
|
||||
if type(k) == 'string' then
|
||||
result = result
|
||||
.. " "
|
||||
.. indent
|
||||
.. tostring(k)
|
||||
.. " = "
|
||||
.. TableUtilities.RecursiveToString(v, " "..indent)
|
||||
..";\n"
|
||||
end
|
||||
if type(k) == 'number' then
|
||||
result = result .. " " .. indent .. TableUtilities.RecursiveToString(v, " "..indent)..",\n"
|
||||
end
|
||||
end
|
||||
result = result .. indent .. "}"
|
||||
return result
|
||||
else
|
||||
return tostring(t)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
For debugging. Prints the table on multiple lines to overcome log-line length
|
||||
limitations which are otherwise necessary for performance. Use sparingly.
|
||||
]]
|
||||
function TableUtilities.Print(t, indent)
|
||||
indent = indent or ' '
|
||||
|
||||
if type(t) ~= "table" then
|
||||
error("TableUtilities.Print must be passed a table", 2)
|
||||
end
|
||||
|
||||
-- For cycle detection
|
||||
local printedTables = {}
|
||||
|
||||
local function recurse(subTable, tableKey, level)
|
||||
-- Prevent cycles by keeping track of what tables we have printed
|
||||
printedTables[subTable] = true
|
||||
|
||||
local indentStr = string.rep(indent, level)
|
||||
local valueIndentStr = string.rep(indent, level + 1)
|
||||
|
||||
if tableKey then
|
||||
print(string.format("%s%s = %s {", indentStr, makeKeyString(tableKey), makeValueString(subTable)))
|
||||
else
|
||||
print(string.format("%s%s {", indentStr, makeValueString(subTable)))
|
||||
end
|
||||
|
||||
for key, value in pairs(subTable) do
|
||||
if type(value) == "table" then
|
||||
if printedTables[value] then
|
||||
printKeypair(key, value, valueIndentStr, "Possible cycle")
|
||||
else
|
||||
recurse(value, key, level + 1)
|
||||
end
|
||||
else
|
||||
printKeypair(key, value, valueIndentStr)
|
||||
end
|
||||
end
|
||||
|
||||
print(string.format("%s}%s", indentStr, (level > 0 and "," or "")))
|
||||
end
|
||||
|
||||
recurse(t, nil, 0)
|
||||
end
|
||||
|
||||
--[[
|
||||
Takes a table and returns the field count
|
||||
]]
|
||||
function TableUtilities.FieldCount(t)
|
||||
local fieldCount = 0
|
||||
for _ in pairs(t) do
|
||||
fieldCount = fieldCount + 1
|
||||
end
|
||||
return fieldCount
|
||||
end
|
||||
|
||||
return TableUtilities
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
local NavigationEvents = require(root.views.NavigationEvents)
|
||||
local Events = require(root.Events)
|
||||
local validate = require(script.Parent.validate)
|
||||
local PageNavigationEvent = require(script.Parent.PageNavigationEvent)
|
||||
|
||||
local TrackNavigationEvents = {}
|
||||
TrackNavigationEvents.__index = TrackNavigationEvents
|
||||
|
||||
function TrackNavigationEvents.new()
|
||||
local self = {
|
||||
navigationEvents = {},
|
||||
}
|
||||
|
||||
setmetatable(self, TrackNavigationEvents)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:getNavigationEvents()
|
||||
return self.navigationEvents
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:printNavigationEvents()
|
||||
print("Total Events: ", #self.navigationEvents)
|
||||
for _, navigationEvent in ipairs(self.navigationEvents) do
|
||||
print(navigationEvent)
|
||||
end
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:waitForNumberEventsMaxWaitTime(numberOfEvents, maxWaitTimeInSeconds)
|
||||
local secondsWaitedFor = 0
|
||||
local waitDurationPerIteration = 0.33
|
||||
while #self.navigationEvents < numberOfEvents
|
||||
and secondsWaitedFor <= maxWaitTimeInSeconds
|
||||
do
|
||||
wait(waitDurationPerIteration)
|
||||
-- print("waiting for number of events to reach:", numberOfEvents, "waited for:", secondsWaitedFor)
|
||||
secondsWaitedFor = secondsWaitedFor + waitDurationPerIteration
|
||||
end
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:resetNavigationEvents()
|
||||
self.navigationEvents = {}
|
||||
end
|
||||
|
||||
local propNameToEvent = {
|
||||
onWillFocus = Events.WillFocus,
|
||||
onDidFocus = Events.DidFocus,
|
||||
onWillBlur = Events.WillBlur,
|
||||
onDidBlur = Events.DidBlur,
|
||||
}
|
||||
|
||||
function TrackNavigationEvents:createNavigationAdapter(pageName)
|
||||
local props = {}
|
||||
for propName, event in pairs(propNameToEvent) do
|
||||
props[propName] = function()
|
||||
PageNavigationEvent.new(pageName, event)
|
||||
table.insert(self.navigationEvents, PageNavigationEvent.new(pageName, event))
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement(NavigationEvents, props)
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:equalTo(pageNavigationEventList)
|
||||
validate(typeof(pageNavigationEventList) == "table", "should be a list")
|
||||
local numberOfEvents = #self.navigationEvents
|
||||
|
||||
if numberOfEvents ~= #pageNavigationEventList then
|
||||
return false, "different amount of events"
|
||||
end
|
||||
|
||||
for i=1, numberOfEvents do
|
||||
if not self.navigationEvents[i]:equalTo(pageNavigationEventList[i]) then
|
||||
return false, ("events at position %d do not match"):format(i)
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function TrackNavigationEvents:expect(pageNavigationEventList)
|
||||
local result, message = self:equalTo(pageNavigationEventList)
|
||||
|
||||
if not result then
|
||||
local selfEvents = "{}"
|
||||
local expectedEvents = "{}"
|
||||
|
||||
if #self.navigationEvents > 0 then
|
||||
selfEvents = ("{\n %s,\n}"):format(
|
||||
table.concat(
|
||||
Cryo.List.map(self.navigationEvents, tostring),
|
||||
',\n '
|
||||
)
|
||||
)
|
||||
end
|
||||
if #pageNavigationEventList > 0 then
|
||||
expectedEvents = ("{\n %s,\n}"):format(
|
||||
table.concat(
|
||||
Cryo.List.map(pageNavigationEventList, tostring),
|
||||
',\n '
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
error(("%s\nGot events: %s\n\nExpected events: %s"):format(
|
||||
message,
|
||||
selfEvents,
|
||||
expectedEvents
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
return TrackNavigationEvents
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
return function()
|
||||
local KeyGenerator = require(script.Parent.Parent.KeyGenerator)
|
||||
|
||||
it("should generate a new string key when called", function()
|
||||
KeyGenerator._TESTING_ONLY_normalize_keys()
|
||||
|
||||
expect(KeyGenerator.generateKey()).to.equal("id-0")
|
||||
expect(KeyGenerator.generateKey()).to.equal("id-1")
|
||||
end)
|
||||
|
||||
it("should generate unique string keys without being normalized", function()
|
||||
expect(KeyGenerator.generateKey()).to.never.equal(KeyGenerator.generateKey())
|
||||
end)
|
||||
end
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
local RoactNavigation = require(script.Parent.Parent.Parent)
|
||||
local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent)
|
||||
|
||||
return function()
|
||||
local testPage = "TEST PAGE"
|
||||
local willFocusEvent = RoactNavigation.Events.WillFocus
|
||||
|
||||
it("should validate constructor inputs", function()
|
||||
expect(function () PageNavigationEvent.new(testPage, willFocusEvent) end).never.to.throw()
|
||||
|
||||
expect(function () PageNavigationEvent.new(testPage, 1) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, "event") end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, nil) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(testPage, {some = "junk"}) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(1, willFocusEvent) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new(nil, willFocusEvent) end).to.throw()
|
||||
expect(function () PageNavigationEvent.new({"bogus"}, willFocusEvent) end).to.throw()
|
||||
end)
|
||||
|
||||
it("should be constructed from page name and RoactNavigation.Events", function()
|
||||
for _, event in pairs(RoactNavigation.Events) do
|
||||
local pageName = testPage .. tostring(event)
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(pageName, event)
|
||||
expect(testPageNavigationEvent.pageName).to.be.equal(pageName)
|
||||
expect(testPageNavigationEvent.event).to.be.equal(event)
|
||||
expect(PageNavigationEvent.isPageNavigationEvent(testPageNavigationEvent)).to.be.equal(true)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should implement tostring and eq", function()
|
||||
for _, event in pairs(RoactNavigation.Events) do
|
||||
local pageName = testPage .. tostring(event)
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(pageName, event)
|
||||
expect(testPageNavigationEvent:equalTo(PageNavigationEvent.new(pageName, event))).to.be.equal(true)
|
||||
expect(tostring(testPageNavigationEvent)).to.be.equal(string.format("%-15s - %s",tostring(event), pageName))
|
||||
end
|
||||
|
||||
local testPageNavigationEvent = PageNavigationEvent.new(testPage, willFocusEvent)
|
||||
local willFocus = PageNavigationEvent.new(testPage, willFocusEvent)
|
||||
expect(testPageNavigationEvent:equalTo(willFocus)).to.be.equal(true)
|
||||
local bogusWillFocus = PageNavigationEvent.new(testPage .. "bogus", willFocusEvent)
|
||||
expect(testPageNavigationEvent:equalTo(bogusWillFocus)).to.be.equal(false)
|
||||
local willBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur)
|
||||
expect(testPageNavigationEvent:equalTo(willBlur)).to.be.equal(false)
|
||||
end)
|
||||
end
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
return function()
|
||||
local TableUtilities = require(script.Parent.Parent.TableUtilities)
|
||||
|
||||
describe("DeepEqual", function()
|
||||
it("should succeed", function()
|
||||
expect(false).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fail with a message when args are not equal", function()
|
||||
local success, message = TableUtilities.DeepEqual(1, 2)
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(message:find("first ~= second")).to.be.ok()
|
||||
|
||||
success, message = TableUtilities.DeepEqual({
|
||||
foo = 1,
|
||||
}, {
|
||||
foo = 2,
|
||||
})
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(message:find("first%[foo%] ~= second%[foo%]")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should compare non-table values using standard '==' equality", function()
|
||||
expect(TableUtilities.DeepEqual(1, 1)).to.equal(true)
|
||||
expect(TableUtilities.DeepEqual("hello", "hello")).to.equal(true)
|
||||
expect(TableUtilities.DeepEqual(nil, nil)).to.equal(true)
|
||||
|
||||
local someFunction = function() end
|
||||
local theSameFunction = someFunction
|
||||
|
||||
expect(TableUtilities.DeepEqual(someFunction, theSameFunction)).to.equal(true)
|
||||
|
||||
local A = {
|
||||
foo = someFunction
|
||||
}
|
||||
local B = {
|
||||
foo = theSameFunction
|
||||
}
|
||||
|
||||
expect(TableUtilities.DeepEqual(A, B)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should fail when types differ", function()
|
||||
local success, message = TableUtilities.DeepEqual(1, "1")
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(message:find("first is of type number, but second is of type string")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should compare (and report about) nested tables", function()
|
||||
local A = {
|
||||
foo = "bar",
|
||||
nested = {
|
||||
foo = 1,
|
||||
bar = 2,
|
||||
}
|
||||
}
|
||||
local B = {
|
||||
foo = "bar",
|
||||
nested = {
|
||||
foo = 1,
|
||||
bar = 2,
|
||||
}
|
||||
}
|
||||
|
||||
expect(TableUtilities.DeepEqual(A, B)).to.equal(true)
|
||||
|
||||
local C = {
|
||||
foo = "bar",
|
||||
nested = {
|
||||
foo = 1,
|
||||
bar = 3,
|
||||
}
|
||||
}
|
||||
|
||||
local success, message = TableUtilities.DeepEqual(A, C)
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(message:find("first%[nested%]%[bar%] ~= second%[nested%]%[bar%]")).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should be commutative", function()
|
||||
local equalArgsA = {
|
||||
foo = "bar",
|
||||
hello = "world",
|
||||
}
|
||||
local equalArgsB = {
|
||||
foo = "bar",
|
||||
hello = "world",
|
||||
}
|
||||
|
||||
expect(TableUtilities.DeepEqual(equalArgsA, equalArgsB)).to.equal(true)
|
||||
expect(TableUtilities.DeepEqual(equalArgsB, equalArgsA)).to.equal(true)
|
||||
|
||||
local nonEqualArgs = {
|
||||
foo = "bar",
|
||||
}
|
||||
|
||||
local successA = TableUtilities.DeepEqual(equalArgsA, nonEqualArgs)
|
||||
local successB = TableUtilities.DeepEqual(nonEqualArgs, equalArgsA)
|
||||
|
||||
expect(successA).to.equal(false)
|
||||
expect(successB).to.equal(false)
|
||||
end)
|
||||
|
||||
it("should give the appropriate message if the second table has extra fields", function()
|
||||
local success, message = TableUtilities.DeepEqual({}, { foo = 1 })
|
||||
|
||||
expect(success).to.equal(false)
|
||||
expect(message).to.equal("first[foo] is of type nil, but second[foo] is of type number")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
local RoactNavigation = require(script.Parent.Parent.Parent)
|
||||
local TrackNavigationEvents = require(script.Parent.Parent.TrackNavigationEvents)
|
||||
local PageNavigationEvent = require(script.Parent.Parent.PageNavigationEvent)
|
||||
|
||||
|
||||
return function()
|
||||
local testPage = "TEST PAGE"
|
||||
local testPageWillFocus = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillFocus)
|
||||
local testPageWillBlur = PageNavigationEvent.new(testPage, RoactNavigation.Events.WillBlur)
|
||||
|
||||
local trackNavigationEvents = TrackNavigationEvents.new()
|
||||
it("should implement equalTo function", function()
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(true)
|
||||
|
||||
local navigationEvents = trackNavigationEvents:getNavigationEvents()
|
||||
table.insert(navigationEvents, testPageWillFocus)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus})).to.be.equal(true)
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(false)
|
||||
|
||||
table.insert(navigationEvents, testPageWillBlur)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(true)
|
||||
|
||||
table.insert(navigationEvents, testPageWillFocus)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur})).to.be.equal(false)
|
||||
expect(trackNavigationEvents:equalTo({testPageWillFocus, testPageWillBlur, testPageWillFocus})).to.be.equal(true)
|
||||
end)
|
||||
|
||||
it("should be empty after reset", function()
|
||||
trackNavigationEvents:resetNavigationEvents()
|
||||
local navigationEvents = trackNavigationEvents:getNavigationEvents()
|
||||
expect(typeof(navigationEvents)).to.be.equal('table')
|
||||
expect(#navigationEvents).to.be.equal(0)
|
||||
expect(trackNavigationEvents:equalTo({})).to.be.equal(true)
|
||||
end)
|
||||
|
||||
end
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
return function()
|
||||
local createSpy = require(script.Parent.Parent.createSpy)
|
||||
|
||||
describe("createSpy", function()
|
||||
it("should create spies", function()
|
||||
local spy = createSpy(function() end)
|
||||
|
||||
expect(spy).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should throw if spies are indexed by an invalid key", function()
|
||||
local spy = createSpy(function() end)
|
||||
|
||||
expect(function()
|
||||
return spy.test
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("value", function()
|
||||
it("should increment callCount when called", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value()
|
||||
|
||||
expect(spy.callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should store all values passed", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value(1, true, "3")
|
||||
|
||||
expect(spy.valuesLength).to.equal(3)
|
||||
expect(spy.values[1]).to.equal(1)
|
||||
expect(spy.values[2]).to.equal(true)
|
||||
expect(spy.values[3]).to.equal("3")
|
||||
end)
|
||||
|
||||
it("should return the value of the inner function", function()
|
||||
local spy = createSpy(function()
|
||||
return true
|
||||
end)
|
||||
|
||||
expect(spy.value()).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("assertCalledWith", function()
|
||||
it("should throw if the number of values differs", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value(1, 2)
|
||||
|
||||
expect(function()
|
||||
spy:assertCalledWith(1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw if any value differs", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value(1, 2)
|
||||
|
||||
expect(function()
|
||||
spy:assertCalledWith(1, 3)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
spy:assertCalledWith(2, 3)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("captureValues", function()
|
||||
it("should throw if the number of values differs", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value(1, 2)
|
||||
|
||||
expect(function()
|
||||
spy:captureValues("a")
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should capture all values in a table", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy.value(1, 2)
|
||||
|
||||
local captured = spy:captureValues("a", "b")
|
||||
expect(captured.a).to.equal(1)
|
||||
expect(captured.b).to.equal(2)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("calls", function()
|
||||
it("should keep all the arguments given to each call", function()
|
||||
local spy = createSpy()
|
||||
local firstCall = {}
|
||||
spy.value(firstCall)
|
||||
local secondCall = {}
|
||||
spy.value(secondCall)
|
||||
|
||||
expect(spy.calls[1][1]).to.equal(firstCall)
|
||||
expect(spy.calls[2][1]).to.equal(secondCall)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("mockClear", function()
|
||||
it("clears the spy state", function()
|
||||
local spy = createSpy()
|
||||
spy.value(1)
|
||||
expect(spy.callCount).to.equal(1)
|
||||
|
||||
spy:mockClear()
|
||||
|
||||
expect(spy.callCount).to.equal(0)
|
||||
expect(#spy.calls).to.equal(0)
|
||||
expect(#spy.values).to.equal(0)
|
||||
expect(spy.valuesLength).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("call", function()
|
||||
it("should increment callCount when called", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy()
|
||||
|
||||
expect(spy.callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should store all values passed", function()
|
||||
local spy = createSpy(function() end)
|
||||
spy(1, true, "3")
|
||||
|
||||
expect(spy.valuesLength).to.equal(3)
|
||||
expect(spy.values[1]).to.equal(1)
|
||||
expect(spy.values[2]).to.equal(true)
|
||||
expect(spy.values[3]).to.equal("3")
|
||||
end)
|
||||
|
||||
it("should return the value of the inner function", function()
|
||||
local spy = createSpy(function()
|
||||
return true
|
||||
end)
|
||||
|
||||
expect(spy()).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
return function()
|
||||
local getActiveChildNavigationOptions = require(script.Parent.Parent.getActiveChildNavigationOptions)
|
||||
|
||||
it("should return a function", function()
|
||||
expect(type(getActiveChildNavigationOptions)).to.equal("function")
|
||||
end)
|
||||
|
||||
it("should ask router for current screen options and return them", function()
|
||||
local testInputScreenOpts = {}
|
||||
local testScreenOpts = {}
|
||||
|
||||
local navigation = {
|
||||
state = {
|
||||
routes = {
|
||||
{ key = "123" }
|
||||
},
|
||||
index = 1,
|
||||
|
||||
},
|
||||
router = {}, -- stub
|
||||
}
|
||||
|
||||
function navigation.getChildNavigation(key)
|
||||
if key == "123" then
|
||||
return navigation
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local testOutputScreenOpts = nil
|
||||
function navigation.router.getScreenOptions(activeNav, screenProps)
|
||||
testOutputScreenOpts = screenProps
|
||||
|
||||
if activeNav == navigation then
|
||||
return testScreenOpts
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
expect(getActiveChildNavigationOptions(navigation, testInputScreenOpts))
|
||||
.to.equal(testScreenOpts)
|
||||
expect(testOutputScreenOpts).to.equal(testInputScreenOpts)
|
||||
end)
|
||||
end
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
return function()
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local isValidScreenComponent = require(script.Parent.Parent.isValidScreenComponent)
|
||||
local TestComponent = Roact.Component:extend("TestFoo")
|
||||
it("should return true for valid element types", function()
|
||||
-- Function Component is valid
|
||||
expect(isValidScreenComponent(function() end)).to.equal(true)
|
||||
-- Stateful Component is valid
|
||||
expect(isValidScreenComponent(TestComponent)).to.equal(true)
|
||||
expect(isValidScreenComponent(
|
||||
{ render = function() return TestComponent end })).to.equal(true)
|
||||
expect(isValidScreenComponent( -- we do not test if render function returns valid component
|
||||
{ render = function() end })).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should return false for invalid element types", function()
|
||||
expect(isValidScreenComponent("foo")).to.equal(false)
|
||||
expect(isValidScreenComponent(Roact.createElement("Frame"))).to.equal(false)
|
||||
expect(isValidScreenComponent(5)).to.equal(false)
|
||||
expect(isValidScreenComponent(Roact.Portal)).to.equal(false)
|
||||
expect(isValidScreenComponent({ render = "bad" })).to.equal(false)
|
||||
expect(isValidScreenComponent(
|
||||
{ notRender = function() return "foo" end })).to.equal(false)
|
||||
end)
|
||||
end
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
return function()
|
||||
local lerp = require(script.Parent.Parent.lerp)
|
||||
|
||||
it("should return bottom of range for bottom input", function()
|
||||
expect(lerp(0, 1, 0)).to.equal(0)
|
||||
expect(lerp(1, 0, 0)).to.equal(1)
|
||||
expect(lerp(-1, 0, 0)).to.equal(-1)
|
||||
end)
|
||||
|
||||
it("should return middle of range for middle input", function()
|
||||
expect(lerp(0, 1, 0.5)).to.equal(0.5)
|
||||
expect(lerp(1, 0, 0.5)).to.equal(0.5)
|
||||
expect(lerp(-1, 0, 0.5)).to.equal(-0.5)
|
||||
end)
|
||||
|
||||
it("should return top of range for top input", function()
|
||||
expect(lerp(0, 1, 1)).to.equal(1)
|
||||
expect(lerp(1, 0, 1)).to.equal(0)
|
||||
expect(lerp(-1, 0, 1)).to.equal(0)
|
||||
end)
|
||||
end
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
-- Taken from Roact
|
||||
|
||||
local expectDeepEqual = require(script.Parent.expectDeepEqual)
|
||||
|
||||
local function createSpy(inner)
|
||||
local self = {
|
||||
callCount = 0,
|
||||
calls = {},
|
||||
values = {},
|
||||
valuesLength = 0,
|
||||
}
|
||||
|
||||
self.value = function(...)
|
||||
self.callCount = self.callCount + 1
|
||||
self.values = {...}
|
||||
self.valuesLength = select("#", ...)
|
||||
table.insert(self.calls, self.values)
|
||||
|
||||
if inner ~= nil then
|
||||
return inner(...)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self.assertCalledWith = function(_, ...)
|
||||
local len = select("#", ...)
|
||||
|
||||
if self.valuesLength ~= len then
|
||||
error(("Expected %d arguments, but was called with %d arguments"):format(
|
||||
self.valuesLength,
|
||||
len
|
||||
), 2)
|
||||
end
|
||||
|
||||
for i = 1, len do
|
||||
local expected = select(i, ...)
|
||||
|
||||
assert(self.values[i] == expected, "value differs")
|
||||
end
|
||||
end
|
||||
|
||||
self.assertCalledWithDeepEqual = function(_, ...)
|
||||
local len = select("#", ...)
|
||||
|
||||
if self.valuesLength ~= len then
|
||||
error(("Expected %d arguments, but was called with %d arguments"):format(
|
||||
self.valuesLength,
|
||||
len
|
||||
), 2)
|
||||
end
|
||||
|
||||
for i = 1, len do
|
||||
local expected = select(i, ...)
|
||||
|
||||
expectDeepEqual(self.values[i], expected)
|
||||
end
|
||||
end
|
||||
|
||||
self.captureValues = function(_, ...)
|
||||
local len = select("#", ...)
|
||||
local result = {}
|
||||
|
||||
assert(self.valuesLength == len, "length of expected values differs from stored values")
|
||||
|
||||
for i = 1, len do
|
||||
local key = select(i, ...)
|
||||
result[key] = self.values[i]
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
self.mockClear = function()
|
||||
self.callCount = 0
|
||||
self.calls = {}
|
||||
self.values = {}
|
||||
self.valuesLength = 0
|
||||
end
|
||||
|
||||
setmetatable(self, {
|
||||
__index = function(_, key)
|
||||
error(("%q is not a valid member of spy"):format(key))
|
||||
end,
|
||||
__call = function(_, ...)
|
||||
return self.value(...)
|
||||
end
|
||||
})
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return createSpy
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
local TableUtilities = require(script.Parent.TableUtilities)
|
||||
|
||||
return function(a, b)
|
||||
local success, innerMessage = TableUtilities.DeepEqual(a, b)
|
||||
|
||||
if not success then
|
||||
local message = ("Values were not deep-equal.\n%s"):format(innerMessage)
|
||||
error(message, 2)
|
||||
end
|
||||
end
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
return function()
|
||||
local expectDeepEqual = require(script.Parent.expectDeepEqual)
|
||||
|
||||
it("should fail with a message when args are not equal", function()
|
||||
expect(function()
|
||||
expectDeepEqual(1, 2)
|
||||
end).to.throw("Values were not deep-equal.\nfirst ~= second")
|
||||
|
||||
expect(function()
|
||||
expectDeepEqual({
|
||||
foo = 1,
|
||||
}, {
|
||||
foo = 2,
|
||||
})
|
||||
end).to.throw("Values were not deep-equal.\nfirst[foo] ~= second[foo]")
|
||||
end)
|
||||
|
||||
it("should succeed when comparing non-table equal values", function()
|
||||
expect(function()
|
||||
expectDeepEqual(1, 1)
|
||||
end).never.to.throw()
|
||||
expect(function()
|
||||
expectDeepEqual("hello", "hello")
|
||||
end).never.to.throw()
|
||||
expect(function()
|
||||
expectDeepEqual(nil, nil)
|
||||
end).never.to.throw()
|
||||
|
||||
local someFunction = function() end
|
||||
local theSameFunction = someFunction
|
||||
|
||||
expect(function()
|
||||
expectDeepEqual(someFunction, theSameFunction)
|
||||
end).never.to.throw()
|
||||
|
||||
end)
|
||||
|
||||
it("should succeed when comparing different table identities with same structure", function()
|
||||
expect(function()
|
||||
expectDeepEqual({
|
||||
foo = "bar",
|
||||
}, {
|
||||
foo = "bar",
|
||||
})
|
||||
end).never.to.throw()
|
||||
end)
|
||||
end
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/utils/getActiveChildNavigationOptions.js
|
||||
|
||||
-- deviation: no theme parameter because no support for theme
|
||||
return function(navigation, screenProps)
|
||||
local state = navigation.state
|
||||
local router = navigation.router
|
||||
local getChildNavigation = navigation.getChildNavigation
|
||||
|
||||
local activeRoute = state.routes[state.index]
|
||||
local activeNavigation = getChildNavigation(activeRoute.key)
|
||||
|
||||
-- deviation: no support for theme
|
||||
return router.getScreenOptions(activeNavigation, screenProps)
|
||||
end
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
|
||||
return function(props)
|
||||
local scene = props.scene
|
||||
local scenes = props.scenes
|
||||
|
||||
local index = scene.index
|
||||
local lastSceneIndexInScenes = #scenes
|
||||
local isBack = not scenes[lastSceneIndexInScenes].isActive
|
||||
|
||||
if isBack then
|
||||
local currentSceneIndexInScenes = Cryo.List.find(scenes, scene)
|
||||
|
||||
local targetSceneIndexInScenes = nil
|
||||
for i, iScene in ipairs(scenes) do
|
||||
if iScene.isActive then
|
||||
targetSceneIndexInScenes = i
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local targetSceneIndex = scenes[targetSceneIndexInScenes].index
|
||||
local lastSceneIndex = scenes[lastSceneIndexInScenes].index
|
||||
|
||||
if index ~= targetSceneIndex and currentSceneIndexInScenes == lastSceneIndexInScenes then
|
||||
return {
|
||||
first = math.min(targetSceneIndex, index - 1),
|
||||
last = index + 1,
|
||||
}
|
||||
elseif index == targetSceneIndex and currentSceneIndexInScenes == targetSceneIndexInScenes then
|
||||
return {
|
||||
first = index - 1,
|
||||
last = math.max(lastSceneIndex, index + 1)
|
||||
}
|
||||
elseif index == targetSceneIndex or currentSceneIndexInScenes > targetSceneIndexInScenes then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
first = index - 1,
|
||||
last = index + 1
|
||||
}
|
||||
end
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- We have to do this using type because ElementKind is not exported by Roact
|
||||
return function (screenComponent)
|
||||
local componentType = type(screenComponent)
|
||||
local valid = componentType == "function" or -- Function Component
|
||||
(componentType == "table" and type(screenComponent.render) == "function") -- Stateful Component
|
||||
return valid
|
||||
end
|
||||
|
||||
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Helper interpolates t with range [0,1] into the range [a,b].
|
||||
local function lerp(a, b, t)
|
||||
return a * (1 - t) + b * t
|
||||
end
|
||||
|
||||
return lerp
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
--[[
|
||||
Validate() provides a mechanism to validate input arguments and
|
||||
internal state for your components. You can call it like this:
|
||||
|
||||
function myFunc(arg1, arg2)
|
||||
validate(arg1 ~= arg2, "arg1 (%s) and arg2 (%s) must be different!",
|
||||
tostring(arg1), tostring(arg2))
|
||||
return doSomething(arg1, arg2)
|
||||
end
|
||||
|
||||
The error will be surfaced at the *call site* of your function.
|
||||
]]
|
||||
return function(result, ...)
|
||||
if not result then
|
||||
error(string.format(...), 3)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/20e2625f351f90fadadbf98890270e43e744225b/packages/core/src/views/NavigationContext.ts
|
||||
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
|
||||
local NavigationContext = Roact.createContext(nil)
|
||||
|
||||
return NavigationContext
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/6390aacd07fd647d925dfec842a766c8aad5272f/packages/core/src/views/NavigationEvents.js
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Roact = require(Packages.Roact)
|
||||
local withNavigation = require(script.Parent.withNavigation)
|
||||
local Events = require(root.Events)
|
||||
|
||||
--[[
|
||||
NavigationEvents providers a wrapper component that allows you to subscribe
|
||||
to the navigation lifecycle events without having to explicitly manage your own
|
||||
listener subscriptions.
|
||||
|
||||
Usage:
|
||||
|
||||
function MyComponent:init()
|
||||
self.willFocus = function()
|
||||
-- Do tasks that need to happen right before the component will appear on screen.
|
||||
end
|
||||
|
||||
self.didFocus = function()
|
||||
-- Do tasks that need to happen right after the component appears on screen.
|
||||
end
|
||||
end
|
||||
|
||||
function MyComponent:render()
|
||||
-- Note that you must capture the self reference lexically, if you need it.
|
||||
return Roact.createElement(RoactNavigation.NavigationEvents, {
|
||||
onWillFocus = self.willFocus,
|
||||
onDidFocus = self.didFocus,
|
||||
onWillBlur = self.willBlur,
|
||||
onDidBlur = self.didBlur,
|
||||
})
|
||||
end
|
||||
|
||||
Remember that focus and blur events may be called more than once in the lifetime of a
|
||||
component. If you navigate away from a component and then come back later, it will receive
|
||||
willBlur/didBlur and then willFocus/didFocus events.
|
||||
|
||||
Also remember that your event handlers must capture any self reference lexically, if necessary.
|
||||
]]
|
||||
|
||||
local EventNameToPropName = {
|
||||
[Events.WillFocus] = "onWillFocus",
|
||||
[Events.DidFocus] = "onDidFocus",
|
||||
[Events.WillBlur] = "onWillBlur",
|
||||
[Events.DidBlur] = "onDidBlur",
|
||||
}
|
||||
|
||||
local NavigationEvents = Roact.Component:extend("NavigationEvents")
|
||||
|
||||
function NavigationEvents:didMount()
|
||||
-- We register all navigation listeners on mount to ensure listener stability across re-render
|
||||
-- A former implementation was replacing (removing/adding) listeners on all update (if prop provided)
|
||||
-- but there were issues (see https://github.com/react-navigation/react-navigation/issues/5058)
|
||||
self:subscribeAll()
|
||||
end
|
||||
|
||||
function NavigationEvents:didUpdate(prevProps)
|
||||
if self.props.navigation ~= prevProps.navigation then
|
||||
-- This component might get reused for different state, so we need to hook back up to events
|
||||
self:removeAll()
|
||||
self:subscribeAll()
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEvents:willUnmount()
|
||||
self:removeAll()
|
||||
end
|
||||
|
||||
function NavigationEvents:getPropListener(eventName)
|
||||
return self.props[EventNameToPropName[eventName]]
|
||||
end
|
||||
|
||||
function NavigationEvents:subscribeAll()
|
||||
local navigation = self.props.navigation
|
||||
|
||||
self.subscriptions = {}
|
||||
|
||||
for symbol in pairs(EventNameToPropName) do
|
||||
self.subscriptions[symbol] = navigation.addListener(symbol, function(...)
|
||||
-- Retrieve callback from props each time, in case props change.
|
||||
local callback = self:getPropListener(symbol)
|
||||
if callback then
|
||||
callback(...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEvents:removeAll()
|
||||
for symbol in pairs(EventNameToPropName) do
|
||||
local sub = self.subscriptions[symbol]
|
||||
if sub then
|
||||
sub.remove()
|
||||
self.subscriptions[symbol] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEvents:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
return withNavigation(NavigationEvents)
|
||||
+347
@@ -0,0 +1,347 @@
|
||||
-- upstream https://github.com/react-navigation/react-navigation/blob/9b55493e7662f4d54c21f75e53eb3911675f61bc/packages/core/src/views/NavigationFocusEvents.js
|
||||
|
||||
local root = script.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Events = require(root.Events)
|
||||
|
||||
local NavigationEventManager = Roact.Component:extend("NavigationEventManager")
|
||||
|
||||
function NavigationEventManager:didMount()
|
||||
local navigation = self.props.navigation
|
||||
|
||||
self._actionSubscription = navigation.addListener(
|
||||
Events.Action,
|
||||
function(...)
|
||||
return self:_handleAction(...)
|
||||
end
|
||||
)
|
||||
|
||||
self._willFocusSubscription = navigation.addListener(
|
||||
Events.WillFocus,
|
||||
function(...)
|
||||
return self:_handleWillFocus(...)
|
||||
end
|
||||
)
|
||||
|
||||
self._willBlurSubscription = navigation.addListener(
|
||||
Events.WillBlur,
|
||||
function(...)
|
||||
return self:_handleWillBlur(...)
|
||||
end
|
||||
)
|
||||
|
||||
self._didFocusSubscription = navigation.addListener(
|
||||
Events.DidFocus,
|
||||
function(...)
|
||||
return self:_handleDidFocus(...)
|
||||
end
|
||||
)
|
||||
|
||||
self._didBlurSubscription = navigation.addListener(
|
||||
Events.DidBlur,
|
||||
function(...)
|
||||
return self:_handleDidBlur(...)
|
||||
end
|
||||
)
|
||||
|
||||
self._refocusSubscription = navigation.addListener(
|
||||
Events.Refocus,
|
||||
function(...)
|
||||
return self:_handleRefocus(...)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
function NavigationEventManager:willUnmount()
|
||||
if self._actionSubscription then
|
||||
self._actionSubscription.remove()
|
||||
end
|
||||
if self._willFocusSubscription then
|
||||
self._willFocusSubscription.remove()
|
||||
end
|
||||
if self._willBlurSubscription then
|
||||
self._willBlurSubscription.remove()
|
||||
end
|
||||
if self._didFocusSubscription then
|
||||
self._didFocusSubscription.remove()
|
||||
end
|
||||
if self._didBlurSubscription then
|
||||
self._didBlurSubscription.remove()
|
||||
end
|
||||
if self._refocusSubscription then
|
||||
self._refocusSubscription.remove()
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleAction(actionPayload)
|
||||
local state = actionPayload.state
|
||||
local lastState = actionPayload.lastState
|
||||
local action = actionPayload.action
|
||||
local type = actionPayload.type
|
||||
local context = actionPayload.context
|
||||
|
||||
local navigation = self.props.navigation
|
||||
local onEvent = self.props.onEvent
|
||||
|
||||
-- We should only emit events when the navigator is focused
|
||||
-- When navigator is not focused, screens inside shouldn"t receive focused status either
|
||||
if not navigation.isFocused() then
|
||||
return
|
||||
end
|
||||
|
||||
local previous
|
||||
if lastState and lastState.routes then
|
||||
previous = lastState.routes[lastState.index]
|
||||
end
|
||||
local current = state.routes[state.index]
|
||||
|
||||
local payload = {
|
||||
context = ("%s:%s_%s"):format(current.key, tostring(action.type), context or "Root"),
|
||||
state = current,
|
||||
lastState = previous,
|
||||
action = action,
|
||||
type = type,
|
||||
}
|
||||
|
||||
if (previous and previous.key) ~= current.key then
|
||||
self:_emitWillFocus(current.key, payload)
|
||||
|
||||
if previous and previous.key then
|
||||
self:_emitWillBlur(previous.key, payload)
|
||||
end
|
||||
end
|
||||
|
||||
if lastState and lastState.isTransitioning ~= state.isTransitioning and
|
||||
state.isTransitioning == false
|
||||
then
|
||||
if self._lastWillBlurKey then
|
||||
self:_emitDidBlur(self._lastWillBlurKey, payload)
|
||||
end
|
||||
|
||||
if self._lastWillFocusKey then
|
||||
self:_emitDidFocus(self._lastWillFocusKey, payload)
|
||||
end
|
||||
end
|
||||
|
||||
onEvent(current.key, Events.Action, payload)
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleWillFocus(args)
|
||||
local lastState = args.lastState
|
||||
local action = args.action
|
||||
local context = args.context
|
||||
local type = args.type
|
||||
|
||||
local navigation = self.props.navigation
|
||||
local route = navigation.state.routes[navigation.state.index]
|
||||
|
||||
local nextLastState = nil
|
||||
if lastState and lastState.routes then
|
||||
local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r)
|
||||
return r.key == route.key
|
||||
end)
|
||||
if nextLastStateIndex then
|
||||
nextLastState = lastState.routes[nextLastStateIndex]
|
||||
end
|
||||
end
|
||||
|
||||
self:_emitWillFocus(route.key, {
|
||||
context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"),
|
||||
state = route,
|
||||
lastState = nextLastState,
|
||||
action = action,
|
||||
type = type,
|
||||
})
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleWillBlur(args)
|
||||
local lastState = args.lastState
|
||||
local action = args.action
|
||||
local context = args.context
|
||||
local type = args.type
|
||||
|
||||
local navigation = self.props.navigation
|
||||
local route = navigation.state.routes[navigation.state.index]
|
||||
|
||||
local nextLastState = nil
|
||||
if lastState and lastState.routes then
|
||||
local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r)
|
||||
return r.key == route.key
|
||||
end)
|
||||
if nextLastStateIndex then
|
||||
nextLastState = lastState.routes[nextLastStateIndex]
|
||||
end
|
||||
end
|
||||
|
||||
self:_emitWillBlur(route.key, {
|
||||
context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"),
|
||||
state = route,
|
||||
lastState = nextLastState,
|
||||
action = action,
|
||||
type = type,
|
||||
});
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleDidFocus(args)
|
||||
local lastState = args.lastState
|
||||
local action = args.action
|
||||
local context = args.context
|
||||
local type = args.type
|
||||
|
||||
local navigation = self.props.navigation
|
||||
|
||||
if self._lastWillFocusKey then
|
||||
local routeIndex = Cryo.List.findWhere(navigation.state.routes, function(r)
|
||||
return r.key == self._lastWillFocusKey
|
||||
end)
|
||||
|
||||
if routeIndex then
|
||||
local route = navigation.state.routes[routeIndex]
|
||||
|
||||
local nextLastState = nil
|
||||
if lastState and lastState.routes then
|
||||
local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r)
|
||||
return r.key == route.key
|
||||
end)
|
||||
if nextLastStateIndex then
|
||||
nextLastState = lastState.routes[nextLastStateIndex]
|
||||
end
|
||||
end
|
||||
|
||||
self:_emitDidFocus(route.key, {
|
||||
context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"),
|
||||
state = route,
|
||||
lastState = nextLastState,
|
||||
action = action,
|
||||
type = type,
|
||||
});
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleDidBlur(args)
|
||||
local lastState = args.lastState
|
||||
local action = args.action
|
||||
local context = args.context
|
||||
local type = args.type
|
||||
|
||||
local navigation = self.props.navigation
|
||||
|
||||
if self._lastWillBlurKey then
|
||||
local routeIndex = Cryo.List.findWhere(navigation.state.routes, function(r)
|
||||
return r.key == self._lastWillBlurKey
|
||||
end)
|
||||
|
||||
if routeIndex then
|
||||
local route = navigation.state.routes[routeIndex]
|
||||
|
||||
local nextLastState = nil
|
||||
if lastState and lastState.routes then
|
||||
local nextLastStateIndex = Cryo.List.findWhere(lastState and lastState.routes or {}, function(r)
|
||||
return r.key == route.key
|
||||
end)
|
||||
if nextLastStateIndex then
|
||||
nextLastState = lastState.routes[nextLastStateIndex]
|
||||
end
|
||||
end
|
||||
|
||||
self:_emitDidBlur(route.key, {
|
||||
context = ("%s:%s_%s"):format(route.key, tostring(action.type), context or "Root"),
|
||||
state = route,
|
||||
lastState = nextLastState,
|
||||
action = action,
|
||||
type = type,
|
||||
});
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventManager:_handleRefocus()
|
||||
local onEvent = self.props.onEvent
|
||||
local navigation = self.props.navigation
|
||||
local route = navigation.state.routes[navigation.state.index]
|
||||
|
||||
onEvent(route.key, Events.Refocus)
|
||||
end
|
||||
|
||||
function NavigationEventManager:_emitWillFocus(target, payload)
|
||||
if self._lastWillBlurKey == target then
|
||||
self._lastWillBlurKey = nil
|
||||
end
|
||||
|
||||
if self._lastWillFocusKey == target then
|
||||
return
|
||||
end
|
||||
|
||||
self._lastDidFocusKey = nil
|
||||
self._lastWillFocusKey = target
|
||||
|
||||
local navigation = self.props.navigation
|
||||
local onEvent = self.props.onEvent
|
||||
|
||||
onEvent(target, Events.WillFocus, payload);
|
||||
|
||||
if typeof(navigation.state.isTransitioning) ~= "boolean" or
|
||||
(navigation.state.isTransitioning ~= true and
|
||||
not navigation._dangerouslyGetParent()) -- TODO: what should we do with dangerouslyGetParent
|
||||
then
|
||||
self:_emitDidFocus(target, payload)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventManager:_emitWillBlur(target, payload)
|
||||
if self._lastWillFocusKey == target then
|
||||
self._lastWillFocusKey = nil
|
||||
end
|
||||
|
||||
if self._lastWillBlurKey == target then
|
||||
return
|
||||
end
|
||||
|
||||
self._lastDidBlurKey = nil
|
||||
self._lastWillBlurKey = target
|
||||
|
||||
local navigation = self.props.navigation
|
||||
local onEvent = self.props.onEvent
|
||||
|
||||
onEvent(target, Events.WillBlur, payload)
|
||||
|
||||
if typeof(navigation.state.isTransitioning) ~= "boolean" or
|
||||
(navigation.state.isTransitioning ~= true and
|
||||
not navigation._dangerouslyGetParent())
|
||||
then
|
||||
self:_emitDidBlur(target, payload)
|
||||
end
|
||||
end
|
||||
|
||||
function NavigationEventManager:_emitDidFocus(target, payload)
|
||||
if self._lastWillFocusKey ~= target or self._lastDidFocusKey == target then
|
||||
return
|
||||
end
|
||||
|
||||
self._lastDidFocusKey = target
|
||||
|
||||
local onEvent = self.props.onEvent
|
||||
|
||||
onEvent(target, Events.DidFocus, payload)
|
||||
end
|
||||
|
||||
function NavigationEventManager:_emitDidBlur(target, payload)
|
||||
if self._lastWillBlurKey ~= target or self._lastDidBlurKey == target then
|
||||
return
|
||||
end
|
||||
|
||||
self._lastDidBlurKey = target
|
||||
|
||||
local onEvent = self.props.onEvent
|
||||
|
||||
onEvent(target, Events.DidBlur, payload)
|
||||
end
|
||||
|
||||
function NavigationEventManager:render()
|
||||
return nil
|
||||
end
|
||||
|
||||
return NavigationEventManager
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
local root = script.Parent.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local TableUtilities = require(root.utils.TableUtilities)
|
||||
local validate = require(root.utils.validate)
|
||||
|
||||
local SCENE_KEY_PREFIX = "scene_"
|
||||
|
||||
-- Compare two scenes based upon index and view key.
|
||||
local function compareScenes(a, b)
|
||||
if a.index == b.index then
|
||||
-- compare the route keys
|
||||
local delta = #a.key - #b.key
|
||||
if delta == 0 then
|
||||
return a.key < b.key
|
||||
else
|
||||
return delta < 0
|
||||
end
|
||||
else
|
||||
-- rank by index first
|
||||
return a.index < b.index
|
||||
end
|
||||
end
|
||||
|
||||
local function routesAreShallowEqual(a, b)
|
||||
if not a or not b then
|
||||
return a == b
|
||||
end
|
||||
|
||||
if a.key ~= b.key then
|
||||
return false
|
||||
end
|
||||
|
||||
return TableUtilities.ShallowEqual(a, b)
|
||||
end
|
||||
|
||||
local function scenesAreShallowEqual(a, b)
|
||||
return
|
||||
a.key == b.key and
|
||||
a.index == b.index and
|
||||
a.isStale == b.isStale and
|
||||
a.isActive == b.isActive and
|
||||
routesAreShallowEqual(a, b)
|
||||
end
|
||||
|
||||
return function(scenes, nextState, prevState, descriptors)
|
||||
-- Always update descriptors. See react-navigation's bug, here:
|
||||
-- https://github.com/react-navigation/react-navigation/issues/4271
|
||||
-- TODO: Do we need this? Can we do a real fix?
|
||||
for _, scene in ipairs(scenes) do
|
||||
local route = scene.route
|
||||
if descriptors and descriptors[route.key] then
|
||||
scene.descriptor = descriptors[route.key]
|
||||
end
|
||||
end
|
||||
|
||||
-- Bail out early if state is not updated
|
||||
if prevState == nextState then
|
||||
return scenes
|
||||
end
|
||||
|
||||
local prevScenes = {}
|
||||
local freshScenes = {}
|
||||
local staleScenes = {}
|
||||
|
||||
-- previously stale scenes should be marked stale
|
||||
for _, scene in ipairs(scenes) do
|
||||
local key = scene.key
|
||||
if scene.isStale then
|
||||
staleScenes[key] = scene
|
||||
end
|
||||
|
||||
prevScenes[key] = scene
|
||||
end
|
||||
|
||||
local nextKeys = {} -- fake set!
|
||||
local nextRoutes = nextState.routes
|
||||
local nextRoutesLength = #nextRoutes
|
||||
|
||||
-- Clip nextRoutes to stop at index because index is top of stack!
|
||||
if nextRoutesLength > nextState.index then
|
||||
print("Warning: StackRouter provided invalid state. Index should always be the top route")
|
||||
nextRoutes = Cryo.List.removeRange(nextRoutes, nextState.index, nextRoutesLength)
|
||||
end
|
||||
|
||||
for index, route in ipairs(nextRoutes) do
|
||||
local key = SCENE_KEY_PREFIX .. route.key
|
||||
local descriptor = descriptors and descriptors[route.key] or nil
|
||||
|
||||
local scene = {
|
||||
index = index,
|
||||
isActive = false,
|
||||
isStale = false,
|
||||
key = key,
|
||||
route = route,
|
||||
descriptor = descriptor,
|
||||
}
|
||||
|
||||
validate(not nextKeys[key], "navigation.state.routes[%d].key '%s' conflicts with another route!", index, key)
|
||||
nextKeys[key] = true
|
||||
|
||||
if staleScenes[key] then
|
||||
-- Previously stale scene was added to nextState, so we remove it from
|
||||
-- the map of stale scenes.
|
||||
staleScenes[key] = nil
|
||||
end
|
||||
|
||||
freshScenes[key] = scene
|
||||
end
|
||||
|
||||
if prevState then
|
||||
local prevRoutes = prevState.routes
|
||||
local prevRoutesLength = #prevRoutes
|
||||
if prevRoutesLength > prevState.index then
|
||||
print("StackRouter provided invalid state. Index should always be the top route.")
|
||||
prevRoutes = Cryo.List.removeRange(prevRoutes, prevState.index, prevRoutesLength)
|
||||
end
|
||||
|
||||
-- Search previous routes and mark any removed scenes as stale
|
||||
for index, route in ipairs(prevRoutes) do
|
||||
local key = SCENE_KEY_PREFIX .. route.key
|
||||
-- Skip any refreshed scenes
|
||||
if not freshScenes[key] then
|
||||
local lastScene = nil
|
||||
for _, scene in ipairs(scenes) do
|
||||
if scene.route.key == route.key then
|
||||
lastScene = scene
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local descriptor = descriptors[route.key]
|
||||
if lastScene then
|
||||
descriptor = lastScene.descriptor
|
||||
end
|
||||
|
||||
if descriptor then
|
||||
staleScenes[key] = {
|
||||
index = index,
|
||||
isActive = false,
|
||||
isStale = true,
|
||||
key = key,
|
||||
route = route,
|
||||
descriptor = descriptor,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local nextScenes = {}
|
||||
|
||||
local function mergeScene(nextScene)
|
||||
local key = nextScene.key
|
||||
local prevScene = prevScenes[key] or nil
|
||||
if prevScene and scenesAreShallowEqual(prevScene, nextScene) then
|
||||
-- reuse prevScene to avoid re-render
|
||||
table.insert(nextScenes, prevScene)
|
||||
else
|
||||
table.insert(nextScenes, nextScene)
|
||||
end
|
||||
end
|
||||
|
||||
for _, scene in pairs(staleScenes) do
|
||||
mergeScene(scene)
|
||||
end
|
||||
|
||||
for _, scene in pairs(freshScenes) do
|
||||
mergeScene(scene)
|
||||
end
|
||||
|
||||
table.sort(nextScenes, compareScenes)
|
||||
|
||||
local activeScenesCount = 0
|
||||
for index, scene in ipairs(nextScenes) do
|
||||
local isActive = not scene.isStale and scene.index == nextState.index
|
||||
if isActive ~= scene.isActive then
|
||||
nextScenes[index] = Cryo.Dictionary.join(scene, {
|
||||
isActive = isActive,
|
||||
})
|
||||
end
|
||||
|
||||
if isActive then
|
||||
activeScenesCount = activeScenesCount + 1
|
||||
end
|
||||
end
|
||||
|
||||
validate(activeScenesCount == 1, "There should only be one active scene, not %d", activeScenesCount)
|
||||
|
||||
-- Conditionally return nextScenes based upon shallow comparison, for performance
|
||||
if #nextScenes ~= #scenes then
|
||||
return nextScenes
|
||||
end
|
||||
|
||||
for index, scene in ipairs(nextScenes) do
|
||||
if not scenesAreShallowEqual(scenes[index], scene) then
|
||||
return nextScenes
|
||||
end
|
||||
end
|
||||
|
||||
-- Scenes have not changed
|
||||
return scenes
|
||||
end
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
local NavigationSymbol = require(script.Parent.Parent.Parent.NavigationSymbol)
|
||||
|
||||
local DEFAULT_SYMBOL = NavigationSymbol("DEFAULT")
|
||||
local MODAL_SYMBOL = NavigationSymbol("MODAL")
|
||||
local OVERLAY_SYMBOL = NavigationSymbol("OVERLAY")
|
||||
|
||||
--[[
|
||||
StackPresentationStyle is used with stack navigators/views to determine
|
||||
the behavior of a given screen when it is pushed/popped from the stack, as
|
||||
well as the visual effects/transitions applied while the view is on screen.
|
||||
]]
|
||||
return {
|
||||
--[[
|
||||
The Default presentation style draws stack cards so that they will
|
||||
slide in and out from the right side of the screen one at a time. No
|
||||
special visual effects are applied, and cards always fill the entire space
|
||||
available. Your screen content is rendered over an opaque background color
|
||||
by default, but you have the option to draw the cards with a semi- or fully
|
||||
transparent background via navigationOptions. Cards always prevent
|
||||
tap-through of the content underneath them (in case your navigation
|
||||
container is transparent).
|
||||
]]
|
||||
Default = DEFAULT_SYMBOL,
|
||||
|
||||
--[[
|
||||
The Modal presentation style causes screens to animate up/down from the
|
||||
bottom of the navigation container and visually stack on top of each
|
||||
other. Cards are opaque by default, but you may set navigationOptions
|
||||
to make them semi- or fully transparent so that you can see the underlying
|
||||
cards. Modal cards always prevent tap-through of any underlying UI, including
|
||||
other cards in the same stack.
|
||||
]]
|
||||
Modal = MODAL_SYMBOL,
|
||||
|
||||
--[[
|
||||
The Overlay presentation style causes screens to pop in (later they will fade in)
|
||||
on top of the underlying screens. Like modals, they visually stack on top of each
|
||||
other. Cards are opaque by default, but you may set navigationOptions to make
|
||||
them semi- or fully transparent. Overlay cards always prevent tap-through of any
|
||||
underlying UI, including other cards in the same stack.
|
||||
]]
|
||||
Overlay = OVERLAY_SYMBOL
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
local root = script.Parent.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
local StackActions = require(root.routers.StackActions)
|
||||
local StackViewLayout = require(script.Parent.StackViewLayout)
|
||||
local Transitioner = require(script.Parent.Transitioner)
|
||||
local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
|
||||
local defaultNavigationConfig = {
|
||||
mode = StackPresentationStyle.Default,
|
||||
}
|
||||
|
||||
|
||||
local StackView = Roact.Component:extend("StackView")
|
||||
|
||||
function StackView:init()
|
||||
self._doRender = function(...)
|
||||
return self:_render(...)
|
||||
end
|
||||
|
||||
self._doConfigureTransition = function(...)
|
||||
return self:_configureTransition(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionStart = function(...)
|
||||
return self:_onTransitionStart(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionEnd = function(...)
|
||||
return self:_onTransitionEnd(...)
|
||||
end
|
||||
|
||||
self._doOnTransitionStep = function(...)
|
||||
return self:_onTransitionStep(...)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:render()
|
||||
local screenProps = self.props.screenProps
|
||||
local navigation = self.props.navigation
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
-- Transitioner handles setting up the animation motors and making that data
|
||||
-- available to the lower layer.
|
||||
return Roact.createElement(Transitioner, {
|
||||
render = self._doRender,
|
||||
configureTransition = self._doConfigureTransition,
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
descriptors = descriptors,
|
||||
onTransitionStart = self._doOnTransitionStart,
|
||||
onTransitionEnd = self._doOnTransitionEnd,
|
||||
onTransitionStep = self._doOnTransitionStep,
|
||||
})
|
||||
end
|
||||
|
||||
function StackView:didMount()
|
||||
local navigation = self.props.navigation
|
||||
if navigation.state.isTransitioning then
|
||||
navigation.dispatch(StackActions.completeTransition({
|
||||
key = navigation.state.key,
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_render(transition, lastTransition)
|
||||
local screenProps = self.props.screenProps
|
||||
local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, self.props.navigationConfig)
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
return Roact.createElement(StackViewLayout, Cryo.Dictionary.join(navigationConfig, {
|
||||
screenProps = screenProps,
|
||||
descriptors = descriptors,
|
||||
transitionProps = transition,
|
||||
lastTransitionProps = lastTransition,
|
||||
}))
|
||||
end
|
||||
|
||||
function StackView:_configureTransition(transition, lastTransition)
|
||||
return StackViewTransitionConfigs.getTransitionConfig(
|
||||
self.props.navigationConfig.transitionConfig,
|
||||
transition,
|
||||
lastTransition,
|
||||
self.props.navigationConfig.mode
|
||||
).transitionSpec
|
||||
end
|
||||
|
||||
function StackView:_onTransitionStart(transition, lastTransition)
|
||||
local onTransitionStart = self.props.onTransitionStart
|
||||
or self.props.navigationConfig.onTransitionStart
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionStart and transition.index ~= lastTransition.index then
|
||||
onTransitionStart(transition.navigation, lastTransition.navigation)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_onTransitionEnd(transition, lastTransition)
|
||||
local navigationConfig = self.props.navigationConfig
|
||||
local navigation = self.props.navigation
|
||||
local onTransitionEnd = self.props.onTransitionEnd or navigationConfig.onTransitionEnd
|
||||
local transitionDestKey = transition.scene.route.key
|
||||
local isCurrentKey = navigation.state.routes[navigation.state.index].key == transitionDestKey
|
||||
|
||||
if transition.navigation.state.isTransitioning and isCurrentKey then
|
||||
navigation.dispatch(StackActions.completeTransition({
|
||||
key = navigation.state.key,
|
||||
toChildKey = transitionDestKey,
|
||||
}))
|
||||
end
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionEnd and transition.index ~= lastTransition.index then
|
||||
onTransitionEnd(transition.navigation, lastTransition.navigation)
|
||||
end
|
||||
end
|
||||
|
||||
function StackView:_onTransitionStep(transition, lastTransition, value)
|
||||
local onTransitionStep = self.props.onTransitionStep
|
||||
or self.props.navigationConfig.onTransitionStep
|
||||
|
||||
-- Only propagate transition changes to caller for transitions where the actual
|
||||
-- index has changed. Transitioner sends updates for _all_ transitions, including
|
||||
-- those to the same screen that result from animation completion events.
|
||||
if onTransitionStep and transition.index ~= lastTransition.index then
|
||||
onTransitionStep(transition.navigation, lastTransition.navigation, value)
|
||||
end
|
||||
end
|
||||
|
||||
return StackView
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local validate = require(script.Parent.Parent.Parent.utils.validate)
|
||||
|
||||
--[[
|
||||
Render a scene as a card for use in a StackView. This component is
|
||||
responsible for correctly positioning the scene content in relation
|
||||
to the other scenes. The content will be rendered inside a Frame
|
||||
whose position and size are controlled by the transition logic. The
|
||||
frame may either be transparent or a solid color, depending upon props.
|
||||
Any additional visual effects must be supplied by the container or the
|
||||
child element created by renderScene().
|
||||
|
||||
Props:
|
||||
renderScene(scene) -- Render prop to draw the scene inside the card.
|
||||
initialPosition -- Starting position for the card. (Animated by Otter from there).
|
||||
positionStep -- Stepper function from StackViewInterpolator.
|
||||
position -- Otter motor for the position of the card.
|
||||
scene -- Scene that the card is to render.
|
||||
forceHidden -- Forcibly disable card rendering (e.g. animated off-screen).
|
||||
transparent -- Card allows underlying content to show through (default: false).
|
||||
cardColor3 -- Color of the card background if it's not transparent (default: white).
|
||||
]]
|
||||
local StackViewCard = Roact.Component:extend("StackViewCard")
|
||||
|
||||
StackViewCard.defaultProps = {
|
||||
transparent = false,
|
||||
cardColor3 = Color3.new(1, 1, 1),
|
||||
}
|
||||
|
||||
function StackViewCard:init()
|
||||
local currentNavIndex = self.props.navigation.state.index
|
||||
|
||||
self._isMounted = false
|
||||
self._positionLastValue = currentNavIndex
|
||||
|
||||
local selfRef = Roact.createRef()
|
||||
self._getRef = function()
|
||||
return self.props[Roact.Ref] or selfRef
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:render()
|
||||
local forceHidden = self.props.forceHidden
|
||||
local cardColor3 = self.props.cardColor3
|
||||
local transparent = self.props.transparent
|
||||
local initialPosition = self.props.initialPosition
|
||||
local renderScene = self.props.renderScene
|
||||
local scene = self.props.scene
|
||||
|
||||
validate(type(renderScene) == "function", "renderScene must be a function")
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Position = initialPosition,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = cardColor3,
|
||||
BackgroundTransparency = transparent and 1 or nil,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
Visible = not forceHidden,
|
||||
[Roact.Ref] = self:_getRef(),
|
||||
}, {
|
||||
Content = renderScene(scene),
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewCard:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
local position = self.props.position
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewCard:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._positionDisconnector then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:didUpdate(oldProps)
|
||||
local position = self.props.position
|
||||
local positionStep = self.props.positionStep
|
||||
|
||||
if position ~= oldProps.position then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
if positionStep ~= oldProps.positionStep then
|
||||
-- The motor won't fire just because stepper function has changed. We have to
|
||||
-- update the position to match new requirements based upon last motor value.
|
||||
self:_onPositionStep(self._positionLastValue)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewCard:_onPositionStep(value)
|
||||
if not self._isMounted then
|
||||
return
|
||||
end
|
||||
|
||||
local positionStep = self.props.positionStep
|
||||
|
||||
if positionStep then
|
||||
positionStep(self:_getRef(), value)
|
||||
end
|
||||
|
||||
self._positionLastValue = value
|
||||
end
|
||||
|
||||
return StackViewCard
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
--[[
|
||||
Provides builders to create functions that interpolate the current Otter motor
|
||||
position into the correct translation for stack cards based upon their associated
|
||||
scene.
|
||||
|
||||
Interpolator builders expect the following props as input:
|
||||
{
|
||||
initialPositionValue = <value of position motor that transition began from>,
|
||||
scene = <scene for the particular card being animated>,
|
||||
layout = {
|
||||
initWidth = <expected width of card>,
|
||||
initHeight = <expected height of card>,
|
||||
isMeasured = <boolean: true if initWidth+Height have been measured, else false>,
|
||||
}
|
||||
}
|
||||
|
||||
Each builder returns a props table to be merged onto your other StackViewCard props, ex:
|
||||
{
|
||||
positionStep = <stepper function, or nil if not needed>,
|
||||
initialPosition = <starting position UDim2 for card based upon current active scene index>,
|
||||
forceHidden = true, -- May disable card visibility if it's outside interpolating range.
|
||||
}
|
||||
|
||||
The props table may contain other changes, depending on the requirements of the animation.
|
||||
]]
|
||||
local getSceneIndicesForInterpolationInputRange = require(
|
||||
script.Parent.Parent.Parent.utils.getSceneIndicesForInterpolationInputRange)
|
||||
local lerp = require(script.Parent.Parent.Parent.utils.lerp)
|
||||
|
||||
-- Render initial style when layout hasn't been measured yet.
|
||||
local function forInitial(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local scene = props.scene
|
||||
|
||||
local forceHidden = initialPositionValue ~= scene.index
|
||||
local translate = forceHidden and 1000000 or 0
|
||||
|
||||
return {
|
||||
forceHidden = forceHidden,
|
||||
initialPosition = UDim2.new(0, translate, 0, translate),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
-- Slide-in from right style (e.g. navigation stack view).
|
||||
local function forHorizontal(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
-- getSceneIndices* returns nil if card is not visible and need not be
|
||||
-- considered for the animation until state changes.
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local first = interpolate.first
|
||||
local last = interpolate.last
|
||||
local index = scene.index
|
||||
|
||||
local width = layout.initWidth
|
||||
|
||||
local function calculate(positionValue)
|
||||
-- 3 range LERP
|
||||
if positionValue < first then
|
||||
return width
|
||||
elseif positionValue < index then
|
||||
return lerp(width, 0, (positionValue - first) / (index - first))
|
||||
elseif positionValue == index then
|
||||
return 0
|
||||
elseif positionValue < last then
|
||||
return lerp(0, -width, (positionValue - index) / (last - index))
|
||||
else
|
||||
return -width
|
||||
end
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
local oldPosition = cardInstance.Position
|
||||
cardInstance.Position = UDim2.new(
|
||||
oldPosition.X.Scale,
|
||||
calculate(positionValue),
|
||||
oldPosition.Y.Scale,
|
||||
oldPosition.Y.Offset
|
||||
)
|
||||
end
|
||||
|
||||
local initialPosition = UDim2.new(0, calculate(initialPositionValue), 0, 0)
|
||||
|
||||
return {
|
||||
initialPosition = initialPosition,
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
-- Slide-in from bottom style (e.g. modals).
|
||||
local function forVertical(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local first = interpolate.first
|
||||
local index = scene.index
|
||||
local height = layout.initHeight
|
||||
|
||||
local function calculate(positionValue)
|
||||
-- 2 range LERP
|
||||
if positionValue < first then
|
||||
return height
|
||||
elseif positionValue < index then
|
||||
return lerp(height, 0, (positionValue - first) / (index - first))
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
local oldPosition = cardInstance.Position
|
||||
cardInstance.Position = UDim2.new(
|
||||
oldPosition.X.Scale,
|
||||
oldPosition.X.Offset,
|
||||
oldPosition.Y.Scale,
|
||||
calculate(positionValue)
|
||||
)
|
||||
end
|
||||
|
||||
local initialPosition = UDim2.new(0, 0, 0, calculate(initialPositionValue))
|
||||
|
||||
return {
|
||||
initialPosition = initialPosition,
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
-- Fade in place animation (e.g. popovers and toasts). Note that since we don't currently have
|
||||
-- group transparency, this 'animation' just pops the views in for now.
|
||||
local function forFade(props)
|
||||
local initialPositionValue = props.initialPositionValue
|
||||
local layout = props.layout
|
||||
local scene = props.scene
|
||||
|
||||
if not layout.isMeasured then
|
||||
return forInitial(props)
|
||||
end
|
||||
|
||||
local interpolate = getSceneIndicesForInterpolationInputRange(props)
|
||||
|
||||
if not interpolate then
|
||||
return {
|
||||
forceHidden = true,
|
||||
initialPosition = UDim2.new(0, 100000, 0, 100000),
|
||||
positionStep = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local index = scene.index
|
||||
|
||||
local function calculate(positionValue)
|
||||
return positionValue >= index - 0.5
|
||||
end
|
||||
|
||||
local function stepper(cardRef, positionValue)
|
||||
local cardInstance = cardRef.current
|
||||
if not cardInstance then
|
||||
return
|
||||
end
|
||||
|
||||
cardInstance.Visible = calculate(positionValue)
|
||||
end
|
||||
|
||||
return {
|
||||
forceHidden = not calculate(initialPositionValue),
|
||||
initialPosition = UDim2.new(0, 0, 0, 0),
|
||||
positionStep = stepper,
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
forHorizontal = forHorizontal,
|
||||
forVertical = forVertical,
|
||||
forFade = forFade,
|
||||
}
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
local StackViewTransitionConfigs = require(script.Parent.StackViewTransitionConfigs)
|
||||
local StackViewOverlayFrame = require(script.Parent.StackViewOverlayFrame)
|
||||
local StackViewCard = require(script.Parent.StackViewCard)
|
||||
local SceneView = require(script.Parent.Parent.SceneView)
|
||||
|
||||
local defaultScreenOptions = {
|
||||
absorbInput = true,
|
||||
overlayEnabled = false,
|
||||
overlayColor3 = Color3.new(0, 0, 0),
|
||||
overlayTransparency = 0.7,
|
||||
-- cardColor3 default is provided by StackViewCard
|
||||
renderOverlay = function(navigationOptions, initialTransitionValue, transitionChangedSignal)
|
||||
-- NOTE: renderOverlay will not be called if sceneOptions.overlayEnabled evaluates false
|
||||
return Roact.createElement(StackViewOverlayFrame, {
|
||||
navigationOptions = navigationOptions,
|
||||
initialTransitionValue = initialTransitionValue,
|
||||
transitionChangedSignal = transitionChangedSignal,
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
local function calculateTransitionValue(index, position)
|
||||
return math.max(math.min(1 + position - index, 1), 0)
|
||||
end
|
||||
|
||||
|
||||
local StackViewLayout = Roact.Component:extend("StackViewLayout")
|
||||
|
||||
function StackViewLayout:init()
|
||||
local startingIndex = self.props.transitionProps.navigation.state.index
|
||||
|
||||
self._isMounted = false
|
||||
self._positionLastValue = startingIndex
|
||||
|
||||
self._renderScene = function(scene)
|
||||
return self:_renderInnerScene(scene)
|
||||
end
|
||||
|
||||
self._subscribeToOverlayUpdates = function(callback)
|
||||
local position = self.props.transitionProps.position
|
||||
local index = self.props.transitionProps.scene.index
|
||||
|
||||
return position:onStep(function(value)
|
||||
callback(calculateTransitionValue(index, value))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:_renderCard(scene, navigationOptions)
|
||||
local transitionProps = self.props.transitionProps -- Core animation info from Transitioner.
|
||||
local lastTransitionProps = self.props.lastTransitionProps -- Previous transition info.
|
||||
local transitionConfig = self.state.transitionConfig -- State based info from scene config.
|
||||
|
||||
local cardColor3 = navigationOptions.cardColor3
|
||||
local overlayEnabled = navigationOptions.overlayEnabled
|
||||
|
||||
local initialPositionValue = transitionProps.scene.index
|
||||
if lastTransitionProps then
|
||||
initialPositionValue = lastTransitionProps.scene.index
|
||||
end
|
||||
|
||||
local cardInterpolationProps = {}
|
||||
local screenInterpolator = transitionConfig.screenInterpolator
|
||||
if screenInterpolator then
|
||||
cardInterpolationProps = screenInterpolator(
|
||||
Cryo.Dictionary.join(transitionProps, {
|
||||
initialPositionValue = initialPositionValue,
|
||||
scene = scene,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
-- Merge down the various prop packages to be applied to StackViewCard.
|
||||
return Roact.createElement(StackViewCard, Cryo.Dictionary.join(
|
||||
transitionProps, cardInterpolationProps, {
|
||||
key = "card_" .. tostring(scene.key),
|
||||
scene = scene,
|
||||
renderScene = self._renderScene,
|
||||
transparent = overlayEnabled,
|
||||
cardColor3 = cardColor3,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
function StackViewLayout:_renderInnerScene(scene)
|
||||
local navigation = scene.descriptor.navigation
|
||||
|
||||
local sceneComponent = scene.descriptor.getComponent()
|
||||
local screenProps = self.props.screenProps
|
||||
|
||||
return Roact.createElement(SceneView, {
|
||||
screenProps = screenProps,
|
||||
navigation = navigation,
|
||||
component = sceneComponent,
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewLayout:render()
|
||||
local transitionProps = self.props.transitionProps
|
||||
local topMostOpaqueSceneIndex = self.state.topMostOpaqueSceneIndex
|
||||
local scenes = transitionProps.scenes
|
||||
|
||||
local renderedScenes = Cryo.List.map(scenes, function(scene)
|
||||
-- The card is obscured if:
|
||||
-- It's not the active card (e.g. we're transitioning TO it).
|
||||
-- It's hidden underneath an opaque card that is NOT currently transitioning.
|
||||
-- It's completely off-screen.
|
||||
local cardObscured = scene.index < topMostOpaqueSceneIndex and not scene.isActive
|
||||
|
||||
local screenOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {})
|
||||
local overlayEnabled = screenOptions.overlayEnabled
|
||||
local absorbInput = screenOptions.absorbInput
|
||||
local renderOverlay = screenOptions.renderOverlay
|
||||
|
||||
local stationaryContent = nil
|
||||
if overlayEnabled then
|
||||
stationaryContent = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 1,
|
||||
}, {
|
||||
Overlay = renderOverlay(
|
||||
screenOptions,
|
||||
calculateTransitionValue(scene.index, self._positionLastValue),
|
||||
self._subscribeToOverlayUpdates)
|
||||
})
|
||||
end
|
||||
|
||||
-- Wrapper frame holds default/custom card background and the card content.
|
||||
-- It MUST be a Frame when absorbInput=false because of legacy behavior on desktop
|
||||
-- for GuiObject.Active=false blocking mouse clicks from falling through.
|
||||
-- (Active=false DOES work on mobile, but not desktop).
|
||||
-- When absorbInput=true, we add a TextButton behind the frame that will catch
|
||||
-- mouse clicks
|
||||
local absorbInputElement = nil
|
||||
if not cardObscured and absorbInput then
|
||||
absorbInputElement = Roact.createElement("TextButton", {
|
||||
Active = true,
|
||||
AutoButtonColor = false,
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
Text = " ",
|
||||
ZIndex = 2 * scene.index - 1,
|
||||
})
|
||||
end
|
||||
|
||||
return Roact.createFragment({
|
||||
AbsorbInput = absorbInputElement,
|
||||
-- use scene index for key, it makes testing with Rhodium easier
|
||||
[tostring(scene.index)] = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
ZIndex = 2 * scene.index,
|
||||
Visible = not cardObscured,
|
||||
}, {
|
||||
StationaryContent = stationaryContent,
|
||||
DynamicContent = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
ZIndex = 2,
|
||||
}, {
|
||||
-- Cards need to have unique keys so that instances of the same components are not
|
||||
-- reused for different scenes. (Could lead to unanticipated lifecycle problems).
|
||||
["card_" .. scene.key] = self:_renderCard(scene, screenOptions),
|
||||
})
|
||||
}),
|
||||
})
|
||||
end)
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
}, renderedScenes)
|
||||
end
|
||||
|
||||
function StackViewLayout.getDerivedStateFromProps(nextProps, lastState)
|
||||
local transitionProps = nextProps.transitionProps
|
||||
local scenes = transitionProps.scenes
|
||||
local state = transitionProps.navigation.state
|
||||
local isTransitioning = state.isTransitioning
|
||||
local topMostIndex = #scenes
|
||||
|
||||
local isOverlayMode = nextProps.mode == StackPresentationStyle.Modal or
|
||||
nextProps.mode == StackPresentationStyle.Overlay
|
||||
|
||||
-- Find the last opaque scene in a modal stack so that we can optimize rendering.
|
||||
local topMostOpaqueSceneIndex = 0
|
||||
if isOverlayMode then
|
||||
for idx = topMostIndex, 1, -1 do
|
||||
local scene = scenes[idx]
|
||||
local navigationOptions = Cryo.Dictionary.join(defaultScreenOptions, scene.descriptor.options or {})
|
||||
|
||||
-- Card covers other pages if it's not an overlay and it's not the top-most index while transitioning.
|
||||
if not navigationOptions.overlayEnabled and not (isTransitioning and idx == topMostIndex) then
|
||||
topMostOpaqueSceneIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
for idx = topMostIndex, 1, -1 do
|
||||
if not (isTransitioning and idx == topMostIndex) then
|
||||
topMostOpaqueSceneIndex = idx
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
topMostOpaqueSceneIndex = topMostOpaqueSceneIndex,
|
||||
transitionConfig = StackViewTransitionConfigs.getTransitionConfig(
|
||||
nextProps.transitionConfig,
|
||||
nextProps.transitionProps,
|
||||
nextProps.lastTransitionProps,
|
||||
nextProps.mode),
|
||||
}
|
||||
end
|
||||
|
||||
function StackViewLayout:didMount()
|
||||
self._isMounted = true
|
||||
|
||||
self._positionDisconnector = self.props.transitionProps.position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewLayout:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._positionDisconnector then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:didUpdate(oldProps)
|
||||
local position = self.props.transitionProps.position
|
||||
|
||||
if position ~= oldProps.transitionProps.position then
|
||||
self._positionDisconnector()
|
||||
self._positionDisconnector = position:onStep(function(...)
|
||||
self:_onPositionStep(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewLayout:_onPositionStep(value)
|
||||
if self._isMounted then
|
||||
self._positionLastValue = value
|
||||
end
|
||||
end
|
||||
|
||||
return StackViewLayout
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Roact)
|
||||
local lerp = require(script.Parent.Parent.Parent.utils.lerp)
|
||||
|
||||
local StackViewOverlayFrame = Roact.Component:extend("StackViewOverlayFrame")
|
||||
|
||||
function StackViewOverlayFrame:init()
|
||||
self._signalDisconnect = nil
|
||||
|
||||
local selfRef = Roact.createRef()
|
||||
self._getRef = function()
|
||||
return self.props[Roact.Ref] or selfRef
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:render()
|
||||
local navigationOptions = self.props.navigationOptions
|
||||
local initialTransitionValue = self.props.initialTransitionValue
|
||||
|
||||
local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, initialTransitionValue)
|
||||
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundColor3 = navigationOptions.overlayColor3,
|
||||
BackgroundTransparency = overlayTransparency,
|
||||
BorderSizePixel = 0,
|
||||
[Roact.Ref] = self:_getRef(),
|
||||
})
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:didUpdate(oldProps)
|
||||
local transitionChangedSignal = self.props.transitionChangedSignal
|
||||
|
||||
if transitionChangedSignal ~= oldProps.transitionChangedSignal then
|
||||
if self._signalDisconnect then
|
||||
self._signalDisconnect()
|
||||
end
|
||||
|
||||
self._signalDisconnect = transitionChangedSignal(function(...)
|
||||
self:_transitionChanged(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:didMount()
|
||||
self._isMounted = true
|
||||
self._signalDisconnect = self.props.transitionChangedSignal(function(...)
|
||||
self:_transitionChanged(...)
|
||||
end)
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:willUnmount()
|
||||
self._isMounted = false
|
||||
if self._signalDisconnect then
|
||||
self._signalDisconnect()
|
||||
end
|
||||
end
|
||||
|
||||
function StackViewOverlayFrame:_transitionChanged(value)
|
||||
if not self._isMounted then
|
||||
return
|
||||
end
|
||||
|
||||
local myRef = self:_getRef()
|
||||
if myRef.current then
|
||||
local navigationOptions = self.props.navigationOptions
|
||||
local overlayTransparency = lerp(1, navigationOptions.overlayTransparency, value)
|
||||
myRef.current.BackgroundTransparency = overlayTransparency
|
||||
end
|
||||
end
|
||||
|
||||
return StackViewOverlayFrame
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Parent.Cryo)
|
||||
local StackViewInterpolator = require(script.Parent.StackViewInterpolator)
|
||||
local StackPresentationStyle = require(script.Parent.StackPresentationStyle)
|
||||
|
||||
local DefaultTransitionSpec = {
|
||||
frequency = 3, -- Hz
|
||||
dampingRatio = 1,
|
||||
}
|
||||
|
||||
local SlideFromRight = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forHorizontal,
|
||||
}
|
||||
|
||||
local ModalSlideFromBottom = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forVertical,
|
||||
}
|
||||
|
||||
local FadeInPlace = {
|
||||
transitionSpec = DefaultTransitionSpec,
|
||||
screenInterpolator = StackViewInterpolator.forFade,
|
||||
}
|
||||
|
||||
local function getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle)
|
||||
if presentationStyle == StackPresentationStyle.Modal then
|
||||
return ModalSlideFromBottom
|
||||
elseif presentationStyle == StackPresentationStyle.Overlay then
|
||||
return FadeInPlace
|
||||
else
|
||||
return SlideFromRight
|
||||
end
|
||||
end
|
||||
|
||||
local function getTransitionConfig(transitionConfigurer, transitionProps, prevTransitionProps, presentationStyle)
|
||||
local defaultConfig = getDefaultTransitionConfig(transitionProps, prevTransitionProps, presentationStyle)
|
||||
if transitionConfigurer then
|
||||
return Cryo.Dictionary.join(
|
||||
defaultConfig,
|
||||
transitionConfigurer(transitionProps, prevTransitionProps, presentationStyle)
|
||||
)
|
||||
end
|
||||
|
||||
return defaultConfig
|
||||
end
|
||||
|
||||
return {
|
||||
getDefaultTransitionConfig = getDefaultTransitionConfig,
|
||||
getTransitionConfig = getTransitionConfig,
|
||||
SlideFromRight = SlideFromRight,
|
||||
ModalSlideFromBottom = ModalSlideFromBottom,
|
||||
FadeInPlace = FadeInPlace,
|
||||
}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
local root = script.Parent.Parent.Parent
|
||||
local Packages = root.Parent
|
||||
local Cryo = require(Packages.Cryo)
|
||||
local Roact = require(Packages.Roact)
|
||||
local Otter = require(Packages.Otter)
|
||||
local ScenesReducer = require(script.Parent.ScenesReducer)
|
||||
local validate = require(root.utils.validate)
|
||||
|
||||
local DEFAULT_TRANSITION_SPEC = {
|
||||
frequency = 4 -- Hz
|
||||
}
|
||||
|
||||
local function buildTransitionProps(props, state)
|
||||
local navigation = props.navigation
|
||||
local options = props.options
|
||||
|
||||
local layout = state.layout
|
||||
local position = state.position
|
||||
local scenes = state.scenes
|
||||
|
||||
local activeScene
|
||||
for _, x in ipairs(scenes) do
|
||||
if x.isActive then
|
||||
activeScene = x
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
validate(activeScene, "Could not find active scene")
|
||||
|
||||
return {
|
||||
layout = layout,
|
||||
navigation = navigation,
|
||||
position = position,
|
||||
scenes = scenes,
|
||||
scene = activeScene,
|
||||
options = options,
|
||||
index = activeScene.index,
|
||||
}
|
||||
end
|
||||
|
||||
local function filterStale(scenes)
|
||||
local filtered = Cryo.List.filter(scenes, function(scene)
|
||||
return not scene.isStale
|
||||
end)
|
||||
|
||||
if #filtered == #scenes then
|
||||
return scenes
|
||||
else
|
||||
return filtered
|
||||
end
|
||||
end
|
||||
|
||||
local Transitioner = Roact.Component:extend("Transitioner")
|
||||
|
||||
function Transitioner:init()
|
||||
local navigationState = self.props.navigation.state
|
||||
local descriptors = self.props.descriptors
|
||||
|
||||
self.state = {
|
||||
-- Layout is passed to StackViewLayout in order to allow it to
|
||||
-- sync animations.
|
||||
layout = {
|
||||
initWidth = 0,
|
||||
initHeight = 0,
|
||||
isMeasured = false,
|
||||
},
|
||||
position = Otter.createSingleMotor(navigationState.index),
|
||||
scenes = ScenesReducer({}, navigationState, nil, descriptors),
|
||||
}
|
||||
|
||||
self._doOnAbsoluteSizeChanged = function(...)
|
||||
return self:_onAbsoluteSizeChanged(...)
|
||||
end
|
||||
|
||||
self._positionLastValue = navigationState.index
|
||||
|
||||
self._prevTransitionProps = nil
|
||||
self._transitionProps = buildTransitionProps(self.props, self.state)
|
||||
|
||||
self._isMounted = false
|
||||
self._isTransitionRunning = false
|
||||
self._transitionQueue = {}
|
||||
|
||||
self._completeSignalDisconnector = self.state.position:onComplete(function()
|
||||
-- This spawn is required because of this Otter bug: https://github.com/Roblox/otter/issues/26
|
||||
-- Otter.SingleMotor's step function calls onComplete before it calls stop(). This leaves their
|
||||
-- __running=true, and the setGoal() in our _onTransitionEnd() does nothing. So the whole queue
|
||||
-- handling just stops cold, and the transition never actually happens!
|
||||
spawn(function()
|
||||
if self._isMounted then
|
||||
self:_onTransitionEnd()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
self._stepSignalDisconnector = self.state.position:onStep(function(value)
|
||||
if self._isMounted then
|
||||
self:_onPositionStep(value)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Transitioner:didMount()
|
||||
self._isMounted = true
|
||||
end
|
||||
|
||||
function Transitioner:willUnmount()
|
||||
self._isMounted = false
|
||||
|
||||
if self._completeSignalDisconnector then
|
||||
self._completeSignalDisconnector()
|
||||
self._completeSignalDisconnector = nil
|
||||
end
|
||||
|
||||
if self._stepSignalDisconnector then
|
||||
self._stepSignalDisconnector()
|
||||
self._stepSignalDisconnector = nil
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:didUpdate(prevProps)
|
||||
-- React-navigation uses componentWillReceiveProps that is only called when Parent
|
||||
-- re-renders or when this component is actually being given new props, so we need to
|
||||
-- filter here. If not, this would trigger on setState and enter an infinite loop.
|
||||
if self.props ~= prevProps then
|
||||
if self._isTransitionRunning then
|
||||
local mostRecentTransition = self._transitionQueue[#self._transitionQueue] or {}
|
||||
-- don't enqueue spurious extra copies of same transition props
|
||||
if mostRecentTransition.prevProps ~= prevProps then
|
||||
table.insert(self._transitionQueue, { prevProps = prevProps })
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
self:_startTransition(prevProps, self.props)
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:render()
|
||||
return Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
BorderSizePixel = 0,
|
||||
ClipsDescendants = true,
|
||||
[Roact.Change.AbsoluteSize] = self._doOnAbsoluteSizeChanged,
|
||||
}, {
|
||||
["TransitionerScenes"] = self.props.render(
|
||||
self._transitionProps, self._prevTransitionProps),
|
||||
})
|
||||
end
|
||||
|
||||
-- equivalent to React-Nav's Transitioner._onLayout
|
||||
function Transitioner:_onAbsoluteSizeChanged(rbx)
|
||||
local width = rbx.AbsoluteSize.X
|
||||
local height = rbx.AbsoluteSize.Y
|
||||
|
||||
if width == self.state.layout.initWidth and
|
||||
height == self.state.layout.initHeight then
|
||||
return
|
||||
end
|
||||
|
||||
local layout = Cryo.Dictionary.join(self.state.layout, {
|
||||
initWidth = width,
|
||||
initHeight = height,
|
||||
isMeasured = true,
|
||||
})
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
layout = layout,
|
||||
})
|
||||
|
||||
self._transitionProps = buildTransitionProps(self.props, nextState)
|
||||
|
||||
self:setState({
|
||||
layout = layout,
|
||||
})
|
||||
end
|
||||
|
||||
function Transitioner:_computeScenes(props, nextProps)
|
||||
local nextScenes = ScenesReducer(
|
||||
self.state.scenes,
|
||||
nextProps.navigation.state,
|
||||
props.navigation.state,
|
||||
nextProps.descriptors)
|
||||
|
||||
if not nextProps.navigation.state.isTransitioning then
|
||||
nextScenes = filterStale(nextScenes)
|
||||
end
|
||||
|
||||
if nextScenes == self.state.scenes then
|
||||
return nil
|
||||
end
|
||||
|
||||
return nextScenes
|
||||
end
|
||||
|
||||
function Transitioner:_startTransition(props, nextProps)
|
||||
local indexHasChanged = props.navigation.state.index ~= nextProps.navigation.state.index
|
||||
local nextScenes = self:_computeScenes(props, nextProps)
|
||||
|
||||
if not nextScenes then
|
||||
-- If nextScenes is nil, nothing has changed, so report transition end, then bail
|
||||
self._prevTransitionProps = self._transitionProps
|
||||
|
||||
-- Trigger end transition logic if we daisy-chained from queued transitions via _onTransitionEnd.
|
||||
if self._isTransitionRunning then
|
||||
self:_onTransitionEnd()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
scenes = nextScenes,
|
||||
})
|
||||
|
||||
local position = nextState.position
|
||||
local toValue = nextProps.navigation.state.index
|
||||
|
||||
-- compute transitionProps
|
||||
self._prevTransitionProps = self._transitionProps
|
||||
self._transitionProps = buildTransitionProps(nextProps, nextState)
|
||||
local isTransitioning = self._transitionProps.navigation.state.isTransitioning
|
||||
|
||||
if not isTransitioning or not indexHasChanged then
|
||||
-- If state is not transitioning, then we go immediately to new index.
|
||||
-- Likewise, if the index has not changed then we still need to set up initial
|
||||
-- positions via setState.
|
||||
self:setState(nextState)
|
||||
|
||||
if nextProps.onTransitionStart then
|
||||
nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps)
|
||||
end
|
||||
|
||||
-- motor will call _endTransition for us
|
||||
position:setGoal(Otter.instant(toValue))
|
||||
elseif isTransitioning then
|
||||
self._isTransitionRunning = true
|
||||
self:setState(nextState)
|
||||
|
||||
if nextProps.onTransitionStart then
|
||||
nextProps.onTransitionStart(self._transitionProps, self._prevTransitionProps)
|
||||
end
|
||||
|
||||
local positionHasChanged = self._positionLastValue ~= toValue
|
||||
if indexHasChanged and positionHasChanged then
|
||||
-- get transition spec
|
||||
local transitionUserSpec = {}
|
||||
if nextProps.configureTransition then
|
||||
transitionUserSpec = nextProps.configureTransition(
|
||||
self._transitionProps, self._prevTransitionProps) or {}
|
||||
end
|
||||
|
||||
local transitionSpec = Cryo.Dictionary.join(DEFAULT_TRANSITION_SPEC, transitionUserSpec)
|
||||
|
||||
-- motor will call _endTransition for us
|
||||
position:setGoal(Otter.spring(nextProps.navigation.state.index, transitionSpec))
|
||||
else
|
||||
-- Set motor to current state to trigger _endTransition call with correct sequencing.
|
||||
position:setGoal(Otter.instant(nextProps.navigation.state.index))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:_onTransitionEnd()
|
||||
local prevTransitionProps = self._prevTransitionProps
|
||||
self._prevTransitionProps = nil
|
||||
|
||||
local scenes = filterStale(self.state.scenes)
|
||||
|
||||
local nextState = Cryo.Dictionary.join(self.state, {
|
||||
scenes = scenes,
|
||||
})
|
||||
|
||||
self._transitionProps = buildTransitionProps(self.props, nextState)
|
||||
|
||||
self:setState(nextState)
|
||||
|
||||
if self.props.onTransitionEnd then
|
||||
self.props.onTransitionEnd(self._transitionProps, prevTransitionProps)
|
||||
end
|
||||
|
||||
local firstQueuedTransition = self._transitionQueue[1]
|
||||
if firstQueuedTransition then
|
||||
local prevProps = firstQueuedTransition.prevProps
|
||||
self._transitionQueue = Cryo.List.removeIndex(self._transitionQueue, 1)
|
||||
self:_startTransition(prevProps, self.props)
|
||||
else
|
||||
self._isTransitionRunning = false
|
||||
end
|
||||
end
|
||||
|
||||
function Transitioner:_onPositionStep(value)
|
||||
self._positionLastValue = value
|
||||
|
||||
local targetIndex = self._transitionProps.index
|
||||
|
||||
-- _prevTransitionProps can be nil, so guard against it.
|
||||
local startingIndex = targetIndex
|
||||
if self._prevTransitionProps then
|
||||
startingIndex = self._prevTransitionProps.index
|
||||
end
|
||||
|
||||
if self.props.onTransitionStep and startingIndex ~= targetIndex then
|
||||
local transitionValue = (value - startingIndex) / (targetIndex - startingIndex)
|
||||
self.props.onTransitionStep(self._transitionProps, self._prevTransitionProps, transitionValue)
|
||||
end
|
||||
end
|
||||
|
||||
return Transitioner
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
return function()
|
||||
-- ScenesReducer(scenes, nextState, prevState, descriptors)
|
||||
local ScenesReducer = require(script.Parent.Parent.ScenesReducer)
|
||||
|
||||
local initialRouteKey = "id-1"
|
||||
local initialRouteName = "First Route"
|
||||
local initialRoute = {
|
||||
routeName = initialRouteName,
|
||||
key = initialRouteKey,
|
||||
}
|
||||
|
||||
local initialState = {
|
||||
index = 1,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = false,
|
||||
routes = {
|
||||
initialRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local initialDescriptors = {
|
||||
[initialRouteKey] = {
|
||||
key = initialRouteKey,
|
||||
navigation = {
|
||||
state = initialRoute,
|
||||
},
|
||||
state = initialRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local initialScenes = nil
|
||||
|
||||
it("should generate valid initial scene", function()
|
||||
local scenes = ScenesReducer({}, initialState, nil, initialDescriptors)
|
||||
local sceneOne = scenes[1]
|
||||
expect(#scenes).to.be.equal(1)
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
initialScenes = scenes
|
||||
end)
|
||||
|
||||
it("should update descriptor", function()
|
||||
local scenes = ScenesReducer({}, initialState, nil, initialDescriptors)
|
||||
local dummyDescriptor = { key = "this is a dummy descriptor" }
|
||||
scenes[1].descriptor = dummyDescriptor
|
||||
local updatedScenes = ScenesReducer(scenes, initialState, initialState, initialDescriptors)
|
||||
expect(#updatedScenes).to.be.equal(1)
|
||||
local sceneOne = updatedScenes[1]
|
||||
expect(sceneOne.descriptor).to.never.be.equal(dummyDescriptor)
|
||||
expect(sceneOne.descriptor).to.be.equal(initialDescriptors[initialRouteKey])
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
end)
|
||||
|
||||
it("should bail out early", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(initialScenes, nil, nil, nil)
|
||||
expect(scenes).to.be.equal(initialScenes)
|
||||
scenes = ScenesReducer(initialScenes, initialState, initialState, initialDescriptors)
|
||||
expect(scenes).to.be.equal(initialScenes)
|
||||
end)
|
||||
|
||||
local secondRouteKey = "id-2"
|
||||
local secondRouteName = "Second Route"
|
||||
local secondRoute = {
|
||||
key = secondRouteKey,
|
||||
routeName = secondRouteName,
|
||||
}
|
||||
|
||||
local secondState = {
|
||||
index = 2,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local secondDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] ={
|
||||
key = secondRouteKey,
|
||||
navigation = {
|
||||
state = secondRoute,
|
||||
},
|
||||
state = secondRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local secondScenes = nil
|
||||
|
||||
it("should add second scene", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(initialScenes, secondState, initialState, secondDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(2)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(true)
|
||||
expect(sceneTwo.isStale).to.be.equal(false)
|
||||
|
||||
secondScenes = scenes
|
||||
end)
|
||||
|
||||
local thirdRouteKey = "id-3"
|
||||
local thirdRouteName = "Third Route"
|
||||
local thirdRoute = {
|
||||
key = thirdRouteKey,
|
||||
routeName = thirdRouteName,
|
||||
}
|
||||
|
||||
local thirdState = {
|
||||
index = 3,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondRoute,
|
||||
thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local thirdDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] = secondDescriptors[secondRouteKey],
|
||||
[thirdRouteKey] = {
|
||||
key = thirdRouteKey,
|
||||
navigation = {
|
||||
state = thirdRoute,
|
||||
},
|
||||
state = thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local thirdScenes = nil
|
||||
|
||||
it("should add third scene", function()
|
||||
expect(initialScenes).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(secondScenes, thirdState, secondState, thirdDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(3)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(false)
|
||||
|
||||
local sceneThree = scenes[3]
|
||||
expect(sceneThree.route).to.be.equal(thirdRoute)
|
||||
expect(sceneThree.index).to.be.equal(3)
|
||||
expect(sceneThree.isActive).to.be.equal(true)
|
||||
expect(sceneThree.isStale).to.be.equal(false)
|
||||
|
||||
thirdScenes = scenes
|
||||
end)
|
||||
|
||||
it("should mark removed scenes as stale", function()
|
||||
expect(secondState).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(thirdScenes, initialState, thirdState, initialDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(3)
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(true)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(true)
|
||||
|
||||
local sceneThree = scenes[3]
|
||||
expect(sceneThree.route).to.be.equal(thirdRoute)
|
||||
expect(sceneThree.index).to.be.equal(3)
|
||||
expect(sceneThree.isActive).to.be.equal(false)
|
||||
expect(sceneThree.isStale).to.be.equal(true)
|
||||
end)
|
||||
|
||||
local secondScreenReplacementKey = "id-22"
|
||||
local secondScreenReplacementName = "Second Route Replacement"
|
||||
local secondScreenReplacementRoute = {
|
||||
key = secondScreenReplacementKey,
|
||||
routeName = secondScreenReplacementName,
|
||||
}
|
||||
|
||||
local replacedSecondSceneState = {
|
||||
index = 3,
|
||||
key = "StackRouterRoot",
|
||||
isTransitioning = true,
|
||||
routes = {
|
||||
initialRoute,
|
||||
secondScreenReplacementRoute,
|
||||
thirdRoute,
|
||||
},
|
||||
}
|
||||
|
||||
local replacedSceneDescriptors = {
|
||||
[initialRouteKey] = initialDescriptors[initialRouteKey],
|
||||
[secondRouteKey] = {
|
||||
key = secondScreenReplacementKey,
|
||||
navigation = {
|
||||
state = secondScreenReplacementRoute
|
||||
},
|
||||
state = secondScreenReplacementRoute,
|
||||
},
|
||||
[thirdRouteKey] = thirdDescriptors[thirdRouteKey],
|
||||
}
|
||||
|
||||
it("should mark replaced scene as stale", function()
|
||||
expect(secondState).to.never.be.equal(nil)
|
||||
local scenes = ScenesReducer(thirdScenes, replacedSecondSceneState, thirdState, replacedSceneDescriptors)
|
||||
expect(scenes).to.never.be.equal(initialScenes)
|
||||
expect(#scenes).to.be.equal(4) -- replaced scene is marked stale, it is not removed
|
||||
|
||||
local sceneOne = scenes[1]
|
||||
expect(sceneOne.route).to.be.equal(initialRoute)
|
||||
expect(sceneOne.index).to.be.equal(1)
|
||||
expect(sceneOne.isActive).to.be.equal(false)
|
||||
expect(sceneOne.isStale).to.be.equal(false)
|
||||
|
||||
local sceneTwo = scenes[2]
|
||||
expect(sceneTwo.route).to.be.equal(secondRoute)
|
||||
expect(sceneTwo.index).to.be.equal(2)
|
||||
expect(sceneTwo.isActive).to.be.equal(false)
|
||||
expect(sceneTwo.isStale).to.be.equal(true)
|
||||
|
||||
-- because of comparison algorithm in SceneReducer.lua compareScenes
|
||||
-- the replacement scene is after the scene it replaced because id-2 < id-22
|
||||
local sceneTwoReplacement = scenes[3]
|
||||
expect(sceneTwoReplacement.route).to.be.equal(secondScreenReplacementRoute)
|
||||
-- index is still 2 because the scene index come from route index in the nextState
|
||||
-- this is ok because filterStale in Transitioner.lua will remove the stale scene
|
||||
expect(sceneTwoReplacement.index).to.be.equal(2)
|
||||
expect(sceneTwoReplacement.isActive).to.be.equal(false)
|
||||
expect(sceneTwoReplacement.isStale).to.be.equal(false)
|
||||
|
||||
local sceneThree = scenes[4]
|
||||
expect(sceneThree.route).to.be.equal(thirdRoute)
|
||||
expect(sceneThree.index).to.be.equal(3)
|
||||
expect(sceneThree.isActive).to.be.equal(true)
|
||||
expect(sceneThree.isStale).to.be.equal(false)
|
||||
end)
|
||||
end
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
return function()
|
||||
local StackPresentationStyle = require(script.Parent.Parent.StackPresentationStyle)
|
||||
|
||||
describe("StackPresentationStyle token tests", function()
|
||||
it("should return same object for each token for multiple calls", function()
|
||||
expect(StackPresentationStyle.Default).to.equal(StackPresentationStyle.Default)
|
||||
expect(StackPresentationStyle.Modal).to.equal(StackPresentationStyle.Modal)
|
||||
expect(StackPresentationStyle.Overlay).to.equal(StackPresentationStyle.Overlay)
|
||||
end)
|
||||
|
||||
it("should return matching string names for symbols", function()
|
||||
expect(tostring(StackPresentationStyle.Default)).to.equal("DEFAULT")
|
||||
expect(tostring(StackPresentationStyle.Modal)).to.equal("MODAL")
|
||||
expect(tostring(StackPresentationStyle.Overlay)).to.equal("OVERLAY")
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
return function()
|
||||
local Otter = require(script.Parent.Parent.Parent.Parent.Parent.Otter)
|
||||
local Roact = require(script.Parent.Parent.Parent.Parent.Parent.Roact)
|
||||
local StackViewCard = require(script.Parent.Parent.StackViewCard)
|
||||
|
||||
it("should mount its renderProp and pass it scene", function()
|
||||
local didRender = false
|
||||
local testScene = {
|
||||
isActive = true,
|
||||
index = 1,
|
||||
}
|
||||
|
||||
local renderedScene = nil
|
||||
local element = Roact.createElement(StackViewCard, {
|
||||
renderScene = function(theScene)
|
||||
renderedScene = theScene
|
||||
return Roact.createElement(function()
|
||||
didRender = true -- verifies component is attached to tree
|
||||
end)
|
||||
end,
|
||||
scene = testScene,
|
||||
position = Otter.createSingleMotor(1),
|
||||
navigation = {
|
||||
state = {
|
||||
index = 1,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
local instance = Roact.mount(element)
|
||||
Roact.unmount(instance)
|
||||
|
||||
expect(renderedScene).to.equal(testScene)
|
||||
expect(didRender).to.equal(true)
|
||||
end)
|
||||
end
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
local Cryo = require(script.Parent.Parent.Parent.Cryo)
|
||||
local Roact = require(script.Parent.Parent.Parent.Roact)
|
||||
local SceneView = require(script.Parent.SceneView)
|
||||
|
||||
local defaultNavigationConfig = {
|
||||
keepVisitedScreensMounted = false,
|
||||
}
|
||||
|
||||
local RobloxSwitchView = Roact.Component:extend("RobloxSwitchView")
|
||||
|
||||
function RobloxSwitchView.getDerivedStateFromProps(nextProps, prevState)
|
||||
local navState = nextProps.navigation.state
|
||||
local activeKey = navState.routes[navState.index].key
|
||||
local descriptors = nextProps.descriptors
|
||||
|
||||
local navigationConfig = Cryo.Dictionary.join(defaultNavigationConfig, nextProps.navigationConfig or {})
|
||||
local keepVisitedScreensMounted = navigationConfig.keepVisitedScreensMounted
|
||||
|
||||
local visitedScreenKeys = {
|
||||
[activeKey] = true
|
||||
}
|
||||
|
||||
if keepVisitedScreensMounted then
|
||||
-- prune visited screen keys if they are not included in incoming descriptors
|
||||
for prevKey in pairs(prevState.visitedScreenKeys or {}) do
|
||||
if descriptors[prevKey] ~= nil then
|
||||
visitedScreenKeys[prevKey] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
visitedScreenKeys = visitedScreenKeys,
|
||||
}
|
||||
end
|
||||
|
||||
function RobloxSwitchView:render()
|
||||
local navState = self.props.navigation.state
|
||||
local screenProps = self.props.screenProps
|
||||
local descriptors = self.props.descriptors
|
||||
local visitedScreenKeys = self.state.visitedScreenKeys
|
||||
local activeKey = navState.routes[navState.index].key
|
||||
|
||||
local screenElements = {}
|
||||
for key, descriptor in pairs(descriptors) do
|
||||
local isActiveKey = (key == activeKey)
|
||||
|
||||
if visitedScreenKeys[key] == true then
|
||||
screenElements["card_" .. key] = Roact.createElement("Frame", {
|
||||
Size = UDim2.new(1, 0, 1, 0),
|
||||
BackgroundTransparency = 1,
|
||||
ClipsDescendants = true,
|
||||
BorderSizePixel = 0,
|
||||
Visible = isActiveKey,
|
||||
}, {
|
||||
Content = Roact.createElement(SceneView, {
|
||||
component = descriptor.getComponent(),
|
||||
navigation = descriptor.navigation,
|
||||
screenProps = screenProps,
|
||||
})
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return Roact.createElement("Folder", nil, screenElements)
|
||||
end
|
||||
|
||||
return RobloxSwitchView
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user