Repository: microsoft/vscode-edge-devtools
Pull request #136 (2020-05-06 02:59:52)

Adding an extension setting that will  toggle network panel on and off
This PR introduces a setting that allows to control whether the "Network" panel should be displayed or not. 

New setting:
 ![image](https://user-images.githubusercontent.com/43078681/81134813-ca9db700-8f0a-11ea-8877-26627e17b950.png)

When _checked_ the network panel will be displayed. 


mliao95: Sorry for the delay in reviewing.  I'll take a closer look next week, but there are a couple high-level points I wanted to discuss.

The first is regarding the main header tab bar.  Before merging the network tool, this was hidden.  Do you think it is worth re-hiding it when the Network tab is hidden?
![image](https://user-images.githubusercontent.com/14304598/81442164-f2b63180-9127-11ea-9047-d0e73bb96b2e.png)
Fix: Re-implement this function and apply it only when the network tool is disabled: https://github.com/microsoft/vscode-edge-devtools/blob/8abd6ee7a93bc61da4c7ed3f4aedbf85281a5067/src/host/polyfills/simpleView.ts#L104

The next is applying the applyHandleAction patch to InspectorView.js.  Applying the applyHandleAction patch to InspectorView.js will disable open/closing the drawer using "ESC" and changing tabs using "CTRL + ]" and "CTRL + [".  This should be disabled when the network tool is hidden as both the drawer toggle and tab change is useless (changing tabs doesn't work for the styles pane tabs, just the main toolbar)

Fix: Apply applyHandleAction patch to InspectorView.js here:
https://github.com/microsoft/vscode-edge-devtools/blob/master/postBuildStep.ts#L108

Patch:
diff --git a/package.json b/package.json
index c97efc1..3f2ab44 100644
--- a/package.json
+++ b/package.json
@@ -157,6 +157,11 @@
                     "description": "Auto attach the Microsoft Edge Tools extension when you launch a browser with the Debugger For Microsoft Edge debug adapter extension",
                     "default": true
                 },
+                "vscode-edge-devtools.enableNetwork": {
+                    "type": "boolean",
+                    "description": "Enable network panel (requires relaunching extension)",
+                    "default": true
+                },
                 "vscode-edge-devtools.timeout": {
                     "type": "number",
                     "description": "The number of milliseconds that the Microsoft Edge Tools will keep trying to attach the browser before timing out",
diff --git a/src/common/tabSettingsProvider.ts b/src/common/tabSettingsProvider.ts
new file mode 100644
index 0000000..4f03620
--- /dev/null
+++ b/src/common/tabSettingsProvider.ts
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+import * as vscode from "vscode";
+import { SETTINGS_STORE_NAME } from "../utils";
+
+export class TabSettingsProvider {
+
+  private static singletonInstance: TabSettingsProvider;
+
+  public static get instance() {
+    if (!TabSettingsProvider.singletonInstance) {
+      TabSettingsProvider.singletonInstance = new TabSettingsProvider();
+    }
+
+    return TabSettingsProvider.singletonInstance;
+  }
+
+  public isNetworkEnabled(): boolean {
+    const settings = vscode.workspace.getConfiguration(SETTINGS_STORE_NAME);
+    const networkEnabled: boolean = settings.get("enableNetwork") || false;
+    return networkEnabled;
+  }
+}
diff --git a/src/common/webviewEvents.ts b/src/common/webviewEvents.ts
index 7116c4e..1de386f 100644
--- a/src/common/webviewEvents.ts
+++ b/src/common/webviewEvents.ts
@@ -1,7 +1,8 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT License.
 
-export type WebviewEvent = "getState" | "getUrl" | "openInEditor" | "ready" | "setState" | "telemetry" | "websocket";
+export type WebviewEvent = "getState" | "getUrl" | "openInEditor" | "ready" | "setState" | "telemetry" | "websocket"
+| "getApprovedTabs";
 export const webviewEventNames: WebviewEvent[] = [
     "getState",
     "getUrl",
@@ -10,6 +11,7 @@ export const webviewEventNames: WebviewEvent[] = [
     "setState",
     "telemetry",
     "websocket",
+    "getApprovedTabs",
 ];
 
 export type WebSocketEvent = "open" | "close" | "error" | "message";
diff --git a/src/devtoolsPanel.ts b/src/devtoolsPanel.ts
index 9bce6dc..a5174b8 100644
--- a/src/devtoolsPanel.ts
+++ b/src/devtoolsPanel.ts
@@ -1,10 +1,11 @@
 // Copyright (c) Microsoft Corporation. All rights reserved.
 // Licensed under the MIT License.
-
 import * as path from "path";
 import * as vscode from "vscode";
 import * as debugCore from "vscode-chrome-debug-core";
 import TelemetryReporter from "vscode-extension-telemetry";
+
+import { TabSettingsProvider } from "./common/tabSettingsProvider";
 import {
     encodeMessageForChannel,
     IOpenEditorData,
@@ -54,6 +55,7 @@ export class DevToolsPanel {
         this.panelSocket.on("websocket", () => this.onSocketMessage());
         this.panelSocket.on("telemetry", (msg) => this.onSocketTelemetry(msg));
         this.panelSocket.on("getState", (msg) => this.onSocketGetState(msg));
+        this.panelSocket.on("getApprovedTabs", (msg) => this.onSocketGetApprovedTabs(msg));
         this.panelSocket.on("setState", (msg) => this.onSocketSetState(msg));
         this.panelSocket.on("getUrl", (msg) => this.onSocketGetUrl(msg));
         this.panelSocket.on("openInEditor", (msg) => this.onSocketOpenInEditor(msg));
@@ -159,6 +161,13 @@ export class DevToolsPanel {
         encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getState", { id, preferences });
     }
 
+    private onSocketGetApprovedTabs(message: string) {
+        const { id } = JSON.parse(message) as { id: number };
+        encodeMessageForChannel((msg) => this.panel.webview.postMessage(msg), "getApprovedTabs", {
+            enableNetwork: TabSettingsProvider.instance.isNetworkEnabled(),
+            id });
+    }
+
     private onSocketSetState(message: string) {
         // Parse the preference from the message and store it
         const { name, value } = JSON.parse(message) as { name: string, value: string };
diff --git a/src/host/polyfills/simpleView.ts b/src/host/polyfills/simpleView.ts
index fef7437..b8ea52f 100644
--- a/src/host/polyfills/simpleView.ts
+++ b/src/host/polyfills/simpleView.ts
@@ -26,6 +26,10 @@ export function revealInVSCode(revealable: IRevealable | undefined, omitFocus: b
     return Promise.resolve();
 }
 
+export function getApprovedTabs(callback: () => void) {
+    InspectorFrontendHost.getApprovedTabs(callback);
+}
+
 export function applyCommonRevealerPatch(content: string) {
     const pattern = /let reveal\s*=\s*function\(revealable,\s*omitFocus\)\s*{/g;
     if (content.match(pattern)) {
@@ -121,7 +125,7 @@ export function applySetTabIconPatch(content: string) {
 export function applyAppendTabPatch(content: string) {
     // The appendTab function chooses which tabs to put in the tabbed pane header section
     // showTabElement and selectTab are only called by tabs that have already been appended via appendTab.
-    const allowedTabs = [
+    const elementsTabs = [
         "elements",
         "Styles",
         "Computed",
@@ -129,7 +133,51 @@ export function applyAppendTabPatch(content: string) {
         "elements.domProperties",
         "elements.domBreakpoints",
         "elements.eventListeners",
-        "network",
+    ];
+
+    const condition = elementsTabs.map((tab) => {
+        return `id !== '${tab}'`;
+    }).join(" && ");
+
+    const appendTabWrapper =
+        /appendTab\(id,\s*tabTitle\s*,\s*view,\s*tabTooltip,\s*userGesture,\s*isCloseable,\s*index\)\s*{/;
+    const injectionPoint = /}(\s|\n)*export\s*const\s*Events={/;
+
+    // Injecting our verifications by redirecting appendTab to appendTabOverride
+    if (content.match(appendTabWrapper)) {
+        content = content.replace(
+            appendTabWrapper,
+            `appendTabOverride(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {`);
+    } else {
+        return null;
+    }
+
+    // We then replace with the verifications itself.
+    if (content.match(injectionPoint)) {
+        content = content.replace(
+            injectionPoint,
+            // points to the getApprovedTabs, then take the out the "function" from the string (slice(9))
+            `${getApprovedTabs.toString().slice(9)};
+            appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {
+                let patchedCondition = ${condition};
+                this.getApprovedTabs((approvedTabs)=>{
+                    ${applyEnableNetworkPatch()}
+                    if (!patchedCondition) {
+                        this.appendTabOverride(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index);
+                    }
+                });
+            }}
+            export const Events={`);
+    } else {
+        return null;
+    }
+
+    return content;
+}
+
+export function applyEnableNetworkPatch(): string {
+    // Creates the condition to display or hide the network panel.
+    const networkTabs = ["network",
         "network.blocked-urls",
         "network.search-network-tab",
         "headers",
@@ -147,22 +195,15 @@ export function applyAppendTabPatch(content: string) {
         "devices",
         "throttling-conditions",
         "emulation-geolocations",
-        "Shortcuts",
-    ];
+        "Shortcuts"];
 
-    const condition = allowedTabs.map((tab) => {
+    const networkCondition = networkTabs.map((tab) => {
         return `id !== '${tab}'`;
     }).join(" && ");
 
-    const pattern = /appendTab\(id,\s*tabTitle\s*,\s*view,\s*tabTooltip,\s*userGesture,\s*isCloseable,\s*index\)\s*{/;
-
-    if (content.match(pattern)) {
-        return content.replace(
-            pattern,
-            `appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) { if (${condition}) return;`);
-    } else {
-        return null;
-    }
+    return `if(approvedTabs.enableNetwork) {
+        patchedCondition = patchedCondition && (${networkCondition});
+    }`;
 }
 
 export function applyDrawerTabLocationPatch(content: string) {
diff --git a/src/host/toolsHost.ts b/src/host/toolsHost.ts
index 43a71f7..37fea2f 100644
--- a/src/host/toolsHost.ts
+++ b/src/host/toolsHost.ts
@@ -13,8 +13,8 @@ import ToolsWebSocket from "./toolsWebSocket";
 
 export default class ToolsHost {
     private resourceLoader: Readonly<ToolsResourceLoader> | undefined;
-    private getStateNextId: number = 0;
-    private getStateCallbacks: Map<number, (preferences: object) => void> = new Map();
+    private getHostCallbacksNextId: number = 0;
+    private getHostCallbacks: Map<number, (preferences: object) => void> = new Map();
 
     public setResourceLoader(resourceLoader: Readonly<ToolsResourceLoader>) {
         this.resourceLoader = resourceLoader;
@@ -27,8 +27,8 @@ export default class ToolsHost {
 
     public getPreferences(callback: (preferences: any) => void) {
         // Load the preference via the extension workspaceState
-        const id = this.getStateNextId++;
-        this.getStateCallbacks.set(id, callback);
+        const id = this.getHostCallbacksNextId++;
+        this.getHostCallbacks.set(id, callback);
         encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "getState", { id });
     }
 
@@ -80,11 +80,17 @@ export default class ToolsHost {
         encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "openInEditor", request);
     }
 
+    public getApprovedTabs(callback: () => void) {
+        const id = this.getHostCallbacksNextId++;
+        this.getHostCallbacks.set(id, callback);
+        encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "getApprovedTabs", {id});
+    }
+
     public onMessageFromChannel(e: WebviewEvent, args: string): boolean {
         switch (e) {
             case "getState": {
                 const { id, preferences } = JSON.parse(args);
-                this.fireGetStateCallback(id, preferences);
+                this.fireGetHostCallback(id, preferences);
                 break;
             }
 
@@ -99,6 +105,14 @@ export default class ToolsHost {
                 this.fireWebSocketCallback(event, message);
                 break;
             }
+
+            case "getApprovedTabs": {
+                const { id, enableNetwork } = JSON.parse(args);
+                this.fireGetHostCallback(id, {
+                        enableNetwork,
+                });
+                break;
+            }
         }
         return true;
     }
@@ -108,10 +122,10 @@ export default class ToolsHost {
         encodeMessageForChannel((msg) => window.parent.postMessage(msg, "*"), "telemetry", telemetry);
     }
 
-    private fireGetStateCallback(id: number, preferences: object) {
-        if (this.getStateCallbacks.has(id)) {
-            this.getStateCallbacks.get(id)!(preferences);
-            this.getStateCallbacks.delete(id);
+    private fireGetHostCallback(id: number, args: object) {
+        if (this.getHostCallbacks.has(id)) {
+            this.getHostCallbacks.get(id)!(args);
+            this.getHostCallbacks.delete(id);
         }
     }
 


Test patch:
diff --git a/src/common/tabSettingsProvider.test.ts b/src/common/tabSettingsProvider.test.ts
new file mode 100644
index 0000000..c6bc343
--- /dev/null
+++ b/src/common/tabSettingsProvider.test.ts
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+
+import { createFakeVSCode } from "../test/helpers";
+
+// Licensed under the MIT License.
+
+jest.mock("vscode", () => createFakeVSCode(), { virtual: true });
+
+describe("tabSettingsProvider", () => {
+  beforeEach(() => {
+    const mockVSCode = createFakeVSCode();
+    jest.doMock("vscode", () => mockVSCode, { virtual: true });
+    jest.resetModules();
+  });
+
+  describe("GetTabConfiguration", () => {
+    it("test that singleton provides the right instance", async () => {
+      const tsp = await import("../common/tabSettingsProvider");
+      const instance = tsp.TabSettingsProvider.instance;
+      const instanceB = tsp.TabSettingsProvider.instance;
+      expect(instance).not.toEqual(null);
+      expect(instance).toEqual(instanceB);
+    });
+
+    it("test that the right value is retrieved for networkEnabled configuration", async () => {
+      jest.requireMock("vscode");
+      const tsp = await import("../common/tabSettingsProvider");
+      const instance = tsp.TabSettingsProvider.instance;
+      expect(instance).not.toEqual(null);
+      const result = instance.isNetworkEnabled();
+      expect(result).toEqual(true);
+    });
+  });
+});
diff --git a/src/devtoolsPanel.test.ts b/src/devtoolsPanel.test.ts
index d2c0000..6936adb 100644
--- a/src/devtoolsPanel.test.ts
+++ b/src/devtoolsPanel.test.ts
@@ -466,6 +466,31 @@ describe("devtoolsPanel", () => {
                 expect(mockVsCode.window.showTextDocument).toHaveBeenCalled();
             });
 
+            it("calls getApprovedTabs", async () => {
+                const expectedId = { id: 0 };
+                const expectedState = { enableNetwork: true };
+                (context.workspaceState.get as jest.Mock).mockReturnValue(expectedState);
+
+                const dtp = await import("./devtoolsPanel");
+                dtp.DevToolsPanel.createOrShow(context, mockTelemetry, "", mockRuntimeConfig);
+
+                hookedEvents.get("getApprovedTabs")!(JSON.stringify(expectedId));
+                expect(mockWebviewEvents.encodeMessageForChannel).toHaveBeenCalledWith(
+                    expect.any(Function),
+                    "getApprovedTabs",
+                    {
+                        enableNetwork: expectedState.enableNetwork,
+                        id: expectedId.id,
+                    },
+                );
+
+                // Ensure that the encoded message is actually passed over to the webview
+                const expectedPostedMessage = "encodedMessage";
+                const { callback, thisObj } = getFirstCallback(mockWebviewEvents.encodeMessageForChannel);
+                callback.call(thisObj, expectedPostedMessage);
+                expect(mockPanel.webview.postMessage).toHaveBeenCalledWith(expectedPostedMessage);
+            });
+
             it("shows an error for unmapped urls", async () => {
                 const expectedRequest = {
                     column: 5,
diff --git a/src/host/polyfills/simpleView.test.ts b/src/host/polyfills/simpleView.test.ts
index eba0a1f..85a4dae 100644
--- a/src/host/polyfills/simpleView.test.ts
+++ b/src/host/polyfills/simpleView.test.ts
@@ -110,13 +110,13 @@ describe("simpleView", () => {
 
     it("applyAppendTabPatch correctly changes text", async () => {
         const apply = await import("./simpleView");
-        const comparableText = "appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {";
+        const comparableText = `appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {}
+        export const Events={}`;
         let fileContents = getTextFromFile("ui/TabbedPane.js");
         fileContents = fileContents ? fileContents : comparableText;
         const result = apply.applyAppendTabPatch(fileContents);
-        expect(result).not.toEqual(null);
         expect(result).toEqual(expect.stringContaining(
-            "appendTab(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) { if (id"));
+            "appendTabOverride(id, tabTitle, view, tabTooltip, userGesture, isCloseable, index) {"));
     });
 
     it("applyShowElementsTab correctly changes text", async () => {
diff --git a/src/test/helpers.ts b/src/test/helpers.ts
index 5114b24..3c35b34 100644
--- a/src/test/helpers.ts
+++ b/src/test/helpers.ts
@@ -57,7 +57,13 @@ export function createFakeVSCode() {
         },
         workspace: {
             getConfiguration: jest.fn(() => {
-                return { get: (name: string) => "" };
+                return { get: (name: string) => {
+                    if ("enableNetwork" === name) {
+                        return true;
+                    } else {
+                        return "";
+                    }
+                } };
             }),
             onDidChangeConfiguration: jest.fn(),
             openTextDocument: jest.fn().mockResolvedValue(null),