Implement AppState and StateTransitionMachine (#579)
The cred explorer app has a variety of valid states. Currently, it is thrown together without explicit documentation of what its states are, how they transition, or error handling or testing. I worry that this will be hard to maintain. This commit creates the AppState type which explicitly reifies every reachable state for the app, and a StateTransitionMachine which handles transitions between states. The transitions are thoroughly tested, including edge cases where the user makes a change while waiting for a promise to resolve, or where one of the promises failes. Test plan: The unit tests are comprehensive. `yarn test` passes. Thanks to @wchargin for much discussion about how to structure the states.
This commit is contained in:
parent
038d166972
commit
56c17d2597
|
@ -0,0 +1,258 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import deepEqual from "lodash.isequal";
|
||||||
|
|
||||||
|
import * as NullUtil from "../../util/null";
|
||||||
|
import {Graph} from "../../core/graph";
|
||||||
|
import type {Repo} from "../../core/repo";
|
||||||
|
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
|
||||||
|
import {
|
||||||
|
type PagerankNodeDecomposition,
|
||||||
|
type PagerankOptions,
|
||||||
|
pagerank,
|
||||||
|
} from "../../core/attribution/pagerank";
|
||||||
|
|
||||||
|
import type {DynamicPluginAdapter} from "../pluginAdapter";
|
||||||
|
import {StaticPluginAdapter as GitAdapter} from "../../plugins/git/pluginAdapter";
|
||||||
|
import {StaticPluginAdapter as GithubAdapter} from "../../plugins/github/pluginAdapter";
|
||||||
|
|
||||||
|
/*
|
||||||
|
This models the UI states of the credExplorer/App as a state machine.
|
||||||
|
|
||||||
|
The different states are all instances of AppState, and the transitions are
|
||||||
|
explicitly managed by the StateTransitionMachine class. All of the
|
||||||
|
transitions, including error cases, are thoroughly tested.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type AppState = Uninitialized | Initialized;
|
||||||
|
export type Uninitialized = {|
|
||||||
|
+type: "UNINITIALIZED",
|
||||||
|
edgeEvaluator: ?EdgeEvaluator,
|
||||||
|
repo: ?Repo,
|
||||||
|
|};
|
||||||
|
export type Initialized = {|
|
||||||
|
+type: "INITIALIZED",
|
||||||
|
+edgeEvaluator: EdgeEvaluator,
|
||||||
|
+repo: Repo,
|
||||||
|
+substate: AppSubstate,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export type LoadingState = "NOT_LOADING" | "LOADING" | "FAILED";
|
||||||
|
export type AppSubstate =
|
||||||
|
| ReadyToLoadGraph
|
||||||
|
| ReadyToRunPagerank
|
||||||
|
| PagerankEvaluated;
|
||||||
|
export type ReadyToLoadGraph = {|
|
||||||
|
+type: "READY_TO_LOAD_GRAPH",
|
||||||
|
+loading: LoadingState,
|
||||||
|
|};
|
||||||
|
export type ReadyToRunPagerank = {|
|
||||||
|
+type: "READY_TO_RUN_PAGERANK",
|
||||||
|
+graphWithAdapters: GraphWithAdapters,
|
||||||
|
+loading: LoadingState,
|
||||||
|
|};
|
||||||
|
export type PagerankEvaluated = {|
|
||||||
|
+type: "PAGERANK_EVALUATED",
|
||||||
|
+graphWithAdapters: GraphWithAdapters,
|
||||||
|
pagerankNodeDecomposition: PagerankNodeDecomposition,
|
||||||
|
+loading: LoadingState,
|
||||||
|
|};
|
||||||
|
|
||||||
|
export function createStateTransitionMachine(
|
||||||
|
getState: () => AppState,
|
||||||
|
setState: (AppState) => void
|
||||||
|
): StateTransitionMachine {
|
||||||
|
return new StateTransitionMachine(
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
loadGraphWithAdapters,
|
||||||
|
pagerank
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initialState(): AppState {
|
||||||
|
return {type: "UNINITIALIZED", repo: null, edgeEvaluator: null};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exported for testing purposes.
|
||||||
|
export interface StateTransitionMachineInterface {
|
||||||
|
+setRepo: (Repo) => void;
|
||||||
|
+setEdgeEvaluator: (EdgeEvaluator) => void;
|
||||||
|
+loadGraph: () => Promise<void>;
|
||||||
|
+runPagerank: () => Promise<void>;
|
||||||
|
}
|
||||||
|
/* In production, instantiate via createStateTransitionMachine; the constructor
|
||||||
|
* implementation allows specification of the loadGraphWithAdapters and
|
||||||
|
* pagerank functions for DI/testing purposes.
|
||||||
|
**/
|
||||||
|
export class StateTransitionMachine implements StateTransitionMachineInterface {
|
||||||
|
getState: () => AppState;
|
||||||
|
setState: (AppState) => void;
|
||||||
|
loadGraphWithAdapters: (repo: Repo) => Promise<GraphWithAdapters>;
|
||||||
|
pagerank: (
|
||||||
|
Graph,
|
||||||
|
EdgeEvaluator,
|
||||||
|
PagerankOptions
|
||||||
|
) => Promise<PagerankNodeDecomposition>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
getState: () => AppState,
|
||||||
|
setState: (AppState) => void,
|
||||||
|
loadGraphWithAdapters: (repo: Repo) => Promise<GraphWithAdapters>,
|
||||||
|
pagerank: (
|
||||||
|
Graph,
|
||||||
|
EdgeEvaluator,
|
||||||
|
PagerankOptions
|
||||||
|
) => Promise<PagerankNodeDecomposition>
|
||||||
|
) {
|
||||||
|
this.getState = getState;
|
||||||
|
this.setState = setState;
|
||||||
|
this.loadGraphWithAdapters = loadGraphWithAdapters;
|
||||||
|
this.pagerank = pagerank;
|
||||||
|
}
|
||||||
|
|
||||||
|
_maybeInitialize(state: Uninitialized): AppState {
|
||||||
|
const {repo, edgeEvaluator} = state;
|
||||||
|
if (repo != null && edgeEvaluator != null) {
|
||||||
|
const substate = {type: "READY_TO_LOAD_GRAPH", loading: "NOT_LOADING"};
|
||||||
|
return {type: "INITIALIZED", repo, edgeEvaluator, substate};
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRepo(repo: Repo) {
|
||||||
|
const state = this.getState();
|
||||||
|
switch (state.type) {
|
||||||
|
case "UNINITIALIZED": {
|
||||||
|
const newState = this._maybeInitialize({...state, repo});
|
||||||
|
this.setState(newState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "INITIALIZED": {
|
||||||
|
const substate = {type: "READY_TO_LOAD_GRAPH", loading: "NOT_LOADING"};
|
||||||
|
const newState = {...state, repo, substate};
|
||||||
|
this.setState(newState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error((state.type: empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setEdgeEvaluator(edgeEvaluator: EdgeEvaluator) {
|
||||||
|
const state = this.getState();
|
||||||
|
switch (state.type) {
|
||||||
|
case "UNINITIALIZED": {
|
||||||
|
const newState = this._maybeInitialize({...state, edgeEvaluator});
|
||||||
|
this.setState(newState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "INITIALIZED": {
|
||||||
|
const newState = {...state, edgeEvaluator};
|
||||||
|
this.setState(newState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error((state.type: empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadGraph() {
|
||||||
|
const state = this.getState();
|
||||||
|
if (
|
||||||
|
state.type !== "INITIALIZED" ||
|
||||||
|
state.substate.type !== "READY_TO_LOAD_GRAPH"
|
||||||
|
) {
|
||||||
|
throw new Error("Tried to loadGraph in incorrect state");
|
||||||
|
}
|
||||||
|
const {repo, substate} = state;
|
||||||
|
const loadingState = {
|
||||||
|
...state,
|
||||||
|
substate: {...substate, loading: "LOADING"},
|
||||||
|
};
|
||||||
|
this.setState(loadingState);
|
||||||
|
let newState: ?AppState;
|
||||||
|
try {
|
||||||
|
const graphWithAdapters = await this.loadGraphWithAdapters(repo);
|
||||||
|
newState = {
|
||||||
|
...state,
|
||||||
|
substate: {
|
||||||
|
type: "READY_TO_RUN_PAGERANK",
|
||||||
|
graphWithAdapters,
|
||||||
|
loading: "NOT_LOADING",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
newState = {...state, substate: {...substate, loading: "FAILED"}};
|
||||||
|
}
|
||||||
|
if (deepEqual(this.getState(), loadingState)) {
|
||||||
|
this.setState(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runPagerank() {
|
||||||
|
const state = this.getState();
|
||||||
|
if (
|
||||||
|
state.type !== "INITIALIZED" ||
|
||||||
|
state.substate.type === "READY_TO_LOAD_GRAPH"
|
||||||
|
) {
|
||||||
|
throw new Error("Tried to runPagerank in incorrect state");
|
||||||
|
}
|
||||||
|
const {edgeEvaluator, substate} = state;
|
||||||
|
// Oh, the things we must do to appease flow
|
||||||
|
const loadingSubstate =
|
||||||
|
substate.type === "PAGERANK_EVALUATED"
|
||||||
|
? {...substate, loading: "LOADING"}
|
||||||
|
: {...substate, loading: "LOADING"};
|
||||||
|
const loadingState = {
|
||||||
|
...state,
|
||||||
|
substate: loadingSubstate,
|
||||||
|
};
|
||||||
|
this.setState(loadingState);
|
||||||
|
const graph = substate.graphWithAdapters.graph;
|
||||||
|
let newState: ?AppState;
|
||||||
|
try {
|
||||||
|
const pagerankNodeDecomposition = await this.pagerank(
|
||||||
|
graph,
|
||||||
|
edgeEvaluator,
|
||||||
|
{
|
||||||
|
verbose: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const newSubstate = {
|
||||||
|
type: "PAGERANK_EVALUATED",
|
||||||
|
graphWithAdapters: substate.graphWithAdapters,
|
||||||
|
pagerankNodeDecomposition,
|
||||||
|
loading: "NOT_LOADING",
|
||||||
|
};
|
||||||
|
newState = {...state, substate: newSubstate};
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
const failedSubstate =
|
||||||
|
// More flow appeasement
|
||||||
|
substate.type === "PAGERANK_EVALUATED"
|
||||||
|
? {...substate, loading: "FAILED"}
|
||||||
|
: {...substate, loading: "FAILED"};
|
||||||
|
newState = {...state, substate: failedSubstate};
|
||||||
|
}
|
||||||
|
if (deepEqual(this.getState(), loadingState)) {
|
||||||
|
this.setState(NullUtil.get(newState));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GraphWithAdapters = {|
|
||||||
|
+graph: Graph,
|
||||||
|
+adapters: $ReadOnlyArray<DynamicPluginAdapter>,
|
||||||
|
|};
|
||||||
|
export function loadGraphWithAdapters(repo: Repo): Promise<GraphWithAdapters> {
|
||||||
|
const statics = [new GitAdapter(), new GithubAdapter()];
|
||||||
|
return Promise.all(statics.map((a) => a.load(repo))).then((adapters) => {
|
||||||
|
const graph = Graph.merge(adapters.map((x) => x.graph()));
|
||||||
|
return {graph, adapters};
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,314 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
StateTransitionMachine,
|
||||||
|
initialState,
|
||||||
|
type AppState,
|
||||||
|
type GraphWithAdapters,
|
||||||
|
} from "./state";
|
||||||
|
|
||||||
|
import {Graph} from "../../core/graph";
|
||||||
|
import {makeRepo, type Repo} from "../../core/repo";
|
||||||
|
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
|
||||||
|
import type {
|
||||||
|
PagerankNodeDecomposition,
|
||||||
|
PagerankOptions,
|
||||||
|
} from "../../core/attribution/pagerank";
|
||||||
|
|
||||||
|
describe("app/credExplorer/state", () => {
|
||||||
|
function example(startingState: AppState) {
|
||||||
|
const stateContainer = {appState: startingState};
|
||||||
|
const getState = () => stateContainer.appState;
|
||||||
|
const setState = (appState) => {
|
||||||
|
stateContainer.appState = appState;
|
||||||
|
};
|
||||||
|
const loadGraphMock: (repo: Repo) => Promise<GraphWithAdapters> = jest.fn();
|
||||||
|
const pagerankMock: (
|
||||||
|
Graph,
|
||||||
|
EdgeEvaluator,
|
||||||
|
PagerankOptions
|
||||||
|
) => Promise<PagerankNodeDecomposition> = jest.fn();
|
||||||
|
const stm = new StateTransitionMachine(
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
loadGraphMock,
|
||||||
|
pagerankMock
|
||||||
|
);
|
||||||
|
return {getState, stm, loadGraphMock, pagerankMock};
|
||||||
|
}
|
||||||
|
function initialized(substate): AppState {
|
||||||
|
return {
|
||||||
|
type: "INITIALIZED",
|
||||||
|
repo: makeRepo("foo", "bar"),
|
||||||
|
edgeEvaluator: edgeEvaluator(),
|
||||||
|
substate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function readyToLoadGraph(): AppState {
|
||||||
|
return initialized({type: "READY_TO_LOAD_GRAPH", loading: "NOT_LOADING"});
|
||||||
|
}
|
||||||
|
function readyToRunPagerank(): AppState {
|
||||||
|
return initialized({
|
||||||
|
type: "READY_TO_RUN_PAGERANK",
|
||||||
|
loading: "NOT_LOADING",
|
||||||
|
graphWithAdapters: graphWithAdapters(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function pagerankEvaluated(): AppState {
|
||||||
|
return initialized({
|
||||||
|
type: "PAGERANK_EVALUATED",
|
||||||
|
graphWithAdapters: graphWithAdapters(),
|
||||||
|
pagerankNodeDecomposition: pagerankNodeDecomposition(),
|
||||||
|
loading: "NOT_LOADING",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function edgeEvaluator(): EdgeEvaluator {
|
||||||
|
return (_unused_Edge) => ({toWeight: 3, froWeight: 4});
|
||||||
|
}
|
||||||
|
function graphWithAdapters(): GraphWithAdapters {
|
||||||
|
return {graph: new Graph(), adapters: []};
|
||||||
|
}
|
||||||
|
function pagerankNodeDecomposition() {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
function getSubstate(state: AppState) {
|
||||||
|
if (state.type !== "INITIALIZED") {
|
||||||
|
throw new Error("Tried to get invalid substate");
|
||||||
|
}
|
||||||
|
return state.substate;
|
||||||
|
}
|
||||||
|
function loading(state: AppState) {
|
||||||
|
if (
|
||||||
|
state.type !== "INITIALIZED" ||
|
||||||
|
state.substate.type === "PAGERANK_EVALUATED"
|
||||||
|
) {
|
||||||
|
throw new Error("Tried to get invalid loading");
|
||||||
|
}
|
||||||
|
return state.substate.loading;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("setRepo", () => {
|
||||||
|
describe("in UNINITIALIZED", () => {
|
||||||
|
it("stays UNINITIALIZED if edge evaluator not set", () => {
|
||||||
|
const {getState, stm} = example(initialState());
|
||||||
|
const repo = makeRepo("foo", "bar");
|
||||||
|
stm.setRepo(repo);
|
||||||
|
const state = getState();
|
||||||
|
expect(state.type).toBe("UNINITIALIZED");
|
||||||
|
expect(state.repo).toEqual(repo);
|
||||||
|
});
|
||||||
|
it("transitions to INITIALIZED if an edge evaluator was set", () => {
|
||||||
|
const {getState, stm} = example(initialState());
|
||||||
|
stm.setEdgeEvaluator(edgeEvaluator());
|
||||||
|
const repo = makeRepo("foo", "bar");
|
||||||
|
stm.setRepo(repo);
|
||||||
|
const state = getState();
|
||||||
|
expect(state.type).toBe("INITIALIZED");
|
||||||
|
expect(state.repo).toEqual(repo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("in INITIALIZED", () => {
|
||||||
|
it("stays in READY_TO_LOAD_GRAPH with new repo", () => {
|
||||||
|
const {getState, stm} = example(readyToLoadGraph());
|
||||||
|
const repo = makeRepo("zoink", "zod");
|
||||||
|
stm.setRepo(repo);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.repo).toEqual(repo);
|
||||||
|
});
|
||||||
|
it("transitions READY_TO_RUN_PAGERANK to READY_TO_LOAD_GRAPH with new repo", () => {
|
||||||
|
const {getState, stm} = example(readyToRunPagerank());
|
||||||
|
const repo = makeRepo("zoink", "zod");
|
||||||
|
stm.setRepo(repo);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.repo).toEqual(repo);
|
||||||
|
});
|
||||||
|
it("transitions PAGERANK_EVALUATED to READY_TO_LOAD_GRAPH with new repo", () => {
|
||||||
|
const {getState, stm} = example(pagerankEvaluated());
|
||||||
|
const repo = makeRepo("zoink", "zod");
|
||||||
|
stm.setRepo(repo);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.repo).toEqual(repo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setEdgeEvaluator", () => {
|
||||||
|
describe("in UNINITIALIZED", () => {
|
||||||
|
it("sets ee without transitioning to INITIALIZE if repo not set", () => {
|
||||||
|
const {getState, stm} = example(initialState());
|
||||||
|
const ee = edgeEvaluator();
|
||||||
|
stm.setEdgeEvaluator(ee);
|
||||||
|
const state = getState();
|
||||||
|
expect(state.type).toBe("UNINITIALIZED");
|
||||||
|
expect(state.edgeEvaluator).toBe(ee);
|
||||||
|
});
|
||||||
|
it("triggers transition to INITIALIZED if repo was set", () => {
|
||||||
|
const {getState, stm} = example(initialState());
|
||||||
|
stm.setRepo(makeRepo("foo", "zod"));
|
||||||
|
const ee = edgeEvaluator();
|
||||||
|
stm.setEdgeEvaluator(ee);
|
||||||
|
const state = getState();
|
||||||
|
expect(state.type).toBe("INITIALIZED");
|
||||||
|
expect(state.edgeEvaluator).toBe(ee);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("in INITIALIZED", () => {
|
||||||
|
it("does not transition READY_TO_LOAD_GRAPH", () => {
|
||||||
|
const {getState, stm} = example(readyToLoadGraph());
|
||||||
|
const ee = edgeEvaluator();
|
||||||
|
stm.setEdgeEvaluator(ee);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.edgeEvaluator).toBe(ee);
|
||||||
|
});
|
||||||
|
it("does not transition READY_TO_RUN_PAGERANK", () => {
|
||||||
|
const {getState, stm} = example(readyToRunPagerank());
|
||||||
|
const ee = edgeEvaluator();
|
||||||
|
stm.setEdgeEvaluator(ee);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("READY_TO_RUN_PAGERANK");
|
||||||
|
expect(state.edgeEvaluator).toBe(ee);
|
||||||
|
});
|
||||||
|
it("does not transition PAGERANK_EVALUATED", () => {
|
||||||
|
const {getState, stm} = example(pagerankEvaluated());
|
||||||
|
const ee = edgeEvaluator();
|
||||||
|
stm.setEdgeEvaluator(ee);
|
||||||
|
const state = getState();
|
||||||
|
expect(getSubstate(state).type).toBe("PAGERANK_EVALUATED");
|
||||||
|
expect(state.edgeEvaluator).toBe(ee);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("loadGraph", () => {
|
||||||
|
it("can only be called when READY_TO_LOAD_GRAPH", async () => {
|
||||||
|
const badStates = [
|
||||||
|
initialState(),
|
||||||
|
readyToRunPagerank(),
|
||||||
|
pagerankEvaluated(),
|
||||||
|
];
|
||||||
|
for (const b of badStates) {
|
||||||
|
const {stm} = example(b);
|
||||||
|
await expect(stm.loadGraph()).rejects.toThrow("incorrect state");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("immediately sets loading status", () => {
|
||||||
|
const {getState, stm} = example(readyToLoadGraph());
|
||||||
|
expect(loading(getState())).toBe("NOT_LOADING");
|
||||||
|
stm.loadGraph();
|
||||||
|
expect(loading(getState())).toBe("LOADING");
|
||||||
|
expect(getSubstate(getState()).type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
});
|
||||||
|
it("transitions to READY_TO_RUN_PAGERANK on success", async () => {
|
||||||
|
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
|
||||||
|
const gwa = graphWithAdapters();
|
||||||
|
loadGraphMock.mockResolvedValue(gwa);
|
||||||
|
await stm.loadGraph();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
expect(loading(state)).toBe("NOT_LOADING");
|
||||||
|
expect(substate.type).toBe("READY_TO_RUN_PAGERANK");
|
||||||
|
if (substate.type !== "READY_TO_RUN_PAGERANK") {
|
||||||
|
throw new Error("Impossible");
|
||||||
|
}
|
||||||
|
expect(substate.graphWithAdapters).toBe(gwa);
|
||||||
|
});
|
||||||
|
it("does not transition if another transition happens first", async () => {
|
||||||
|
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
|
||||||
|
const swappedRepo = makeRepo("too", "fast");
|
||||||
|
loadGraphMock.mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
stm.setRepo(swappedRepo);
|
||||||
|
resolve(graphWithAdapters());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await stm.loadGraph();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
expect(loading(state)).toBe("NOT_LOADING");
|
||||||
|
expect(substate.type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.repo).toBe(swappedRepo);
|
||||||
|
});
|
||||||
|
it("sets loading state FAILED on reject", async () => {
|
||||||
|
const {getState, stm, loadGraphMock} = example(readyToLoadGraph());
|
||||||
|
const error = new Error("Oh no!");
|
||||||
|
// $ExpectFlowError
|
||||||
|
console.error = jest.fn();
|
||||||
|
loadGraphMock.mockRejectedValue(error);
|
||||||
|
await stm.loadGraph();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
expect(loading(state)).toBe("FAILED");
|
||||||
|
expect(substate.type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
|
expect(console.error).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runPagerank", () => {
|
||||||
|
it("can only be called when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
||||||
|
const badStates = [initialState(), readyToLoadGraph()];
|
||||||
|
for (const b of badStates) {
|
||||||
|
const {stm} = example(b);
|
||||||
|
await expect(stm.runPagerank()).rejects.toThrow("incorrect state");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
|
||||||
|
const goodStates = [readyToRunPagerank(), pagerankEvaluated()];
|
||||||
|
for (const g of goodStates) {
|
||||||
|
const {stm, getState, pagerankMock} = example(g);
|
||||||
|
const pnd = pagerankNodeDecomposition();
|
||||||
|
pagerankMock.mockResolvedValue(pnd);
|
||||||
|
await stm.runPagerank();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
if (substate.type !== "PAGERANK_EVALUATED") {
|
||||||
|
throw new Error("Impossible");
|
||||||
|
}
|
||||||
|
expect(substate.type).toBe("PAGERANK_EVALUATED");
|
||||||
|
expect(substate.pagerankNodeDecomposition).toBe(pnd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("immediately sets loading status", () => {
|
||||||
|
const {getState, stm} = example(readyToRunPagerank());
|
||||||
|
expect(loading(getState())).toBe("NOT_LOADING");
|
||||||
|
stm.runPagerank();
|
||||||
|
expect(loading(getState())).toBe("LOADING");
|
||||||
|
});
|
||||||
|
it("does not transition if another transition happens first", async () => {
|
||||||
|
const {getState, stm, pagerankMock} = example(readyToRunPagerank());
|
||||||
|
const swappedRepo = makeRepo("too", "fast");
|
||||||
|
pagerankMock.mockImplementation(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
stm.setRepo(swappedRepo);
|
||||||
|
resolve(graphWithAdapters());
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await stm.runPagerank();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
expect(loading(state)).toBe("NOT_LOADING");
|
||||||
|
expect(substate.type).toBe("READY_TO_LOAD_GRAPH");
|
||||||
|
expect(state.repo).toBe(swappedRepo);
|
||||||
|
});
|
||||||
|
it("sets loading state FAILED on reject", async () => {
|
||||||
|
const {getState, stm, pagerankMock} = example(readyToRunPagerank());
|
||||||
|
const error = new Error("Oh no!");
|
||||||
|
// $ExpectFlowError
|
||||||
|
console.error = jest.fn();
|
||||||
|
pagerankMock.mockRejectedValue(error);
|
||||||
|
await stm.runPagerank();
|
||||||
|
const state = getState();
|
||||||
|
const substate = getSubstate(state);
|
||||||
|
expect(loading(state)).toBe("FAILED");
|
||||||
|
expect(substate.type).toBe("READY_TO_RUN_PAGERANK");
|
||||||
|
expect(console.error).toHaveBeenCalledTimes(1);
|
||||||
|
expect(console.error).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue