mirror of
https://github.com/re-rxjs/react-rxjs.git
synced 2025-12-08 18:01:51 +00:00
add tests for substate (wip)
This commit is contained in:
parent
c79850194c
commit
eca5e63f8f
35
packages/context-state/src/create-root.ts
Normal file
35
packages/context-state/src/create-root.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { children } from "./internal"
|
||||
import { StateNode } from "./types"
|
||||
|
||||
export interface RootNode extends StateNode<never> {
|
||||
run(): () => void
|
||||
}
|
||||
|
||||
export function createRoot(): RootNode {
|
||||
const childRunners = new Set<(isActive: boolean, value: null) => void>()
|
||||
const runChildren = (isActive: boolean) => {
|
||||
childRunners.forEach((cb) => {
|
||||
cb(isActive, null)
|
||||
})
|
||||
}
|
||||
|
||||
const result: RootNode = {
|
||||
getValue: () => {
|
||||
throw new Error("RootNode doesn't have value")
|
||||
},
|
||||
state$: () => {
|
||||
throw new Error("RootNode doesn't have value")
|
||||
},
|
||||
run: () => {
|
||||
// Maybe more fancy with refcount, etc?
|
||||
runChildren(true)
|
||||
return () => {
|
||||
runChildren(false)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
children.set(result, childRunners)
|
||||
|
||||
return result
|
||||
}
|
||||
6
packages/context-state/src/internal/children.ts
Normal file
6
packages/context-state/src/internal/children.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { StateNode } from "../types"
|
||||
|
||||
export const children = new WeakMap<
|
||||
StateNode<any>,
|
||||
Set<(isActive: boolean, value: any) => void>
|
||||
>()
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./empty-value"
|
||||
export * from "./errors"
|
||||
export * from "./promisses"
|
||||
export * from "./children"
|
||||
|
||||
224
packages/context-state/src/substate.test.ts
Normal file
224
packages/context-state/src/substate.test.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import { EMPTY, NEVER, Observable, of, Subject, throwError } from "rxjs"
|
||||
import { createRoot } from "./create-root"
|
||||
import { substate } from "./substate"
|
||||
|
||||
describe("subState", () => {
|
||||
describe("constructor", () => {
|
||||
it("subscribes to the inner observable when the parent has a value", () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
|
||||
let ranFunction = false
|
||||
substate(contextNode, () => {
|
||||
ranFunction = true
|
||||
return EMPTY
|
||||
})
|
||||
|
||||
expect(ranFunction).toBe(false)
|
||||
|
||||
root.run()
|
||||
expect(ranFunction).toBe(false)
|
||||
|
||||
contextSource$.next(1)
|
||||
expect(ranFunction).toBe(true)
|
||||
})
|
||||
|
||||
it("unsubscribes from the previous observable before subscribing to the new one when the context changes", () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
|
||||
let subscribed = false,
|
||||
unsubscribed = false
|
||||
substate(contextNode, () => {
|
||||
return new Observable(() => {
|
||||
subscribed = true
|
||||
|
||||
return () => {
|
||||
// the test will reset `subscribed` to false when this function should be run.
|
||||
expect(subscribed).toBe(false)
|
||||
unsubscribed = true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
root.run()
|
||||
contextSource$.next(1)
|
||||
expect(subscribed).toBe(true)
|
||||
expect(unsubscribed).toBe(false)
|
||||
|
||||
subscribed = false
|
||||
contextSource$.next(2)
|
||||
expect(subscribed).toBe(true)
|
||||
expect(unsubscribed).toBe(true)
|
||||
})
|
||||
|
||||
it("can access any observable from the context with the ctx function", () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
|
||||
let lastContextValue: number | null = null
|
||||
substate(contextNode, (ctx) => {
|
||||
expect(ctx(contextNode)).toBe(lastContextValue)
|
||||
return EMPTY
|
||||
})
|
||||
|
||||
root.run()
|
||||
expect.assertions(3)
|
||||
contextSource$.next((lastContextValue = 1))
|
||||
contextSource$.next((lastContextValue = 2))
|
||||
contextSource$.next((lastContextValue = 3))
|
||||
})
|
||||
})
|
||||
|
||||
describe("getValue", () => {
|
||||
it("throws when the node is not active", () => {
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => of(1))
|
||||
|
||||
expect(() => subNode.getValue()).toThrowError("Inactive Context")
|
||||
})
|
||||
|
||||
it("after an error it throws an InactiveContextError", () => {
|
||||
const root = createRoot()
|
||||
const error = new Error()
|
||||
const subNode = substate(root, () => throwError(() => error))
|
||||
root.run()
|
||||
|
||||
expect(() => {
|
||||
console.log(subNode.getValue())
|
||||
}).toThrowError("Inactive Context")
|
||||
})
|
||||
|
||||
it("returns the latest value if the observable has already emitted", () => {
|
||||
const source$ = new Subject<number>()
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => source$)
|
||||
root.run()
|
||||
|
||||
source$.next(1)
|
||||
source$.next(2)
|
||||
source$.next(3)
|
||||
|
||||
expect(subNode.getValue()).toBe(3)
|
||||
})
|
||||
|
||||
it("returns a promise that resolves when the first value is emitted", async () => {
|
||||
const source$ = new Subject<number>()
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => source$)
|
||||
source$.next(1)
|
||||
root.run()
|
||||
|
||||
const promise = subNode.getValue()
|
||||
|
||||
source$.next(2)
|
||||
source$.next(3)
|
||||
|
||||
await expect(promise).resolves.toBe(2)
|
||||
})
|
||||
|
||||
it("rejects the promise if the node becomes inactive", async () => {
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => NEVER)
|
||||
const stop = root.run()
|
||||
|
||||
const promise = subNode.getValue()
|
||||
|
||||
stop()
|
||||
|
||||
await expect(promise).rejects.toBeTruthy()
|
||||
})
|
||||
|
||||
it("rejects the promise when the observable emits an error", async () => {
|
||||
const source$ = new Subject<number>()
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => source$)
|
||||
root.run()
|
||||
|
||||
const promise = subNode.getValue()
|
||||
|
||||
const error = new Error()
|
||||
source$.error(error)
|
||||
|
||||
await expect(promise).rejects.toBe(error)
|
||||
})
|
||||
|
||||
it("ignores the observable until its context has a value", async () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
const source$ = new Subject<number>()
|
||||
const subNode = substate(contextNode, () => source$)
|
||||
root.run()
|
||||
|
||||
source$.next(1)
|
||||
|
||||
const promise = subNode.getValue()
|
||||
expect(promise).toBeInstanceOf(Promise)
|
||||
|
||||
source$.next(2)
|
||||
source$.next(3)
|
||||
|
||||
contextSource$.next(1)
|
||||
|
||||
source$.next(4)
|
||||
|
||||
await expect(promise).resolves.toBe(4)
|
||||
})
|
||||
|
||||
it("discards the old value after a context changes, returning a new promise", async () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
const source$ = new Subject<number>()
|
||||
const subNode = substate(contextNode, () => source$)
|
||||
root.run()
|
||||
|
||||
contextSource$.next(1)
|
||||
source$.next(2)
|
||||
expect(subNode.getValue()).toBe(2)
|
||||
|
||||
contextSource$.next(3)
|
||||
const promise = subNode.getValue()
|
||||
|
||||
source$.next(4)
|
||||
await expect(promise).resolves.toBe(4)
|
||||
})
|
||||
|
||||
it("returns a promise that yields the value after a context has changed", async () => {
|
||||
const root = createRoot()
|
||||
const contextSource$ = new Subject<number>()
|
||||
const contextNode = substate(root, () => contextSource$)
|
||||
const subNode = substate(contextNode, (ctx) =>
|
||||
ctx(contextNode) === 2 ? of("done!") : EMPTY,
|
||||
)
|
||||
root.run()
|
||||
|
||||
const promise = subNode.getValue()
|
||||
|
||||
contextSource$.next(1)
|
||||
contextSource$.next(2)
|
||||
|
||||
await expect(promise).resolves.toBe("done!")
|
||||
})
|
||||
|
||||
it("rejects the promise if any of its context emits an error", async () => {
|
||||
const source$ = new Subject<number>()
|
||||
const root = createRoot()
|
||||
const subNode = substate(root, () => source$)
|
||||
root.run()
|
||||
|
||||
const promise = subNode.getValue()
|
||||
|
||||
const error = new Error()
|
||||
source$.error(error)
|
||||
|
||||
await expect(promise).rejects.toBe(error)
|
||||
})
|
||||
})
|
||||
|
||||
describe("state$", () => {})
|
||||
})
|
||||
@ -7,13 +7,9 @@ import {
|
||||
StatePromise,
|
||||
DeferredPromise,
|
||||
createDeferredPromise,
|
||||
children,
|
||||
} from "./internal"
|
||||
|
||||
const children = new WeakMap<
|
||||
StateNode<any>,
|
||||
Set<(isActive: boolean, value: any) => void>
|
||||
>()
|
||||
|
||||
export const ctx = <V>(node: StateNode<V>): V => {
|
||||
const value = node.getValue()
|
||||
if (value instanceof StatePromise) throw invalidContext()
|
||||
@ -24,7 +20,7 @@ export type Ctx = typeof ctx
|
||||
export const substate = <T, P>(
|
||||
parent: StateNode<P>,
|
||||
getState$: (ctx: Ctx) => Observable<T>,
|
||||
equalityFn: (a: T, b: T) => boolean,
|
||||
equalityFn: (a: T, b: T) => boolean = Object.is,
|
||||
): StateNode<T> => {
|
||||
let subject: ReplaySubject<T> | null = null
|
||||
let subscription: Subscription | null = null
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user