explorer: tweak weights on a per-node basis (#1143)

This pull request adds a weight slider to every NodeRow in the explorer,
enabling the user to manually set a weight for that node. The weights are
multiplicative with the type level weights, so that they can be changed
independently (e.g. you can have a comment that is weighted 2x higher than
regular comments, but still have comments get a low weight in general).

This pull coordinates a number of different changes across the codebase, all of
which are tested:

Adding support for manual weights in the weights and
weightsToEdgeEvaluator modules.
Modifying pagerankTable.TableRow so that it can show a slider in the second
column.
Adding piping for manual weights into the PagerankTable shared props, and
into the explorer app
Adding the slider to the NodeRow class that displays the current weight,
and can trigger the upstream weight change
Ensuring that the runPagerank call in the explorer actually uses the manual
weights
At present, there is no way to save these weights (they are ephemeral in the
frontend) and so this is clearly a prototype/tech demo level feature rather
than being ready for real usage. Correspondingly, CLI pagerank command always
uses an empty set of manual weights. I plan to remedy this in a follow-on pull
request.

Test plan: Run the included unit tests (yarn test) and also spin up the UI,
verify that it visually looks good in both Firefox and Chrome, and verify that
changing the weights and then re-running PageRank actually causes the cred of
the modified node to change.

Review plan: In addition to carefully reading the code, ensure that all of the
changes described a few paragraphs up are actually tested.

Merge plan: Squash and merge.

Thanks to @s-ben for proposing this feature in Discord, and to everyone
discussing its implications in this Discourse thread.
This commit is contained in:
Dandelion Mané 2019-05-18 19:20:27 +03:00
parent 2999d24593
commit d2559960bb
22 changed files with 306 additions and 84 deletions

View File

@ -2,6 +2,7 @@
## [Unreleased] ## [Unreleased]
- Allow tweaking weights on a per-node basis (#1143)
- Add the `pagerank` command (#1114) - Add the `pagerank` command (#1114)
- Add the `clear` command (#1111) - Add the `clear` command (#1111)
- Add description tooltips for node and edge types in the weight configuration UI (#1081) - Add description tooltips for node and edge types in the weight configuration UI (#1081)

View File

@ -19,6 +19,8 @@ export type WeightedTypes = {|
+edges: Map<EdgeAddressT, WeightedEdgeType>, +edges: Map<EdgeAddressT, WeightedEdgeType>,
|}; |};
export type ManualWeights = Map<NodeAddressT, number>;
export function defaultWeightedNodeType(type: NodeType): WeightedNodeType { export function defaultWeightedNodeType(type: NodeType): WeightedNodeType {
return { return {
type, type,

View File

@ -1,11 +1,15 @@
// @flow // @flow
import type {Edge} from "../core/graph"; import * as NullUtil from "../util/null";
import type {WeightedTypes} from "./weights"; import type {Edge, NodeAddressT} from "../core/graph";
import type {WeightedTypes, ManualWeights} from "./weights";
import type {EdgeEvaluator} from "./pagerank"; import type {EdgeEvaluator} from "./pagerank";
import {NodeTrie, EdgeTrie} from "../core/trie"; import {NodeTrie, EdgeTrie} from "../core/trie";
export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator { export function weightsToEdgeEvaluator(
weights: WeightedTypes,
manualWeights: ManualWeights
): EdgeEvaluator {
const nodeTrie = new NodeTrie(); const nodeTrie = new NodeTrie();
for (const {type, weight} of weights.nodes.values()) { for (const {type, weight} of weights.nodes.values()) {
nodeTrie.add(type.prefix, weight); nodeTrie.add(type.prefix, weight);
@ -15,9 +19,15 @@ export function weightsToEdgeEvaluator(weights: WeightedTypes): EdgeEvaluator {
edgeTrie.add(type.prefix, {forwardWeight, backwardWeight}); edgeTrie.add(type.prefix, {forwardWeight, backwardWeight});
} }
function nodeWeight(n: NodeAddressT): number {
const typeWeight = nodeTrie.getLast(n);
const manualWeight = NullUtil.orElse(manualWeights.get(n), 1);
return typeWeight * manualWeight;
}
return function evaluator(edge: Edge) { return function evaluator(edge: Edge) {
const srcWeight = nodeTrie.getLast(edge.src); const srcWeight = nodeWeight(edge.src);
const dstWeight = nodeTrie.getLast(edge.dst); const dstWeight = nodeWeight(edge.dst);
const {forwardWeight, backwardWeight} = edgeTrie.getLast(edge.address); const {forwardWeight, backwardWeight} = edgeTrie.getLast(edge.address);
return { return {
toWeight: dstWeight * forwardWeight, toWeight: dstWeight * forwardWeight,

View File

@ -7,7 +7,11 @@ import {
machineNodeType, machineNodeType,
assemblesEdgeType, assemblesEdgeType,
} from "../plugins/demo/declaration"; } from "../plugins/demo/declaration";
import {edges as factorioEdges} from "../plugins/demo/graph"; import {
edges as factorioEdges,
nodes as factorioNodes,
} from "../plugins/demo/graph";
import type {ManualWeights} from "./weights";
import {weightsToEdgeEvaluator} from "./weightsToEdgeEvaluator"; import {weightsToEdgeEvaluator} from "./weightsToEdgeEvaluator";
describe("analysis/weightsToEdgeEvaluator", () => { describe("analysis/weightsToEdgeEvaluator", () => {
@ -20,6 +24,7 @@ describe("analysis/weightsToEdgeEvaluator", () => {
+inserter?: number, +inserter?: number,
+machine?: number, +machine?: number,
+baseNode?: number, +baseNode?: number,
+manualWeights?: ManualWeights,
|}; |};
function weights({ function weights({
assemblesForward, assemblesForward,
@ -53,7 +58,8 @@ describe("analysis/weightsToEdgeEvaluator", () => {
} }
function exampleEdgeWeights(weightArgs: WeightArgs) { function exampleEdgeWeights(weightArgs: WeightArgs) {
const ws = weights(weightArgs); const ws = weights(weightArgs);
const ee = weightsToEdgeEvaluator(ws); const manualWeights = weightArgs.manualWeights || new Map();
const ee = weightsToEdgeEvaluator(ws, manualWeights);
// src is a machine, dst is an inserter, edge type is assembles // src is a machine, dst is an inserter, edge type is assembles
return ee(factorioEdges.assembles1); return ee(factorioEdges.assembles1);
} }
@ -91,5 +97,22 @@ describe("analysis/weightsToEdgeEvaluator", () => {
}) })
).toEqual({toWeight: 8, froWeight: 15}); ).toEqual({toWeight: 8, froWeight: 15});
}); });
it("manualWeight and nodeTypeWeight both multiply the weight", () => {
const manualWeights = new Map();
manualWeights.set(factorioNodes.inserter2, 2);
// Putting a weight of 2 on the inserter node type as a whole or on the the
// particular insterter node will have the same effect
expect(exampleEdgeWeights({inserter: 2})).toEqual(
exampleEdgeWeights({manualWeights})
);
});
it("manualWeight and nodeTypeWeight compose multiplicatively", () => {
const manualWeights = new Map();
manualWeights.set(factorioNodes.inserter2, 2);
expect(exampleEdgeWeights({inserter: 3, manualWeights})).toEqual({
toWeight: 6,
froWeight: 1,
});
});
}); });
}); });

View File

@ -20,6 +20,7 @@ import {loadGraph, type LoadGraphResult} from "../analysis/loadGraph";
import { import {
type WeightedTypes, type WeightedTypes,
type ManualWeights,
combineWeights, combineWeights,
defaultWeightsForDeclaration, defaultWeightsForDeclaration,
} from "../analysis/weights"; } from "../analysis/weights";
@ -144,10 +145,11 @@ export function makePagerankCommand(
} }
export async function runPagerank( export async function runPagerank(
weights: WeightedTypes, typeWeights: WeightedTypes,
manualWeights: ManualWeights,
graph: Graph graph: Graph
): Promise<PagerankGraph> { ): Promise<PagerankGraph> {
const evaluator = weightsToEdgeEvaluator(weights); const evaluator = weightsToEdgeEvaluator(typeWeights, manualWeights);
const pagerankGraph = new PagerankGraph( const pagerankGraph = new PagerankGraph(
graph, graph,
evaluator, evaluator,
@ -187,7 +189,8 @@ export const defaultAdapters = () => [
const defaultLoader = (r: RepoId) => const defaultLoader = (r: RepoId) =>
loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r); loadGraph(Common.sourcecredDirectory(), defaultAdapters(), r);
export const defaultWeights = () => weightsForAdapters(defaultAdapters()); export const defaultWeights = () => weightsForAdapters(defaultAdapters());
export const defaultPagerank = (g: Graph) => runPagerank(defaultWeights(), g); export const defaultPagerank = (g: Graph) =>
runPagerank(defaultWeights(), new Map(), g);
export const defaultSaver = (r: RepoId, pg: PagerankGraph) => export const defaultSaver = (r: RepoId, pg: PagerankGraph) =>
savePagerankGraph(Common.sourcecredDirectory(), r, pg); savePagerankGraph(Common.sourcecredDirectory(), r, pg);

View File

@ -230,11 +230,17 @@ describe("cli/pagerank", () => {
fallbackWeightedTypes, fallbackWeightedTypes,
]); ]);
const manualWeights = new Map();
manualWeights.set(advancedGraph().nodes.src(), 4);
const graph = advancedGraph().graph1(); const graph = advancedGraph().graph1();
const actualPagerankGraph = await runPagerank(weightedTypes, graph); const actualPagerankGraph = await runPagerank(
weightedTypes,
manualWeights,
graph
);
const expectedPagerankGraph = new PagerankGraph( const expectedPagerankGraph = new PagerankGraph(
graph, graph,
weightsToEdgeEvaluator(weightedTypes), weightsToEdgeEvaluator(weightedTypes, manualWeights),
DEFAULT_SYNTHETIC_LOOP_WEIGHT DEFAULT_SYNTHETIC_LOOP_WEIGHT
); );
await expectedPagerankGraph.runPagerank({ await expectedPagerankGraph.runPagerank({

View File

@ -8,6 +8,7 @@ import CheckedLocalStore from "../webutil/checkedLocalStore";
import BrowserLocalStore from "../webutil/browserLocalStore"; import BrowserLocalStore from "../webutil/browserLocalStore";
import Link from "../webutil/Link"; import Link from "../webutil/Link";
import type {RepoId} from "../core/repoId"; import type {RepoId} from "../core/repoId";
import {type NodeAddressT} from "../core/graph";
import {PagerankTable} from "./pagerankTable/Table"; import {PagerankTable} from "./pagerankTable/Table";
import type {WeightedTypes} from "../analysis/weights"; import type {WeightedTypes} from "../analysis/weights";
@ -61,6 +62,7 @@ type Props = {|
type State = {| type State = {|
appState: AppState, appState: AppState,
weightedTypes: WeightedTypes, weightedTypes: WeightedTypes,
manualWeights: Map<NodeAddressT, number>,
|}; |};
export function createApp( export function createApp(
@ -77,6 +79,7 @@ export function createApp(
this.state = { this.state = {
appState: initialState(this.props.repoId), appState: initialState(this.props.repoId),
weightedTypes: defaultWeightsForAdapterSet(props.adapters), weightedTypes: defaultWeightsForAdapterSet(props.adapters),
manualWeights: new Map(),
}; };
this.stateTransitionMachine = createSTM( this.stateTransitionMachine = createSTM(
() => this.state.appState, () => this.state.appState,
@ -98,6 +101,13 @@ export function createApp(
onWeightedTypesChange={(weightedTypes) => onWeightedTypesChange={(weightedTypes) =>
this.setState({weightedTypes}) this.setState({weightedTypes})
} }
manualWeights={this.state.manualWeights}
onManualWeightsChange={(addr: NodeAddressT, weight: number) =>
this.setState(({manualWeights}) => {
manualWeights.set(addr, weight);
return {manualWeights};
})
}
pnd={pnd} pnd={pnd}
maxEntriesPerList={100} maxEntriesPerList={100}
/> />
@ -124,6 +134,7 @@ export function createApp(
this.props.assets, this.props.assets,
this.props.adapters, this.props.adapters,
this.state.weightedTypes, this.state.weightedTypes,
this.state.manualWeights,
GithubPrefix.user GithubPrefix.user
) )
} }

View File

@ -3,7 +3,7 @@
import React from "react"; import React from "react";
import {shallow} from "enzyme"; import {shallow} from "enzyme";
import {Graph} from "../core/graph"; import {Graph, NodeAddress} from "../core/graph";
import {makeRepoId} from "../core/repoId"; import {makeRepoId} from "../core/repoId";
import {Assets} from "../webutil/assets"; import {Assets} from "../webutil/assets";
import testLocalStore from "../webutil/testLocalStore"; import testLocalStore from "../webutil/testLocalStore";
@ -153,6 +153,7 @@ describe("explorer/App", () => {
el.instance().props.assets, el.instance().props.assets,
el.instance().props.adapters, el.instance().props.adapters,
el.instance().state.weightedTypes, el.instance().state.weightedTypes,
el.instance().state.manualWeights,
GithubPrefix.user GithubPrefix.user
); );
} }
@ -184,6 +185,10 @@ describe("explorer/App", () => {
); );
prtWeightedTypesChange(newTypes); prtWeightedTypesChange(newTypes);
expect(el.instance().state.weightedTypes).toBe(newTypes); expect(el.instance().state.weightedTypes).toBe(newTypes);
const prtManualWeightsChange = prt.props().onManualWeightsChange;
const node = NodeAddress.fromParts(["foo"]);
prtManualWeightsChange(node, 32);
expect(el.instance().state.manualWeights.get(node)).toEqual(32);
} else { } else {
expect(prt).toHaveLength(0); expect(prt).toHaveLength(0);
} }

View File

@ -59,13 +59,14 @@ export class AggregationRow extends React.PureComponent<AggregationRowProps> {
const score = aggregation.summary.score; const score = aggregation.summary.score;
const {score: targetScore} = NullUtil.get(pnd.get(target)); const {score: targetScore} = NullUtil.get(pnd.get(target));
const connectionProportion = score / targetScore; const connectionProportion = score / targetScore;
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
return ( return (
<TableRow <TableRow
depth={depth} depth={depth}
indent={1} indent={1}
showPadding={false} showPadding={false}
connectionProportion={connectionProportion} multiuseColumn={connectionPercent}
cred={score} cred={score}
description={<AggregationView aggregation={aggregation} />} description={<AggregationView aggregation={aggregation} />}
> >

View File

@ -23,11 +23,9 @@ require("../../webutil/testUtil").configureEnzyme();
describe("explorer/pagerankTable/Aggregation", () => { describe("explorer/pagerankTable/Aggregation", () => {
describe("AggregationRowList", () => { describe("AggregationRowList", () => {
it("instantiates AggregationRows for each aggregation", async () => { it("instantiates AggregationRows for each aggregation", async () => {
const {adapters, pnd} = await example(); const {adapters, pnd, sharedProps} = await example();
const node = factorioNodes.inserter1; const node = factorioNodes.inserter1;
const depth = 20; const depth = 20;
const maxEntriesPerList = 50;
const sharedProps = {adapters, pnd, maxEntriesPerList};
const connections = NullUtil.get(pnd.get(node)).scoredConnections; const connections = NullUtil.get(pnd.get(node)).scoredConnections;
const aggregations = aggregateFlat( const aggregations = aggregateFlat(
connections, connections,
@ -58,8 +56,7 @@ describe("explorer/pagerankTable/Aggregation", () => {
describe("AggregationRow", () => { describe("AggregationRow", () => {
async function setup() { async function setup() {
const {pnd, adapters} = await example(); const {pnd, adapters, sharedProps} = await example();
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
const target = factorioNodes.inserter1; const target = factorioNodes.inserter1;
const {scoredConnections} = NullUtil.get(pnd.get(target)); const {scoredConnections} = NullUtil.get(pnd.get(target));
const aggregations = aggregateFlat( const aggregations = aggregateFlat(
@ -105,12 +102,12 @@ describe("explorer/pagerankTable/Aggregation", () => {
const {row, aggregation} = await setup(); const {row, aggregation} = await setup();
expect(row.props().cred).toBe(aggregation.summary.score); expect(row.props().cred).toBe(aggregation.summary.score);
}); });
it("with the aggregation's contribution proportion", async () => { it("with the aggregation's score contribution as a %", async () => {
const {row, target, aggregation, sharedProps} = await setup(); const {row, target, aggregation, sharedProps} = await setup();
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score; const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
expect(row.props().connectionProportion).toBe( const expectedPercent =
aggregation.summary.score / targetScore ((aggregation.summary.score * 100) / targetScore).toFixed(2) + "%";
); expect(row.props().multiuseColumn).toBe(expectedPercent);
}); });
it("with a AggregationView as description", async () => { it("with a AggregationView as description", async () => {
const {row, aggregation} = await setup(); const {row, aggregation} = await setup();

View File

@ -61,6 +61,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
const {pnd, adapters} = sharedProps; const {pnd, adapters} = sharedProps;
const {score: targetScore} = NullUtil.get(pnd.get(target)); const {score: targetScore} = NullUtil.get(pnd.get(target));
const connectionProportion = connectionScore / targetScore; const connectionProportion = connectionScore / targetScore;
const connectionPercent = (connectionProportion * 100).toFixed(2) + "%";
const connectionView = ( const connectionView = (
<ConnectionView connection={connection} adapters={adapters} /> <ConnectionView connection={connection} adapters={adapters} />
@ -70,7 +71,7 @@ export class ConnectionRow extends React.PureComponent<ConnectionRowProps> {
indent={2} indent={2}
depth={depth} depth={depth}
description={connectionView} description={connectionView}
connectionProportion={connectionProportion} multiuseColumn={connectionPercent}
showPadding={false} showPadding={false}
cred={connectionScore} cred={connectionScore}
> >

View File

@ -15,11 +15,11 @@ require("../../webutil/testUtil").configureEnzyme();
describe("explorer/pagerankTable/Connection", () => { describe("explorer/pagerankTable/Connection", () => {
describe("ConnectionRowList", () => { describe("ConnectionRowList", () => {
async function setup(maxEntriesPerList: number = 100000) { async function setup(maxEntriesPerList: number = 123) {
const {adapters, pnd} = await example(); let {sharedProps} = await example();
sharedProps = {...sharedProps, maxEntriesPerList};
const depth = 2; const depth = 2;
const node = factorioNodes.inserter1; const node = factorioNodes.inserter1;
const sharedProps = {adapters, pnd, maxEntriesPerList};
const connections = NullUtil.get(sharedProps.pnd.get(node)) const connections = NullUtil.get(sharedProps.pnd.get(node))
.scoredConnections; .scoredConnections;
const component = ( const component = (
@ -66,8 +66,7 @@ describe("explorer/pagerankTable/Connection", () => {
describe("ConnectionRow", () => { describe("ConnectionRow", () => {
async function setup() { async function setup() {
const {pnd, adapters} = await example(); const {pnd, sharedProps} = await example();
const sharedProps = {adapters, pnd, maxEntriesPerList: 123};
const target = factorioNodes.inserter1; const target = factorioNodes.inserter1;
const {scoredConnections} = NullUtil.get(pnd.get(target)); const {scoredConnections} = NullUtil.get(pnd.get(target));
const scoredConnection = scoredConnections[0]; const scoredConnection = scoredConnections[0];
@ -100,12 +99,13 @@ describe("explorer/pagerankTable/Connection", () => {
const {row, scoredConnection} = await setup(); const {row, scoredConnection} = await setup();
expect(row.props().cred).toBe(scoredConnection.connectionScore); expect(row.props().cred).toBe(scoredConnection.connectionScore);
}); });
it("with the connectionProportion", async () => { it("with the connectionProportion in the multiuseColumn", async () => {
const {row, target, scoredConnection, sharedProps} = await setup(); const {row, target, scoredConnection, sharedProps} = await setup();
const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score; const targetScore = NullUtil.get(sharedProps.pnd.get(target)).score;
expect(row.props().connectionProportion).toBe( const expectedPercent =
scoredConnection.connectionScore / targetScore ((scoredConnection.connectionScore * 100) / targetScore).toFixed(2) +
); "%";
expect(row.props().multiuseColumn).toBe(expectedPercent);
}); });
it("with a ConnectionView as description", async () => { it("with a ConnectionView as description", async () => {
const {row, sharedProps, scoredConnection} = await setup(); const {row, sharedProps, scoredConnection} = await setup();

View File

@ -6,6 +6,13 @@ import * as NullUtil from "../../util/null";
import {type NodeAddressT} from "../../core/graph"; import {type NodeAddressT} from "../../core/graph";
import {TableRow} from "./TableRow"; import {TableRow} from "./TableRow";
import {
MIN_SLIDER,
MAX_SLIDER,
formatWeight,
sliderToWeight,
weightToSlider,
} from "../weights/WeightSlider";
import {nodeDescription, type SharedProps} from "./shared"; import {nodeDescription, type SharedProps} from "./shared";
@ -48,8 +55,25 @@ export type NodeRowProps = {|
export class NodeRow extends React.PureComponent<NodeRowProps> { export class NodeRow extends React.PureComponent<NodeRowProps> {
render() { render() {
const {depth, node, sharedProps, showPadding} = this.props; const {depth, node, sharedProps, showPadding} = this.props;
const {pnd, adapters} = sharedProps; const {pnd, adapters, onManualWeightsChange, manualWeights} = sharedProps;
const {score} = NullUtil.get(pnd.get(node)); const {score} = NullUtil.get(pnd.get(node));
const weight = NullUtil.orElse(manualWeights.get(node), 1);
const slider = (
<label>
<span>{formatWeight(weight)}</span>
<input
type="range"
min={MIN_SLIDER}
max={MAX_SLIDER}
step={1}
value={weightToSlider(weight)}
onChange={(e) => {
const weight = sliderToWeight(e.target.valueAsNumber);
onManualWeightsChange(node, weight);
}}
/>
</label>
);
const description = <span>{nodeDescription(node, adapters)}</span>; const description = <span>{nodeDescription(node, adapters)}</span>;
return ( return (
<TableRow <TableRow
@ -57,7 +81,7 @@ export class NodeRow extends React.PureComponent<NodeRowProps> {
indent={0} indent={0}
showPadding={showPadding} showPadding={showPadding}
description={description} description={description}
connectionProportion={null} multiuseColumn={slider}
cred={score} cred={score}
> >
<AggregationRowList <AggregationRowList

View File

@ -6,6 +6,12 @@ import sortBy from "lodash.sortby";
import * as NullUtil from "../../util/null"; import * as NullUtil from "../../util/null";
import {TableRow} from "./TableRow"; import {TableRow} from "./TableRow";
import {AggregationRowList} from "./Aggregation"; import {AggregationRowList} from "./Aggregation";
import {
MIN_SLIDER,
MAX_SLIDER,
sliderToWeight,
weightToSlider,
} from "../weights/WeightSlider";
import type {NodeAddressT} from "../../core/graph"; import type {NodeAddressT} from "../../core/graph";
@ -21,11 +27,11 @@ describe("explorer/pagerankTable/Node", () => {
function sortedByScore(nodes: $ReadOnlyArray<NodeAddressT>, pnd) { function sortedByScore(nodes: $ReadOnlyArray<NodeAddressT>, pnd) {
return sortBy(nodes, (node) => -NullUtil.get(pnd.get(node)).score); return sortBy(nodes, (node) => -NullUtil.get(pnd.get(node)).score);
} }
async function setup(maxEntriesPerList: number = 100000) { async function setup(maxEntriesPerList: number = 123) {
const {adapters, pnd} = await example(); const {adapters, pnd, sharedProps: changeEntries} = await example();
const sharedProps = {...changeEntries, maxEntriesPerList};
const nodes = Array.from(pnd.keys()); const nodes = Array.from(pnd.keys());
expect(nodes).not.toHaveLength(0); expect(nodes).not.toHaveLength(0);
const sharedProps = {adapters, pnd, maxEntriesPerList};
const component = <NodeRowList sharedProps={sharedProps} nodes={nodes} />; const component = <NodeRowList sharedProps={sharedProps} nodes={nodes} />;
const element = shallow(component); const element = shallow(component);
return {element, adapters, sharedProps, nodes}; return {element, adapters, sharedProps, nodes};
@ -71,15 +77,17 @@ describe("explorer/pagerankTable/Node", () => {
describe("NodeRow", () => { describe("NodeRow", () => {
async function setup(props: $Shape<{...NodeRowProps}>) { async function setup(props: $Shape<{...NodeRowProps}>) {
props = props || {}; props = props || {};
const {pnd, adapters} = await example(); let {sharedProps} = await example();
const sharedProps = {adapters, pnd, maxEntriesPerList: 123}; if (props.sharedProps !== null) {
sharedProps = {...sharedProps, ...props.sharedProps};
}
const node = factorioNodes.inserter1; const node = factorioNodes.inserter1;
const component = shallow( const component = shallow(
<NodeRow <NodeRow
node={NullUtil.orElse(props.node, node)} node={NullUtil.orElse(props.node, node)}
showPadding={NullUtil.orElse(props.showPadding, false)} showPadding={NullUtil.orElse(props.showPadding, false)}
depth={NullUtil.orElse(props.depth, 0)} depth={NullUtil.orElse(props.depth, 0)}
sharedProps={NullUtil.orElse(props.sharedProps, sharedProps)} sharedProps={sharedProps}
/> />
); );
const row = component.find(TableRow); const row = component.find(TableRow);
@ -103,9 +111,58 @@ describe("explorer/pagerankTable/Node", () => {
const score = NullUtil.get(sharedProps.pnd.get(node)).score; const score = NullUtil.get(sharedProps.pnd.get(node)).score;
expect(row.props().cred).toBe(score); expect(row.props().cred).toBe(score);
}); });
it("with no connectionProportion", async () => { describe("with a weight slider", () => {
const {row} = await setup(); async function setupSlider(initialWeight: number = 0) {
expect(row.props().connectionProportion).not.toEqual(expect.anything()); const node = factorioNodes.inserter1;
const manualWeights = new Map([[node, initialWeight]]);
const partialSharedProps: any = {manualWeights};
const {row, sharedProps} = await setup({
sharedProps: partialSharedProps,
});
const multiuseColumn = shallow(row.props().multiuseColumn);
const label = multiuseColumn.find("label");
const input = label.find("input");
const span = label.find("span");
const {onManualWeightsChange} = sharedProps;
return {
onManualWeightsChange,
manualWeights,
input,
span,
node,
label,
};
}
it("which consists of a range input and span within a label", async () => {
const {label, input, span} = await setupSlider();
expect(label).toHaveLength(1);
expect(input).toHaveLength(1);
expect(input.props().type).toEqual("range");
expect(span).toHaveLength(1);
});
it("whose onChange triggers onManualWeightsChange", async () => {
const {node, input, onManualWeightsChange} = await setupSlider();
expect(onManualWeightsChange).toHaveBeenCalledTimes(0);
input.simulate("change", {target: {valueAsNumber: MIN_SLIDER}});
expect(onManualWeightsChange).toHaveBeenLastCalledWith(
node,
sliderToWeight(MIN_SLIDER)
);
input.simulate("change", {target: {valueAsNumber: MAX_SLIDER}});
expect(onManualWeightsChange).toHaveBeenLastCalledWith(
node,
sliderToWeight(MAX_SLIDER)
);
expect(onManualWeightsChange).toHaveBeenCalledTimes(2);
});
it("which encodes the weight in slider position", async () => {
const {input} = await setupSlider(4);
expect(input.props().value).toEqual(weightToSlider(4));
});
it("which prints the weight in text format", async () => {
const {span} = await setupSlider(4);
expect(span.text()).toEqual("4×");
});
}); });
it("with the node description", async () => { it("with the node description", async () => {
const {row, sharedProps, node} = await setup(); const {row, sharedProps, node} = await setup();

View File

@ -4,7 +4,7 @@ import React from "react";
import sortBy from "lodash.sortby"; import sortBy from "lodash.sortby";
import * as NullUtil from "../../util/null"; import * as NullUtil from "../../util/null";
import {NodeAddress} from "../../core/graph"; import {NodeAddress, type NodeAddressT} from "../../core/graph";
import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition"; import type {PagerankNodeDecomposition} from "../../analysis/pagerankNodeDecomposition";
import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet"; import {DynamicExplorerAdapterSet} from "../adapters/explorerAdapterSet";
import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter"; import type {DynamicExplorerAdapter} from "../adapters/explorerAdapter";
@ -22,6 +22,8 @@ type PagerankTableProps = {|
+onWeightedTypesChange: (WeightedTypes) => void, +onWeightedTypesChange: (WeightedTypes) => void,
+maxEntriesPerList: number, +maxEntriesPerList: number,
+defaultNodeType: ?NodeType, +defaultNodeType: ?NodeType,
+manualWeights: Map<NodeAddressT, number>,
+onManualWeightsChange: (NodeAddressT, number) => void,
|}; |};
type PagerankTableState = {| type PagerankTableState = {|
selectedNodeType: NodeType, selectedNodeType: NodeType,
@ -128,12 +130,24 @@ export class PagerankTable extends React.PureComponent<
} }
renderTable() { renderTable() {
const {pnd, adapters, maxEntriesPerList} = this.props; const {
pnd,
adapters,
maxEntriesPerList,
manualWeights,
onManualWeightsChange,
} = this.props;
if (pnd == null || adapters == null || maxEntriesPerList == null) { if (pnd == null || adapters == null || maxEntriesPerList == null) {
throw new Error("Impossible."); throw new Error("Impossible.");
} }
const selectedNodeTypePrefix = this.state.selectedNodeType.prefix; const selectedNodeTypePrefix = this.state.selectedNodeType.prefix;
const sharedProps = {pnd, adapters, maxEntriesPerList}; const sharedProps = {
pnd,
adapters,
maxEntriesPerList,
manualWeights,
onManualWeightsChange,
};
return ( return (
<table <table
style={{ style={{

View File

@ -18,9 +18,16 @@ require("../../webutil/testUtil").configureEnzyme();
describe("explorer/pagerankTable/Table", () => { describe("explorer/pagerankTable/Table", () => {
describe("PagerankTable", () => { describe("PagerankTable", () => {
async function setup(defaultNodeType?: NodeType) { async function setup(defaultNodeType?: NodeType) {
const {pnd, adapters, weightedTypes} = await example(); const {
const onWeightedTypesChange = jest.fn(); pnd,
const maxEntriesPerList = 321; adapters,
weightedTypes,
sharedProps,
manualWeights,
onManualWeightsChange,
onWeightedTypesChange,
maxEntriesPerList,
} = await example();
const element = shallow( const element = shallow(
<PagerankTable <PagerankTable
defaultNodeType={defaultNodeType} defaultNodeType={defaultNodeType}
@ -29,9 +36,20 @@ describe("explorer/pagerankTable/Table", () => {
pnd={pnd} pnd={pnd}
adapters={adapters} adapters={adapters}
maxEntriesPerList={maxEntriesPerList} maxEntriesPerList={maxEntriesPerList}
manualWeights={manualWeights}
onManualWeightsChange={onManualWeightsChange}
/> />
); );
return {pnd, adapters, element, maxEntriesPerList, onWeightedTypesChange}; return {
pnd,
adapters,
element,
maxEntriesPerList,
onWeightedTypesChange,
onManualWeightsChange,
manualWeights,
sharedProps,
};
} }
it("renders thead column order properly", async () => { it("renders thead column order properly", async () => {
const {element} = await setup(); const {element} = await setup();
@ -145,10 +163,9 @@ describe("explorer/pagerankTable/Table", () => {
describe("creates a NodeRowList", () => { describe("creates a NodeRowList", () => {
it("with the correct SharedProps", async () => { it("with the correct SharedProps", async () => {
const {element, adapters, pnd, maxEntriesPerList} = await setup(); const {element, sharedProps} = await setup();
const nrl = element.find(NodeRowList); const nrl = element.find(NodeRowList);
const expectedSharedProps = {adapters, pnd, maxEntriesPerList}; expect(nrl.props().sharedProps).toEqual(sharedProps);
expect(nrl.props().sharedProps).toEqual(expectedSharedProps);
}); });
it("including all nodes by default", async () => { it("including all nodes by default", async () => {
const {element, pnd} = await setup(); const {element, pnd} = await setup();

View File

@ -10,8 +10,10 @@ type TableRowProps = {|
+indent: number, +indent: number,
// The node that goes in the Description column // The node that goes in the Description column
+description: ReactNode, +description: ReactNode,
// What proportion should be formatted in the connection column
+connectionProportion: ?number, // The content for the "multiuse column"
// Could be a weight slider or a cred proportion depending on context.
+multiuseColumn: ReactNode,
// The cred amount to format and display // The cred amount to format and display
+cred: number, +cred: number,
// Children to show when the row is expanded // Children to show when the row is expanded
@ -39,16 +41,12 @@ export class TableRow extends React.PureComponent<
depth, depth,
indent, indent,
description, description,
connectionProportion,
cred, cred,
children, children,
showPadding, showPadding,
multiuseColumn,
} = this.props; } = this.props;
const {expanded} = this.state; const {expanded} = this.state;
const percent =
connectionProportion == null
? ""
: (connectionProportion * 100).toFixed(2) + "%";
const backgroundColor = `hsla(150,100%,28%,${1 - 0.9 ** depth})`; const backgroundColor = `hsla(150,100%,28%,${1 - 0.9 ** depth})`;
const makeGradient = (color) => const makeGradient = (color) =>
`linear-gradient(to top, ${color}, ${color})`; `linear-gradient(to top, ${color}, ${color})`;
@ -80,7 +78,7 @@ export class TableRow extends React.PureComponent<
</button> </button>
{description} {description}
</td> </td>
<td style={{textAlign: "right"}}>{percent}</td> <td style={{textAlign: "right"}}>{multiuseColumn}</td>
<td style={{textAlign: "right"}}> <td style={{textAlign: "right"}}>
<span style={{marginRight: 5}}>{credDisplay(cred)}</span> <span style={{marginRight: 5}}>{credDisplay(cred)}</span>
</td> </td>

View File

@ -15,7 +15,7 @@ describe("explorer/pagerankTable/TableRow", () => {
depth={1} depth={1}
indent={1} indent={1}
description={<span data-test-description={true} />} description={<span data-test-description={true} />}
connectionProportion={0.5} multiuseColumn={"50.00%"}
cred={133.7} cred={133.7}
children={<div data-test-children={true} />} children={<div data-test-children={true} />}
showPadding={false} showPadding={false}
@ -30,7 +30,7 @@ describe("explorer/pagerankTable/TableRow", () => {
indent={1} indent={1}
showPadding={false} showPadding={false}
description={<span data-test-description={true} />} description={<span data-test-description={true} />}
connectionProportion={0.5} multiuseColumn={"50.00%"}
cred={133.7} cred={133.7}
children={<div data-test-children={true} />} children={<div data-test-children={true} />}
/> />
@ -50,7 +50,7 @@ describe("explorer/pagerankTable/TableRow", () => {
indent={indent} indent={indent}
showPadding={false} showPadding={false}
description={<span data-test-description={true} />} description={<span data-test-description={true} />}
connectionProportion={0.5} multiuseColumn={"50.00%"}
cred={133.7} cred={133.7}
children={<div data-test-children={true} />} children={<div data-test-children={true} />}
/> />
@ -92,7 +92,7 @@ describe("explorer/pagerankTable/TableRow", () => {
const el = example(); const el = example();
expect(el.find("td")).toHaveLength(COLUMNS().length); expect(el.find("td")).toHaveLength(COLUMNS().length);
}); });
it("displays formatted connectionPercentage in the correct column", () => { it("can display literal text in the multiuseColumn", () => {
const index = COLUMNS().indexOf(""); const index = COLUMNS().indexOf("");
expect(index).not.toEqual(-1); expect(index).not.toEqual(-1);
const td = example() const td = example()
@ -100,13 +100,14 @@ describe("explorer/pagerankTable/TableRow", () => {
.at(index); .at(index);
expect(td.text()).toEqual("50.00%"); expect(td.text()).toEqual("50.00%");
}); });
it("displays empty column when connectionProportion not set", () => { it("displays general react nodes in the multiuseColumn", () => {
const index = COLUMNS().indexOf(""); const index = COLUMNS().indexOf("");
expect(index).not.toEqual(-1); expect(index).not.toEqual(-1);
const el = example(); const el = example();
el.setProps({connectionProportion: null}); const multiuseColumn = <span data-test-multiuse={true} />;
el.setProps({multiuseColumn});
const td = el.find("td").at(index); const td = el.find("td").at(index);
expect(td.text()).toEqual(""); expect(td.find({"data-test-multiuse": true})).toHaveLength(1);
}); });
it("displays formatted cred in the correct column", () => { it("displays formatted cred in the correct column", () => {
const index = COLUMNS().indexOf("Cred"); const index = COLUMNS().indexOf("Cred");
@ -135,7 +136,7 @@ describe("explorer/pagerankTable/TableRow", () => {
depth={2} depth={2}
indent={1} indent={1}
description={<span data-test-description={true} />} description={<span data-test-description={true} />}
connectionProportion={0.5} multiuseColumn={"50.00%"}
cred={133.7} cred={133.7}
children={<div data-test-children={true} />} children={<div data-test-children={true} />}
showPadding={true} showPadding={true}

View File

@ -38,6 +38,8 @@ export type SharedProps = {|
+pnd: PagerankNodeDecomposition, +pnd: PagerankNodeDecomposition,
+adapters: DynamicExplorerAdapterSet, +adapters: DynamicExplorerAdapterSet,
+maxEntriesPerList: number, +maxEntriesPerList: number,
+manualWeights: Map<NodeAddressT, number>,
+onManualWeightsChange: (NodeAddressT, number) => void,
|}; |};
export function Badge({children}: {children: ReactNode}): ReactNode { export function Badge({children}: {children: ReactNode}): ReactNode {

View File

@ -1,8 +1,11 @@
// @flow // @flow
import {type NodeAddressT} from "../../core/graph";
import {pagerank} from "../../analysis/pagerank"; import {pagerank} from "../../analysis/pagerank";
import type {WeightedTypes} from "../../analysis/weights";
import {defaultWeightsForAdapterSet} from "../weights/weights"; import {defaultWeightsForAdapterSet} from "../weights/weights";
import {dynamicExplorerAdapterSet} from "../../plugins/demo/explorerAdapter"; import {dynamicExplorerAdapterSet} from "../../plugins/demo/explorerAdapter";
import type {SharedProps} from "./shared";
export const COLUMNS = () => ["Description", "", "Cred"]; export const COLUMNS = () => ["Description", "", "Cred"];
@ -14,6 +17,27 @@ export async function example() {
toWeight: 1, toWeight: 1,
froWeight: 1, froWeight: 1,
})); }));
const maxEntriesPerList = 123;
const manualWeights: Map<NodeAddressT, number> = new Map();
const onManualWeightsChange: (NodeAddressT, number) => void = jest.fn();
const onWeightedTypesChange: (WeightedTypes) => void = jest.fn();
return {adapters, pnd, weightedTypes}; const sharedProps: SharedProps = {
adapters,
pnd,
maxEntriesPerList,
manualWeights,
onManualWeightsChange,
};
return {
adapters,
pnd,
weightedTypes,
maxEntriesPerList,
sharedProps,
manualWeights,
onManualWeightsChange,
onWeightedTypesChange,
};
} }

View File

@ -52,6 +52,8 @@ export type PagerankEvaluated = {|
+loading: LoadingState, +loading: LoadingState,
|}; |};
type ManualWeights = Map<NodeAddressT, number>;
export function initialState(repoId: RepoId): ReadyToLoadGraph { export function initialState(repoId: RepoId): ReadyToLoadGraph {
return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"}; return {type: "READY_TO_LOAD_GRAPH", repoId, loading: "NOT_LOADING"};
} }
@ -71,11 +73,12 @@ export function createStateTransitionMachine(
// Exported for testing purposes. // Exported for testing purposes.
export interface StateTransitionMachineInterface { export interface StateTransitionMachineInterface {
+loadGraph: (Assets, StaticExplorerAdapterSet) => Promise<boolean>; +loadGraph: (Assets, StaticExplorerAdapterSet) => Promise<boolean>;
+runPagerank: (WeightedTypes, NodeAddressT) => Promise<void>; +runPagerank: (WeightedTypes, ManualWeights, NodeAddressT) => Promise<void>;
+loadGraphAndRunPagerank: ( +loadGraphAndRunPagerank: (
Assets, Assets,
StaticExplorerAdapterSet, StaticExplorerAdapterSet,
WeightedTypes, WeightedTypes,
ManualWeights,
NodeAddressT NodeAddressT
) => Promise<void>; ) => Promise<void>;
} }
@ -157,6 +160,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
async runPagerank( async runPagerank(
weightedTypes: WeightedTypes, weightedTypes: WeightedTypes,
manualWeights: ManualWeights,
totalScoreNodePrefix: NodeAddressT totalScoreNodePrefix: NodeAddressT
) { ) {
const state = this.getState(); const state = this.getState();
@ -177,7 +181,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
try { try {
const pagerankNodeDecomposition = await this.pagerank( const pagerankNodeDecomposition = await this.pagerank(
graph, graph,
weightsToEdgeEvaluator(weightedTypes), weightsToEdgeEvaluator(weightedTypes, manualWeights),
{ {
verbose: true, verbose: true,
totalScoreNodePrefix: totalScoreNodePrefix, totalScoreNodePrefix: totalScoreNodePrefix,
@ -207,6 +211,7 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
assets: Assets, assets: Assets,
adapters: StaticExplorerAdapterSet, adapters: StaticExplorerAdapterSet,
weightedTypes: WeightedTypes, weightedTypes: WeightedTypes,
manualWeights: ManualWeights,
totalScoreNodePrefix: NodeAddressT totalScoreNodePrefix: NodeAddressT
) { ) {
const state = this.getState(); const state = this.getState();
@ -215,12 +220,20 @@ export class StateTransitionMachine implements StateTransitionMachineInterface {
case "READY_TO_LOAD_GRAPH": case "READY_TO_LOAD_GRAPH":
const loadedGraph = await this.loadGraph(assets, adapters); const loadedGraph = await this.loadGraph(assets, adapters);
if (loadedGraph) { if (loadedGraph) {
await this.runPagerank(weightedTypes, totalScoreNodePrefix); await this.runPagerank(
weightedTypes,
manualWeights,
totalScoreNodePrefix
);
} }
break; break;
case "READY_TO_RUN_PAGERANK": case "READY_TO_RUN_PAGERANK":
case "PAGERANK_EVALUATED": case "PAGERANK_EVALUATED":
await this.runPagerank(weightedTypes, totalScoreNodePrefix); await this.runPagerank(
weightedTypes,
manualWeights,
totalScoreNodePrefix
);
break; break;
default: default:
throw new Error((type: empty)); throw new Error((type: empty));

View File

@ -167,7 +167,7 @@ describe("explorer/state", () => {
const badState = readyToLoadGraph(); const badState = readyToLoadGraph();
const {stm} = example(badState); const {stm} = example(badState);
await expect( await expect(
stm.runPagerank(weightedTypes(), NodeAddress.empty) stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty)
).rejects.toThrow("incorrect state"); ).rejects.toThrow("incorrect state");
}); });
it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => { it("can be run when READY_TO_RUN_PAGERANK or PAGERANK_EVALUATED", async () => {
@ -176,7 +176,7 @@ describe("explorer/state", () => {
const {stm, getState, pagerankMock} = example(g); const {stm, getState, pagerankMock} = example(g);
const pnd = pagerankNodeDecomposition(); const pnd = pagerankNodeDecomposition();
pagerankMock.mockResolvedValue(pnd); pagerankMock.mockResolvedValue(pnd);
await stm.runPagerank(weightedTypes(), NodeAddress.empty); await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
const state = getState(); const state = getState();
if (state.type !== "PAGERANK_EVALUATED") { if (state.type !== "PAGERANK_EVALUATED") {
throw new Error("Impossible"); throw new Error("Impossible");
@ -188,13 +188,13 @@ describe("explorer/state", () => {
it("immediately sets loading status", () => { it("immediately sets loading status", () => {
const {getState, stm} = example(readyToRunPagerank()); const {getState, stm} = example(readyToRunPagerank());
expect(loading(getState())).toBe("NOT_LOADING"); expect(loading(getState())).toBe("NOT_LOADING");
stm.runPagerank(weightedTypes(), NodeAddress.empty); stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
expect(loading(getState())).toBe("LOADING"); expect(loading(getState())).toBe("LOADING");
}); });
it("calls pagerank with the totalScoreNodePrefix option", async () => { it("calls pagerank with the totalScoreNodePrefix option", async () => {
const {pagerankMock, stm} = example(readyToRunPagerank()); const {pagerankMock, stm} = example(readyToRunPagerank());
const foo = NodeAddress.fromParts(["foo"]); const foo = NodeAddress.fromParts(["foo"]);
await stm.runPagerank(weightedTypes(), foo); await stm.runPagerank(weightedTypes(), new Map(), foo);
const args = pagerankMock.mock.calls[0]; const args = pagerankMock.mock.calls[0];
expect(args[2].totalScoreNodePrefix).toBe(foo); expect(args[2].totalScoreNodePrefix).toBe(foo);
}); });
@ -204,7 +204,7 @@ describe("explorer/state", () => {
// $ExpectFlowError // $ExpectFlowError
console.error = jest.fn(); console.error = jest.fn();
pagerankMock.mockRejectedValue(error); pagerankMock.mockRejectedValue(error);
await stm.runPagerank(weightedTypes(), NodeAddress.empty); await stm.runPagerank(weightedTypes(), new Map(), NodeAddress.empty);
const state = getState(); const state = getState();
expect(loading(state)).toBe("FAILED"); expect(loading(state)).toBe("FAILED");
expect(state.type).toBe("READY_TO_RUN_PAGERANK"); expect(state.type).toBe("READY_TO_RUN_PAGERANK");
@ -222,12 +222,19 @@ describe("explorer/state", () => {
const assets = new Assets("/gateway/"); const assets = new Assets("/gateway/");
const adapters = new StaticExplorerAdapterSet([]); const adapters = new StaticExplorerAdapterSet([]);
const prefix = NodeAddress.fromParts(["bar"]); const prefix = NodeAddress.fromParts(["bar"]);
const manualWeights = new Map();
const wt = weightedTypes(); const wt = weightedTypes();
await stm.loadGraphAndRunPagerank(assets, adapters, wt, prefix); await stm.loadGraphAndRunPagerank(
assets,
adapters,
wt,
manualWeights,
prefix
);
expect(stm.loadGraph).toHaveBeenCalledTimes(1); expect(stm.loadGraph).toHaveBeenCalledTimes(1);
expect(stm.loadGraph).toHaveBeenCalledWith(assets, adapters); expect(stm.loadGraph).toHaveBeenCalledWith(assets, adapters);
expect(stm.runPagerank).toHaveBeenCalledTimes(1); expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix); expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
}); });
it("does not run pagerank if loadGraph did not succeed", async () => { it("does not run pagerank if loadGraph did not succeed", async () => {
const {stm} = example(readyToLoadGraph()); const {stm} = example(readyToLoadGraph());
@ -241,6 +248,7 @@ describe("explorer/state", () => {
assets, assets,
adapters, adapters,
weightedTypes(), weightedTypes(),
new Map(),
prefix prefix
); );
expect(stm.loadGraph).toHaveBeenCalledTimes(1); expect(stm.loadGraph).toHaveBeenCalledTimes(1);
@ -252,15 +260,17 @@ describe("explorer/state", () => {
(stm: any).runPagerank = jest.fn(); (stm: any).runPagerank = jest.fn();
const prefix = NodeAddress.fromParts(["bar"]); const prefix = NodeAddress.fromParts(["bar"]);
const wt = weightedTypes(); const wt = weightedTypes();
const manualWeights = new Map();
await stm.loadGraphAndRunPagerank( await stm.loadGraphAndRunPagerank(
new Assets("/gateway/"), new Assets("/gateway/"),
new StaticExplorerAdapterSet([]), new StaticExplorerAdapterSet([]),
wt, wt,
manualWeights,
prefix prefix
); );
expect(stm.loadGraph).toHaveBeenCalledTimes(0); expect(stm.loadGraph).toHaveBeenCalledTimes(0);
expect(stm.runPagerank).toHaveBeenCalledTimes(1); expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix); expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
}); });
it("when PAGERANK_EVALUATED, runs pagerank", async () => { it("when PAGERANK_EVALUATED, runs pagerank", async () => {
const {stm} = example(pagerankEvaluated()); const {stm} = example(pagerankEvaluated());
@ -268,15 +278,17 @@ describe("explorer/state", () => {
(stm: any).runPagerank = jest.fn(); (stm: any).runPagerank = jest.fn();
const prefix = NodeAddress.fromParts(["bar"]); const prefix = NodeAddress.fromParts(["bar"]);
const wt = weightedTypes(); const wt = weightedTypes();
const manualWeights = new Map();
await stm.loadGraphAndRunPagerank( await stm.loadGraphAndRunPagerank(
new Assets("/gateway/"), new Assets("/gateway/"),
new StaticExplorerAdapterSet([]), new StaticExplorerAdapterSet([]),
wt, wt,
manualWeights,
prefix prefix
); );
expect(stm.loadGraph).toHaveBeenCalledTimes(0); expect(stm.loadGraph).toHaveBeenCalledTimes(0);
expect(stm.runPagerank).toHaveBeenCalledTimes(1); expect(stm.runPagerank).toHaveBeenCalledTimes(1);
expect(stm.runPagerank).toHaveBeenCalledWith(wt, prefix); expect(stm.runPagerank).toHaveBeenCalledWith(wt, manualWeights, prefix);
}); });
}); });
}); });