Use AppStateTransitionMachine to drive App (#583)
Pull #579 reifies the cred explorer state as an explicit state transition machine, with a well-tested implementation. This pull re-writes `credExplorer/App.js` to use that implementation, and thoroughly tests it. The result is that `credExplorer/App.js` has much simpler code (because it just binds the rendered components to the state machine), and is much more thoroughly tested. To ensure easy testability of the `App` class, it was refactored so that the module exports a factory function which takes a method for creating the `AppStateTransitionMachine` and returns an `App` class. This ensures that in test code, we can easily mock the state transition machine. This had no effect on external callers, since the higher-level `<AppPage>` class, which already wraps over `LocalStore` choice, was already the preferred call site. I also added a loading indicator component, which presently displays a status text corresponding to the state, such as "Loading graph...", or "Error while running PageRank". This way, there is always some user feedback during loading states (which could take a while). Test plan: Visual inspection, and the very thorough included unit tests.
This commit is contained in:
parent
20c80c3390
commit
10f704ebd2
|
@ -6,19 +6,15 @@ import type {LocalStore} from "../localStore";
|
||||||
import CheckedLocalStore from "../checkedLocalStore";
|
import CheckedLocalStore from "../checkedLocalStore";
|
||||||
import BrowserLocalStore from "../browserLocalStore";
|
import BrowserLocalStore from "../browserLocalStore";
|
||||||
|
|
||||||
import {StaticPluginAdapter as GithubAdapter} from "../../plugins/github/pluginAdapter";
|
|
||||||
import {StaticPluginAdapter as GitAdapter} from "../../plugins/git/pluginAdapter";
|
|
||||||
import {Graph} from "../../core/graph";
|
|
||||||
import {pagerank} from "../../core/attribution/pagerank";
|
|
||||||
import {PagerankTable} from "./PagerankTable";
|
import {PagerankTable} from "./PagerankTable";
|
||||||
import type {DynamicPluginAdapter} from "../pluginAdapter";
|
|
||||||
import {type EdgeEvaluator} from "../../core/attribution/pagerank";
|
|
||||||
import {WeightConfig} from "./WeightConfig";
|
import {WeightConfig} from "./WeightConfig";
|
||||||
import type {PagerankNodeDecomposition} from "../../core/attribution/pagerankNodeDecomposition";
|
|
||||||
import RepositorySelect from "./RepositorySelect";
|
import RepositorySelect from "./RepositorySelect";
|
||||||
import type {Repo} from "../../core/repo";
|
import {
|
||||||
|
createStateTransitionMachine,
|
||||||
import * as NullUtil from "../../util/null";
|
type AppState,
|
||||||
|
type StateTransitionMachineInterface,
|
||||||
|
initialState,
|
||||||
|
} from "./state";
|
||||||
|
|
||||||
export default class AppPage extends React.Component<{||}> {
|
export default class AppPage extends React.Component<{||}> {
|
||||||
static _LOCAL_STORE = new CheckedLocalStore(
|
static _LOCAL_STORE = new CheckedLocalStore(
|
||||||
|
@ -29,109 +25,139 @@ export default class AppPage extends React.Component<{||}> {
|
||||||
);
|
);
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const App = createApp(createStateTransitionMachine);
|
||||||
return <App localStore={AppPage._LOCAL_STORE} />;
|
return <App localStore={AppPage._LOCAL_STORE} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {|+localStore: LocalStore|};
|
type Props = {|+localStore: LocalStore|};
|
||||||
type State = {
|
type State = {|
|
||||||
selectedRepo: ?Repo,
|
appState: AppState,
|
||||||
data: {|
|
|};
|
||||||
graphWithAdapters: ?{|
|
|
||||||
+graph: Graph,
|
|
||||||
+adapters: $ReadOnlyArray<DynamicPluginAdapter>,
|
|
||||||
|},
|
|
||||||
+pnd: ?PagerankNodeDecomposition,
|
|
||||||
|},
|
|
||||||
edgeEvaluator: ?EdgeEvaluator,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_ENTRIES_PER_LIST = 100;
|
export function createApp(
|
||||||
|
createSTM: (
|
||||||
|
getState: () => AppState,
|
||||||
|
setState: (AppState) => void
|
||||||
|
) => StateTransitionMachineInterface
|
||||||
|
) {
|
||||||
|
return class App extends React.Component<Props, State> {
|
||||||
|
stateTransitionMachine: StateTransitionMachineInterface;
|
||||||
|
|
||||||
export class App extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
selectedRepo: null,
|
appState: initialState(),
|
||||||
data: {graphWithAdapters: null, pnd: null},
|
|
||||||
edgeEvaluator: null,
|
|
||||||
};
|
};
|
||||||
|
this.stateTransitionMachine = createSTM(
|
||||||
|
() => this.state.appState,
|
||||||
|
(appState) => this.setState({appState})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {localStore} = this.props;
|
const {localStore} = this.props;
|
||||||
const {edgeEvaluator, selectedRepo} = this.state;
|
const {appState} = this.state;
|
||||||
const {graphWithAdapters, pnd} = this.state.data;
|
const subType =
|
||||||
|
appState.type === "INITIALIZED" ? appState.substate.type : null;
|
||||||
|
const loadingState =
|
||||||
|
appState.type === "INITIALIZED" ? appState.substate.loading : null;
|
||||||
|
let pagerankTable;
|
||||||
|
if (
|
||||||
|
appState.type === "INITIALIZED" &&
|
||||||
|
appState.substate.type === "PAGERANK_EVALUATED"
|
||||||
|
) {
|
||||||
|
const adapters = appState.substate.graphWithAdapters.adapters;
|
||||||
|
const pnd = appState.substate.pagerankNodeDecomposition;
|
||||||
|
pagerankTable = (
|
||||||
|
<PagerankTable
|
||||||
|
adapters={adapters}
|
||||||
|
pnd={pnd}
|
||||||
|
maxEntriesPerList={100}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div style={{maxWidth: 900, margin: "0 auto", padding: "0 10px"}}>
|
<div style={{maxWidth: 900, margin: "0 auto", padding: "0 10px"}}>
|
||||||
<div>
|
|
||||||
<div style={{marginBottom: 10}}>
|
<div style={{marginBottom: 10}}>
|
||||||
<RepositorySelect
|
<RepositorySelect
|
||||||
localStore={localStore}
|
localStore={localStore}
|
||||||
onChange={(selectedRepo) => this.setState({selectedRepo})}
|
onChange={(repo) => this.stateTransitionMachine.setRepo(repo)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
disabled={selectedRepo == null}
|
disabled={subType !== "READY_TO_LOAD_GRAPH"}
|
||||||
onClick={() => this.loadData()}
|
onClick={() => this.stateTransitionMachine.loadGraph()}
|
||||||
>
|
>
|
||||||
Load data
|
Load graph
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
disabled={graphWithAdapters == null || edgeEvaluator == null}
|
disabled={
|
||||||
onClick={() => {
|
!(
|
||||||
if (graphWithAdapters == null || edgeEvaluator == null) {
|
(subType === "READY_TO_RUN_PAGERANK" ||
|
||||||
throw new Error("Unexpected null value");
|
subType === "PAGERANK_EVALUATED") &&
|
||||||
|
loadingState !== "LOADING"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const {graph} = graphWithAdapters;
|
onClick={() => this.stateTransitionMachine.runPagerank()}
|
||||||
pagerank(graph, edgeEvaluator, {
|
|
||||||
verbose: true,
|
|
||||||
}).then((pnd) => {
|
|
||||||
const data = {graphWithAdapters, pnd};
|
|
||||||
// In case a new graph was loaded while waiting for
|
|
||||||
// PageRank.
|
|
||||||
const stomped =
|
|
||||||
this.state.data.graphWithAdapters &&
|
|
||||||
this.state.data.graphWithAdapters.graph !== graph;
|
|
||||||
if (!stomped) {
|
|
||||||
this.setState({data});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Run PageRank
|
Run PageRank
|
||||||
</button>
|
</button>
|
||||||
<WeightConfig
|
<WeightConfig
|
||||||
localStore={localStore}
|
localStore={localStore}
|
||||||
onChange={(ee) => this.setState({edgeEvaluator: ee})}
|
onChange={(ee) => this.stateTransitionMachine.setEdgeEvaluator(ee)}
|
||||||
/>
|
/>
|
||||||
<PagerankTable
|
<LoadingIndicator appState={this.state.appState} />
|
||||||
adapters={NullUtil.map(graphWithAdapters, (x) => x.adapters)}
|
{pagerankTable}
|
||||||
pnd={pnd}
|
|
||||||
maxEntriesPerList={MAX_ENTRIES_PER_LIST}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData() {
|
|
||||||
const {selectedRepo} = this.state;
|
|
||||||
if (selectedRepo == null) {
|
|
||||||
throw new Error(`Impossible`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const statics = [new GithubAdapter(), new GitAdapter()];
|
|
||||||
Promise.all(statics.map((a) => a.load(selectedRepo))).then((adapters) => {
|
|
||||||
const graph = Graph.merge(adapters.map((x) => x.graph()));
|
|
||||||
const data = {
|
|
||||||
graphWithAdapters: {
|
|
||||||
graph,
|
|
||||||
adapters,
|
|
||||||
},
|
|
||||||
pnd: null,
|
|
||||||
};
|
};
|
||||||
this.setState({data});
|
}
|
||||||
});
|
|
||||||
|
export class LoadingIndicator extends React.PureComponent<{|
|
||||||
|
+appState: AppState,
|
||||||
|
|}> {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<span style={{paddingLeft: 10}}>{loadingText(this.props.appState)}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadingText(state: AppState) {
|
||||||
|
switch (state.type) {
|
||||||
|
case "UNINITIALIZED": {
|
||||||
|
return "Initializing...";
|
||||||
|
}
|
||||||
|
case "INITIALIZED": {
|
||||||
|
switch (state.substate.type) {
|
||||||
|
case "READY_TO_LOAD_GRAPH": {
|
||||||
|
return {
|
||||||
|
LOADING: "Loading graph...",
|
||||||
|
NOT_LOADING: "Ready to load graph",
|
||||||
|
FAILED: "Error while loading graph",
|
||||||
|
}[state.substate.loading];
|
||||||
|
}
|
||||||
|
case "READY_TO_RUN_PAGERANK": {
|
||||||
|
return {
|
||||||
|
LOADING: "Running PageRank...",
|
||||||
|
NOT_LOADING: "Ready to run PageRank",
|
||||||
|
FAILED: "Error while running PageRank",
|
||||||
|
}[state.substate.loading];
|
||||||
|
}
|
||||||
|
case "PAGERANK_EVALUATED": {
|
||||||
|
return {
|
||||||
|
LOADING: "Re-running PageRank...",
|
||||||
|
NOT_LOADING: "",
|
||||||
|
FAILED: "Error while running PageRank",
|
||||||
|
}[state.substate.loading];
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error((state.substate.type: empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error((state.type: empty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,114 +1,335 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {shallow} from "enzyme";
|
import {shallow} from "enzyme";
|
||||||
|
|
||||||
|
import {Graph} from "../../core/graph";
|
||||||
|
import {makeRepo} from "../../core/repo";
|
||||||
import testLocalStore from "../testLocalStore";
|
import testLocalStore from "../testLocalStore";
|
||||||
import {pagerank} from "../../core/attribution/pagerank";
|
|
||||||
import {App} from "./App";
|
|
||||||
|
|
||||||
import {Graph, NodeAddress, EdgeAddress} from "../../core/graph";
|
import RepositorySelect from "./RepositorySelect";
|
||||||
|
import {PagerankTable} from "./PagerankTable";
|
||||||
|
import {WeightConfig} from "./WeightConfig";
|
||||||
|
import {createApp, LoadingIndicator} from "./App";
|
||||||
|
import {initialState} from "./state";
|
||||||
|
|
||||||
require("../testUtil").configureEnzyme();
|
require("../testUtil").configureEnzyme();
|
||||||
require("../testUtil").configureAphrodite();
|
|
||||||
|
|
||||||
function example() {
|
|
||||||
const graph = new Graph();
|
|
||||||
const nodes = {
|
|
||||||
fooAlpha: NodeAddress.fromParts(["foo", "a", "1"]),
|
|
||||||
fooBeta: NodeAddress.fromParts(["foo", "b", "2"]),
|
|
||||||
bar1: NodeAddress.fromParts(["bar", "a", "1"]),
|
|
||||||
bar2: NodeAddress.fromParts(["bar", "2"]),
|
|
||||||
xox: NodeAddress.fromParts(["xox"]),
|
|
||||||
empty: NodeAddress.empty,
|
|
||||||
};
|
|
||||||
Object.values(nodes).forEach((n) => graph.addNode((n: any)));
|
|
||||||
|
|
||||||
function addEdge(parts, src, dst) {
|
|
||||||
const edge = {address: EdgeAddress.fromParts(parts), src, dst};
|
|
||||||
graph.addEdge(edge);
|
|
||||||
}
|
|
||||||
|
|
||||||
addEdge(["a"], nodes.fooAlpha, nodes.fooBeta);
|
|
||||||
addEdge(["b"], nodes.fooAlpha, nodes.bar1);
|
|
||||||
addEdge(["c"], nodes.fooAlpha, nodes.xox);
|
|
||||||
addEdge(["d"], nodes.bar1, nodes.bar1);
|
|
||||||
addEdge(["e"], nodes.bar1, nodes.xox);
|
|
||||||
addEdge(["e'"], nodes.bar1, nodes.xox);
|
|
||||||
|
|
||||||
const adapters = [
|
|
||||||
{
|
|
||||||
name: () => "foo",
|
|
||||||
graph: () => {
|
|
||||||
throw new Error("unused");
|
|
||||||
},
|
|
||||||
renderer: () => ({
|
|
||||||
nodeDescription: (x) => `foo: ${NodeAddress.toString(x)}`,
|
|
||||||
}),
|
|
||||||
nodePrefix: () => NodeAddress.fromParts(["foo"]),
|
|
||||||
nodeTypes: () => [
|
|
||||||
{name: "alpha", prefix: NodeAddress.fromParts(["foo", "a"])},
|
|
||||||
{name: "beta", prefix: NodeAddress.fromParts(["foo", "b"])},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: () => "bar",
|
|
||||||
graph: () => {
|
|
||||||
throw new Error("unused");
|
|
||||||
},
|
|
||||||
renderer: () => ({
|
|
||||||
nodeDescription: (x) => `bar: ${NodeAddress.toString(x)}`,
|
|
||||||
}),
|
|
||||||
nodePrefix: () => NodeAddress.fromParts(["bar"]),
|
|
||||||
nodeTypes: () => [
|
|
||||||
{name: "alpha", prefix: NodeAddress.fromParts(["bar", "a"])},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: () => "xox",
|
|
||||||
graph: () => {
|
|
||||||
throw new Error("unused");
|
|
||||||
},
|
|
||||||
renderer: () => ({
|
|
||||||
nodeDescription: (_unused_arg) => `xox node!`,
|
|
||||||
}),
|
|
||||||
nodePrefix: () => NodeAddress.fromParts(["xox"]),
|
|
||||||
nodeTypes: () => [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: () => "unused",
|
|
||||||
graph: () => {
|
|
||||||
throw new Error("unused");
|
|
||||||
},
|
|
||||||
renderer: () => {
|
|
||||||
throw new Error("Impossible!");
|
|
||||||
},
|
|
||||||
nodePrefix: () => NodeAddress.fromParts(["unused"]),
|
|
||||||
nodeTypes: () => [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const pagerankResult = pagerank(graph, (_unused_Edge) => ({
|
|
||||||
toWeight: 1,
|
|
||||||
froWeight: 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {adapters, nodes, graph, pagerankResult};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("app/credExplorer/App", () => {
|
describe("app/credExplorer/App", () => {
|
||||||
it("renders with clean state", () => {
|
function example() {
|
||||||
shallow(<App localStore={testLocalStore()} />);
|
let setState, getState;
|
||||||
|
const setRepo = jest.fn();
|
||||||
|
const setEdgeEvaluator = jest.fn();
|
||||||
|
const loadGraph = jest.fn();
|
||||||
|
const runPagerank = jest.fn();
|
||||||
|
const localStore = testLocalStore();
|
||||||
|
function createMockSTM(_getState, _setState) {
|
||||||
|
setState = _setState;
|
||||||
|
getState = _getState;
|
||||||
|
return {
|
||||||
|
setRepo,
|
||||||
|
setEdgeEvaluator,
|
||||||
|
loadGraph,
|
||||||
|
runPagerank,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const App = createApp(createMockSTM);
|
||||||
|
const el = shallow(<App localStore={localStore} />);
|
||||||
|
if (setState == null || getState == null) {
|
||||||
|
throw new Error("Initialization problems");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
el,
|
||||||
|
setState,
|
||||||
|
getState,
|
||||||
|
setRepo,
|
||||||
|
setEdgeEvaluator,
|
||||||
|
loadGraph,
|
||||||
|
runPagerank,
|
||||||
|
localStore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEvaluator() {
|
||||||
|
return function(_unused_edge) {
|
||||||
|
return {toWeight: 1, froWeight: 1};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialized(substate) {
|
||||||
|
return {
|
||||||
|
type: "INITIALIZED",
|
||||||
|
repo: makeRepo("foo", "bar"),
|
||||||
|
edgeEvaluator: createEvaluator(),
|
||||||
|
substate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const exampleStates = {
|
||||||
|
uninitialized: initialState,
|
||||||
|
readyToLoadGraph: (loadingState) => {
|
||||||
|
return () =>
|
||||||
|
initialized({
|
||||||
|
type: "READY_TO_LOAD_GRAPH",
|
||||||
|
loading: loadingState,
|
||||||
});
|
});
|
||||||
it("renders with graph and adapters set", () => {
|
},
|
||||||
const app = shallow(<App localStore={testLocalStore()} />);
|
readyToRunPagerank: (loadingState) => {
|
||||||
const {graph, adapters} = example();
|
return () =>
|
||||||
const data = {graph, adapters, pagerankResult: null};
|
initialized({
|
||||||
app.setState({data});
|
type: "READY_TO_RUN_PAGERANK",
|
||||||
|
loading: loadingState,
|
||||||
|
graphWithAdapters: {graph: new Graph(), adapters: []},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
pagerankEvaluated: (loadingState) => {
|
||||||
|
return () =>
|
||||||
|
initialized({
|
||||||
|
type: "PAGERANK_EVALUATED",
|
||||||
|
loading: loadingState,
|
||||||
|
graphWithAdapters: {graph: new Graph(), adapters: []},
|
||||||
|
pagerankNodeDecomposition: new Map(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it("getState is wired properly", () => {
|
||||||
|
const {getState, el} = example();
|
||||||
|
expect(el.state().appState).toBe(getState());
|
||||||
|
});
|
||||||
|
it("setState is wired properly", () => {
|
||||||
|
const {setState, el} = example();
|
||||||
|
expect(initialState()).not.toBe(initialState()); // sanity check
|
||||||
|
const newState = initialState();
|
||||||
|
setState(newState);
|
||||||
|
expect(el.state().appState).toBe(newState);
|
||||||
|
});
|
||||||
|
it("localStore is wired properly", () => {
|
||||||
|
const {el, localStore} = example();
|
||||||
|
expect(el.instance().props.localStore).toBe(localStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when in state:", () => {
|
||||||
|
function testRepositorySelect(stateFn) {
|
||||||
|
it("creates a working RepositorySelect", () => {
|
||||||
|
const {el, setRepo, setState, localStore} = example();
|
||||||
|
setState(stateFn());
|
||||||
|
const rs = el.find(RepositorySelect);
|
||||||
|
const newRepo = makeRepo("zoo", "zod");
|
||||||
|
rs.props().onChange(newRepo);
|
||||||
|
expect(setRepo).toHaveBeenCalledWith(newRepo);
|
||||||
|
expect(rs.props().localStore).toBe(localStore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWeightConfig(stateFn) {
|
||||||
|
it("creates a working WeightConfig", () => {
|
||||||
|
const {el, setEdgeEvaluator, setState, localStore} = example();
|
||||||
|
setState(stateFn());
|
||||||
|
const wc = el.find(WeightConfig);
|
||||||
|
const ee = createEvaluator();
|
||||||
|
wc.props().onChange(ee);
|
||||||
|
expect(setEdgeEvaluator).toHaveBeenCalledWith(ee);
|
||||||
|
expect(wc.props().localStore).toBe(localStore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testGraphButton(stateFn, {disabled}) {
|
||||||
|
const adjective = disabled ? "disabled" : "working";
|
||||||
|
it(`has a ${adjective} load graph button`, () => {
|
||||||
|
const {el, loadGraph, setState} = example();
|
||||||
|
setState(stateFn());
|
||||||
|
el.update();
|
||||||
|
const button = el.findWhere(
|
||||||
|
(b) => b.text() === "Load graph" && b.is("button")
|
||||||
|
);
|
||||||
|
if (disabled) {
|
||||||
|
expect(button.props().disabled).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(button.props().disabled).toBe(false);
|
||||||
|
button.simulate("click");
|
||||||
|
expect(loadGraph).toBeCalledTimes(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPagerankButton(stateFn, {disabled}) {
|
||||||
|
const adjective = disabled ? "disabled" : "working";
|
||||||
|
it(`has a ${adjective} run PageRank button`, () => {
|
||||||
|
const {el, runPagerank, setState} = example();
|
||||||
|
setState(stateFn());
|
||||||
|
el.update();
|
||||||
|
const button = el.findWhere(
|
||||||
|
(b) => b.text() === "Run PageRank" && b.is("button")
|
||||||
|
);
|
||||||
|
if (disabled) {
|
||||||
|
expect(button.props().disabled).toBe(true);
|
||||||
|
} else {
|
||||||
|
expect(button.props().disabled).toBe(false);
|
||||||
|
button.simulate("click");
|
||||||
|
expect(runPagerank).toBeCalledTimes(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testPagerankTable(stateFn, present: boolean) {
|
||||||
|
const verb = present ? "has" : "doesn't have";
|
||||||
|
it(`${verb} a PagerankTable`, () => {
|
||||||
|
const {el, setState} = example();
|
||||||
|
const state = stateFn();
|
||||||
|
setState(state);
|
||||||
|
el.update();
|
||||||
|
const prt = el.find(PagerankTable);
|
||||||
|
if (present) {
|
||||||
|
expect(prt).toHaveLength(1);
|
||||||
|
if (
|
||||||
|
state.type !== "INITIALIZED" ||
|
||||||
|
state.substate.type !== "PAGERANK_EVALUATED"
|
||||||
|
) {
|
||||||
|
throw new Error("This test case is impossible to satisfy");
|
||||||
|
}
|
||||||
|
const adapters = state.substate.graphWithAdapters.adapters;
|
||||||
|
const pnd = state.substate.pagerankNodeDecomposition;
|
||||||
|
expect(prt.props().adapters).toBe(adapters);
|
||||||
|
expect(prt.props().pnd).toBe(pnd);
|
||||||
|
} else {
|
||||||
|
expect(prt).toHaveLength(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function testLoadingIndicator(stateFn) {
|
||||||
|
it("has a LoadingIndicator", () => {
|
||||||
|
const {el, setState} = example();
|
||||||
|
const state = stateFn();
|
||||||
|
setState(state);
|
||||||
|
el.update();
|
||||||
|
const li = el.find(LoadingIndicator);
|
||||||
|
expect(li.props().appState).toEqual(state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateTestSuite(
|
||||||
|
suiteName,
|
||||||
|
stateFn,
|
||||||
|
{loadGraphDisabled, runPagerankDisabled, hasPagerankTable}
|
||||||
|
) {
|
||||||
|
describe(suiteName, () => {
|
||||||
|
testWeightConfig(stateFn);
|
||||||
|
testRepositorySelect(stateFn);
|
||||||
|
testGraphButton(stateFn, {disabled: loadGraphDisabled});
|
||||||
|
testPagerankButton(stateFn, {disabled: runPagerankDisabled});
|
||||||
|
testPagerankTable(stateFn, hasPagerankTable);
|
||||||
|
testLoadingIndicator(stateFn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
stateTestSuite("UNINITIALIZED", exampleStates.uninitialized, {
|
||||||
|
loadGraphDisabled: true,
|
||||||
|
runPagerankDisabled: true,
|
||||||
|
hasPagerankTable: false,
|
||||||
|
});
|
||||||
|
describe("READY_TO_LOAD_GRAPH", () => {
|
||||||
|
for (const loadingState of ["LOADING", "NOT_LOADING", "FAILED"]) {
|
||||||
|
stateTestSuite(
|
||||||
|
loadingState,
|
||||||
|
exampleStates.readyToLoadGraph(loadingState),
|
||||||
|
{
|
||||||
|
loadGraphDisabled: false,
|
||||||
|
runPagerankDisabled: true,
|
||||||
|
hasPagerankTable: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("READY_TO_RUN_PAGERANK", () => {
|
||||||
|
for (const loadingState of ["LOADING", "NOT_LOADING", "FAILED"]) {
|
||||||
|
stateTestSuite(
|
||||||
|
loadingState,
|
||||||
|
exampleStates.readyToRunPagerank(loadingState),
|
||||||
|
{
|
||||||
|
loadGraphDisabled: true,
|
||||||
|
runPagerankDisabled: loadingState === "LOADING",
|
||||||
|
hasPagerankTable: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("PAGERANK_EVALUATED", () => {
|
||||||
|
for (const loadingState of ["LOADING", "NOT_LOADING", "FAILED"]) {
|
||||||
|
stateTestSuite(
|
||||||
|
loadingState,
|
||||||
|
exampleStates.pagerankEvaluated(loadingState),
|
||||||
|
{
|
||||||
|
loadGraphDisabled: true,
|
||||||
|
runPagerankDisabled: loadingState === "LOADING",
|
||||||
|
hasPagerankTable: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("LoadingIndicator", () => {
|
||||||
|
describe("displays the right status text when ", () => {
|
||||||
|
function testStatusText(stateName, stateFn, expectedText) {
|
||||||
|
it(stateName, () => {
|
||||||
|
const el = shallow(<LoadingIndicator appState={stateFn()} />);
|
||||||
|
expect(el.text()).toEqual(expectedText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
testStatusText(
|
||||||
|
"initializing",
|
||||||
|
exampleStates.uninitialized,
|
||||||
|
"Initializing..."
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"ready to load graph",
|
||||||
|
exampleStates.readyToLoadGraph("NOT_LOADING"),
|
||||||
|
"Ready to load graph"
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"loading graph",
|
||||||
|
exampleStates.readyToLoadGraph("LOADING"),
|
||||||
|
"Loading graph..."
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"failed to load graph",
|
||||||
|
exampleStates.readyToLoadGraph("FAILED"),
|
||||||
|
"Error while loading graph"
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"ready to run pagerank",
|
||||||
|
exampleStates.readyToRunPagerank("NOT_LOADING"),
|
||||||
|
"Ready to run PageRank"
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"running pagerank",
|
||||||
|
exampleStates.readyToRunPagerank("LOADING"),
|
||||||
|
"Running PageRank..."
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"pagerank failed",
|
||||||
|
exampleStates.readyToRunPagerank("FAILED"),
|
||||||
|
"Error while running PageRank"
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"pagerank succeeded",
|
||||||
|
exampleStates.pagerankEvaluated("NOT_LOADING"),
|
||||||
|
""
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"re-running pagerank",
|
||||||
|
exampleStates.pagerankEvaluated("LOADING"),
|
||||||
|
"Re-running PageRank..."
|
||||||
|
);
|
||||||
|
testStatusText(
|
||||||
|
"re-running pagerank failed",
|
||||||
|
exampleStates.pagerankEvaluated("FAILED"),
|
||||||
|
"Error while running PageRank"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
it("renders with graph and adapters and pagerankResult", () => {
|
|
||||||
const app = shallow(<App localStore={testLocalStore()} />);
|
|
||||||
const {graph, adapters, pagerankResult} = example();
|
|
||||||
const data = {graph, adapters, pagerankResult};
|
|
||||||
app.setState({data});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue