diff options
Diffstat (limited to 'devtools/client')
52 files changed, 1531 insertions, 220 deletions
diff --git a/devtools/client/locales/en-US/netmonitor.properties b/devtools/client/locales/en-US/netmonitor.properties index 021b56a2b..4926c234b 100644 --- a/devtools/client/locales/en-US/netmonitor.properties +++ b/devtools/client/locales/en-US/netmonitor.properties @@ -150,6 +150,11 @@ networkMenu.empty=No requests # #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). networkMenu.sizeB=%S B 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/netmonitor/actions/index.js b/devtools/client/netmonitor/actions/index.js index 2ce23448e..de8f4ae1d 100644 --- a/devtools/client/netmonitor/actions/index.js +++ b/devtools/client/netmonitor/actions/index.js @@ -5,6 +5,7 @@ const filters = require("./filters"); const requests = require("./requests"); +const timingMarkers = require("./timing-markers"); const ui = require("./ui"); -module.exports = Object.assign({}, filters, requests, ui); +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 d0ac61944..ce904cae8 100644 --- a/devtools/client/netmonitor/actions/moz.build +++ b/devtools/client/netmonitor/actions/moz.build @@ -7,5 +7,6 @@ DevToolsModules( 'filters.js', 'index.js', 'requests.js', + 'timing-markers.js', 'ui.js', ) 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/components/summary-button.js b/devtools/client/netmonitor/components/summary-button.js index 223552fbf..00595e5e6 100644 --- a/devtools/client/netmonitor/components/summary-button.js +++ b/devtools/client/netmonitor/components/summary-button.js @@ -15,16 +15,22 @@ const { connect } = require("devtools/client/shared/vendor/react-redux"); const { PluralForm } = require("devtools/shared/plural-form"); const { L10N } = require("../l10n"); const { - getDisplayedRequestsSummary + getDisplayedRequestsSummary, + getDisplayedTimingMarker } = require("../selectors/index"); const { button, span } = DOM; function SummaryButton({ summary, - triggerSummary + 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) @@ -33,7 +39,13 @@ function SummaryButton({ .replace("#3", L10N.numberWithDecimals(transferredSize / 1024, CONTENT_SIZE_DECIMALS)) .replace("#4", L10N.numberWithDecimals(millis / 1000, - REQUEST_TIME_DECIMALS)); + 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", @@ -47,11 +59,17 @@ function SummaryButton({ 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: () => { diff --git a/devtools/client/netmonitor/constants.js b/devtools/client/netmonitor/constants.js index 33bc71ea7..1605496a5 100644 --- a/devtools/client/netmonitor/constants.js +++ b/devtools/client/netmonitor/constants.js @@ -10,6 +10,8 @@ const general = { }; 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", SET_FILTER_TEXT: "SET_FILTER_TEXT", diff --git a/devtools/client/netmonitor/netmonitor-controller.js b/devtools/client/netmonitor/netmonitor-controller.js index 739e174fb..939b6e4f5 100644 --- a/devtools/client/netmonitor/netmonitor-controller.js +++ b/devtools/client/netmonitor/netmonitor-controller.js @@ -414,6 +414,7 @@ TargetEventsHandler.prototype = { } // Clear any accumulated markers. NetMonitorController.NetworkEventsHandler.clearMarkers(); + gStore.dispatch(Actions.clearTimingMarkers()); window.emit(EVENTS.TARGET_WILL_NAVIGATE); break; @@ -534,6 +535,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/reducers/index.js b/devtools/client/netmonitor/reducers/index.js index f36b1d91f..f003c7805 100644 --- a/devtools/client/netmonitor/reducers/index.js +++ b/devtools/client/netmonitor/reducers/index.js @@ -6,10 +6,12 @@ const { combineReducers } = require("devtools/client/shared/vendor/redux"); const filters = require("./filters"); const requests = require("./requests"); +const timingMarkers = require("./timing-markers"); const ui = require("./ui"); module.exports = combineReducers({ filters, requests, + timingMarkers, ui, }); diff --git a/devtools/client/netmonitor/reducers/moz.build b/devtools/client/netmonitor/reducers/moz.build index d0ac61944..ce904cae8 100644 --- a/devtools/client/netmonitor/reducers/moz.build +++ b/devtools/client/netmonitor/reducers/moz.build @@ -7,5 +7,6 @@ DevToolsModules( 'filters.js', 'index.js', 'requests.js', + 'timing-markers.js', 'ui.js', ) 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/requests-menu-view.js b/devtools/client/netmonitor/requests-menu-view.js index 4ee307145..c491fcb0e 100644 --- a/devtools/client/netmonitor/requests-menu-view.js +++ b/devtools/client/netmonitor/requests-menu-view.js @@ -275,6 +275,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { this._addQueue = []; this._updateQueue = []; this._firstRequestStartedMillis = -1; + this._firstRequestStartedMillisInRequests = false; this._lastRequestEndedMillis = -1; }, @@ -650,6 +651,9 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { // Append a network request item to this container. let requestItem = this.push([menuView, id], { attachment: { + firstRequestStartedMillis: this._firstRequestStartedMillisInRequests + ? null + : this._firstRequestStartedMillis, startedDeltaMillis: unixTime - this._firstRequestStartedMillis, startedMillis: unixTime, method: method, @@ -661,6 +665,8 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { } }); + this._firstRequestStartedMillisInRequests = true; + if (id == this._preferredItemId) { this.selectedItem = requestItem; } @@ -1563,6 +1569,7 @@ RequestsMenuView.prototype = Heritage.extend(WidgetMethods, { _ctx: null, _cachedWaterfallWidth: 0, _firstRequestStartedMillis: -1, + _firstRequestStartedMillisInRequests: false, _lastRequestEndedMillis: -1, _updateQueue: [], _addQueue: [], diff --git a/devtools/client/netmonitor/selectors/index.js b/devtools/client/netmonitor/selectors/index.js index 60d6007cd..612188758 100644 --- a/devtools/client/netmonitor/selectors/index.js +++ b/devtools/client/netmonitor/selectors/index.js @@ -73,6 +73,24 @@ const getDisplayedRequestsSummary = createSelector( }) ); +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[0] + .attachment.firstRequestStartedMillis; + } + if (timingMarker && firstRequestStartedMillis) { + return timingMarker - firstRequestStartedMillis; + } else { + return -1; + } +} + module.exports = { getDisplayedRequestsSummary, + getDisplayedTimingMarker, }; 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/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/storage/storage.xul b/devtools/client/storage/storage.xul index 9fbef5199..a91900add 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,9 @@ <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> + <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..1250d35d5 100644 --- a/devtools/client/storage/test/browser.ini +++ b/devtools/client/storage/test/browser.ini @@ -7,6 +7,7 @@ support-files = storage-cookies.html storage-empty-objectstores.html storage-idb-delete-blocked.html + storage-indexeddb-duplicate-names.html storage-listings.html storage-localstorage.html storage-overflow.html @@ -21,6 +22,7 @@ support-files = [browser_storage_basic.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_edit.js] @@ -29,15 +31,21 @@ support-files = [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..7585eed1f 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", "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", "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", 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..4640ebecb 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,24 +51,94 @@ 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", "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", "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", "test1.example.org"], id, "deleteAllFrom"); + yield performDelete(["cookies", "test1.example.org"], id, false); 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", "test1.example.org"], + [ + getCookieId("cs2", ".example.org", "/"), + getCookieId("c4", ".example.org", "/"), + getCookieId("uc1", ".example.org", "/"), + getCookieId("uc2", ".example.org", "/") + ] + ], + [ + ["cookies", "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", "sectest1.example.org"], id, "deleteAll"); info("test state after delete all"); yield checkState([ diff --git a/devtools/client/storage/test/browser_storage_cookies_domain.js b/devtools/client/storage/test/browser_storage_cookies_domain.js index dc93d6e67..7b194b73e 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", "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_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..a3ef7c290 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", "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..2bca4c344 100644 --- a/devtools/client/storage/test/browser_storage_delete_tree.js +++ b/devtools/client/storage/test/browser_storage_delete_tree.js @@ -17,10 +17,20 @@ add_task(function* () { info("test state before delete"); yield checkState([ - [["cookies", "test1.example.org"], ["c1", "c3", "cs2", "uc1"]], + [ + ["cookies", "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"]], ]); @@ -30,7 +40,7 @@ add_task(function* () { ["cookies", "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"], ]; @@ -59,7 +69,7 @@ add_task(function* () { [["cookies", "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..6cf52f2d3 --- /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", "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", "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", "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", "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", "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", "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", "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", "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..6712ac013 100644 --- a/devtools/client/storage/test/browser_storage_sidebar.js +++ b/devtools/client/storage/test/browser_storage_sidebar.js @@ -20,22 +20,22 @@ const testCases = [ 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..181132a45 100644 --- a/devtools/client/storage/test/head.js +++ b/devtools/client/storage/test/head.js @@ -15,6 +15,7 @@ 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 @@ -24,6 +25,11 @@ const MAIN_DOMAIN = "http://test1.example.org/" + 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 +39,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 +512,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 +599,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 +734,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 +842,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 +891,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.html b/devtools/client/storage/test/storage-listings.html index de3054d3a..313b36b71 100644 --- a/devtools/client/storage/test/storage-listings.html +++ b/devtools/client/storage/test/storage-listings.html @@ -1,4 +1,4 @@ -<!DOCTYPE HTML> +<!DOCTYPE HTML> <html> <!-- Bug 970517 - Storage inspector front end - tests @@ -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..ef07a8e88 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,20 @@ function StorageUI(front, target, panelWin, toolbox) { this._tablePopup = this._panelDoc.getElementById("storage-table-popup"); this._tablePopup.addEventListener("popupshowing", this.onTablePopupShowing); + 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._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 +187,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 +217,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 +229,19 @@ StorageUI.prototype = { this.searchBox = null; this._treePopup.removeEventListener("popupshowing", this.onTreePopupShowing); + 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 +253,7 @@ StorageUI.prototype = { this.table.clearSelection(); }, - getCurrentActor: function () { + getCurrentFront: function () { let type = this.table.datatype; return this.storageTypes[type]; @@ -250,9 +274,9 @@ StorageUI.prototype = { }, editItem: function (eventType, data) { - let actor = this.getCurrentActor(); + let front = this.getCurrentFront(); - actor.editItem(data); + front.editItem(data); }, /** @@ -261,17 +285,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 +477,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 +529,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 +538,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 +554,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 +562,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 +630,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 +677,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 +693,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 +822,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 +865,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 +878,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 +904,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 +949,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 +1002,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 +1067,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 +1089,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 +1121,54 @@ StorageUI.prototype = { }, /** + * 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 +1177,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 +1195,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 +1217,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/themes/storage.css b/devtools/client/themes/storage.css index 1e611f842..1d4da9bd6 100644 --- a/devtools/client/themes/storage.css +++ b/devtools/client/themes/storage.css @@ -32,6 +32,20 @@ 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 .devtools-button { + min-width: unset; +} + +#storage-toolbar .devtools-button hbox { + display: none; +} + /* Responsive sidebar */ @media (max-width: 700px) { #storage-tree, |