diff options
Diffstat (limited to 'devtools/client')
120 files changed, 2863 insertions, 647 deletions
diff --git a/devtools/client/canvasdebugger/test/browser_profiling-canvas.js b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js index ede8a4dbf..75f8da4f3 100644..100755 --- a/devtools/client/canvasdebugger/test/browser_profiling-canvas.js +++ b/devtools/client/canvasdebugger/test/browser_profiling-canvas.js @@ -37,7 +37,7 @@ function* ifTestingSupported() { for (let i = 0; i < functionCalls.length - 1; i += 2) { ok(functionCalls[i].timestamp > 0, "The timestamp of the called function is larger than 0."); ok(functionCalls[i].timestamp < currentTime, "The timestamp has been minus the frame start time."); - ok(functionCalls[i + 1].timestamp > functionCalls[i].timestamp, "The timestamp of the called function is correct."); + ok(functionCalls[i + 1].timestamp >= functionCalls[i].timestamp, "The timestamp of the called function is correct."); } yield removeTab(target.tab); diff --git a/devtools/client/commandline/test/browser_cmd_commands.js b/devtools/client/commandline/test/browser_cmd_commands.js index 6c69034ec..78db77bfd 100644 --- a/devtools/client/commandline/test/browser_cmd_commands.js +++ b/devtools/client/commandline/test/browser_cmd_commands.js @@ -4,7 +4,7 @@ // Test various GCLI commands const TEST_URI = "data:text/html;charset=utf-8,gcli-commands"; -const HUDService = require("devtools/client/webconsole/hudservice"); +const {HUDService} = require("devtools/client/webconsole/hudservice"); // Use the old webconsole since pprint isn't working on new one (Bug 1304794) Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false); diff --git a/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js b/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js index 739d3b2a7..b2fa66872 100644 --- a/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js +++ b/devtools/client/debugger/test/mochitest/browser_dbg_closure-inspection.js @@ -53,13 +53,13 @@ function test() { .getAttribute("value"), "getName", "Should have the right property name for 'getName' in person."); is(personNode.get("getName").target.querySelector(".value") - .getAttribute("value"), "_pfactory/<.getName()", + .getAttribute("value"), "getName()", "'getName' in person should have the right value."); is(personNode.get("getFoo").target.querySelector(".name") .getAttribute("value"), "getFoo", "Should have the right property name for 'getFoo' in person."); is(personNode.get("getFoo").target.querySelector(".value") - .getAttribute("value"), "_pfactory/<.getFoo()", + .getAttribute("value"), "getFoo()", "'getFoo' in person should have the right value."); // Expand the function nodes. This causes their properties to be diff --git a/devtools/client/devtools-startup.js b/devtools/client/devtools-startup.js index 2271dd790..4a385a9d5 100644 --- a/devtools/client/devtools-startup.js +++ b/devtools/client/devtools-startup.js @@ -25,7 +25,7 @@ function DevToolsStartup() {} DevToolsStartup.prototype = { handle: function (cmdLine) { - let consoleFlag = cmdLine.handleFlag("jsconsole", false); + let consoleFlag = cmdLine.handleFlag("browserconsole", false); let debuggerFlag = cmdLine.handleFlag("jsdebugger", false); let devtoolsFlag = cmdLine.handleFlag("devtools", false); @@ -75,9 +75,9 @@ DevToolsStartup.prototype = { this.initDevTools(); let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); - let hudservice = require("devtools/client/webconsole/hudservice"); + let { HUDService } = require("devtools/client/webconsole/hudservice"); let { console } = Cu.import("resource://gre/modules/Console.jsm", {}); - hudservice.toggleBrowserConsole().then(null, console.error); + HUDService.toggleBrowserConsole().then(null, console.error); } else { // the Browser Console was already open window.focus(); @@ -104,12 +104,14 @@ DevToolsStartup.prototype = { return Services.prefs.getBoolPref(pref); }); } catch (ex) { + let { console } = Cu.import("resource://gre/modules/Console.jsm", {}); console.error(ex); return false; } if (!remoteDebuggingEnabled) { let errorMsg = "Could not run chrome debugger! You need the following " + "prefs to be set to true: " + kDebuggerPrefs.join(", "); + let { console } = Cu.import("resource://gre/modules/Console.jsm", {}); console.error(new Error(errorMsg)); // Dump as well, as we're doing this from a commandline, make sure people // don't miss it: @@ -190,7 +192,10 @@ DevToolsStartup.prototype = { listener.open(); dump("Started debugger server on " + portOrPath + "\n"); } catch (e) { - dump("Unable to start debugger server on " + portOrPath + ": " + e); + let _error = "Unable to start debugger server on " + portOrPath + ": " + + e; + Cu.reportError(_error); + dump(_error + "\n"); } if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { @@ -199,12 +204,14 @@ DevToolsStartup.prototype = { }, /* eslint-disable max-len */ - helpInfo: " --jsconsole Open the Browser Console.\n" + - " --jsdebugger Open the Browser Toolbox.\n" + - " --devtools Open DevTools on initial load.\n" + - " --start-debugger-server [ws:][ <port> | <path> ] Start the debugger server on\n" + - " a TCP port or Unix domain socket path. Defaults to TCP port\n" + - " 6000. Use WebSocket protocol if ws: prefix is specified.\n", + helpInfo: " --browserconsole Open the Browser Console.\n" + + " --jsdebugger Open the Browser Toolbox.\n" + + " --devtools Open DevTools on initial load.\n" + + " --start-debugger-server [ws:][<port>|<path>] Start the debugger server on\n" + + " a TCP port or Unix domain socket path.\n" + + " Defaults to TCP port 6000.\n" + + " Use WebSocket protocol if ws: prefix\n" + + " is specified.\n", /* eslint-disable max-len */ classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"), diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 82d5d2915..926e30647 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -22,7 +22,7 @@ var {Task} = require("devtools/shared/task"); var {gDevTools} = require("devtools/client/framework/devtools"); var EventEmitter = require("devtools/shared/event-emitter"); var Telemetry = require("devtools/client/shared/telemetry"); -var HUDService = require("devtools/client/webconsole/hudservice"); +var { HUDService } = require("devtools/client/webconsole/hudservice"); var viewSource = require("devtools/client/shared/view-source"); var { attachThread, detachThread } = require("./attach-thread"); var Menu = require("devtools/client/framework/menu"); diff --git a/devtools/client/inspector/markup/test/browser_markup_events3.js b/devtools/client/inspector/markup/test/browser_markup_events3.js index a9dc2a499..a38e9a011 100644 --- a/devtools/client/inspector/markup/test/browser_markup_events3.js +++ b/devtools/client/inspector/markup/test/browser_markup_events3.js @@ -115,7 +115,7 @@ const TEST_DATA = [ // eslint-disable-line expected: [ { type: "click", - filename: TEST_URL + ":1", + filename: TEST_URL + ":0", attributes: [ "Bubbling", "DOM2" @@ -129,7 +129,7 @@ const TEST_DATA = [ // eslint-disable-line expected: [ { type: "click", - filename: TEST_URL + ":1", + filename: TEST_URL + ":0", attributes: [ "Bubbling", "DOM2" diff --git a/devtools/client/inspector/rules/models/rule.js b/devtools/client/inspector/rules/models/rule.js index 1a3fa057a..4c978cb58 100644 --- a/devtools/client/inspector/rules/models/rule.js +++ b/devtools/client/inspector/rules/models/rule.js @@ -140,11 +140,18 @@ Rule.prototype = { line, mediaText}) => { let mediaString = mediaText ? " @" + mediaText : ""; let linePart = line > 0 ? (":" + line) : ""; + let decodedHref = href; + + if (decodedHref) { + try { + decodedHref = decodeURIComponent(href); + } catch (e) {} + } let sourceStrings = { - full: (href || CssLogic.l10n("rule.sourceInline")) + linePart + + full: (decodedHref || CssLogic.l10n("rule.sourceInline")) + linePart + mediaString, - short: CssLogic.shortSource({href: href}) + linePart + mediaString + short: CssLogic.shortSource({href: decodedHref}) + linePart + mediaString }; return sourceStrings; diff --git a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js index 927deb8ce..6a4fd1d66 100644 --- a/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js +++ b/devtools/client/inspector/rules/test/browser_rules_style-editor-link.js @@ -10,10 +10,12 @@ thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source"); // Test the links from the rule-view to the styleeditor -const STYLESHEET_URL = "data:text/css," + encodeURIComponent( - ["#first {", - "color: blue", - "}"].join("\n")); +const STYLESHEET_DATA_URL_CONTENTS = ["#first {", + "color: blue", + "}"].join("\n"); +const STYLESHEET_DATA_URL = + `data:text/css,${encodeURIComponent(STYLESHEET_DATA_URL_CONTENTS)}`; +const STYLESHEET_DECODED_DATA_URL = `data:text/css,${STYLESHEET_DATA_URL_CONTENTS}`; const EXTERNAL_STYLESHEET_FILE_NAME = "doc_style_editor_link.css"; const EXTERNAL_STYLESHEET_URL = URL_ROOT + EXTERNAL_STYLESHEET_FILE_NAME; @@ -31,7 +33,7 @@ const DOCUMENT_URL = "data:text/html;charset=utf-8," + encodeURIComponent(` <style> div { font-weight: bold; } </style> - <link rel="stylesheet" type="text/css" href="${STYLESHEET_URL}"> + <link rel="stylesheet" type="text/css" href="${STYLESHEET_DATA_URL}"> <link rel="stylesheet" type="text/css" href="${EXTERNAL_STYLESHEET_URL}"> </head> <body> @@ -178,15 +180,28 @@ function* testDisabledStyleEditor(view, toolbox) { } function testRuleViewLinkLabel(view) { - let link = getRuleViewLinkByIndex(view, 2); + info("Checking the data URL link label"); + + let link = getRuleViewLinkByIndex(view, 1); let labelElem = link.querySelector(".ruleview-rule-source-label"); let value = labelElem.textContent; let tooltipText = labelElem.getAttribute("title"); - is(value, EXTERNAL_STYLESHEET_FILE_NAME + ":1", - "rule view stylesheet display value matches filename and line number"); - is(tooltipText, EXTERNAL_STYLESHEET_URL + ":1", - "rule view stylesheet tooltip text matches the full URI path"); + is(value, `${STYLESHEET_DATA_URL_CONTENTS}:1`, + "Rule view data URL stylesheet display value matches contents"); + is(tooltipText, `${STYLESHEET_DECODED_DATA_URL}:1`, + "Rule view data URL stylesheet tooltip text matches the full URI path"); + + info("Checking the external link label"); + link = getRuleViewLinkByIndex(view, 2); + labelElem = link.querySelector(".ruleview-rule-source-label"); + value = labelElem.textContent; + tooltipText = labelElem.getAttribute("title"); + + is(value, `${EXTERNAL_STYLESHEET_FILE_NAME}:1`, + "Rule view external stylesheet display value matches filename and line number"); + is(tooltipText, `${EXTERNAL_STYLESHEET_URL}:1`, + "Rule view external stylesheet tooltip text matches the full URI path"); } function testUnselectableRuleViewLink(view, index) { diff --git a/devtools/client/inspector/test/browser.ini b/devtools/client/inspector/test/browser.ini index 65ad71c0c..499df25f0 100644 --- a/devtools/client/inspector/test/browser.ini +++ b/devtools/client/inspector/test/browser.ini @@ -113,6 +113,7 @@ subsuite = clipboard [browser_inspector_infobar_01.js] [browser_inspector_infobar_02.js] [browser_inspector_infobar_03.js] +[browser_inspector_infobar_04.js] [browser_inspector_infobar_textnode.js] [browser_inspector_initialization.js] skip-if = (e10s && debug) # Bug 1250058 - Docshell leak on debug e10s diff --git a/devtools/client/inspector/test/browser_inspector_infobar_04.js b/devtools/client/inspector/test/browser_inspector_infobar_04.js new file mode 100644 index 000000000..f1b9eca49 --- /dev/null +++ b/devtools/client/inspector/test/browser_inspector_infobar_04.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/. */ + +"use strict"; + +// Check the position and text content of the highlighter nodeinfo bar under page zoom. + +const TEST_URI = URL_ROOT + "doc_inspector_infobar_01.html"; + +add_task(function* () { + let {inspector, testActor} = yield openInspectorForURL(TEST_URI); + let testData = { + selector: "#top", + dims: "500" + " \u00D7 " + "100" + }; + + yield testInfobar(testData, inspector, testActor); + info("Change zoom page to level 2."); + yield testActor.zoomPageTo(2); + info("Testing again the infobar after zoom."); + yield testInfobar(testData, inspector, testActor); +}); + +function* testInfobar(test, inspector, testActor) { + info(`Testing ${test.selector}`); + + yield selectAndHighlightNode(test.selector, inspector); + + // Ensure the node is the correct one. + let id = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-id"); + is(id, test.selector, `Node ${test.selector} selected.`); + + let dims = yield testActor.getHighlighterNodeTextContent( + "box-model-infobar-dimensions"); + is(dims, test.dims, "Node's infobar displays the right dimensions."); +} diff --git a/devtools/client/locales/en-US/inspector.properties b/devtools/client/locales/en-US/inspector.properties index b6f3e072b..252d72bfa 100644 --- a/devtools/client/locales/en-US/inspector.properties +++ b/devtools/client/locales/en-US/inspector.properties @@ -66,7 +66,7 @@ inspector.collapsePane=Collapse pane # inspector UI. inspector.expandPane=Expand pane -# LOCALIZATION NOTE (inspector.searchResultsCount): This is the label that +# LOCALIZATION NOTE (inspector.searchResultsCount2): This is the label that # will show up next to the inspector search box. %1$S is the current result # index and %2$S is the total number of search results. For example: "3 of 9". # This won't be visible until the search box is updated in Bug 835896. diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties index e6118ca9f..4926c234b 100644 --- a/devtools/client/locales/en-US/netmonitor.properties +++ b/devtools/client/locales/en-US/netmonitor.properties @@ -143,12 +143,17 @@ networkMenu.sortedDesc=Sorted descending # in the network table footer when there are no requests available. networkMenu.empty=No requests -# LOCALIZATION NOTE (networkMenu.summary): Semi-colon list of plural forms. +# LOCALIZATION NOTE (networkMenu.summary2): Semi-colon list of plural forms. # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals # This label is displayed in the network table footer providing concise # information about all requests. Parameters: #1 is the number of requests, -# #2 is the size, #3 is the number of seconds. -networkMenu.summary=One request, #2 KB, #3 s;#1 requests, #2 KB, #3 s +# #2 is the size, #3 is the transferred size, #4 is the number of seconds. +networkMenu.summary2=One request, #2 KB (transferred: #3 KB), #4 s;#1 requests, #2 KB (transferred: #3 KB), #4 s + +# LOCALIZATION NOTE (networkMenu.timeS): This is the label displayed +# in the network table footer providing concise information about all requests. +# Events DOMContentLoaded and load - specifying the number of seconds. +networkMenu.timeS=%S s # LOCALIZATION NOTE (networkMenu.sizeB): This is the label displayed # in the network menu specifying the size of a request (in bytes). @@ -221,10 +226,22 @@ tableChart.unavailable=No data available # in pie or table charts specifying the size of a request (in kilobytes). charts.sizeKB=%S KB +# LOCALIZATION NOTE (charts.transferredSizeKB): This is the label displayed +# in pie or table charts specifying the size of a transferred request (in kilobytes). +charts.transferredSizeKB=%S KB + # LOCALIZATION NOTE (charts.totalS): This is the label displayed # in pie or table charts specifying the time for a request to finish (in seconds). charts.totalS=%S s +# LOCALIZATION NOTE (charts.totalSize): This is the label displayed +# in the performance analysis view for total requests size, in kilobytes. +charts.totalSize=Size: %S KB + +# LOCALIZATION NOTE (charts.totalTranferredSize): This is the label displayed +# in the performance analysis view for total transferred size, in kilobytes. +charts.totalTransferredSize=Transferred Size: %S KB + # LOCALIZATION NOTE (charts.cacheEnabled): This is the label displayed # in the performance analysis view for "cache enabled" charts. charts.cacheEnabled=Primed cache @@ -233,10 +250,6 @@ charts.cacheEnabled=Primed cache # in the performance analysis view for "cache disabled" charts. charts.cacheDisabled=Empty cache -# LOCALIZATION NOTE (charts.totalSize): This is the label displayed -# in the performance analysis view for total requests size, in kilobytes. -charts.totalSize=Size: %S KB - # LOCALIZATION NOTE (charts.totalSeconds): Semi-colon list of plural forms. # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals # This is the label displayed in the performance analysis view for the @@ -251,6 +264,23 @@ charts.totalCached=Cached responses: %S # in the performance analysis view for total requests. charts.totalCount=Total requests: %S +# LOCALIZATION NOTE (charts.size): This is the label displayed +# in the header column in the performance analysis view for size of the request. +charts.size=Size + +# LOCALIZATION NOTE (charts.type): This is the label displayed +# in the header column in the performance analysis view for type of request. +charts.type=Type + +# LOCALIZATION NOTE (charts.transferred): This is the label displayed +# in the header column in the performance analysis view for transferred +# size of the request. +charts.transferred=Transferred + +# LOCALIZATION NOTE (charts.time): This is the label displayed +# in the header column in the performance analysis view for time of request. +charts.time=Time + # LOCALIZATION NOTE (netRequest.headers): A label used for Headers tab # This tab displays list of HTTP headers netRequest.headers=Headers @@ -556,6 +586,11 @@ netmonitor.timings.blocked=Blocked: # in a "dns" state. netmonitor.timings.dns=DNS resolution: +# LOCALIZATION NOTE (netmonitor.timings.ssl): This is the label displayed +# in the network details timings tab identifying the amount of time spent +# in a "tls" handshake state. +netmonitor.timings.ssl=TLS setup: + # LOCALIZATION NOTE (netmonitor.timings.connect): This is the label displayed # in the network details timings tab identifying the amount of time spent # in a "connect" state. @@ -592,6 +627,37 @@ netmonitor.security.protocolVersion=Protocol version: # in the security tab describing the cipher suite used to secure this connection. netmonitor.security.cipherSuite=Cipher suite: +# LOCALIZATION NOTE (netmonitor.security.keaGroup): This is the label displayed +# in the security tab describing the key exchange group suite used to secure +# this connection. +netmonitor.security.keaGroup=Key Exchange Group: + +# LOCALIZATION NOTE (netmonitor.security.keaGroup.none): This is the label +# displayed in the security tab describing the case when no group was used. +netmonitor.security.keaGroup.none=none + +# LOCALIZATION NOTE (netmonitor.security.keaGroup.custom): This is the label +# displayed in the security tab describing the case when a custom group was used. +netmonitor.security.keaGroup.custom=custom + +# LOCALIZATION NOTE (netmonitor.security.keaGroup.unknown): This is the value +# displayed in the security tab describing an unknown group. +netmonitor.security.keaGroup.unknown=unknown group + +# LOCALIZATION NOTE (netmonitor.security.signatureScheme): This is the label +# displayed in the security tab describing the signature scheme used by for +# the server certificate in this connection. +netmonitor.security.signatureScheme=Signature Scheme: + +# LOCALIZATION NOTE (netmonitor.security.signatureScheme.none): This is the +# label displayed in the security tab describing the case when no signature +# was used. +netmonitor.security.signatureScheme.none=none + +# LOCALIZATION NOTE (netmonitor.security.signatureScheme.unknown): This is the +# value displayed in the security tab describing an unknown signature scheme. +netmonitor.security.signatureScheme.unknown=unknown signature scheme + # LOCALIZATION NOTE (netmonitor.security.hsts): This is the label displayed # in the security tab describing the usage of HTTP Strict Transport Security. netmonitor.security.hsts=HTTP Strict Transport Security: diff --git a/devtools/client/locales/en-US/storage.dtd b/devtools/client/locales/en-US/storage.dtd index 211c79436..adfc03183 100644 --- a/devtools/client/locales/en-US/storage.dtd +++ b/devtools/client/locales/en-US/storage.dtd @@ -9,3 +9,6 @@ <!-- LOCALIZATION NOTE : Label of popup menu action to delete all storage items. --> <!ENTITY storage.popupMenu.deleteAllLabel "Delete All"> + +<!-- LOCALIZATION NOTE : Label of popup menu action to delete all session cookies. --> +<!ENTITY storage.popupMenu.deleteAllSessionCookiesLabel "Delete All Session Cookies"> diff --git a/devtools/client/locales/en-US/storage.properties b/devtools/client/locales/en-US/storage.properties index 1eeb88ff9..4719520bd 100644 --- a/devtools/client/locales/en-US/storage.properties +++ b/devtools/client/locales/en-US/storage.properties @@ -35,6 +35,7 @@ tree.labels.Cache=Cache Storage # LOCALIZATION NOTE (table.headers.*.*): # These strings are the header names of the columns in the Storage Table for # each type of storage available through the Storage Tree to the side. +table.headers.cookies.uniqueKey=Unique key table.headers.cookies.name=Name table.headers.cookies.path=Path table.headers.cookies.host=Domain @@ -52,14 +53,16 @@ table.headers.sessionStorage.value=Value table.headers.Cache.url=URL table.headers.Cache.status=Status +table.headers.indexedDB.uniqueKey=Unique key table.headers.indexedDB.name=Key table.headers.indexedDB.db=Database Name +table.headers.indexedDB.storage=Storage table.headers.indexedDB.objectStore=Object Store Name table.headers.indexedDB.value=Value table.headers.indexedDB.origin=Origin table.headers.indexedDB.version=Version table.headers.indexedDB.objectStores=Object Stores -table.headers.indexedDB.keyPath=Key +table.headers.indexedDB.keyPath2=Key Path table.headers.indexedDB.autoIncrement=Auto Increment table.headers.indexedDB.indexes=Indexes @@ -84,7 +87,11 @@ storage.parsedValue.label=Parsed Value # Label of popup menu action to delete storage item. storage.popupMenu.deleteLabel=Delete “%S” -# LOCALIZATION NOTE (storage.popupMenu.deleteAllLabel): +# LOCALIZATION NOTE (storage.popupMenu.addItemLabel): +# Label of popup menu action to add an item. +storage.popupMenu.addItemLabel=Add Item + +# LOCALIZATION NOTE (storage.popupMenu.deleteAllFromLabel): # Label of popup menu action to delete all storage items. storage.popupMenu.deleteAllFromLabel=Delete All From “%S” diff --git a/devtools/client/menus.js b/devtools/client/menus.js index 1aee85095..7e36839da 100644 --- a/devtools/client/menus.js +++ b/devtools/client/menus.js @@ -120,7 +120,7 @@ exports.menuitems = [ { id: "menu_browserConsole", l10nKey: "browserConsoleCmd", oncommand() { - let HUDService = require("devtools/client/webconsole/hudservice"); + let {HUDService} = require("devtools/client/webconsole/hudservice"); HUDService.openBrowserConsoleOrFocus(); }, key: { diff --git a/devtools/client/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js index 3f7b0bd2f..de8f4ae1d 100644 --- a/devtools/client/netmonitor/actions/index.js +++ b/devtools/client/netmonitor/actions/index.js @@ -4,6 +4,8 @@ "use strict"; const filters = require("./filters"); -const sidebar = require("./sidebar"); +const requests = require("./requests"); +const timingMarkers = require("./timing-markers"); +const ui = require("./ui"); -module.exports = Object.assign({}, filters, sidebar); +module.exports = Object.assign({}, filters, requests, timingMarkers, ui); diff --git a/devtools/client/netmonitor/actions/moz.build b/devtools/client/netmonitor/actions/moz.build index 477cafb41..ce904cae8 100644 --- a/devtools/client/netmonitor/actions/moz.build +++ b/devtools/client/netmonitor/actions/moz.build @@ -6,5 +6,7 @@ DevToolsModules( 'filters.js', 'index.js', - 'sidebar.js', + 'requests.js', + 'timing-markers.js', + 'ui.js', ) diff --git a/devtools/client/netmonitor/actions/requests.js b/devtools/client/netmonitor/actions/requests.js new file mode 100644 index 000000000..ae794a437 --- /dev/null +++ b/devtools/client/netmonitor/actions/requests.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/. */ + +"use strict"; + +const { + UPDATE_REQUESTS, +} = require("../constants"); + +/** + * Update request items + * + * @param {array} requests - visible request items + */ +function updateRequests(items) { + return { + type: UPDATE_REQUESTS, + items, + }; +} + +module.exports = { + updateRequests, +}; diff --git a/devtools/client/netmonitor/actions/sidebar.js b/devtools/client/netmonitor/actions/sidebar.js deleted file mode 100644 index 7e8dca5c1..000000000 --- a/devtools/client/netmonitor/actions/sidebar.js +++ /dev/null @@ -1,49 +0,0 @@ -/* 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/. */ -"use strict"; - -const { - DISABLE_TOGGLE_BUTTON, - SHOW_SIDEBAR, - TOGGLE_SIDEBAR, -} = require("../constants"); - -/** - * Change ToggleButton disabled state. - * - * @param {boolean} disabled - expected button disabled state - */ -function disableToggleButton(disabled) { - return { - type: DISABLE_TOGGLE_BUTTON, - disabled: disabled, - }; -} - -/** - * Change sidebar visible state. - * - * @param {boolean} visible - expected sidebar visible state - */ -function showSidebar(visible) { - return { - type: SHOW_SIDEBAR, - visible: visible, - }; -} - -/** - * Toggle to show/hide sidebar. - */ -function toggleSidebar() { - return { - type: TOGGLE_SIDEBAR, - }; -} - -module.exports = { - disableToggleButton, - showSidebar, - toggleSidebar, -}; diff --git a/devtools/client/netmonitor/actions/timing-markers.js b/devtools/client/netmonitor/actions/timing-markers.js new file mode 100644 index 000000000..4f1363a70 --- /dev/null +++ b/devtools/client/netmonitor/actions/timing-markers.js @@ -0,0 +1,19 @@ +/* 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/. */ +"use strict"; + +const { ADD_TIMING_MARKER, CLEAR_TIMING_MARKERS } = require("../constants"); + +exports.addTimingMarker = (marker) => { + return { + type: ADD_TIMING_MARKER, + marker + }; +}; + +exports.clearTimingMarkers = () => { + return { + type: CLEAR_TIMING_MARKERS + }; +}; diff --git a/devtools/client/netmonitor/actions/ui.js b/devtools/client/netmonitor/actions/ui.js new file mode 100644 index 000000000..c6df307ed --- /dev/null +++ b/devtools/client/netmonitor/actions/ui.js @@ -0,0 +1,36 @@ ++/* 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/. */
+
+"use strict";
+
+const {
+ OPEN_SIDEBAR,
+ TOGGLE_SIDEBAR,
+} = require("../constants");
+
+/**
+ * Change sidebar open state.
+ *
+ * @param {boolean} open - open state
+ */
+function openSidebar(open) {
+ return {
+ type: OPEN_SIDEBAR,
+ open,
+ };
+}
+
+/**
+ * Toggle sidebar open state.
+ */
+function toggleSidebar() {
+ return {
+ type: TOGGLE_SIDEBAR,
+ };
+}
+
+module.exports = {
+ openSidebar,
+ toggleSidebar,
+};
diff --git a/devtools/client/netmonitor/components/moz.build b/devtools/client/netmonitor/components/moz.build index 47ef7f026..42459584d 100644 --- a/devtools/client/netmonitor/components/moz.build +++ b/devtools/client/netmonitor/components/moz.build @@ -6,5 +6,6 @@ DevToolsModules( 'filter-buttons.js', 'search-box.js', + 'summary-button.js', 'toggle-button.js', ) diff --git a/devtools/client/netmonitor/components/summary-button.js b/devtools/client/netmonitor/components/summary-button.js new file mode 100644 index 000000000..00595e5e6 --- /dev/null +++ b/devtools/client/netmonitor/components/summary-button.js @@ -0,0 +1,79 @@ +/* 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/. */ + +/* globals NetMonitorView */ + +"use strict"; + +const { + CONTENT_SIZE_DECIMALS, + REQUEST_TIME_DECIMALS, +} = require("../constants"); +const { DOM, PropTypes } = require("devtools/client/shared/vendor/react"); +const { connect } = require("devtools/client/shared/vendor/react-redux"); +const { PluralForm } = require("devtools/shared/plural-form"); +const { L10N } = require("../l10n"); +const { + getDisplayedRequestsSummary, + getDisplayedTimingMarker +} = require("../selectors/index"); + +const { button, span } = DOM; + +function SummaryButton({ + summary, + triggerSummary, + timingMarkers +}) { + let { count, contentSize, transferredSize, millis } = summary; + let { + DOMContentLoaded, + load, + } = timingMarkers; + const text = (count === 0) ? L10N.getStr("networkMenu.empty") : + PluralForm.get(count, L10N.getStr("networkMenu.summary2")) + .replace("#1", count) + .replace("#2", L10N.numberWithDecimals(contentSize / 1024, + CONTENT_SIZE_DECIMALS)) + .replace("#3", L10N.numberWithDecimals(transferredSize / 1024, + CONTENT_SIZE_DECIMALS)) + .replace("#4", L10N.numberWithDecimals(millis / 1000, + REQUEST_TIME_DECIMALS)) + + ((DOMContentLoaded > -1) + ? ", " + "DOMContentLoaded: " + L10N.getFormatStrWithNumbers("networkMenu.timeS", L10N.numberWithDecimals(DOMContentLoaded / 1000, REQUEST_TIME_DECIMALS)) + : "") + + ((load > -1) + ? ", " + "load: " + L10N.getFormatStrWithNumbers("networkMenu.timeS", L10N.numberWithDecimals(load / 1000, REQUEST_TIME_DECIMALS)) + : ""); + + return button({ + id: "requests-menu-network-summary-button", + className: "devtools-button", + title: count ? text : L10N.getStr("netmonitor.toolbar.perf"), + onClick: triggerSummary, + }, + span({ className: "summary-info-icon" }), + span({ className: "summary-info-text" }, text)); +} + +SummaryButton.propTypes = { + summary: PropTypes.object.isRequired, + timingMarkers: PropTypes.object.isRequired, +}; + +module.exports = connect( + (state) => ({ + summary: getDisplayedRequestsSummary(state), + timingMarkers: { + DOMContentLoaded: + getDisplayedTimingMarker(state, "firstDocumentDOMContentLoadedTimestamp"), + load: getDisplayedTimingMarker(state, "firstDocumentLoadTimestamp"), + }, + }), + (dispatch) => ({ + triggerSummary: () => { + NetMonitorView.toggleFrontendMode(); + }, + }) +)(SummaryButton); diff --git a/devtools/client/netmonitor/components/toggle-button.js b/devtools/client/netmonitor/components/toggle-button.js index db546c55d..c9a59a76b 100644 --- a/devtools/client/netmonitor/components/toggle-button.js +++ b/devtools/client/netmonitor/components/toggle-button.js @@ -1,5 +1,3 @@ -/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ -/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* 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/. */ @@ -11,47 +9,43 @@ const { connect } = require("devtools/client/shared/vendor/react-redux"); const { L10N } = require("../l10n"); const Actions = require("../actions/index"); -// Shortcuts const { button } = DOM; -/** - * Button used to toggle sidebar - */ function ToggleButton({ disabled, - onToggle, - visible, + open, + triggerSidebar, }) { let className = ["devtools-button"]; - if (!visible) { + if (!open) { className.push("pane-collapsed"); } - let titleMsg = visible ? L10N.getStr("collapseDetailsPane") : - L10N.getStr("expandDetailsPane"); + + const title = open ? L10N.getStr("collapseDetailsPane") : + L10N.getStr("expandDetailsPane"); return button({ id: "details-pane-toggle", className: className.join(" "), - title: titleMsg, - disabled: disabled, + title, + disabled, tabIndex: "0", - onMouseDown: onToggle, + onMouseDown: triggerSidebar, }); } ToggleButton.propTypes = { disabled: PropTypes.bool.isRequired, - onToggle: PropTypes.func.isRequired, - visible: PropTypes.bool.isRequired, + triggerSidebar: PropTypes.func.isRequired, }; module.exports = connect( (state) => ({ - disabled: state.sidebar.toggleButtonDisabled, - visible: state.sidebar.visible, + disabled: state.requests.items.length === 0, + open: state.ui.sidebar.open, }), (dispatch) => ({ - onToggle: () => { + triggerSidebar: () => { dispatch(Actions.toggleSidebar()); let requestsMenu = NetMonitorView.RequestsMenu; diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js index a540d74b2..1605496a5 100644 --- a/devtools/client/netmonitor/constants.js +++ b/devtools/client/netmonitor/constants.js @@ -5,15 +5,19 @@ const general = { FREETEXT_FILTER_SEARCH_DELAY: 200, + CONTENT_SIZE_DECIMALS: 2, + REQUEST_TIME_DECIMALS: 2, }; const actionTypes = { + ADD_TIMING_MARKER: "ADD_TIMING_MARKER", + CLEAR_TIMING_MARKERS: "CLEAR_TIMING_MARKERS", TOGGLE_FILTER_TYPE: "TOGGLE_FILTER_TYPE", ENABLE_FILTER_TYPE_ONLY: "ENABLE_FILTER_TYPE_ONLY", - TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR", - SHOW_SIDEBAR: "SHOW_SIDEBAR", - DISABLE_TOGGLE_BUTTON: "DISABLE_TOGGLE_BUTTON", SET_FILTER_TEXT: "SET_FILTER_TEXT", + OPEN_SIDEBAR: "OPEN_SIDEBAR", + TOGGLE_SIDEBAR: "TOGGLE_SIDEBAR", + UPDATE_REQUESTS: "UPDATE_REQUESTS", }; module.exports = Object.assign({}, general, actionTypes); diff --git a/devtools/client/netmonitor/moz.build b/devtools/client/netmonitor/moz.build index 4b34b093b..2c9b32d3f 100644 --- a/devtools/client/netmonitor/moz.build +++ b/devtools/client/netmonitor/moz.build @@ -17,6 +17,7 @@ DevToolsModules( 'events.js', 'filter-predicates.js', 'l10n.js', + 'open-request-in-tab.js', 'panel.js', 'performance-statistics-view.js', 'prefs.js', diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 739e174fb..39bee8570 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -407,6 +407,9 @@ TargetEventsHandler.prototype = { if (!Services.prefs.getBoolPref("devtools.webconsole.persistlog")) { NetMonitorView.RequestsMenu.reset(); NetMonitorView.Sidebar.toggle(false); + } else { + // If the log is persistent, just clear some informations. + NetMonitorView.RequestsMenu.resetNotPersistent(); } // Switch to the default network traffic inspector view. if (NetMonitorController.getCurrentActivity() == ACTIVITY_TYPE.NONE) { @@ -414,6 +417,7 @@ TargetEventsHandler.prototype = { } // Clear any accumulated markers. NetMonitorController.NetworkEventsHandler.clearMarkers(); + gStore.dispatch(Actions.clearTimingMarkers()); window.emit(EVENTS.TARGET_WILL_NAVIGATE); break; @@ -534,6 +538,7 @@ NetworkEventsHandler.prototype = { _onDocLoadingMarker: function (marker) { window.emit(EVENTS.TIMELINE_EVENT, marker); this._markers.push(marker); + gStore.dispatch(Actions.addTimingMarker(marker)); }, /** diff --git a/devtools/client/netmonitor/netmonitor-view.js b/devtools/client/netmonitor/netmonitor-view.js index 68470f7a9..414b9ab8f 100644 --- a/devtools/client/netmonitor/netmonitor-view.js +++ b/devtools/client/netmonitor/netmonitor-view.js @@ -125,7 +125,6 @@ var NetMonitorView = { if (!Prefs.statistics) { $("#request-menu-context-perf").hidden = true; $("#notice-perf-message").hidden = true; - $("#requests-menu-network-summary-button").hidden = true; } }, @@ -171,10 +170,10 @@ var NetMonitorView = { if (flags.visible) { this._body.classList.remove("pane-collapsed"); - gStore.dispatch(Actions.showSidebar(true)); + gStore.dispatch(Actions.openSidebar(true)); } else { this._body.classList.add("pane-collapsed"); - gStore.dispatch(Actions.showSidebar(false)); + gStore.dispatch(Actions.openSidebar(false)); } if (tabIndex !== undefined) { @@ -234,7 +233,7 @@ var NetMonitorView = { // populating the statistics view. // • The response mime type is used for categorization. yield whenDataAvailable(requestsView, [ - "responseHeaders", "status", "contentSize", "mimeType", "totalTime" + "responseHeaders", "status", "contentSize", "transferredSize", "mimeType", "totalTime" ]); } catch (ex) { // Timed out while waiting for data. Continue with what we have. @@ -964,7 +963,7 @@ NetworkDetailsView.prototype = { if (!response) { return; } - let { blocked, dns, connect, send, wait, receive } = response.timings; + let { blocked, dns, connect, ssl, send, wait, receive } = response.timings; let tabboxWidth = $("#details-pane").getAttribute("width"); @@ -989,6 +988,11 @@ NetworkDetailsView.prototype = { $("#timings-summary-connect .requests-menu-timings-total") .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", connect)); + $("#timings-summary-ssl .requests-menu-timings-box") + .setAttribute("width", ssl * scale); + $("#timings-summary-ssl .requests-menu-timings-total") + .setAttribute("value", L10N.getFormatStr("networkMenu.totalMS", ssl)); + $("#timings-summary-send .requests-menu-timings-box") .setAttribute("width", send * scale); $("#timings-summary-send .requests-menu-timings-total") @@ -1008,6 +1012,8 @@ NetworkDetailsView.prototype = { .style.transform = "translateX(" + (scale * blocked) + "px)"; $("#timings-summary-connect .requests-menu-timings-box") .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; + $("#timings-summary-ssl .requests-menu-timings-box") + .style.transform = "translateX(" + (scale * blocked) + "px)"; $("#timings-summary-send .requests-menu-timings-box") .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; @@ -1023,6 +1029,8 @@ NetworkDetailsView.prototype = { .style.transform = "translateX(" + (scale * blocked) + "px)"; $("#timings-summary-connect .requests-menu-timings-total") .style.transform = "translateX(" + (scale * (blocked + dns)) + "px)"; + $("#timings-summary-ssl .requests-menu-timings-total") + .style.transform = "translateX(" + (scale * blocked) + "px)"; $("#timings-summary-send .requests-menu-timings-total") .style.transform = "translateX(" + (scale * (blocked + dns + connect)) + "px)"; @@ -1125,6 +1133,8 @@ NetworkDetailsView.prototype = { setValue("#security-protocol-version-value", securityInfo.protocolVersion); setValue("#security-ciphersuite-value", securityInfo.cipherSuite); + setValue("#security-keagroup-value", securityInfo.keaGroupName); + setValue("#security-signaturescheme-value", securityInfo.signatureSchemeName); // Host header let domain = getUriHostPort(url); diff --git a/devtools/client/netmonitor/netmonitor.xul b/devtools/client/netmonitor/netmonitor.xul index bb580f7ad..aa5c4d848 100644 --- a/devtools/client/netmonitor/netmonitor.xul +++ b/devtools/client/netmonitor/netmonitor.xul @@ -28,9 +28,8 @@ id="react-filter-buttons-hook"/> <spacer id="requests-menu-spacer" flex="1"/> - <toolbarbutton id="requests-menu-network-summary-button" - class="devtools-toolbarbutton icon-and-text" - data-localization="tooltiptext=netmonitor.toolbar.perf"/> + <html:div xmlns="http://www.w3.org/1999/xhtml" + id="react-summary-button-hook"/> <html:div xmlns="http://www.w3.org/1999/xhtml" id="react-search-box-hook"/> <html:div xmlns="http://www.w3.org/1999/xhtml" @@ -479,6 +478,14 @@ <hbox class="requests-menu-timings-box connect"/> <label class="plain requests-menu-timings-total"/> </hbox> + <hbox id="timings-summary-ssl" + class="tabpanel-summary-container" + align="center"> + <label class="plain tabpanel-summary-label" + data-localization="content=netmonitor.timings.ssl"/> + <hbox class="requests-menu-timings-box ssl"/> + <label class="plain requests-menu-timings-total"/> + </hbox> <hbox id="timings-summary-send" class="tabpanel-summary-container" align="center"> @@ -551,6 +558,26 @@ id="security-warning-cipher" data-localization="tooltiptext=netmonitor.security.warning.cipher" /> </hbox> + <hbox id="security-keagroup" + class="tabpanel-summary-container" + align="baseline"> + <label class="plain tabpanel-summary-label" + data-localization="content=netmonitor.security.keaGroup"/> + <textbox id="security-keagroup-value" + class="plain tabpanel-summary-value devtools-monospace cropped-textbox" + flex="1" + readonly="true"/> + </hbox> + <hbox id="security-signaturescheme" + class="tabpanel-summary-container" + align="baseline"> + <label class="plain tabpanel-summary-label" + data-localization="content=netmonitor.security.signatureScheme"/> + <textbox id="security-signaturescheme-value" + class="plain tabpanel-summary-value devtools-monospace cropped-textbox" + flex="1" + readonly="true"/> + </hbox> </vbox> </vbox> <vbox id="security-info-domain" diff --git a/devtools/client/netmonitor/open-request-in-tab.js b/devtools/client/netmonitor/open-request-in-tab.js new file mode 100644 index 000000000..aeb35dad0 --- /dev/null +++ b/devtools/client/netmonitor/open-request-in-tab.js @@ -0,0 +1,40 @@ +/* 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/. */
+/* eslint-disable mozilla/reject-some-requires */
+
+"use strict";
+
+let { Cc, Ci } = require("chrome");
+const Services = require("Services");
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+/**
+ * Opens given request in a new tab.
+ */
+function openRequestInTab(request) {
+ let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ let rawData = request.requestPostData ? request.requestPostData.postData : null;
+ let postData;
+
+ if (rawData && rawData.text) {
+ let stringStream = getInputStreamFromString(rawData.text);
+ postData = Cc["@mozilla.org/network/mime-input-stream;1"]
+ .createInstance(Ci.nsIMIMEInputStream);
+ postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
+ postData.setData(stringStream);
+ }
+
+ win.gBrowser.selectedTab = win.gBrowser.addTab(request.url, null, null, postData);
+}
+
+function getInputStreamFromString(data) {
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Ci.nsIStringInputStream);
+ stringStream.data = data;
+ return stringStream;
+}
+
+module.exports = {
+ openRequestInTab,
+};
diff --git a/devtools/client/netmonitor/performance-statistics-view.js b/devtools/client/netmonitor/performance-statistics-view.js index c712c083d..38b98fb68 100644 --- a/devtools/client/netmonitor/performance-statistics-view.js +++ b/devtools/client/netmonitor/performance-statistics-view.js @@ -92,27 +92,35 @@ PerformanceStatisticsView.prototype = { let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS); return L10N.getFormatStr("charts.sizeKB", string); }, + transferredSize: value => { + let string = L10N.numberWithDecimals(value / 1024, CONTENT_SIZE_DECIMALS); + return L10N.getFormatStr("charts.transferredSizeKB", string); + }, time: value => { let string = L10N.numberWithDecimals(value / 1000, REQUEST_TIME_DECIMALS); return L10N.getFormatStr("charts.totalS", string); } }, _commonChartTotals: { + cached: total => { + return L10N.getFormatStr("charts.totalCached", total); + }, + count: total => { + return L10N.getFormatStr("charts.totalCount", total); + }, size: total => { let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS); return L10N.getFormatStr("charts.totalSize", string); }, + transferredSize: total => { + let string = L10N.numberWithDecimals(total / 1024, CONTENT_SIZE_DECIMALS); + return L10N.getFormatStr("charts.totalTransferredSize", string); + }, time: total => { let seconds = total / 1000; let string = L10N.numberWithDecimals(seconds, REQUEST_TIME_DECIMALS); return PluralForm.get(seconds, L10N.getStr("charts.totalSeconds")).replace("#1", string); - }, - cached: total => { - return L10N.getFormatStr("charts.totalCached", total); - }, - count: total => { - return L10N.getFormatStr("charts.totalCount", total); } }, @@ -136,6 +144,14 @@ PerformanceStatisticsView.prototype = { let chart = Chart.PieTable(document, { diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER, title: L10N.getStr(title), + header: { + cached: "", + count: "", + label: L10N.getStr("charts.type"), + size: L10N.getStr("charts.size"), + transferredSize: L10N.getStr("charts.transferred"), + time: L10N.getStr("charts.time") + }, data: data, strings: strings, totals: totals, @@ -161,13 +177,14 @@ PerformanceStatisticsView.prototype = { * True if the cache is considered enabled, false for disabled. */ _sanitizeChartDataSource: function (items, emptyCache) { - let data = [ + const data = [ "html", "css", "js", "xhr", "fonts", "images", "media", "flash", "ws", "other" - ].map(e => ({ + ].map((type) => ({ cached: 0, count: 0, - label: e, + label: type, size: 0, + transferredSize: 0, time: 0 })); @@ -211,6 +228,7 @@ PerformanceStatisticsView.prototype = { if (emptyCache || !responseIsFresh(details)) { data[type].time += details.totalTime || 0; data[type].size += details.contentSize || 0; + data[type].transferredSize += details.transferredSize || 0; } else { data[type].cached++; } diff --git a/devtools/client/netmonitor/reducers/index.js b/devtools/client/netmonitor/reducers/index.js index 58638a030..f003c7805 100644 --- a/devtools/client/netmonitor/reducers/index.js +++ b/devtools/client/netmonitor/reducers/index.js @@ -5,9 +5,13 @@ const { combineReducers } = require("devtools/client/shared/vendor/redux"); const filters = require("./filters"); -const sidebar = require("./sidebar"); +const requests = require("./requests"); +const timingMarkers = require("./timing-markers"); +const ui = require("./ui"); module.exports = combineReducers({ filters, - sidebar, + requests, + timingMarkers, + ui, }); diff --git a/devtools/client/netmonitor/reducers/moz.build b/devtools/client/netmonitor/reducers/moz.build index 477cafb41..ce904cae8 100644 --- a/devtools/client/netmonitor/reducers/moz.build +++ b/devtools/client/netmonitor/reducers/moz.build @@ -6,5 +6,7 @@ DevToolsModules( 'filters.js', 'index.js', - 'sidebar.js', + 'requests.js', + 'timing-markers.js', + 'ui.js', ) diff --git a/devtools/client/netmonitor/reducers/requests.js b/devtools/client/netmonitor/reducers/requests.js new file mode 100644 index 000000000..9ba888cad --- /dev/null +++ b/devtools/client/netmonitor/reducers/requests.js @@ -0,0 +1,28 @@ +/* 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/. */ +"use strict"; + +const I = require("devtools/client/shared/vendor/immutable"); +const { + UPDATE_REQUESTS, +} = require("../constants"); + +const Requests = I.Record({ + items: [], +}); + +function updateRequests(state, action) { + return state.set("items", action.items || state.items); +} + +function requests(state = new Requests(), action) { + switch (action.type) { + case UPDATE_REQUESTS: + return updateRequests(state, action); + default: + return state; + } +} + +module.exports = requests; diff --git a/devtools/client/netmonitor/reducers/sidebar.js b/devtools/client/netmonitor/reducers/sidebar.js deleted file mode 100644 index eaa8b63df..000000000 --- a/devtools/client/netmonitor/reducers/sidebar.js +++ /dev/null @@ -1,43 +0,0 @@ -/* 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/. */ -"use strict"; - -const I = require("devtools/client/shared/vendor/immutable"); -const { - DISABLE_TOGGLE_BUTTON, - SHOW_SIDEBAR, - TOGGLE_SIDEBAR, -} = require("../constants"); - -const SidebarState = I.Record({ - toggleButtonDisabled: true, - visible: false, -}); - -function disableToggleButton(state, action) { - return state.set("toggleButtonDisabled", action.disabled); -} - -function showSidebar(state, action) { - return state.set("visible", action.visible); -} - -function toggleSidebar(state, action) { - return state.set("visible", !state.visible); -} - -function sidebar(state = new SidebarState(), action) { - switch (action.type) { - case DISABLE_TOGGLE_BUTTON: - return disableToggleButton(state, action); - case SHOW_SIDEBAR: - return showSidebar(state, action); - case TOGGLE_SIDEBAR: - return toggleSidebar(state, action); - default: - return state; - } -} - -module.exports = sidebar; diff --git a/devtools/client/netmonitor/reducers/timing-markers.js b/devtools/client/netmonitor/reducers/timing-markers.js new file mode 100644 index 000000000..cb500b2c4 --- /dev/null +++ b/devtools/client/netmonitor/reducers/timing-markers.js @@ -0,0 +1,52 @@ +/* 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/. */ + +"use strict"; + +const I = require("devtools/client/shared/vendor/immutable"); +const { ADD_TIMING_MARKER, + CLEAR_TIMING_MARKERS } = require("../constants"); + +const TimingMarkers = I.Record({ + firstDocumentDOMContentLoadedTimestamp: -1, + firstDocumentLoadTimestamp: -1, +}); + +function addTimingMarker(state, action) { + if (action.marker.name == "document::DOMContentLoaded" && + state.firstDocumentDOMContentLoadedTimestamp == -1) { + return state.set("firstDocumentDOMContentLoadedTimestamp", + action.marker.unixTime / 1000); + } + + if (action.marker.name == "document::Load" && + state.firstDocumentLoadTimestamp == -1) { + return state.set("firstDocumentLoadTimestamp", + action.marker.unixTime / 1000); + } + + return state; +} + +function clearTimingMarkers(state) { + return state.withMutations(st => { + st.remove("firstDocumentDOMContentLoadedTimestamp"); + st.remove("firstDocumentLoadTimestamp"); + }); +} + +function timingMarkers(state = new TimingMarkers(), action) { + switch (action.type) { + case ADD_TIMING_MARKER: + return addTimingMarker(state, action); + + case CLEAR_TIMING_MARKERS: + return clearTimingMarkers(state); + + default: + return state; + } +} + +module.exports = timingMarkers; diff --git a/devtools/client/netmonitor/reducers/ui.js b/devtools/client/netmonitor/reducers/ui.js new file mode 100644 index 000000000..033b944f3 --- /dev/null +++ b/devtools/client/netmonitor/reducers/ui.js @@ -0,0 +1,40 @@ +/* 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/. */ + +"use strict"; + +const I = require("devtools/client/shared/vendor/immutable"); +const { + OPEN_SIDEBAR, + TOGGLE_SIDEBAR, +} = require("../constants"); + +const Sidebar = I.Record({ + open: false, +}); + +const UI = I.Record({ + sidebar: new Sidebar(), +}); + +function openSidebar(state, action) { + return state.setIn(["sidebar", "open"], action.open); +} + +function toggleSidebar(state, action) { + return state.setIn(["sidebar", "open"], !state.sidebar.open); +} + +function ui(state = new UI(), action) { + switch (action.type) { + case OPEN_SIDEBAR: + return openSidebar(state, action); + case TOGGLE_SIDEBAR: + return toggleSidebar(state, action); + default: + return state; + } +} + +module.exports = ui; diff --git a/devtools/client/netmonitor/request-list-context-menu.js b/devtools/client/netmonitor/request-list-context-menu.js index 215296265..331a7bde3 100644 --- a/devtools/client/netmonitor/request-list-context-menu.js +++ b/devtools/client/netmonitor/request-list-context-menu.js @@ -10,6 +10,7 @@ const Services = require("Services"); const { Task } = require("devtools/shared/task"); const { Curl } = require("devtools/client/shared/curl"); const { gDevTools } = require("devtools/client/framework/devtools"); +const { openRequestInTab } = require("devtools/client/netmonitor/open-request-in-tab"); const Menu = require("devtools/client/framework/menu"); const MenuItem = require("devtools/client/framework/menu-item"); const { L10N } = require("./l10n"); @@ -186,8 +187,7 @@ RequestListContextMenu.prototype = { */ openRequestInTab() { let win = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); - let { url } = this.selectedItem.attachment; - win.openUILinkIn(url, "tab", { relatedToCurrent: true }); + openRequestInTab(this.selectedItem.attachment); }, /** diff --git a/devtools/client/netmonitor/request-utils.js b/devtools/client/netmonitor/request-utils.js index ba54efb4f..647d71e7c 100644 --- a/devtools/client/netmonitor/request-utils.js +++ b/devtools/client/netmonitor/request-utils.js @@ -92,7 +92,7 @@ exports.getFormDataSections = Task.async(function* (headers, uploadHeaders, post exports.formDataURI = function (mimeType, encoding, text) { if (!encoding) { encoding = "base64"; - text = btoa(text); + text = btoa(unescape(encodeURIComponent(text))); } return "data:" + mimeType + ";" + encoding + "," + text; }; diff --git a/devtools/client/netmonitor/requests-menu-view.js b/devtools/client/netmonitor/requests-menu-view.js index 6ea6381ec..0c854d264 100644 --- a/devtools/client/netmonitor/requests-menu-view.js +++ b/devtools/client/netmonitor/requests-menu-view.js @@ -19,7 +19,6 @@ const {setImageTooltip, getImageDimensions} = const {Heritage, WidgetMethods, setNamedTimeout} = require("devtools/client/shared/widgets/view-helpers"); const {CurlUtils} = require("devtools/client/shared/curl"); -const {PluralForm} = require("devtools/shared/plural-form"); const {Filters, isFreetextMatch} = require("./filter-predicates"); const {Sorters} = require("./sort-predicates"); const {L10N, WEBCONSOLE_L10N} = require("./l10n"); @@ -90,10 +89,11 @@ function storeWatcher(initialValue, reduceValue, onChange) { let currentValue = initialValue; return () => { + const oldValue = currentValue; const newValue = reduceValue(currentValue); - if (newValue !== currentValue) { - onChange(newValue, currentValue); + if (newValue !== oldValue) { currentValue = newValue; + onChange(newValue, oldValue); } }; } @@ -129,8 +129,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { let widgetParentEl = $("#requests-menu-contents"); this.widget = new SideMenuWidget(widgetParentEl); this._splitter = $("#network-inspector-view-splitter"); - this._summary = $("#requests-menu-network-summary-button"); - this._summary.setAttribute("label", L10N.getStr("networkMenu.empty")); // Create a tooltip for the newly appended network request item. this.tooltip = new HTMLTooltip(NetMonitorController._toolbox.doc, { type: "arrow" }); @@ -211,13 +209,10 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { if (NetMonitorController.supportsPerfStats) { $("#requests-menu-perf-notice-button").addEventListener("command", this._onContextPerfCommand, false); - $("#requests-menu-network-summary-button").addEventListener("command", - this._onContextPerfCommand, false); $("#network-statistics-back-button").addEventListener("command", this._onContextPerfCommand, false); } else { $("#notice-perf-message").hidden = true; - $("#requests-menu-network-summary-button").hidden = true; } if (!NetMonitorController.supportsTransferredResponseSize) { @@ -257,8 +252,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this._onReloadCommand, false); $("#requests-menu-perf-notice-button").removeEventListener("command", this._onContextPerfCommand, false); - $("#requests-menu-network-summary-button").removeEventListener("command", - this._onContextPerfCommand, false); $("#network-statistics-back-button").removeEventListener("command", this._onContextPerfCommand, false); @@ -283,6 +276,14 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this._updateQueue = []; this._firstRequestStartedMillis = -1; this._lastRequestEndedMillis = -1; + this.resetNotPersistent(); + }, + + /** + * Reset informations that "devtools.webconsole.persistlog == true". + */ + resetNotPersistent: function () { + this._firstRequestStartedMillisNotPersistent = -1; }, /** @@ -406,9 +407,20 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { if (rawHeadersHidden) { let selected = this.selectedItem.attachment; let selectedRequestHeaders = selected.requestHeaders.headers; - let selectedResponseHeaders = selected.responseHeaders.headers; + // display Status-Line above other response headers + let selectedStatusLine = selected.httpVersion + + " " + selected.status + + " " + selected.statusText + + "\n"; requestTextarea.value = writeHeaderText(selectedRequestHeaders); - responseTextare.value = writeHeaderText(selectedResponseHeaders); + // sometimes it's empty + if (selected.responseHeaders) { + let selectedResponseHeaders = selected.responseHeaders.headers; + responseTextare.value = selectedStatusLine + + writeHeaderText(selectedResponseHeaders); + } else { + responseTextare.value = selectedStatusLine; + } $("#raw-headers").hidden = false; } else { requestTextarea.value = null; @@ -422,7 +434,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { */ reFilterRequests: function () { this.filterContents(this._filterPredicate); - this.refreshSummary(); + this.updateRequests(); this.refreshZebra(); }, @@ -541,7 +553,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { break; } - this.refreshSummary(); + this.updateRequests(); this.refreshZebra(); }, @@ -552,41 +564,17 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { NetMonitorController.NetworkEventsHandler.clearMarkers(); NetMonitorView.Sidebar.toggle(false); - this.store.dispatch(Actions.disableToggleButton(true)); $("#requests-menu-empty-notice").hidden = false; this.empty(); - this.refreshSummary(); + this.updateRequests(); }, /** - * Refreshes the status displayed in this container's footer, providing - * concise information about all requests. + * Update store request itmes and trigger related UI update */ - refreshSummary: function () { - let visibleItems = this.visibleItems; - let visibleRequestsCount = visibleItems.length; - if (!visibleRequestsCount) { - this._summary.setAttribute("label", L10N.getStr("networkMenu.empty")); - return; - } - - let totalBytes = this._getTotalBytesOfRequests(visibleItems); - let totalMillis = - this._getNewestRequest(visibleItems).attachment.endedMillis - - this._getOldestRequest(visibleItems).attachment.startedMillis; - - // https://developer.mozilla.org/en-US/docs/Localization_and_Plurals - let str = PluralForm.get(visibleRequestsCount, - L10N.getStr("networkMenu.summary")); - - this._summary.setAttribute("label", str - .replace("#1", visibleRequestsCount) - .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, - CONTENT_SIZE_DECIMALS)) - .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, - REQUEST_TIME_DECIMALS)) - ); + updateRequests: function () { + this.store.dispatch(Actions.updateRequests(this.visibleItems)); }, /** @@ -670,6 +658,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { // Append a network request item to this container. let requestItem = this.push([menuView, id], { attachment: { + firstRequestStartedMillisNotPersistent: this._firstRequestStartedMillisNotPersistent, startedDeltaMillis: unixTime - this._firstRequestStartedMillis, startedMillis: unixTime, method: method, @@ -865,7 +854,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this._updateQueue = []; this._addQueue = []; - this.store.dispatch(Actions.disableToggleButton(!this.itemCount)); $("#requests-menu-empty-notice").hidden = !!this.itemCount; // Make sure all the requests are sorted and filtered. @@ -875,7 +863,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { // so this doesn't happen once per network event update). this.sortContents(); this.filterContents(); - this.refreshSummary(); + this.updateRequests(); this.refreshZebra(); // Rescale all the waterfalls so that everything is visible at once. @@ -1131,7 +1119,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { */ _createWaterfallView: function (item, timings, fromCache) { let { target } = item; - let sections = ["blocked", "dns", "connect", "send", "wait", "receive"]; + let sections = ["blocked", "dns", "connect", "ssl", "send", "wait", "receive"]; // Skipping "blocked" because it doesn't work yet. let timingsNode = $(".requests-menu-timings", target); @@ -1543,6 +1531,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { if (this._firstRequestStartedMillis == -1) { this._firstRequestStartedMillis = unixTime; } + if (this._firstRequestStartedMillisNotPersistent == -1) { + this._firstRequestStartedMillisNotPersistent = unixTime; + } }, /** @@ -1559,59 +1550,6 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { }, /** - * Gets the total number of bytes representing the cumulated content size of - * a set of requests. Returns 0 for an empty set. - * - * @param array itemsArray - * @return number - */ - _getTotalBytesOfRequests: function (itemsArray) { - if (!itemsArray.length) { - return 0; - } - - let result = 0; - itemsArray.forEach(item => { - let size = item.attachment.contentSize; - result += (typeof size == "number") ? size : 0; - }); - - return result; - }, - - /** - * Gets the oldest (first performed) request in a set. Returns null for an - * empty set. - * - * @param array itemsArray - * @return object - */ - _getOldestRequest: function (itemsArray) { - if (!itemsArray.length) { - return null; - } - return itemsArray.reduce((prev, curr) => - prev.attachment.startedMillis < curr.attachment.startedMillis ? - prev : curr); - }, - - /** - * Gets the newest (latest performed) request in a set. Returns null for an - * empty set. - * - * @param array itemsArray - * @return object - */ - _getNewestRequest: function (itemsArray) { - if (!itemsArray.length) { - return null; - } - return itemsArray.reduce((prev, curr) => - prev.attachment.startedMillis > curr.attachment.startedMillis ? - prev : curr); - }, - - /** * Gets the available waterfall width in this container. * @return number */ @@ -1637,6 +1575,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { _ctx: null, _cachedWaterfallWidth: 0, _firstRequestStartedMillis: -1, + _firstRequestStartedMillisNotPersistent: -1, _lastRequestEndedMillis: -1, _updateQueue: [], _addQueue: [], diff --git a/devtools/client/netmonitor/selectors/index.js b/devtools/client/netmonitor/selectors/index.js index f473149b5..ba5c19094 100644 --- a/devtools/client/netmonitor/selectors/index.js +++ b/devtools/client/netmonitor/selectors/index.js @@ -1,8 +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/. */ + "use strict"; +const { createSelector } = require("devtools/client/shared/vendor/reselect"); + +/** + * Gets the total number of bytes representing the cumulated content size of + * a set of requests. Returns 0 for an empty set. + * + * @param {array} items - an array of request items + * @return {number} total bytes of requests + */ +function getContentSizeOfRequests(items) { + if (!items.length) { + return 0; + } + + let result = 0; + items.forEach((item) => { + let size = item.attachment.contentSize; + result += (typeof size == "number") ? size : 0; + }); + + return result; +} + +function getTransferredSizeOfRequests(items) { + if (!items.length) { + return 0; + } + + let result = 0; + items.forEach((item) => { + let size = item.attachment.transferredSize; + result += (typeof size == "number") ? size : 0; + }); + + return result; +} + +/** + * Gets the total milliseconds for all requests. Returns null for an + * empty set. + * + * @param {array} items - an array of request items + * @return {object} total milliseconds for all requests + */ +function getMillisOfRequests(items) { + if (!items.length) { + return null; + } + + const oldest = items.reduce((prev, curr) => + prev.attachment.startedMillis < curr.attachment.startedMillis ? + prev : curr); + const newest = items.reduce((prev, curr) => + prev.attachment.startedMillis > curr.attachment.startedMillis ? + prev : curr); + + return newest.attachment.endedMillis - oldest.attachment.startedMillis; +} + +const getDisplayedRequestsSummary = createSelector( + (state) => state.requests.items, + (requests) => ({ + count: requests.length, + contentSize: getContentSizeOfRequests(requests), + transferredSize: getTransferredSizeOfRequests(requests), + millis: getMillisOfRequests(requests), + }) +); + +function getDisplayedTimingMarker(state, marker) { + let timingMarker = null; + if (state.timingMarkers) { + timingMarker = state.timingMarkers.get(marker); + } + let firstRequestStartedMillis = null; + if (state.requests.items.length) { + firstRequestStartedMillis = state.requests + .items[state.requests.items.length - 1] + .attachment + .firstRequestStartedMillisNotPersistent; + } + if (timingMarker && firstRequestStartedMillis) { + return timingMarker - firstRequestStartedMillis; + } else { + return -1; + } +} + module.exports = { - // selectors... + getDisplayedRequestsSummary, + getDisplayedTimingMarker, }; diff --git a/devtools/client/netmonitor/test/browser_net_charts-03.js b/devtools/client/netmonitor/test/browser_net_charts-03.js index c7d9b0c1a..4a655a8ca 100644 --- a/devtools/client/netmonitor/test/browser_net_charts-03.js +++ b/devtools/client/netmonitor/test/browser_net_charts-03.js @@ -33,6 +33,10 @@ add_task(function* () { totals: { label1: value => "Hello " + L10N.numberWithDecimals(value, 2), label2: value => "World " + L10N.numberWithDecimals(value, 2) + }, + header: { + label1: "label1header", + label2: "label2header", } }); @@ -51,39 +55,48 @@ add_task(function* () { is(title.getAttribute("value"), "Table title", "The title node displays the correct text."); - is(rows.length, 3, "There should be 3 table chart rows created."); + is(rows.length, 4, "There should be 3 table chart rows and a header created."); - ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"), - "A colored blob exists for the firt row."); is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "label1", - "The first column of the first row exists."); + "The first column of the header exists."); is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label2", + "The second column of the header exists."); + is(rows[0].querySelectorAll("span")[0].textContent, "label1header", + "The first column of the header displays the correct text."); + is(rows[0].querySelectorAll("span")[1].textContent, "label2header", + "The second column of the header displays the correct text."); + + ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"), + "A colored blob exists for the firt row."); + is(rows[1].querySelectorAll("span")[0].getAttribute("name"), "label1", + "The first column of the first row exists."); + is(rows[1].querySelectorAll("span")[1].getAttribute("name"), "label2", "The second column of the first row exists."); - is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "1", + is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "1", "The first column of the first row displays the correct text."); - is(rows[0].querySelectorAll("label")[1].getAttribute("value"), "11.1foo", + is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "11.1foo", "The second column of the first row displays the correct text."); - ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"), + ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"), "A colored blob exists for the second row."); - is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "label1", + is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1", "The first column of the second row exists."); - is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label2", + is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2", "The second column of the second row exists."); - is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "2", + is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "2", "The first column of the second row displays the correct text."); - is(rows[1].querySelectorAll("label")[1].getAttribute("value"), "12.2bar", + is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "12.2bar", "The second column of the first row displays the correct text."); - ok(rows[2].querySelector(".table-chart-row-box.chart-colored-blob"), + ok(rows[3].querySelector(".table-chart-row-box.chart-colored-blob"), "A colored blob exists for the third row."); - is(rows[2].querySelectorAll("label")[0].getAttribute("name"), "label1", + is(rows[3].querySelectorAll("label")[0].getAttribute("name"), "label1", "The first column of the third row exists."); - is(rows[2].querySelectorAll("label")[1].getAttribute("name"), "label2", + is(rows[3].querySelectorAll("label")[1].getAttribute("name"), "label2", "The second column of the third row exists."); - is(rows[2].querySelectorAll("label")[0].getAttribute("value"), "3", + is(rows[3].querySelectorAll("label")[0].getAttribute("value"), "3", "The first column of the third row displays the correct text."); - is(rows[2].querySelectorAll("label")[1].getAttribute("value"), "13.3baz", + is(rows[3].querySelectorAll("label")[1].getAttribute("value"), "13.3baz", "The second column of the third row displays the correct text."); is(sums.length, 2, "There should be 2 total summaries created."); diff --git a/devtools/client/netmonitor/test/browser_net_charts-04.js b/devtools/client/netmonitor/test/browser_net_charts-04.js index 0d150c409..921701ae5 100644 --- a/devtools/client/netmonitor/test/browser_net_charts-04.js +++ b/devtools/client/netmonitor/test/browser_net_charts-04.js @@ -22,6 +22,10 @@ add_task(function* () { totals: { label1: value => "Hello " + L10N.numberWithDecimals(value, 2), label2: value => "World " + L10N.numberWithDecimals(value, 2) + }, + header: { + label1: "", + label2: "" } }); @@ -40,17 +44,17 @@ add_task(function* () { is(title.getAttribute("value"), "Table title", "The title node displays the correct text."); - is(rows.length, 1, "There should be 1 table chart row created."); + is(rows.length, 2, "There should be 1 table chart row and a 1 header created."); - ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"), + ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"), "A colored blob exists for the firt row."); - is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size", + is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "size", "The first column of the first row exists."); - is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label", + is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label", "The second column of the first row exists."); - is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "", + is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "", "The first column of the first row displays the correct text."); - is(rows[0].querySelectorAll("label")[1].getAttribute("value"), + is(rows[1].querySelectorAll("label")[1].getAttribute("value"), L10N.getStr("tableChart.loading"), "The second column of the first row displays the correct text."); diff --git a/devtools/client/netmonitor/test/browser_net_charts-05.js b/devtools/client/netmonitor/test/browser_net_charts-05.js index 00445b132..c444d2c65 100644 --- a/devtools/client/netmonitor/test/browser_net_charts-05.js +++ b/devtools/client/netmonitor/test/browser_net_charts-05.js @@ -33,6 +33,10 @@ add_task(function* () { totals: { size: value => "Hello " + L10N.numberWithDecimals(value, 2), label: value => "World " + L10N.numberWithDecimals(value, 2) + }, + header: { + label1: "", + label2: "" } }); @@ -53,9 +57,9 @@ add_task(function* () { ok(node.querySelector(".table-chart-container"), "A table chart was created successfully."); - is(rows.length, 3, "There should be 3 pie chart slices created."); - is(rows.length, 3, "There should be 3 table chart rows created."); - is(sums.length, 2, "There should be 2 total summaries created."); + is(rows.length, 4, "There should be 3 pie chart slices and 1 header created."); + is(rows.length, 4, "There should be 3 table chart rows and 1 header created."); + is(sums.length, 2, "There should be 2 total summaries and 1 header created."); yield teardown(monitor); }); diff --git a/devtools/client/netmonitor/test/browser_net_charts-07.js b/devtools/client/netmonitor/test/browser_net_charts-07.js index bb992e4eb..a655f258c 100644 --- a/devtools/client/netmonitor/test/browser_net_charts-07.js +++ b/devtools/client/netmonitor/test/browser_net_charts-07.js @@ -20,6 +20,10 @@ add_task(function* () { totals: { label1: value => "Hello " + L10N.numberWithDecimals(value, 2), label2: value => "World " + L10N.numberWithDecimals(value, 2) + }, + header: { + label1: "", + label2: "" } }); @@ -29,17 +33,17 @@ add_task(function* () { let rows = grid.querySelectorAll(".table-chart-row"); let sums = node.querySelectorAll(".table-chart-summary-label"); - is(rows.length, 1, "There should be 1 table chart row created."); + is(rows.length, 2, "There should be 1 table chart row and 1 header created."); - ok(rows[0].querySelector(".table-chart-row-box.chart-colored-blob"), + ok(rows[1].querySelector(".table-chart-row-box.chart-colored-blob"), "A colored blob exists for the firt row."); - is(rows[0].querySelectorAll("label")[0].getAttribute("name"), "size", + is(rows[1].querySelectorAll("label")[0].getAttribute("name"), "size", "The first column of the first row exists."); - is(rows[0].querySelectorAll("label")[1].getAttribute("name"), "label", + is(rows[1].querySelectorAll("label")[1].getAttribute("name"), "label", "The second column of the first row exists."); - is(rows[0].querySelectorAll("label")[0].getAttribute("value"), "", + is(rows[1].querySelectorAll("label")[0].getAttribute("value"), "", "The first column of the first row displays the correct text."); - is(rows[0].querySelectorAll("label")[1].getAttribute("value"), + is(rows[1].querySelectorAll("label")[1].getAttribute("value"), L10N.getStr("tableChart.unavailable"), "The second column of the first row displays the correct text."); diff --git a/devtools/client/netmonitor/test/browser_net_curl-utils.js b/devtools/client/netmonitor/test/browser_net_curl-utils.js index 7a5fc7926..afac78cfc 100644 --- a/devtools/client/netmonitor/test/browser_net_curl-utils.js +++ b/devtools/client/netmonitor/test/browser_net_curl-utils.js @@ -7,7 +7,7 @@ * Tests Curl Utils functionality. */ -const { CurlUtils } = require("devtools/client/shared/curl"); +const { Curl, CurlUtils } = require("devtools/client/shared/curl"); add_task(function* () { let { tab, monitor } = yield initNetMonitor(CURL_UTILS_URL); @@ -18,7 +18,7 @@ add_task(function* () { RequestsMenu.lazyUpdate = false; - let wait = waitForNetworkEvents(monitor, 1, 3); + let wait = waitForNetworkEvents(monitor, 1, 4); yield ContentTask.spawn(tab.linkedBrowser, SIMPLE_SJS, function* (url) { content.wrappedJSObject.performRequests(url); }); @@ -27,8 +27,9 @@ add_task(function* () { let requests = { get: RequestsMenu.getItemAtIndex(0), post: RequestsMenu.getItemAtIndex(1), - multipart: RequestsMenu.getItemAtIndex(2), - multipartForm: RequestsMenu.getItemAtIndex(3) + patch: RequestsMenu.getItemAtIndex(2), + multipart: RequestsMenu.getItemAtIndex(3), + multipartForm: RequestsMenu.getItemAtIndex(4) }; let data = yield createCurlData(requests.get.attachment, gNetwork); @@ -37,6 +38,12 @@ add_task(function* () { data = yield createCurlData(requests.post.attachment, gNetwork); testIsUrlEncodedRequest(data); testWritePostDataTextParams(data); + testWriteEmptyPostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); + + data = yield createCurlData(requests.patch.attachment, gNetwork); + testWritePostDataTextParams(data); + testDataArgumentOnGeneratedCommand(data); data = yield createCurlData(requests.multipart.attachment, gNetwork); testIsMultipartRequest(data); @@ -85,6 +92,18 @@ function testWritePostDataTextParams(data) { "Should return a serialized representation of the request parameters"); } +function testWriteEmptyPostDataTextParams(data) { + let params = CurlUtils.writePostDataTextParams(null); + is(params, "", + "Should return a empty string when no parameters provided"); +} + +function testDataArgumentOnGeneratedCommand(data) { + let curlCommand = Curl.generateCommand(data); + ok(curlCommand.includes("--data"), + "Should return a curl command with --data"); +} + function testGetMultipartBoundary(data) { let boundary = CurlUtils.getMultipartBoundary(data); ok(/-{3,}\w+/.test(boundary), diff --git a/devtools/client/netmonitor/test/browser_net_footer-summary.js b/devtools/client/netmonitor/test/browser_net_footer-summary.js index e484b2097..8faa8470b 100644 --- a/devtools/client/netmonitor/test/browser_net_footer-summary.js +++ b/devtools/client/netmonitor/test/browser_net_footer-summary.js @@ -10,13 +10,14 @@ add_task(function* () { requestLongerTimeout(2); + let { getSummary } = require("devtools/client/netmonitor/selectors/index"); let { L10N } = require("devtools/client/netmonitor/l10n"); let { PluralForm } = require("devtools/shared/plural-form"); let { tab, monitor } = yield initNetMonitor(FILTERING_URL); info("Starting test... "); - let { $, NetMonitorView } = monitor.panelWin; + let { $, NetMonitorView, gStore } = monitor.panelWin; let { RequestsMenu } = NetMonitorView; RequestsMenu.lazyUpdate = false; @@ -43,33 +44,27 @@ add_task(function* () { yield teardown(monitor); function testStatus() { - let summary = $("#requests-menu-network-summary-button"); - let value = summary.getAttribute("label"); + const { count, contentSize, transferredSize, millis } = getSummary(gStore.getState()); + let value = $("#requests-menu-network-summary-button").textContent; info("Current summary: " + value); - let visibleItems = RequestsMenu.visibleItems; - let visibleRequestsCount = visibleItems.length; let totalRequestsCount = RequestsMenu.itemCount; - info("Current requests: " + visibleRequestsCount + " of " + totalRequestsCount + "."); + info("Current requests: " + count + " of " + totalRequestsCount + "."); - if (!totalRequestsCount || !visibleRequestsCount) { + if (!totalRequestsCount || !count) { is(value, L10N.getStr("networkMenu.empty"), "The current summary text is incorrect, expected an 'empty' label."); return; } - let totalBytes = RequestsMenu._getTotalBytesOfRequests(visibleItems); - let totalMillis = - RequestsMenu._getNewestRequest(visibleItems).attachment.endedMillis - - RequestsMenu._getOldestRequest(visibleItems).attachment.startedMillis; + info("Computed total bytes: " + contentSize); + info("Computed total millis: " + millis); - info("Computed total bytes: " + totalBytes); - info("Computed total millis: " + totalMillis); - - is(value, PluralForm.get(visibleRequestsCount, L10N.getStr("networkMenu.summary")) - .replace("#1", visibleRequestsCount) - .replace("#2", L10N.numberWithDecimals((totalBytes || 0) / 1024, 2)) - .replace("#3", L10N.numberWithDecimals((totalMillis || 0) / 1000, 2)) + is(value, PluralForm.get(count, L10N.getStr("networkMenu.summary2")) + .replace("#1", count) + .replace("#2", L10N.numberWithDecimals((contentSize || 0) / 1024, 2)) + .replace("#3", L10N.numberWithDecimals((transferredSize || 0) / 1024, 2)) + .replace("#4", L10N.numberWithDecimals((millis || 0) / 1000, 2)) , "The current summary text is incorrect."); } }); diff --git a/devtools/client/netmonitor/test/browser_net_security-details.js b/devtools/client/netmonitor/test/browser_net_security-details.js index 0a83b3ed9..61f39a414 100644 --- a/devtools/client/netmonitor/test/browser_net_security-details.js +++ b/devtools/client/netmonitor/test/browser_net_security-details.js @@ -66,6 +66,10 @@ add_task(function* () { checkLabel("#security-cert-issuer-o", "Mozilla Testing"); checkLabel("#security-cert-issuer-ou", "<Not Available>"); + // These two values can change. So only check they're not empty. + checkLabelNotEmpty("#security-keagroup-value"); + checkLabelNotEmpty("#security-signaturescheme-value"); + // Locale sensitive and varies between timezones. Cant't compare equality or // the test fails depending on which part of the world the test is executed. checkLabelNotEmpty("#security-cert-validity-begins"); diff --git a/devtools/client/netmonitor/test/browser_net_simple-request-data.js b/devtools/client/netmonitor/test/browser_net_simple-request-data.js index 1b952bd71..f21318d7a 100644 --- a/devtools/client/netmonitor/test/browser_net_simple-request-data.js +++ b/devtools/client/netmonitor/test/browser_net_simple-request-data.js @@ -226,6 +226,8 @@ function test() { "The eventTimings attachment has an incorrect |timings.blocked| property."); is(typeof requestItem.attachment.eventTimings.timings.dns, "number", "The eventTimings attachment has an incorrect |timings.dns| property."); + is(typeof requestItem.attachment.eventTimings.timings.ssl, "number", + "The eventTimings attachment has an incorrect |timings.ssl| property."); is(typeof requestItem.attachment.eventTimings.timings.connect, "number", "The eventTimings attachment has an incorrect |timings.connect| property."); is(typeof requestItem.attachment.eventTimings.timings.send, "number", diff --git a/devtools/client/netmonitor/test/html_curl-utils.html b/devtools/client/netmonitor/test/html_curl-utils.html index 8ff7ecdf0..eb5c0c5b6 100644 --- a/devtools/client/netmonitor/test/html_curl-utils.html +++ b/devtools/client/netmonitor/test/html_curl-utils.html @@ -52,6 +52,17 @@ xhr.send(params); } + function ajaxPatch(aUrl, aCallback) { + let xhr = new XMLHttpRequest(); + xhr.open("PATCH", aUrl, true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.onload = function() { + aCallback(); + }; + var params = "param1=value1¶m2=value2¶m3=value3"; + xhr.send(params); + } + function ajaxMultipart(aUrl, aCallback) { var xhr = new XMLHttpRequest(); xhr.open("POST", aUrl, true); @@ -88,8 +99,10 @@ function performRequests(aUrl) { ajaxGet(aUrl, () => { ajaxPost(aUrl, () => { - ajaxMultipart(aUrl, () => { - submitForm(); + ajaxPatch(aUrl, () => { + ajaxMultipart(aUrl, () => { + submitForm(); + }); }); }); }); diff --git a/devtools/client/netmonitor/toolbar-view.js b/devtools/client/netmonitor/toolbar-view.js index 28c3cf99b..1834d3ee9 100644 --- a/devtools/client/netmonitor/toolbar-view.js +++ b/devtools/client/netmonitor/toolbar-view.js @@ -7,6 +7,7 @@ const Provider = createFactory(require("devtools/client/shared/vendor/react-redu const FilterButtons = createFactory(require("./components/filter-buttons")); const ToggleButton = createFactory(require("./components/toggle-button")); const SearchBox = createFactory(require("./components/search-box")); +const SummaryButton = createFactory(require("./components/summary-button")); const { L10N } = require("./l10n"); // Shortcuts @@ -28,8 +29,9 @@ ToolbarView.prototype = { this._clearContainerNode = $("#react-clear-button-hook"); this._filterContainerNode = $("#react-filter-buttons-hook"); - this._toggleContainerNode = $("#react-details-pane-toggle-hook"); + this._summaryContainerNode = $("#react-summary-button-hook"); this._searchContainerNode = $("#react-search-box-hook"); + this._toggleContainerNode = $("#react-details-pane-toggle-hook"); // clear button ReactDOM.render(button({ @@ -47,6 +49,12 @@ ToolbarView.prototype = { FilterButtons() ), this._filterContainerNode); + // summary button + ReactDOM.render(Provider( + { store }, + SummaryButton() + ), this._summaryContainerNode); + // search box ReactDOM.render(Provider( { store }, @@ -68,8 +76,9 @@ ToolbarView.prototype = { ReactDOM.unmountComponentAtNode(this._clearContainerNode); ReactDOM.unmountComponentAtNode(this._filterContainerNode); - ReactDOM.unmountComponentAtNode(this._toggleContainerNode); + ReactDOM.unmountComponentAtNode(this._summaryContainerNode); ReactDOM.unmountComponentAtNode(this._searchContainerNode); + ReactDOM.unmountComponentAtNode(this._toggleContainerNode); } }; diff --git a/devtools/client/performance/performance-controller.js b/devtools/client/performance/performance-controller.js index e47a0c401..4bd272f41 100644 --- a/devtools/client/performance/performance-controller.js +++ b/devtools/client/performance/performance-controller.js @@ -527,11 +527,10 @@ var PerformanceController = { if (flags.testing) { return { supported: true, enabled: true }; } - let supported = system.constants.E10S_TESTING_ONLY; // This is only checked on tool startup -- requires a restart if // e10s subsequently enabled. let enabled = this._e10s; - return { supported, enabled }; + return { supported: false, enabled }; }, /** diff --git a/devtools/client/performance/test/browser_perf-recording-notices-05.js b/devtools/client/performance/test/browser_perf-recording-notices-05.js index b6267470d..7a6b6b6b9 100644 --- a/devtools/client/performance/test/browser_perf-recording-notices-05.js +++ b/devtools/client/performance/test/browser_perf-recording-notices-05.js @@ -41,8 +41,8 @@ add_task(function* () { enabled = true; PerformanceController._setMultiprocessAttributes(); ok($("#performance-view").getAttribute("e10s"), "", - "When e10s is enabled, but not supported, this probably means we no longer have " + - "E10S_TESTING_ONLY, and we have no e10s attribute."); + "When e10s is enabled, but not supported, this probably means we " + + "have no e10s attribute."); supported = true; enabled = true; diff --git a/devtools/client/scratchpad/scratchpad.js b/devtools/client/scratchpad/scratchpad.js index 306b635df..60221f39d 100644 --- a/devtools/client/scratchpad/scratchpad.js +++ b/devtools/client/scratchpad/scratchpad.js @@ -81,7 +81,7 @@ loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true); loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "EnvironmentClient", "devtools/shared/client/main", true); loader.lazyRequireGetter(this, "ObjectClient", "devtools/shared/client/main", true); -loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice"); +loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true); XPCOMUtils.defineLazyGetter(this, "REMOTE_TIMEOUT", () => Services.prefs.getIntPref("devtools.debugger.remote-timeout")); @@ -1744,6 +1744,9 @@ var Scratchpad = { this.editor.focus(); this.editor.setCursor({ line: lines.length, ch: lines.pop().length }); + // Add the commands controller for the source-editor. + this.editor.insertCommandsController(); + if (state) this.dirty = !state.saved; diff --git a/devtools/client/scratchpad/scratchpad.xul b/devtools/client/scratchpad/scratchpad.xul index 0603fa95e..8694c1bb5 100644 --- a/devtools/client/scratchpad/scratchpad.xul +++ b/devtools/client/scratchpad/scratchpad.xul @@ -121,7 +121,7 @@ <key id="sp-key-reloadAndRun" key="&reloadAndRun.key;" command="sp-cmd-reloadAndRun" - modifiers="accel,shift"/> + modifiers="shift,alt"/> <key id="sp-key-evalFunction" key="&evalFunction.key;" command="sp-cmd-evalFunction" diff --git a/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js index 4da2a2daf..418bdfb56 100644 --- a/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js +++ b/devtools/client/scratchpad/test/browser_scratchpad_open_error_console.js @@ -2,7 +2,7 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ -const HUDService = require("devtools/client/webconsole/hudservice"); +const {HUDService} = require("devtools/client/webconsole/hudservice"); function test() { diff --git a/devtools/client/shared/curl.js b/devtools/client/shared/curl.js index 44465193f..420fe6aa5 100644 --- a/devtools/client/shared/curl.js +++ b/devtools/client/shared/curl.js @@ -76,7 +76,8 @@ const Curl = { // Create post data. let postData = []; - if (utils.isUrlEncodedRequest(data) || data.method == "PUT") { + if (utils.isUrlEncodedRequest(data) || + ["PUT", "POST", "PATCH"].includes(data.method)) { postDataText = data.postDataText; postData.push("--data"); postData.push(escapeString(utils.writePostDataTextParams(postDataText))); @@ -207,6 +208,9 @@ const CurlUtils = { * Post data parameters. */ writePostDataTextParams: function (postDataText) { + if (!postDataText) { + return ""; + } let lines = postDataText.split("\r\n"); return lines[lines.length - 1]; }, diff --git a/devtools/client/shared/moz.build b/devtools/client/shared/moz.build index 1c61970c0..7be4a0088 100644 --- a/devtools/client/shared/moz.build +++ b/devtools/client/shared/moz.build @@ -35,6 +35,7 @@ DevToolsModules( 'Jsbeautify.jsm', 'key-shortcuts.js', 'keycodes.js', + 'natural-sort.js', 'network-throttling-profiles.js', 'node-attribute-parser.js', 'options-view.js', diff --git a/devtools/client/shared/natural-sort.js b/devtools/client/shared/natural-sort.js new file mode 100644 index 000000000..904d76431 --- /dev/null +++ b/devtools/client/shared/natural-sort.js @@ -0,0 +1,106 @@ +/* + * Natural Sort algorithm for Javascript - Version 0.8.1 - Released under MIT license + * Author: Jim Palmer (based on chunking idea from Dave Koelle) + * + * Includes pull request to move regexes out of main function for performance + * increases. + * + * Repository: + * https://github.com/overset/javascript-natural-sort/ + */ + +"use strict"; + +var re = /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g; +var sre = /^\s+|\s+$/g; // trim pre-post whitespace +var snre = /\s+/g; // normalize all whitespace to single ' ' character + +// eslint-disable-next-line +var dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/; +var hre = /^0x[0-9a-f]+$/i; +var ore = /^0/; +var b0re = /^\0/; +var e0re = /\0$/; + +exports.naturalSortCaseSensitive = +function naturalSortCaseSensitive(a, b) { + return naturalSort(a, b, false); +}; + +exports.naturalSortCaseInsensitive = +function naturalSortCaseInsensitive(a, b) { + return naturalSort(a, b, true); +}; + +/** + * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc. + * "the way humans do." + * + * This function should only be called via naturalSortCaseSensitive and + * naturalSortCaseInsensitive. + * + * e.g. [3, 2, 1, 10].sort(naturalSort) + * + * @param {Object} a + * Passed in by Array.sort(a, b) + * @param {Object} b + * Passed in by Array.sort(a, b) + * @param {Boolean} insensitive + * Should the search be case insensitive? + */ +function naturalSort(a, b, insensitive) { + // convert all to strings strip whitespace + let i = function (s) { + return (insensitive && ("" + s).toLowerCase() || "" + s) + .replace(sre, ""); + }; + let x = i(a) || ""; + let y = i(b) || ""; + // chunk/tokenize + let xN = x.replace(re, "\0$1\0").replace(e0re, "").replace(b0re, "").split("\0"); + let yN = y.replace(re, "\0$1\0").replace(e0re, "").replace(b0re, "").split("\0"); + // numeric, hex or date detection + let xD = parseInt(x.match(hre), 16) || (xN.length !== 1 && Date.parse(x)); + let yD = parseInt(y.match(hre), 16) || xD && y.match(dre) && Date.parse(y) || null; + let normChunk = function (s, l) { + // normalize spaces; find floats not starting with '0', string or 0 if + // not defined (Clint Priest) + return (!s.match(ore) || l == 1) && + parseFloat(s) || s.replace(snre, " ").replace(sre, "") || 0; + }; + let oFxNcL; + let oFyNcL; + + // first try and sort Hex codes or Dates + if (yD) { + if (xD < yD) { + return -1; + } else if (xD > yD) { + return 1; + } + } + + // natural sorting through split numeric strings and default strings + // eslint-disable-next-line + for (let cLoc = 0, xNl = xN.length, yNl = yN.length, numS = Math.max(xNl, yNl); cLoc < numS; cLoc++) { + oFxNcL = normChunk(xN[cLoc] || "", xNl); + oFyNcL = normChunk(yN[cLoc] || "", yNl); + + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(oFxNcL) !== isNaN(oFyNcL)) { + return isNaN(oFxNcL) ? 1 : -1; + } + // if unicode use locale comparison + // eslint-disable-next-line + if (/[^\x00-\x80]/.test(oFxNcL + oFyNcL) && oFxNcL.localeCompare) { + let comp = oFxNcL.localeCompare(oFyNcL); + return comp / Math.abs(comp); + } + if (oFxNcL < oFyNcL) { + return -1; + } else if (oFxNcL > oFyNcL) { + return 1; + } + } + return null; +} diff --git a/devtools/client/shared/widgets/Chart.jsm b/devtools/client/shared/widgets/Chart.jsm index 0894a62ca..0b7cb71fb 100644 --- a/devtools/client/shared/widgets/Chart.jsm +++ b/devtools/client/shared/widgets/Chart.jsm @@ -105,7 +105,7 @@ function PieTableChart(node, pie, table) { * - "mouseout", when the mouse leaves a slice or a row * - "click", when the mouse enters a slice or a row */ -function createPieTableChart(document, { title, diameter, data, strings, totals, sorted }) { +function createPieTableChart(document, { title, diameter, data, strings, totals, sorted, header }) { if (data && sorted) { data = data.slice().sort((a, b) => +(a.size < b.size)); } @@ -119,7 +119,8 @@ function createPieTableChart(document, { title, diameter, data, strings, totals, title: title, data: data, strings: strings, - totals: totals + totals: totals, + header: header, }); let container = document.createElement("hbox"); @@ -338,7 +339,7 @@ function createPieChart(document, { data, width, height, centerX, centerY, radiu * - "mouseout", when the mouse leaves a row * - "click", when the mouse clicks a row */ -function createTableChart(document, { title, data, strings, totals }) { +function createTableChart(document, { title, data, strings, totals, header }) { strings = strings || {}; totals = totals || {}; let isPlaceholder = false; @@ -371,6 +372,24 @@ function createTableChart(document, { title, data, strings, totals }) { tableNode.className = "plain table-chart-grid"; container.appendChild(tableNode); + const headerNode = document.createElement("div"); + headerNode.className = "table-chart-row"; + + const headerBoxNode = document.createElement("div"); + headerBoxNode.className = "table-chart-row-box"; + headerNode.appendChild(headerBoxNode); + + for (let [key, value] of Object.entries(header)) { + let headerLabelNode = document.createElement("span"); + headerLabelNode.className = "plain table-chart-row-label"; + headerLabelNode.setAttribute("name", key); + headerLabelNode.textContent = value; + + headerNode.appendChild(headerLabelNode); + } + + tableNode.appendChild(headerNode); + for (let rowInfo of data) { let rowNode = document.createElement("hbox"); rowNode.className = "table-chart-row"; diff --git a/devtools/client/shared/widgets/TableWidget.js b/devtools/client/shared/widgets/TableWidget.js index 5dacd1b67..a0f0dfc11 100644 --- a/devtools/client/shared/widgets/TableWidget.js +++ b/devtools/client/shared/widgets/TableWidget.js @@ -8,6 +8,8 @@ loader.lazyRequireGetter(this, "setNamedTimeout", "devtools/client/shared/widgets/view-helpers", true); loader.lazyRequireGetter(this, "clearNamedTimeout", "devtools/client/shared/widgets/view-helpers", true); +loader.lazyRequireGetter(this, "naturalSortCaseInsensitive", + "devtools/client/shared/natural-sort", true); const {KeyCodes} = require("devtools/client/shared/keycodes"); const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; @@ -123,6 +125,8 @@ function TableWidget(node, options = {}) { TableWidget.prototype = { items: null, + editBookmark: null, + scrollIntoViewOnUpdate: null, /** * Getter for the headers context menu popup id. @@ -139,7 +143,12 @@ TableWidget.prototype = { */ set selectedRow(id) { for (let column of this.columns.values()) { - column.selectRow(id[this.uniqueId] || id); + if (id) { + column.selectRow(id[this.uniqueId] || id); + } else { + column.selectedRow = null; + column.selectRow(null); + } } }, @@ -615,8 +624,13 @@ TableWidget.prototype = { /** * Populates the header context menu with the names of the columns along with * displaying which columns are hidden or visible. + * + * @param {Array} privateColumns=[] + * An array of column names that should never appear in the table. This + * allows us to e.g. have an invisible compound primary key for a + * table's rows. */ - populateMenuPopup: function () { + populateMenuPopup: function (privateColumns = []) { if (!this.menupopup) { return; } @@ -626,6 +640,10 @@ TableWidget.prototype = { } for (let column of this.columns.values()) { + if (privateColumns.includes(column.id)) { + continue; + } + let menuitem = this.document.createElementNS(XUL_NS, "menuitem"); menuitem.setAttribute("label", column.header.getAttribute("value")); menuitem.setAttribute("data-id", column.id); @@ -663,16 +681,21 @@ TableWidget.prototype = { * Creates the columns in the table. Without calling this method, data cannot * be inserted into the table unless `initialColumns` was supplied. * - * @param {object} columns + * @param {Object} columns * A key value pair representing the columns of the table. Where the * key represents the id of the column and the value is the displayed * label in the header of the column. - * @param {string} sortOn + * @param {String} sortOn * The id of the column on which the table will be initially sorted on. - * @param {array} hiddenColumns + * @param {Array} hiddenColumns * Ids of all the columns that are hidden by default. + * @param {Array} privateColumns=[] + * An array of column names that should never appear in the table. This + * allows us to e.g. have an invisible compound primary key for a + * table's rows. */ - setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = []) { + setColumns: function (columns, sortOn = this.sortedOn, hiddenColumns = [], + privateColumns = []) { for (let column of this.columns.values()) { column.destroy(); } @@ -702,13 +725,18 @@ TableWidget.prototype = { } this.columns.set(id, new Column(this, id, columns[id])); - if (hiddenColumns.indexOf(id) > -1) { + if (hiddenColumns.includes(id) || privateColumns.includes(id)) { + // Hide the column. this.columns.get(id).toggleColumn(); + + if (privateColumns.includes(id)) { + this.columns.get(id).private = true; + } } } this.sortedOn = sortOn; this.sortBy(this.sortedOn); - this.populateMenuPopup(); + this.populateMenuPopup(privateColumns); }, /** @@ -778,6 +806,11 @@ TableWidget.prototype = { return; } + if (this.editBookmark && !this.items.has(this.editBookmark)) { + // Key has been updated... update bookmark. + this.editBookmark = item[this.uniqueId]; + } + let index = this.columns.get(this.sortedOn).push(item); for (let [key, column] of this.columns) { if (key != this.sortedOn) { @@ -814,7 +847,8 @@ TableWidget.prototype = { column.remove(item); column.updateZebra(); } - if (this.items.size == 0) { + if (this.items.size === 0) { + this.selectedRow = null; this.tbody.setAttribute("empty", "empty"); } @@ -857,6 +891,8 @@ TableWidget.prototype = { this.tbody.setAttribute("empty", "empty"); this.setPlaceholderText(this.emptyText); + this.selectedRow = null; + this.emit(EVENTS.TABLE_CLEARED, this); }, @@ -958,6 +994,9 @@ module.exports.TableWidget = TableWidget; * The displayed string on the column's header. */ function Column(table, id, header) { + // By default cells are visible in the UI. + this._private = false; + this.tbody = table.tbody; this.document = table.document; this.window = table.window; @@ -1041,6 +1080,23 @@ Column.prototype = { }, /** + * Get the private state of the column (visibility in the UI). + */ + get private() { + return this._private; + }, + + /** + * Set the private state of the column (visibility in the UI). + * + * @param {Boolean} state + * Private (true or false) + */ + set private(state) { + this._private = state; + }, + + /** * Sets the sorted value */ set sorted(value) { @@ -1115,7 +1171,9 @@ Column.prototype = { }, /** - * Called when a row is updated. + * Called when a row is updated e.g. a cell is changed. This means that + * for a new row this method will be called once for each column. If a single + * cell is changed this method will be called just once. * * @param {string} event * The event name of the event. i.e. EVENTS.ROW_UPDATED @@ -1124,7 +1182,23 @@ Column.prototype = { */ onRowUpdated: function (event, id) { this._updateItems(); + if (this.highlightUpdated && this.items[id] != null) { + if (this.table.scrollIntoViewOnUpdate) { + let cell = this.cells[this.items[id]]; + + // When a new row is created this method is called once for each column + // as each cell is updated. We can only scroll to cells if they are + // visible. We check for visibility and once we find the first visible + // cell in a row we scroll it into view and reset the + // scrollIntoViewOnUpdate flag. + if (cell.label.clientHeight > 0) { + cell.scrollIntoView(); + + this.table.scrollIntoViewOnUpdate = null; + } + } + if (this.table.editBookmark) { // A rows position in the table can change as the result of an edit. In // order to ensure that the correct row is highlighted after an edit we @@ -1136,6 +1210,7 @@ Column.prototype = { this.cells[this.items[id]].flash(); } + this.updateZebra(); }, @@ -1160,15 +1235,16 @@ Column.prototype = { */ selectRowAt: function (index) { if (this.selectedRow != null) { - this.cells[this.items[this.selectedRow]].toggleClass("theme-selected"); + this.cells[this.items[this.selectedRow]].classList.remove("theme-selected"); } - if (index < 0) { + + let cell = this.cells[index]; + if (cell) { + cell.classList.add("theme-selected"); + this.selectedRow = cell.id; + } else { this.selectedRow = null; - return; } - let cell = this.cells[index]; - cell.toggleClass("theme-selected"); - this.selectedRow = cell.id; }, /** @@ -1218,11 +1294,11 @@ Column.prototype = { let index; if (this.sorted == 1) { index = this.cells.findIndex(element => { - return value < element.value; + return naturalSortCaseInsensitive(value, element.value) === -1; }); } else { index = this.cells.findIndex(element => { - return value > element.value; + return naturalSortCaseInsensitive(value, element.value) === 1; }); } index = index >= 0 ? index : this.cells.length; @@ -1332,7 +1408,6 @@ Column.prototype = { this.cells = []; this.items = {}; this._itemsDirty = false; - this.selectedRow = null; while (this.header.nextSibling) { this.header.nextSibling.remove(); } @@ -1350,7 +1425,7 @@ Column.prototype = { a[this.id].textContent : a[this.id]; let val2 = (b[this.id] instanceof Node) ? b[this.id].textContent : b[this.id]; - return val1 > val2; + return naturalSortCaseInsensitive(val1, val2); }); } else if (this.sorted > 1) { items.sort((a, b) => { @@ -1358,12 +1433,12 @@ Column.prototype = { a[this.id].textContent : a[this.id]; let val2 = (b[this.id] instanceof Node) ? b[this.id].textContent : b[this.id]; - return val2 > val1; + return naturalSortCaseInsensitive(val2, val1); }); } if (this.selectedRow) { - this.cells[this.items[this.selectedRow]].toggleClass("theme-selected"); + this.cells[this.items[this.selectedRow]].classList.remove("theme-selected"); } this.items = {}; // Otherwise, just use the sorted array passed to update the cells value. @@ -1373,7 +1448,7 @@ Column.prototype = { this.cells[i].id = item[this.uniqueId]; }); if (this.selectedRow) { - this.cells[this.items[this.selectedRow]].toggleClass("theme-selected"); + this.cells[this.items[this.selectedRow]].classList.add("theme-selected"); } this._itemsDirty = false; this.updateZebra(); @@ -1387,7 +1462,9 @@ Column.prototype = { if (!cell.hidden) { i++; } - cell.toggleClass("even", !(i % 2)); + + let even = !(i % 2); + cell.classList.toggle("even", even); } }, @@ -1523,8 +1600,8 @@ Cell.prototype = { return this._value; }, - toggleClass: function (className, condition) { - this.label.classList.toggle(className, condition); + get classList() { + return this.label.classList; }, /** @@ -1550,6 +1627,10 @@ Cell.prototype = { this.label.focus(); }, + scrollIntoView: function () { + this.label.scrollIntoView(false); + }, + destroy: function () { this.label.remove(); this.label = null; diff --git a/devtools/client/sourceeditor/editor-commands-controller.js b/devtools/client/sourceeditor/editor-commands-controller.js new file mode 100644 index 000000000..2587f9a1f --- /dev/null +++ b/devtools/client/sourceeditor/editor-commands-controller.js @@ -0,0 +1,97 @@ +/* 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/. */ + +"use strict"; + +/** + * The source editor exposes XUL commands that can be used when embedded in a XUL + * document. This controller drives the availability and behavior of the commands. When + * the editor input field is focused, this controller will update the matching menu item + * entries found in application menus or context menus. + */ + +/** + * Returns a controller object that can be used for editor-specific commands: + * - find + * - find again + * - go to line + * - undo + * - redo + * - delete + * - select all + */ +function createController(ed) { + return { + supportsCommand: function (cmd) { + switch (cmd) { + case "cmd_find": + case "cmd_findAgain": + case "cmd_gotoLine": + case "cmd_undo": + case "cmd_redo": + case "cmd_delete": + case "cmd_selectAll": + return true; + } + + return false; + }, + + isCommandEnabled: function (cmd) { + let cm = ed.codeMirror; + + switch (cmd) { + case "cmd_find": + case "cmd_gotoLine": + case "cmd_selectAll": + return true; + case "cmd_findAgain": + return cm.state.search != null && cm.state.search.query != null; + case "cmd_undo": + return ed.canUndo(); + case "cmd_redo": + return ed.canRedo(); + case "cmd_delete": + return ed.somethingSelected(); + } + + return false; + }, + + doCommand: function (cmd) { + let cm = ed.codeMirror; + + let map = { + "cmd_selectAll": "selectAll", + "cmd_find": "find", + "cmd_undo": "undo", + "cmd_redo": "redo", + "cmd_delete": "delCharAfter", + "cmd_findAgain": "findNext" + }; + + if (map[cmd]) { + cm.execCommand(map[cmd]); + return; + } + + if (cmd == "cmd_gotoLine") { + ed.jumpToLine(); + } + }, + + onEvent: function () {} + }; +} + +/** + * Create and insert a commands controller for the provided SourceEditor instance. + */ +function insertCommandsController(sourceEditor) { + let input = sourceEditor.codeMirror.getInputField(); + let controller = createController(sourceEditor); + input.controllers.insertControllerAt(0, controller); +} + +module.exports = { insertCommandsController }; diff --git a/devtools/client/sourceeditor/editor.js b/devtools/client/sourceeditor/editor.js index ce2136afc..1b3c1d31a 100644 --- a/devtools/client/sourceeditor/editor.js +++ b/devtools/client/sourceeditor/editor.js @@ -489,6 +489,16 @@ Editor.prototype = { }, /** + * The source editor can expose several commands linked from system and context menus. + * Kept for backward compatibility with scratchpad and styleeditor. + */ + insertCommandsController: function () { + const { insertCommandsController } = + require("devtools/client/sourceeditor/editor-commands-controller"); + insertCommandsController(this); + }, + + /** * Returns text from the text area. If line argument is provided * the method returns only that line. */ diff --git a/devtools/client/sourceeditor/moz.build b/devtools/client/sourceeditor/moz.build index 5081325c5..765accb14 100644 --- a/devtools/client/sourceeditor/moz.build +++ b/devtools/client/sourceeditor/moz.build @@ -12,6 +12,7 @@ DevToolsModules( 'autocomplete.js', 'css-autocompleter.js', 'debugger.js', + 'editor-commands-controller.js', 'editor.js' ) diff --git a/devtools/client/storage/storage.xul b/devtools/client/storage/storage.xul index 9fbef5199..fa006ac7c 100644 --- a/devtools/client/storage/storage.xul +++ b/devtools/client/storage/storage.xul @@ -26,13 +26,18 @@ <menupopup id="storage-tree-popup"> <menuitem id="storage-tree-popup-delete-all" label="&storage.popupMenu.deleteAllLabel;"/> + <menuitem id="storage-tree-popup-delete-all-session-cookies" + label="&storage.popupMenu.deleteAllSessionCookiesLabel;"/> <menuitem id="storage-tree-popup-delete"/> </menupopup> <menupopup id="storage-table-popup"> + <menuitem id="storage-table-popup-add"/> <menuitem id="storage-table-popup-delete"/> <menuitem id="storage-table-popup-delete-all-from"/> <menuitem id="storage-table-popup-delete-all" label="&storage.popupMenu.deleteAllLabel;"/> + <menuitem id="storage-table-popup-delete-all-session-cookies" + label="&storage.popupMenu.deleteAllSessionCookiesLabel;"/> </menupopup> </popupset> @@ -41,6 +46,11 @@ <splitter class="devtools-side-splitter"/> <vbox flex="1"> <hbox id="storage-toolbar" class="devtools-toolbar"> + <button id="add-button" + class="devtools-button add-button"></button> + <button id="refresh-button" + class="devtools-button refresh-button"></button> + <spacer flex="1"/> <textbox id="storage-searchbox" class="devtools-filterinput" type="search" diff --git a/devtools/client/storage/test/browser.ini b/devtools/client/storage/test/browser.ini index dd7f48bd7..0290aaa5e 100644 --- a/devtools/client/storage/test/browser.ini +++ b/devtools/client/storage/test/browser.ini @@ -7,7 +7,9 @@ support-files = storage-cookies.html storage-empty-objectstores.html storage-idb-delete-blocked.html + storage-indexeddb-duplicate-names.html storage-listings.html + storage-listings-with-fragment.html storage-localstorage.html storage-overflow.html storage-search.html @@ -19,25 +21,34 @@ support-files = !/devtools/client/framework/test/shared-head.js [browser_storage_basic.js] +[browser_storage_basic_with_fragment.js] [browser_storage_cache_delete.js] [browser_storage_cache_error.js] +[browser_storage_cookies_add.js] [browser_storage_cookies_delete_all.js] [browser_storage_cookies_domain.js] +[browser_storage_cookies_domain_port.js] [browser_storage_cookies_edit.js] [browser_storage_cookies_edit_keyboard.js] [browser_storage_cookies_tab_navigation.js] [browser_storage_delete.js] [browser_storage_delete_all.js] [browser_storage_delete_tree.js] -[browser_storage_dynamic_updates.js] +[browser_storage_dom_cache_disabled.js] +[browser_storage_dynamic_updates_cookies.js] +[browser_storage_dynamic_updates_localStorage.js] +[browser_storage_dynamic_updates_sessionStorage.js] [browser_storage_empty_objectstores.js] [browser_storage_indexeddb_delete.js] [browser_storage_indexeddb_delete_blocked.js] +[browser_storage_indexeddb_duplicate_names.js] +[browser_storage_localstorage_add.js] [browser_storage_localstorage_edit.js] [browser_storage_localstorage_error.js] [browser_storage_overflow.js] [browser_storage_search.js] [browser_storage_search_keyboard_trap.js] +[browser_storage_sessionstorage_add.js] [browser_storage_sessionstorage_edit.js] [browser_storage_sidebar.js] [browser_storage_sidebar_update.js] diff --git a/devtools/client/storage/test/browser_storage_basic.js b/devtools/client/storage/test/browser_storage_basic.js index 343d46170..35d08afce 100644 --- a/devtools/client/storage/test/browser_storage_basic.js +++ b/devtools/client/storage/test/browser_storage_basic.js @@ -2,6 +2,8 @@ * 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/. */ +/* import-globals-from head.js */ + // Basic test to assert that the storage tree and table corresponding to each // item in the storage tree is correctly displayed @@ -21,10 +23,30 @@ "use strict"; const testCases = [ - [["cookies", "test1.example.org"], - ["c1", "cs2", "c3", "uc1"]], - [["cookies", "sectest1.example.org"], - ["uc1", "cs2", "sc1"]], + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("c3", "test1.example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], + [ + ["cookies", "https://sectest1.example.org"], + [ + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("sc1", "sectest1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("sc2", "sectest1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]], [["localStorage", "http://sectest1.example.org"], @@ -38,28 +60,28 @@ const testCases = [ [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]], [["indexedDB", "http://test1.example.org"], - ["idb1", "idb2"]], - [["indexedDB", "http://test1.example.org", "idb1"], + ["idb1 (default)", "idb2 (default)"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)"], ["obj1", "obj2"]], - [["indexedDB", "http://test1.example.org", "idb2"], + [["indexedDB", "http://test1.example.org", "idb2 (default)"], ["obj3"]], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]], - [["indexedDB", "http://test1.example.org", "idb1", "obj2"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]], - [["indexedDB", "http://test1.example.org", "idb2", "obj3"], + [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], []], [["indexedDB", "http://sectest1.example.org"], []], [["indexedDB", "https://sectest1.example.org"], - ["idb-s1", "idb-s2"]], - [["indexedDB", "https://sectest1.example.org", "idb-s1"], + ["idb-s1 (default)", "idb-s2 (default)"]], + [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"], ["obj-s1"]], - [["indexedDB", "https://sectest1.example.org", "idb-s2"], + [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"], ["obj-s2"]], - [["indexedDB", "https://sectest1.example.org", "idb-s1", "obj-s1"], + [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"], [6, 7]], - [["indexedDB", "https://sectest1.example.org", "idb-s2", "obj-s2"], + [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"], [16]], [["Cache", "http://test1.example.org", "plop"], [MAIN_DOMAIN + "404_cached_file.js", @@ -71,8 +93,8 @@ const testCases = [ */ function testTree() { let doc = gPanelWindow.document; - for (let item of testCases) { - ok(doc.querySelector("[data-id='" + JSON.stringify(item[0]) + "']"), + for (let [item] of testCases) { + ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"), "Tree item " + item[0] + " should be present in the storage tree"); } } @@ -86,8 +108,8 @@ function* testTables() { gUI.tree.expandAll(); // First tree item is already selected so no clicking and waiting for update - for (let id of testCases[0][1]) { - ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), + for (let [treeItem, items] of testCases.slice(1)) { + yield selectTreeItem(treeItem); "Table item " + id + " should be present"); } @@ -98,10 +120,10 @@ function* testTables() { // Check whether correct number of items are present in the table is(doc.querySelectorAll( ".table-widget-wrapper:first-of-type .table-widget-cell" - ).length, item[1].length, "Number of items in table is correct"); + ).length, items.length, "Number of items in table is correct"); // Check if all the desired items are present in the table - for (let id of item[1]) { + for (let id of items) { ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), "Table item " + id + " should be present"); } diff --git a/devtools/client/storage/test/browser_storage_basic_with_fragment.js b/devtools/client/storage/test/browser_storage_basic_with_fragment.js new file mode 100644 index 000000000..7769781c0 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_basic_with_fragment.js @@ -0,0 +1,139 @@ +/* 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/. */ + +/* import-globals-from head.js */ + +// A second basic test to assert that the storage tree and table corresponding +// to each item in the storage tree is correctly displayed. + +// This test differs from browser_storage_basic.js because the URLs we load +// include fragments e.g. http://example.com/test.js#abcdefg +// ^^^^^^^^ +// fragment + +// Entries that should be present in the tree for this test +// Format for each entry in the array : +// [ +// ["path", "to", "tree", "item"], - The path to the tree item to click formed +// by id of each item +// ["key_value1", "key_value2", ...] - The value of the first (unique) column +// for each row in the table corresponding +// to the tree item selected. +// ] +// These entries are formed by the cookies, local storage, session storage and +// indexedDB entries created in storage-listings.html, +// storage-secured-iframe.html and storage-unsecured-iframe.html + +"use strict"; + +const testCases = [ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("c3", "test1.example.org", "/"), + getCookieId("uc1", ".example.org", "/") + ] + ], + [ + ["cookies", "https://sectest1.example.org"], + [ + getCookieId("uc1", ".example.org", "/"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("sc1", "sectest1.example.org", "/browser/devtools/client/storage/test/") + ] + ], + [["localStorage", "http://test1.example.org"], + ["ls1", "ls2"]], + [["localStorage", "http://sectest1.example.org"], + ["iframe-u-ls1"]], + [["localStorage", "https://sectest1.example.org"], + ["iframe-s-ls1"]], + [["sessionStorage", "http://test1.example.org"], + ["ss1"]], + [["sessionStorage", "http://sectest1.example.org"], + ["iframe-u-ss1", "iframe-u-ss2"]], + [["sessionStorage", "https://sectest1.example.org"], + ["iframe-s-ss1"]], + [["indexedDB", "http://test1.example.org"], + ["idb1 (default)", "idb2 (default)"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)"], + ["obj1", "obj2"]], + [["indexedDB", "http://test1.example.org", "idb2 (default)"], + ["obj3"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], + [1, 2, 3]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], + [1]], + [["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], + []], + [["indexedDB", "http://sectest1.example.org"], + []], + [["indexedDB", "https://sectest1.example.org"], + ["idb-s1 (default)", "idb-s2 (default)"]], + [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)"], + ["obj-s1"]], + [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"], + ["obj-s2"]], + [["indexedDB", "https://sectest1.example.org", "idb-s1 (default)", "obj-s1"], + [6, 7]], + [["indexedDB", "https://sectest1.example.org", "idb-s2 (default)", "obj-s2"], + [16]], + [["Cache", "http://test1.example.org", "plop"], + [MAIN_DOMAIN + "404_cached_file.js", + MAIN_DOMAIN + "browser_storage_basic.js"]], +]; + +/** + * Test that the desired number of tree items are present + */ +function testTree() { + let doc = gPanelWindow.document; + for (let [item] of testCases) { + ok(doc.querySelector("[data-id='" + JSON.stringify(item) + "']"), + "Tree item " + item[0] + " should be present in the storage tree"); + } +} + +/** + * Test that correct table entries are shown for each of the tree item + */ +function* testTables() { + let doc = gPanelWindow.document; + // Expand all nodes so that the synthesized click event actually works + gUI.tree.expandAll(); + + // First tree item is already selected so no clicking and waiting for update + for (let id of testCases[0][1]) { + ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), + "Table item " + id + " should be present"); + } + + // Click rest of the tree items and wait for the table to be updated + for (let [treeItem, items] of testCases.slice(1)) { + yield selectTreeItem(treeItem); + + // Check whether correct number of items are present in the table + is(doc.querySelectorAll( + ".table-widget-wrapper:first-of-type .table-widget-cell" + ).length, items.length, "Number of items in table is correct"); + + // Check if all the desired items are present in the table + for (let id of items) { + ok(doc.querySelector(".table-widget-cell[data-id='" + id + "']"), + "Table item " + id + " should be present"); + } + } +} + +add_task(function* () { + yield openTabAndSetupStorage( + MAIN_DOMAIN + "storage-listings-with-fragment.html#abc"); + + testTree(); + yield testTables(); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_cookies_add.js b/devtools/client/storage/test/browser_storage_cookies_add.js new file mode 100644 index 000000000..ac66eb92c --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_add.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/. */ + +// Basic test to check the adding of cookies. + +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); + showAllColumns(true); + + yield performAdd(["cookies", "http://test1.example.org"]); + yield performAdd(["cookies", "http://test1.example.org"]); + yield performAdd(["cookies", "http://test1.example.org"]); + yield performAdd(["cookies", "http://test1.example.org"]); + yield performAdd(["cookies", "http://test1.example.org"]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_cookies_delete_all.js b/devtools/client/storage/test/browser_storage_cookies_delete_all.js index 6e6008e66..f8e9bb288 100644 --- a/devtools/client/storage/test/browser_storage_cookies_delete_all.js +++ b/devtools/client/storage/test/browser_storage_cookies_delete_all.js @@ -8,11 +8,13 @@ // Test deleting all cookies -function* performDelete(store, rowName, deleteAll) { +function* performDelete(store, rowName, action) { let contextMenu = gPanelWindow.document.getElementById( "storage-table-popup"); let menuDeleteAllItem = contextMenu.querySelector( "#storage-table-popup-delete-all"); + let menuDeleteAllSessionCookiesItem = contextMenu.querySelector( + "#storage-table-popup-delete-all-session-cookies"); let menuDeleteAllFromItem = contextMenu.querySelector( "#storage-table-popup-delete-all-from"); @@ -21,17 +23,23 @@ function* performDelete(store, rowName, deleteAll) { yield selectTreeItem(store); let eventWait = gUI.once("store-objects-updated"); + let cells = getRowCells(rowName, true); - let cells = getRowCells(rowName); yield waitForContextMenu(contextMenu, cells.name, () => { info(`Opened context menu in ${storeName}, row '${rowName}'`); - if (deleteAll) { - menuDeleteAllItem.click(); - } else { - menuDeleteAllFromItem.click(); - let hostName = cells.host.value; - ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName), + switch (action) { + case "deleteAll": + menuDeleteAllItem.click(); + break; + case "deleteAllSessionCookies": + menuDeleteAllSessionCookiesItem.click(); + break; + case "deleteAllFrom": + menuDeleteAllFromItem.click(); + let hostName = cells.host.value; + ok(menuDeleteAllFromItem.getAttribute("label").includes(hostName), `Context menu item label contains '${hostName}'`); + break; } }); @@ -43,31 +51,101 @@ add_task(function* () { info("test state before delete"); yield checkState([ - [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]], - [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]], + [ + ["cookies", "http://test1.example.org"], [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("c3", "test1.example.org", "/"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], + [ + ["cookies", "https://sectest1.example.org"], [ + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("sc1", "sectest1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("sc2", "sectest1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], ]); info("delete all from domain"); // delete only cookies that match the host exactly - yield performDelete(["cookies", "test1.example.org"], "c1", false); + let id = getCookieId("c1", "test1.example.org", "/browser"); + yield performDelete(["cookies", "http://test1.example.org"], id, "deleteAllFrom"); info("test state after delete all from domain"); yield checkState([ // Domain cookies (.example.org) must not be deleted. - [["cookies", "test1.example.org"], ["cs2", "uc1"]], - [["cookies", "sectest1.example.org"], ["cs2", "sc1", "uc1"]], + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], + [ + ["cookies", "https://sectest1.example.org"], + [ + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/"), + getCookieId("sc1", "sectest1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("sc2", "sectest1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + + info("delete all session cookies"); + // delete only session cookies + id = getCookieId("cs2", ".example.org", "/"); + yield performDelete(["cookies", "sectest1.example.org"], id, + "deleteAllSessionCookies"); + + info("test state after delete all session cookies"); + yield checkState([ + // Cookies with expiry date must not be deleted. + [ + ["cookies", "test1.example.org"], + [ + getCookieId("c4", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], + [ + ["cookies", "sectest1.example.org"], + [ + getCookieId("c4", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/"), + getCookieId("sc2", "sectest1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], ]); info("delete all"); // delete all cookies for host, including domain cookies - yield performDelete(["cookies", "sectest1.example.org"], "uc1", true); + id = getCookieId("uc2", ".example.org", "/"); + yield performDelete(["cookies", "http://sectest1.example.org"], id, + "deleteAll"); info("test state after delete all"); yield checkState([ // Domain cookies (.example.org) are deleted too, so deleting in sectest1 // also removes stuff from test1. - [["cookies", "test1.example.org"], []], - [["cookies", "sectest1.example.org"], []], + [["cookies", "http://test1.example.org"], []], + [["cookies", "https://sectest1.example.org"], []], ]); yield finishTests(); diff --git a/devtools/client/storage/test/browser_storage_cookies_domain.js b/devtools/client/storage/test/browser_storage_cookies_domain.js index dc93d6e67..06f0a464d 100644 --- a/devtools/client/storage/test/browser_storage_cookies_domain.js +++ b/devtools/client/storage/test/browser_storage_cookies_domain.js @@ -13,8 +13,16 @@ add_task(function* () { yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); yield checkState([ - [["cookies", "test1.example.org"], - ["test1", "test2", "test3", "test4", "test5"]], + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("test1", ".test1.example.org", "/browser"), + getCookieId("test2", "test1.example.org", "/browser"), + getCookieId("test3", ".test1.example.org", "/browser"), + getCookieId("test4", "test1.example.org", "/browser"), + getCookieId("test5", ".test1.example.org", "/browser") + ] + ], ]); yield finishTests(); diff --git a/devtools/client/storage/test/browser_storage_cookies_domain_port.js b/devtools/client/storage/test/browser_storage_cookies_domain_port.js new file mode 100644 index 000000000..a5bc2e5be --- /dev/null +++ b/devtools/client/storage/test/browser_storage_cookies_domain_port.js @@ -0,0 +1,29 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test that cookies with domain equal to full host name and port are listed. +// E.g., ".example.org:8000" vs. example.org:8000). + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN_WITH_PORT + "storage-cookies.html"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org:8000"], + [ + getCookieId("test1", ".test1.example.org", "/browser"), + getCookieId("test2", "test1.example.org", "/browser"), + getCookieId("test3", ".test1.example.org", "/browser"), + getCookieId("test4", "test1.example.org", "/browser"), + getCookieId("test5", ".test1.example.org", "/browser") + ] + ], + ]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_cookies_edit.js b/devtools/client/storage/test/browser_storage_cookies_edit.js index 5818e4864..14944b398 100644 --- a/devtools/client/storage/test/browser_storage_cookies_edit.js +++ b/devtools/client/storage/test/browser_storage_cookies_edit.js @@ -10,13 +10,20 @@ add_task(function* () { yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); showAllColumns(true); - yield editCell("test3", "name", "newTest3"); - yield editCell("newTest3", "path", "/"); - yield editCell("newTest3", "host", "test1.example.org"); - yield editCell("newTest3", "expires", "Tue, 14 Feb 2040 17:41:14 GMT"); - yield editCell("newTest3", "value", "newValue3"); - yield editCell("newTest3", "isSecure", "true"); - yield editCell("newTest3", "isHttpOnly", "true"); + let id = getCookieId("test3", ".test1.example.org", "/browser"); + yield editCell(id, "name", "newTest3"); + + id = getCookieId("newTest3", ".test1.example.org", "/browser"); + yield editCell(id, "host", "test1.example.org"); + + id = getCookieId("newTest3", "test1.example.org", "/browser"); + yield editCell(id, "path", "/"); + + id = getCookieId("newTest3", "test1.example.org", "/"); + yield editCell(id, "expires", "Tue, 14 Feb 2040 17:41:14 GMT"); + yield editCell(id, "value", "newValue3"); + yield editCell(id, "isSecure", "true"); + yield editCell(id, "isHttpOnly", "true"); yield finishTests(); }); diff --git a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js index 1208c4376..4bbb63fbe 100644 --- a/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js +++ b/devtools/client/storage/test/browser_storage_cookies_edit_keyboard.js @@ -10,10 +10,11 @@ add_task(function* () { yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); showAllColumns(true); - yield startCellEdit("test4", "name"); + let id = getCookieId("test4", "test1.example.org", "/browser"); + yield startCellEdit(id, "name"); yield typeWithTerminator("test6", "VK_TAB"); - yield typeWithTerminator("/", "VK_TAB"); yield typeWithTerminator(".example.org", "VK_TAB"); + yield typeWithTerminator("/", "VK_TAB"); yield typeWithTerminator("Tue, 25 Dec 2040 12:00:00 GMT", "VK_TAB"); yield typeWithTerminator("test6value", "VK_TAB"); yield typeWithTerminator("false", "VK_TAB"); diff --git a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js index 783a0c844..5da359b8d 100644 --- a/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js +++ b/devtools/client/storage/test/browser_storage_cookies_tab_navigation.js @@ -10,7 +10,8 @@ add_task(function* () { yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-cookies.html"); showAllColumns(true); - yield startCellEdit("test1", "name"); + let id = getCookieId("test1", ".test1.example.org", "/browser"); + yield startCellEdit(id, "name"); PressKeyXTimes("VK_TAB", 18); is(getCurrentEditorValue(), "value3", diff --git a/devtools/client/storage/test/browser_storage_delete.js b/devtools/client/storage/test/browser_storage_delete.js index c0e2b0ad7..306c33d24 100644 --- a/devtools/client/storage/test/browser_storage_delete.js +++ b/devtools/client/storage/test/browser_storage_delete.js @@ -13,9 +13,11 @@ const TEST_CASES = [ "ls1", "name"], [["sessionStorage", "http://test1.example.org"], "ss1", "name"], - [["cookies", "test1.example.org"], - "c1", "name"], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [ + ["cookies", "http://test1.example.org"], + getCookieId("c1", "test1.example.org", "/browser"), "name" + ], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], 1, "name"], [["Cache", "http://test1.example.org", "plop"], MAIN_DOMAIN + "404_cached_file.js", "url"], @@ -41,7 +43,7 @@ add_task(function* () { yield waitForContextMenu(contextMenu, row[cellToClick], () => { info(`Opened context menu in ${treeItemName}, row '${rowName}'`); menuDeleteItem.click(); - let truncatedRowName = String(rowName).substr(0, 16); + let truncatedRowName = String(rowName).replace(SEPARATOR_GUID, "-").substr(0, 16); ok(menuDeleteItem.getAttribute("label").includes(truncatedRowName), `Context menu item label contains '${rowName}' (maybe truncated)`); }); diff --git a/devtools/client/storage/test/browser_storage_delete_all.js b/devtools/client/storage/test/browser_storage_delete_all.js index c4b6048fb..60b417bdb 100644 --- a/devtools/client/storage/test/browser_storage_delete_all.js +++ b/devtools/client/storage/test/browser_storage_delete_all.js @@ -29,7 +29,7 @@ add_task(function* () { ["iframe-u-ss1", "iframe-u-ss2"]], [["sessionStorage", "https://sectest1.example.org"], ["iframe-s-ss1"]], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]], [["Cache", "http://test1.example.org", "plop"], [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]], @@ -41,7 +41,7 @@ add_task(function* () { const deleteHosts = [ [["localStorage", "https://sectest1.example.org"], "iframe-s-ls1", "name"], [["sessionStorage", "https://sectest1.example.org"], "iframe-s-ss1", "name"], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], 1, "name"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], 1, "name"], [["Cache", "http://test1.example.org", "plop"], MAIN_DOMAIN + "404_cached_file.js", "url"], ]; @@ -78,7 +78,7 @@ add_task(function* () { ["iframe-u-ss1", "iframe-u-ss2"]], [["sessionStorage", "https://sectest1.example.org"], []], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []], [["Cache", "http://test1.example.org", "plop"], []], diff --git a/devtools/client/storage/test/browser_storage_delete_tree.js b/devtools/client/storage/test/browser_storage_delete_tree.js index 867a1c8b6..6705dba8a 100644 --- a/devtools/client/storage/test/browser_storage_delete_tree.js +++ b/devtools/client/storage/test/browser_storage_delete_tree.js @@ -17,20 +17,30 @@ add_task(function* () { info("test state before delete"); yield checkState([ - [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]], + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("cs2", ".example.org", "/"), + getCookieId("c3", "test1.example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], [["localStorage", "http://test1.example.org"], ["ls1", "ls2"]], [["sessionStorage", "http://test1.example.org"], ["ss1"]], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], [1, 2, 3]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]], [["Cache", "http://test1.example.org", "plop"], [MAIN_DOMAIN + "404_cached_file.js", MAIN_DOMAIN + "browser_storage_basic.js"]], ]); info("do the delete"); const deleteHosts = [ - ["cookies", "test1.example.org"], + ["cookies", "http://test1.example.org"], ["localStorage", "http://test1.example.org"], ["sessionStorage", "http://test1.example.org"], - ["indexedDB", "http://test1.example.org", "idb1", "obj1"], + ["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], ["Cache", "http://test1.example.org", "plop"], ]; @@ -56,10 +66,10 @@ add_task(function* () { info("test state after delete"); yield checkState([ - [["cookies", "test1.example.org"], []], + [["cookies", "http://test1.example.org"], []], [["localStorage", "http://test1.example.org"], []], [["sessionStorage", "http://test1.example.org"], []], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], []], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], []], [["Cache", "http://test1.example.org", "plop"], []], ]); diff --git a/devtools/client/storage/test/browser_storage_dom_cache_disabled.js b/devtools/client/storage/test/browser_storage_dom_cache_disabled.js new file mode 100644 index 000000000..db0aca392 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_dom_cache_disabled.js @@ -0,0 +1,37 @@ +/* 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/. */ + +/* import-globals-from ../../framework/test/shared-head.js */ + +"use strict"; + +// Test the storage inspector when dom.caches.enabled=false. + +add_task(function* () { + // Disable the DOM cache + Services.prefs.setBoolPref(DOM_CACHE, false); + + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-listings.html"); + + const state = [ + [["localStorage", "http://test1.example.org"], + ["ls1", "ls2"]], + [["localStorage", "http://sectest1.example.org"], + ["iframe-u-ls1"]], + [["localStorage", "https://sectest1.example.org"], + ["iframe-s-ls1"]], + [["sessionStorage", "http://test1.example.org"], + ["ss1"]], + [["sessionStorage", "http://sectest1.example.org"], + ["iframe-u-ss1", "iframe-u-ss2"]], + [["sessionStorage", "https://sectest1.example.org"], + ["iframe-s-ss1"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], + [1, 2, 3]], + ]; + + yield checkState(state); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js new file mode 100644 index 000000000..032e7b7b9 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_dynamic_updates_cookies.js @@ -0,0 +1,188 @@ +/* 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/. */ + +"use strict"; + +// Test dynamic updates in the storage inspector for cookies. + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html"); + + gUI.tree.expandAll(); + + ok(gUI.sidebar.hidden, "Sidebar is initially hidden"); + let c1id = getCookieId("c1", "test1.example.org", "/browser"); + yield selectTableItem(c1id); + + // test that value is something initially + let initialValue = [[ + {name: "c1", value: "1.2.3.4.5.6.7"}, + {name: "c1.Path", value: "/browser"} + ], [ + {name: "c1", value: "Array"}, + {name: "c1.0", value: "1"}, + {name: "c1.6", value: "7"} + ]]; + + // test that value is something initially + let finalValue = [[ + {name: "c1", value: '{"foo": 4,"bar":6}'}, + {name: "c1.Path", value: "/browser"} + ], [ + {name: "c1", value: "Object"}, + {name: "c1.foo", value: "4"}, + {name: "c1.bar", value: "6"} + ]]; + + // Check that sidebar shows correct initial value + yield findVariableViewProperties(initialValue[0], false); + + yield findVariableViewProperties(initialValue[1], true); + // Check if table shows correct initial value + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("c2", "test1.example.org", "/browser") + ] + ], + ]); + checkCell(c1id, "value", "1.2.3.4.5.6.7"); + + gWindow.addCookie("c1", '{"foo": 4,"bar":6}', "/browser"); + yield gUI.once("sidebar-updated"); + + yield findVariableViewProperties(finalValue[0], false); + yield findVariableViewProperties(finalValue[1], true); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("c2", "test1.example.org", "/browser") + ] + ], + ]); + checkCell(c1id, "value", '{"foo": 4,"bar":6}'); + + // Add a new entry + gWindow.addCookie("c3", "booyeah"); + + // Wait once for update and another time for value fetching + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("c2", "test1.example.org", "/browser"), + getCookieId("c3", "test1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + let c3id = getCookieId("c3", "test1.example.org", + "/browser/devtools/client/storage/test/"); + checkCell(c3id, "value", "booyeah"); + + // Add another + gWindow.addCookie("c4", "booyeah"); + + // Wait once for update and another time for value fetching + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c1", "test1.example.org", "/browser"), + getCookieId("c2", "test1.example.org", "/browser"), + getCookieId("c3", "test1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("c4", "test1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + let c4id = getCookieId("c4", "test1.example.org", + "/browser/devtools/client/storage/test/"); + checkCell(c4id, "value", "booyeah"); + + // Removing cookies + gWindow.removeCookie("c1", "/browser"); + + yield gUI.once("sidebar-updated"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c2", "test1.example.org", "/browser"), + getCookieId("c3", "test1.example.org", + "/browser/devtools/client/storage/test/"), + getCookieId("c4", "test1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + + ok(!gUI.sidebar.hidden, "Sidebar still visible for next row"); + + // Check if next element's value is visible in sidebar + yield findVariableViewProperties([{name: "c2", value: "foobar"}]); + + // Keep deleting till no rows + gWindow.removeCookie("c3"); + + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c2", "test1.example.org", "/browser"), + getCookieId("c4", "test1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + + // Check if next element's value is visible in sidebar + yield findVariableViewProperties([{name: "c2", value: "foobar"}]); + + gWindow.removeCookie("c2", "/browser"); + + yield gUI.once("sidebar-updated"); + + yield checkState([ + [ + ["cookies", "http://test1.example.org"], + [ + getCookieId("c4", "test1.example.org", + "/browser/devtools/client/storage/test/") + ] + ], + ]); + + // Check if next element's value is visible in sidebar + yield findVariableViewProperties([{name: "c4", value: "booyeah"}]); + + gWindow.removeCookie("c4"); + + yield gUI.once("store-objects-updated"); + + yield checkState([ + [["cookies", "http://test1.example.org"], [ ]], + ]); + + ok(gUI.sidebar.hidden, "Sidebar is hidden when no rows"); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js new file mode 100644 index 000000000..35912ce3a --- /dev/null +++ b/devtools/client/storage/test/browser_storage_dynamic_updates_localStorage.js @@ -0,0 +1,70 @@ +/* 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/. */ + +"use strict"; + +// Test dynamic updates in the storage inspector for localStorage. + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html"); + + gUI.tree.expandAll(); + + ok(gUI.sidebar.hidden, "Sidebar is initially hidden"); + + yield checkState([ + [ + ["localStorage", "http://test1.example.org"], + ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"] + ], + ]); + + gWindow.localStorage.removeItem("ls4"); + + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["localStorage", "http://test1.example.org"], + ["ls1", "ls2", "ls3", "ls5", "ls6", "ls7"] + ], + ]); + + gWindow.localStorage.setItem("ls4", "again"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["localStorage", "http://test1.example.org"], + ["ls1", "ls2", "ls3", "ls4", "ls5", "ls6", "ls7"] + ], + ]); + // Updating a row + gWindow.localStorage.setItem("ls2", "ls2-changed"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + checkCell("ls2", "value", "ls2-changed"); + + // Clearing items. Bug 1233497 makes it so that we can no longer yield + // CPOWs from Tasks. We work around this by calling clear via a ContentTask + // instead. + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + return Task.spawn(content.wrappedJSObject.clear); + }); + + yield gUI.once("store-objects-cleared"); + + yield checkState([ + [ + ["localStorage", "http://test1.example.org"], + [ ] + ], + ]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js new file mode 100644 index 000000000..8c2f2537e --- /dev/null +++ b/devtools/client/storage/test/browser_storage_dynamic_updates_sessionStorage.js @@ -0,0 +1,83 @@ +/* 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/. */ + +"use strict"; + +// Test dynamic updates in the storage inspector for sessionStorage. + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-updates.html"); + + gUI.tree.expandAll(); + + ok(gUI.sidebar.hidden, "Sidebar is initially hidden"); + yield checkState([ + [ + ["sessionStorage", "http://test1.example.org"], + ["ss1", "ss2", "ss3"] + ], + ]); + + gWindow.sessionStorage.setItem("ss4", "new-item"); + + yield gUI.once("store-objects-updated"); + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["sessionStorage", "http://test1.example.org"], + ["ss1", "ss2", "ss3", "ss4"] + ], + ]); + + // deleting item + + gWindow.sessionStorage.removeItem("ss3"); + + yield gUI.once("store-objects-updated"); + + gWindow.sessionStorage.removeItem("ss1"); + + yield gUI.once("store-objects-updated"); + + yield checkState([ + [ + ["sessionStorage", "http://test1.example.org"], + ["ss2", "ss4"] + ], + ]); + + yield selectTableItem("ss2"); + + ok(!gUI.sidebar.hidden, "sidebar is visible"); + + // Checking for correct value in sidebar before update + yield findVariableViewProperties([{name: "ss2", value: "foobar"}]); + + gWindow.sessionStorage.setItem("ss2", "changed=ss2"); + + yield gUI.once("sidebar-updated"); + + checkCell("ss2", "value", "changed=ss2"); + + yield findVariableViewProperties([{name: "ss2", value: "changed=ss2"}]); + + // Clearing items. Bug 1233497 makes it so that we can no longer yield + // CPOWs from Tasks. We work around this by calling clear via a ContentTask + // instead. + yield ContentTask.spawn(gBrowser.selectedBrowser, null, function* () { + return Task.spawn(content.wrappedJSObject.clear); + }); + + yield gUI.once("store-objects-cleared"); + + yield checkState([ + [ + ["sessionStorage", "http://test1.example.org"], + [ ] + ], + ]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_empty_objectstores.js b/devtools/client/storage/test/browser_storage_empty_objectstores.js index 1749c91b8..e6f259742 100644 --- a/devtools/client/storage/test/browser_storage_empty_objectstores.js +++ b/devtools/client/storage/test/browser_storage_empty_objectstores.js @@ -21,14 +21,14 @@ // storage-secured-iframe.html and storage-unsecured-iframe.html const storeItems = [ [["indexedDB", "http://test1.example.org"], - ["idb1", "idb2"]], - [["indexedDB", "http://test1.example.org", "idb1"], + ["idb1 (default)", "idb2 (default)"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)"], ["obj1", "obj2"]], - [["indexedDB", "http://test1.example.org", "idb2"], + [["indexedDB", "http://test1.example.org", "idb2 (default)"], []], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"], [1, 2, 3]], - [["indexedDB", "http://test1.example.org", "idb1", "obj2"], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"], [1]] ]; diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete.js b/devtools/client/storage/test/browser_storage_indexeddb_delete.js index 18a0daf69..5c499c9e9 100644 --- a/devtools/client/storage/test/browser_storage_indexeddb_delete.js +++ b/devtools/client/storage/test/browser_storage_indexeddb_delete.js @@ -16,11 +16,11 @@ add_task(function* () { info("test state before delete"); yield checkState([ - [["indexedDB", "http://test1.example.org"], ["idb1", "idb2"]], + [["indexedDB", "http://test1.example.org"], ["idb1 (default)", "idb2 (default)"]], ]); info("do the delete"); - const deletedDb = ["indexedDB", "http://test1.example.org", "idb1"]; + const deletedDb = ["indexedDB", "http://test1.example.org", "idb1 (default)"]; yield selectTreeItem(deletedDb); @@ -40,7 +40,7 @@ add_task(function* () { info("test state after delete"); yield checkState([ - [["indexedDB", "http://test1.example.org"], ["idb2"]], + [["indexedDB", "http://test1.example.org"], ["idb2 (default)"]], ]); yield finishTests(); diff --git a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js index 6e89c4f28..2d77896f3 100644 --- a/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js +++ b/devtools/client/storage/test/browser_storage_indexeddb_delete_blocked.js @@ -13,19 +13,19 @@ add_task(function* () { info("test state before delete"); yield checkState([ - [["indexedDB", "http://test1.example.org"], ["idb"]] + [["indexedDB", "http://test1.example.org"], ["idb (default)"]] ]); info("do the delete"); yield selectTreeItem(["indexedDB", "http://test1.example.org"]); - let actor = gUI.getCurrentActor(); - let result = yield actor.removeDatabase("http://test1.example.org", "idb"); + let front = gUI.getCurrentFront(); + let result = yield front.removeDatabase("http://test1.example.org", "idb (default)"); ok(result.blocked, "removeDatabase attempt is blocked"); info("test state after blocked delete"); yield checkState([ - [["indexedDB", "http://test1.example.org"], ["idb"]] + [["indexedDB", "http://test1.example.org"], ["idb (default)"]] ]); let eventWait = gUI.once("store-objects-updated"); @@ -47,7 +47,7 @@ add_task(function* () { info("try to delete database from nonexistent host"); let errorThrown = false; try { - result = yield actor.removeDatabase("http://test2.example.org", "idb"); + result = yield front.removeDatabase("http://test2.example.org", "idb (default)"); } catch (ex) { errorThrown = true; } diff --git a/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js new file mode 100644 index 000000000..8316d22c5 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_indexeddb_duplicate_names.js @@ -0,0 +1,31 @@ +/* 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/. */ + +// Test to verify that indexedDBs with duplicate names (different types / paths) +// work as expected. + +"use strict"; + +add_task(function* () { + const TESTPAGE = MAIN_DOMAIN + "storage-indexeddb-duplicate-names.html"; + + setPermission(TESTPAGE, "indexedDB"); + + yield openTabAndSetupStorage(TESTPAGE); + + yield checkState([ + [ + ["indexedDB", "http://test1.example.org"], [ + "idb1 (default)", + "idb1 (temporary)", + "idb1 (persistent)", + "idb2 (default)", + "idb2 (temporary)", + "idb2 (persistent)" + ] + ] + ]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_localstorage_add.js b/devtools/client/storage/test/browser_storage_localstorage_add.js new file mode 100644 index 000000000..de40957b8 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_localstorage_add.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/. */ + +// Basic test to check the adding of localStorage entries. + +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-localstorage.html"); + showAllColumns(true); + + yield performAdd(["localStorage", "http://test1.example.org"]); + yield performAdd(["localStorage", "http://test1.example.org"]); + yield performAdd(["localStorage", "http://test1.example.org"]); + yield performAdd(["localStorage", "http://test1.example.org"]); + yield performAdd(["localStorage", "http://test1.example.org"]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_overflow.js b/devtools/client/storage/test/browser_storage_overflow.js index 88181ca05..21b931c8e 100644 --- a/devtools/client/storage/test/browser_storage_overflow.js +++ b/devtools/client/storage/test/browser_storage_overflow.js @@ -2,40 +2,58 @@ // inspector table. "use strict"; +const ITEMS_PER_PAGE = 50; + add_task(function* () { yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-overflow.html"); - let $ = id => gPanelWindow.document.querySelector(id); - let $$ = sel => gPanelWindow.document.querySelectorAll(sel); - gUI.tree.expandAll(); yield selectTreeItem(["localStorage", "http://test1.example.org"]); + checkCellLength(ITEMS_PER_PAGE); + + yield scroll(); + checkCellLength(ITEMS_PER_PAGE * 2); - let table = $("#storage-table .table-widget-body"); - let cellHeight = $(".table-widget-cell").getBoundingClientRect().height; + yield scroll(); + checkCellLength(ITEMS_PER_PAGE * 3); - is($$("#value .table-widget-cell").length, 50, - "Table should initially display 50 items"); + // Check that the columns are sorted in a human readable way (ascending). + checkCellValues("ASC"); - let onStoresUpdate = gUI.once("store-objects-updated"); - table.scrollTop += cellHeight * 50; - yield onStoresUpdate; + // Sort descending. + clickColumnHeader("name"); - is($$("#value .table-widget-cell").length, 100, - "Table should display 100 items after scrolling"); + // Check that the columns are sorted in a human readable way (descending). + checkCellValues("DEC"); - onStoresUpdate = gUI.once("store-objects-updated"); - table.scrollTop += cellHeight * 50; - yield onStoresUpdate; + yield finishTests(); +}); - is($$("#value .table-widget-cell").length, 150, - "Table should display 150 items after scrolling"); +function checkCellLength(len) { + let cells = gPanelWindow.document + .querySelectorAll("#name .table-widget-cell"); + let msg = `Table should initially display ${len} items`; - onStoresUpdate = gUI.once("store-objects-updated"); + is(cells.length, len, msg); +} + +function checkCellValues(order) { + let cells = [...gPanelWindow.document + .querySelectorAll("#name .table-widget-cell")]; + cells.forEach(function (cell, index, arr) { + let i = order === "ASC" ? index + 1 : arr.length - index; + is(cell.value, `item-${i}`, `Cell value is correct (${order}).`); + }); +} + +function* scroll() { + let $ = id => gPanelWindow.document.querySelector(id); + + let table = $("#storage-table .table-widget-body"); + let cell = $("#name .table-widget-cell"); + let cellHeight = cell.getBoundingClientRect().height; + + let onStoresUpdate = gUI.once("store-objects-updated"); table.scrollTop += cellHeight * 50; yield onStoresUpdate; - - is($$("#value .table-widget-cell").length, 160, - "Table should display all 160 items after scrolling"); - yield finishTests(); -}); +} diff --git a/devtools/client/storage/test/browser_storage_sessionstorage_add.js b/devtools/client/storage/test/browser_storage_sessionstorage_add.js new file mode 100644 index 000000000..8f220bc81 --- /dev/null +++ b/devtools/client/storage/test/browser_storage_sessionstorage_add.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/. */ + +// Basic test to check the adding of sessionStorage entries. + +"use strict"; + +add_task(function* () { + yield openTabAndSetupStorage(MAIN_DOMAIN + "storage-sessionstorage.html"); + showAllColumns(true); + + yield performAdd(["sessionStorage", "http://test1.example.org"]); + yield performAdd(["sessionStorage", "http://test1.example.org"]); + yield performAdd(["sessionStorage", "http://test1.example.org"]); + yield performAdd(["sessionStorage", "http://test1.example.org"]); + yield performAdd(["sessionStorage", "http://test1.example.org"]); + + yield finishTests(); +}); diff --git a/devtools/client/storage/test/browser_storage_sidebar.js b/devtools/client/storage/test/browser_storage_sidebar.js index 9b60026a0..ed8a333e2 100644 --- a/devtools/client/storage/test/browser_storage_sidebar.js +++ b/devtools/client/storage/test/browser_storage_sidebar.js @@ -16,26 +16,26 @@ const testCases = [ { - location: ["cookies", "sectest1.example.org"], + location: ["cookies", "https://sectest1.example.org"], sidebarHidden: true }, { - location: "cs2", + location: getCookieId("cs2", ".example.org", "/"), sidebarHidden: false }, { sendEscape: true }, { - location: "cs2", + location: getCookieId("cs2", ".example.org", "/"), sidebarHidden: false }, { - location: "uc1", + location: getCookieId("uc1", ".example.org", "/"), sidebarHidden: false }, { - location: "uc1", + location: getCookieId("uc1", ".example.org", "/"), sidebarHidden: false }, @@ -72,17 +72,17 @@ const testCases = [ sidebarHidden: true }, { - location: "idb2", + location: "idb2 (default)", sidebarHidden: false }, { - location: ["indexedDB", "http://test1.example.org", "idb2", "obj3"], + location: ["indexedDB", "http://test1.example.org", "idb2 (default)", "obj3"], sidebarHidden: true }, { - location: ["indexedDB", "https://sectest1.example.org", "idb-s2"], + location: ["indexedDB", "https://sectest1.example.org", "idb-s2 (default)"], sidebarHidden: true }, { diff --git a/devtools/client/storage/test/browser_storage_sidebar_update.js b/devtools/client/storage/test/browser_storage_sidebar_update.js index 419d63020..92547815a 100644 --- a/devtools/client/storage/test/browser_storage_sidebar_update.js +++ b/devtools/client/storage/test/browser_storage_sidebar_update.js @@ -26,7 +26,7 @@ add_task(function* () { for (let i = 0; i < UPDATE_COUNT; i++) { info(`Performing update #${i}`); updates.push(gUI.once("sidebar-updated")); - gUI.displayObjectSidebar(); + gUI.updateObjectSidebar(); } yield promise.all(updates); diff --git a/devtools/client/storage/test/browser_storage_values.js b/devtools/client/storage/test/browser_storage_values.js index 920ce350e..1d3e9ff76 100644 --- a/devtools/client/storage/test/browser_storage_values.js +++ b/devtools/client/storage/test/browser_storage_values.js @@ -17,7 +17,7 @@ const LONG_WORD = "a".repeat(1000); const testCases = [ - ["cs2", [ + [getCookieId("cs2", ".example.org", "/"), [ {name: "cs2", value: "sessionCookie"}, {name: "cs2.Path", value: "/"}, {name: "cs2.HostOnly", value: "false"}, @@ -26,7 +26,7 @@ const testCases = [ {name: "cs2.Expires", value: "Session"}, {name: "cs2.Secure", value: "false"}, ]], - ["c1", [ + [getCookieId("c1", "test1.example.org", "/browser"), [ {name: "c1", value: JSON.stringify(["foo", "Bar", {foo: "Bar"}])}, {name: "c1.Path", value: "/browser"}, {name: "c1.HostOnly", value: "true"}, @@ -42,9 +42,13 @@ const testCases = [ {name: "c1.2", value: "Object"}, {name: "c1.2.foo", value: "Bar"}, ], true], - ["c_encoded", [ - {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))} - ]], + [ + getCookieId("c_encoded", "test1.example.org", + "/browser/devtools/client/storage/test/"), + [ + {name: "c_encoded", value: encodeURIComponent(JSON.stringify({foo: {foo1: "bar"}}))} + ] + ], [null, [ {name: "c_encoded", value: "Object"}, {name: "c_encoded.foo", value: "Object"}, @@ -120,7 +124,7 @@ const testCases = [ {name: "ss5.3", value: `${LONG_WORD}&${LONG_WORD}`}, {name: "ss5.4", value: `${LONG_WORD}&${LONG_WORD}`}, ], true], - [["indexedDB", "http://test1.example.org", "idb1", "obj1"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj1"]], [1, [ {name: 1, value: JSON.stringify({id: 1, name: "foo", email: "foo@bar.com"})} ]], @@ -129,7 +133,7 @@ const testCases = [ {name: "1.name", value: "foo"}, {name: "1.email", value: "foo@bar.com"}, ], true], - [["indexedDB", "http://test1.example.org", "idb1", "obj2"]], + [["indexedDB", "http://test1.example.org", "idb1 (default)", "obj2"]], [1, [ {name: 1, value: JSON.stringify({ id2: 1, name: "foo", email: "foo@bar.com", extra: "baz" diff --git a/devtools/client/storage/test/head.js b/devtools/client/storage/test/head.js index 9662393cf..c734f7b8f 100644 --- a/devtools/client/storage/test/head.js +++ b/devtools/client/storage/test/head.js @@ -15,15 +15,22 @@ Services.scriptloader.loadSubScript( const {TableWidget} = require("devtools/client/shared/widgets/TableWidget"); const SPLIT_CONSOLE_PREF = "devtools.toolbox.splitconsoleEnabled"; const STORAGE_PREF = "devtools.storage.enabled"; +const DOM_CACHE = "dom.caches.enabled"; const DUMPEMIT_PREF = "devtools.dump.emit"; const DEBUGGERLOG_PREF = "devtools.debugger.log"; // Allows Cache API to be working on usage `http` test page const CACHES_ON_HTTP_PREF = "dom.caches.testing.enabled"; const PATH = "browser/devtools/client/storage/test/"; const MAIN_DOMAIN = "http://test1.example.org/" + PATH; +const MAIN_DOMAIN_WITH_PORT = "http://test1.example.org:8000/" + PATH; const ALT_DOMAIN = "http://sectest1.example.org/" + PATH; const ALT_DOMAIN_SECURED = "https://sectest1.example.org:443/" + PATH; +// GUID to be used as a separator in compound keys. This must match the same +// constant in devtools/server/actors/storage.js, +// devtools/client/storage/ui.js and devtools/server/tests/browser/head.js +const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; + var gToolbox, gPanelWindow, gWindow, gUI; // Services.prefs.setBoolPref(DUMPEMIT_PREF, true); @@ -33,11 +40,12 @@ Services.prefs.setBoolPref(STORAGE_PREF, true); Services.prefs.setBoolPref(CACHES_ON_HTTP_PREF, true); registerCleanupFunction(() => { gToolbox = gPanelWindow = gWindow = gUI = null; - Services.prefs.clearUserPref(STORAGE_PREF); - Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF); - Services.prefs.clearUserPref(DUMPEMIT_PREF); - Services.prefs.clearUserPref(DEBUGGERLOG_PREF); Services.prefs.clearUserPref(CACHES_ON_HTTP_PREF); + Services.prefs.clearUserPref(DEBUGGERLOG_PREF); + Services.prefs.clearUserPref(DOM_CACHE); + Services.prefs.clearUserPref(DUMPEMIT_PREF); + Services.prefs.clearUserPref(SPLIT_CONSOLE_PREF); + Services.prefs.clearUserPref(STORAGE_PREF); }); /** @@ -505,10 +513,16 @@ function* selectTreeItem(ids) { * The id of the row in the table widget */ function* selectTableItem(id) { - let selector = ".table-widget-cell[data-id='" + id + "']"; + let table = gUI.table; + let selector = ".table-widget-column#" + table.uniqueId + + " .table-widget-cell[value='" + id + "']"; let target = gPanelWindow.document.querySelector(selector); ok(target, "table item found with ids " + id); + if (!target) { + showAvailableIds(); + } + yield click(target); yield gUI.once("sidebar-updated"); } @@ -586,22 +600,39 @@ function getRowCells(id, includeHidden = false) { if (!item) { ok(false, "Row id '" + id + "' exists"); + + showAvailableIds(); } - let index = table.columns.get(table.uniqueId).visibleCellNodes.indexOf(item); + let index = table.columns.get(table.uniqueId).cellNodes.indexOf(item); let cells = {}; for (let [name, column] of [...table.columns]) { if (!includeHidden && column.column.parentNode.hidden) { continue; } - cells[name] = column.visibleCellNodes[index]; + cells[name] = column.cellNodes[index]; } return cells; } /** + * Show available ids. + */ +function showAvailableIds() { + let doc = gPanelWindow.document; + let table = gUI.table; + + info("Available ids:"); + let cells = doc.querySelectorAll(".table-widget-column#" + table.uniqueId + + " .table-widget-cell"); + for (let cell of cells) { + info(" - " + cell.getAttribute("value")); + } +} + +/** * Get a cell value. * * @param {String} id @@ -704,6 +735,20 @@ function showColumn(id, state) { } /** + * Toggle sort direction on a column by clicking on the column header. + * + * @param {String} id + * The uniqueId of the given column. + */ +function clickColumnHeader(id) { + let columns = gUI.table.columns; + let column = columns.get(id); + let header = column.header; + + header.click(); +} + +/** * Show or hide all columns. * * @param {Boolean} state @@ -798,9 +843,18 @@ function* checkState(state) { is(items.size, names.length, `There is correct number of rows in ${storeName}`); + + if (names.length === 0) { + showAvailableIds(); + } + for (let name of names) { ok(items.has(name), `There is item with name '${name}' in ${storeName}`); + + if (!items.has(name)) { + showAvailableIds(); + } } } } @@ -838,3 +892,59 @@ var focusSearchBoxUsingShortcut = Task.async(function* (panelWin, callback) { callback(); } }); + +function getCookieId(name, domain, path) { + return `${name}${SEPARATOR_GUID}${domain}${SEPARATOR_GUID}${path}`; +} + +function setPermission(url, permission) { + const nsIPermissionManager = Components.interfaces.nsIPermissionManager; + + let uri = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService) + .newURI(url, null, null); + let ssm = Components.classes["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + let principal = ssm.createCodebasePrincipal(uri, {}); + + Components.classes["@mozilla.org/permissionmanager;1"] + .getService(nsIPermissionManager) + .addFromPrincipal(principal, permission, + nsIPermissionManager.ALLOW_ACTION); +} + +/** + * Add an item. + * @param {Array} store + * An array containing the path to the store to which we wish to add an + * item. + */ +function* performAdd(store) { + let storeName = store.join(" > "); + let toolbar = gPanelWindow.document.getElementById("storage-toolbar"); + let type = store[0]; + + yield selectTreeItem(store); + + let menuAdd = toolbar.querySelector( + "#add-button"); + + if (menuAdd.hidden) { + is(menuAdd.hidden, false, + `performAdd called for ${storeName} but it is not supported`); + return; + } + + let eventEdit = gUI.table.once("row-edit"); + let eventWait = gUI.once("store-objects-updated"); + + menuAdd.click(); + + let rowId = yield eventEdit; + yield eventWait; + + let key = type === "cookies" ? "uniqueKey" : "name"; + let value = getCellValue(rowId, key); + + is(rowId, value, `Row '${rowId}' was successfully added.`); +} diff --git a/devtools/client/storage/test/storage-indexeddb-duplicate-names.html b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html new file mode 100644 index 000000000..d8c76dc2a --- /dev/null +++ b/devtools/client/storage/test/storage-indexeddb-duplicate-names.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta charset="utf-8"> + <title>Storage inspector IndexedDBs with duplicate names</title> + + <script type="application/javascript;version=1.7"> + "use strict"; + + function createIndexedDBs() { + createIndexedDB("idb1", "temporary"); + createIndexedDB("idb1", "default"); + createIndexedDB("idb1", "persistent"); + createIndexedDB("idb2", "temporary"); + createIndexedDB("idb2", "default"); + createIndexedDB("idb2", "persistent"); + } + + function createIndexedDB(name, storage) { + let open = indexedDB.open(name, {storage: storage}); + + open.onsuccess = function () { + let db = open.result; + db.close(); + }; + } + + function deleteDB(dbName, storage) { + return new Promise(resolve => { + dump(`removing database ${dbName} (${storage}) from ${document.location}\n`); + indexedDB.deleteDatabase(dbName, { storage: storage }).onsuccess = resolve; + }); + } + + window.clear = function* () { + yield deleteDB("idb1", "temporary"); + yield deleteDB("idb1", "default"); + yield deleteDB("idb1", "persistent"); + yield deleteDB("idb2", "temporary"); + yield deleteDB("idb2", "default"); + yield deleteDB("idb2", "persistent"); + + dump(`removed indexedDB data from ${document.location}\n`); + }; + </script> +</head> +<body onload="createIndexedDBs()"> + <h1>storage-indexeddb-duplicate-names.html</h1> +</body> +</html> diff --git a/devtools/client/storage/test/storage-listings-with-fragment.html b/devtools/client/storage/test/storage-listings-with-fragment.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/devtools/client/storage/test/storage-listings-with-fragment.html diff --git a/devtools/client/storage/test/storage-listings.html b/devtools/client/storage/test/storage-listings.html index de3054d3a..385d33193 100644 --- a/devtools/client/storage/test/storage-listings.html +++ b/devtools/client/storage/test/storage-listings.html @@ -1,7 +1,7 @@ -<!DOCTYPE HTML> +<!DOCTYPE HTML> <html> <!-- -Bug 970517 - Storage inspector front end - tests +Storage inspector front end - tests --> <head> <meta charset="utf-8"> @@ -20,7 +20,10 @@ document.cookie = "c1=foobar; expires=" + new Date(cookieExpiresTime1).toGMTString() + "; path=/browser"; document.cookie = "cs2=sessionCookie; path=/; domain=" + partialHostname; document.cookie = "c3=foobar-2; expires=" + - new Date(cookieExpiresTime2).toGMTString() + "; path=/"; + new Date(cookieExpiresTime1).toGMTString() + "; path=/"; +document.cookie = "c4=foobar-3; expires=" + + new Date(cookieExpiresTime2).toGMTString() + "; path=/; domain=" + + partialHostname; // ... and some local storage items .. localStorage.setItem("ls1", "foobar"); localStorage.setItem("ls2", "foobar-2"); @@ -110,14 +113,19 @@ let cacheGenerator = function*() { window.setup = function*() { yield idbGenerator(); - yield cacheGenerator(); + + if (window.caches) { + yield cacheGenerator(); + } }; window.clear = function*() { yield deleteDB("idb1"); yield deleteDB("idb2"); - yield caches.delete("plop"); + if (window.caches) { + yield caches.delete("plop"); + } dump("removed indexedDB and cache data from " + document.location + "\n"); }; diff --git a/devtools/client/storage/test/storage-overflow.html b/devtools/client/storage/test/storage-overflow.html index ee8db36e6..6309046b3 100644 --- a/devtools/client/storage/test/storage-overflow.html +++ b/devtools/client/storage/test/storage-overflow.html @@ -11,7 +11,7 @@ Bug 1171903 - Storage Inspector endless scrolling <script type="text/javascript;version=1.8"> "use strict"; -for (let i = 0; i < 160; i++) { +for (let i = 1; i < 151; i++) { localStorage.setItem(`item-${i}`, `value-${i}`); } </script> diff --git a/devtools/client/storage/test/storage-secured-iframe.html b/devtools/client/storage/test/storage-secured-iframe.html index 8424fd4cd..9e1ef60a0 100644 --- a/devtools/client/storage/test/storage-secured-iframe.html +++ b/devtools/client/storage/test/storage-secured-iframe.html @@ -1,4 +1,4 @@ -<!DOCTYPE HTML> +<!DOCTYPE HTML> <html> <!-- Iframe for testing multiple host detetion in storage actor @@ -9,7 +9,10 @@ Iframe for testing multiple host detetion in storage actor <body> <script type="application/javascript;version=1.7"> "use strict"; +let cookieExpiresTime = 2000000000000; document.cookie = "sc1=foobar;"; +document.cookie = "sc2=foobar-2; expires=" + + new Date(cookieExpiresTime).toGMTString() + ";"; localStorage.setItem("iframe-s-ls1", "foobar"); sessionStorage.setItem("iframe-s-ss1", "foobar-2"); dump("added cookies and storage from secured iframe\n"); diff --git a/devtools/client/storage/test/storage-unsecured-iframe.html b/devtools/client/storage/test/storage-unsecured-iframe.html index a69ffdfd1..cd08a6164 100644 --- a/devtools/client/storage/test/storage-unsecured-iframe.html +++ b/devtools/client/storage/test/storage-unsecured-iframe.html @@ -9,7 +9,10 @@ Iframe for testing multiple host detetion in storage actor <body> <script> "use strict"; +let cookieExpiresTime = 2000000000000; document.cookie = "uc1=foobar; domain=.example.org; path=/"; +document.cookie = "uc2=foobar-2; expires=" + + new Date(cookieExpiresTime).toGMTString() + "; path=/; domain=.example.org"; localStorage.setItem("iframe-u-ls1", "foobar"); sessionStorage.setItem("iframe-u-ss1", "foobar1"); sessionStorage.setItem("iframe-u-ss2", "foobar2"); diff --git a/devtools/client/storage/test/storage-updates.html b/devtools/client/storage/test/storage-updates.html index a009814b2..341992f61 100644 --- a/devtools/client/storage/test/storage-updates.html +++ b/devtools/client/storage/test/storage-updates.html @@ -38,8 +38,10 @@ window.removeCookie = function(name, path) { * can be tested. */ window.clear = function*() { - sessionStorage.clear(); + localStorage.clear(); + dump("removed localStorage from " + document.location + "\n"); + sessionStorage.clear(); dump("removed sessionStorage from " + document.location + "\n"); }; diff --git a/devtools/client/storage/ui.js b/devtools/client/storage/ui.js index 6af493e44..7745c8da9 100644 --- a/devtools/client/storage/ui.js +++ b/devtools/client/storage/ui.js @@ -12,6 +12,12 @@ const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts"); const JSOL = require("devtools/client/shared/vendor/jsol"); const {KeyCodes} = require("devtools/client/shared/keycodes"); +// GUID to be used as a separator in compound keys. This must match the same +// constant in devtools/server/actors/storage.js, +// devtools/client/storage/test/head.js and +// devtools/server/tests/browser/head.js +const SEPARATOR_GUID = "{9d414cc5-8319-0a04-0586-c0a6ae01670a}"; + loader.lazyRequireGetter(this, "TreeWidget", "devtools/client/shared/widgets/TreeWidget", true); loader.lazyRequireGetter(this, "TableWidget", @@ -36,13 +42,6 @@ const GENERIC_VARIABLES_VIEW_SETTINGS = { preventDescriptorModifiers: true }; -// Columns which are hidden by default in the storage table -const HIDDEN_COLUMNS = [ - "creationTime", - "isDomain", - "isSecure" -]; - const REASON = { NEW_ROW: "new-row", NEXT_50_ITEMS: "next-50-items", @@ -114,8 +113,8 @@ function StorageUI(front, target, panelWin, toolbox) { cellContextMenuId: "storage-table-popup" }); - this.displayObjectSidebar = this.displayObjectSidebar.bind(this); - this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + this.updateObjectSidebar = this.updateObjectSidebar.bind(this); + this.table.on(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar); this.handleScrollEnd = this.handleScrollEnd.bind(this); this.table.on(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); @@ -161,11 +160,24 @@ function StorageUI(front, target, panelWin, toolbox) { this._tablePopup = this._panelDoc.getElementById("storage-table-popup"); this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing); + this.onRefreshTable = this.onRefreshTable.bind(this); + this.onAddItem = this.onAddItem.bind(this); this.onRemoveItem = this.onRemoveItem.bind(this); this.onRemoveAllFrom = this.onRemoveAllFrom.bind(this); this.onRemoveAll = this.onRemoveAll.bind(this); + this.onRemoveAllSessionCookies = this.onRemoveAllSessionCookies.bind(this); this.onRemoveTreeItem = this.onRemoveTreeItem.bind(this); + this._refreshButton = this._panelDoc.getElementById("refresh-button"); + this._refreshButton.addEventListener("command", this.onRefreshTable); + + this._addButton = this._panelDoc.getElementById("add-button"); + this._addButton.addEventListener("command", this.onAddItem); + + this._tablePopupAddItem = this._panelDoc.getElementById( + "storage-table-popup-add"); + this._tablePopupAddItem.addEventListener("command", this.onAddItem); + this._tablePopupDelete = this._panelDoc.getElementById( "storage-table-popup-delete"); this._tablePopupDelete.addEventListener("command", this.onRemoveItem); @@ -179,10 +191,20 @@ function StorageUI(front, target, panelWin, toolbox) { "storage-table-popup-delete-all"); this._tablePopupDeleteAll.addEventListener("command", this.onRemoveAll); + this._tablePopupDeleteAllSessionCookies = this._panelDoc.getElementById( + "storage-table-popup-delete-all-session-cookies"); + this._tablePopupDeleteAllSessionCookies.addEventListener("command", + this.onRemoveAllSessionCookies); + this._treePopupDeleteAll = this._panelDoc.getElementById( "storage-tree-popup-delete-all"); this._treePopupDeleteAll.addEventListener("command", this.onRemoveAll); + this._treePopupDeleteAllSessionCookies = this._panelDoc.getElementById( + "storage-tree-popup-delete-all-session-cookies"); + this._treePopupDeleteAllSessionCookies.addEventListener("command", + this.onRemoveAllSessionCookies); + this._treePopupDelete = this._panelDoc.getElementById("storage-tree-popup-delete"); this._treePopupDelete.addEventListener("command", this.onRemoveTreeItem); } @@ -199,7 +221,7 @@ StorageUI.prototype = { }, destroy: function () { - this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.displayObjectSidebar); + this.table.off(TableWidget.EVENTS.ROW_SELECTED, this.updateObjectSidebar); this.table.off(TableWidget.EVENTS.SCROLL_END, this.handleScrollEnd); this.table.off(TableWidget.EVENTS.CELL_EDIT, this.editItem); this.table.destroy(); @@ -211,13 +233,20 @@ StorageUI.prototype = { this.searchBox = null; this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing); + this._refreshButton.removeEventListener("command", this.onRefreshTable); + this._addButton.removeEventListener("command", this.onAddItem); + this._tablePopupAddItem.removeEventListener("command", this.onAddItem); this._treePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + this._treePopupDeleteAllSessionCookies.removeEventListener("command", + this.onRemoveAllSessionCookies); this._treePopupDelete.removeEventListener("command", this.onRemoveTreeItem); this._tablePopup.removeEventListener("popupshowing", this.onTablePopupShowing); this._tablePopupDelete.removeEventListener("command", this.onRemoveItem); this._tablePopupDeleteAllFrom.removeEventListener("command", this.onRemoveAllFrom); this._tablePopupDeleteAll.removeEventListener("command", this.onRemoveAll); + this._tablePopupDeleteAllSessionCookies.removeEventListener("command", + this.onRemoveAllSessionCookies); }, /** @@ -229,7 +258,7 @@ StorageUI.prototype = { this.table.clearSelection(); }, - getCurrentActor: function () { + getCurrentFront: function () { let type = this.table.datatype; return this.storageTypes[type]; @@ -250,9 +279,9 @@ StorageUI.prototype = { }, editItem: function (eventType, data) { - let actor = this.getCurrentActor(); + let front = this.getCurrentFront(); - actor.editItem(data); + front.editItem(data); }, /** @@ -261,17 +290,16 @@ StorageUI.prototype = { * being removed was selected. */ removeItemFromTable: function (name) { - if (this.table.isSelected(name)) { + if (this.table.isSelected(name) && this.table.items.size > 1) { if (this.table.selectedIndex == 0) { this.table.selectNextRow(); } else { this.table.selectPreviousRow(); } - this.table.remove(name); - this.displayObjectSidebar(); - } else { - this.table.remove(name); } + + this.table.remove(name); + this.updateObjectSidebar(); }, /** @@ -454,22 +482,24 @@ StorageUI.prototype = { * @param {object} See onUpdate docs */ handleChangedItems: function (changed) { - let [type, host, db, objectStore] = this.tree.selectedItem; - if (!changed[type] || !changed[type][host] || - changed[type][host].length == 0) { - return; - } - try { - let toUpdate = []; - for (let name of changed[type][host]) { - let names = JSON.parse(name); - if (names[0] == db && names[1] == objectStore && names[2]) { - toUpdate.push(name); + if (this.tree.selectedItem) { + let [type, host, db, objectStore] = this.tree.selectedItem; + if (!changed[type] || !changed[type][host] || + changed[type][host].length == 0) { + return; + } + try { + let toUpdate = []; + for (let name of changed[type][host]) { + let names = JSON.parse(name); + if (names[0] == db && names[1] == objectStore && names[2]) { + toUpdate.push(name); + } } + this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE); + } catch (ex) { + this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE); } - this.fetchStorageObjects(type, host, toUpdate, REASON.UPDATE); - } catch (ex) { - this.fetchStorageObjects(type, host, changed[type][host], REASON.UPDATE); } }, @@ -504,7 +534,7 @@ StorageUI.prototype = { // The indexedDB type could have sub-type data to fetch. // If having names specified, then it means // we are fetching details of specific database or of object store. - if (type == "indexedDB" && names) { + if (type === "indexedDB" && names) { let [ dbName, objectStoreName ] = JSON.parse(names[0]); if (dbName) { subType = "database"; @@ -513,6 +543,15 @@ StorageUI.prototype = { subType = "object store"; } } + + this.actorSupportsAddItem = yield this._target.actorHasMethod(type, "addItem"); + this.actorSupportsRemoveItem = + yield this._target.actorHasMethod(type, "removeItem"); + this.actorSupportsRemoveAll = + yield this._target.actorHasMethod(type, "removeAll"); + this.actorSupportsRemoveAllSessionCookies = + yield this._target.actorHasMethod(type, "removeAllSessionCookies"); + yield this.resetColumns(type, host, subType); } @@ -520,6 +559,7 @@ StorageUI.prototype = { if (data.length) { this.populateTable(data, reason); } + yield this.updateToolbar(); this.emit("store-objects-updated"); } catch (ex) { console.error(ex); @@ -527,6 +567,27 @@ StorageUI.prototype = { }), /** + * Updates the toolbar hiding and showing buttons as appropriate. + */ + updateToolbar: Task.async(function* () { + let item = this.tree.selectedItem; + let howManyNodesIn = item ? item.length : 0; + + // The first node is just a title e.g. "Cookies" so we need to be at least + // 2 nodes in to show the add button. + let canAdd = this.actorSupportsAddItem && howManyNodesIn > 1; + + if (canAdd) { + this._addButton.hidden = false; + this._addButton.setAttribute("tooltiptext", + L10N.getFormatStr("storage.popupMenu.addItemLabel")); + } else { + this._addButton.hidden = true; + this._addButton.removeAttribute("tooltiptext"); + } + }), + + /** * Populates the storage tree which displays the list of storages present for * the page. * @@ -574,23 +635,28 @@ StorageUI.prototype = { }, /** - * Populates the selected entry from teh table in the sidebar for a more + * Populates the selected entry from the table in the sidebar for a more * detailed view. */ - displayObjectSidebar: Task.async(function* () { + updateObjectSidebar: Task.async(function* () { let item = this.table.selectedRow; - if (!item) { - // Make sure that sidebar is hidden and return - this.sidebar.hidden = true; - return; - } + let value; // Get the string value (async action) and the update the UI synchronously. - let value; - if (item.name && item.valueActor) { + if (item && item.name && item.valueActor) { value = yield item.valueActor.string(); } + // Bail if the selectedRow is no longer selected, the item doesn't exist or the state + // changed in another way during the above yield. + if (this.table.items.size === 0 || + !item || + !this.table.selectedRow || + item.uniqueKey !== this.table.selectedRow.uniqueKey) { + this.hideSidebar(); + return; + } + // Start updating the UI. Everything is sync beyond this point. this.sidebar.hidden = false; this.view.empty(); @@ -616,6 +682,11 @@ StorageUI.prototype = { let otherProps = itemProps.filter( e => !["name", "value", "valueActor"].includes(e)); for (let prop of otherProps) { + let column = this.table.columns.get(prop); + if (column && column.private) { + continue; + } + let cookieProp = COOKIE_KEY_MAP[prop] || prop; // The pseduo property of HostOnly refers to converse of isDomain property rawObject[cookieProp] = (prop === "isDomain") ? !item[prop] : item[prop]; @@ -627,6 +698,11 @@ StorageUI.prototype = { } else { // Case when displaying IndexedDB db/object store properties. for (let key in item) { + let column = this.table.columns.get(key); + if (column && column.private) { + continue; + } + mainScope.addItem(key, {}, true).setGrip(item[key]); this.parseItemValue(key, item[key]); } @@ -751,11 +827,19 @@ StorageUI.prototype = { * the storage tree */ onHostSelect: function (event, item) { + if (!item) { + return; + } this.table.clear(); this.hideSidebar(); this.searchBox.value = ""; let [type, host] = item; + this.table.host = host; + this.table.datatype = type; + + this.updateToolbar(); + let names = null; if (!host) { return; @@ -786,7 +870,9 @@ StorageUI.prototype = { let uniqueKey = null; let columns = {}; let editableFields = []; - let fields = yield this.getCurrentActor().getFields(subtype); + let hiddenFields = []; + let privateFields = []; + let fields = yield this.getCurrentFront().getFields(subtype); fields.forEach(f => { if (!uniqueKey) { @@ -797,10 +883,21 @@ StorageUI.prototype = { editableFields.push(f.name); } + if (f.hidden) { + hiddenFields.push(f.name); + } + + if (f.private) { + privateFields.push(f.name); + } + columns[f.name] = f.name; let columnName; try { - columnName = L10N.getStr("table.headers." + type + "." + f.name); + // Path key names for l10n in the case of a string change. + let name = f.name === "keyPath" ? "keyPath2" : f.name; + + columnName = L10N.getStr("table.headers." + type + "." + name); } catch (e) { columnName = COOKIE_KEY_MAP[f.name]; } @@ -812,7 +909,7 @@ StorageUI.prototype = { } }); - this.table.setColumns(columns, null, HIDDEN_COLUMNS); + this.table.setColumns(columns, null, hiddenFields, privateFields); this.hideSidebar(); yield this.makeFieldsEditable(editableFields); @@ -857,7 +954,7 @@ StorageUI.prototype = { case REASON.UPDATE: this.table.update(item); if (item == this.table.selectedRow && !this.sidebar.hidden) { - this.displayObjectSidebar(); + this.updateObjectSidebar(); } break; } @@ -910,27 +1007,53 @@ StorageUI.prototype = { }, /** - * Fires before a cell context menu with the "Delete" action is shown. - * If the currently selected storage object doesn't support removing items, prevent - * showing the menu. + * Fires before a cell context menu with the "Add" or "Delete" action is + * shown. If the currently selected storage object doesn't support adding or + * removing items, prevent showing the menu. */ onTablePopupShowing: function (event) { let selectedItem = this.tree.selectedItem; let type = selectedItem[0]; - let actor = this.getCurrentActor(); // IndexedDB only supports removing items from object stores (level 4 of the tree) - if (!actor.removeItem || (type === "indexedDB" && selectedItem.length !== 4)) { + if ((!this.actorSupportsAddItem && !this.actorSupportsRemoveItem && + type !== "cookies") || + (type === "indexedDB" && selectedItem.length !== 4)) { event.preventDefault(); return; } let rowId = this.table.contextMenuRowId; let data = this.table.items.get(rowId); - let name = addEllipsis(data[this.table.uniqueId]); - this._tablePopupDelete.setAttribute("label", - L10N.getFormatStr("storage.popupMenu.deleteLabel", name)); + if (this.actorSupportsRemoveItem) { + let name = data[this.table.uniqueId]; + let separatorRegex = new RegExp(SEPARATOR_GUID, "g"); + let label = addEllipsis((name + "").replace(separatorRegex, "-")); + + this._tablePopupDelete.hidden = false; + this._tablePopupDelete.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.deleteLabel", label)); + } else { + this._tablePopupDelete.hidden = true; + } + + if (this.actorSupportsAddItem) { + this._tablePopupAddItem.hidden = false; + this._tablePopupAddItem.setAttribute("label", + L10N.getFormatStr("storage.popupMenu.addItemLabel")); + } else { + this._tablePopupAddItem.hidden = true; + } + + let showDeleteAllSessionCookies = false; + if (this.actorSupportsRemoveAllSessionCookies) { + if (type === "cookies" && selectedItem.length === 2) { + showDeleteAllSessionCookies = true; + } + } + + this._tablePopupDeleteAllSessionCookies.hidden = !showDeleteAllSessionCookies; if (type === "cookies") { let host = addEllipsis(data.host); @@ -949,13 +1072,12 @@ StorageUI.prototype = { if (selectedItem) { let type = selectedItem[0]; - let actor = this.storageTypes[type]; // The delete all (aka clear) action is displayed for IndexedDB object stores // (level 4 of tree), for Cache objects (level 3) and for the whole host (level 2) // for other storage types (cookies, localStorage, ...). let showDeleteAll = false; - if (actor.removeAll) { + if (this.actorSupportsRemoveAll) { let level; if (type == "indexedDB") { level = 4; @@ -972,6 +1094,17 @@ StorageUI.prototype = { this._treePopupDeleteAll.hidden = !showDeleteAll; + // The delete all session cookies action is displayed for cookie object stores + // (level 2 of tree) + let showDeleteAllSessionCookies = false; + if (this.actorSupportsRemoveAllSessionCookies) { + if (type === "cookies" && selectedItem.length === 2) { + showDeleteAllSessionCookies = true; + } + } + + this._treePopupDeleteAllSessionCookies.hidden = !showDeleteAllSessionCookies; + // The delete action is displayed for: // - IndexedDB databases (level 3 of the tree) // - Cache objects (level 3 of the tree) @@ -993,31 +1126,61 @@ StorageUI.prototype = { }, /** + * Handles refreshing the selected storage + */ + onRefreshTable: function (event) { + this.onHostSelect(event, this.tree.selectedItem); + }, + + /** + * Handles adding an item from the storage + */ + onAddItem: function () { + if (!this.tree.selectedItem) { + return; + } + let front = this.getCurrentFront(); + let [, host] = this.tree.selectedItem; + + // Prepare to scroll into view. + this.table.scrollIntoViewOnUpdate = true; + this.table.editBookmark = createGUID(); + front.addItem(this.table.editBookmark, host); + }, + + /** * Handles removing an item from the storage */ onRemoveItem: function () { let [, host, ...path] = this.tree.selectedItem; - let actor = this.getCurrentActor(); + let front = this.getCurrentFront(); let rowId = this.table.contextMenuRowId; let data = this.table.items.get(rowId); let name = data[this.table.uniqueId]; if (path.length > 0) { name = JSON.stringify([...path, name]); } - actor.removeItem(host, name); + front.removeItem(host, name); }, /** * Handles removing all items from the storage */ onRemoveAll: function () { - // Cannot use this.currentActor() if the handler is called from the - // tree context menu: it returns correct value only after the table - // data from server are successfully fetched (and that's async). - let [type, host, ...path] = this.tree.selectedItem; - let actor = this.storageTypes[type]; + let [, host, ...path] = this.tree.selectedItem; + let front = this.getCurrentFront(); let name = path.length > 0 ? JSON.stringify(path) : undefined; - actor.removeAll(host, name); + front.removeAll(host, name); + }, + + /** + * Handles removing all session cookies from the storage + */ + onRemoveAllSessionCookies: function () { + let [, host, ...path] = this.tree.selectedItem; + let front = this.getCurrentFront(); + let name = path.length > 0 ? JSON.stringify(path) : undefined; + front.removeAllSessionCookies(host, name); }, /** @@ -1026,11 +1189,11 @@ StorageUI.prototype = { */ onRemoveAllFrom: function () { let [, host] = this.tree.selectedItem; - let actor = this.getCurrentActor(); + let front = this.getCurrentFront(); let rowId = this.table.contextMenuRowId; let data = this.table.items.get(rowId); - actor.removeAll(host, data.host); + front.removeAll(host, data.host); }, onRemoveTreeItem: function () { @@ -1044,9 +1207,9 @@ StorageUI.prototype = { }, removeDatabase: function (host, dbName) { - let actor = this.storageTypes.indexedDB; + let front = this.getCurrentFront(); - actor.removeDatabase(host, dbName).then(result => { + front.removeDatabase(host, dbName).then(result => { if (result.blocked) { let notificationBox = this._toolbox.getNotificationBox(); notificationBox.appendNotification( @@ -1066,8 +1229,17 @@ StorageUI.prototype = { }, removeCache: function (host, cacheName) { - let actor = this.storageTypes.Cache; + let front = this.getCurrentFront(); - actor.removeItem(host, JSON.stringify([ cacheName ])); + front.removeItem(host, JSON.stringify([ cacheName ])); }, }; + +// Helper Functions + +function createGUID() { + return "{cccccccc-cccc-4ccc-yccc-cccccccccccc}".replace(/[cy]/g, c => { + let r = Math.random() * 16 | 0, v = c == "c" ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} diff --git a/devtools/client/styleeditor/StyleEditorUI.jsm b/devtools/client/styleeditor/StyleEditorUI.jsm index cdb267669..b2735b3fc 100644 --- a/devtools/client/styleeditor/StyleEditorUI.jsm +++ b/devtools/client/styleeditor/StyleEditorUI.jsm @@ -72,10 +72,18 @@ function StyleEditorUI(debuggee, target, panelDoc, cssProperties) { this.editors = []; this.selectedEditor = null; this.savedLocations = {}; + this._seenSheets = new Map(); + + // Don't add any style sheets that might arrive via events, until + // the call to initialize. Style sheets can arrive from the server + // at any time, for example if a new style sheet was added, or if + // the style sheet actor was just created and is walking the style + // sheets for the first time. In any case, in |initialize| we're + // going to fetch the list of sheets anyway. + this._suppressAdd = true; this._onOptionsPopupShowing = this._onOptionsPopupShowing.bind(this); this._onOptionsPopupHiding = this._onOptionsPopupHiding.bind(this); - this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this); this._onNewDocument = this._onNewDocument.bind(this); this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this); this._updateMediaList = this._updateMediaList.bind(this); @@ -83,10 +91,13 @@ function StyleEditorUI(debuggee, target, panelDoc, cssProperties) { this._onError = this._onError.bind(this); this._updateOpenLinkItem = this._updateOpenLinkItem.bind(this); this._openLinkNewTab = this._openLinkNewTab.bind(this); + this._addStyleSheet = this._addStyleSheet.bind(this); this._prefObserver = new PrefObserver("devtools.styleeditor."); this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument); this._prefObserver.on(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged); + + this._debuggee.on("stylesheet-added", this._addStyleSheet); } this.StyleEditorUI = StyleEditorUI; @@ -165,7 +176,7 @@ StyleEditorUI.prototype = { this._view = new SplitView(viewRoot); wire(this._view.rootElement, ".style-editor-newButton", () =>{ - this._debuggee.addStyleSheet(null).then(this._onStyleSheetCreated); + this._debuggee.addStyleSheet(null); }); wire(this._view.rootElement, ".style-editor-importButton", ()=> { @@ -233,6 +244,7 @@ StyleEditorUI.prototype = { * StyleSheet object for new sheet */ _onNewDocument: function () { + this._suppressAdd = true; this._debuggee.getStyleSheets().then((styleSheets) => { return this._resetStyleSheetList(styleSheets); }).then(null, e => console.error(e)); @@ -246,6 +258,7 @@ StyleEditorUI.prototype = { */ _resetStyleSheetList: Task.async(function* (styleSheets) { this._clear(); + this._suppressAdd = false; for (let sheet of styleSheets) { try { @@ -288,6 +301,10 @@ StyleEditorUI.prototype = { this._view.removeAll(); this.selectedEditor = null; + // Here the keys are style sheet actors, and the values are + // promises that resolve to the sheet's editor. See |_addStyleSheet|. + this._seenSheets = new Map(); + this._suppressAdd = true; this._root.classList.add("loading"); }, @@ -298,46 +315,67 @@ StyleEditorUI.prototype = { * * @param {StyleSheetFront} styleSheet * Style sheet to add to style editor + * @param {Boolean} isNew + * True if this style sheet was created by a call to the + * style sheets actor's @see addStyleSheet method. + * @return {Promise} + * A promise that resolves to the style sheet's editor when the style sheet has + * been fully loaded. If the style sheet has a source map, and source mapping + * is enabled, then the promise resolves to null. */ - _addStyleSheet: Task.async(function* (styleSheet) { - let editor = yield this._addStyleSheetEditor(styleSheet); - - if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { - return; + _addStyleSheet: function (styleSheet, isNew) { + if (this._suppressAdd) { + return null; } - let sources = yield styleSheet.getOriginalSources(); - if (sources && sources.length) { - let parentEditorName = editor.friendlyName; - this._removeStyleSheetEditor(editor); - - for (let source of sources) { - // set so the first sheet will be selected, even if it's a source - source.styleSheetIndex = styleSheet.styleSheetIndex; - source.relatedStyleSheet = styleSheet; - source.relatedEditorName = parentEditorName; - yield this._addStyleSheetEditor(source); - } + if (!this._seenSheets.has(styleSheet)) { + let promise = (async () => { + let editor = await this._addStyleSheetEditor(styleSheet, isNew); + + if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + return editor; + } + + let sources = await styleSheet.getOriginalSources(); + // A single generated sheet might map to multiple original + // sheets, so make editors for each of them. + if (sources && sources.length) { + let parentEditorName = editor.friendlyName; + this._removeStyleSheetEditor(editor); + editor = null; + + for (let source of sources) { + // set so the first sheet will be selected, even if it's a source + source.styleSheetIndex = styleSheet.styleSheetIndex; + source.relatedStyleSheet = styleSheet; + source.relatedEditorName = parentEditorName; + await this._addStyleSheetEditor(source); + } + } + + return editor; + })(); + this._seenSheets.set(styleSheet, promise); } - }), + return this._seenSheets.get(styleSheet); + }, /** * Add a new editor to the UI for a source. * * @param {StyleSheet} styleSheet * Object representing stylesheet - * @param {nsIfile} file - * Optional file object that sheet was imported from * @param {Boolean} isNew * Optional if stylesheet is a new sheet created by user * @return {Promise} that is resolved with the created StyleSheetEditor when * the editor is fully initialized or rejected on error. */ - _addStyleSheetEditor: Task.async(function* (styleSheet, file, isNew) { + _addStyleSheetEditor: Task.async(function* (styleSheet, isNew) { // recall location of saved file for this sheet after page reload + let file = null; let identifier = this.getStyleSheetIdentifier(styleSheet); let savedFile = this.savedLocations[identifier]; - if (savedFile && !file) { + if (savedFile) { file = savedFile; } @@ -388,8 +426,16 @@ StyleEditorUI.prototype = { NetUtil.readInputStreamToString(stream, stream.available()); stream.close(); + this._suppressAdd = true; this._debuggee.addStyleSheet(source).then((styleSheet) => { - this._onStyleSheetCreated(styleSheet, selectedFile); + this._suppressAdd = false; + this._addStyleSheet(styleSheet, true).then(editor => { + if (editor) { + editor.savedFile = selectedFile; + } + // Just for testing purposes. + this.emit("test:editor-updated", editor); + }); }); }); }; @@ -398,14 +444,6 @@ StyleEditorUI.prototype = { }, /** - * When a new or imported stylesheet has been added to the document. - * Add an editor for it. - */ - _onStyleSheetCreated: function (styleSheet, file) { - this._addStyleSheetEditor(styleSheet, file, true); - }, - - /** * Forward any error from a stylesheet. * * @param {string} event @@ -1013,6 +1051,9 @@ StyleEditorUI.prototype = { this._clearStyleSheetEditors(); + this._seenSheets = null; + this._suppressAdd = false; + let sidebar = this._panelDoc.querySelector(".splitview-controller"); let sidebarWidth = sidebar.getAttribute("width"); Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth); @@ -1025,5 +1066,7 @@ StyleEditorUI.prototype = { this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument); this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged); this._prefObserver.destroy(); + + this._debuggee.off("stylesheet-added", this._addStyleSheet); } }; diff --git a/devtools/client/styleeditor/StyleSheetEditor.jsm b/devtools/client/styleeditor/StyleSheetEditor.jsm index 980e51974..832fcacde 100644 --- a/devtools/client/styleeditor/StyleSheetEditor.jsm +++ b/devtools/client/styleeditor/StyleSheetEditor.jsm @@ -468,6 +468,9 @@ StyleSheetEditor.prototype = { sourceEditor.container.addEventListener("mousemove", this._onMouseMove); } + // Add the commands controller for the source-editor. + sourceEditor.insertCommandsController(); + this.emit("source-editor-load"); }); }, diff --git a/devtools/client/styleeditor/test/browser.ini b/devtools/client/styleeditor/test/browser.ini index 1a85546af..4a84d45e6 100644 --- a/devtools/client/styleeditor/test/browser.ini +++ b/devtools/client/styleeditor/test/browser.ini @@ -60,6 +60,7 @@ support-files = !/devtools/client/shared/test/test-actor-registry.js !/devtools/client/shared/test/test-actor.js +[browser_styleeditor_add_stylesheet.js] [browser_styleeditor_autocomplete.js] [browser_styleeditor_autocomplete-disabled.js] [browser_styleeditor_bom.js] diff --git a/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js new file mode 100644 index 000000000..d8315d212 --- /dev/null +++ b/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js @@ -0,0 +1,37 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Test that a newly-added style sheet shows up in the style editor. + +const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html"; + +add_task(function* () { + let { ui } = yield openStyleEditorForURL(TESTCASE_URI); + + is(ui.editors.length, 2, "Two sheets present after load."); + + // We have to wait for the length to change, because we might still + // be seeing events from the initial open. + let added = new Promise(resolve => { + let handler = () => { + if (ui.editors.length === 3) { + ui.off("editor-added", handler); + resolve(); + } + }; + ui.on("editor-added", handler); + }); + + info("Adding a style sheet"); + yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => { + let document = content.document; + const style = document.createElement("style"); + style.appendChild(document.createTextNode("div { background: #f06; }")); + document.head.appendChild(style); + }); + yield added; + + is(ui.editors.length, 3, "Three sheets present after new style sheet"); +}); diff --git a/devtools/client/styleeditor/test/browser_styleeditor_import.js b/devtools/client/styleeditor/test/browser_styleeditor_import.js index f31f72ce7..2f42317b9 100644 --- a/devtools/client/styleeditor/test/browser_styleeditor_import.js +++ b/devtools/client/styleeditor/test/browser_styleeditor_import.js @@ -18,7 +18,7 @@ const SOURCE = "body{background:red;}"; add_task(function* () { let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI); - let added = ui.once("editor-added"); + let added = ui.once("test:editor-updated"); importSheet(ui, panel.panelWindow); info("Waiting for editor to be added for the imported sheet."); diff --git a/devtools/client/themes/common.css b/devtools/client/themes/common.css index 3d713ada7..133770150 100644 --- a/devtools/client/themes/common.css +++ b/devtools/client/themes/common.css @@ -2,7 +2,9 @@ * 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/. */ + @import url("resource://devtools/client/themes/splitters.css"); +@namespace html url("http://www.w3.org/1999/xhtml"); :root { font: message-box; @@ -33,6 +35,10 @@ font-size: 80%; } +/* Override wrong system font from forms.css */ +html|button, html|select { + font: message-box; +} /* Autocomplete Popup */ diff --git a/devtools/client/themes/images/reload.svg b/devtools/client/themes/images/reload.svg index b04262784..c5d9bf991 100644 --- a/devtools/client/themes/images/reload.svg +++ b/devtools/client/themes/images/reload.svg @@ -1,6 +1,7 @@ <!-- 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/. --> -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="14" height="14"> - <path d="M12,7H6l2.4-2.4C7.6,4,6.6,3.8,5.5,4.1C4.3,4.5,3.3,5.5,3,6.8 C2.6,9,4.3,11,6.5,11c1,0,2-0.5,2.6-1.2l1.7,1c-1.3,1.6-3.3,2.5-5.6,2c-2-0.5-3.6-2.1-4-4.1C0.4,5.1,3.1,2,6.5,2 c1.3,0,2.4,0.4,3.3,1.2L12,1V7z"/> +<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> + <path d="M13.917 7C13.44 4.162 10.973 2 8 2 4.686 2 2 4.686 2 8s2.686 6 6 6c2.22 0 4.16-1.207 5.197-3H12c-.912 1.214-2.364 2-4 2-2.76 0-5-2.24-5-5s2.24-5 5-5c2.42 0 4.437 1.718 4.9 4h1.017z"/> + <path d="M14 1L8 7h6V1zm-1 1L9 6h4V2z" fill-rule="evenodd"/> </svg> diff --git a/devtools/client/themes/netmonitor.css b/devtools/client/themes/netmonitor.css index fea634a0e..ccffb2acc 100644 --- a/devtools/client/themes/netmonitor.css +++ b/devtools/client/themes/netmonitor.css @@ -8,6 +8,7 @@ } #react-clear-button-hook, +#react-summary-button-hook, #react-details-pane-toggle-hook { display: flex; } @@ -47,7 +48,7 @@ #details-pane-toggle, #details-pane.pane-collapsed, .requests-menu-waterfall, - #requests-menu-network-summary-button > .toolbarbutton-text { + #requests-menu-network-summary-button > .summary-info-text { display: none; } } @@ -58,6 +59,7 @@ --timing-blocked-color: rgba(235, 83, 104, 0.8); --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */ + --timing-ssl-color: rgba(217, 102, 41, 0.8); /* orange */ --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */ --timing-send-color: rgba(70, 175, 227, 0.8); /* light blue */ --timing-wait-color: rgba(94, 136, 176, 0.8); /* blue grey */ @@ -73,6 +75,7 @@ --timing-blocked-color: rgba(235, 83, 104, 0.8); --timing-dns-color: rgba(223, 128, 255, 0.8); /* pink */ + --timing-ssl-color: rgba(217, 102, 41, 0.8); /* orange */ --timing-connect-color: rgba(217, 102, 41, 0.8); /* orange */ --timing-send-color: rgba(0, 136, 204, 0.8); /* blue */ --timing-wait-color: rgba(95, 136, 176, 0.8); /* blue grey */ @@ -504,6 +507,10 @@ background-color: var(--timing-connect-color); } +.requests-menu-timings-box.ssl { + background-color: var(--timing-ssl-color); +} + .requests-menu-timings-box.send { background-color: var(--timing-send-color); } @@ -744,16 +751,35 @@ /* Performance analysis buttons */ #requests-menu-network-summary-button { + display: flex; + align-items: center; background: none; box-shadow: none; border-color: transparent; - list-style-image: url(images/profiler-stopwatch.svg); padding-inline-end: 0; cursor: pointer; margin-inline-end: 1em; min-width: 0; } +#requests-menu-network-summary-button > .summary-info-icon { + background-image: url(images/profiler-stopwatch.svg); + filter: var(--icon-filter); + width: 16px; + height: 16px; + opacity: 0.8; +} + +#requests-menu-network-summary-button > .summary-info-text { + opacity: 0.8; + margin-inline-start: 0.5em; +} + +#requests-menu-network-summary-button:hover > .summary-info-icon, +#requests-menu-network-summary-button:hover > .summary-info-text { + opacity: 1; +} + /* Performance analysis view */ #network-statistics-toolbar { diff --git a/devtools/client/themes/storage.css b/devtools/client/themes/storage.css index 1e611f842..314b6b7fe 100644 --- a/devtools/client/themes/storage.css +++ b/devtools/client/themes/storage.css @@ -32,6 +32,26 @@ min-width: 250px; } +#storage-toolbar .add-button::before { + margin: 0; + background-image: url("chrome://devtools/skin/images/add.svg"); + -moz-user-focus: normal; +} + +#storage-toolbar .refresh-button::before { + margin: 0; + background-image: url("chrome://devtools/skin/images/reload.svg"); + -moz-user-focus: normal; +} + +#storage-toolbar .devtools-button { + min-width: unset; +} + +#storage-toolbar .devtools-button hbox { + display: none; +} + /* Responsive sidebar */ @media (max-width: 700px) { #storage-tree, diff --git a/devtools/client/webconsole/hudservice.js b/devtools/client/webconsole/hudservice.js index 46b4f2a13..3023b7bb3 100644 --- a/devtools/client/webconsole/hudservice.js +++ b/devtools/client/webconsole/hudservice.js @@ -51,6 +51,23 @@ HUD_SERVICE.prototype = */ consoles: null, + _browerConsoleSessionState: false, + storeBrowserConsoleSessionState() { + this._browerConsoleSessionState = !!this.getBrowserConsole(); + }, + getBrowserConsoleSessionState() { + return this._browerConsoleSessionState; + }, + + /** + * Restore the Browser Console as provided by SessionStore. + */ + restoreBrowserConsoleSession: function HS_restoreBrowserConsoleSession() { + if (!HUDService.getBrowserConsole()) { + HUDService.toggleBrowserConsole(); + } + }, + /** * Assign a function to this property to listen for every request that * completes. Used by unit tests. The callback takes one argument: the HTTP @@ -647,6 +664,9 @@ BrowserConsole.prototype = extend(WebConsole.prototype, { return this._bc_init; } + // Only add the shutdown observer if we've opened a Browser Console window. + ShutdownObserver.init(); + this.ui._filterPrefsPrefix = BROWSER_CONSOLE_FILTER_PREFS_PREFIX; let window = this.iframeWindow; @@ -703,16 +723,32 @@ BrowserConsole.prototype = extend(WebConsole.prototype, { }); const HUDService = new HUD_SERVICE(); +exports.HUDService = HUDService; -(() => { - let methods = ["openWebConsole", "openBrowserConsole", - "toggleBrowserConsole", "getOpenWebConsole", - "getBrowserConsole", "getHudByWindow", - "openBrowserConsoleOrFocus", "getHudReferenceById"]; - for (let method of methods) { - exports[method] = HUDService[method].bind(HUDService); - } +/** + * The ShutdownObserver listens for app shutdown and saves the current state + * of the Browser Console for session restore. + */ +var ShutdownObserver = { + _initialized: false, + init() { + if (this._initialized) { + return; + } + + Services.obs.addObserver(this, "quit-application-granted", false); + + this._initialized = true; + }, - exports.consoles = HUDService.consoles; - exports.lastFinishedRequest = HUDService.lastFinishedRequest; -})(); + observe(message, topic) { + if (topic == "quit-application-granted") { + HUDService.storeBrowserConsoleSessionState(); + this.uninit(); + } + }, + + uninit() { + Services.obs.removeObserver(this, "quit-application-granted"); + } +}; diff --git a/devtools/client/webconsole/panel.js b/devtools/client/webconsole/panel.js index 3e3a4f4b9..b692de681 100644 --- a/devtools/client/webconsole/panel.js +++ b/devtools/client/webconsole/panel.js @@ -8,7 +8,7 @@ const promise = require("promise"); -loader.lazyGetter(this, "HUDService", () => require("devtools/client/webconsole/hudservice")); +loader.lazyRequireGetter(this, "HUDService", "devtools/client/webconsole/hudservice", true); loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter")); /** diff --git a/devtools/client/webconsole/test/browser.ini b/devtools/client/webconsole/test/browser.ini index 918411182..1c7913835 100644 --- a/devtools/client/webconsole/test/browser.ini +++ b/devtools/client/webconsole/test/browser.ini @@ -182,6 +182,7 @@ subsuite = clipboard [browser_console_optimized_out_vars.js] [browser_console_private_browsing.js] skip-if = e10s # Bug 1042253 - webconsole e10s tests +[browser_console_restore.js] [browser_console_server_logging.js] [browser_console_variables_view.js] [browser_console_variables_view_filter.js] diff --git a/devtools/client/webconsole/test/browser_console.js b/devtools/client/webconsole/test/browser_console.js index 7bd1ffdc2..4358ac0f1 100644 --- a/devtools/client/webconsole/test/browser_console.js +++ b/devtools/client/webconsole/test/browser_console.js @@ -22,7 +22,7 @@ const TEST_IMAGE = "http://example.com/browser/devtools/client/webconsole/" + add_task(function* () { yield loadTab(TEST_URI); - let opened = waitForConsole(); + let opened = waitForBrowserConsole(); let hud = HUDService.getBrowserConsole(); ok(!hud, "browser console is not open"); @@ -141,20 +141,3 @@ function consoleOpened(hud) { ], }); } - -function waitForConsole() { - let deferred = promise.defer(); - - Services.obs.addObserver(function observer(aSubject) { - Services.obs.removeObserver(observer, "web-console-created"); - aSubject.QueryInterface(Ci.nsISupportsString); - - let hud = HUDService.getBrowserConsole(); - ok(hud, "browser console is open"); - is(aSubject.data, hud.hudId, "notification hudId is correct"); - - executeSoon(() => deferred.resolve(hud)); - }, "web-console-created", false); - - return deferred.promise; -} diff --git a/devtools/client/webconsole/test/browser_console_restore.js b/devtools/client/webconsole/test/browser_console_restore.js new file mode 100644 index 000000000..fb08d9c70 --- /dev/null +++ b/devtools/client/webconsole/test/browser_console_restore.js @@ -0,0 +1,30 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Check that the browser console gets session state is set correctly, and that +// it re-opens when restore is requested. + +"use strict"; + +add_task(function* () { + is(HUDService.getBrowserConsoleSessionState(), false, "Session state false by default"); + HUDService.storeBrowserConsoleSessionState(); + is(HUDService.getBrowserConsoleSessionState(), false, + "Session state still not true even after setting (since Browser Console is closed)"); + + yield HUDService.toggleBrowserConsole(); + HUDService.storeBrowserConsoleSessionState(); + is(HUDService.getBrowserConsoleSessionState(), true, + "Session state true (since Browser Console is opened)"); + + info("Closing the browser console and waiting for the session restore to reopen it") + yield HUDService.toggleBrowserConsole(); + + let opened = waitForBrowserConsole(); + HUDService.restoreBrowserConsoleSession(); + + info("Waiting for the console to open after session restore") + yield opened; +}); diff --git a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js index 6a29d61aa..769fc0fe5 100644 --- a/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js +++ b/devtools/client/webconsole/test/browser_webconsole_closure_inspection.js @@ -58,7 +58,7 @@ function consoleOpened(hud) { waitForMessages({ webconsole: gWebConsole, messages: [{ - text: "function _pfactory/<.getName()", + text: "getName()", category: CATEGORY_OUTPUT, objects: true, }], diff --git a/devtools/client/webconsole/test/browser_webconsole_jsterm.js b/devtools/client/webconsole/test/browser_webconsole_jsterm.js index 221c96fa6..ae5dc71fe 100644 --- a/devtools/client/webconsole/test/browser_webconsole_jsterm.js +++ b/devtools/client/webconsole/test/browser_webconsole_jsterm.js @@ -175,7 +175,6 @@ function* testJSTerm(hud) { "JSMSG_BAD_RADIX": "(42).toString(0);", "JSMSG_BAD_ARRAY_LENGTH": "([]).length = -1", "JSMSG_NEGATIVE_REPETITION_COUNT": "'abc'.repeat(-1);", - "JSMSG_BAD_FORMAL": "var f = Function('x y', 'return x + y;');", "JSMSG_PRECISION_RANGE": "77.1234.toExponential(-1);", }; diff --git a/devtools/client/webconsole/test/browser_webconsole_output_02.js b/devtools/client/webconsole/test/browser_webconsole_output_02.js index 8018669a9..4c61cf041 100644 --- a/devtools/client/webconsole/test/browser_webconsole_output_02.js +++ b/devtools/client/webconsole/test/browser_webconsole_output_02.js @@ -36,10 +36,10 @@ var inputTests = [ suppressClick: true }, - // 3 - anonymous function, but spidermonkey gives us an inferred name. + // 3 - anonymous function, but gets name. { input: "testobj1.testfn2", - output: "function testobj1.testfn2()", + output: "function testfn2()", printOutput: "function () { return 42; }", suppressClick: true }, diff --git a/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js index c8f2200f9..cdaf2764e 100644 --- a/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js +++ b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js @@ -55,7 +55,7 @@ add_task(function* () { webconsole: hud, messages: [ { - text: "TypeError: setting a property that has only a getter", + text: 'TypeError: setting getter-only property "p"', category: CATEGORY_JS, severity: SEVERITY_ERROR, }, diff --git a/devtools/client/webconsole/test/head.js b/devtools/client/webconsole/test/head.js index 519cb78b0..2787933c4 100644 --- a/devtools/client/webconsole/test/head.js +++ b/devtools/client/webconsole/test/head.js @@ -12,7 +12,7 @@ Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtool var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils"); var {Messages} = require("devtools/client/webconsole/console-output"); const asyncStorage = require("devtools/shared/async-storage"); -const HUDService = require("devtools/client/webconsole/hudservice"); +const {HUDService} = require("devtools/client/webconsole/hudservice"); // Services.prefs.setBoolPref("devtools.debugger.log", true); @@ -1842,3 +1842,18 @@ function getRenderedSource(root) { column: location.getAttribute("data-column"), } : null; } + +function waitForBrowserConsole() { + return new Promise(resolve => { + Services.obs.addObserver(function observer(subject) { + Services.obs.removeObserver(observer, "web-console-created"); + subject.QueryInterface(Ci.nsISupportsString); + + let hud = HUDService.getBrowserConsole(); + ok(hud, "browser console is open"); + is(subject.data, hud.hudId, "notification hudId is correct"); + + executeSoon(() => resolve(hud)); + }, "web-console-created"); + }); +} |