summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/utils/sources-tree
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/src/utils/sources-tree')
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/addToTree.js187
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/collapseTree.js55
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/formatTree.js26
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/getDirectories.js71
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/getURL.js144
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/index.js20
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/moz.build19
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/sortTree.js38
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap90
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap42
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap233
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/addToTree.spec.js374
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/collapseTree.spec.js127
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/getDirectories.spec.js98
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js107
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/treeOrder.spec.js25
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/updateTree.spec.js148
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/tests/utils.spec.js223
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/treeOrder.js148
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/types.js35
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/updateTree.js262
-rw-r--r--devtools/client/debugger/src/utils/sources-tree/utils.js292
22 files changed, 2764 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/utils/sources-tree/addToTree.js b/devtools/client/debugger/src/utils/sources-tree/addToTree.js
new file mode 100644
index 0000000000..fdc1f97ccd
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/addToTree.js
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import {
+ nodeHasChildren,
+ isPathDirectory,
+ isInvalidUrl,
+ partIsFile,
+ createSourceNode,
+ createDirectoryNode,
+ getPathParts,
+ type PathPart,
+} from "./utils";
+import { createTreeNodeMatcher, findNodeInContents } from "./treeOrder";
+import { getDisplayURL } from "./getURL";
+
+import type { ParsedURL } from "./getURL";
+import type { TreeDirectory, TreeNode } from "./types";
+import type { DisplaySource, Source } from "../../types";
+
+function createNodeInTree(
+ part: string,
+ path: string,
+ tree: TreeDirectory,
+ index: number
+): TreeDirectory {
+ const node = createDirectoryNode(part, path, []);
+
+ // we are modifying the tree
+ const contents = tree.contents.slice(0);
+ contents.splice(index, 0, node);
+ tree.contents = contents;
+
+ return node;
+}
+
+/*
+ * Look for the child node
+ * 1. if it exists return it
+ * 2. if it does not exist create it
+ */
+function findOrCreateNode(
+ parts: PathPart[],
+ subTree: TreeDirectory,
+ path: string,
+ part: string,
+ index: number,
+ url: Object,
+ debuggeeHost: ?string,
+ source: Source
+): TreeDirectory {
+ const addedPartIsFile = partIsFile(index, parts, url);
+
+ const { found: childFound, index: childIndex } = findNodeInContents(
+ subTree,
+ createTreeNodeMatcher(part, !addedPartIsFile, debuggeeHost)
+ );
+
+ // we create and enter the new node
+ if (!childFound) {
+ return createNodeInTree(part, path, subTree, childIndex);
+ }
+
+ // we found a path with the same name as the part. We need to determine
+ // if this is the correct child, or if we have a naming conflict
+ const child = subTree.contents[childIndex];
+ const childIsFile = !nodeHasChildren(child);
+
+ // if we have a naming conflict, we'll create a new node
+ if (childIsFile != addedPartIsFile) {
+ // pass true to findNodeInContents to sort node by url
+ const { index: insertIndex } = findNodeInContents(
+ subTree,
+ createTreeNodeMatcher(part, !addedPartIsFile, debuggeeHost, source, true)
+ );
+ return createNodeInTree(part, path, subTree, insertIndex);
+ }
+
+ // if there is no naming conflict, we can traverse into the child
+ return (child: any);
+}
+
+/*
+ * walk the source tree to the final node for a given url,
+ * adding new nodes along the way
+ */
+function traverseTree(
+ url: ParsedURL,
+ tree: TreeDirectory,
+ debuggeeHost: ?string,
+ source: Source,
+ thread: string
+): TreeNode {
+ const parts = getPathParts(url, thread, debuggeeHost);
+ return parts.reduce(
+ (subTree, { part, path, debuggeeHostIfRoot }, index) =>
+ findOrCreateNode(
+ parts,
+ subTree,
+ path,
+ part,
+ index,
+ url,
+ debuggeeHostIfRoot,
+ source
+ ),
+ tree
+ );
+}
+
+/*
+ * Add a source file to a directory node in the tree
+ */
+function addSourceToNode(
+ node: TreeDirectory,
+ url: ParsedURL,
+ source: Source
+): Source | TreeNode[] {
+ const isFile = !isPathDirectory(url.path);
+
+ if (node.type == "source" && !isFile) {
+ throw new Error(`Unexpected type "source" at: ${node.name}`);
+ }
+
+ // if we have a file, and the subtree has no elements, overwrite the
+ // subtree contents with the source
+ if (isFile) {
+ // $FlowIgnore
+ node.type = "source";
+ return source;
+ }
+
+ let { filename } = url;
+
+ if (filename === "(index)" && url.search) {
+ filename = url.search;
+ } else {
+ filename += url.search;
+ }
+
+ const { found: childFound, index: childIndex } = findNodeInContents(
+ node,
+ createTreeNodeMatcher(filename, false, null)
+ );
+
+ // if we are readding an existing file in the node, overwrite the existing
+ // file and return the node's contents
+ if (childFound) {
+ const existingNode = node.contents[childIndex];
+ if (existingNode.type === "source") {
+ existingNode.contents = source;
+ }
+
+ return node.contents;
+ }
+
+ // if this is a new file, add the new file;
+ const newNode = createSourceNode(filename, source.url, source);
+ const contents = node.contents.slice(0);
+ contents.splice(childIndex, 0, newNode);
+ return contents;
+}
+
+/**
+ * @memberof utils/sources-tree
+ * @static
+ */
+export function addToTree(
+ tree: TreeDirectory,
+ source: DisplaySource,
+ debuggeeHost: ?string,
+ thread: string
+): void {
+ const url = getDisplayURL(source, debuggeeHost);
+
+ if (isInvalidUrl(url, source)) {
+ return;
+ }
+
+ const finalNode = traverseTree(url, tree, debuggeeHost, source, thread);
+
+ // $FlowIgnore
+ finalNode.contents = addSourceToNode(finalNode, url, source);
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/collapseTree.js b/devtools/client/debugger/src/utils/sources-tree/collapseTree.js
new file mode 100644
index 0000000000..57d3e89849
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/collapseTree.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { createDirectoryNode } from "./utils";
+
+import type { TreeDirectory, TreeNode } from "./types";
+
+/**
+ * Take an existing source tree, and return a new one with collapsed nodes.
+ */
+function _collapseTree(node: TreeNode, depth: number): TreeNode {
+ // Node is a folder.
+ if (node.type === "directory") {
+ if (!Array.isArray(node.contents)) {
+ console.log(`Expected array at: ${node.path}`);
+ }
+
+ // Node is not a (1) thread and (2) root/domain node,
+ // and only contains 1 item.
+ if (depth > 2 && node.contents.length === 1) {
+ const next = node.contents[0];
+ // Do not collapse if the next node is a leaf node.
+ if (next.type === "directory") {
+ if (!Array.isArray(next.contents)) {
+ console.log(
+ `Expected array at: ${next.name} -- ${
+ node.name
+ } -- ${JSON.stringify(next.contents)}`
+ );
+ }
+ const name = `${node.name}/${next.name}`;
+ const nextNode = createDirectoryNode(name, next.path, next.contents);
+ return _collapseTree(nextNode, depth + 1);
+ }
+ }
+
+ // Map the contents.
+ return createDirectoryNode(
+ node.name,
+ node.path,
+ node.contents.map(next => _collapseTree(next, depth + 1))
+ );
+ }
+
+ // Node is a leaf, not a folder, do not modify it.
+ return node;
+}
+
+export function collapseTree(node: TreeDirectory): TreeDirectory {
+ const tree = _collapseTree(node, 0);
+ return ((tree: any): TreeDirectory);
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/formatTree.js b/devtools/client/debugger/src/utils/sources-tree/formatTree.js
new file mode 100644
index 0000000000..83598a0e44
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/formatTree.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { TreeNode } from "./types";
+
+export function formatTree(
+ tree: TreeNode,
+ depth: number = 0,
+ str: string = ""
+): string {
+ const whitespace = new Array(depth * 2).join(" ");
+
+ if (tree.type === "directory") {
+ str += `${whitespace} - ${tree.name} path=${tree.path} \n`;
+ tree.contents.forEach(t => {
+ str = formatTree(t, depth + 1, str);
+ });
+ } else {
+ str += `${whitespace} - ${tree.name} path=${tree.path} source_id=${tree.contents.id} \n`;
+ }
+
+ return str;
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/getDirectories.js b/devtools/client/debugger/src/utils/sources-tree/getDirectories.js
new file mode 100644
index 0000000000..fe5b813d09
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/getDirectories.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { createParentMap } from "./utils";
+import flattenDeep from "lodash/flattenDeep";
+import type { TreeNode, TreeDirectory } from "./types";
+import type { Source } from "../../types";
+
+function findSourceItem(sourceTree: TreeDirectory, source: Source): ?TreeNode {
+ function _traverse(subtree: TreeNode) {
+ if (subtree.type === "source") {
+ if (subtree.contents.id === source.id) {
+ return subtree;
+ }
+
+ return null;
+ }
+
+ const matches = subtree.contents.map(child => _traverse(child));
+ return matches && matches.filter(Boolean)[0];
+ }
+
+ return _traverse(sourceTree);
+}
+
+export function findSourceTreeNodes(
+ sourceTree: TreeDirectory,
+ path: string
+): TreeNode[] {
+ function _traverse(subtree: TreeNode) {
+ if (subtree.path.endsWith(path)) {
+ return subtree;
+ }
+
+ if (subtree.type === "directory") {
+ const matches = subtree.contents.map(child => _traverse(child));
+ return matches && matches.filter(Boolean);
+ }
+ }
+
+ const result = _traverse(sourceTree);
+ // $FlowIgnore
+ return Array.isArray(result) ? flattenDeep(result) : result;
+}
+
+function getAncestors(sourceTree: TreeDirectory, item: ?TreeNode) {
+ if (!item) {
+ return null;
+ }
+
+ const parentMap = createParentMap(sourceTree);
+ const directories = [];
+
+ directories.push(item);
+ while (true) {
+ item = parentMap.get(item);
+ if (!item) {
+ return directories;
+ }
+ directories.push(item);
+ }
+}
+
+export function getDirectories(source: Source, sourceTree: TreeDirectory) {
+ const item = findSourceItem(sourceTree, source);
+ const ancestors = getAncestors(sourceTree, item);
+ return ancestors || [sourceTree];
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/getURL.js b/devtools/client/debugger/src/utils/sources-tree/getURL.js
new file mode 100644
index 0000000000..78713db17e
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/getURL.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { parse } from "../url";
+
+const {
+ getUnicodeHostname,
+ getUnicodeUrlPath,
+ // $FlowIgnore
+} = require("devtools/client/shared/unicode-url");
+
+import type { DisplaySource, Source } from "../../types";
+export type ParsedURL = {
+ path: string,
+ search: string,
+ group: string,
+ filename: string,
+};
+
+export function getFilenameFromPath(pathname?: string): string {
+ let filename = "";
+ if (pathname) {
+ filename = pathname.substring(pathname.lastIndexOf("/") + 1);
+ // This file does not have a name. Default should be (index).
+ if (filename == "") {
+ filename = "(index)";
+ }
+ }
+ return filename;
+}
+
+const NoDomain = "(no domain)";
+const def = { path: "", search: "", group: "", filename: "" };
+
+export function getURL(source: Source, defaultDomain: ?string = ""): ParsedURL {
+ const { url } = source;
+ if (!url) {
+ return def;
+ }
+ return getURLInternal(url, defaultDomain);
+}
+
+export function getDisplayURL(
+ source: DisplaySource,
+ defaultDomain: ?string = ""
+): ParsedURL {
+ const { displayURL } = source;
+ if (!displayURL) {
+ return def;
+ }
+ return getURLInternal(displayURL, defaultDomain);
+}
+
+function getURLInternal(url: string, defaultDomain: ?string): ParsedURL {
+ const { pathname, search, protocol, host } = parse(url);
+ const filename = getUnicodeUrlPath(getFilenameFromPath(pathname));
+
+ switch (protocol) {
+ case "javascript:":
+ // Ignore `javascript:` URLs for now
+ return def;
+
+ case "moz-extension:":
+ case "resource:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ group: `${protocol}//${host || ""}`,
+ };
+
+ case "webpack:":
+ case "ng:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ group: `${protocol}//`,
+ };
+
+ case "about:":
+ // An about page is a special case
+ return {
+ ...def,
+ path: "/",
+ search,
+ filename,
+ group: url,
+ };
+
+ case "data:":
+ return {
+ ...def,
+ path: "/",
+ search,
+ group: NoDomain,
+ filename: url,
+ };
+
+ case "":
+ if (pathname && pathname.startsWith("/")) {
+ // use file protocol for a URL like "/foo/bar.js"
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ group: "file://",
+ };
+ } else if (!host) {
+ return {
+ ...def,
+ path: pathname,
+ search,
+ group: defaultDomain || "",
+ filename,
+ };
+ }
+ break;
+
+ case "http:":
+ case "https:":
+ return {
+ ...def,
+ path: pathname,
+ search,
+ filename,
+ group: getUnicodeHostname(host),
+ };
+ }
+
+ return {
+ ...def,
+ path: pathname,
+ search,
+ group: protocol ? `${protocol}//` : "",
+ filename,
+ };
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/index.js b/devtools/client/debugger/src/utils/sources-tree/index.js
new file mode 100644
index 0000000000..e9cc35e1c4
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/index.js
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+/**
+ * Utils for Sources Tree Component
+ * @module utils/sources-tree
+ */
+
+export { addToTree } from "./addToTree";
+export { collapseTree } from "./collapseTree";
+export { formatTree } from "./formatTree";
+export { getDirectories, findSourceTreeNodes } from "./getDirectories";
+export { getFilenameFromPath, getURL } from "./getURL";
+export { sortTree } from "./sortTree";
+export { createTree, updateTree } from "./updateTree";
+
+export * from "./utils";
diff --git a/devtools/client/debugger/src/utils/sources-tree/moz.build b/devtools/client/debugger/src/utils/sources-tree/moz.build
new file mode 100644
index 0000000000..f1439a46e3
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/moz.build
@@ -0,0 +1,19 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += []
+
+CompiledModules(
+ "addToTree.js",
+ "collapseTree.js",
+ "formatTree.js",
+ "getDirectories.js",
+ "getURL.js",
+ "index.js",
+ "sortTree.js",
+ "treeOrder.js",
+ "updateTree.js",
+ "utils.js",
+)
diff --git a/devtools/client/debugger/src/utils/sources-tree/sortTree.js b/devtools/client/debugger/src/utils/sources-tree/sortTree.js
new file mode 100644
index 0000000000..c090917fea
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/sortTree.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { nodeHasChildren, isExactUrlMatch } from "./utils";
+
+import type { TreeDirectory } from "./types";
+import type { URL } from "../../types";
+
+/**
+ * Look at the nodes in the source tree, and determine the index of where to
+ * insert a new node. The ordering is index -> folder -> file.
+ * @memberof utils/sources-tree
+ * @static
+ */
+export function sortTree(tree: TreeDirectory, debuggeeUrl: URL = ""): number {
+ return (tree.contents: any).sort((previousNode, currentNode) => {
+ const currentNodeIsDir = nodeHasChildren(currentNode);
+ const previousNodeIsDir = nodeHasChildren(previousNode);
+ if (currentNode.name === "(index)") {
+ return 1;
+ } else if (previousNode.name === "(index)") {
+ return -1;
+ } else if (isExactUrlMatch(currentNode.name, debuggeeUrl)) {
+ return 1;
+ } else if (isExactUrlMatch(previousNode.name, debuggeeUrl)) {
+ return -1;
+ // If neither is the case, continue to compare alphabetically
+ } else if (previousNodeIsDir && !currentNodeIsDir) {
+ return -1;
+ } else if (!previousNodeIsDir && currentNodeIsDir) {
+ return 1;
+ }
+ return previousNode.name.localeCompare(currentNode.name);
+ });
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap
new file mode 100644
index 0000000000..a76261e0dc
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/addToTree.spec.js.snap
@@ -0,0 +1,90 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sources-tree addToTree can add a file to an intermediate directory 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - unpkg.com path=FakeThread/unpkg.com
+ - codemirror@5.1 path=FakeThread/unpkg.com/codemirror@5.1
+ - mode path=FakeThread/unpkg.com/codemirror@5.1/mode
+ - xml path=FakeThread/unpkg.com/codemirror@5.1/mode/xml
+ - xml.js path=FakeThread/unpkg.com/codemirror@5.1/mode/xml/xml.js source_id=server1.conn13.child1/39
+ - codemirror@5.1 path=FakeThread/unpkg.com/codemirror@5.1 source_id=server1.conn13.child1/37
+"
+`;
+
+exports[`sources-tree addToTree correctly parses file sources 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - file:// path=FakeThread/file://
+ - a path=FakeThread/file:///a
+ - b.js path=FakeThread/file:///a/b.js source_id=actor1
+"
+`;
+
+exports[`sources-tree addToTree does not attempt to add two of the same directory 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - davidwalsh.name path=FakeThread/davidwalsh.name
+ - (index) path=https://davidwalsh.name/ source_id=server1.conn13.child1/37
+ - wp-content path=FakeThread/davidwalsh.name/wp-content
+ - prism.js path=FakeThread/davidwalsh.name/wp-content/prism.js source_id=server1.conn13.child1/39
+"
+`;
+
+exports[`sources-tree addToTree does not attempt to add two of the same file 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - davidwalsh.name path=FakeThread/davidwalsh.name
+ - (index) path=https://davidwalsh.name/ source_id=server1.conn13.child1/39
+ - util.js path=FakeThread/davidwalsh.name/util.js source_id=server1.conn13.child1/37
+ - FakeThread2 path=FakeThread2
+ - davidwalsh.name path=FakeThread2/davidwalsh.name
+ - util.js path=FakeThread2/davidwalsh.name/util.js source_id=server1.conn13.child1/37
+"
+`;
+
+exports[`sources-tree addToTree does not mangle encoded URLs 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - example.com path=FakeThread/example.com
+ - foo path=FakeThread/example.com/foo
+ - B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 path=FakeThread/example.com/foo/B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1 source_id=actor1
+"
+`;
+
+exports[`sources-tree addToTree excludes javascript: URLs from the tree 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - example.com path=FakeThread/example.com
+ - source1.js path=FakeThread/example.com/source1.js source_id=actor2
+"
+`;
+
+exports[`sources-tree addToTree name does include query params 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - example.com path=FakeThread/example.com
+ - foo path=FakeThread/example.com/foo
+ - name.js?bar=3 path=FakeThread/example.com/foo/name.js?bar=3 source_id=actor1
+"
+`;
+
+exports[`sources-tree addToTree replaces a file with a directory 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - unpkg.com path=FakeThread/unpkg.com
+ - codemirror@5.1 path=FakeThread/unpkg.com/codemirror@5.1
+ - mode path=FakeThread/unpkg.com/codemirror@5.1/mode
+ - xml path=FakeThread/unpkg.com/codemirror@5.1/mode/xml
+ - xml.js path=FakeThread/unpkg.com/codemirror@5.1/mode/xml/xml.js source_id=server1.conn13.child1/39
+ - codemirror@5.1 path=FakeThread/unpkg.com/codemirror@5.1 source_id=server1.conn13.child1/37
+"
+`;
+
+exports[`sources-tree addToTree supports data URLs 1`] = `
+" - root path=
+ - FakeThread path=FakeThread
+ - (no domain) path=FakeThread/(no domain)
+ - data:text/html,<script>console.log(123)</script> path=data:text/html,<script>console.log(123)</script> source_id=server1.conn13.child1/39
+"
+`;
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap
new file mode 100644
index 0000000000..529874f8bd
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/collapseTree.spec.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sources tree collapseTree can collapse a single source 1`] = `
+" - root path=
+ - Main Thread path=Main Thread
+ - example.com path=Main Thread/example.com
+ - a/b path=Main Thread/example.com/a/b
+ - c.js path=Main Thread/example.com/a/b/c.js source_id=actor1
+"
+`;
+
+exports[`sources tree collapseTree correctly merges in a collapsed source with a deeper level 1`] = `
+" - root path=
+ - Main Thread path=Main Thread
+ - example.com path=Main Thread/example.com
+ - a/b path=Main Thread/example.com/a/b
+ - c/d path=Main Thread/example.com/a/b/c/d
+ - e.js path=Main Thread/example.com/a/b/c/d/e.js source_id=actor2
+ - c.js path=Main Thread/example.com/a/b/c.js source_id=actor1
+"
+`;
+
+exports[`sources tree collapseTree correctly merges in a collapsed source with a shallower level 1`] = `
+" - root path=
+ - Main Thread path=Main Thread
+ - example.com path=Main Thread/example.com
+ - a/b path=Main Thread/example.com/a/b
+ - c.js path=Main Thread/example.com/a/b/c.js source_id=actor1
+ - x.js path=Main Thread/example.com/a/b/x.js source_id=actor3
+"
+`;
+
+exports[`sources tree collapseTree correctly merges in a collapsed source with the same level 1`] = `
+" - root path=
+ - Main Thread path=Main Thread
+ - example.com path=Main Thread/example.com
+ - a/b path=Main Thread/example.com/a/b
+ - c/d path=Main Thread/example.com/a/b/c/d
+ - e.js path=Main Thread/example.com/a/b/c/d/e.js source_id=actor2
+ - c.js path=Main Thread/example.com/a/b/c.js source_id=actor1
+"
+`;
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap
new file mode 100644
index 0000000000..d6bc9a653e
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/__snapshots__/updateTree.spec.js.snap
@@ -0,0 +1,233 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`calls updateTree.js adds one source 1`] = `
+"{
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"root\\",
+ \\"path\\": \\"\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"FakeThread\\",
+ \\"path\\": \\"FakeThread\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"davidwalsh.name\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"(index)\\",
+ \\"path\\": \\"https://davidwalsh.name/\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/39\\",
+ \\"url\\": \\"https://davidwalsh.name/\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/\\"
+ }
+ },
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"source1.js\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name/source1.js\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/37\\",
+ \\"url\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/source1.js\\"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}"
+`;
+
+exports[`calls updateTree.js adds two sources 1`] = `
+"{
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"root\\",
+ \\"path\\": \\"\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"FakeThread\\",
+ \\"path\\": \\"FakeThread\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"davidwalsh.name\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"(index)\\",
+ \\"path\\": \\"https://davidwalsh.name/\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/39\\",
+ \\"url\\": \\"https://davidwalsh.name/\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/\\"
+ }
+ },
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"source1.js\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name/source1.js\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/37\\",
+ \\"url\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/source1.js\\"
+ }
+ },
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"source2.js\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name/source2.js\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/40\\",
+ \\"url\\": \\"https://davidwalsh.name/source2.js\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/source2.js\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/source2.js\\"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}"
+`;
+
+exports[`calls updateTree.js shows all the sources 1`] = `
+"{
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"root\\",
+ \\"path\\": \\"\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"FakeThread\\",
+ \\"path\\": \\"FakeThread\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"davidwalsh.name\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"(index)\\",
+ \\"path\\": \\"https://davidwalsh.name/\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/39\\",
+ \\"url\\": \\"https://davidwalsh.name/\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/\\"
+ }
+ },
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"source1.js\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name/source1.js\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/37\\",
+ \\"url\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/source1.js\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/source1.js\\"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}"
+`;
+
+exports[`calls updateTree.js update sources that change their display URL 1`] = `
+"{
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"root\\",
+ \\"path\\": \\"\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"FakeThread\\",
+ \\"path\\": \\"FakeThread\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"directory\\",
+ \\"name\\": \\"davidwalsh.name\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name\\",
+ \\"contents\\": [
+ {
+ \\"type\\": \\"source\\",
+ \\"name\\": \\"?param\\",
+ \\"path\\": \\"FakeThread/davidwalsh.name/?param\\",
+ \\"contents\\": {
+ \\"id\\": \\"server1.conn13.child1/39\\",
+ \\"url\\": \\"https://davidwalsh.name/?param\\",
+ \\"isBlackBoxed\\": false,
+ \\"isPrettyPrinted\\": false,
+ \\"relativeUrl\\": \\"https://davidwalsh.name/?param\\",
+ \\"isWasm\\": false,
+ \\"extensionName\\": null,
+ \\"isExtension\\": false,
+ \\"isOriginal\\": false,
+ \\"displayURL\\": \\"https://davidwalsh.name/?param\\"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}"
+`;
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/addToTree.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/addToTree.spec.js
new file mode 100644
index 0000000000..392d854bea
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/addToTree.spec.js
@@ -0,0 +1,374 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+/* eslint max-nested-callbacks: ["error", 4]*/
+
+import { makeMockDisplaySource } from "../../../utils/test-mockup";
+
+import {
+ addToTree,
+ createDirectoryNode,
+ createSourceNode,
+ createTree,
+ formatTree,
+ nodeHasChildren,
+} from "../index";
+
+type RawSource = {| url: string, id: string, actors?: any |};
+
+function createSourcesMap(sources: RawSource[]) {
+ const sourcesMap = sources.reduce((map, source) => {
+ map[source.id] = makeMockDisplaySource(source.url, source.id);
+ return map;
+ }, {});
+
+ return sourcesMap;
+}
+
+function createSourcesList(sources: { url: string, id?: string }[]) {
+ return sources.map((s, i) => makeMockDisplaySource(s.url, s.id));
+}
+
+function getChildNode(tree, ...path) {
+ return path.reduce((child, index) => child.contents[index], tree);
+}
+
+describe("sources-tree", () => {
+ describe("addToTree", () => {
+ it("should provide node API", () => {
+ const source = makeMockDisplaySource(
+ "http://example.com/a/b/c.js",
+ "actor1"
+ );
+
+ const root = createDirectoryNode("root", "", [
+ createSourceNode("foo", "/foo", source),
+ ]);
+
+ expect(root.name).toBe("root");
+ expect(nodeHasChildren(root)).toBe(true);
+ expect(root.contents).toHaveLength(1);
+
+ const child = root.contents[0];
+ expect(child.name).toBe("foo");
+ expect(child.path).toBe("/foo");
+ expect(child.contents).toBe(source);
+ expect(nodeHasChildren(child)).toBe(false);
+ });
+
+ it("builds a path-based tree", () => {
+ const source1 = makeMockDisplaySource(
+ "http://example.com/foo/source1.js",
+ "actor1"
+ );
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "FakeThread");
+ expect(tree.contents).toHaveLength(1);
+
+ const base = tree.contents[0].contents[0];
+ expect(base.name).toBe("example.com");
+ expect(base.contents).toHaveLength(1);
+
+ const fooNode = base.contents[0];
+ expect(fooNode.name).toBe("foo");
+ expect(fooNode.contents).toHaveLength(1);
+
+ const source1Node = fooNode.contents[0];
+ expect(source1Node.name).toBe("source1.js");
+ });
+
+ it("builds a path-based tree for webpack URLs", () => {
+ const source1 = makeMockDisplaySource(
+ "webpack:///foo/source1.js",
+ "actor1"
+ );
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "");
+ expect(tree.contents).toHaveLength(1);
+
+ const base = tree.contents[0];
+ expect(base.name).toBe("webpack://");
+ expect(base.contents).toHaveLength(1);
+
+ const fooNode = base.contents[0];
+ expect(fooNode.name).toBe("foo");
+ expect(fooNode.contents).toHaveLength(1);
+
+ const source1Node = fooNode.contents[0];
+ expect(source1Node.name).toBe("source1.js");
+ });
+
+ it("builds a path-based tree for webpack URLs with absolute path", () => {
+ const source1 = makeMockDisplaySource(
+ "webpack:////Users/foo/source1.js",
+ "actor1"
+ );
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "");
+ expect(tree.contents).toHaveLength(1);
+
+ const base = tree.contents[0];
+ expect(base.name).toBe("webpack://");
+ expect(base.contents).toHaveLength(1);
+
+ const emptyNode = base.contents[0];
+ expect(emptyNode.name).toBe("");
+ expect(emptyNode.contents).toHaveLength(1);
+
+ const userNode = emptyNode.contents[0];
+ expect(userNode.name).toBe("Users");
+ expect(userNode.contents).toHaveLength(1);
+
+ const fooNode = userNode.contents[0];
+ expect(fooNode.name).toBe("foo");
+ expect(fooNode.contents).toHaveLength(1);
+
+ const source1Node = fooNode.contents[0];
+ expect(source1Node.name).toBe("source1.js");
+ });
+
+ it("handles url with no filename", function() {
+ const source1 = makeMockDisplaySource("http://example.com/", "actor1");
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "");
+ expect(tree.contents).toHaveLength(1);
+
+ const base = tree.contents[0];
+ expect(base.name).toBe("example.com");
+ expect(base.contents).toHaveLength(1);
+
+ const indexNode = base.contents[0];
+ expect(indexNode.name).toBe("(index)");
+ });
+
+ it("does not mangle encoded URLs", () => {
+ const sourceName = // eslint-disable-next-line max-len
+ "B9724220.131821496;dc_ver=42.111;sz=468x60;u_sd=2;dc_adk=2020465299;ord=a53rpc;dc_rfl=1,https%3A%2F%2Fdavidwalsh.name%2F$0;xdt=1";
+
+ const source1 = makeMockDisplaySource(
+ `https://example.com/foo/${sourceName}`,
+ "actor1"
+ );
+
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "FakeThread");
+ const childNode = getChildNode(tree, 0, 0, 0, 0);
+ expect(childNode.name).toEqual(sourceName);
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("name does include query params", () => {
+ const sourceName = "name.js?bar=3";
+
+ const source1 = makeMockDisplaySource(
+ `https://example.com/foo/${sourceName}`,
+ "actor1"
+ );
+
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "FakeThread");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("does not attempt to add two of the same directory", () => {
+ const sources = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://davidwalsh.name/wp-content/prism.js",
+ },
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://davidwalsh.name/",
+ },
+ ];
+
+ const sourceMap = { FakeThread: createSourcesMap(sources) };
+ const tree = createTree({
+ sources: sourceMap,
+ debuggeeUrl: "",
+ threads: [
+ {
+ actor: "FakeThread",
+ name: "FakeThread",
+ url: "https://davidwalsh.name",
+ targetType: "worker",
+ isTopLevel: false,
+ },
+ ],
+ }).sourceTree;
+
+ expect(tree.contents[0].contents).toHaveLength(1);
+ const subtree = tree.contents[0].contents[0];
+ expect(subtree.contents).toHaveLength(2);
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("supports data URLs", () => {
+ const sources = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "data:text/html,<script>console.log(123)</script>",
+ },
+ ];
+
+ const sourceMap = { FakeThread: createSourcesMap(sources) };
+ const tree = createTree({
+ sources: sourceMap,
+ debuggeeUrl: "",
+ threads: [
+ {
+ actor: "FakeThread",
+ url: "https://davidwalsh.name",
+ targetType: "worker",
+ name: "FakeThread",
+ isTopLevel: false,
+ },
+ ],
+ }).sourceTree;
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("does not attempt to add two of the same file", () => {
+ const sources = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://davidwalsh.name/",
+ },
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://davidwalsh.name/util.js",
+ },
+ ];
+
+ const sourceMap = {
+ FakeThread: createSourcesMap(sources),
+ FakeThread2: createSourcesMap([sources[1]]),
+ };
+
+ const tree = createTree({
+ sources: sourceMap,
+ debuggeeUrl: "https://davidwalsh.name",
+ threads: [
+ {
+ actor: "FakeThread",
+ name: "FakeThread",
+ url: "https://davidwalsh.name",
+ targetType: "worker",
+ isTopLevel: false,
+ },
+ {
+ actor: "FakeThread2",
+ name: "FakeThread2",
+ url: "https://davidwalsh.name/WorkerA.js",
+ targetType: "worker",
+ isTopLevel: false,
+ },
+ ],
+ }).sourceTree;
+
+ expect(tree.contents[0].contents).toHaveLength(1);
+ const subtree = tree.contents[0].contents[0];
+ expect(subtree.contents).toHaveLength(2);
+ const subtree2 = tree.contents[1].contents[0];
+ expect(subtree2.contents).toHaveLength(1);
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("excludes javascript: URLs from the tree", () => {
+ const source1 = makeMockDisplaySource(
+ "javascript:alert('Hello World')",
+ "actor1"
+ );
+ const source2 = makeMockDisplaySource(
+ "http://example.com/source1.js",
+ "actor2"
+ );
+ const source3 = makeMockDisplaySource(
+ "javascript:let i = 10; while (i > 0) i--; console.log(i);",
+ "actor3"
+ );
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source1, "http://example.com/", "FakeThread");
+ addToTree(tree, source2, "http://example.com/", "FakeThread");
+ addToTree(tree, source3, "http://example.com/", "FakeThread");
+
+ const base = tree.contents[0].contents[0];
+ expect(tree.contents).toHaveLength(1);
+
+ const source1Node = base.contents[0];
+ expect(source1Node.name).toBe("source1.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("correctly parses file sources", () => {
+ const source = makeMockDisplaySource("file:///a/b.js", "actor1");
+ const tree = createDirectoryNode("root", "", []);
+
+ addToTree(tree, source, "file:///a/index.html", "FakeThread");
+ expect(tree.contents).toHaveLength(1);
+
+ const base = tree.contents[0].contents[0];
+ expect(base.name).toBe("file://");
+ expect(base.contents).toHaveLength(1);
+
+ const aNode = base.contents[0];
+ expect(aNode.name).toBe("a");
+ expect(aNode.contents).toHaveLength(1);
+
+ const bNode = aNode.contents[0];
+ expect(bNode.name).toBe("b.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("can add a file to an intermediate directory", () => {
+ const testData = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://unpkg.com/codemirror@5.1/mode/xml/xml.js",
+ },
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://unpkg.com/codemirror@5.1",
+ },
+ ];
+
+ const sources = createSourcesList(testData);
+ const tree = createDirectoryNode("root", "", []);
+ sources.forEach(source =>
+ addToTree(tree, source, "https://unpkg.com/", "FakeThread")
+ );
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("replaces a file with a directory", () => {
+ const testData = [
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://unpkg.com/codemirror@5.1",
+ },
+
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://unpkg.com/codemirror@5.1/mode/xml/xml.js",
+ },
+ ];
+
+ const sources = createSourcesList(testData);
+ const tree = createDirectoryNode("root", "", []);
+ sources.forEach(source =>
+ addToTree(tree, source, "https://unpkg.com/", "FakeThread")
+ );
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/collapseTree.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/collapseTree.spec.js
new file mode 100644
index 0000000000..fba925c2ed
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/collapseTree.spec.js
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { makeMockDisplaySource } from "../../../utils/test-mockup";
+
+import {
+ collapseTree,
+ formatTree,
+ addToTree,
+ createDirectoryNode,
+} from "../index";
+
+const abcSource = makeMockDisplaySource(
+ "http://example.com/a/b/c.js",
+ "actor1"
+);
+const abcdeSource = makeMockDisplaySource(
+ "http://example.com/a/b/c/d/e.js",
+ "actor2"
+);
+const abxSource = makeMockDisplaySource(
+ "http://example.com/a/b/x.js",
+ "actor3"
+);
+
+describe("sources tree", () => {
+ describe("collapseTree", () => {
+ it("can collapse a single source", () => {
+ const fullTree = createDirectoryNode("root", "", []);
+ addToTree(fullTree, abcSource, "http://example.com/", "Main Thread");
+ expect(fullTree.contents).toHaveLength(1);
+ const tree = collapseTree(fullTree);
+
+ const host = tree.contents[0].contents[0];
+ expect(host.name).toBe("example.com");
+ expect(host.contents).toHaveLength(1);
+
+ const abFolder = host.contents[0];
+ expect(abFolder.name).toBe("a/b");
+ expect(abFolder.contents).toHaveLength(1);
+
+ const abcNode = abFolder.contents[0];
+ expect(abcNode.name).toBe("c.js");
+ expect(abcNode.path).toBe("Main Thread/example.com/a/b/c.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("correctly merges in a collapsed source with a deeper level", () => {
+ const fullTree = createDirectoryNode("root", "", []);
+ addToTree(fullTree, abcSource, "http://example.com/", "Main Thread");
+ addToTree(fullTree, abcdeSource, "http://example.com/", "Main Thread");
+ const tree = collapseTree(fullTree);
+
+ const host = tree.contents[0].contents[0];
+ expect(host.name).toBe("example.com");
+ expect(host.contents).toHaveLength(1);
+
+ const abFolder = host.contents[0];
+ expect(abFolder.name).toBe("a/b");
+ expect(abFolder.contents).toHaveLength(2);
+
+ const [cdFolder, abcNode] = abFolder.contents;
+ expect(abcNode.name).toBe("c.js");
+ expect(abcNode.path).toBe("Main Thread/example.com/a/b/c.js");
+ expect(cdFolder.name).toBe("c/d");
+
+ const [abcdeNode] = cdFolder.contents;
+ expect(abcdeNode.name).toBe("e.js");
+ expect(abcdeNode.path).toBe("Main Thread/example.com/a/b/c/d/e.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("correctly merges in a collapsed source with a shallower level", () => {
+ const fullTree = createDirectoryNode("root", "", []);
+ addToTree(fullTree, abcSource, "http://example.com/", "Main Thread");
+ addToTree(fullTree, abxSource, "http://example.com/", "Main Thread");
+ const tree = collapseTree(fullTree);
+
+ expect(tree.contents).toHaveLength(1);
+
+ const host = tree.contents[0].contents[0];
+ expect(host.name).toBe("example.com");
+ expect(host.contents).toHaveLength(1);
+
+ const abFolder = host.contents[0];
+ expect(abFolder.name).toBe("a/b");
+ expect(abFolder.contents).toHaveLength(2);
+
+ const [abcNode, abxNode] = abFolder.contents;
+ expect(abcNode.name).toBe("c.js");
+ expect(abcNode.path).toBe("Main Thread/example.com/a/b/c.js");
+ expect(abxNode.name).toBe("x.js");
+ expect(abxNode.path).toBe("Main Thread/example.com/a/b/x.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+
+ it("correctly merges in a collapsed source with the same level", () => {
+ const fullTree = createDirectoryNode("root", "", []);
+ addToTree(fullTree, abcdeSource, "http://example.com/", "Main Thread");
+ addToTree(fullTree, abcSource, "http://example.com/", "Main Thread");
+ const tree = collapseTree(fullTree);
+
+ expect(tree.contents).toHaveLength(1);
+
+ const host = tree.contents[0].contents[0];
+ expect(host.name).toBe("example.com");
+ expect(host.contents).toHaveLength(1);
+
+ const abFolder = host.contents[0];
+ expect(abFolder.name).toBe("a/b");
+ expect(abFolder.contents).toHaveLength(2);
+
+ const [cdFolder, abcNode] = abFolder.contents;
+ expect(abcNode.name).toBe("c.js");
+ expect(abcNode.path).toBe("Main Thread/example.com/a/b/c.js");
+ expect(cdFolder.name).toBe("c/d");
+
+ const [abcdeNode] = cdFolder.contents;
+ expect(abcdeNode.name).toBe("e.js");
+ expect(abcdeNode.path).toBe("Main Thread/example.com/a/b/c/d/e.js");
+ expect(formatTree(tree)).toMatchSnapshot();
+ });
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/getDirectories.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/getDirectories.spec.js
new file mode 100644
index 0000000000..ffcad21905
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/getDirectories.spec.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { makeMockDisplaySource } from "../../../utils/test-mockup";
+
+import { getDirectories, findSourceTreeNodes, createTree } from "../index";
+
+function formatDirectories(source, tree) {
+ const paths: any = getDirectories(source, tree);
+ return paths.map(node => node.path);
+}
+
+function createSources(urls) {
+ return {
+ FakeThread: urls.reduce((sources, url, index) => {
+ const id = `a${index}`;
+ sources[id] = makeMockDisplaySource(url, id);
+ return sources;
+ }, {}),
+ };
+}
+
+describe("getDirectories", () => {
+ it("gets a source's ancestor directories", function() {
+ const sources = createSources([
+ "http://a/b.js",
+ "http://a/c.js",
+ "http://b/c.js",
+ ]);
+
+ const threads = [
+ {
+ actor: "FakeThread",
+ url: "http://a",
+ targetType: "worker",
+ name: "FakeThread",
+ isTopLevel: false,
+ },
+ ];
+
+ const debuggeeUrl = "http://a/";
+ const { sourceTree } = createTree({
+ sources,
+ debuggeeUrl,
+ threads,
+ });
+
+ expect(formatDirectories(sources.FakeThread.a0, sourceTree)).toEqual([
+ "FakeThread/a/b.js",
+ "FakeThread/a",
+ "FakeThread",
+ ]);
+ expect(formatDirectories(sources.FakeThread.a1, sourceTree)).toEqual([
+ "FakeThread/a/c.js",
+ "FakeThread/a",
+ "FakeThread",
+ ]);
+ expect(formatDirectories(sources.FakeThread.a2, sourceTree)).toEqual([
+ "FakeThread/b/c.js",
+ "FakeThread/b",
+ "FakeThread",
+ ]);
+ });
+});
+
+describe("findSourceTreeNodes", () => {
+ it("finds a node", () => {
+ const sources = createSources([
+ "http://src/main.js",
+ "http://src/utils/help.js",
+ "http://src/utils/print.js",
+ "http://workers/worker.js",
+ ]);
+
+ const threads = [
+ {
+ actor: "FakeThread",
+ url: "http://a",
+ targetType: "worker",
+ name: "FakeThread",
+ isTopLevel: false,
+ },
+ ];
+
+ const debuggeeUrl = "http://a/";
+ const { sourceTree } = createTree({
+ sources,
+ debuggeeUrl,
+ threads,
+ });
+
+ const nodes = findSourceTreeNodes(sourceTree, "src") || [];
+ expect(nodes[0].path).toEqual("FakeThread/src");
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js
new file mode 100644
index 0000000000..1b176908e9
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/getUrl.spec.js
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { getURL } from "../getURL";
+import { makeMockSource } from "../../../utils/test-mockup";
+import type { Source } from "../../../types";
+
+function createMockSource(props): Source {
+ const rv = {
+ ...makeMockSource(),
+ ...Object.assign(
+ {
+ id: "server1.conn13.child1/39",
+ url: "",
+ sourceMapURL: "",
+ isBlackBoxed: false,
+ isPrettyPrinted: false,
+ isWasm: false,
+ },
+ props
+ ),
+ };
+ return (rv: any);
+}
+
+describe("getUrl", () => {
+ it("handles normal url with http and https for filename", function() {
+ const urlObject = getURL(createMockSource({ url: "https://a/b.js" }));
+ expect(urlObject.filename).toBe("b.js");
+
+ const urlObject2 = getURL(
+ createMockSource({ id: "server1.conn13.child1/40", url: "http://a/b.js" })
+ );
+ expect(urlObject2.filename).toBe("b.js");
+ });
+
+ it("handles url with querystring for filename", function() {
+ const urlObject = getURL(
+ createMockSource({
+ url: "https://a/b.js?key=randomKey",
+ })
+ );
+ expect(urlObject.filename).toBe("b.js");
+ });
+
+ it("handles url with '#' for filename", function() {
+ const urlObject = getURL(
+ createMockSource({
+ url: "https://a/b.js#specialSection",
+ })
+ );
+ expect(urlObject.filename).toBe("b.js");
+ });
+
+ it("handles url with no file extension for filename", function() {
+ const urlObject = getURL(
+ createMockSource({
+ url: "https://a/c",
+ id: "c",
+ })
+ );
+ expect(urlObject.filename).toBe("c");
+ });
+
+ it("handles url with no name for filename", function() {
+ const urlObject = getURL(
+ createMockSource({
+ url: "https://a/",
+ id: "c",
+ })
+ );
+ expect(urlObject.filename).toBe("(index)");
+ });
+
+ it("separates resources by protocol and host", () => {
+ const urlObject = getURL(
+ createMockSource({
+ url: "moz-extension://xyz/123",
+ id: "c2",
+ })
+ );
+ expect(urlObject.group).toBe("moz-extension://xyz");
+ });
+
+ it("creates a group name for webpack", () => {
+ const urlObject = getURL(
+ createMockSource({
+ url: "webpack:///src/component.jsx",
+ id: "c3",
+ })
+ );
+ expect(urlObject.group).toBe("webpack://");
+ });
+
+ it("creates a group name for angular source", () => {
+ const urlObject = getURL(
+ createMockSource({
+ url: "ng://src/component.jsx",
+ id: "c3",
+ })
+ );
+ expect(urlObject.group).toBe("ng://");
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/treeOrder.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/treeOrder.spec.js
new file mode 100644
index 0000000000..6835be7bc9
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/treeOrder.spec.js
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { getDomain } from "../treeOrder";
+
+describe("getDomain", () => {
+ it("parses a url and returns the host name", () => {
+ expect(getDomain("http://www.mozilla.com")).toBe("mozilla.com");
+ });
+
+ it("returns null for an undefined string", () => {
+ expect(getDomain(undefined)).toBe(null);
+ });
+
+ it("returns null for an empty string", () => {
+ expect(getDomain("")).toBe(null);
+ });
+
+ it("returns null for a poorly formed string", () => {
+ expect(getDomain("\\/~`?,.{}[]!@$%^&*")).toBe(null);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/updateTree.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/updateTree.spec.js
new file mode 100644
index 0000000000..f837c7c1be
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/updateTree.spec.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { makeMockDisplaySource } from "../../../utils/test-mockup";
+import { updateTree, createTree } from "../index";
+
+type RawSource = {| url: string, id: string, actors?: any |};
+
+function createSourcesMap(sources: RawSource[]) {
+ const sourcesMap = sources.reduce((map, source) => {
+ map[source.id] = makeMockDisplaySource(source.url, source.id);
+ return map;
+ }, {});
+
+ return { FakeThread: sourcesMap };
+}
+
+function formatTree(tree) {
+ if (!tree) {
+ throw new Error("Tree must exist");
+ }
+ return JSON.stringify(tree.uncollapsedTree, null, 2);
+}
+
+const sources = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://davidwalsh.name/",
+ },
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://davidwalsh.name/source1.js",
+ },
+ {
+ id: "server1.conn13.child1/40",
+ url: "https://davidwalsh.name/source2.js",
+ },
+];
+
+const threads = [
+ {
+ actor: "FakeThread",
+ url: "https://davidwalsh.name",
+ targetType: "worker",
+ name: "FakeThread",
+ isTopLevel: false,
+ },
+];
+
+const debuggeeUrl = "blah";
+
+describe("calls updateTree.js", () => {
+ it("adds one source", () => {
+ const prevSources = createSourcesMap([sources[0]]);
+ const { sourceTree, uncollapsedTree } = createTree({
+ debuggeeUrl,
+ sources: prevSources,
+ threads,
+ });
+
+ const newTree = updateTree({
+ debuggeeUrl,
+ prevSources,
+ newSources: createSourcesMap([sources[0], sources[1]]),
+ uncollapsedTree,
+ sourceTree,
+ threads,
+ });
+
+ expect(formatTree(newTree)).toMatchSnapshot();
+ });
+
+ it("adds two sources", () => {
+ const prevSources = createSourcesMap([sources[0]]);
+
+ const { sourceTree, uncollapsedTree } = createTree({
+ debuggeeUrl,
+ sources: prevSources,
+ threads,
+ });
+
+ const newTree = updateTree({
+ debuggeeUrl,
+ prevSources,
+ newSources: createSourcesMap([sources[0], sources[1], sources[2]]),
+ uncollapsedTree,
+ sourceTree,
+ projectRoot: "",
+ threads,
+ });
+
+ expect(formatTree(newTree)).toMatchSnapshot();
+ });
+
+ it("update sources that change their display URL", () => {
+ const prevSources = createSourcesMap([sources[0]]);
+
+ const { sourceTree, uncollapsedTree } = createTree({
+ debuggeeUrl,
+ sources: prevSources,
+ threads,
+ });
+
+ const newTree = updateTree({
+ debuggeeUrl,
+ prevSources,
+ newSources: createSourcesMap([
+ {
+ ...sources[0],
+ url: `${sources[0].url}?param`,
+ },
+ ]),
+ uncollapsedTree,
+ sourceTree,
+ projectRoot: "",
+ threads,
+ });
+
+ expect(formatTree(newTree)).toMatchSnapshot();
+ });
+
+ // NOTE: we currently only add sources to the tree and clear the tree
+ // on navigate.
+ it("shows all the sources", () => {
+ const prevSources = createSourcesMap([sources[0]]);
+
+ const { sourceTree, uncollapsedTree } = createTree({
+ debuggeeUrl,
+ sources: prevSources,
+ threads,
+ });
+
+ const newTree = updateTree({
+ debuggeeUrl,
+ prevSources,
+ newSources: createSourcesMap([sources[0], sources[1]]),
+ uncollapsedTree,
+ sourceTree,
+ projectRoot: "",
+ threads,
+ });
+
+ expect(formatTree(newTree)).toMatchSnapshot();
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/tests/utils.spec.js b/devtools/client/debugger/src/utils/sources-tree/tests/utils.spec.js
new file mode 100644
index 0000000000..0740800c4d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/tests/utils.spec.js
@@ -0,0 +1,223 @@
+/* eslint max-nested-callbacks: ["error", 4]*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { makeMockDisplaySource } from "../../test-mockup";
+
+import {
+ createDirectoryNode,
+ getRelativePath,
+ isExactUrlMatch,
+ isDirectory,
+ addToTree,
+ isNotJavaScript,
+ getPathWithoutThread,
+ createTree,
+ getSourcesInsideGroup,
+ getAllSources,
+} from "../index";
+
+type RawSource = {| url: string, id: string, actors?: any |};
+
+function createSourcesMap(sources: RawSource[]) {
+ const sourcesMap = sources.reduce((map, source) => {
+ map[source.id] = makeMockDisplaySource(source.url, source.id);
+ return map;
+ }, {});
+
+ return sourcesMap;
+}
+
+describe("sources tree", () => {
+ describe("isExactUrlMatch", () => {
+ it("recognizes root url match", () => {
+ const rootA = "http://example.com/path/to/file.html";
+ const rootB = "https://www.demo.com/index.html";
+
+ expect(isExactUrlMatch("example.com", rootA)).toBe(true);
+ expect(isExactUrlMatch("www.example.com", rootA)).toBe(true);
+ expect(isExactUrlMatch("api.example.com", rootA)).toBe(false);
+ expect(isExactUrlMatch("example.example.com", rootA)).toBe(false);
+ expect(isExactUrlMatch("www.example.example.com", rootA)).toBe(false);
+ expect(isExactUrlMatch("demo.com", rootA)).toBe(false);
+
+ expect(isExactUrlMatch("demo.com", rootB)).toBe(true);
+ expect(isExactUrlMatch("www.demo.com", rootB)).toBe(true);
+ expect(isExactUrlMatch("maps.demo.com", rootB)).toBe(false);
+ expect(isExactUrlMatch("demo.demo.com", rootB)).toBe(false);
+ expect(isExactUrlMatch("www.demo.demo.com", rootB)).toBe(false);
+ expect(isExactUrlMatch("example.com", rootB)).toBe(false);
+ });
+ });
+
+ describe("isDirectory", () => {
+ it("identifies directories correctly", () => {
+ const sources = [
+ makeMockDisplaySource("http://example.com/a.js", "actor1"),
+ makeMockDisplaySource("http://example.com/b/c/d.js", "actor2"),
+ ];
+
+ const tree = createDirectoryNode("root", "", []);
+ sources.forEach(source =>
+ addToTree(tree, source, "http://example.com/", "Main Thread")
+ );
+ const [bFolderNode, aFileNode] = tree.contents[0].contents[0].contents;
+ const [cFolderNode] = bFolderNode.contents;
+ const [dFileNode] = cFolderNode.contents;
+
+ expect(isDirectory(bFolderNode)).toBe(true);
+ expect(isDirectory(aFileNode)).toBe(false);
+ expect(isDirectory(cFolderNode)).toBe(true);
+ expect(isDirectory(dFileNode)).toBe(false);
+ });
+ });
+
+ describe("getRelativePath", () => {
+ it("gets the relative path of the file", () => {
+ const relPath = "path/to/file.html";
+ expect(getRelativePath("http://example.com/path/to/file.html")).toBe(
+ relPath
+ );
+ expect(getRelativePath("http://www.example.com/path/to/file.html")).toBe(
+ relPath
+ );
+ expect(getRelativePath("https://www.example.com/path/to/file.js")).toBe(
+ "path/to/file.js"
+ );
+ expect(getRelativePath("webpack:///path/to/file.html")).toBe(relPath);
+ expect(getRelativePath("file:///path/to/file.html")).toBe(relPath);
+ expect(getRelativePath("file:///path/to/file.html?bla")).toBe(relPath);
+ expect(getRelativePath("file:///path/to/file.html#bla")).toBe(relPath);
+ expect(getRelativePath("file:///path/to/file")).toBe("path/to/file");
+ });
+ });
+
+ describe("isNotJavaScript", () => {
+ it("js file", () => {
+ const source = makeMockDisplaySource("http://example.com/foo.js");
+ expect(isNotJavaScript(source)).toBe(false);
+ });
+
+ it("css file", () => {
+ const source = makeMockDisplaySource("http://example.com/foo.css");
+ expect(isNotJavaScript(source)).toBe(true);
+ });
+
+ it("svg file", () => {
+ const source = makeMockDisplaySource("http://example.com/foo.svg");
+ expect(isNotJavaScript(source)).toBe(true);
+ });
+
+ it("png file", () => {
+ const source = makeMockDisplaySource("http://example.com/foo.png");
+ expect(isNotJavaScript(source)).toBe(true);
+ });
+ });
+
+ describe("getPathWithoutThread", () => {
+ it("main thread pattern", () => {
+ const path = getPathWithoutThread("server1.conn0.child1/context18");
+ expect(path).toBe("");
+ });
+
+ it("main thread host", () => {
+ const path = getPathWithoutThread(
+ "server1.conn0.child1/context18/dbg-workers.glitch.me"
+ );
+ expect(path).toBe("dbg-workers.glitch.me");
+ });
+
+ it("main thread children", () => {
+ const path = getPathWithoutThread(
+ "server1.conn0.child1/context18/dbg-workers.glitch.me/more"
+ );
+ expect(path).toBe("dbg-workers.glitch.me/more");
+ });
+
+ it("worker thread", () => {
+ const path = getPathWithoutThread(
+ "server1.conn0.child1/workerTarget25/context1"
+ );
+ expect(path).toBe("");
+ });
+
+ it("worker thread with children", () => {
+ const path = getPathWithoutThread(
+ "server1.conn0.child1/workerTarget25/context1/dbg-workers.glitch.me/utils"
+ );
+ expect(path).toBe("dbg-workers.glitch.me/utils");
+ });
+
+ it("worker thread with file named like pattern", () => {
+ const path = getPathWithoutThread(
+ "server1.conn0.child1/workerTarget25/context1/dbg-workers.glitch.me/utils/context38/index.js"
+ );
+ expect(path).toBe("dbg-workers.glitch.me/utils/context38/index.js");
+ });
+ });
+
+ it("gets all sources in all threads and gets sources inside of the selected directory", () => {
+ const testData1 = [
+ {
+ id: "server1.conn13.child1/39",
+ url: "https://example.com/a.js",
+ },
+ {
+ id: "server1.conn13.child1/37",
+ url: "https://example.com/b.js",
+ },
+ {
+ id: "server1.conn13.child1/35",
+ url: "https://example.com/c.js",
+ },
+ ];
+ const testData2 = [
+ {
+ id: "server1.conn13.child1/33",
+ url: "https://example.com/d.js",
+ },
+ {
+ id: "server1.conn13.child1/31",
+ url: "https://example.com/e.js",
+ },
+ ];
+ const sources = {
+ FakeThread: createSourcesMap(testData1),
+ OtherThread: createSourcesMap(testData2),
+ };
+ const threads = [
+ {
+ actor: "FakeThread",
+ name: "FakeThread",
+ url: "https://example.com/",
+ targetType: "worker",
+ isTopLevel: false,
+ },
+ {
+ actor: "OtherThread",
+ name: "OtherThread",
+ url: "https://example.com/",
+ targetType: "worker",
+ isTopLevel: false,
+ },
+ ];
+
+ const tree = createTree({
+ sources,
+ debuggeeUrl: "https://example.com/",
+ threads,
+ }).sourceTree;
+
+ const dirA = tree.contents[0];
+ const dirB = tree.contents[1];
+
+ expect(getSourcesInsideGroup(dirA, { threads, sources })).toHaveLength(3);
+ expect(getSourcesInsideGroup(dirB, { threads, sources })).toHaveLength(2);
+ expect(getSourcesInsideGroup(tree, { threads, sources })).toHaveLength(5);
+
+ expect(getAllSources({ threads, sources })).toHaveLength(5);
+ });
+});
diff --git a/devtools/client/debugger/src/utils/sources-tree/treeOrder.js b/devtools/client/debugger/src/utils/sources-tree/treeOrder.js
new file mode 100644
index 0000000000..1cf2536f32
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/treeOrder.js
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { parse } from "../url";
+
+import { nodeHasChildren } from "./utils";
+
+import type { TreeNode } from "./types";
+
+import type { Source } from "../../types";
+
+/*
+ * Gets domain from url (without www prefix)
+ */
+export function getDomain(url?: string): ?string {
+ if (!url) {
+ return null;
+ }
+ const { host } = parse(url);
+ if (!host) {
+ return null;
+ }
+ return host.startsWith("www.") ? host.substr("www.".length) : host;
+}
+
+/*
+ * Checks if node name matches debugger host/domain.
+ */
+function isExactDomainMatch(part: string, debuggeeHost: string): boolean {
+ return part.startsWith("www.")
+ ? part.substr("www.".length) === debuggeeHost
+ : part === debuggeeHost;
+}
+
+/*
+ * Checks if node name matches IndexName
+ */
+function isIndexName(part: string, ...rest): boolean {
+ return part === IndexName;
+}
+
+/*
+ * Function to assist with node search for a defined sorted order, see e.g.
+ * `createTreeNodeMatcher`. Returns negative number if the node
+ * stands earlier in sorting order, positive number if the node stands later
+ * in sorting order, or zero if the node is found.
+ */
+export type FindNodeInContentsMatcher = (node: TreeNode) => number;
+
+/*
+ * Performs a binary search to insert a node into contents. Returns positive
+ * number, index of the found child, or negative number, which can be used
+ * to calculate a position where a new node can be inserted (`-index - 1`).
+ * The matcher is a function that returns result of comparision of a node with
+ * lookup value.
+ */
+export function findNodeInContents(
+ tree: TreeNode,
+ matcher: FindNodeInContentsMatcher
+): {| found: boolean, index: number |} {
+ if (tree.type === "source" || tree.contents.length === 0) {
+ return { found: false, index: 0 };
+ }
+
+ let left = 0;
+ let right = tree.contents.length - 1;
+ while (left < right) {
+ const middle = Math.floor((left + right) / 2);
+ if (matcher(tree.contents[middle]) < 0) {
+ left = middle + 1;
+ } else {
+ right = middle;
+ }
+ }
+ const result = matcher(tree.contents[left]);
+ if (result === 0) {
+ return { found: true, index: left };
+ }
+ return { found: false, index: result > 0 ? left : left + 1 };
+}
+
+const IndexName = "(index)";
+
+/*
+ * An array of functions to identify exceptions when sorting sourcesTree.
+ * Each function must return a boolean. Keep functions in array in the
+ * order exceptions should be sorted in.
+ */
+const matcherFunctions = [isIndexName, isExactDomainMatch];
+
+/*
+ * Creates a matcher for findNodeInContents.
+ * The sorting order of nodes during comparison is:
+ * - "(index)" node
+ * - root node with the debuggee host/domain
+ * - hosts/directories (not files) sorted by name
+ * - files sorted by name
+ */
+export function createTreeNodeMatcher(
+ part: string,
+ isDir: boolean,
+ debuggeeHost: ?string,
+ source?: Source,
+ sortByUrl?: boolean
+): FindNodeInContentsMatcher {
+ return (node: TreeNode) => {
+ for (let i = 0; i < matcherFunctions.length; i++) {
+ // Check part against exceptions
+ if (matcherFunctions[i](part, debuggeeHost)) {
+ for (let j = 0; j < i; j++) {
+ // Check node.name against exceptions
+ if (matcherFunctions[j](node.name, debuggeeHost)) {
+ return -1;
+ }
+ }
+ // If part and node.name share the same exception, return 0
+ if (matcherFunctions[i](node.name, debuggeeHost)) {
+ return 0;
+ }
+ return 1;
+ }
+ // Check node.name against exceptions if part is not exception
+ if (matcherFunctions[i](node.name, debuggeeHost)) {
+ return -1;
+ }
+ }
+ // Sort directories before files
+ const nodeIsDir = nodeHasChildren(node);
+ if (nodeIsDir && !isDir) {
+ return -1;
+ } else if (!nodeIsDir && isDir) {
+ return 1;
+ }
+
+ if (sortByUrl && node.type === "source" && source) {
+ return node.contents.url.localeCompare(source.url);
+ }
+
+ if (isExactDomainMatch(part, node.name)) {
+ return 0;
+ }
+
+ return node.name.localeCompare(part);
+ };
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/types.js b/devtools/client/debugger/src/utils/sources-tree/types.js
new file mode 100644
index 0000000000..c8dacd6849
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/types.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import type { Source } from "../../types";
+
+/**
+ * TODO: createNode is exported so this type could be useful to other modules
+ * @memberof utils/sources-tree
+ * @static
+ */
+export type TreeNode = TreeSource | TreeDirectory;
+
+export type TreeSource = {
+ type: "source",
+ name: string,
+ path: string,
+ contents: Source,
+};
+
+export type TreeDirectory = {
+ type: "directory",
+ name: string,
+ path: string,
+ contents: TreeNode[],
+};
+
+export type ParentMap = WeakMap<TreeNode, TreeDirectory>;
+
+export type SourcesGroups = {
+ sourcesInside: Source[],
+ sourcesOuside: Source[],
+};
diff --git a/devtools/client/debugger/src/utils/sources-tree/updateTree.js b/devtools/client/debugger/src/utils/sources-tree/updateTree.js
new file mode 100644
index 0000000000..af67f0f95d
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/updateTree.js
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+
+import { addToTree } from "./addToTree";
+import { collapseTree } from "./collapseTree";
+import {
+ createDirectoryNode,
+ createParentMap,
+ getPathParts,
+ isInvalidUrl,
+} from "./utils";
+import {
+ getDomain,
+ createTreeNodeMatcher,
+ findNodeInContents,
+} from "./treeOrder";
+
+import { getDisplayURL } from "./getURL";
+
+import type { SourcesMapByThread } from "../../reducers/types";
+import type { Thread, DisplaySource, URL } from "../../types";
+import type { TreeDirectory, TreeSource, TreeNode } from "./types";
+
+function getSourcesDiff(
+ newSources,
+ prevSources
+): {
+ toAdd: Array<DisplaySource>,
+ toUpdate: Array<[DisplaySource, DisplaySource]>,
+} {
+ const toAdd = [];
+ const toUpdate = [];
+
+ for (const sourceId in newSources) {
+ const newSource = newSources[sourceId];
+ const prevSource = prevSources ? prevSources[sourceId] : null;
+ if (!prevSource) {
+ toAdd.push(newSource);
+ } else if (prevSource.displayURL !== newSource.displayURL) {
+ toUpdate.push([prevSource, newSource]);
+ }
+ }
+
+ return { toAdd, toUpdate };
+}
+
+type UpdateTreeParams = {
+ newSources: SourcesMapByThread,
+ prevSources: SourcesMapByThread,
+ uncollapsedTree: TreeDirectory,
+ debuggeeUrl: URL,
+ threads: Thread[],
+ sourceTree?: TreeNode,
+};
+
+type CreateTreeParams = {
+ sources: SourcesMapByThread,
+ debuggeeUrl: URL,
+ threads: Thread[],
+};
+
+export function createTree({
+ debuggeeUrl,
+ sources,
+ threads,
+}: CreateTreeParams) {
+ const uncollapsedTree = createDirectoryNode("root", "", []);
+ const result = updateTree({
+ debuggeeUrl,
+ newSources: sources,
+ prevSources: {},
+ threads,
+ uncollapsedTree,
+ });
+
+ if (!result) {
+ throw new Error("Tree must exist");
+ }
+
+ return result;
+}
+
+export function updateTree({
+ newSources,
+ prevSources,
+ debuggeeUrl,
+ uncollapsedTree,
+ threads,
+ create,
+ sourceTree,
+}: UpdateTreeParams) {
+ const debuggeeHost = getDomain(debuggeeUrl);
+ const contexts = (Object.keys(newSources): any);
+
+ let shouldUpdate = !sourceTree;
+ for (const context of contexts) {
+ const thread = threads.find(t => t.actor === context);
+ if (!thread) {
+ continue;
+ }
+
+ const { toAdd, toUpdate } = getSourcesDiff(
+ (Object.values(newSources[context]): any),
+ prevSources[context] ? (Object.values(prevSources[context]): any) : null
+ );
+
+ for (const source of toAdd) {
+ shouldUpdate = true;
+ addToTree(uncollapsedTree, source, debuggeeHost, thread.actor);
+ }
+
+ for (const [prevSource, newSource] of toUpdate) {
+ shouldUpdate = true;
+ updateInTree(
+ uncollapsedTree,
+ prevSource,
+ newSource,
+ debuggeeHost,
+ thread.actor
+ );
+ }
+ }
+
+ if (!shouldUpdate) {
+ return false;
+ }
+
+ const newSourceTree = collapseTree(uncollapsedTree);
+
+ return {
+ uncollapsedTree,
+ sourceTree: newSourceTree,
+ parentMap: createParentMap(newSourceTree),
+ };
+}
+
+export function updateInTree(
+ tree: TreeDirectory,
+ prevSource: DisplaySource,
+ newSource: DisplaySource,
+ debuggeeHost: ?string,
+ thread: string
+): void {
+ const newUrl = getDisplayURL(newSource, debuggeeHost);
+ const prevUrl = getDisplayURL(prevSource, debuggeeHost);
+
+ const prevEntries = findEntries(
+ tree,
+ prevUrl,
+ prevSource,
+ thread,
+ debuggeeHost
+ );
+ if (!prevEntries) {
+ return;
+ }
+
+ if (!isInvalidUrl(newUrl, newSource)) {
+ const parts = getPathParts(newUrl, thread, debuggeeHost);
+
+ if (parts.length === prevEntries.length) {
+ let match = true;
+ for (let i = 0; i < parts.length - 2; i++) {
+ if (parts[i].path !== prevEntries[i + 1].node.path) {
+ match = false;
+ break;
+ }
+ }
+
+ if (match) {
+ const { node, index } = prevEntries.pop();
+ // This is guaranteed to be a TreeSource or else findEntries would
+ // not have returned anything.
+ const fileNode: TreeSource = (node.contents[index]: any);
+ fileNode.name = parts[parts.length - 1].part;
+ fileNode.path = parts[parts.length - 1].path;
+ fileNode.contents = newSource;
+ return;
+ }
+ }
+ }
+
+ // Fall back to removing the current entry and inserting a new one if we
+ // are unable do a straight find-replace of the name and contents.
+ for (let i = prevEntries.length - 1; i >= 0; i--) {
+ const { node, index } = prevEntries[i];
+
+ // If the node has only a single child, we want to keep stepping upward
+ // to find the overall value to remove.
+ if (node.contents.length > 1 || (i === 0 && thread)) {
+ node.contents.splice(index, 1);
+ break;
+ }
+ }
+ addToTree(tree, newSource, debuggeeHost, thread);
+}
+
+type Entry = {
+ node: TreeDirectory,
+ index: number,
+};
+
+function findEntries(tree, url, source, thread, debuggeeHost): ?Array<Entry> {
+ const parts = getPathParts(url, thread, debuggeeHost);
+
+ // We're searching for the directory containing the file so we pop off the
+ // potential filename. This is because the tree has some logic to inject
+ // special entries when filename parts either conflict with directories, and
+ // so the last bit of removal needs to do a broad search to find the exact
+ // target location.
+ parts.pop();
+
+ const entries = [];
+ let currentNode = tree;
+ for (const { part } of parts) {
+ const { found: childFound, index: childIndex } = findNodeInContents(
+ currentNode,
+ createTreeNodeMatcher(part, true, debuggeeHost)
+ );
+
+ if (!childFound || currentNode.type !== "directory") {
+ return null;
+ }
+
+ entries.push({
+ node: currentNode,
+ index: childIndex,
+ });
+
+ currentNode = currentNode.contents[childIndex];
+ }
+
+ // From this point, we do a depth-first search for a node containing the
+ // specified source, as mentioned above.
+ const found = (function search(node) {
+ if (node.type !== "directory") {
+ if (node.contents.id === source.id) {
+ return [];
+ }
+ return null;
+ }
+
+ for (let i = 0; i < node.contents.length; i++) {
+ const child = node.contents[i];
+ const result = search(child);
+ if (result) {
+ result.unshift({
+ node,
+ index: i,
+ });
+ return result;
+ }
+ }
+
+ return null;
+ })(currentNode);
+
+ return found ? [...entries, ...found] : null;
+}
diff --git a/devtools/client/debugger/src/utils/sources-tree/utils.js b/devtools/client/debugger/src/utils/sources-tree/utils.js
new file mode 100644
index 0000000000..f893fa76a5
--- /dev/null
+++ b/devtools/client/debugger/src/utils/sources-tree/utils.js
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+// @flow
+import { parse } from "../../utils/url";
+
+import type { TreeNode, TreeSource, TreeDirectory, ParentMap } from "./types";
+import type { Source, Thread, URL } from "../../types";
+import type { SourcesMapByThread } from "../../reducers/types";
+import { isPretty } from "../source";
+import { getURL, type ParsedURL } from "./getURL";
+const IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+
+export type PathPart = {
+ part: string,
+ path: string,
+ debuggeeHostIfRoot: ?string,
+};
+export function getPathParts(
+ url: ParsedURL,
+ thread: string,
+ debuggeeHost: ?string
+): Array<PathPart> {
+ const parts = url.path.split("/");
+ if (parts.length > 1 && parts[parts.length - 1] === "") {
+ parts.pop();
+ if (url.search) {
+ parts.push(url.search);
+ }
+ } else {
+ parts[parts.length - 1] += url.search;
+ }
+
+ parts[0] = url.group;
+ if (thread) {
+ parts.unshift(thread);
+ }
+
+ let path = "";
+ return parts.map((part, index) => {
+ if (index == 0 && thread) {
+ path = thread;
+ } else {
+ path = `${path}/${part}`;
+ }
+
+ const debuggeeHostIfRoot = index === 1 ? debuggeeHost : null;
+
+ return {
+ part,
+ path,
+ debuggeeHostIfRoot,
+ };
+ });
+}
+
+export function nodeHasChildren(item: TreeNode): boolean {
+ return item.type == "directory" && Array.isArray(item.contents);
+}
+
+export function isExactUrlMatch(pathPart: string, debuggeeUrl: URL): boolean {
+ // compare to hostname with an optional 'www.' prefix
+ const { host } = parse(debuggeeUrl);
+ if (!host) {
+ return false;
+ }
+ return (
+ host === pathPart ||
+ host.replace(/^www\./, "") === pathPart.replace(/^www\./, "")
+ );
+}
+
+export function isPathDirectory(path: string): boolean {
+ // Assume that all urls point to files except when they end with '/'
+ // Or directory node has children
+
+ if (path.endsWith("/")) {
+ return true;
+ }
+
+ let separators = 0;
+ for (let i = 0; i < path.length - 1; ++i) {
+ if (path[i] === "/") {
+ if (path[i + i] !== "/") {
+ return false;
+ }
+
+ ++separators;
+ }
+ }
+
+ switch (separators) {
+ case 0: {
+ return false;
+ }
+ case 1: {
+ return !path.startsWith("/");
+ }
+ default: {
+ return true;
+ }
+ }
+}
+
+export function isDirectory(item: TreeNode): boolean {
+ return (
+ (item.type === "directory" || isPathDirectory(item.path)) &&
+ item.name != "(index)"
+ );
+}
+
+export function getSourceFromNode(item: TreeNode): ?Source {
+ const { contents } = item;
+ if (!isDirectory(item) && !Array.isArray(contents)) {
+ return contents;
+ }
+}
+
+export function isSource(item: TreeNode): boolean {
+ return item.type === "source";
+}
+
+export function getFileExtension(source: Source): string {
+ const { path } = getURL(source);
+ if (!path) {
+ return "";
+ }
+
+ const lastIndex = path.lastIndexOf(".");
+ return lastIndex !== -1 ? path.slice(lastIndex + 1) : "";
+}
+
+export function isNotJavaScript(source: Source): boolean {
+ return ["css", "svg", "png"].includes(getFileExtension(source));
+}
+
+export function isInvalidUrl(url: ParsedURL, source: Source): boolean {
+ return (
+ !source.url ||
+ !url.group ||
+ isNotJavaScript(source) ||
+ IGNORED_URLS.includes(url) ||
+ isPretty(source)
+ );
+}
+
+export function partIsFile(
+ index: number,
+ parts: Array<PathPart>,
+ url: Object
+): boolean {
+ const isLastPart = index === parts.length - 1;
+ return isLastPart && !isDirectory(url);
+}
+
+export function createDirectoryNode(
+ name: string,
+ path: string,
+ contents: TreeNode[]
+): TreeDirectory {
+ return {
+ type: "directory",
+ name,
+ path,
+ contents,
+ };
+}
+
+export function createSourceNode(
+ name: string,
+ path: string,
+ contents: Source
+): TreeSource {
+ return {
+ type: "source",
+ name,
+ path,
+ contents,
+ };
+}
+
+export function createParentMap(tree: TreeNode): ParentMap {
+ const map = new WeakMap();
+
+ function _traverse(subtree) {
+ if (subtree.type === "directory") {
+ for (const child of subtree.contents) {
+ map.set(child, subtree);
+ _traverse(child);
+ }
+ }
+ }
+
+ if (tree.type === "directory") {
+ // Don't link each top-level path to the "root" node because the
+ // user never sees the root
+ tree.contents.forEach(_traverse);
+ }
+
+ return map;
+}
+
+export function getRelativePath(url: URL): string {
+ const { pathname } = parse(url);
+ if (!pathname) {
+ return url;
+ }
+ const index = pathname.indexOf("/");
+
+ return index !== -1 ? pathname.slice(index + 1) : "";
+}
+
+export function getPathWithoutThread(path: string): string {
+ const pathParts = path.split(/(context\d+?\/)/).splice(2);
+ if (pathParts && pathParts.length > 0) {
+ return pathParts.join("");
+ }
+ return "";
+}
+
+export function findSource(
+ { threads, sources }: { threads: Thread[], sources: SourcesMapByThread },
+ itemPath: string,
+ source: ?Source
+): ?Source {
+ const targetThread = threads.find(thread => itemPath.includes(thread.actor));
+ if (targetThread && source) {
+ const { actor } = targetThread;
+ if (sources[actor]) {
+ return sources[actor][source.id];
+ }
+ }
+ return source;
+}
+
+// NOTE: we get the source from sources because item.contents is cached
+export function getSource(
+ item: TreeNode,
+ { threads, sources }: { threads: Thread[], sources: SourcesMapByThread }
+): ?Source {
+ const source = getSourceFromNode(item);
+ return findSource({ threads, sources }, item.path, source);
+}
+
+export function getChildren(item: $Shape<TreeDirectory>) {
+ return nodeHasChildren(item) ? item.contents : [];
+}
+
+export function getAllSources({
+ threads,
+ sources,
+}: {
+ threads: Thread[],
+ sources: SourcesMapByThread,
+}): Source[] {
+ const sourcesAll = [];
+ threads.forEach(thread => {
+ const { actor } = thread;
+
+ for (const source in sources[actor]) {
+ sourcesAll.push(sources[actor][source]);
+ }
+ });
+ return sourcesAll;
+}
+
+export function getSourcesInsideGroup(
+ item: TreeNode,
+ { threads, sources }: { threads: Thread[], sources: SourcesMapByThread }
+): Source[] {
+ const sourcesInsideDirectory = [];
+
+ const findAllSourcesInsideDirectory = (directoryToSearch: TreeDirectory) => {
+ const childrenItems = getChildren(directoryToSearch);
+
+ childrenItems.forEach((itemChild: TreeNode) => {
+ if (itemChild.type === "directory") {
+ findAllSourcesInsideDirectory(itemChild);
+ } else {
+ const source = getSource(itemChild, { threads, sources });
+ if (source) {
+ sourcesInsideDirectory.push(source);
+ }
+ }
+ });
+ };
+ if (item.type === "directory") {
+ findAllSourcesInsideDirectory(item);
+ }
+ return sourcesInsideDirectory;
+}