mirror of
https://github.com/re-rxjs/react-rxjs.git
synced 2025-12-08 18:01:51 +00:00
test: add tests for combineStates
This commit is contained in:
parent
cf0ce6d2e6
commit
1c3cc752b7
114
packages/context-state/src/combineStates.test.ts
Normal file
114
packages/context-state/src/combineStates.test.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { createRoot } from "./create-root"
|
||||
import { BehaviorSubject, of, Subject } from "rxjs"
|
||||
import { substate } from "./substate"
|
||||
import { combineStates } from "./combineStates"
|
||||
import { StateNode } from "./types"
|
||||
import { routeState } from "./route-state"
|
||||
|
||||
describe("combineStates", () => {
|
||||
it("combines state nodes into one", () => {
|
||||
const root = createRoot()
|
||||
const nodeA = substate(root, () => of("a"))
|
||||
const nodeB = substate(root, () => of("b"))
|
||||
const nodeC = substate(root, () => of("c"))
|
||||
|
||||
const combined = combineStates({ nodeA, nodeB, nodeC })
|
||||
root.run()
|
||||
|
||||
expect(combined.getValue()).toEqual({ nodeA: "a", nodeB: "b", nodeC: "c" })
|
||||
})
|
||||
|
||||
it("substates can access the context of either branch", () => {
|
||||
const root = createRoot()
|
||||
const contextA = substate(root, () => of("context A"))
|
||||
const nodeA = substate(contextA, () => of("a"))
|
||||
const contextB = substate(root, () => of("context B"))
|
||||
const nodeB = substate(contextB, () => of("b"))
|
||||
|
||||
const combined = combineStates({ nodeA, nodeB })
|
||||
const result: string[] = []
|
||||
substate(combined, (ctx) => {
|
||||
// Can't put assertions here because errors get captured and not emitted on globalThis
|
||||
result.push(ctx(contextA))
|
||||
result.push(ctx(contextB))
|
||||
return of("substate")
|
||||
})
|
||||
root.run()
|
||||
|
||||
expect(result).toEqual(["context A", "context B"])
|
||||
})
|
||||
|
||||
it("only activates if all branches are active", () => {
|
||||
function createActivableNode(root: StateNode<any>) {
|
||||
const ctxSource = new BehaviorSubject(false)
|
||||
const ctxNode = substate(root, () => ctxSource)
|
||||
const [, { node }] = routeState(
|
||||
ctxNode,
|
||||
{
|
||||
other: null,
|
||||
node: null,
|
||||
},
|
||||
(value) => (value ? "node" : "other"),
|
||||
)
|
||||
return [node, (active: boolean) => ctxSource.next(active)] as const
|
||||
}
|
||||
|
||||
const root = createRoot()
|
||||
const [nodeA, activateA] = createActivableNode(root)
|
||||
const [nodeB, activateB] = createActivableNode(root)
|
||||
const combined = combineStates({ nodeA, nodeB })
|
||||
root.run()
|
||||
|
||||
expect(() => combined.getValue()).toThrowError("Inactive Context")
|
||||
activateA(true)
|
||||
expect(() => combined.getValue()).toThrowError("Inactive Context")
|
||||
activateA(false)
|
||||
activateB(true)
|
||||
expect(() => combined.getValue()).toThrowError("Inactive Context")
|
||||
activateA(true)
|
||||
|
||||
expect(combined.getValue()).toEqual({
|
||||
nodeA: true,
|
||||
nodeB: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("doesn't emit a value until all branches have one", async () => {
|
||||
function createSettableNode(root: StateNode<any>) {
|
||||
const source = new Subject()
|
||||
const node = substate(root, () => source)
|
||||
return [node, (value: any) => source.next(value)] as const
|
||||
}
|
||||
|
||||
const root = createRoot()
|
||||
const [nodeA, setA] = createSettableNode(root)
|
||||
const [nodeB, setB] = createSettableNode(root)
|
||||
const combined = combineStates({ nodeA, nodeB })
|
||||
root.run()
|
||||
|
||||
const promise = combined.getValue()
|
||||
expect(promise).toBeInstanceOf(Promise)
|
||||
|
||||
const next = jest.fn()
|
||||
const complete = jest.fn()
|
||||
combined.state$().subscribe({ next, complete })
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
expect(complete).not.toHaveBeenCalled()
|
||||
|
||||
setA("a")
|
||||
expect(next).not.toHaveBeenCalled()
|
||||
|
||||
setB("b")
|
||||
expect(next).toHaveBeenCalledWith({ nodeA: "a", nodeB: "b" })
|
||||
expect(complete).not.toHaveBeenCalled()
|
||||
|
||||
await expect(promise).resolves.toEqual({ nodeA: "a", nodeB: "b" })
|
||||
|
||||
next.mockReset()
|
||||
setA("a2")
|
||||
expect(next).toHaveBeenCalledWith({ nodeA: "a2", nodeB: "b" })
|
||||
expect(complete).not.toHaveBeenCalled()
|
||||
|
||||
expect(combined.getValue()).toEqual({ nodeA: "a2", nodeB: "b" })
|
||||
})
|
||||
})
|
||||
@ -16,10 +16,19 @@ type StringRecordNodeToNodeStringRecord<
|
||||
[K in keyof States]: States[K] extends StateNode<infer V> ? V : never
|
||||
}>
|
||||
|
||||
interface CombinedStateInstance {
|
||||
inactiveStates: number
|
||||
emptyStates: number
|
||||
activeStates: Record<string, boolean>
|
||||
loadedStates: Record<string, boolean>
|
||||
latestIsActive: boolean | null
|
||||
latestIsLoaded: boolean | null
|
||||
}
|
||||
|
||||
export const combineStates = <States extends StringRecord<StateNode<any>>>(
|
||||
states: States,
|
||||
): StringRecordNodeToNodeStringRecord<States> => {
|
||||
const instances = new NestedMap()
|
||||
const instances = new NestedMap<any[], CombinedStateInstance>()
|
||||
const nKeys = Object.keys(states).length
|
||||
const _allFalse = mapRecord(states, () => false)
|
||||
|
||||
@ -33,7 +42,7 @@ export const combineStates = <States extends StringRecord<StateNode<any>>>(
|
||||
recordEntries(states).forEach(([key, node]) => {
|
||||
parents.push(node)
|
||||
globalChildRunners.get(node)!.push((ctxKey, isActive, isParentLoaded) => {
|
||||
let instance: any = instances.get(ctxKey)
|
||||
let instance = instances.get(ctxKey)
|
||||
if (!instance) {
|
||||
instance = {
|
||||
inactiveStates: nKeys,
|
||||
@ -53,11 +62,11 @@ export const combineStates = <States extends StringRecord<StateNode<any>>>(
|
||||
|
||||
if (isParentLoaded !== instance.loadedStates[key]) {
|
||||
instance.emptyStates += isParentLoaded ? -1 : 1
|
||||
instance.loadedStates[key] = isParentLoaded
|
||||
instance.loadedStates[key] = !!isParentLoaded
|
||||
}
|
||||
|
||||
const isCurrentlyActive = instance.inactiveStates === 0
|
||||
const isLoaded = instance.activeStates === nKeys
|
||||
const isLoaded = Object.values(instance.activeStates).every((v) => v)
|
||||
if (
|
||||
isCurrentlyActive !== instance.latestIsActive ||
|
||||
isLoaded !== instance.latestIsLoaded
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export class NestedMap<K extends [], V extends Object> {
|
||||
export class NestedMap<K extends any[], V extends Object> {
|
||||
private root: Map<K, V>
|
||||
private rootValue?: V
|
||||
constructor() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user