From 0815c788fc2f7ca0d447aa80cb827c93e1b990aa Mon Sep 17 00:00:00 2001 From: chujun Date: Wed, 30 Oct 2019 07:32:39 +0000 Subject: [PATCH] Bug 1588803-Show sources after setting directory root r=davidwalsh 1.Show sources and thread name after a thread is set to root 2.Show sources under chrome:// 3.Show extension name and icon if an extension directory is set to root {F1665109} I'm building a better understanding of sources, sourcesTree and their utils. There could be better solutions than the ones I thought of. I'll appreciate feedback or discussions! Differential Revision: https://phabricator.services.mozilla.com/D50438 --HG-- extra : moz-landing-system : lando --- devtools/client/debugger/src/actions/ui.js | 7 +- .../PrimaryPanes/SourcesTreeItem.js | 10 +- .../src/components/PrimaryPanes/index.js | 51 ++- .../PrimaryPanes/tests/PrimaryPanes.spec.js | 17 + .../tests/SourcesTreeItem.spec.js | 31 +- .../__snapshots__/PrimaryPanes.spec.js.snap | 78 ++++ .../SourcesTreeItem.spec.js.snap | 372 +++++++++++++++++- .../client/debugger/src/reducers/sources.js | 9 +- devtools/client/debugger/src/utils/source.js | 30 +- .../debugger/src/utils/tests/source.spec.js | 45 +++ 10 files changed, 625 insertions(+), 25 deletions(-) diff --git a/devtools/client/debugger/src/actions/ui.js b/devtools/client/debugger/src/actions/ui.js index 51e06c22487f..abc4ff02724f 100644 --- a/devtools/client/debugger/src/actions/ui.js +++ b/devtools/client/debugger/src/actions/ui.js @@ -194,13 +194,16 @@ export function clearProjectDirectoryRoot(cx: Context) { export function setProjectDirectoryRoot(cx: Context, newRoot: string) { return ({ dispatch, getState }: ThunkArgs) => { - // Remove the thread actor ID from the root path const threadActor = startsWithThreadActor(getState(), newRoot); + + let curRoot = getProjectDirectoryRoot(getState()); + + // Remove the thread actor ID from the root path if (threadActor) { newRoot = newRoot.slice(threadActor.length + 1); + curRoot = curRoot.slice(threadActor.length + 1); } - const curRoot = getProjectDirectoryRoot(getState()); if (newRoot && curRoot) { const newRootArr = newRoot.replace(/\/+/g, "/").split("/"); const curRootArr = curRoot diff --git a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js index 155c7264c0bb..97b9430e5e31 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/SourcesTreeItem.js @@ -28,6 +28,7 @@ import { isOriginal as isOriginalSource, getSourceQueryString, isUrlExtension, + isExtensionDirectoryPath, shouldBlackbox, } from "../../utils/source"; import { isDirectory, getPathWithoutThread } from "../../utils/sources-tree"; @@ -241,7 +242,7 @@ class SourceTreeItem extends Component { return ; } else if (item.name === "ng://") { return ; - } else if (isUrlExtension(item.path) && depth === 1) { + } else if (isExtensionDirectoryPath(item.path)) { return ; } @@ -263,7 +264,10 @@ class SourceTreeItem extends Component { if (isDirectory(item)) { // Domain level - if (depth === 1 && projectRoot === "") { + if ( + (depth === 1 && projectRoot === "") || + (depth === 0 && threads.find(thrd => thrd.actor === projectRoot)) + ) { return ; } return ; @@ -379,7 +383,7 @@ function getSourceContentValue(state, source: Source) { } function isExtensionDirectory(depth, extensionName) { - return extensionName && depth === 1; + return extensionName && (depth === 1 || depth === 0); } const mapStateToProps = (state, props: OwnProps) => { diff --git a/devtools/client/debugger/src/components/PrimaryPanes/index.js b/devtools/client/debugger/src/components/PrimaryPanes/index.js index 78de1066dfb0..3b9b642f9653 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/index.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/index.js @@ -10,13 +10,14 @@ import { Tab, Tabs, TabList, TabPanels } from "react-aria-components/src/tabs"; import actions from "../../actions"; import { - getDisplayedSources, getActiveSearch, getProjectDirectoryRoot, getSelectedPrimaryPaneTab, getAllThreads, getContext, + getExtensionNameBySourceUrl, } from "../../selectors"; +import { isExtensionDirectoryPath } from "../../utils/source"; import { features, prefs } from "../../utils/prefs"; import { connect } from "../../utils/connect"; import { formatKeyShortcut } from "../../utils/text"; @@ -25,7 +26,6 @@ import Outline from "./Outline"; import SourcesTree from "./SourcesTree"; import AccessibleImage from "../shared/AccessibleImage"; -import type { SourcesMapByThread } from "../../reducers/types"; import type { SelectedPrimaryPaneTabType } from "../../selectors"; import type { Thread, Context } from "../../types"; @@ -41,9 +41,9 @@ type OwnProps = {| type Props = { cx: Context, selectedTab: SelectedPrimaryPaneTabType, - sources: SourcesMapByThread, horizontal: boolean, projectRoot: string, + rootExtensionName: ?string, sourceSearchOn: boolean, setPrimaryPaneTab: typeof actions.setPrimaryPaneTab, setActiveSearch: typeof actions.setActiveSearch, @@ -79,6 +79,26 @@ class PrimaryPanes extends Component { } }; + getRootLabel = (projectRoot: string) => { + const { threads, rootExtensionName } = this.props; + const targetThread = threads.find(thread => thread.actor === projectRoot); + + if (targetThread) { + return targetThread.name; + } else if (rootExtensionName) { + return rootExtensionName; + } else if (projectRoot.endsWith("://")) { + if (projectRoot === "ng://") { + return "Angular"; + } else if (projectRoot === "webpack://") { + return "Webpack"; + } + return `${unescape(projectRoot)}`; + } + + return projectRoot.split("/").pop(); + }; + renderOutlineTabs() { if (!features.outline) { return; @@ -112,7 +132,7 @@ class PrimaryPanes extends Component { return null; } - const rootLabel = projectRoot.split("/").pop(); + const rootLabel = this.getRootLabel(projectRoot); return (
@@ -166,14 +186,21 @@ class PrimaryPanes extends Component { } } -const mapStateToProps = state => ({ - cx: getContext(state), - selectedTab: getSelectedPrimaryPaneTab(state), - sources: getDisplayedSources(state), - sourceSearchOn: getActiveSearch(state) === "source", - threads: getAllThreads(state), - projectRoot: getProjectDirectoryRoot(state), -}); +const mapStateToProps = state => { + const newProjectRoot = getProjectDirectoryRoot(state); + const extensionAsRoot = isExtensionDirectoryPath(newProjectRoot); + + return { + cx: getContext(state), + selectedTab: getSelectedPrimaryPaneTab(state), + sourceSearchOn: getActiveSearch(state) === "source", + threads: getAllThreads(state), + projectRoot: newProjectRoot, + rootExtensionName: extensionAsRoot + ? getExtensionNameBySourceUrl(state, newProjectRoot) + : null, + }; +}; const connector = connect( mapStateToProps, diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/PrimaryPanes.spec.js b/devtools/client/debugger/src/components/PrimaryPanes/tests/PrimaryPanes.spec.js index 7d13e50a9d32..a7d93a4df4b8 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/PrimaryPanes.spec.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/PrimaryPanes.spec.js @@ -46,6 +46,23 @@ describe("PrimaryPanes", () => { expect(component).toMatchSnapshot(); }); }); + + describe("with a thread set to root", () => { + it("uses the thread name as root label", () => { + const { component } = render({ + projectRoot: "FakeThread", + threads: [ + { + actor: "FakeThread", + name: "Main Thread", + type: "mainThread", + url: "http://a", + }, + ], + }); + expect(component).toMatchSnapshot(); + }); + }); }); function generateDefaults(overrides) { diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js b/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js index d66a8058f764..54963794399e 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/SourcesTreeItem.spec.js @@ -282,7 +282,36 @@ describe("SourceTreeItem", () => { "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856" ); - const node = render({ item, depth: 0 }); + const node = render({ item, depth: 1 }); + expect(node).toMatchSnapshot(); + }); + + it("should show icon for moz-extension item when a thread is set to root", async () => { + const item = createMockDirectory( + "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", + "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856" + ); + const node = render({ + item, + depth: 0, + projectRoot: "server1.conn13.child1/thread19", + threads: [ + { name: "Main Thread", actor: "server1.conn13.child1/thread19" }, + ], + }); + expect(node).toMatchSnapshot(); + }); + + it("should show icon for domain item when a thread is set to root", async () => { + const item = createMockDirectory(); + const node = render({ + item, + depth: 0, + projectRoot: "server1.conn13.child1/thread19", + threads: [ + { name: "Main Thread", actor: "server1.conn13.child1/thread19" }, + ], + }); expect(node).toMatchSnapshot(); }); diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/PrimaryPanes.spec.js.snap b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/PrimaryPanes.spec.js.snap index d07959bd25f9..957b21a6ae6b 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/PrimaryPanes.spec.js.snap +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/PrimaryPanes.spec.js.snap @@ -137,3 +137,81 @@ exports[`PrimaryPanes with custom root renders empty custom root source list 1`] `; + +exports[`PrimaryPanes with a thread set to root uses the thread name as root label 1`] = ` + + + + Sources + + + Outline + + + +
+
+ +
+ +
+ +
+
+`; diff --git a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap index 51242fcf0e64..6f972979b09a 100644 --- a/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap +++ b/devtools/client/debugger/src/components/PrimaryPanes/tests/__snapshots__/SourcesTreeItem.spec.js.snap @@ -1700,7 +1700,7 @@ Object { className="arrow" /> + + + + moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856 + + +
, + "defaultState": null, + "instance": SourceTreeItem { + "addCollapseExpandAllOptions": [Function], + "context": Object {}, + "handleDownloadFile": [Function], + "onClick": [Function], + "onContextMenu": [Function], + "props": Object { + "clearProjectDirectoryRoot": [MockFunction], + "debuggeeUrl": "http://mdn.com", + "depth": 0, + "expanded": false, + "focusItem": [MockFunction], + "item": Object { + "contents": Array [], + "name": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", + "path": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", + "type": "directory", + }, + "projectRoot": "server1.conn13.child1/thread19", + "selectItem": [MockFunction], + "setExpanded": [MockFunction], + "setProjectDirectoryRoot": [MockFunction], + "source": Object { + "extensionName": null, + "id": "server1.conn13.child1/39", + "introductionType": undefined, + "introductionUrl": null, + "isBlackBoxed": false, + "isExtension": false, + "isPrettyPrinted": false, + "isWasm": false, + "relativeUrl": "http://mdn.com/one.js", + "url": "http://mdn.com/one.js", + }, + "threads": Array [ + Object { + "actor": "server1.conn13.child1/thread19", + "name": "Main Thread", + }, + ], + "toggleBlackBox": [MockFunction], + }, + "refs": Object {}, + "setState": [Function], + "state": null, + "updater": Updater { + "_callbacks": Array [], + "_renderer": ReactShallowRenderer { + "_context": Object {}, + "_element": , + "_forcedUpdate": false, + "_instance": [Circular], + "_newState": null, + "_rendered":
+ + + + moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856 + + +
, + "_rendering": false, + "_updater": [Circular], + }, + }, + Symbol(enzyme.__setState__): [Function], + }, + "props": Object { + "clearProjectDirectoryRoot": [MockFunction], + "debuggeeUrl": "http://mdn.com", + "depth": 0, + "expanded": false, + "focusItem": [MockFunction], + "item": Object { + "contents": Array [], + "name": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", + "path": "moz-extension://e37c3c08-beac-a04b-8032-c4f699a1a856", + "type": "directory", + }, + "projectRoot": "server1.conn13.child1/thread19", + "selectItem": [MockFunction], + "setExpanded": [MockFunction], + "setProjectDirectoryRoot": [MockFunction], + "source": Object { + "extensionName": null, + "id": "server1.conn13.child1/39", + "introductionType": undefined, + "introductionUrl": null, + "isBlackBoxed": false, + "isExtension": false, + "isPrettyPrinted": false, + "isWasm": false, + "relativeUrl": "http://mdn.com/one.js", + "url": "http://mdn.com/one.js", + }, + "threads": Array [ + Object { + "actor": "server1.conn13.child1/thread19", + "name": "Main Thread", + }, + ], + "toggleBlackBox": [MockFunction], + }, +} +`; + +exports[`SourceTreeItem renderItem should show icon for domain item when a thread is set to root 1`] = ` +Object { + "component":
+ + + + folder + + +
, + "defaultState": null, + "instance": SourceTreeItem { + "addCollapseExpandAllOptions": [Function], + "context": Object {}, + "handleDownloadFile": [Function], + "onClick": [Function], + "onContextMenu": [Function], + "props": Object { + "clearProjectDirectoryRoot": [MockFunction], + "debuggeeUrl": "http://mdn.com", + "depth": 0, + "expanded": false, + "focusItem": [MockFunction], + "item": Object { + "contents": Array [], + "name": "folder", + "path": "domain/subfolder", + "type": "directory", + }, + "projectRoot": "server1.conn13.child1/thread19", + "selectItem": [MockFunction], + "setExpanded": [MockFunction], + "setProjectDirectoryRoot": [MockFunction], + "source": Object { + "extensionName": null, + "id": "server1.conn13.child1/39", + "introductionType": undefined, + "introductionUrl": null, + "isBlackBoxed": false, + "isExtension": false, + "isPrettyPrinted": false, + "isWasm": false, + "relativeUrl": "http://mdn.com/one.js", + "url": "http://mdn.com/one.js", + }, + "threads": Array [ + Object { + "actor": "server1.conn13.child1/thread19", + "name": "Main Thread", + }, + ], + "toggleBlackBox": [MockFunction], + }, + "refs": Object {}, + "setState": [Function], + "state": null, + "updater": Updater { + "_callbacks": Array [], + "_renderer": ReactShallowRenderer { + "_context": Object {}, + "_element": , + "_forcedUpdate": false, + "_instance": [Circular], + "_newState": null, + "_rendered":
+ + + + folder + + +
, + "_rendering": false, + "_updater": [Circular], + }, + }, + Symbol(enzyme.__setState__): [Function], + }, + "props": Object { + "clearProjectDirectoryRoot": [MockFunction], + "debuggeeUrl": "http://mdn.com", + "depth": 0, + "expanded": false, + "focusItem": [MockFunction], + "item": Object { + "contents": Array [], + "name": "folder", + "path": "domain/subfolder", + "type": "directory", + }, + "projectRoot": "server1.conn13.child1/thread19", + "selectItem": [MockFunction], + "setExpanded": [MockFunction], + "setProjectDirectoryRoot": [MockFunction], + "source": Object { + "extensionName": null, + "id": "server1.conn13.child1/39", + "introductionType": undefined, + "introductionUrl": null, + "isBlackBoxed": false, + "isExtension": false, + "isPrettyPrinted": false, + "isWasm": false, + "relativeUrl": "http://mdn.com/one.js", + "url": "http://mdn.com/one.js", + }, + "threads": Array [ + Object { + "actor": "server1.conn13.child1/thread19", + "name": "Main Thread", + }, + ], + "toggleBlackBox": [MockFunction], + }, +} +`; + exports[`SourceTreeItem renderItem should show icon for webpack item 1`] = ` Object { "component":
, |}, Array > = makeReduceQuery( @@ -807,11 +809,12 @@ const queryAllDisplayedSources: ReduceQuery< projectDirectoryRoot, chromeAndExtensionsEnabled, debuggeeIsWebExtension, + threadActors, } ) => ({ id: resource.id, displayed: - underRoot(resource, projectDirectoryRoot) && + underRoot(resource, projectDirectoryRoot, threadActors) && (!resource.isExtension || chromeAndExtensionsEnabled || debuggeeIsWebExtension), @@ -833,6 +836,10 @@ function getAllDisplayedSources( projectDirectoryRoot: state.sources.projectDirectoryRoot, chromeAndExtensionsEnabled: state.sources.chromeAndExtenstionsEnabled, debuggeeIsWebExtension: state.threads.isWebExtension, + threadActors: [ + getMainThread(state).actor, + ...getThreads(state).map(t => t.actor), + ], }); } diff --git a/devtools/client/debugger/src/utils/source.js b/devtools/client/debugger/src/utils/source.js index f288b388e89d..639f7cf33846 100644 --- a/devtools/client/debugger/src/utils/source.js +++ b/devtools/client/debugger/src/utils/source.js @@ -28,6 +28,7 @@ import type { SourceActor, SourceContent, SourceLocation, + ThreadId, } from "../types"; import { isFulfilled, type AsyncValue } from "./async-value"; import type { Symbols } from "../reducers/types"; @@ -483,7 +484,23 @@ export function getRelativeUrl(source: Source, root: string) { return url.slice(url.indexOf(root) + root.length + 1); } -export function underRoot(source: Source, root: string) { +export function underRoot( + source: Source, + root: string, + threadActors: Array +) { + // source.url doesn't include thread actor ID, so remove the thread actor ID from the root + threadActors.forEach(threadActor => { + if (root.includes(threadActor)) { + root = root.slice(threadActor.length + 1); + } + }); + + if (source.url && source.url.includes("chrome://")) { + const { group, path } = getURL(source); + return (group + path).includes(root); + } + return source.url && source.url.includes(root); } @@ -509,6 +526,17 @@ export function isUrlExtension(url: string) { return url.includes("moz-extension:") || url.includes("chrome-extension"); } +export function isExtensionDirectoryPath(url: string) { + if (isUrlExtension(url)) { + const urlArr = url.replace(/\/+/g, "/").split("/"); + let extensionIndex = urlArr.indexOf("moz-extension:"); + if (extensionIndex === -1) { + extensionIndex = urlArr.indexOf("chrome-extension:"); + } + return !urlArr[extensionIndex + 2]; + } +} + export function getPlainUrl(url: string): string { const queryStart = url.indexOf("?"); return queryStart !== -1 ? url.slice(0, queryStart) : url; diff --git a/devtools/client/debugger/src/utils/tests/source.spec.js b/devtools/client/debugger/src/utils/tests/source.spec.js index e537ce4f31d8..97b2532ba94a 100644 --- a/devtools/client/debugger/src/utils/tests/source.spec.js +++ b/devtools/client/debugger/src/utils/tests/source.spec.js @@ -13,7 +13,9 @@ import { getSourceLineCount, isThirdParty, isJavaScript, + underRoot, isUrlExtension, + isExtensionDirectoryPath, } from "../source.js"; import { @@ -504,6 +506,35 @@ describe("sources", () => { }); }); + describe("underRoot", () => { + const threadActors = ["server0.conn1.child1/thread19"]; + + it("should detect normal source urls", () => { + const source = makeMockSource( + "resource://activity-stream/vendor/react.js" + ); + expect( + underRoot(source, "resource://activity-stream", threadActors) + ).toBe(true); + }); + + it("should detect source urls under chrome:// as root", () => { + const source = makeMockSource( + "chrome://browser/content/contentSearchUI.js" + ); + expect(underRoot(source, "chrome://", threadActors)).toBe(true); + }); + + it("should detect source urls if root is a thread actor Id", () => { + const source = makeMockSource( + "resource://activity-stream/vendor/react-dom.js" + ); + expect( + underRoot(source, "server0.conn1.child1/thread19", threadActors) + ).toBe(true); + }); + }); + describe("isUrlExtension", () => { it("should detect mozilla extenstion", () => { expect(isUrlExtension("moz-extension://id/js/content.js")).toBe(true); @@ -515,4 +546,18 @@ describe("sources", () => { expect(isUrlExtension("https://example.org/init.js")).toBe(false); }); }); + + describe("isExtensionDirectoryPath", () => { + it("should detect mozilla extenstion directory", () => { + expect(isExtensionDirectoryPath("moz-extension://id")).toBe(true); + }); + it("should detect chrome extenstion directory", () => { + expect(isExtensionDirectoryPath("chrome-extension://id")).toBe(true); + }); + it("should return false for child file within the extenstion directory", () => { + expect(isExtensionDirectoryPath("moz-extension://id/js/content.js")).toBe( + false + ); + }); + }); });