Recreate Pagerank Table (#466)
Ports #265 to the v3 branch, along with some tweaks: - Only display log score, and normalize them by adding 10 (so that most are non-negative) - Change colors to a soothing green - Improve display, e.g. make overflowing node description text wrap within the row Implements most of the tests requested in #269 Test plan: Many unit tests added Paired with @wchargin
This commit is contained in:
parent
72be58a5c0
commit
65b5babac4
|
@ -0,0 +1,252 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import sortBy from "lodash.sortby";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Graph,
|
||||||
|
NodeAddress,
|
||||||
|
type NodeAddressT,
|
||||||
|
Direction,
|
||||||
|
EdgeAddress,
|
||||||
|
} from "../../core/graph";
|
||||||
|
import type {PagerankResult} from "../../core/attribution/pagerank";
|
||||||
|
import type {PluginAdapter} from "../pluginAdapter";
|
||||||
|
|
||||||
|
const MAX_TABLE_ENTRIES = 100;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pagerankResult: ?PagerankResult,
|
||||||
|
graph: ?Graph,
|
||||||
|
adapters: ?$ReadOnlyArray<PluginAdapter>,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
topLevelFilter: NodeAddressT,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Factor this out and test it (#465)
|
||||||
|
export function nodeDescription(
|
||||||
|
address: NodeAddressT,
|
||||||
|
adapters: $ReadOnlyArray<PluginAdapter>
|
||||||
|
): string {
|
||||||
|
const adapter = adapters.find((adapter) =>
|
||||||
|
NodeAddress.hasPrefix(address, adapter.nodePrefix())
|
||||||
|
);
|
||||||
|
if (adapter == null) {
|
||||||
|
const result = NodeAddress.toString(address);
|
||||||
|
console.warn(`No adapter for ${result}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return adapter.renderer().nodeDescription(address);
|
||||||
|
} catch (e) {
|
||||||
|
const result = NodeAddress.toString(address);
|
||||||
|
console.error(`Error getting description for ${result}: ${e.message}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PagerankTable extends React.Component<Props, State> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {topLevelFilter: NodeAddress.empty};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.props.graph == null || this.props.adapters == null) {
|
||||||
|
return <p>You must load a graph before seeing PageRank analysis.</p>;
|
||||||
|
}
|
||||||
|
if (this.props.pagerankResult == null) {
|
||||||
|
return <p>Please run PageRank to see analysis.</p>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Contributions</h2>
|
||||||
|
{this.renderFilterSelect()}
|
||||||
|
{this.renderTable()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFilterSelect() {
|
||||||
|
const {graph, pagerankResult, adapters} = this.props;
|
||||||
|
if (graph == null || pagerankResult == null || adapters == null) {
|
||||||
|
throw new Error("Impossible.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionGroup(adapter: PluginAdapter) {
|
||||||
|
const header = (
|
||||||
|
<option
|
||||||
|
key={adapter.nodePrefix()}
|
||||||
|
value={adapter.nodePrefix()}
|
||||||
|
style={{fontWeight: "bold"}}
|
||||||
|
>
|
||||||
|
{adapter.name()}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
const entries = adapter.nodeTypes().map((type) => (
|
||||||
|
<option key={type.prefix} value={type.prefix}>
|
||||||
|
{"\u2003" + type.name}
|
||||||
|
</option>
|
||||||
|
));
|
||||||
|
return [header, ...entries];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<label>
|
||||||
|
Filter by contribution type:{" "}
|
||||||
|
<select
|
||||||
|
value={this.state.topLevelFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
this.setState({topLevelFilter: e.target.value});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value={NodeAddress.empty}>Show all</option>
|
||||||
|
{sortBy(adapters, (a) => a.name()).map(optionGroup)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTable() {
|
||||||
|
const {graph, pagerankResult, adapters} = this.props;
|
||||||
|
if (graph == null || pagerankResult == null || adapters == null) {
|
||||||
|
throw new Error("Impossible.");
|
||||||
|
}
|
||||||
|
const topLevelFilter = this.state.topLevelFilter;
|
||||||
|
return (
|
||||||
|
<table style={{borderCollapse: "collapse", marginTop: 10}}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{textAlign: "left"}}>Node</th>
|
||||||
|
<th style={{textAlign: "right"}}>log(score)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<RecursiveTables
|
||||||
|
addresses={
|
||||||
|
topLevelFilter == null
|
||||||
|
? Array.from(graph.nodes())
|
||||||
|
: Array.from(graph.nodes()).filter((node) =>
|
||||||
|
NodeAddress.hasPrefix(node, topLevelFilter)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
graph={graph}
|
||||||
|
pagerankResult={pagerankResult}
|
||||||
|
depth={0}
|
||||||
|
adapters={adapters}
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RTState = {expanded: boolean};
|
||||||
|
type RTProps = {|
|
||||||
|
+address: NodeAddressT,
|
||||||
|
+graph: Graph,
|
||||||
|
+pagerankResult: PagerankResult,
|
||||||
|
+depth: number,
|
||||||
|
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
class RecursiveTable extends React.Component<RTProps, RTState> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {expanded: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {address, adapters, depth, graph, pagerankResult} = this.props;
|
||||||
|
const {expanded} = this.state;
|
||||||
|
const probability = pagerankResult.get(address);
|
||||||
|
if (probability == null) {
|
||||||
|
throw new Error(`no PageRank value for ${NodeAddress.toString(address)}`);
|
||||||
|
}
|
||||||
|
const modifiedLogScore = Math.log(probability) + 10;
|
||||||
|
return [
|
||||||
|
<tr
|
||||||
|
key="self"
|
||||||
|
style={{backgroundColor: `rgba(0,143.4375,0,${1 - 0.9 ** depth})`}}
|
||||||
|
>
|
||||||
|
<td style={{display: "flex", alignItems: "flex-start"}}>
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
marginRight: 5,
|
||||||
|
marginLeft: 15 * depth,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
this.setState(({expanded}) => ({
|
||||||
|
expanded: !expanded,
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expanded ? "\u2212" : "+"}
|
||||||
|
</button>
|
||||||
|
<span>{nodeDescription(address, adapters)}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{textAlign: "right"}}>{modifiedLogScore.toFixed(2)}</td>
|
||||||
|
</tr>,
|
||||||
|
expanded && (
|
||||||
|
<RecursiveTables
|
||||||
|
key="children"
|
||||||
|
addresses={Array.from(
|
||||||
|
new Set( // deduplicate same node reached by several edges
|
||||||
|
Array.from(
|
||||||
|
graph.neighbors(address, {
|
||||||
|
direction: Direction.ANY,
|
||||||
|
nodePrefix: NodeAddress.empty,
|
||||||
|
edgePrefix: EdgeAddress.empty,
|
||||||
|
})
|
||||||
|
).map((neighbor) => neighbor.node)
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
graph={graph}
|
||||||
|
pagerankResult={pagerankResult}
|
||||||
|
depth={depth + 1}
|
||||||
|
adapters={adapters}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecursiveTablesProps = {|
|
||||||
|
+addresses: $ReadOnlyArray<NodeAddressT>,
|
||||||
|
+graph: Graph,
|
||||||
|
+pagerankResult: PagerankResult,
|
||||||
|
+depth: number,
|
||||||
|
+adapters: $ReadOnlyArray<PluginAdapter>,
|
||||||
|
|};
|
||||||
|
|
||||||
|
class RecursiveTables extends React.Component<RecursiveTablesProps> {
|
||||||
|
render() {
|
||||||
|
const {addresses, graph, pagerankResult, depth, adapters} = this.props;
|
||||||
|
return addresses
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const x = pagerankResult.get(a);
|
||||||
|
const y = pagerankResult.get(b);
|
||||||
|
if (x == null) {
|
||||||
|
throw new Error(`No pagerank result for ${NodeAddress.toString(a)}`);
|
||||||
|
}
|
||||||
|
if (y == null) {
|
||||||
|
throw new Error(`No pagerank result for ${NodeAddress.toString(b)}`);
|
||||||
|
}
|
||||||
|
return y - x;
|
||||||
|
})
|
||||||
|
.slice(0, MAX_TABLE_ENTRIES)
|
||||||
|
.map((address) => (
|
||||||
|
<RecursiveTable
|
||||||
|
depth={depth}
|
||||||
|
address={address}
|
||||||
|
graph={graph}
|
||||||
|
pagerankResult={pagerankResult}
|
||||||
|
key={address}
|
||||||
|
adapters={adapters}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,326 @@
|
||||||
|
// @flow
|
||||||
|
import React from "react";
|
||||||
|
import {mount, shallow} from "enzyme";
|
||||||
|
import enzymeToJSON from "enzyme-to-json";
|
||||||
|
|
||||||
|
import {PagerankTable, nodeDescription} from "./PagerankTable";
|
||||||
|
import {pagerank} from "../../core/attribution/pagerank";
|
||||||
|
import sortBy from "lodash.sortby";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Graph,
|
||||||
|
type NodeAddressT,
|
||||||
|
NodeAddress,
|
||||||
|
EdgeAddress,
|
||||||
|
} from "../../core/graph";
|
||||||
|
|
||||||
|
require("../testUtil").configureEnzyme();
|
||||||
|
|
||||||
|
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/PagerankTable", () => {
|
||||||
|
function verifyNoAdapterWarning() {
|
||||||
|
expect(console.warn).toHaveBeenCalledWith("No adapter for NodeAddress[]");
|
||||||
|
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||||
|
// $ExpectFlowError
|
||||||
|
console.warn = jest.fn();
|
||||||
|
}
|
||||||
|
beforeEach(() => {
|
||||||
|
// $ExpectFlowError
|
||||||
|
console.error = jest.fn();
|
||||||
|
// $ExpectFlowError
|
||||||
|
console.warn = jest.fn();
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
expect(console.warn).not.toHaveBeenCalled();
|
||||||
|
expect(console.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rendering with incomplete props", () => {
|
||||||
|
it("renders expected message with null props", () => {
|
||||||
|
const element = shallow(
|
||||||
|
<PagerankTable pagerankResult={null} graph={null} adapters={null} />
|
||||||
|
);
|
||||||
|
expect(enzymeToJSON(element)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it("renders with just pagerankResult", () => {
|
||||||
|
const {pagerankResult} = example();
|
||||||
|
// No snapshot since this should never actually happen
|
||||||
|
shallow(
|
||||||
|
<PagerankTable
|
||||||
|
pagerankResult={pagerankResult}
|
||||||
|
graph={null}
|
||||||
|
adapters={null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("renders with just graph", () => {
|
||||||
|
const {graph} = example();
|
||||||
|
// No snapshot since this should never actually happen
|
||||||
|
shallow(
|
||||||
|
<PagerankTable pagerankResult={null} graph={graph} adapters={null} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("renders with just adapters", () => {
|
||||||
|
const {adapters} = example();
|
||||||
|
// No snapshot since this should never actually happen
|
||||||
|
shallow(
|
||||||
|
<PagerankTable pagerankResult={null} graph={null} adapters={adapters} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("renders expected message when there's no pagerank", () => {
|
||||||
|
const {graph, adapters} = example();
|
||||||
|
const element = shallow(
|
||||||
|
<PagerankTable
|
||||||
|
pagerankResult={null}
|
||||||
|
graph={graph}
|
||||||
|
adapters={adapters}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(enzymeToJSON(element)).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("full rendering", () => {
|
||||||
|
function exampleRender() {
|
||||||
|
const {nodes, adapters, graph, pagerankResult} = example();
|
||||||
|
const element = mount(
|
||||||
|
<PagerankTable
|
||||||
|
pagerankResult={pagerankResult}
|
||||||
|
graph={graph}
|
||||||
|
adapters={adapters}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
verifyNoAdapterWarning();
|
||||||
|
const select = element.find("select");
|
||||||
|
expect(select).toHaveLength(1);
|
||||||
|
return {nodes, adapters, graph, pagerankResult, element, select};
|
||||||
|
}
|
||||||
|
it("full render doesn't crash or error", () => {
|
||||||
|
example();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("tables ", () => {
|
||||||
|
it("are sorted by score", () => {
|
||||||
|
const {element, graph, pagerankResult} = exampleRender();
|
||||||
|
const rows = element.find("RecursiveTable");
|
||||||
|
expect(rows).toHaveLength(Array.from(graph.nodes()).length);
|
||||||
|
const scores = rows.map((x) => pagerankResult.get(x.prop("address")));
|
||||||
|
expect(scores).toEqual(sortBy(scores).reverse());
|
||||||
|
});
|
||||||
|
function expectColumnCorrect(
|
||||||
|
element: *,
|
||||||
|
name: string,
|
||||||
|
tdToExpected: (x: *) => string,
|
||||||
|
addressToExpected: (NodeAddressT) => string
|
||||||
|
) {
|
||||||
|
const header = element.find("th");
|
||||||
|
const headerTexts = header.map((x) => x.text());
|
||||||
|
const headerIndex = headerTexts.indexOf(name);
|
||||||
|
if (headerIndex === -1) {
|
||||||
|
throw new Error("Could not find column: " + name);
|
||||||
|
}
|
||||||
|
const tables = element.find("RecursiveTable");
|
||||||
|
const actual = tables.map((x) =>
|
||||||
|
tdToExpected(x.find("td").at(headerIndex))
|
||||||
|
);
|
||||||
|
const expected = tables.map((x) =>
|
||||||
|
addressToExpected(x.prop("address"))
|
||||||
|
);
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
}
|
||||||
|
it("has a node description column", () => {
|
||||||
|
const {element, adapters} = exampleRender();
|
||||||
|
expectColumnCorrect(
|
||||||
|
element,
|
||||||
|
"Node",
|
||||||
|
(td) => td.find("span").text(),
|
||||||
|
(address) => nodeDescription(address, adapters)
|
||||||
|
);
|
||||||
|
verifyNoAdapterWarning();
|
||||||
|
});
|
||||||
|
it("has a log score column", () => {
|
||||||
|
const {element, pagerankResult} = exampleRender();
|
||||||
|
expectColumnCorrect(
|
||||||
|
element,
|
||||||
|
"log(score)",
|
||||||
|
(td) => td.text(),
|
||||||
|
(address) => {
|
||||||
|
const probability = pagerankResult.get(address);
|
||||||
|
if (probability == null) {
|
||||||
|
throw new Error(address);
|
||||||
|
}
|
||||||
|
const modifiedLogScore = Math.log(probability) + 10;
|
||||||
|
return modifiedLogScore.toFixed(2);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it("subtables have depth-based styling", () => {
|
||||||
|
const {element} = exampleRender();
|
||||||
|
const getLevel = (level) => {
|
||||||
|
const rt = element.find("RecursiveTable").at(level);
|
||||||
|
const button = rt.find("button").first();
|
||||||
|
return {rt, button};
|
||||||
|
};
|
||||||
|
getLevel(0).button.simulate("click");
|
||||||
|
getLevel(1).button.simulate("click");
|
||||||
|
const f = ({rt, button}) => ({
|
||||||
|
row: rt
|
||||||
|
.find("tr")
|
||||||
|
.first()
|
||||||
|
.prop("style"),
|
||||||
|
button: button.prop("style"),
|
||||||
|
});
|
||||||
|
expect([0, 1, 2].map((x) => f(getLevel(x)))).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
it("button toggles between +/- and adds sub-RecursiveTable", () => {
|
||||||
|
const {element} = exampleRender();
|
||||||
|
const rt = () => element.find("RecursiveTable").first();
|
||||||
|
const button = rt().find("button");
|
||||||
|
expect(button).toEqual(expect.anything());
|
||||||
|
expect(button.text()).toEqual("+");
|
||||||
|
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||||
|
|
||||||
|
button.simulate("click");
|
||||||
|
expect(button.text()).toEqual("\u2212");
|
||||||
|
expect(rt().find("RecursiveTables")).toHaveLength(1);
|
||||||
|
|
||||||
|
button.simulate("click");
|
||||||
|
expect(button.text()).toEqual("+");
|
||||||
|
expect(rt().find("RecursiveTables")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("filter selector", () => {
|
||||||
|
it("has the correct options", () => {
|
||||||
|
const {select} = exampleRender();
|
||||||
|
const options = select.children();
|
||||||
|
expect(options.every("option")).toBe(true);
|
||||||
|
const results = options.map((x) => ({
|
||||||
|
valueString: NodeAddress.toString(x.prop("value")),
|
||||||
|
style: x.prop("style"),
|
||||||
|
text: x.text(),
|
||||||
|
}));
|
||||||
|
expect(results).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectFilterByName(select, name) {
|
||||||
|
const option = select.children().filterWhere((x) => x.text() === name);
|
||||||
|
if (option.length !== 1) {
|
||||||
|
throw new Error(`ambiguous select, got ${option.length} options`);
|
||||||
|
}
|
||||||
|
const value = option.prop("value");
|
||||||
|
select.simulate("change", {target: {value}});
|
||||||
|
}
|
||||||
|
it("plugin-level filter with no nodes works", () => {
|
||||||
|
const {select, element} = exampleRender();
|
||||||
|
expect(element.find("tbody tr")).not.toHaveLength(0);
|
||||||
|
selectFilterByName(select, "unused");
|
||||||
|
expect(element.find("tbody tr")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
it("type-level filter with some nodes works", () => {
|
||||||
|
const {select, element} = exampleRender();
|
||||||
|
selectFilterByName(select, "\u2003beta");
|
||||||
|
const rt = element.find("RecursiveTable");
|
||||||
|
expect(rt).toHaveLength(1);
|
||||||
|
expect(rt.prop("address")).toEqual(example().nodes.fooBeta);
|
||||||
|
});
|
||||||
|
it("filter doesn't apply to sub-tables", () => {
|
||||||
|
const {select, element} = exampleRender();
|
||||||
|
selectFilterByName(select, "\u2003beta");
|
||||||
|
const rt = element.find("RecursiveTable");
|
||||||
|
expect(rt).toHaveLength(1);
|
||||||
|
const button = rt.find("button");
|
||||||
|
expect(button).toHaveLength(1);
|
||||||
|
button.simulate("click");
|
||||||
|
|
||||||
|
const rts = element.find("RecursiveTable");
|
||||||
|
expect(rts).toHaveLength(2);
|
||||||
|
const subRt = rts.last();
|
||||||
|
expect(subRt.prop("address")).toEqual(example().nodes.fooAlpha);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,98 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`app/credExplorer/PagerankTable full rendering filter selector has the correct options 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"style": undefined,
|
||||||
|
"text": "Show all",
|
||||||
|
"valueString": "NodeAddress[]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": Object {
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
"text": "bar",
|
||||||
|
"valueString": "NodeAddress[\\"bar\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": undefined,
|
||||||
|
"text": " alpha",
|
||||||
|
"valueString": "NodeAddress[\\"bar\\",\\"a\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": Object {
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
"text": "foo",
|
||||||
|
"valueString": "NodeAddress[\\"foo\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": undefined,
|
||||||
|
"text": " alpha",
|
||||||
|
"valueString": "NodeAddress[\\"foo\\",\\"a\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": undefined,
|
||||||
|
"text": " beta",
|
||||||
|
"valueString": "NodeAddress[\\"foo\\",\\"b\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": Object {
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
"text": "unused",
|
||||||
|
"valueString": "NodeAddress[\\"unused\\"]",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"style": Object {
|
||||||
|
"fontWeight": "bold",
|
||||||
|
},
|
||||||
|
"text": "xox",
|
||||||
|
"valueString": "NodeAddress[\\"xox\\"]",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app/credExplorer/PagerankTable full rendering tables subtables have depth-based styling 1`] = `
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"button": Object {
|
||||||
|
"marginLeft": 0,
|
||||||
|
"marginRight": 5,
|
||||||
|
},
|
||||||
|
"row": Object {
|
||||||
|
"backgroundColor": "rgba(0,143.4375,0,0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"button": Object {
|
||||||
|
"marginLeft": 15,
|
||||||
|
"marginRight": 5,
|
||||||
|
},
|
||||||
|
"row": Object {
|
||||||
|
"backgroundColor": "rgba(0,143.4375,0,0.09999999999999998)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"button": Object {
|
||||||
|
"marginLeft": 30,
|
||||||
|
"marginRight": 5,
|
||||||
|
},
|
||||||
|
"row": Object {
|
||||||
|
"backgroundColor": "rgba(0,143.4375,0,0.18999999999999995)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app/credExplorer/PagerankTable rendering with incomplete props renders expected message when there's no pagerank 1`] = `
|
||||||
|
<p>
|
||||||
|
Please run PageRank to see analysis.
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`app/credExplorer/PagerankTable rendering with incomplete props renders expected message with null props 1`] = `
|
||||||
|
<p>
|
||||||
|
You must load a graph before seeing PageRank analysis.
|
||||||
|
</p>
|
||||||
|
`;
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
export function configureEnzyme() {
|
||||||
|
const Enzyme = require("enzyme");
|
||||||
|
const Adapter = require("enzyme-adapter-react-16");
|
||||||
|
Enzyme.configure({adapter: new Adapter()});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureAphrodite() {
|
||||||
|
const {StyleSheetTestUtils} = require("aphrodite/no-important");
|
||||||
|
beforeEach(() => {
|
||||||
|
StyleSheetTestUtils.suppressStyleInjection();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
StyleSheetTestUtils.clearBufferAndResumeStyleInjection();
|
||||||
|
});
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
|
|
||||||
import {findStationaryDistribution} from "./markovChain";
|
import {findStationaryDistribution} from "./markovChain";
|
||||||
|
|
||||||
|
export type {PagerankResult} from "./graphToMarkovChain";
|
||||||
export type PagerankOptions = {|
|
export type PagerankOptions = {|
|
||||||
+selfLoopWeight?: number,
|
+selfLoopWeight?: number,
|
||||||
+verbose?: boolean,
|
+verbose?: boolean,
|
||||||
|
|
Loading…
Reference in New Issue