Skip to content
This repository has been archived by the owner on Jan 11, 2023. It is now read-only.

Commit

Permalink
move source tree data structure into utility lib, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jlongster committed May 13, 2016
1 parent 953144a commit b4cc9ed
Show file tree
Hide file tree
Showing 5 changed files with 275 additions and 142 deletions.
171 changes: 32 additions & 139 deletions public/js/components/Sources.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";

const React = require("react");
const { DOM: dom, PropTypes } = React;
const { bindActionCreators } = require("redux");
const { connect } = require("react-redux");
const classnames = require("classnames");
Expand All @@ -9,126 +10,14 @@ const ManagedTree = React.createFactory(require("./util/ManagedTree"));
const { Set } = require("immutable");
const actions = require("../actions");
const { getSelectedSource, getSources } = require("../selectors");
const { DOM: dom, PropTypes } = React;
const {
createNode, nodeHasChildren, nodeName,
nodeContents, nodePath, createParentMap,
addToTree
} = require("../util/sources-tree.js");

require("./Sources.css");

function nodeHasChildren(item) {
return item[2] instanceof Array;
}

function nodeName(item) {
return item[0];
}

function nodePath(item) {
return item[1];
}

function nodeContents(item) {
return item[2];
}

function setNodeContents(item, contents) {
item[2] = contents;
}

function createNode(name, path, contents) {
return [name, path, contents];
}

function createParentMap(tree) {
const map = new WeakMap();

function _traverse(subtree) {
if (nodeHasChildren(subtree)) {
for (let child of nodeContents(subtree)) {
map.set(child, subtree);
_traverse(child);
}
}
}

// Don't link each top-level path to the "root" node because the
// user never sees the root
nodeContents(tree).forEach(_traverse);
return map;
}

function getURL(source) {
try {
if (!source.get("url")) {
return null;
}

const url = new URL(source.get("url"));

// Filter out things like `javascript:<code>` URLs for now.
// Whitelist the protocols because there may be several strange
// ones.
if (url.protocol !== "http:" && url.protocol !== "https:") {
return null;
}

return url;
} catch (e) {
// If there is a parse error (which may happen with various
// internal script that don't have a correct URL), just ignore it.
return null;
}
}

function addToTree(tree, source) {
const url = getURL(source);
if (!url) {
return;
}

const parts = url.pathname.split("/").filter(p => p !== "");
const isDir = (parts.length === 0 ||
parts[parts.length - 1].indexOf(".") === -1);
parts.unshift(url.host);

let path = "";
let subtree = tree;

for (let part of parts) {
const subpaths = nodeContents(subtree);
// We want to sort alphabetically, so find the index where we
// should insert this part.
let idx = subpaths.findIndex(subpath => {
return nodeName(subpath).localeCompare(part) >= 0;
});

// The node always acts like one with children, but the code below
// this loop will set the contents of the final node to the source
// object.
const pathItem = createNode(part, path + "/" + part, []);

if (idx >= 0 && nodeName(subpaths[idx]) === part) {
subtree = subpaths[idx];
} else {
// Add a new one
const where = idx === -1 ? subpaths.length : idx;
subpaths.splice(where, 0, pathItem);
subtree = subpaths[where];
}

// Keep track of the subpaths so we can tag each node with them.
path = path + "/" + part;
}

// Store the soure in the final created node.
if (isDir) {
setNodeContents(
subtree,
[createNode("(index)", source.get("url"), source)]
);
} else {
setNodeContents(subtree, source);
}
}

// This is inline because it's much faster. We need to revisit how we
// load SVGs, at least for components that render them several times.
let Arrow = (props) => {
Expand Down Expand Up @@ -194,6 +83,31 @@ let SourcesTree = React.createClass({
}
},

renderItem(item, depth, focused, _, expanded, { setExpanded }) {
const arrow = Arrow({
className: classnames(
"arrow",
{ expanded: expanded,
hidden: !nodeHasChildren(item) }
),
onClick: e => {
e.stopPropagation();
setExpanded(item, !expanded);
}
});

return dom.div(
{ className: classnames("node", { focused }),
style: { marginLeft: depth * 15 + "px" },
onClick: () => this.selectItem(item),
onDoubleClick: e => {
setExpanded(item, !expanded);
} },
arrow,
nodeName(item)
);
},

render() {
const { focusedItem, sourceTree, parentMap } = this.state;

Expand All @@ -212,28 +126,7 @@ let SourcesTree = React.createClass({
itemHeight: 30,
autoExpandDepth: 2,
onFocus: this.focusItem,
renderItem: (item, depth, focused, _, expanded, { setExpanded }) => {
const arrow = Arrow({
className: classnames("arrow",
{ expanded: expanded,
hidden: !nodeHasChildren(item) }),
onClick: e => {
e.stopPropagation();
setExpanded(item, !expanded);
}
});

return dom.div(
{ className: classnames("node", { focused }),
style: { marginLeft: depth * 15 + "px" },
onClick: () => this.selectItem(item),
onDoubleClick: e => {
setExpanded(item, !expanded);
} },
arrow,
nodeName(item)
);
}
renderItem: this.renderItem
});

return dom.div({
Expand Down
4 changes: 1 addition & 3 deletions public/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ connectClient(response => {
// otherwise, just show the toolbox.
if (hasSelectedTab()) {
const selectedTab = getSelectedTab(store.getState().tabs.get("tabs"));
debugTab(selectedTab.toJS(), actions).then(() => {
setTimeout(renderToolbox, 0);
});
debugTab(selectedTab.toJS(), actions).then(renderToolbox);
} else {
renderToolbox();
}
Expand Down
32 changes: 32 additions & 0 deletions public/js/test/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"globals": {
"after": true,
"afterEach": true,
"before": true,
"beforeEach": true,
"context": true,
"describe": true,
"it": true,
"mocha": true,
"setup": true,
"specify": true,
"suite": true,
"suiteSetup": true,
"suiteTeardown": true,
"teardown": true,
"test": true,
"xcontext": true,
"xdescribe": true,
"xit": true,
"xspecify": true,
"assert": true,
"expect": true,
"equal": true,
"ok": true
},
"rules": {
"camelcase": 0,
"no-unused-vars": [2, {"vars": "all", "args": "none", "varsIgnorePattern": "run_test"}],
"max-nested-callbacks": [2, 4],
}
}
79 changes: 79 additions & 0 deletions public/js/test/sources-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use strict";

const expect = require("expect.js");
const { Map } = require("immutable");
const {
createNode, nodeHasChildren, nodeName,
nodeContents, nodePath, addToTree
} = require("../util/sources-tree.js");

describe("sources-tree", () => {
it("should provide node API", () => {
const root = createNode("root", "", [createNode("foo", "/foo")]);
expect(nodeName(root)).to.be("root");
expect(nodeHasChildren(root)).to.be(true);
expect(nodeContents(root).length).to.be(1);

const child = nodeContents(root)[0];
expect(nodeName(child)).to.be("foo");
expect(nodePath(child)).to.be("/foo");
expect(nodeContents(child)).to.be(null);
expect(nodeHasChildren(child)).to.be(false);
});

it("builds a path-based tree", () => {
const source1 = Map({
url: "http://example.com/foo/source1.js",
actor: "actor1"
});
const tree = createNode("root", "", []);

addToTree(tree, source1);
expect(nodeContents(tree).length).to.be(1);

let base = nodeContents(tree)[0];
expect(nodeName(base)).to.be("example.com");
expect(nodeContents(base).length).to.be(1);

let fooNode = nodeContents(base)[0];
expect(nodeName(fooNode)).to.be("foo");
expect(nodeContents(fooNode).length).to.be(1);

let source1Node = nodeContents(fooNode)[0];
expect(nodeName(source1Node)).to.be("source1.js");
});

it("alphabetically sorts children", () => {
const source1 = Map({
url: "http://example.com/source1.js",
actor: "actor1"
});
const source2 = Map({
url: "http://example.com/foo/b_source2.js",
actor: "actor2"
});
const source3 = Map({
url: "http://example.com/foo/a_source3.js",
actor: "actor3"
});
const tree = createNode("root", "", []);

addToTree(tree, source1);
addToTree(tree, source2);
addToTree(tree, source3);

let base = nodeContents(tree)[0];
let fooNode = nodeContents(base)[0];
expect(nodeName(fooNode)).to.be("foo");
expect(nodeContents(fooNode).length).to.be(2);

let source1Node = nodeContents(base)[1];
expect(nodeName(source1Node)).to.be("source1.js");

// source2 should be after source1 alphabetically
let source2Node = nodeContents(fooNode)[1];
let source3Node = nodeContents(fooNode)[0];
expect(nodeName(source2Node)).to.be("b_source2.js");
expect(nodeName(source3Node)).to.be("a_source3.js");
});
});
Loading

0 comments on commit b4cc9ed

Please sign in to comment.