author | Ryan VanderMeulen <ryanvm@gmail.com> |
Thu, 14 Mar 2013 21:45:34 -0400 | |
changeset 124843 | 8f5b1f9f580492af9096e903ef225d7ffc8297f7 |
parent 124842 | 3e1241c6d2ced86d238f08d7573b2cfdaa53747c (current diff) |
parent 124794 | 0f7261e288f21f400e55e7681f43c1009ce03e42 (diff) |
child 124844 | 6d587302645ad19a586d6f4fc056f6c7252899f8 |
child 124845 | 116a46d187fe9b0b7376348efd803acee9ed5258 |
child 125048 | 9aba82c4908b86953c138bec784f6f54b74a6fbe |
child 125134 | a4d096b5a2f99a6bcd61f640c9be48ca4bbaa5f8 |
push id | 24436 |
push user | ryanvm@gmail.com |
push date | Fri, 15 Mar 2013 11:52:55 +0000 |
treeherder | mozilla-central@8f5b1f9f5804 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 22.0a1 |
first release with | nightly linux32
8f5b1f9f5804
/
22.0a1
/
20130316030854
/
files
nightly linux64
8f5b1f9f5804
/
22.0a1
/
20130316030854
/
files
nightly mac
8f5b1f9f5804
/
22.0a1
/
20130316030854
/
files
nightly win32
8f5b1f9f5804
/
22.0a1
/
20130316030854
/
files
nightly win64
8f5b1f9f5804
/
22.0a1
/
20130316030854
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
22.0a1
/
20130316030854
/
pushlog to previous
nightly linux64
22.0a1
/
20130316030854
/
pushlog to previous
nightly mac
22.0a1
/
20130316030854
/
pushlog to previous
nightly win32
22.0a1
/
20130316030854
/
pushlog to previous
nightly win64
22.0a1
/
20130316030854
/
pushlog to previous
|
browser/devtools/inspector/test/browser_inspector_bug_566084_location_changed.js | file | annotate | diff | comparison | revisions | |
browser/devtools/webconsole/AutocompletePopup.jsm | file | annotate | diff | comparison | revisions |
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1038,17 +1038,17 @@ pref("devtools.gcli.allowSet", false); pref("devtools.commands.dir", ""); // Toolbox preferences pref("devtools.toolbox.footer.height", 250); pref("devtools.toolbox.sidebar.width", 500); pref("devtools.toolbox.host", "bottom"); pref("devtools.toolbox.selectedTool", "webconsole"); pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]'); -pref("devtools.toolbox.sideEnabled", false); +pref("devtools.toolbox.sideEnabled", true); // Enable the Inspector pref("devtools.inspector.enabled", true); pref("devtools.inspector.activeSidebar", "ruleview"); pref("devtools.inspector.markupPreview", false); // Enable the Layout View pref("devtools.layoutview.enabled", true);
--- a/browser/devtools/framework/Toolbox.jsm +++ b/browser/devtools/framework/Toolbox.jsm @@ -397,48 +397,64 @@ Toolbox.prototype = { } let tabs = this.doc.getElementById("toolbox-tabs"); let deck = this.doc.getElementById("toolbox-deck"); let id = toolDefinition.id; let radio = this.doc.createElement("radio"); - radio.setAttribute("label", toolDefinition.label); radio.className = "toolbox-tab devtools-tab"; radio.id = "toolbox-tab-" + id; + radio.setAttribute("flex", "1"); radio.setAttribute("toolid", id); radio.setAttribute("tooltiptext", toolDefinition.tooltip); - if (toolDefinition.icon) { - radio.setAttribute("src", toolDefinition.icon); - } radio.addEventListener("command", function(id) { this.selectTool(id); }.bind(this, id)); + if (toolDefinition.icon) { + let image = this.doc.createElement("image"); + image.setAttribute("src", toolDefinition.icon); + radio.appendChild(image); + } + + let label = this.doc.createElement("label"); + label.setAttribute("value", toolDefinition.label) + label.setAttribute("crop", "end"); + label.setAttribute("flex", "1"); + let vbox = this.doc.createElement("vbox"); vbox.className = "toolbox-panel"; vbox.id = "toolbox-panel-" + id; + radio.appendChild(label); tabs.appendChild(radio); deck.appendChild(vbox); this._addKeysToWindow(); }, /** * Switch to the tool with the given id * * @param {string} id * The id of the tool to switch to */ selectTool: function TBOX_selectTool(id) { let deferred = Promise.defer(); + let selected = this.doc.querySelector(".devtools-tab[selected]"); + if (selected) { + selected.removeAttribute("selected"); + } + let tab = this.doc.getElementById("toolbox-tab-" + id); + tab.setAttribute("selected", "true"); + if (this._currentToolId == id) { // Return the existing panel in order to have a consistent return value. return Promise.resolve(this._toolPanels.get(id)); } if (!this.isReady) { throw new Error("Can't select tool, wait for toolbox 'ready' event"); }
--- a/browser/devtools/framework/toolbox.xul +++ b/browser/devtools/framework/toolbox.xul @@ -23,19 +23,19 @@ <toolbar class="devtools-tabbar"> #ifdef XP_MACOSX <hbox id="toolbox-controls"> <toolbarbutton id="toolbox-close" tooltiptext="&toolboxCloseButton.tooltip;"/> <hbox id="toolbox-dock-buttons"/> </hbox> #endif - <radiogroup id="toolbox-tabs" orient="horizontal"> - </radiogroup> - <hbox id="toolbox-buttons" flex="1" pack="end"/> + <hbox id="toolbox-tabs" flex="1"> + </hbox> + <hbox id="toolbox-buttons" pack="end"/> #ifndef XP_MACOSX <vbox id="toolbox-controls-separator"/> <hbox id="toolbox-controls"> <hbox id="toolbox-dock-buttons"/> <toolbarbutton id="toolbox-close" tooltiptext="&toolboxCloseButton.tooltip;"/> </hbox> #endif
--- a/browser/devtools/inspector/InspectorPanel.jsm +++ b/browser/devtools/inspector/InspectorPanel.jsm @@ -19,16 +19,18 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "Selection", "resource:///modules/devtools/Selection.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "HTMLBreadcrumbs", "resource:///modules/devtools/Breadcrumbs.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Highlighter", "resource:///modules/devtools/Highlighter.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ToolSidebar", "resource:///modules/devtools/Sidebar.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SelectorSearch", + "resource:///modules/devtools/SelectorSearch.jsm"); const LAYOUT_CHANGE_TIMER = 250; /** * Represents an open instance of the Inspector for a tab. * The inspector controls the highlighter, the breadcrumbs, * the markup view, and the sidebar (computed view, rule view * and layout view). @@ -45,38 +47,26 @@ this.InspectorPanel = function Inspector InspectorPanel.prototype = { /** * open is effectively an asynchronous constructor */ open: function InspectorPanel_open() { let deferred = Promise.defer(); - this.preventNavigateAway = this.preventNavigateAway.bind(this); this.onNavigatedAway = this.onNavigatedAway.bind(this); - this.target.on("will-navigate", this.preventNavigateAway); this.target.on("navigate", this.onNavigatedAway); this.nodemenu = this.panelDoc.getElementById("inspector-node-popup"); this.lastNodemenuItem = this.nodemenu.lastChild; this._setupNodeMenu = this._setupNodeMenu.bind(this); this._resetNodeMenu = this._resetNodeMenu.bind(this); this.nodemenu.addEventListener("popupshowing", this._setupNodeMenu, true); this.nodemenu.addEventListener("popuphiding", this._resetNodeMenu, true); - // Initialize the search related items - this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); - this._lastSearched = null; - this._searchResults = null; - this._searchIndex = 0; - this._onHTMLSearch = this._onHTMLSearch.bind(this); - this._onSearchKeypress = this._onSearchKeypress.bind(this); - this.searchBox.addEventListener("command", this._onHTMLSearch, true); - this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); - // Create an empty selection this._selection = new Selection(); this.onNewSelection = this.onNewSelection.bind(this); this.selection.on("new-node", this.onNewSelection); this.onBeforeNewSelection = this.onBeforeNewSelection.bind(this); this.selection.on("before-new-node", this.onBeforeNewSelection); this.onDetached = this.onDetached.bind(this); this.selection.on("detached", this.onDetached); @@ -148,16 +138,17 @@ InspectorPanel.prototype = { if (this.highlighter) { this.highlighter.unlock(); } this.emit("ready"); deferred.resolve(this); }.bind(this)); + this.setupSearchBox(); this.setupSidebar(); return deferred.promise; }, /** * Selection object (read only) */ @@ -191,16 +182,34 @@ InspectorPanel.prototype = { * decide whether to show the "are you sure you want to navigate" * notification. */ markDirty: function InspectorPanel_markDirty() { this.isDirty = true; }, /** + * Hooks the searchbar to show result and auto completion suggestions. + */ + setupSearchBox: function InspectorPanel_setupSearchBox() { + // Initiate the selectors search object. + let setNodeFunction = function(node) { + this.selection.setNode(node, "selectorsearch"); + }.bind(this); + if (this.searchSuggestions) { + this.searchSuggestions.destroy(); + this.searchSuggestions = null; + } + this.searchBox = this.panelDoc.getElementById("inspector-searchbox"); + this.searchSuggestions = new SelectorSearch(this.browser.contentDocument, + this.searchBox, + setNodeFunction); + }, + + /** * Build the sidebar. */ setupSidebar: function InspectorPanel_setupSidebar() { let tabbox = this.panelDoc.querySelector("#inspector-sidebar"); this.sidebar = new ToolSidebar(tabbox, this); let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar"); @@ -251,97 +260,27 @@ InspectorPanel.prototype = { if (self._destroyed) { return; } if (!self.selection.node) { self.selection.setNode(newWindow.document.documentElement, "navigateaway"); } self._initMarkup(); + self.setupSearchBox(); } if (newWindow.document.readyState == "loading") { newWindow.addEventListener("DOMContentLoaded", onDOMReady, true); } else { onDOMReady(); } }, /** - * Show a message if the inspector is dirty. - */ - preventNavigateAway: function InspectorPanel_preventNavigateAway(event, request) { - if (!this.isDirty) { - return; - } - - request.suspend(); - - let notificationBox = null; - if (this.target.isLocalTab) { - let gBrowser = this.target.tab.ownerDocument.defaultView.gBrowser; - notificationBox = gBrowser.getNotificationBox(); - } - else { - notificationBox = this._toolbox.getNotificationBox(); - } - - let notification = notificationBox. - getNotificationWithValue("inspector-page-navigation"); - - if (notification) { - notificationBox.removeNotification(notification, true); - } - - let cancelRequest = function onCancelRequest() { - if (request) { - request.cancel(Cr.NS_BINDING_ABORTED); - request.resume(); // needed to allow the connection to be cancelled. - request = null; - } - }; - - let eventCallback = function onNotificationCallback(event) { - if (event == "removed") { - cancelRequest(); - } - }; - - let buttons = [ - { - id: "inspector.confirmNavigationAway.buttonLeave", - label: this.strings.GetStringFromName("confirmNavigationAway.buttonLeave"), - accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonLeaveAccesskey"), - callback: function onButtonLeave() { - if (request) { - request.resume(); - request = null; - } - }.bind(this), - }, - { - id: "inspector.confirmNavigationAway.buttonStay", - label: this.strings.GetStringFromName("confirmNavigationAway.buttonStay"), - accessKey: this.strings.GetStringFromName("confirmNavigationAway.buttonStayAccesskey"), - callback: cancelRequest - }, - ]; - - let message = this.strings.GetStringFromName("confirmNavigationAway.message2"); - - notification = notificationBox.appendNotification(message, - "inspector-page-navigation", "chrome://browser/skin/Info.png", - notificationBox.PRIORITY_WARNING_HIGH, buttons, eventCallback); - - // Make sure this not a transient notification, to avoid the automatic - // transient notification removal. - notification.persistence = -1; - }, - - /** * When a new node is selected. */ onNewSelection: function InspectorPanel_onNewSelection() { this.cancelLayoutChange(); }, /** * When a new node is selected, before the selection has changed. @@ -374,17 +313,16 @@ InspectorPanel.prototype = { this.cancelLayoutChange(); if (this.browser) { this.browser.removeEventListener("resize", this.scheduleLayoutChange, true); this.browser = null; } - this.target.off("will-navigate", this.preventNavigateAway); this.target.off("navigate", this.onNavigatedAway); if (this.highlighter) { this.highlighter.off("locked", this.onLockStateChanged); this.highlighter.off("unlocked", this.onLockStateChanged); this.highlighter.destroy(); } @@ -395,107 +333,38 @@ InspectorPanel.prototype = { this._toolbox = null; this.sidebar.off("select", this._setDefaultSidebar); this.sidebar.destroy(); this.sidebar = null; this.nodemenu.removeEventListener("popupshowing", this._setupNodeMenu, true); this.nodemenu.removeEventListener("popuphiding", this._resetNodeMenu, true); - this.searchBox.removeEventListener("command", this._onHTMLSearch, true); - this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); this.breadcrumbs.destroy(); + this.searchSuggestions.destroy(); this.selection.off("new-node", this.onNewSelection); this.selection.off("before-new-node", this.onBeforeNewSelection); this.selection.off("detached", this.onDetached); this._destroyMarkup(); this._selection.destroy(); this._selection = null; this.panelWin.inspector = null; this.target = null; this.panelDoc = null; this.panelWin = null; this.breadcrumbs = null; + this.searchSuggestions = null; this.lastNodemenuItem = null; this.nodemenu = null; - this.searchBox = null; this.highlighter = null; - this._searchResults = null; return Promise.resolve(null); }, /** - * The command callback for the HTML search box. This function is - * automatically invoked as the user is typing. - */ - _onHTMLSearch: function InspectorPanel__onHTMLSearch() { - let query = this.searchBox.value; - if (query == this._lastSearched) { - return; - } - this._lastSearched = query; - this._searchIndex = 0; - - if (query.length == 0) { - this.searchBox.removeAttribute("filled"); - this.searchBox.classList.remove("devtools-no-search-result"); - return; - } - - this.searchBox.setAttribute("filled", true); - this._searchResults = this.browser.contentDocument.querySelectorAll(query); - if (this._searchResults.length > 0) { - this.searchBox.classList.remove("devtools-no-search-result"); - this.cancelLayoutChange(); - this.selection.setNode(this._searchResults[0]); - } else { - this.searchBox.classList.add("devtools-no-search-result"); - } - }, - - /** - * Search for the search box value as a query selector. - */ - _onSearchKeypress: function InspectorPanel__onSearchKeypress(aEvent) { - let query = this.searchBox.value; - switch(aEvent.keyCode) { - case aEvent.DOM_VK_ENTER: - case aEvent.DOM_VK_RETURN: - if (query == this._lastSearched) { - this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; - } else { - this._onHTMLSearch(); - return; - } - break; - - case aEvent.DOM_VK_UP: - if (--this._searchIndex < 0) { - this._searchIndex = this._searchResults.length - 1; - } - break; - - case aEvent.DOM_VK_DOWN: - this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; - break; - - default: - return; - } - - aEvent.preventDefault(); - aEvent.stopPropagation(); - this.cancelLayoutChange(); - if (this._searchResults.length > 0) { - this.selection.setNode(this._searchResults[this._searchIndex]); - } - }, - - /** * Show the node menu. */ showNodeMenu: function InspectorPanel_showNodeMenu(aButton, aPosition, aExtraItems) { if (aExtraItems) { for (let item of aExtraItems) { this.nodemenu.appendChild(item); } }
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/SelectorSearch.jsm @@ -0,0 +1,549 @@ +/* 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 Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", + "resource:///modules/devtools/AutocompletePopup.jsm"); +this.EXPORTED_SYMBOLS = ["SelectorSearch"]; + +// Maximum number of selector suggestions shown in the panel. +const MAX_SUGGESTIONS = 15; + +/** + * Converts any input box on a page to a CSS selector search and suggestion box. + * + * @constructor + * @param nsIDOMDocument aContentDocument + * The content document which inspector is attached to. + * @param nsiInputElement aInputNode + * The input element to which the panel will be attached and from where + * search input will be taken. + * @param Function aCallback + * The method to callback when a search is available. + * This method is called with the matched node as the first argument. + */ +this.SelectorSearch = function(aContentDocument, aInputNode, aCallback) { + this.doc = aContentDocument; + this.callback = aCallback; + this.searchBox = aInputNode; + this.panelDoc = this.searchBox.ownerDocument; + + // initialize variables. + this._lastSearched = null; + this._lastValidSearch = ""; + this._lastToLastValidSearch = null; + this._searchResults = null; + this._searchSuggestions = {}; + this._searchIndex = 0; + + // bind! + this._showPopup = this._showPopup.bind(this); + this._onHTMLSearch = this._onHTMLSearch.bind(this); + this._onSearchKeypress = this._onSearchKeypress.bind(this); + this._onListBoxKeypress = this._onListBoxKeypress.bind(this); + + // Options for the AutocompletePopup. + let options = { + panelId: "inspector-searchbox-panel", + listBoxId: "searchbox-panel-listbox", + fixedWidth: true, + autoSelect: true, + position: "before_start", + direction: "ltr", + onClick: this._onListBoxKeypress, + onKeypress: this._onListBoxKeypress, + }; + this.searchPopup = new AutocompletePopup(this.panelDoc, options); + + // event listeners. + this.searchBox.addEventListener("command", this._onHTMLSearch, true); + this.searchBox.addEventListener("keypress", this._onSearchKeypress, true); +} + +this.SelectorSearch.prototype = { + + // The possible states of the query. + States: { + CLASS: "class", + ID: "id", + TAG: "tag", + }, + + // The current state of the query. + _state: null, + + // The query corresponding to last state computation. + _lastStateCheckAt: null, + + /** + * Computes the state of the query. State refers to whether the query + * currently requires a class suggestion, or a tag, or an Id suggestion. + * This getter will effectively compute the state by traversing the query + * character by character each time the query changes. + * + * @example + * '#f' requires an Id suggestion, so the state is States.ID + * 'div > .foo' requires class suggestion, so state is States.CLASS + */ + get state() { + if (!this.searchBox || !this.searchBox.value) { + return null; + } + + let query = this.searchBox.value; + if (this._lastStateCheckAt == query) { + // If query is the same, return early. + return this._state; + } + this._lastStateCheckAt = query; + + this._state = null; + let subQuery = ""; + // Now we iterate over the query and decide the state character by character. + // The logic here is that while iterating, the state can go from one to + // another with some restrictions. Like, if the state is Class, then it can + // never go to Tag state without a space or '>' character; Or like, a Class + // state with only '.' cannot go to an Id state without any [a-zA-Z] after + // the '.' which means that '.#' is a selector matching a class name '#'. + // Similarily for '#.' which means a selctor matching an id '.'. + for (let i = 1; i <= query.length; i++) { + // Calculate the state. + subQuery = query.slice(0, i); + let [secondLastChar, lastChar] = subQuery.slice(-2); + switch (this._state) { + case null: + // This will happen only in the first iteration of the for loop. + lastChar = secondLastChar; + case this.States.TAG: + this._state = lastChar == "." + ? this.States.CLASS + : lastChar == "#" + ? this.States.ID + : this.States.TAG; + break; + + case this.States.CLASS: + if (subQuery.match(/[\.]+[^\.]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the '.'. + this._state = (lastChar == " " || lastChar == ">") + ? this.States.TAG + : lastChar == "#" + ? this.States.ID + : this.States.CLASS; + } + break; + + case this.States.ID: + if (subQuery.match(/[#]+[^#]*$/)[0].length > 2) { + // Checks whether the subQuery has atleast one [a-zA-Z] after the '#'. + this._state = (lastChar == " " || lastChar == ">") + ? this.States.TAG + : lastChar == "." + ? this.States.CLASS + : this.States.ID; + } + break; + } + } + return this._state; + }, + + /** + * Removes event listeners and cleans up references. + */ + destroy: function SelectorSearch_destroy() { + // event listeners. + this.searchBox.removeEventListener("command", this._onHTMLSearch, true); + this.searchBox.removeEventListener("keypress", this._onSearchKeypress, true); + this.searchPopup.destroy(); + this.searchPopup = null; + this.searchBox = null; + this.doc = null; + this.panelDoc = null; + this._searchResults = null; + this._searchSuggestions = null; + this.callback = null; + }, + + /** + * The command callback for the input box. This function is automatically + * invoked as the user is typing if the input box type is search. + */ + _onHTMLSearch: function SelectorSearch__onHTMLSearch() { + let query = this.searchBox.value; + if (query == this._lastSearched) { + return; + } + this._lastSearched = query; + this._searchIndex = 0; + + if (query.length == 0) { + this._lastValidSearch = ""; + this.searchBox.removeAttribute("filled"); + this.searchBox.classList.remove("devtools-no-search-result"); + if (this.searchPopup.isOpen) { + this.searchPopup.hidePopup(); + } + return; + } + + this.searchBox.setAttribute("filled", true); + try { + this._searchResults = this.doc.querySelectorAll(query); + } + catch (ex) { + this._searchResults = []; + } + if (this._searchResults.length > 0) { + this._lastValidSearch = query; + // Even though the selector matched atleast one node, there is still + // possibility of suggestions. + if (query.match(/[\s>+]$/)) { + // If the query has a space or '>' at the end, create a selector to match + // the children of the selector inside the search box by adding a '*'. + this._lastValidSearch += "*"; + } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + // If the query is a partial descendant selector which does not matches + // any node, remove the last incomplete part and add a '*' to match + // everything. For ex, convert 'foo > b' to 'foo > *' . + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + + if (!query.slice(-1).match(/[\.#\s>+]/)) { + // Hide the popup if we have some matching nodes and the query is not + // ending with [.# >] which means that the selector is not at the + // beginning of a new class, tag or id. + if (this.searchPopup.isOpen) { + this.searchPopup.hidePopup(); + } + } + else { + this.showSuggestions(); + } + this.searchBox.classList.remove("devtools-no-search-result"); + this.callback(this._searchResults[0]); + } + else { + if (query.match(/[\s>+]$/)) { + this._lastValidSearch = query + "*"; + } + else if (query.match(/[\s>+][\.#a-zA-Z][\.#>\s+]*$/)) { + let lastPart = query.match(/[\s+>][\.#a-zA-Z][^>\s+]*$/)[0]; + this._lastValidSearch = query.slice(0, -1 * lastPart.length + 1) + "*"; + } + this.searchBox.classList.add("devtools-no-search-result"); + this.showSuggestions(); + } + }, + + /** + * Handles keypresses inside the input box. + */ + _onSearchKeypress: function SelectorSearch__onSearchKeypress(aEvent) { + let query = this.searchBox.value; + switch(aEvent.keyCode) { + case aEvent.DOM_VK_ENTER: + case aEvent.DOM_VK_RETURN: + if (query == this._lastSearched) { + this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; + } + else { + this._onHTMLSearch(); + return; + } + break; + + case aEvent.DOM_VK_UP: + if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) { + this.searchPopup.focus(); + if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { + this.searchPopup.selectedIndex = + Math.max(0, this.searchPopup.itemCount - 2); + } + else { + this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1; + } + this.searchBox.value = this.searchPopup.selectedItem.label; + } + else if (--this._searchIndex < 0) { + this._searchIndex = this._searchResults.length - 1; + } + break; + + case aEvent.DOM_VK_DOWN: + if (this.searchPopup.isOpen && this.searchPopup.itemCount > 0) { + this.searchPopup.focus(); + this.searchPopup.selectedIndex = 0; + this.searchBox.value = this.searchPopup.selectedItem.label; + } + this._searchIndex = (this._searchIndex + 1) % this._searchResults.length; + break; + + case aEvent.DOM_VK_TAB: + if (this.searchPopup.isOpen && + this.searchPopup.getItemAtIndex(this.searchPopup.itemCount - 1) + .preLabel == query) { + this.searchPopup.selectedIndex = this.searchPopup.itemCount - 1; + this.searchBox.value = this.searchPopup.selectedItem.label; + this._onHTMLSearch(); + } + break; + + case aEvent.DOM_VK_BACK_SPACE: + case aEvent.DOM_VK_DELETE: + // need to throw away the lastValidSearch. + this._lastToLastValidSearch = null; + // This gets the most complete selector from the query. For ex. + // '.foo.ba' returns '.foo' , '#foo > .bar.baz' returns '#foo > .bar' + // '.foo +bar' returns '.foo +' and likewise. + this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || + query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || + ["",""])[1]; + return; + + default: + return; + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + if (this._searchResults.length > 0) { + this.callback(this._searchResults[this._searchIndex]); + } + }, + + /** + * Handles keypress and mouse click on the suggestions richlistbox. + */ + _onListBoxKeypress: function SelectorSearch__onListBoxKeypress(aEvent) { + switch(aEvent.keyCode || aEvent.button) { + case aEvent.DOM_VK_ENTER: + case aEvent.DOM_VK_RETURN: + case aEvent.DOM_VK_TAB: + case 0: // left mouse button + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.value = this.searchPopup.selectedItem.label; + this.searchBox.focus(); + this._onHTMLSearch(); + break; + + case aEvent.DOM_VK_UP: + if (this.searchPopup.selectedIndex == 0) { + this.searchPopup.selectedIndex = -1; + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + } + else { + let index = this.searchPopup.selectedIndex; + this.searchBox.value = this.searchPopup.getItemAtIndex(index - 1).label; + } + break; + + case aEvent.DOM_VK_DOWN: + if (this.searchPopup.selectedIndex == this.searchPopup.itemCount - 1) { + this.searchPopup.selectedIndex = -1; + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + } + else { + let index = this.searchPopup.selectedIndex; + this.searchBox.value = this.searchPopup.getItemAtIndex(index + 1).label; + } + break; + + case aEvent.DOM_VK_BACK_SPACE: + aEvent.stopPropagation(); + aEvent.preventDefault(); + this.searchBox.focus(); + if (this.searchBox.selectionStart > 0) { + this.searchBox.value = + this.searchBox.value.substring(0, this.searchBox.selectionStart - 1); + } + this._lastToLastValidSearch = null; + let query = this.searchBox.value; + this._lastValidSearch = (query.match(/(.*)[\.#][^\.# ]{0,}$/) || + query.match(/(.*[\s>+])[a-zA-Z][^\.# ]{0,}$/) || + ["",""])[1]; + this._onHTMLSearch(); + break; + } + }, + + + /** + * Populates the suggestions list and show the suggestion popup. + */ + _showPopup: function SelectorSearch__showPopup(aList, aFirstPart) { + // Sort alphabetically in increaseing order. + aList = aList.sort(); + // Sort based on count= in decreasing order. + aList = aList.sort(function([a1,a2], [b1,b2]) { + return a2 < b2; + }); + + let total = 0; + let query = this.searchBox.value; + let toLowerCase = false; + let items = []; + // In case of tagNames, change the case to small. + if (query.match(/.*[\.#][^\.#]{0,}$/) == null) { + toLowerCase = true; + } + for (let [value, count] of aList) { + // for cases like 'div ' or 'div >' or 'div+' + if (query.match(/[\s>+]$/)) { + value = query + value; + } + // for cases like 'div #a' or 'div .a' or 'div > d' and likewise + else if (query.match(/[\s>+][\.#a-zA-Z][^\s>+\.#]*$/)) { + let lastPart = query.match(/[\s>+][\.#a-zA-Z][^>\s+\.#]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } + // for cases like 'div.class' or '#foo.bar' and likewise + else if (query.match(/[a-zA-Z][#\.][^#\.\s+>]*$/)) { + let lastPart = query.match(/[a-zA-Z][#\.][^#\.\s>+]*$/)[0]; + value = query.slice(0, -1 * lastPart.length + 1) + value; + } + let item = { + preLabel: query, + label: value, + count: count + }; + if (toLowerCase) { + item.label = value.toLowerCase(); + } + items.unshift(item); + if (++total > MAX_SUGGESTIONS - 1) { + break; + } + } + if (total > 0) { + this.searchPopup.setItems(items); + this.searchPopup.openPopup(this.searchBox); + } + else { + this.searchPopup.hidePopup(); + } + }, + + /** + * Suggests classes,ids and tags based on the user input as user types in the + * searchbox. + */ + showSuggestions: function SelectorSearch_showSuggestions() { + let query = this.searchBox.value; + if (this._lastValidSearch != "" && + this._lastToLastValidSearch != this._lastValidSearch) { + this._searchSuggestions = { + ids: new Map(), + classes: new Map(), + tags: new Map(), + }; + + let nodes = []; + try { + nodes = this.doc.querySelectorAll(this._lastValidSearch); + } catch (ex) {} + for (let node of nodes) { + this._searchSuggestions.ids.set(node.id, 1); + this._searchSuggestions.tags + .set(node.tagName, + (this._searchSuggestions.tags.get(node.tagName) || 0) + 1); + for (let className of node.classList) { + this._searchSuggestions.classes + .set(className, + (this._searchSuggestions.classes.get(className) || 0) + 1); + } + } + this._lastToLastValidSearch = this._lastValidSearch; + } + else if (this._lastToLastValidSearch != this._lastValidSearch) { + this._searchSuggestions = { + ids: new Map(), + classes: new Map(), + tags: new Map(), + }; + + if (query.length == 0) { + return; + } + + let nodes = null; + if (this.state == this.States.CLASS) { + nodes = this.doc.querySelectorAll("[class]"); + for (let node of nodes) { + for (let className of node.classList) { + this._searchSuggestions.classes + .set(className, + (this._searchSuggestions.classes.get(className) || 0) + 1); + } + } + } + else if (this.state == this.States.ID) { + nodes = this.doc.querySelectorAll("[id]"); + for (let node of nodes) { + this._searchSuggestions.ids.set(node.id, 1); + } + } + else if (this.state == this.States.TAG) { + nodes = this.doc.getElementsByTagName("*"); + for (let node of nodes) { + this._searchSuggestions.tags + .set(node.tagName, + (this._searchSuggestions.tags.get(node.tagName) || 0) + 1); + } + } + else { + return; + } + this._lastToLastValidSearch = this._lastValidSearch; + } + + // Filter the suggestions based on search box value. + let result = []; + let firstPart = ""; + if (this.state == this.States.TAG) { + // gets the tag that is being completed. For ex. 'div.foo > s' returns 's', + // 'di' returns 'di' and likewise. + firstPart = (query.match(/[\s>+]?([a-zA-Z]*)$/) || ["",query])[1]; + for (let [tag, count] of this._searchSuggestions.tags) { + if (tag.toLowerCase().startsWith(firstPart.toLowerCase())) { + result.push([tag, count]); + } + } + } + else if (this.state == this.States.CLASS) { + // gets the class that is being completed. For ex. '.foo.b' returns 'b' + firstPart = query.match(/\.([^\.]*)$/)[1]; + for (let [className, count] of this._searchSuggestions.classes) { + if (className.startsWith(firstPart)) { + result.push(["." + className, count]); + } + } + firstPart = "." + firstPart; + } + else if (this.state == this.States.ID) { + // gets the id that is being completed. For ex. '.foo#b' returns 'b' + firstPart = query.match(/#([^#]*)$/)[1]; + for (let [id, count] of this._searchSuggestions.ids) { + if (id.startsWith(firstPart)) { + result.push(["#" + id, 1]); + } + } + firstPart = "#" + firstPart; + } + + this._showPopup(result, firstPart); + }, +};
--- a/browser/devtools/inspector/inspector.css +++ b/browser/devtools/inspector/inspector.css @@ -1,8 +1,28 @@ /* vim:set ts=2 sw=2 sts=2 et: */ /* 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/. */ #inspector-sidebar { min-width: 250px; } + +#searchbox-panel-listbox { + width: 250px; + max-width: 250px; + overflow-x: hidden; +} + +#searchbox-panel-listbox > richlistitem, +#searchbox-panel-listbox > richlistitem[selected] { + overflow-x: hidden; +} + +#searchbox-panel-listbox > richlistitem > .initial-value { + max-width: 130px; + margin-left: 15px; +} + +#searchbox-panel-listbox > richlistitem > .autocomplete-value { + max-width: 150px; +}
--- a/browser/devtools/inspector/test/Makefile.in +++ b/browser/devtools/inspector/test/Makefile.in @@ -23,27 +23,30 @@ include $(topsrcdir)/config/rules.mk browser_inspector_invalidate.js \ browser_inspector_menu.js \ browser_inspector_menu.html \ browser_inspector_pseudoClass_menu.js \ browser_inspector_destroyselection.html \ browser_inspector_destroyselection.js \ browser_inspector_bug_699308_iframe_navigation.js \ browser_inspector_bug_672902_keyboard_shortcuts.js \ - browser_inspector_bug_566084_location_changed.js \ browser_inspector_sidebarstate.js \ browser_inspector_pseudoclass_lock.js \ browser_inspector_cmd_inspect.js \ browser_inspector_cmd_inspect.html \ browser_inspector_highlighter_autohide.js \ browser_inspector_changes.js \ browser_inspector_bug_674871.js \ browser_inspector_bug_817558_delete_node.js \ browser_inspector_bug_650804_search.js \ browser_inspector_bug_650804_search.html \ + browser_inspector_bug_831693_input_suggestion.js \ + browser_inspector_bug_831693_searchbox_panel_navigation.js \ + browser_inspector_bug_831693_combinator_suggestions.js \ + browser_inspector_bug_831693_search_suggestions.html \ browser_inspector_bug_835722_infobar_reappears.js \ browser_inspector_bug_840156_destroy_after_navigation.js \ head.js \ helpers.js \ $(NULL) libs:: $(_BROWSER_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
deleted file mode 100644 --- a/browser/devtools/inspector/test/browser_inspector_bug_566084_location_changed.js +++ /dev/null @@ -1,131 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - -let tempScope = {}; -Cu.import("resource:///modules/devtools/Target.jsm", tempScope); -let TargetFactory = tempScope.TargetFactory; - -function test() { - let notificationBox, inspector; - let alertActive1_called = false; - let alertActive2_called = false; - - function startLocationTests() { - openInspector(runInspectorTests); - } - - function runInspectorTests(aInspector) { - inspector = aInspector; - - let para = content.document.querySelector("p"); - ok(para, "found the paragraph element"); - is(para.textContent, "init", "paragraph content is correct"); - - inspector.markDirty(); - - let target = TargetFactory.forTab(gBrowser.selectedTab); - let toolbox = gDevTools.getToolbox(target); - notificationBox = gBrowser.getNotificationBox(); - notificationBox.addEventListener("AlertActive", alertActive1, false); - - ok(toolbox, "We have access to the notificationBox"); - - gBrowser.selectedBrowser.addEventListener("load", onPageLoad, true); - - content.location = "data:text/html,<div>location change test 1 for " + - "inspector</div><p>test1</p>"; - } - - function alertActive1() { - alertActive1_called = true; - notificationBox.removeEventListener("AlertActive", alertActive1, false); - - let notification = notificationBox. - getNotificationWithValue("inspector-page-navigation"); - ok(notification, "found the inspector-page-navigation notification"); - - // By closing the notification it is expected that page navigation is - // canceled. - executeSoon(function() { - notification.close(); - locationTest2(); - }); - } - - function locationTest2() { - // Location did not change. - let para = content.document.querySelector("p"); - ok(para, "found the paragraph element, second time"); - is(para.textContent, "init", "paragraph content is correct"); - - let target = TargetFactory.forTab(gBrowser.selectedTab); - let inspector = gDevTools.getToolbox(target).getPanel("inspector"); - ok(inspector, "Inspector still alive"); - - notificationBox.addEventListener("AlertActive", alertActive2, false); - - content.location = "data:text/html,<div>location change test 2 for " + - "inspector</div><p>test2</p>"; - } - - function alertActive2() { - alertActive2_called = true; - notificationBox.removeEventListener("AlertActive", alertActive2, false); - - let notification = notificationBox. - getNotificationWithValue("inspector-page-navigation"); - ok(notification, "found the inspector-page-navigation notification"); - - let buttons = notification.querySelectorAll("button"); - let buttonLeave = null; - for (let i = 0; i < buttons.length; i++) { - if (buttons[i].buttonInfo.id == "inspector.confirmNavigationAway.buttonLeave") { - buttonLeave = buttons[i]; - break; - } - } - - ok(buttonLeave, "the Leave page button was found"); - - // Accept page navigation. - executeSoon(function(){ - buttonLeave.doCommand(); - }); - } - - function onPageLoad() { - gBrowser.selectedBrowser.removeEventListener("load", onPageLoad, true); - - isnot(content.location.href.indexOf("test2"), -1, - "page navigated to the correct location"); - - let para = content.document.querySelector("p"); - ok(para, "found the paragraph element, third time"); - is(para.textContent, "test2", "paragraph content is correct"); - - let root = content.document.documentElement; - is(inspector.selection.node, root, "Selection is the root of the new page."); - - ok(alertActive1_called, "first notification box has been showed"); - ok(alertActive2_called, "second notification box has been showed"); - testEnd(); - } - - - function testEnd() { - notificationBox = null; - gBrowser.removeCurrentTab(); - executeSoon(finish); - } - - waitForExplicitFinish(); - - gBrowser.selectedTab = gBrowser.addTab(); - gBrowser.selectedBrowser.addEventListener("load", function onBrowserLoad() { - gBrowser.selectedBrowser.removeEventListener("load", onBrowserLoad, true); - waitForFocus(startLocationTests, content); - }, true); - - content.location = "data:text/html,<div>location change tests for " + - "inspector.</div><p>init</p>"; -}
--- a/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js +++ b/browser/devtools/inspector/test/browser_inspector_bug_650804_search.js @@ -15,27 +15,27 @@ function test() // is the searched text valid selector // ] let keyStates = [ ["d", "b1", false], ["i", "b1", false], ["v", "d1", true], ["VK_DOWN", "d2", true], ["VK_ENTER", "d1", true], - [".", "d1", true], + [".", "d1", false], ["c", "d1", false], ["1", "d2", true], ["VK_DOWN", "d2", true], ["VK_BACK_SPACE", "d2", false], ["VK_BACK_SPACE", "d2", false], ["VK_BACK_SPACE", "d1", true], ["VK_BACK_SPACE", "d1", false], ["VK_BACK_SPACE", "d1", false], ["VK_BACK_SPACE", "d1", true], - [".", "d1", true], + [".", "d1", false], ["c", "d1", false], ["1", "d2", true], ["VK_DOWN", "s2", true], ["VK_DOWN", "p1", true], ["VK_UP", "s2", true], ["VK_UP", "d2", true], ["VK_UP", "p1", true], ["VK_BACK_SPACE", "p1", false],
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_combinator_suggestions.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() +{ + waitForExplicitFinish(); + + let inspector, searchBox, state, popup; + + // The various states of the inspector: [key, suggestions array] + // [ + // what key to press, + // suggestions array with count [ + // [suggestion1, count1], [suggestion2] ... + // ] count can be left to represent 1 + // ] + let keyStates = [ + ["d", [["div", 4]]], + ["i", [["div", 4]]], + ["v", []], + [" ", [["div div", 2], ["div span", 2]]], + [">", [["div >div", 2], ["div >span", 2]]], + ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]], + ["+", [["div +span"]]], + ["VK_BACK_SPACE", [["div div", 2], ["div span", 2]]], + ["VK_BACK_SPACE", []], + ["VK_BACK_SPACE", [["div", 4]]], + ["VK_BACK_SPACE", [["div", 4]]], + ["VK_BACK_SPACE", []], + ["p", []], + [" ", [["p strong"]]], + ["+", [["p +button"], ["p +p"]]], + ["b", [["p +button"]]], + ["u", [["p +button"]]], + ["t", [["p +button"]]], + ["t", [["p +button"]]], + ["o", [["p +button"]]], + ["n", []], + ["+", [["p +button+p"]]], + ]; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + waitForFocus(setupTest, content); + }, true); + + content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html"; + + function $(id) { + if (id == null) return null; + return content.document.getElementById(id); + } + + function setupTest() + { + openInspector(startTest); + } + + function startTest(aInspector) + { + inspector = aInspector; + searchBox = + inspector.panelWin.document.getElementById("inspector-searchbox"); + popup = inspector.searchSuggestions.searchPopup; + + focusSearchBoxUsingShortcut(inspector.panelWin, function() { + searchBox.addEventListener("command", checkState, true); + checkStateAndMoveOn(0); + }); + } + + function checkStateAndMoveOn(index) { + if (index == keyStates.length) { + finishUp(); + return; + } + + let [key, suggestions] = keyStates[index]; + state = index; + + info("pressing key " + key + " to get suggestions " + + JSON.stringify(suggestions)); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + } + + function checkState(event) { + executeSoon(function() { + let [key, suggestions] = keyStates[state]; + let actualSuggestions = popup.getItems(); + is(popup._panel.state == "open" || popup._panel.state == "showing" + ? actualSuggestions.length: 0, suggestions.length, + "There are expected number of suggestions at " + state + "th step."); + actualSuggestions = actualSuggestions.reverse(); + for (let i = 0; i < suggestions.length; i++) { + is(suggestions[i][0], actualSuggestions[i].label, + "The suggestion at " + i + "th index for " + state + + "th step is correct.") + is(suggestions[i][1] || 1, actualSuggestions[i].count, + "The count for suggestion at " + i + "th index for " + state + + "th step is correct.") + } + checkStateAndMoveOn(state + 1); + }); + } + + function finishUp() { + searchBox = null; + popup = null; + gBrowser.removeCurrentTab(); + finish(); + } +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_input_suggestion.js @@ -0,0 +1,115 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() +{ + waitForExplicitFinish(); + + let inspector, searchBox, state, popup; + + // The various states of the inspector: [key, suggestions array] + // [ + // what key to press, + // suggestions array with count [ + // [suggestion1, count1], [suggestion2] ... + // ] count can be left to represent 1 + // ] + let keyStates = [ + ["d", [["div", 2]]], + ["i", [["div", 2]]], + ["v", []], + [".", [["div.c1"]]], + ["VK_BACK_SPACE", []], + ["#", [["div#d1"], ["div#d2"]]], + ["VK_BACK_SPACE", []], + ["VK_BACK_SPACE", [["div", 2]]], + ["VK_BACK_SPACE", [["div", 2]]], + ["VK_BACK_SPACE", []], + [".", [[".c1", 3], [".c2"]]], + ["c", [[".c1", 3], [".c2"]]], + ["2", []], + ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]], + ["1", []], + ["#", [["#d2"], ["#p1"], ["#s2"]]], + ["VK_BACK_SPACE", []], + ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]], + ["VK_BACK_SPACE", [[".c1", 3], [".c2"]]], + ["VK_BACK_SPACE", []], + ["#", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]], + ["p", [["#p1"], ["#p2"], ["#p3"]]], + ["VK_BACK_SPACE", [["#b1"], ["#d1"], ["#d2"], ["#p1"], ["#p2"], ["#p3"], ["#s1"], ["#s2"]]], + ["VK_BACK_SPACE", []], + ]; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + waitForFocus(setupTest, content); + }, true); + + content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_650804_search.html"; + + function $(id) { + if (id == null) return null; + return content.document.getElementById(id); + } + + function setupTest() + { + openInspector(startTest); + } + + function startTest(aInspector) + { + inspector = aInspector; + searchBox = + inspector.panelWin.document.getElementById("inspector-searchbox"); + popup = inspector.searchSuggestions.searchPopup; + + focusSearchBoxUsingShortcut(inspector.panelWin, function() { + searchBox.addEventListener("command", checkState, true); + checkStateAndMoveOn(0); + }); + } + + function checkStateAndMoveOn(index) { + if (index == keyStates.length) { + finishUp(); + return; + } + + let [key, suggestions] = keyStates[index]; + state = index; + + info("pressing key " + key + " to get suggestions " + + JSON.stringify(suggestions)); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + } + + function checkState(event) { + executeSoon(function() { + let [key, suggestions] = keyStates[state]; + let actualSuggestions = popup.getItems(); + is(popup._panel.state == "open" || popup._panel.state == "showing" + ? actualSuggestions.length: 0, suggestions.length, + "There are expected number of suggestions at " + state + "th step."); + actualSuggestions = actualSuggestions.reverse(); + for (let i = 0; i < suggestions.length; i++) { + is(suggestions[i][0], actualSuggestions[i].label, + "The suggestion at " + i + "th index for " + state + + "th step is correct.") + is(suggestions[i][1] || 1, actualSuggestions[i].count, + "The count for suggestion at " + i + "th index for " + state + + "th step is correct.") + } + checkStateAndMoveOn(state + 1); + }); + } + + function finishUp() { + searchBox = null; + popup = null; + gBrowser.removeCurrentTab(); + finish(); + } +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html @@ -0,0 +1,27 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <title>Inspector Search Box Test</title> +</head> +<body> + <div id="d1"> + <div class="l1"> + <div id="d2" class="c1">Hello, I'm nested div</div> + </div> + </div> + <span id="s1">Hello, I'm a span + <div class="l1"> + <span>Hi I am a nested span</span> + <span class="s4">Hi I am a nested classed span</span> + </div> + </span> + <span class="c1" id="s2">And me</span> + + <p class="c1" id="p1">.someclass</p> + <p id="p2">#someid</p> + <button id="b1" disabled>button[disabled]</button> + <p id="p3" class="c2"><strong>p>strong</strong></p> + +</body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/inspector/test/browser_inspector_bug_831693_searchbox_panel_navigation.js @@ -0,0 +1,156 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() +{ + waitForExplicitFinish(); + requestLongerTimeout(2); + + let inspector, searchBox, state, panel; + let panelOpeningStates = [0, 3, 14, 17]; + let panelClosingStates = [2, 13, 16]; + + // The various states of the inspector: [key, query] + // [ + // what key to press, + // what should be the text in the searchbox + // ] + let keyStates = [ + ["d", "d"], + ["i", "di"], + ["v", "div"], + [".", "div."], + ["VK_UP", "div.c1"], + ["VK_DOWN", "div.l1"], + ["VK_DOWN", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_TAB", "div.l1"], + [" ", "div.l1 "], + ["VK_UP", "div.l1 DIV"], + ["VK_UP", "div.l1 DIV"], + [".", "div.l1 DIV."], + ["VK_TAB", "div.l1 DIV.c1"], + ["VK_BACK_SPACE", "div.l1 DIV.c"], + ["VK_BACK_SPACE", "div.l1 DIV."], + ["VK_BACK_SPACE", "div.l1 DIV"], + ["VK_BACK_SPACE", "div.l1 DI"], + ["VK_BACK_SPACE", "div.l1 D"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 DIV"], + ["VK_BACK_SPACE", "div.l1 DI"], + ["VK_BACK_SPACE", "div.l1 D"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_UP", "div.l1 DIV"], + ["VK_UP", "div.l1 DIV"], + ["VK_TAB", "div.l1 DIV"], + ["VK_BACK_SPACE", "div.l1 DI"], + ["VK_BACK_SPACE", "div.l1 D"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_DOWN", "div.l1 DIV"], + ["VK_DOWN", "div.l1 SPAN"], + ["VK_DOWN", "div.l1 SPAN"], + ["VK_BACK_SPACE", "div.l1 SPA"], + ["VK_BACK_SPACE", "div.l1 SP"], + ["VK_BACK_SPACE", "div.l1 S"], + ["VK_BACK_SPACE", "div.l1 "], + ["VK_BACK_SPACE", "div.l1"], + ["VK_BACK_SPACE", "div.l"], + ["VK_BACK_SPACE", "div."], + ["VK_BACK_SPACE", "div"], + ["VK_BACK_SPACE", "di"], + ["VK_BACK_SPACE", "d"], + ["VK_BACK_SPACE", ""], + ]; + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + waitForFocus(setupTest, content); + }, true); + + content.location = "http://mochi.test:8888/browser/browser/devtools/inspector/test/browser_inspector_bug_831693_search_suggestions.html"; + + function $(id) { + if (id == null) return null; + return content.document.getElementById(id); + } + + function setupTest() + { + openInspector(startTest); + } + + function startTest(aInspector) + { + inspector = aInspector; + searchBox = + inspector.panelWin.document.getElementById("inspector-searchbox"); + panel = inspector.searchSuggestions.searchPopup._list; + + focusSearchBoxUsingShortcut(inspector.panelWin, function() { + searchBox.addEventListener("keypress", checkState, true); + panel.addEventListener("keypress", checkState, true); + checkStateAndMoveOn(0); + }); + } + + function checkStateAndMoveOn(index) { + if (index == keyStates.length) { + finishUp(); + return; + } + + let [key, query] = keyStates[index]; + state = index; + + info("pressing key " + key + " to get searchbox value as " + query); + EventUtils.synthesizeKey(key, {}, inspector.panelWin); + } + + function checkState(event) { + if (panelOpeningStates.indexOf(state) != -1 && + !inspector.searchSuggestions.searchPopup.isOpen) { + info("Panel is not open, should wait before it shows up."); + panel.parentNode.addEventListener("popupshown", function retry() { + panel.parentNode.removeEventListener("popupshown", retry, false); + info("Panel is visible now"); + executeSoon(checkState); + }, false); + return; + } + else if (panelClosingStates.indexOf(state) != -1 && + panel.parentNode.state != "closed") { + info("Panel is open, should wait for it to close."); + panel.parentNode.addEventListener("popuphidden", function retry() { + panel.parentNode.removeEventListener("popuphidden", retry, false); + info("Panel is hidden now"); + executeSoon(checkState); + }, false); + return; + } + + // Using setTimout as the "command" event fires at delay after keypress + window.setTimeout(function() { + let [key, query] = keyStates[state]; + + if (searchBox.value == query) { + ok(true, "The suggestion at " + state + "th step on " + + "pressing " + key + " key is correct."); + } + else { + info("value is not correct, waiting longer for state " + state + + " with panel " + panel.parentNode.state); + checkState(); + return; + } + checkStateAndMoveOn(state + 1); + }, 200); + } + + function finishUp() { + searchBox = null; + panel = null; + gBrowser.removeCurrentTab(); + finish(); + } +}
rename from browser/devtools/webconsole/AutocompletePopup.jsm rename to browser/devtools/shared/AutocompletePopup.jsm --- a/browser/devtools/webconsole/AutocompletePopup.jsm +++ b/browser/devtools/shared/AutocompletePopup.jsm @@ -2,118 +2,148 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const Cu = Components.utils; // The XUL and XHTML namespace. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const XHTML_NS = "http://www.w3.org/1999/xhtml"; - -const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; - Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyGetter(this, "stringBundle", function () { - return Services.strings.createBundle(HUD_STRINGS_URI); -}); - - this.EXPORTED_SYMBOLS = ["AutocompletePopup"]; /** * Autocomplete popup UI implementation. * * @constructor * @param nsIDOMDocument aDocument * The document you want the popup attached to. + * @param Object aOptions + * An object consiting any of the following options: + * - panelId {String} The id for the popup panel. + * - listBoxId {String} The id for the richlistbox inside the panel. + * - position {String} The position for the popup panel. + * - theme {String} String related to the theme of the popup. + * - autoSelect {Boolean} Boolean to allow the first entry of the popup + * panel to be automatically selected when the popup shows. + * - fixedWidth {Boolean} Boolean to control dynamic width of the popup. + * - direction {String} The direction of the text in the panel. rtl or ltr + * - onSelect {String} The select event handler for the richlistbox + * - onClick {String} The click event handler for the richlistbox. + * - onKeypress {String} The keypress event handler for the richlistitems. */ -this.AutocompletePopup = function AutocompletePopup(aDocument) +this.AutocompletePopup = +function AutocompletePopup(aDocument, aOptions = {}) { this._document = aDocument; + this.fixedWidth = aOptions.fixedWidth || false; + this.autoSelect = aOptions.autoSelect || false; + this.position = aOptions.position || "after_start"; + this.direction = aOptions.direction || "ltr"; + + this.onSelect = aOptions.onSelect; + this.onClick = aOptions.onClick; + this.onKeypress = aOptions.onKeypress; + + let id = aOptions.panelId || "devtools_autoCompletePopup"; + let theme = aOptions.theme || "dark"; // Reuse the existing popup elements. - this._panel = this._document.getElementById("webConsole_autocompletePopup"); + this._panel = this._document.getElementById(id); if (!this._panel) { this._panel = this._document.createElementNS(XUL_NS, "panel"); - this._panel.setAttribute("id", "webConsole_autocompletePopup"); - this._panel.setAttribute("label", - stringBundle.GetStringFromName("Autocomplete.label")); + this._panel.setAttribute("id", id); + this._panel.className = "devtools-autocomplete-popup " + theme + "-theme"; + this._panel.setAttribute("noautofocus", "true"); - this._panel.setAttribute("ignorekeys", "true"); this._panel.setAttribute("level", "top"); + if (!aOptions.onKeypress) { + this._panel.setAttribute("ignorekeys", "true"); + } let mainPopupSet = this._document.getElementById("mainPopupSet"); if (mainPopupSet) { mainPopupSet.appendChild(this._panel); } else { this._document.documentElement.appendChild(this._panel); } + this._list = null; + } + else { + this._list = this._panel.firstChild; + } + if (!this._list) { this._list = this._document.createElementNS(XUL_NS, "richlistbox"); - this._list.flex = 1; this._panel.appendChild(this._list); // Open and hide the panel, so we initialize the API of the richlistbox. - this._panel.width = 1; - this._panel.height = 1; - this._panel.openPopup(null, "overlap", 0, 0, false, false); + this._panel.openPopup(null, this.popup, 0, 0); this._panel.hidePopup(); - this._panel.width = ""; - this._panel.height = ""; + } + + this._list.flex = 1; + this._list.setAttribute("seltype", "single"); + + if (aOptions.listBoxId) { + this._list.setAttribute("id", aOptions.listBoxId); } - else { - this._list = this._panel.firstChild; + this._list.className = "devtools-autocomplete-listbox " + theme + "-theme"; + + if (this.onSelect) { + this._list.addEventListener("select", this.onSelect, false); + } + + if (this.onClick) { + this._list.addEventListener("click", this.onClick, false); + } + + if (this.onKeypress) { + this._list.addEventListener("keypress", this.onKeypress, false); } } AutocompletePopup.prototype = { _document: null, _panel: null, _list: null, + // Event handlers. + onSelect: null, + onClick: null, + onKeypress: null, + /** * Open the autocomplete popup panel. * * @param nsIDOMNode aAnchor * Optional node to anchor the panel to. */ openPopup: function AP_openPopup(aAnchor) { - this._panel.openPopup(aAnchor, "after_start", 0, 0, false, false); - - if (this.onSelect) { - this._list.addEventListener("select", this.onSelect, false); - } + this._panel.openPopup(aAnchor, this.position, 0, 0); - if (this.onClick) { - this._list.addEventListener("click", this.onClick, false); + if (this.autoSelect) { + this.selectFirstItem(); } - - this._updateSize(); + if (!this.fixedWidth) { + this._updateSize(); + } }, /** * Hide the autocomplete popup panel. */ hidePopup: function AP_hidePopup() { this._panel.hidePopup(); - - if (this.onSelect) { - this._list.removeEventListener("select", this.onSelect, false); - } - - if (this.onClick) { - this._list.removeEventListener("click", this.onClick, false); - } }, /** * Check if the autocomplete popup is open. */ get isOpen() { return this._panel.state == "open"; }, @@ -126,24 +156,48 @@ AutocompletePopup.prototype = { */ destroy: function AP_destroy() { if (this.isOpen) { this.hidePopup(); } this.clearItems(); + if (this.onSelect) { + this._list.removeEventListener("select", this.onSelect, false); + } + + if (this.onClick) { + this._list.removeEventListener("click", this.onClick, false); + } + + if (this.onKeypress) { + this._list.removeEventListener("keypress", this.onKeypress, false); + } + this._document = null; this._list = null; this._panel = null; }, /** * Get the autocomplete items array. * + * @param Number aIndex The index of the item what is wanted. + * + * @return The autocomplete item at index aIndex. + */ + getItemAtIndex: function AP_getItemAtIndex(aIndex) + { + return this._list.getItemAtIndex(aIndex)._autocompleteItem; + }, + + /** + * Get the autocomplete items array. + * * @return array * The array of autocomplete items. */ getItems: function AP_getItems() { let items = []; Array.forEach(this._list.childNodes, function(aItem) { @@ -161,56 +215,88 @@ AutocompletePopup.prototype = { */ setItems: function AP_setItems(aItems) { this.clearItems(); aItems.forEach(this.appendItem, this); // Make sure that the new content is properly fitted by the XUL richlistbox. if (this.isOpen) { - // We need the timeout to allow the content to reflow. Attempting to - // update the richlistbox size too early does not work. - this._document.defaultView.setTimeout(this._updateSize.bind(this), 1); + if (this.autoSelect) { + this.selectFirstItem(); + } + if (!this.fixedWidth) { + this._updateSize(); + } + } + }, + + /** + * Selects the first item of the richlistbox. Note that first item here is the + * item closes to the input element, which means that 0th index if position is + * below, and last index if position is above. + */ + selectFirstItem: function AP_selectFirstItem() + { + if (this.position.contains("before")) { + this.selectedIndex = this.itemCount - 1; + } + else { + this.selectedIndex = 0; } }, /** * Update the panel size to fit the content. * * @private */ _updateSize: function AP__updateSize() { - if (!this._panel) { - return; - } - this._list.width = this._panel.clientWidth + - this._scrollbarWidth; + // We need the timeout to allow the content to reflow. Attempting to + // update the richlistbox size too early does not work. + this._document.defaultView.setTimeout(function() { + if (!this._panel) { + return; + } + this._list.width = this._panel.clientWidth + + this._scrollbarWidth; + // Height change is required, otherwise the panel is drawn at an offset + // the first time. + this._list.height = this._panel.clientHeight; + // This brings the panel back at right position. + this._list.top = 0; + // Changing panel height might make the selected item out of view, so + // bring it back to view. + this._list.ensureIndexIsVisible(this._list.selectedIndex); + }.bind(this), 5); }, /** * Clear all the items from the autocomplete list. */ clearItems: function AP_clearItems() { // Reset the selectedIndex to -1 before clearing the list this.selectedIndex = -1; while (this._list.hasChildNodes()) { this._list.removeChild(this._list.firstChild); } - // Reset the panel and list dimensions. New dimensions are calculated when a - // new set of items is added to the autocomplete popup. - this._list.width = ""; - this._list.height = ""; - this._panel.width = ""; - this._panel.height = ""; - this._panel.top = ""; - this._panel.left = ""; + if (!this.fixedWidth) { + // Reset the panel and list dimensions. New dimensions are calculated when + // a new set of items is added to the autocomplete popup. + this._list.width = ""; + this._list.height = ""; + this._panel.width = ""; + this._panel.height = ""; + this._panel.top = ""; + this._panel.left = ""; + } }, /** * Getter for the index of the selected item. * * @type number */ get selectedIndex() { @@ -220,17 +306,17 @@ AutocompletePopup.prototype = { /** * Setter for the selected index. * * @param number aIndex * The number (index) of the item you want to select in the list. */ set selectedIndex(aIndex) { this._list.selectedIndex = aIndex; - if (this._list.ensureIndexIsVisible) { + if (this.isOpen && this._list.ensureIndexIsVisible) { this._list.ensureIndexIsVisible(this._list.selectedIndex); } }, /** * Getter for the selected item. * @type object */ @@ -242,33 +328,61 @@ AutocompletePopup.prototype = { /** * Setter for the selected item. * * @param object aItem * The object you want selected in the list. */ set selectedItem(aItem) { this._list.selectedItem = this._findListItem(aItem); - this._list.ensureIndexIsVisible(this._list.selectedIndex); + if (this.isOpen) { + this._list.ensureIndexIsVisible(this._list.selectedIndex); + } }, /** * Append an item into the autocomplete list. * * @param object aItem - * The item you want appended to the list. The object must have a - * "label" property which is used as the displayed value. + * The item you want appended to the list. + * The item object can have the following properties: + * - label {String} Property which is used as the displayed value. + * - preLabel {String} [Optional] The String that will be displayed + * before the label indicating that this is the already + * present text in the input box, and label is the text + * that will be auto completed. When this property is + * present, |preLabel.length| starting characters will be + * removed from label. + * - count {Number} [Optional] The number to represent the count of + * autocompleted label. */ appendItem: function AP_appendItem(aItem) { - let description = this._document.createElementNS(XUL_NS, "description"); - description.textContent = aItem.label; - let listItem = this._document.createElementNS(XUL_NS, "richlistitem"); - listItem.appendChild(description); + if (this.direction) { + listItem.setAttribute("dir", this.direction); + } + let label = this._document.createElementNS(XUL_NS, "label"); + label.setAttribute("value", aItem.label); + label.setAttribute("class", "autocomplete-value"); + if (aItem.preLabel) { + let preDesc = this._document.createElementNS(XUL_NS, "label"); + preDesc.setAttribute("value", aItem.preLabel); + preDesc.setAttribute("class", "initial-value"); + listItem.appendChild(preDesc); + label.setAttribute("value", aItem.label.slice(aItem.preLabel.length)); + } + listItem.appendChild(label); + if (aItem.count && aItem.count > 1) { + let countDesc = this._document.createElementNS(XUL_NS, "label"); + countDesc.setAttribute("value", aItem.count); + countDesc.setAttribute("flex", "1"); + countDesc.setAttribute("class", "autocomplete-count"); + listItem.appendChild(countDesc); + } listItem._autocompleteItem = aItem; this._list.appendChild(listItem); }, /** * Find the richlistitem element that belongs to an item. * @@ -347,16 +461,24 @@ AutocompletePopup.prototype = { else { this.selectedIndex = this.itemCount - 1; } return this.selectedItem; }, /** + * Focuses the richlistbox. + */ + focus: function AP_focus() + { + this._list.focus(); + }, + + /** * Determine the scrollbar width in the current document. * * @private */ get _scrollbarWidth() { if (this.__scrollbarWidth) { return this.__scrollbarWidth;
--- a/browser/devtools/webconsole/Makefile.in +++ b/browser/devtools/webconsole/Makefile.in @@ -8,13 +8,12 @@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk EXTRA_JS_MODULES = \ HUDService.jsm \ PropertyPanel.jsm \ NetworkPanel.jsm \ - AutocompletePopup.jsm \ WebConsolePanel.jsm \ $(NULL) include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_keys.js @@ -36,17 +36,17 @@ function consoleOpened(aHud) { ok(popup.isOpen, "popup is open"); // 4 values, and the following properties: // __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ // hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString // toSource unwatch valueOf watch constructor. is(popup.itemCount, 18, "popup.itemCount is correct"); - let sameItems = popup.getItems().map(function(e) {return e.label;}); + let sameItems = popup.getItems().reverse().map(function(e) {return e.label;}); ok(sameItems.every(function(prop, index) { return [ "__defineGetter__", "__defineSetter__", "__lookupGetter__", "__lookupSetter__", "constructor", "hasOwnProperty", @@ -59,39 +59,41 @@ function consoleOpened(aHud) { "toLocaleString", "toSource", "toString", "unwatch", "valueOf", "watch", ][index] === prop}), "getItems returns the items we expect"); - is(popup.selectedIndex, -1, "no index is selected"); + is(popup.selectedIndex, 17, + "Index of the first item from bottom is selected."); + EventUtils.synthesizeKey("VK_DOWN", {}); EventUtils.synthesizeKey("VK_DOWN", {}); let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); is(popup.selectedIndex, 0, "index 0 is selected"); - is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected"); - is(completeNode.value, prefix + "__defineGetter__", - "completeNode.value holds __defineGetter__"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); EventUtils.synthesizeKey("VK_DOWN", {}); is(popup.selectedIndex, 1, "index 1 is selected"); - is(popup.selectedItem.label, "__defineSetter__", "__defineSetter__ is selected"); - is(completeNode.value, prefix + "__defineSetter__", - "completeNode.value holds __defineSetter__"); + is(popup.selectedItem.label, "valueOf", "valueOf is selected"); + is(completeNode.value, prefix + "valueOf", + "completeNode.value holds valueOf"); EventUtils.synthesizeKey("VK_UP", {}); is(popup.selectedIndex, 0, "index 0 is selected"); - is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected"); - is(completeNode.value, prefix + "__defineGetter__", - "completeNode.value holds __defineGetter__"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); popup._panel.addEventListener("popuphidden", autocompletePopupHidden, false); EventUtils.synthesizeKey("VK_TAB", {}); }, false); jsterm.setInputValue("window.foobarBug585991"); EventUtils.synthesizeKey(".", {}); @@ -103,37 +105,38 @@ function autocompletePopupHidden() let popup = jsterm.autocompletePopup; let completeNode = jsterm.completeNode; let inputNode = jsterm.inputNode; popup._panel.removeEventListener("popuphidden", autocompletePopupHidden, false); ok(!popup.isOpen, "popup is not open"); - is(inputNode.value, "window.foobarBug585991.__defineGetter__", + is(inputNode.value, "window.foobarBug585991.watch", "completion was successful after VK_TAB"); ok(!completeNode.value, "completeNode is empty"); popup._panel.addEventListener("popupshown", function onShown() { popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); is(popup.itemCount, 18, "popup.itemCount is correct"); - is(popup.selectedIndex, -1, "no index is selected"); + is(popup.selectedIndex, 17, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); EventUtils.synthesizeKey("VK_DOWN", {}); let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); is(popup.selectedIndex, 0, "index 0 is selected"); - is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected"); - is(completeNode.value, prefix + "__defineGetter__", - "completeNode.value holds __defineGetter__"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); popup._panel.addEventListener("popuphidden", function onHidden() { popup._panel.removeEventListener("popuphidden", onHidden, false); ok(!popup.isOpen, "popup is not open after VK_ESCAPE"); is(inputNode.value, "window.foobarBug585991.", "completion was cancelled"); @@ -163,39 +166,40 @@ function testReturnKey() popup._panel.addEventListener("popupshown", function onShown() { popup._panel.removeEventListener("popupshown", onShown, false); ok(popup.isOpen, "popup is open"); is(popup.itemCount, 18, "popup.itemCount is correct"); - is(popup.selectedIndex, -1, "no index is selected"); + is(popup.selectedIndex, 17, "First index from bottom is selected"); + EventUtils.synthesizeKey("VK_DOWN", {}); EventUtils.synthesizeKey("VK_DOWN", {}); let prefix = jsterm.inputNode.value.replace(/[\S]/g, " "); is(popup.selectedIndex, 0, "index 0 is selected"); - is(popup.selectedItem.label, "__defineGetter__", "__defineGetter__ is selected"); - is(completeNode.value, prefix + "__defineGetter__", - "completeNode.value holds __defineGetter__"); + is(popup.selectedItem.label, "watch", "watch is selected"); + is(completeNode.value, prefix + "watch", + "completeNode.value holds watch"); EventUtils.synthesizeKey("VK_DOWN", {}); is(popup.selectedIndex, 1, "index 1 is selected"); - is(popup.selectedItem.label, "__defineSetter__", "__defineSetter__ is selected"); - is(completeNode.value, prefix + "__defineSetter__", - "completeNode.value holds __defineSetter__"); + is(popup.selectedItem.label, "valueOf", "valueOf is selected"); + is(completeNode.value, prefix + "valueOf", + "completeNode.value holds valueOf"); popup._panel.addEventListener("popuphidden", function onHidden() { popup._panel.removeEventListener("popuphidden", onHidden, false); ok(!popup.isOpen, "popup is not open after VK_RETURN"); - is(inputNode.value, "window.foobarBug585991.__defineSetter__", + is(inputNode.value, "window.foobarBug585991.valueOf", "completion was successful after VK_RETURN"); ok(!completeNode.value, "completeNode is empty"); dontShowArrayNumbers(); }, false); EventUtils.synthesizeKey("VK_RETURN", {});
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js +++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js @@ -35,18 +35,19 @@ function consoleOpened(HUD) { is(popup.itemCount, items.length, "items added"); let sameItems = popup.getItems(); is(sameItems.every(function(aItem, aIndex) { return aItem === items[aIndex]; }), true, "getItems returns back the same items"); - is(popup.selectedIndex, -1, "no index is selected"); - ok(!popup.selectedItem, "no item is selected"); + is(popup.selectedIndex, 2, + "Index of the first item from bottom is selected."); + is(popup.selectedItem, items[2], "First item from bottom is selected"); popup.selectedIndex = 1; is(popup.selectedIndex, 1, "index 1 is selected"); is(popup.selectedItem, items[1], "item1 is selected"); popup.selectedItem = items[2];
--- a/browser/devtools/webconsole/test/browser_webconsole_completion.js +++ b/browser/devtools/webconsole/test/browser_webconsole_completion.js @@ -61,31 +61,31 @@ function testCompletion(hud) { // Test typing 'document.getElem'. input.value = "document.getElem"; input.setSelectionRange(16, 16); jsterm.complete(jsterm.COMPLETE_FORWARD, testNext); yield; is(input.value, "document.getElem", "'document.getElem' completion"); - is(jsterm.completeNode.value, " entById", "'document.getElem' completion"); + is(jsterm.completeNode.value, "", "'document.getElem' completion"); // Test pressing tab another time. jsterm.complete(jsterm.COMPLETE_FORWARD, testNext); yield; is(input.value, "document.getElem", "'document.getElem' completion"); - is(jsterm.completeNode.value, " entsByClassName", "'document.getElem' another tab completion"); + is(jsterm.completeNode.value, " entsByTagNameNS", "'document.getElem' another tab completion"); // Test pressing shift_tab. jsterm.complete(jsterm.COMPLETE_BACKWARD, testNext); yield; is(input.value, "document.getElem", "'document.getElem' untab completion"); - is(jsterm.completeNode.value, " entById", "'document.getElem' completion"); + is(jsterm.completeNode.value, "", "'document.getElem' completion"); jsterm.clearOutput(); input.value = "docu"; jsterm.complete(jsterm.COMPLETE_HINT_ONLY, testNext); yield; is(jsterm.completeNode.value, " ment", "'docu' completion");
--- a/browser/devtools/webconsole/webconsole.js +++ b/browser/devtools/webconsole/webconsole.js @@ -24,17 +24,17 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "PropertyTreeView", "resource:///modules/PropertyPanel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetworkPanel", "resource:///modules/NetworkPanel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AutocompletePopup", - "resource:///modules/AutocompletePopup.jsm"); + "resource:///modules/devtools/AutocompletePopup.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebConsoleUtils", "resource://gre/modules/devtools/WebConsoleUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js"); const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; @@ -2726,19 +2726,28 @@ JSTerm.prototype = { COMPLETE_HINT_ONLY: 2, /** * Initialize the JSTerminal UI. */ init: function JST_init() { let chromeDocument = this.hud.owner.chromeDocument; - this.autocompletePopup = new AutocompletePopup(chromeDocument); - this.autocompletePopup.onSelect = this.onAutocompleteSelect.bind(this); - this.autocompletePopup.onClick = this.acceptProposedCompletion.bind(this); + let autocompleteOptions = { + onSelect: this.onAutocompleteSelect.bind(this), + onClick: this.acceptProposedCompletion.bind(this), + panelId: "webConsole_autocompletePopup", + listBoxId: "webConsole_autocompletePopupListBox", + position: "before_start", + theme: "light", + direction: "ltr", + autoSelect: true + }; + this.autocompletePopup = new AutocompletePopup(chromeDocument, + autocompleteOptions); let doc = this.hud.document; this.completeNode = doc.querySelector(".jsterm-complete-node"); this.inputNode = doc.querySelector(".jsterm-input-node"); this.inputNode.addEventListener("keypress", this._keyPress, false); this.inputNode.addEventListener("input", this._inputEventHandler, false); this.inputNode.addEventListener("keyup", this._inputEventHandler, false); @@ -3474,32 +3483,33 @@ JSTerm.prototype = { let inputNode = this.inputNode; let inputValue = inputNode.value; if (this.lastCompletion.value == inputValue || aRequestId != this.lastCompletion.requestId) { return; } let matches = aMessage.matches; + let lastPart = aMessage.matchProp; if (!matches.length) { this.clearCompletion(); return; } - let items = matches.map(function(aMatch) { - return { label: aMatch }; + let items = matches.reverse().map(function(aMatch) { + return { preLabel: lastPart, label: aMatch }; }); let popup = this.autocompletePopup; popup.setItems(items); let completionType = this.lastCompletion.completionType; this.lastCompletion = { value: inputValue, - matchProp: aMessage.matchProp, + matchProp: lastPart, }; if (items.length > 1 && !popup.isOpen) { popup.openPopup(inputNode); } else if (items.length < 2 && popup.isOpen) { popup.hidePopup(); }
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties @@ -105,20 +105,16 @@ webConsoleMixedContentWarning=Mixed Cont # JavaScript is being entered, to indicate how to jump into scratchpad mode scratchpad.linkText=Shift+RETURN - Open in Scratchpad # LOCALIZATION NOTE (gcliterm.instanceLabel): The console displays # objects using their type (from the constructor function) in this descriptive # string gcliterm.instanceLabel=Instance of %S -# LOCALIZATION NOTE (Autocomplete.label): -# The autocomplete popup panel label/title. -Autocomplete.label=Autocomplete popup - # LOCALIZATION NOTE (stacktrace.anonymousFunction): # This string is used to display JavaScript functions that have no given name - # they are said to be anonymous. See stacktrace.outputMessage. stacktrace.anonymousFunction=<anonymous> # LOCALIZATION NOTE (stacktrace.outputMessage): # This string is used in the Web Console output to identify a web developer call # to console.trace(). The stack trace of JavaScript function calls is displayed.
--- a/browser/themes/linux/devtools/common.css +++ b/browser/themes/linux/devtools/common.css @@ -214,16 +214,20 @@ width: 3px; background-color: transparent; -moz-margin-end: -3px; position: relative; } /* In-tools sidebar */ +.devtools-toolbox-side-iframe { + min-width: 465px; +} + .devtools-sidebar-tabs { -moz-appearance: none; margin: 0; } .devtools-sidebar-tabs > tabpanels { -moz-appearance: none; padding: 0; @@ -355,8 +359,10 @@ .devtools-theme-attrname { color: hsl(208,56%,40%); /* blue */ } .devtools-theme-attrvalue { color: hsl(24,85%,39%); /* orange */ } + +%include ../../shared/devtools/common.inc.css
--- a/browser/themes/linux/devtools/toolbox.css +++ b/browser/themes/linux/devtools/toolbox.css @@ -145,48 +145,48 @@ } #toolbox-tabs { margin: 0; } .devtools-tab { -moz-appearance: none; - min-width: 88px; + width: 47px; + min-width: 47px; min-height: 32px; + max-width: 137px; color: #b6babf; margin: 0; padding: 0; background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)); background-size: 1px 100%; background-repeat: no-repeat; background-position: left, right; border-right: 1px solid hsla(206,37%,4%,.45); } -.devtools-tab > .radio-label-center-box > .radio-label-box { - -moz-appearance: none; +.devtools-tab > image { border: none; - padding: 0 16px; -} - -.devtools-tab > .radio-label-center-box >.radio-label-box > .radio-icon { -moz-margin-end: 6px; + -moz-margin-start: 16px; opacity: 0.6; } -.devtools-tab:hover > .radio-label-center-box > .radio-label-box > -.radio-icon { +.devtools-tab > label { + white-space: nowrap; +} + +.devtools-tab:hover > image { opacity: 0.8; } -.devtools-tab:active > .radio-label-center-box > .radio-label-box > .radio-icon, -.devtools-tab[selected=true] > .radio-label-center-box > .radio-label-box > -.radio-icon { +.devtools-tab:active > image, +.devtools-tab[selected=true] > label { opacity: 1; } .devtools-tab:hover { background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2)); background-size: 1px 100%,
--- a/browser/themes/osx/devtools/common.css +++ b/browser/themes/osx/devtools/common.css @@ -228,16 +228,20 @@ min-width: 0; width: 3px; -moz-margin-end: -3px; position: relative; } /* In-tools sidebar */ +.devtools-toolbox-side-iframe { + min-width: 465px; +} + .devtools-sidebar-tabs { -moz-appearance: none; margin: 0; } .devtools-sidebar-tabs > tabpanels { padding: 0; } @@ -374,8 +378,10 @@ .devtools-theme-attrname { color: hsl(208,56%,40%); /* blue */ } .devtools-theme-attrvalue { color: hsl(24,85%,39%); /* orange */ } + +%include ../../shared/devtools/common.inc.css
--- a/browser/themes/osx/devtools/toolbox.css +++ b/browser/themes/osx/devtools/toolbox.css @@ -134,44 +134,47 @@ #toolbox-tabs { margin: 0; border-left: 1px solid hsla(206,37%,4%,.45); } .devtools-tab { -moz-appearance: none; - min-width: 88px; + width: 47px; + min-width: 47px; min-height: 32px; + max-width: 137px; color: #b6babf; margin: 0; padding: 0; background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)); background-size: 1px 100%; background-repeat: no-repeat; background-position: left, right; border-right: 1px solid hsla(206,37%,4%,.45); } -.devtools-tab > .radio-label-box { - padding: 0 16px; -} - -.devtools-tab > .radio-label-box > .radio-icon { +.devtools-tab > image { -moz-margin-end: 6px; + -moz-margin-start: 16px; opacity: 0.6; } -.devtools-tab:hover > .radio-label-box > .radio-icon { +.devtools-tab > label { + white-space: nowrap; +} + +.devtools-tab:hover > image { opacity: 0.8; } -.devtools-tab:active > .radio-label-box > .radio-icon, -.devtools-tab[selected=true] > .radio-label-box > .radio-icon { +.devtools-tab:active > image, +.devtools-tab[selected=true] > image { opacity: 1; } .devtools-tab:hover { background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2)); background-size: 1px 100%,
new file mode 100644 --- /dev/null +++ b/browser/themes/shared/devtools/common.inc.css @@ -0,0 +1,85 @@ +%if 0 +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +%endif + +/* Autocomplete Popup */ +/* Dark and light theme */ + +.devtools-autocomplete-popup { + -moz-appearance: none !important; + border: 1px solid hsl(210,11%,10%); + box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset; + background-color: transparent; + background-image: linear-gradient(to bottom, hsla(209,18%,18%,0.9), hsl(210,11%,16%)); + border-radius: 3px; +%ifdef XP_LINUX + max-height: 32rem; +%else + max-height: 40rem; +%endif +} + +.devtools-autocomplete-listbox { + -moz-appearance: none !important; + background-color: transparent; + border-width: 0px !important; +} + +.devtools-autocomplete-listbox > richlistitem, +.devtools-autocomplete-listbox > richlistitem[selected] { + width: 100%; + background-color: transparent; + border-radius: 4px; +} + +.devtools-autocomplete-listbox.dark-theme > richlistitem[selected], +.devtools-autocomplete-listbox.dark-theme > richlistitem:hover { + background-color: rgba(0,0,0,0.5); +} + +.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > .autocomplete-value, +.devtools-autocomplete-listbox:focus.dark-theme > richlistitem[selected] > .initial-value { + color: hsl(208,100%,60%); +} + +.devtools-autocomplete-listbox.dark-theme > richlistitem[selected] > label { + color: #eee; +} + +.devtools-autocomplete-listbox.dark-theme > richlistitem > label { + color: #ccc; +} + +.devtools-autocomplete-listbox > richlistitem > .initial-value, +.devtools-autocomplete-listbox > richlistitem > .autocomplete-value { + margin: 0; + padding: 1px 0; +} + +.devtools-autocomplete-listbox > richlistitem > .autocomplete-count { + text-align: right; +} + +/* Rest of the light theme */ + +.devtools-autocomplete-popup.light-theme { + border: 1px solid hsl(210,24%,90%); + box-shadow: 0 1px 0 hsla(209,29%,90%,.25) inset; + background-image: linear-gradient(to bottom, hsla(209,18%,100%,0.9), hsl(210,24%,95%)); +} + +.devtools-autocomplete-listbox.light-theme > richlistitem[selected], +.devtools-autocomplete-listbox.light-theme > richlistitem:hover { + background-color: rgba(128,128,128,0.3); +} + +.devtools-autocomplete-listbox.light-theme > richlistitem[selected] > .autocomplete-value, +.devtools-autocomplete-listbox:focus.light-theme > richlistitem[selected] > .initial-value { + color: #222; +} + +.devtools-autocomplete-listbox.light-theme > richlistitem > label { + color: #666; +}
--- a/browser/themes/windows/devtools/common.css +++ b/browser/themes/windows/devtools/common.css @@ -234,16 +234,20 @@ width: 3px; background-color: transparent; -moz-margin-end: -3px; position: relative; } /* In-tools sidebar */ +.devtools-toolbox-side-iframe { + min-width: 465px; +} + .devtools-sidebar-tabs { -moz-appearance: none; margin: 0; } .devtools-sidebar-tabs > tabpanels { -moz-appearance: none; padding: 0; @@ -383,8 +387,10 @@ .devtools-theme-attrname { color: hsl(208,56%,40%); /* blue */ } .devtools-theme-attrvalue { color: hsl(24,85%,39%); /* orange */ } + +%include ../../shared/devtools/common.inc.css
--- a/browser/themes/windows/devtools/toolbox.css +++ b/browser/themes/windows/devtools/toolbox.css @@ -146,46 +146,44 @@ } #toolbox-tabs { margin: 0; } .devtools-tab { -moz-appearance: none; - min-width: 88px; + width: 47px; + min-width: 47px; min-height: 32px; + max-width: 137px; color: #b6babf; margin: 0; padding: 0; background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)); background-size: 1px 100%; background-repeat: no-repeat; background-position: left, right; border-top: 1px solid #060a0d; border-right: 1px solid hsla(206,37%,4%,.45); } -.devtools-tab > .radio-label-box { - border: none; - padding: 0 16px; -} - -.devtools-tab > .radio-label-box > .radio-icon { +.devtools-tab > image { -moz-margin-end: 6px; + -moz-margin-start: 16px; opacity: 0.6; } -.devtools-tab:hover > .radio-label-box > .radio-icon { +.devtools-tab:hover > image { opacity: 0.8; } -.devtools-tab:active > .radio-label-box > .radio-icon, -.devtools-tab[selected=true] > .radio-label-box > .radio-icon { +.devtools-tab:active > image, +.devtools-tab[selected=true] > image { opacity: 1; } .devtools-tab:hover { background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)), linear-gradient(hsla(206,37%,4%,.1), hsla(206,37%,4%,.2)); background-size: 1px 100%,
new file mode 100644 --- /dev/null +++ b/services/common/tests/unit/test_utils_exceptionStr.js @@ -0,0 +1,17 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function run_test() { + run_next_test(); +} + +add_test(function test_exceptionStr_non_exceptions() { + do_check_eq(CommonUtils.exceptionStr(null), "null"); + do_check_eq(CommonUtils.exceptionStr(false), "false"); + do_check_eq(CommonUtils.exceptionStr(undefined), "undefined"); + do_check_eq(CommonUtils.exceptionStr(12), "12 No traceback available"); + + run_next_test(); +});
--- a/services/common/tests/unit/xpcshell.ini +++ b/services/common/tests/unit/xpcshell.ini @@ -8,16 +8,17 @@ firefox-appdir = browser [test_utils_atob.js] [test_utils_convert_string.js] [test_utils_dateprefs.js] [test_utils_deepCopy.js] [test_utils_encodeBase32.js] [test_utils_encodeBase64URL.js] [test_utils_ensureMillisecondsTimestamp.js] +[test_utils_exceptionStr.js] [test_utils_json.js] [test_utils_makeURI.js] [test_utils_namedTimer.js] [test_utils_stackTrace.js] [test_utils_utf8.js] [test_utils_uuid.js] [test_async_chain.js]
--- a/services/common/utils.js +++ b/services/common/utils.js @@ -8,16 +8,19 @@ this.EXPORTED_SYMBOLS = ["CommonUtils"]; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/osfile.jsm") Cu.import("resource://services-common/log4moz.js"); this.CommonUtils = { exceptionStr: function exceptionStr(e) { + if (!e) { + return "" + e; + } let message = e.message ? e.message : e; return message + " " + CommonUtils.stackTrace(e); }, stackTrace: function stackTrace(e) { // Wrapped nsIException if (e.location) { let frame = e.location;
--- a/services/healthreport/healthreporter.jsm +++ b/services/healthreport/healthreporter.jsm @@ -23,16 +23,18 @@ Cu.import("resource://services-common/pr Cu.import("resource://services-common/utils.js"); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); Cu.import("resource://gre/modules/osfile.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel", + "resource://gre/modules/UpdateChannel.jsm"); // Oldest year to allow in date preferences. This module was implemented in // 2012 and no dates older than that should be encountered. const OLDEST_ALLOWED_YEAR = 2012; const DAYS_IN_PAYLOAD = 180; const DEFAULT_DATABASE_NAME = "healthreport.sqlite"; @@ -606,18 +608,19 @@ AbstractHealthReporter.prototype = Objec return deferred.promise; }, _getJSONPayload: function (now, asObject=false) { let pingDateString = this._formatDate(now); this._log.info("Producing JSON payload for " + pingDateString); let o = { - version: 1, + version: 2, thisPingDate: pingDateString, + geckoAppInfo: this.obtainAppInfo(this._log), data: {last: {}, days: {}}, }; let outputDataDays = o.data.days; // Guard here in case we don't track this (e.g., on Android). let lastPingDate = this.lastPingDate; if (lastPingDate && lastPingDate.getTime() > 0) { @@ -779,16 +782,62 @@ AbstractHealthReporter.prototype = Objec return Promise.reject(error); } ); }, _now: function _now() { return new Date(); }, + + // These are stolen from AppInfoProvider. + appInfoVersion: 1, + appInfoFields: { + // From nsIXULAppInfo. + vendor: "vendor", + name: "name", + id: "ID", + version: "version", + appBuildID: "appBuildID", + platformVersion: "platformVersion", + platformBuildID: "platformBuildID", + + // From nsIXULRuntime. + os: "OS", + xpcomabi: "XPCOMABI", + }, + + /** + * Statically return a bundle of app info data, a subset of that produced by + * AppInfoProvider._populateConstants. This allows us to more usefully handle + * payloads that, due to error, contain no data. + * + * Returns a very sparse object if Services.appinfo is unavailable. + */ + obtainAppInfo: function () { + let out = {"_v": this.appInfoVersion}; + try { + let ai = Services.appinfo; + for (let [k, v] in Iterator(this.appInfoFields)) { + out[k] = ai[v]; + } + } catch (ex) { + this._log.warn("Could not obtain Services.appinfo: " + + CommonUtils.exceptionStr(ex)); + } + + try { + out["updateChannel"] = UpdateChannel.get(); + } catch (ex) { + this._log.warn("Could not obtain update channel: " + + CommonUtils.exceptionStr(ex)); + } + + return out; + }, }); /** * HealthReporter and its abstract superclass coordinate collection and * submission of health report metrics. * * This is the main type for Firefox Health Report on desktop. It glues all the * lower-level components (such as collection and submission) together.
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js +++ b/services/healthreport/tests/xpcshell/test_healthreporter.js @@ -246,17 +246,17 @@ add_task(function test_json_payload_simp let reporter = yield getReporter("json_payload_simple"); try { let now = new Date(); let payload = yield reporter.getJSONPayload(); do_check_eq(typeof payload, "string"); let original = JSON.parse(payload); - do_check_eq(original.version, 1); + do_check_eq(original.version, 2); do_check_eq(original.thisPingDate, reporter._formatDate(now)); do_check_eq(Object.keys(original.data.last).length, 0); do_check_eq(Object.keys(original.data.days).length, 0); reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10); original = JSON.parse(yield reporter.getJSONPayload()); do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate)); @@ -580,26 +580,51 @@ add_task(function test_error_message_scr reporter._recordError("Foo " + uri.spec); do_check_eq(reporter._errors[0], "Foo <AppDataURI>"); } finally { reporter._shutdown(); } }); +add_task(function test_basic_appinfo() { + function verify(d) { + do_check_eq(d["_v"], 1); + do_check_eq(d._v, 1); + do_check_eq(d.vendor, "Mozilla"); + do_check_eq(d.name, "xpcshell"); + do_check_eq(d.id, "xpcshell@tests.mozilla.org"); + do_check_eq(d.version, "1"); + do_check_eq(d.appBuildID, "20121107"); + do_check_eq(d.platformVersion, "p-ver"); + do_check_eq(d.platformBuildID, "20121106"); + do_check_eq(d.os, "XPCShell"); + do_check_eq(d.xpcomabi, "noarch-spidermonkey"); + do_check_true("updateChannel" in d); + } + let reporter = yield getReporter("basic_appinfo"); + try { + verify(reporter.obtainAppInfo()); + let payload = yield reporter.collectAndObtainJSONPayload(true); + do_check_eq(payload["version"], 2); + verify(payload["geckoAppInfo"]); + } finally { + reporter._shutdown(); + } +}); + // Ensure collection occurs if upload is disabled. add_task(function test_collect_when_upload_disabled() { let reporter = getJustReporter("collect_when_upload_disabled"); reporter._policy.recordHealthReportUploadEnabled(false, "testing-collect"); do_check_false(reporter._policy.healthReportUploadEnabled); let name = "healthreport-testing-collect_when_upload_disabled-healthreport-lastDailyCollection"; let pref = "app.update.lastUpdateTime." + name; do_check_false(Services.prefs.prefHasUserValue(pref)); - try { yield reporter.onInit(); do_check_true(Services.prefs.prefHasUserValue(pref)); // We would ideally ensure the timer fires and does the right thing. // However, testing the update timer manager is quite involved. } finally { reporter._shutdown();