Merge mozilla-central to b2g-inbound

This commit is contained in:
Carsten "Tomcat" Book 2015-04-27 12:47:45 +02:00
commit 4ceb798519
558 changed files with 9641 additions and 4359 deletions

View File

@ -22,4 +22,7 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please # changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more. # don't change CLOBBER for WebIDL changes any more.
Bug 1039866: Removing the metro browser Bug 1038068: Check add-on signatures and refuse to install unsigned or broken add-ons
Not sure why this needs a clobber but tests perma-failed when they don't on
try (2).

View File

@ -743,7 +743,6 @@
#endif #endif
@RESPATH@/@PREF_DIR@/channel-prefs.js @RESPATH@/@PREF_DIR@/channel-prefs.js
@RESPATH@/greprefs.js @RESPATH@/greprefs.js
@RESPATH@/defaults/autoconfig/platform.js
@RESPATH@/defaults/autoconfig/prefcalls.js @RESPATH@/defaults/autoconfig/prefcalls.js
@RESPATH@/defaults/profile/prefs.js @RESPATH@/defaults/profile/prefs.js

View File

@ -69,6 +69,9 @@ pref("extensions.hotfix.certs.1.sha1Fingerprint", "91:53:98:0C:C1:86:DF:47:8F:35
// See the SCOPE constants in AddonManager.jsm for values to use here. // See the SCOPE constants in AddonManager.jsm for values to use here.
pref("extensions.autoDisableScopes", 15); pref("extensions.autoDisableScopes", 15);
// Don't require signed add-ons by default
pref("xpinstall.signatures.required", false);
// Dictionary download preference // Dictionary download preference
pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/"); pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
@ -1425,6 +1428,8 @@ pref("devtools.performance.enabled", true);
pref("devtools.performance.memory.sample-probability", "0.05"); pref("devtools.performance.memory.sample-probability", "0.05");
pref("devtools.performance.memory.max-log-length", 2147483647); // Math.pow(2,31) - 1 pref("devtools.performance.memory.max-log-length", 2147483647); // Math.pow(2,31) - 1
pref("devtools.performance.timeline.hidden-markers", "[]"); pref("devtools.performance.timeline.hidden-markers", "[]");
pref("devtools.performance.profiler.buffer-size", 10000000);
pref("devtools.performance.profiler.sample-frequency-khz", 1);
pref("devtools.performance.ui.invert-call-tree", true); pref("devtools.performance.ui.invert-call-tree", true);
pref("devtools.performance.ui.invert-flame-graph", false); pref("devtools.performance.ui.invert-flame-graph", false);
pref("devtools.performance.ui.flatten-tree-recursion", true); pref("devtools.performance.ui.flatten-tree-recursion", true);
@ -1530,6 +1535,9 @@ pref("devtools.webconsole.filter.info", true);
pref("devtools.webconsole.filter.log", true); pref("devtools.webconsole.filter.log", true);
pref("devtools.webconsole.filter.secerror", true); pref("devtools.webconsole.filter.secerror", true);
pref("devtools.webconsole.filter.secwarn", true); pref("devtools.webconsole.filter.secwarn", true);
pref("devtools.webconsole.filter.serviceworkers", false);
pref("devtools.webconsole.filter.sharedworkers", false);
pref("devtools.webconsole.filter.windowlessworkers", false);
// Remember the Browser Console filters // Remember the Browser Console filters
pref("devtools.browserconsole.filter.network", true); pref("devtools.browserconsole.filter.network", true);
@ -1548,6 +1556,9 @@ pref("devtools.browserconsole.filter.info", true);
pref("devtools.browserconsole.filter.log", true); pref("devtools.browserconsole.filter.log", true);
pref("devtools.browserconsole.filter.secerror", true); pref("devtools.browserconsole.filter.secerror", true);
pref("devtools.browserconsole.filter.secwarn", true); pref("devtools.browserconsole.filter.secwarn", true);
pref("devtools.browserconsole.filter.serviceworkers", true);
pref("devtools.browserconsole.filter.sharedworkers", true);
pref("devtools.browserconsole.filter.windowlessworkers", true);
// Text size in the Web Console. Use 0 for the system default size. // Text size in the Web Console. Use 0 for the system default size.
pref("devtools.webconsole.fontSize", 0); pref("devtools.webconsole.fontSize", 0);
@ -1891,3 +1902,8 @@ pref("reader.parse-node-limit", 0);
#ifdef NIGHTLY_BUILD #ifdef NIGHTLY_BUILD
pref("dom.serviceWorkers.enabled", true); pref("dom.serviceWorkers.enabled", true);
#endif #endif
pref("browser.pocket.enabled", false);
pref("browser.pocket.removedByUser", false);
pref("browser.pocket.useLocaleList", true);
pref("browser.pocket.enabledLocales", "en-US");

View File

@ -114,5 +114,6 @@
<stringbundleset id="stringbundleset"> <stringbundleset id="stringbundleset">
<stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/> <stringbundle id="bundle_browser" src="chrome://browser/locale/browser.properties"/>
<stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/> <stringbundle id="bundle_browser_region" src="chrome://browser-region/locale/region.properties"/>
<stringbundle id="bundle_pocket" src="chrome://browser/content/browser-pocket.properties"/>
</stringbundleset> </stringbundleset>
</overlay> </overlay>

View File

@ -0,0 +1,22 @@
# 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/.
# This is a temporary file and not meant for localization; later versions
# of Firefox include these strings in browser.properties
pocket-button.label = Pocket
pocket-button.tooltiptext = Send this page to Pocket
pocket-header = Pocket
pocket-login-required-tagline = Catch the best content you find online with Pocket.
pocket-signup-with-fxa = Sign up with Firefox
pocket-signup-with-email = Sign up with email
pocket-account-question = Already have an account?
pocket-login-now = Log in now
pocket-page-saved-header = Page Saved
pocket-open-pocket = Open Pocket
pocket-remove-page = Remove Page
pocket-page-tags-field = Add Tags
pocket-page-tags-add = Save
pocket-page-suggested-tags-header = Suggested Tags
pocket-signup-or = Or

View File

@ -782,9 +782,12 @@
// failed URI (particularly for SSL errors). However, don't clear the value // failed URI (particularly for SSL errors). However, don't clear the value
// if the error page's URI is about:blank, because that causes complete // if the error page's URI is about:blank, because that causes complete
// loss of urlbar contents for invalid URI errors (see bug 867957). // loss of urlbar contents for invalid URI errors (see bug 867957).
// Another reason to clear the userTypedValue is if this was an anchor
// navigation.
if (this.mBrowser.userTypedClear > 0 || if (this.mBrowser.userTypedClear > 0 ||
((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
aLocation.spec != "about:blank")) aLocation.spec != "about:blank") ||
aFlags && Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
this.mBrowser.userTypedValue = null; this.mBrowser.userTypedValue = null;
if (this.mTabBrowser.isFindBarInitialized(this.mTab)) { if (this.mTabBrowser.isFindBarInitialized(this.mTab)) {

View File

@ -1,6 +1,7 @@
/* Tests for correct behaviour of getEffectiveHost on identity handler */ /* Tests for correct behaviour of getEffectiveHost on identity handler */
function test() { function test() {
waitForExplicitFinish(); waitForExplicitFinish();
requestLongerTimeout(2);
ok(gIdentityHandler, "gIdentityHandler should exist"); ok(gIdentityHandler, "gIdentityHandler should exist");

View File

@ -75,6 +75,7 @@ browser.jar:
* content/browser/browser.css (content/browser.css) * content/browser/browser.css (content/browser.css)
* content/browser/browser.js (content/browser.js) * content/browser/browser.js (content/browser.js)
* content/browser/browser.xul (content/browser.xul) * content/browser/browser.xul (content/browser.xul)
content/browser/browser-pocket.properties (content/browser-pocket.properties)
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml) * content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
* content/browser/chatWindow.xul (content/chatWindow.xul) * content/browser/chatWindow.xul (content/chatWindow.xul)
content/browser/tab-content.js (content/tab-content.js) content/browser/tab-content.js (content/tab-content.js)

View File

@ -33,6 +33,10 @@ XPCOMUtils.defineLazyGetter(this, "BrandBundle", function() {
const kBrandBundle = "chrome://branding/locale/brand.properties"; const kBrandBundle = "chrome://branding/locale/brand.properties";
return Services.strings.createBundle(kBrandBundle); return Services.strings.createBundle(kBrandBundle);
}); });
XPCOMUtils.defineLazyGetter(this, "PocketBundle", function() {
const kPocketBundle = "chrome://browser/content/browser-pocket.properties";
return Services.strings.createBundle(kPocketBundle);
});
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const kPrefCustomizationDebug = "browser.uiCustomization.debug"; const kPrefCustomizationDebug = "browser.uiCustomization.debug";
@ -1057,6 +1061,133 @@ if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
}); });
} }
if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
let isEnabledForLocale = true;
if (Services.prefs.getBoolPref("browser.pocket.useLocaleList")) {
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
let browserLocale = chromeRegistry.getSelectedLocale("browser");
let enabledLocales = [];
try {
enabledLocales = Services.prefs.getCharPref("browser.pocket.enabledLocales").split(' ');
} catch (ex) {
Cu.reportError(ex);
}
isEnabledForLocale = enabledLocales.indexOf(browserLocale) != -1;
}
if (isEnabledForLocale) {
let pocketButton = {
id: "pocket-button",
type: "view",
viewId: "PanelUI-pocketView",
label: PocketBundle.GetStringFromName("pocket-button.label"),
tooltiptext: PocketBundle.GetStringFromName("pocket-button.tooltiptext"),
onCreated(node) {
let doc = node.ownerDocument;
let elementsNeedingStrings = [
"pocket-header",
"pocket-login-required-tagline",
"pocket-signup-with-fxa",
"pocket-signup-with-email",
"pocket-account-question",
"pocket-login-now",
"pocket-page-saved-header",
"pocket-open-pocket",
"pocket-remove-page",
"pocket-page-tags-field",
"pocket-page-tags-add",
"pocket-page-suggested-tags-header",
"pocket-signup-or",
];
for (let elementId of elementsNeedingStrings) {
let el = doc.getElementById(elementId);
let string = PocketBundle.GetStringFromName(elementId);
switch (el.localName) {
case "button":
el.label = string;
break;
case "textbox":
el.setAttribute("placeholder", string);
break;
default:
el.textContent = string;
break;
}
}
let addTagsField = doc.getElementById("pocket-page-tags-field");
let addTagsButton = doc.getElementById("pocket-page-tags-add");
addTagsField.addEventListener("input", this);
addTagsButton.addEventListener("command", this);
},
onViewShowing(event) {
let doc = event.target.ownerDocument;
let loginView = doc.getElementById("pocket-login-required");
let pageSavedView = doc.getElementById("pocket-page-saved");
let showPageSaved = Math.random() < 0.5;
loginView.hidden = !showPageSaved;
pageSavedView.hidden = showPageSaved;
},
handleEvent: function(event) {
let doc = event.target.ownerDocument;
let field = doc.getElementById("pocket-page-tags-field");
let button = doc.getElementById("pocket-page-tags-add");
switch (event.type) {
case "input":
button.disabled = !field.value.trim();
break;
case "command":
//XXXjaws Send tag to the Pocket server
field.value = "";
break;
}
},
USER_REMOVED_PREF: "browser.pocket.removedByUser",
onWidgetAdded(aWidgetId, aArea, aPosition) {
if (aWidgetId != this.id) {
return;
}
let placement = CustomizableUI.getPlacementOfWidget(this.id);
let widgetInUI = placement && placement.area;
Services.prefs.setBoolPref(this.USER_REMOVED_PREF, !widgetInUI);
},
onWidgetRemoved(aWidgetId, aArea) {
if (aWidgetId != this.id) {
return;
}
let placement = CustomizableUI.getPlacementOfWidget(this.id);
let widgetInUI = placement && placement.area;
Services.prefs.setBoolPref(this.USER_REMOVED_PREF, !widgetInUI);
},
onWidgetReset(aNode, aContainer) {
if (aNode.id != this.id) {
return;
}
Services.prefs.setBoolPref(this.USER_REMOVED_PREF, !aContainer);
},
onWidgetUndoMove(aNode, aContainer) {
if (aNode.id != this.id) {
return;
}
Services.prefs.setBoolPref(this.USER_REMOVED_PREF, !aContainer);
}
};
CustomizableWidgets.push(pocketButton);
CustomizableUI.addListener(pocketButton);
}
}
#ifdef E10S_TESTING_ONLY #ifdef E10S_TESTING_ONLY
/** /**
* The e10s button's purpose is to lower the barrier of entry * The e10s button's purpose is to lower the barrier of entry

View File

@ -229,6 +229,45 @@
</vbox> </vbox>
</panelview> </panelview>
<panelview id="PanelUI-pocketView" flex="1">
<vbox class="panel-subview-body">
<label id="pocket-header"/>
<vbox id="pocket-login-required" hidden="true">
<label id="pocket-login-required-tagline" class="pocket-subheader"/>
<description id="pocket-fxa-options">
<button id="pocket-signup-with-fxa" class="pocket-button"/>
<label id="pocket-signup-or"/>
<button id="pocket-signup-with-email" class="pocket-button"/>
</description>
<label id="pocket-account-question"/>
<label id="pocket-login-now" class="text-link"/>
</vbox>
<vbox id="pocket-page-saved" hidden="true">
<label id="pocket-page-saved-header" class="pocket-header"/>
<hbox id="pocket-page-saved-next-steps" pack="center">
<label id="pocket-open-pocket" class="text-link"/>
<label id="pocket-remove-page" class="text-link"/>
</hbox>
<hbox id="pocket-separator">
<box class="pocket-separator-colorstop"/>
<box class="pocket-separator-colorstop"/>
<box class="pocket-separator-colorstop"/>
<box class="pocket-separator-colorstop"/>
</hbox>
<vbox id="pocket-page-tags">
<hbox id="pocket-page-tags-form">
<textbox id="pocket-page-tags-field"
flex="1"/>
<button id="pocket-page-tags-add" disabled="true"/>
</hbox>
<label id="pocket-page-suggested-tags-header"/>
<description id="pocket-page-suggested-tags"/>
</vbox>
</vbox>
</vbox>
</panelview>
</panelmultiview> </panelmultiview>
<!-- These menupopups are located here to prevent flickering, <!-- These menupopups are located here to prevent flickering,
see bug 492960 comment 20. --> see bug 492960 comment 20. -->

View File

@ -69,6 +69,14 @@ class LoopTestServers:
p = processhandler.ProcessHandler(CONTENT_SERVER_COMMAND, p = processhandler.ProcessHandler(CONTENT_SERVER_COMMAND,
env=CONTENT_SERVER_ENV) env=CONTENT_SERVER_ENV)
p.run() p.run()
# Give the content server time to start.
import time
output = p.output
while not output:
time.sleep(1)
output = p.output
return p return p
def shutdown(self): def shutdown(self):

View File

@ -102,6 +102,8 @@ class Test1BrowserCall(MarionetteTestCase):
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media") media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media")
self.assertEqual(media_container.tag_name, "div", "expect a video container") self.assertEqual(media_container.tag_name, "div", "expect a video container")
self.check_video(".local .OT_publisher .OT_widget-container");
def local_get_and_verify_room_url(self): def local_get_and_verify_room_url(self):
self.switch_to_chatbox() self.switch_to_chatbox()
button = self.wait_for_element_displayed(By.CLASS_NAME, "btn-copy") button = self.wait_for_element_displayed(By.CLASS_NAME, "btn-copy")
@ -163,6 +165,10 @@ class Test1BrowserCall(MarionetteTestCase):
feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces") feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
self.assertEqual(feedback_form.tag_name, "div", "expect feedback form") self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
self.switch_to_chatbox()
# check that the local view reverts to the preview mode
self.wait_for_element_displayed(By.CLASS_NAME, "room-invitation-content")
def local_get_chatbox_window_expr(self, expr): def local_get_chatbox_window_expr(self, expr):
""" """
:expr: a sub-expression which must begin with a property of the :expr: a sub-expression which must begin with a property of the

View File

@ -13,6 +13,7 @@ DIRS += [
'loop', 'loop',
'migration', 'migration',
'places', 'places',
'pocket',
'preferences', 'preferences',
'privatebrowsing', 'privatebrowsing',
'readinglist', 'readinglist',

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,9 @@
# 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/.
browser.jar:
content/browser/pocket/img/signup_logo.png (img/signup_logo.png)
content/browser/pocket/img/signup_logo@2x.png (img/signup_logo@2x.png)
content/browser/pocket/img/signup_or.png (img/signup_or.png)
content/browser/pocket/img/signup_or@2x.png (img/signup_or@2x.png)

View File

@ -0,0 +1,5 @@
# 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/.
JAR_MANIFESTS += ['jar.mn']

View File

@ -97,6 +97,8 @@ skip-if = e10s
[browser_sessionStorage.js] [browser_sessionStorage.js]
[browser_swapDocShells.js] [browser_swapDocShells.js]
skip-if = e10s # See bug 918634 skip-if = e10s # See bug 918634
[browser_switch_remoteness.js]
run-if = e10s
[browser_telemetry.js] [browser_telemetry.js]
[browser_upgrade_backup.js] [browser_upgrade_backup.js]
[browser_windowRestore_perwindowpb.js] [browser_windowRestore_perwindowpb.js]

View File

@ -0,0 +1,47 @@
"use strict";
const URL = "http://example.com/browser_switch_remoteness_";
function countHistoryEntries(browser) {
return ContentTask.spawn(browser, null, function* () {
let Ci = Components.interfaces;
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
return history && history.count;
});
}
add_task(function* () {
// Add a new tab.
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
ok(browser.isRemoteBrowser, "browser is remote");
// Get the maximum number of preceding entries to save.
const MAX_BACK = Services.prefs.getIntPref("browser.sessionstore.max_serialize_back");
ok(MAX_BACK > -1, "check that the default has a value that caps data");
// Load more pages than we would save to disk on a clean shutdown.
for (let i = 0; i < MAX_BACK + 2; i++) {
browser.loadURI(URL + i);
yield promiseBrowserLoaded(browser);
ok(browser.isRemoteBrowser, "browser is still remote");
}
// Check we have the right number of shistory entries.
let count = yield countHistoryEntries(browser);
is(count, MAX_BACK + 2, "correct number of shistory entries");
// Load a non-remote page.
browser.loadURI("about:robots");
yield promiseTabRestored(tab);
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
// Check that we didn't lose any shistory entries.
count = yield countHistoryEntries(browser);
is(count, MAX_BACK + 3, "correct number of shistory entries");
// Cleanup.
gBrowser.removeTab(tab);
});

View File

@ -384,7 +384,7 @@ let CallsListView = Heritage.extend(WidgetMethods, {
// If clicking on the location, jump to the Debugger. // If clicking on the location, jump to the Debugger.
if (e.target.classList.contains("call-item-location")) { if (e.target.classList.contains("call-item-location")) {
let { file, line } = callItem.attachment.actor; let { file, line } = callItem.attachment.actor;
viewSourceInDebugger(file, line); this._viewSourceInDebugger(file, line);
return; return;
} }
// Otherwise hide the call stack. // Otherwise hide the call stack.
@ -456,7 +456,7 @@ let CallsListView = Heritage.extend(WidgetMethods, {
* The line of the respective function. * The line of the respective function.
*/ */
_onStackFileClick: function(e, { file, line }) { _onStackFileClick: function(e, { file, line }) {
viewSourceInDebugger(file, line); this._viewSourceInDebugger(file, line);
}, },
/** /**
@ -501,7 +501,7 @@ let CallsListView = Heritage.extend(WidgetMethods, {
} }
let callItem = this.selectedItem; let callItem = this.selectedItem;
let { file, line } = callItem.attachment.actor; let { file, line } = callItem.attachment.actor;
viewSourceInDebugger(file, line); this._viewSourceInDebugger(file, line);
}, },
/** /**
@ -509,5 +509,18 @@ let CallsListView = Heritage.extend(WidgetMethods, {
*/ */
_onStepOut: function() { _onStepOut: function() {
this.selectedIndex = this.itemCount - 1; this.selectedIndex = this.itemCount - 1;
},
/**
* Opens the specified file and line in the debugger. Falls back to Firefox's View Source.
*/
_viewSourceInDebugger: function (file, line) {
gToolbox.viewSourceInDebugger(file, line).then(success => {
if (success) {
window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
} else {
window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
}
});
} }
}); });

View File

@ -12,11 +12,13 @@ Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
Cu.import("resource://gre/modules/devtools/Console.jsm"); Cu.import("resource://gre/modules/devtools/Console.jsm");
Cu.import("resource:///modules/devtools/gDevTools.jsm"); Cu.import("resource:///modules/devtools/gDevTools.jsm");
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const { require } = devtools;
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const EventEmitter = require("devtools/toolkit/event-emitter"); const EventEmitter = require("devtools/toolkit/event-emitter");
const { CallWatcherFront } = require("devtools/server/actors/call-watcher"); const { CallWatcherFront } = require("devtools/server/actors/call-watcher");
const { CanvasFront } = require("devtools/server/actors/canvas"); const { CanvasFront } = require("devtools/server/actors/canvas");
const Telemetry = require("devtools/shared/telemetry"); const Telemetry = require("devtools/shared/telemetry");
const telemetry = new Telemetry(); const telemetry = new Telemetry();
@ -354,32 +356,3 @@ function getThumbnailForCall(thumbnails, index) {
} }
return CanvasFront.INVALID_SNAPSHOT_IMAGE; return CanvasFront.INVALID_SNAPSHOT_IMAGE;
} }
/**
* Opens/selects the debugger in this toolbox and jumps to the specified
* file name and line number.
*/
function viewSourceInDebugger(url, line) {
let showSource = ({ DebuggerView }) => {
let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === url);
if (item) {
DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true }).then(() => {
window.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
}, () => {
window.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
});
}
}
// If the Debugger was already open, switch to it and try to show the
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
gToolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
if (debuggerAlreadyOpen) {
showSource(dbg);
} else {
dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
}
});
}

View File

@ -1222,7 +1222,7 @@ SourceScripts.prototype = {
* Callback for the debugger's active thread getSources() method. * Callback for the debugger's active thread getSources() method.
*/ */
_onSourcesAdded: function(aResponse) { _onSourcesAdded: function(aResponse) {
if (aResponse.error) { if (aResponse.error || !aResponse.sources) {
let msg = "Error getting sources: " + aResponse.message; let msg = "Error getting sources: " + aResponse.message;
Cu.reportError(msg); Cu.reportError(msg);
dumpn(msg); dumpn(msg);

View File

@ -70,13 +70,22 @@ DevTools.prototype = {
}, },
set testing(state) { set testing(state) {
let oldState = this._testing;
this._testing = state; this._testing = state;
if (state) { if (state !== oldState) {
// dom.send_after_paint_to_content is set to true (non-default) in if (state) {
// testing/profiles/prefs_general.js so lets set it to the same as it is this._savedSendAfterPaintToContentPref =
// in a default browser profile for the duration of the test. Services.prefs.getBoolPref("dom.send_after_paint_to_content");
Services.prefs.setBoolPref("dom.send_after_paint_to_content", false);
// dom.send_after_paint_to_content is set to true (non-default) in
// testing/profiles/prefs_general.js so lets set it to the same as it is
// in a default browser profile for the duration of the test.
Services.prefs.setBoolPref("dom.send_after_paint_to_content", false);
} else {
Services.prefs.setBoolPref("dom.send_after_paint_to_content",
this._savedSendAfterPaintToContentPref);
}
} }
}, },

View File

@ -6,9 +6,11 @@ support-files =
browser_toolbox_options_disable_js_iframe.html browser_toolbox_options_disable_js_iframe.html
browser_toolbox_options_disable_cache.sjs browser_toolbox_options_disable_cache.sjs
browser_toolbox_sidebar_tool.xul browser_toolbox_sidebar_tool.xul
code_math.js
head.js head.js
helper_disable_cache.js helper_disable_cache.js
doc_theme.css doc_theme.css
doc_viewsource.html
browser_toolbox_options_enable_serviceworkers_testing.html browser_toolbox_options_enable_serviceworkers_testing.html
serviceworker.js serviceworker.js
@ -48,6 +50,10 @@ skip-if = e10s # Bug 1069044 - destroyInspector may hang during shutdown
[browser_toolbox_tool_ready.js] [browser_toolbox_tool_ready.js]
[browser_toolbox_tool_remote_reopen.js] [browser_toolbox_tool_remote_reopen.js]
[browser_toolbox_transport_events.js] [browser_toolbox_transport_events.js]
[browser_toolbox_view_source_01.js]
[browser_toolbox_view_source_02.js]
[browser_toolbox_view_source_03.js]
[browser_toolbox_view_source_04.js]
[browser_toolbox_window_reload_target.js] [browser_toolbox_window_reload_target.js]
[browser_toolbox_window_shortcuts.js] [browser_toolbox_window_shortcuts.js]
skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
@ -56,7 +62,7 @@ skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5
[browser_toolbox_custom_host.js] [browser_toolbox_custom_host.js]
[browser_toolbox_theme_registration.js] [browser_toolbox_theme_registration.js]
[browser_toolbox_options_enable_serviceworkers_testing.js] [browser_toolbox_options_enable_serviceworkers_testing.js]
skip-if = e10s # Bug 1030318 skip-if = true # Bug 1153407 - this test breaks subsequent tests and is not e10s compatible
# We want this test to run for mochitest-dt as well, so we include it here: # We want this test to run for mochitest-dt as well, so we include it here:
[../../../base/content/test/general/browser_parsable_css.js] [../../../base/content/test/general/browser_parsable_css.js]

View File

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInDebugger works when debugger is not
* yet opened.
*/
let URL = `${URL_ROOT}doc_viewsource.html`;
let JS_URL = `${URL_ROOT}code_math.js`;
function *viewSource() {
let toolbox = yield loadToolbox(URL);
yield toolbox.viewSourceInDebugger(JS_URL, 2);
let debuggerPanel = toolbox.getPanel("jsdebugger");
ok(debuggerPanel, "The debugger panel was opened.");
is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
let { DebuggerView } = debuggerPanel.panelWin;
let Sources = DebuggerView.Sources;
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 2,
"The correct line is highlighted in the debugger's source editor.");
yield unloadToolbox(toolbox);
finish();
}
function test () {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View File

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInDebugger works when debugger is already loaded.
*/
let URL = `${URL_ROOT}doc_viewsource.html`;
let JS_URL = `${URL_ROOT}code_math.js`;
function *viewSource() {
let toolbox = yield loadToolbox(URL);
let { panelWin: debuggerWin } = yield toolbox.selectTool("jsdebugger");
let debuggerEvents = debuggerWin.EVENTS;
let { DebuggerView } = debuggerWin;
let Sources = DebuggerView.Sources;
yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
ok("A source was shown in the debugger.");
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is initially shown in the debugger.");
is(DebuggerView.editor.getCursor().line, 0,
"The correct line is initially highlighted in the debugger's source editor.");
yield toolbox.viewSourceInDebugger(JS_URL, 2);
let debuggerPanel = toolbox.getPanel("jsdebugger");
ok(debuggerPanel, "The debugger panel was opened.");
is(toolbox.currentToolId, "jsdebugger", "The debugger panel was selected.");
is(Sources.selectedValue, getSourceActor(Sources, JS_URL),
"The correct source is shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 2,
"The correct line is highlighted in the debugger's source editor.");
yield unloadToolbox(toolbox);
finish();
}
function test () {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View File

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInStyleEditor works when style editor is not
* yet opened.
*/
let URL = `${URL_ROOT}doc_viewsource.html`;
let CSS_URL = `${URL_ROOT}doc_theme.css`;
function *viewSource() {
let toolbox = yield loadToolbox(URL);
let fileFound = yield toolbox.viewSourceInStyleEditor(CSS_URL, 2);
ok(fileFound, "viewSourceInStyleEditor should resolve to true if source found.");
let stylePanel = toolbox.getPanel("styleeditor");
ok(stylePanel, "The style editor panel was opened.");
is(toolbox.currentToolId, "styleeditor", "The style editor panel was selected.");
let { UI } = stylePanel;
is(UI.selectedEditor.styleSheet.href, CSS_URL,
"The correct source is shown in the style editor.");
is(UI.selectedEditor.sourceEditor.getCursor().line + 1, 2,
"The correct line is highlighted in the style editor's source editor.");
yield unloadToolbox(toolbox);
finish();
}
function test () {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that Toolbox#viewSourceInScratchpad works.
*/
let URL = `${URL_ROOT}doc_viewsource.html`;
function *viewSource() {
let toolbox = yield loadToolbox(URL);
let win = yield openScratchpadWindow();
let { Scratchpad: scratchpad } = win;
// Brahm's Cello Sonata No.1, Op.38 now in the scratchpad
scratchpad.setText("E G B C B\nA B A G A B\nG E");
let scratchpadURL = scratchpad.uniqueName;
// Now select another tool for focus
yield toolbox.selectTool("webconsole");
yield toolbox.viewSourceInScratchpad(scratchpadURL, 2);
is(scratchpad.editor.getCursor().line, 2,
"The correct line is highlighted in scratchpad's editor.");
win.close();
yield unloadToolbox(toolbox);
finish();
}
function test () {
Task.spawn(viewSource).then(finish, (aError) => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();
});
}

View File

@ -0,0 +1,4 @@
function add(a, b, k) {
var result = a + b;
return k(result);
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Toolbox test for View Source methods</title>
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<link charset="UTF-8" rel="stylesheet" href="doc_theme.css" />
<script src="code_math.js"></script>
</head>
<body>
</body>
</html>

View File

@ -7,6 +7,7 @@ let TargetFactory = gDevTools.TargetFactory;
const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); const { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { ScratchpadManager } = Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", {});
const URL_ROOT = "http://example.com/browser/browser/devtools/framework/test/"; const URL_ROOT = "http://example.com/browser/browser/devtools/framework/test/";
const CHROME_URL_ROOT = "chrome://mochitests/content/browser/browser/devtools/framework/test/"; const CHROME_URL_ROOT = "chrome://mochitests/content/browser/browser/devtools/framework/test/";
@ -175,3 +176,49 @@ function getChromeActors(callback)
DebuggerServer.destroy(); DebuggerServer.destroy();
}); });
} }
function loadToolbox (url) {
let { promise: p, resolve } = promise.defer();
gBrowser.selectedTab = gBrowser.addTab();
let target = TargetFactory.forTab(gBrowser.selectedTab);
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
gDevTools.showToolbox(target).then(resolve);
}, true);
content.location = url;
return p;
}
function unloadToolbox (toolbox) {
return toolbox.destroy().then(function() {
gBrowser.removeCurrentTab();
});
}
function getSourceActor(aSources, aURL) {
let item = aSources.getItemForAttachment(a => a.source.url === aURL);
return item && item.value;
}
/**
* Open a Scratchpad window.
*
* @return nsIDOMWindow
* The new window object that holds Scratchpad.
*/
function *openScratchpadWindow () {
let { promise: p, resolve } = promise.defer();
let win = ScratchpadManager.openScratchpad();
yield once(win, "load");
win.Scratchpad.addObserver({
onReady: function () {
win.Scratchpad.removeObserver(this);
resolve(win);
}
});
return p;
}

View File

@ -21,6 +21,7 @@ let Telemetry = require("devtools/shared/telemetry");
let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils"); let {getHighlighterUtils} = require("devtools/framework/toolbox-highlighter-utils");
let HUDService = require("devtools/webconsole/hudservice"); let HUDService = require("devtools/webconsole/hudservice");
let {showDoorhanger} = require("devtools/shared/doorhanger"); let {showDoorhanger} = require("devtools/shared/doorhanger");
let sourceUtils = require("devtools/shared/source-utils");
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
@ -380,11 +381,14 @@ Toolbox.prototype = {
framesPromise framesPromise
]); ]);
// Lazily connect to the profiler here and don't wait for it to complete,
// used to intercept console.profile calls before the performance tools are open.
let profilerReady = this._connectProfiler(); let profilerReady = this._connectProfiler();
// Only wait for the profiler initialization during tests. Otherwise, // However, while testing, we must wait for the performance connection to finish,
// lazily load this. This is to intercept console.profile calls; the performance // as most tests shut down without waiting for a toolbox destruction event,
// tools will explicitly wait for the connection opening when opened. // resulting in the shared profiler connection being opened and closed
// outside of the test that originally opened the toolbox.
if (gDevTools.testing) { if (gDevTools.testing) {
yield profilerReady; yield profilerReady;
} }
@ -1708,6 +1712,7 @@ Toolbox.prototype = {
gDevTools.off("pref-changed", this._prefChanged); gDevTools.off("pref-changed", this._prefChanged);
this._lastFocusedElement = null; this._lastFocusedElement = null;
if (this.webconsolePanel) { if (this.webconsolePanel) {
this._saveSplitConsoleHeight(); this._saveSplitConsoleHeight();
this.webconsolePanel.removeEventListener("resize", this.webconsolePanel.removeEventListener("resize",
@ -1866,7 +1871,9 @@ Toolbox.prototype = {
}), }),
/** /**
* Disconnects the underlying Performance Actor Connection. * Disconnects the underlying Performance Actor Connection. If the connection
* has not finished initializing, as opening a toolbox does not wait,
* the performance connection destroy method will wait for it on its own.
*/ */
_disconnectProfiler: Task.async(function*() { _disconnectProfiler: Task.async(function*() {
if (!this._performanceConnection) { if (!this._performanceConnection) {
@ -1875,4 +1882,48 @@ Toolbox.prototype = {
yield this._performanceConnection.destroy(); yield this._performanceConnection.destroy();
this._performanceConnection = null; this._performanceConnection = null;
}), }),
/**
* Returns gViewSourceUtils for viewing source.
*/
get gViewSourceUtils() {
return this.frame.contentWindow.gViewSourceUtils;
},
/**
* Opens source in style editor. Falls back to plain "view-source:".
* @see browser/devtools/shared/source-utils.js
*/
viewSourceInStyleEditor: function (sourceURL, sourceLine) {
return sourceUtils.viewSourceInStyleEditor(this, sourceURL, sourceLine);
},
/**
* Opens source in debugger. Falls back to plain "view-source:".
* @see browser/devtools/shared/source-utils.js
*/
viewSourceInDebugger: function (sourceURL, sourceLine) {
return sourceUtils.viewSourceInDebugger(this, sourceURL, sourceLine);
},
/**
* Opens source in scratchpad. Falls back to plain "view-source:".
* TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
* If instances are scoped one-per-browser-window, then we should be able
* to infer the URL from this toolbox, or use the built in scratchpad IN
* the toolbox.
*
* @see browser/devtools/shared/source-utils.js
*/
viewSourceInScratchpad: function (sourceURL, sourceLine) {
return sourceUtils.viewSourceInScratchpad(sourceURL, sourceLine);
},
/**
* Opens source in plain "view-source:".
* @see browser/devtools/shared/source-utils.js
*/
viewSource: function (sourceURL, sourceLine) {
return sourceUtils.viewSource(this, sourceURL, sourceLine);
},
}; };

View File

@ -18,6 +18,9 @@
<script type="application/javascript;version=1.8" <script type="application/javascript;version=1.8"
src="chrome://browser/content/devtools/theme-switching.js"/> src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript"
src="chrome://global/content/viewSourceUtils.js"/>
<script type="application/javascript" src="chrome://global/content/globalOverlay.js"/> <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
<commandset id="editMenuCommands"/> <commandset id="editMenuCommands"/>

View File

@ -283,13 +283,6 @@ InspectorPanel.prototype = {
this._target = value; this._target = value;
}, },
/**
* Expose gViewSourceUtils so that other tools can make use of them.
*/
get viewSourceUtils() {
return this.panelWin.gViewSourceUtils;
},
/** /**
* Indicate that a tool has modified the state of the page. Used to * Indicate that a tool has modified the state of the page. Used to
* decide whether to show the "are you sure you want to navigate" * decide whether to show the "are you sure you want to navigate"

View File

@ -18,9 +18,6 @@
<script type="application/javascript;version=1.8" <script type="application/javascript;version=1.8"
src="chrome://browser/content/devtools/theme-switching.js"/> src="chrome://browser/content/devtools/theme-switching.js"/>
<script type="application/javascript"
src="chrome://global/content/viewSourceUtils.js"/>
<commandset> <commandset>
<command id="nodeSearchCommand" <command id="nodeSearchCommand"
oncommand="inspector.searchBox.focus()"/> oncommand="inspector.searchBox.focus()"/>

View File

@ -141,6 +141,8 @@ browser.jar:
content/browser/devtools/spectrum.css (shared/widgets/spectrum.css) content/browser/devtools/spectrum.css (shared/widgets/spectrum.css)
content/browser/devtools/cubic-bezier-frame.xhtml (shared/widgets/cubic-bezier-frame.xhtml) content/browser/devtools/cubic-bezier-frame.xhtml (shared/widgets/cubic-bezier-frame.xhtml)
content/browser/devtools/cubic-bezier.css (shared/widgets/cubic-bezier.css) content/browser/devtools/cubic-bezier.css (shared/widgets/cubic-bezier.css)
content/browser/devtools/mdn-docs-frame.xhtml (shared/widgets/mdn-docs-frame.xhtml)
content/browser/devtools/mdn-docs.css (shared/widgets/mdn-docs.css)
content/browser/devtools/filter-frame.xhtml (shared/widgets/filter-frame.xhtml) content/browser/devtools/filter-frame.xhtml (shared/widgets/filter-frame.xhtml)
content/browser/devtools/filter-widget.css (shared/widgets/filter-widget.css) content/browser/devtools/filter-widget.css (shared/widgets/filter-widget.css)
content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul) content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul)

View File

@ -4,13 +4,73 @@
"use strict"; "use strict";
const { Task } = require("resource://gre/modules/Task.jsm"); const { Task } = require("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "promise");
loader.lazyRequireGetter(this, "EventEmitter", loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter"); "devtools/toolkit/event-emitter");
loader.lazyRequireGetter(this, "RecordingUtils",
"devtools/performance/recording-utils", true);
const REQUIRED_MEMORY_ACTOR_METHODS = [ const REQUIRED_MEMORY_ACTOR_METHODS = [
"attach", "detach", "startRecordingAllocations", "stopRecordingAllocations", "getAllocations" "attach", "detach", "startRecordingAllocations", "stopRecordingAllocations", "getAllocations"
]; ];
/**
* Constructor for a facade around an underlying ProfilerFront.
*/
function ProfilerFront (target) {
this._target = target;
}
ProfilerFront.prototype = {
// Connects to the targets underlying real ProfilerFront.
connect: Task.async(function*() {
let target = this._target;
// Chrome and content process targets already have obtained a reference
// to the profiler tab actor. Use it immediately.
if (target.form && target.form.profilerActor) {
this._profiler = target.form.profilerActor;
}
// Check if we already have a grip to the `listTabs` response object
// and, if we do, use it to get to the profiler actor.
else if (target.root && target.root.profilerActor) {
this._profiler = target.root.profilerActor;
}
// Otherwise, call `listTabs`.
else {
this._profiler = (yield listTabs(target.client)).profilerActor;
}
// Fetch and store information about the SPS profiler and
// server profiler.
this.traits = {};
this.traits.filterable = target.getTrait("profilerDataFilterable");
}),
/**
* Makes a request to the underlying real profiler actor. Handles
* backwards compatibility differences based off of the features
* and traits of the actor.
*/
_request: function (method, ...args) {
let deferred = promise.defer();
let data = args[0] || {};
data.to = this._profiler;
data.type = method;
this._target.client.request(data, res => {
// If the backend does not support filtering by start and endtime on platform (< Fx40),
// do it on the client (much slower).
if (method === "getProfile" && !this.traits.filterable) {
RecordingUtils.filterSamples(res.profile, data.startTime || 0);
}
deferred.resolve(res);
});
return deferred.promise;
}
};
exports.ProfilerFront = ProfilerFront;
/** /**
* A dummy front decorated with the provided methods. * A dummy front decorated with the provided methods.
* *
@ -107,3 +167,14 @@ function timelineActorSupported(target) {
return target.hasActor("timeline"); return target.hasActor("timeline");
} }
exports.timelineActorSupported = Task.async(timelineActorSupported); exports.timelineActorSupported = Task.async(timelineActorSupported);
/**
* Returns a promise resolved with a listing of all the tabs in the
* provided thread client.
*/
function listTabs(client) {
let deferred = promise.defer();
client.listTabs(deferred.resolve);
return deferred.promise;
}

View File

@ -144,7 +144,9 @@ PerformanceActorsConnection.prototype = {
*/ */
destroy: Task.async(function*() { destroy: Task.async(function*() {
if (this._connecting && !this._connected) { if (this._connecting && !this._connected) {
console.warn("Attempting to destroy SharedPerformanceActorsConnection before initialization completion. If testing, ensure `gDevTools.testing` is set."); yield this._connecting.promise;
} else if (!this._connected) {
return;
} }
yield this._unregisterListeners(); yield this._unregisterListeners();
@ -152,26 +154,16 @@ PerformanceActorsConnection.prototype = {
this._memory = this._timeline = this._profiler = this._target = this._client = null; this._memory = this._timeline = this._profiler = this._target = this._client = null;
this._connected = false; this._connected = false;
this._connecting = null;
}), }),
/** /**
* Initializes a connection to the profiler actor. * Initializes a connection to the profiler actor. Uses a facade around the ProfilerFront
* for similarity to the other actors in the shared connection.
*/ */
_connectProfilerActor: Task.async(function*() { _connectProfilerActor: Task.async(function*() {
// Chrome and content process targets already have obtained a reference this._profiler = new compatibility.ProfilerFront(this._target);
// to the profiler tab actor. Use it immediately. yield this._profiler.connect();
if (this._target.form && this._target.form.profilerActor) {
this._profiler = this._target.form.profilerActor;
}
// Check if we already have a grip to the `listTabs` response object
// and, if we do, use it to get to the profiler actor.
else if (this._target.root && this._target.root.profilerActor) {
this._profiler = this._target.root.profilerActor;
}
// Otherwise, call `listTabs`.
else {
this._profiler = (yield listTabs(this._client)).profilerActor;
}
}), }),
/** /**
@ -253,12 +245,7 @@ PerformanceActorsConnection.prototype = {
_request: function(actor, method, ...args) { _request: function(actor, method, ...args) {
// Handle requests to the profiler actor. // Handle requests to the profiler actor.
if (actor == "profiler") { if (actor == "profiler") {
let deferred = promise.defer(); return this._profiler._request(method, ...args);
let data = args[0] || {};
data.to = this._profiler;
data.type = method;
this._client.request(data, deferred.resolve);
return deferred.promise;
} }
// Handle requests to the timeline actor. // Handle requests to the timeline actor.
@ -331,10 +318,20 @@ PerformanceActorsConnection.prototype = {
/** /**
* Invoked whenever `console.profileEnd` is called. * Invoked whenever `console.profileEnd` is called.
* *
* @param object profilerData * @param string profileLabel
* The dump of data from the profiler triggered by this console.profileEnd call. * The provided string argument if available; undefined otherwise.
* @param number currentTime
* The time (in milliseconds) when the call was made, relative to when
* the nsIProfiler module was started.
*/ */
_onConsoleProfileEnd: Task.async(function *(profilerData) { _onConsoleProfileEnd: Task.async(function *(data) {
// If no data, abort; can occur if profiler isn't running and we get a surprise
// call to console.profileEnd()
if (!data) {
return;
}
let { profileLabel, currentTime: endTime } = data;
let pending = this._recordings.filter(r => r.isConsole() && r.isRecording()); let pending = this._recordings.filter(r => r.isConsole() && r.isRecording());
if (pending.length === 0) { if (pending.length === 0) {
return; return;
@ -343,8 +340,8 @@ PerformanceActorsConnection.prototype = {
let model; let model;
// Try to find the corresponding `console.profile` call if // Try to find the corresponding `console.profile` call if
// a label was used in profileEnd(). If no matches, abort. // a label was used in profileEnd(). If no matches, abort.
if (profilerData.profileLabel) { if (profileLabel) {
model = pending.find(e => e.getLabel() === profilerData.profileLabel); model = pending.find(e => e.getLabel() === profileLabel);
} }
// If no label supplied, pop off the most recent pending console recording // If no label supplied, pop off the most recent pending console recording
else { else {
@ -401,7 +398,7 @@ PerformanceActorsConnection.prototype = {
let model = new RecordingModel(options); let model = new RecordingModel(options);
// All actors are started asynchronously over the remote debugging protocol. // All actors are started asynchronously over the remote debugging protocol.
// Get the corresponding start times from each one of them. // Get the corresponding start times from each one of them.
let profilerStartTime = yield this._startProfiler(); let profilerStartTime = yield this._startProfiler(options);
let timelineStartTime = yield this._startTimeline(options); let timelineStartTime = yield this._startTimeline(options);
let memoryStartTime = yield this._startMemory(options); let memoryStartTime = yield this._startMemory(options);
@ -445,7 +442,8 @@ PerformanceActorsConnection.prototype = {
this._recordings.splice(this._recordings.indexOf(model), 1); this._recordings.splice(this._recordings.indexOf(model), 1);
let config = model.getConfiguration(); let config = model.getConfiguration();
let profilerData = yield this._request("profiler", "getProfile"); let startTime = model.getProfilerStartTime();
let profilerData = yield this._request("profiler", "getProfile", { startTime });
let memoryEndTime = Date.now(); let memoryEndTime = Date.now();
let timelineEndTime = Date.now(); let timelineEndTime = Date.now();
@ -485,7 +483,7 @@ PerformanceActorsConnection.prototype = {
/** /**
* Starts the profiler actor, if necessary. * Starts the profiler actor, if necessary.
*/ */
_startProfiler: Task.async(function *() { _startProfiler: Task.async(function *(options={}) {
// Start the profiler only if it wasn't already active. The built-in // Start the profiler only if it wasn't already active. The built-in
// nsIPerformance module will be kept recording, because it's the same instance // nsIPerformance module will be kept recording, because it's the same instance
// for all targets and interacts with the whole platform, so we don't want // for all targets and interacts with the whole platform, so we don't want
@ -496,10 +494,13 @@ PerformanceActorsConnection.prototype = {
return profilerStatus.currentTime; return profilerStatus.currentTime;
} }
// If this._customProfilerOptions is defined, use those to pass in // Translate options from the recording model into profiler-specific
// to the profiler actor. The profiler actor handles all the defaults // options for the nsIProfiler
// now, so this should only be used for tests. let profilerOptions = {
let profilerOptions = this._customProfilerOptions || {}; entries: options.bufferSize,
interval: options.sampleFrequency ? (1000 / (options.sampleFrequency * 1000)) : void 0
};
yield this._request("profiler", "startProfiler", profilerOptions); yield this._request("profiler", "startProfiler", profilerOptions);
this.emit("profiler-activated"); this.emit("profiler-activated");
@ -667,16 +668,6 @@ PerformanceFront.prototype = {
} }
}; };
/**
* Returns a promise resolved with a listing of all the tabs in the
* provided thread client.
*/
function listTabs(client) {
let deferred = promise.defer();
client.listTabs(deferred.resolve);
return deferred.promise;
}
/** /**
* Creates an object of configurations based off of preferences for a RecordingModel. * Creates an object of configurations based off of preferences for a RecordingModel.
*/ */

View File

@ -26,7 +26,9 @@ const RecordingModel = function (options={}) {
withMemory: options.withMemory || false, withMemory: options.withMemory || false,
withAllocations: options.withAllocations || false, withAllocations: options.withAllocations || false,
allocationsSampleProbability: options.allocationsSampleProbability || 0, allocationsSampleProbability: options.allocationsSampleProbability || 0,
allocationsMaxLogLength: options.allocationsMaxLogLength || 0 allocationsMaxLogLength: options.allocationsMaxLogLength || 0,
bufferSize: options.bufferSize || 0,
sampleFrequency: options.sampleFrequency || 1
}; };
}; };
@ -113,10 +115,10 @@ RecordingModel.prototype = {
this._duration = info.profilerEndTime - this._profilerStartTime; this._duration = info.profilerEndTime - this._profilerStartTime;
this._recording = false; this._recording = false;
// We'll need to filter out all samples that fall out of current profile's // We filter out all samples that fall out of current profile's range
// range since the profiler is continuously running. Because of this, sample // since the profiler is continuously running. Because of this, sample
// times are not guaranteed to have a zero epoch, so offset the timestamps. // times are not guaranteed to have a zero epoch, so offset the
RecordingUtils.filterSamples(this._profile, this._profilerStartTime); // timestamps.
RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime); RecordingUtils.offsetSampleTimes(this._profile, this._profilerStartTime);
// Markers need to be sorted ascending by time, to be properly displayed // Markers need to be sorted ascending by time, to be properly displayed
@ -124,6 +126,14 @@ RecordingModel.prototype = {
this._markers = this._markers.sort((a, b) => (a.start > b.start)); this._markers = this._markers.sort((a, b) => (a.start > b.start));
}), }),
/**
* Gets the profile's start time.
* @return number
*/
getProfilerStartTime: function () {
return this._profilerStartTime;
},
/** /**
* Gets the profile's label, from `console.profile(LABEL)`. * Gets the profile's label, from `console.profile(LABEL)`.
* @return string * @return string

View File

@ -206,7 +206,9 @@ let PerformanceController = {
this._nonBooleanPrefs = new ViewHelpers.Prefs("devtools.performance", { this._nonBooleanPrefs = new ViewHelpers.Prefs("devtools.performance", {
"hidden-markers": ["Json", "timeline.hidden-markers"], "hidden-markers": ["Json", "timeline.hidden-markers"],
"memory-sample-probability": ["Float", "memory.sample-probability"], "memory-sample-probability": ["Float", "memory.sample-probability"],
"memory-max-log-length": ["Int", "memory.max-log-length"] "memory-max-log-length": ["Int", "memory.max-log-length"],
"profiler-buffer-size": ["Int", "profiler.buffer-size"],
"profiler-sample-frequency": ["Int", "profiler.sample-frequency-khz"],
}); });
this._nonBooleanPrefs.registerObserver(); this._nonBooleanPrefs.registerObserver();
@ -293,7 +295,9 @@ let PerformanceController = {
withTicks: this.getOption("enable-framerate"), withTicks: this.getOption("enable-framerate"),
withAllocations: this.getOption("enable-memory"), withAllocations: this.getOption("enable-memory"),
allocationsSampleProbability: this.getPref("memory-sample-probability"), allocationsSampleProbability: this.getPref("memory-sample-probability"),
allocationsMaxLogLength: this.getPref("memory-max-log-length") allocationsMaxLogLength: this.getPref("memory-max-log-length"),
bufferSize: this.getPref("profiler-buffer-size"),
sampleFrequency: this.getPref("profiler-sample-frequency")
}; };
this.emit(EVENTS.RECORDING_WILL_START); this.emit(EVENTS.RECORDING_WILL_START);

View File

@ -19,6 +19,7 @@ support-files =
[browser_perf-compatibility-02.js] [browser_perf-compatibility-02.js]
[browser_perf-compatibility-03.js] [browser_perf-compatibility-03.js]
[browser_perf-compatibility-04.js] [browser_perf-compatibility-04.js]
[browser_perf-compatibility-05.js]
[browser_perf-clear-01.js] [browser_perf-clear-01.js]
[browser_perf-clear-02.js] [browser_perf-clear-02.js]
[browser_perf-columns-js-calltree.js] [browser_perf-columns-js-calltree.js]
@ -57,8 +58,6 @@ support-files =
[browser_perf-jit-view-02.js] [browser_perf-jit-view-02.js]
[browser_perf-jit-model-01.js] [browser_perf-jit-model-01.js]
[browser_perf-jit-model-02.js] [browser_perf-jit-model-02.js]
[browser_perf-jump-to-debugger-01.js]
[browser_perf-jump-to-debugger-02.js]
[browser_perf-options-01.js] [browser_perf-options-01.js]
[browser_perf-options-02.js] [browser_perf-options-02.js]
[browser_perf-options-invert-call-tree-01.js] [browser_perf-options-invert-call-tree-01.js]
@ -75,6 +74,7 @@ support-files =
[browser_perf-options-enable-memory-02.js] [browser_perf-options-enable-memory-02.js]
[browser_perf-options-enable-framerate.js] [browser_perf-options-enable-framerate.js]
[browser_perf-options-allocations.js] [browser_perf-options-allocations.js]
[browser_perf-options-profiler.js]
[browser_perf-overview-render-01.js] [browser_perf-overview-render-01.js]
[browser_perf-overview-render-02.js] [browser_perf-overview-render-02.js]
[browser_perf-overview-render-03.js] [browser_perf-overview-render-03.js]

View File

@ -0,0 +1,75 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that when using an older server (< Fx40) where the profiler actor does not
* have the `filterable` trait, the samples are filtered by time on the client, rather
* than the more performant platform code.
*/
const WAIT_TIME = 1000; // ms
function spawnTest () {
let { panel } = yield initPerformance(SIMPLE_URL);
let { gFront: front, gTarget: target } = panel.panelWin;
let connection = getPerformanceActorsConnection(target);
// Explicitly override the profiler's trait `filterable`
connection._profiler.traits.filterable = false;
// Ugly, but we need to also not send the startTime to the server
// so we don't filter it both on the server and client
let request = target.client.request;
target.client.request = (data, res) => {
// Copy so we don't destructively change the request object, as we use
// the startTime on this object for filtering in the callback
let newData = merge({}, data, { startTime: void 0 });
request.call(target.client, newData, res);
};
// Perform the first recording...
let firstRecording = yield front.startRecording();
let firstRecordingStartTime = firstRecording._profilerStartTime;
info("Started profiling at: " + firstRecordingStartTime);
busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
yield front.stopRecording(firstRecording);
is(firstRecordingStartTime, 0,
"The profiling start time should be 0 for the first recording.");
ok(firstRecording.getDuration() >= WAIT_TIME,
"The first recording duration is correct.");
// Perform the second recording...
let secondRecording = yield front.startRecording();
let secondRecordingStartTime = secondRecording._profilerStartTime;
info("Started profiling at: " + secondRecordingStartTime);
busyWait(WAIT_TIME); // allow the profiler module to sample more cpu activity
yield front.stopRecording(secondRecording);
let secondRecordingProfile = secondRecording.getProfile();
let secondRecordingSamples = secondRecordingProfile.threads[0].samples;
isnot(secondRecording._profilerStartTime, 0,
"The profiling start time should not be 0 on the second recording.");
ok(secondRecording.getDuration() >= WAIT_TIME,
"The second recording duration is correct.");
info("Second profile's first sample time: " + secondRecordingSamples[0].time);
ok(secondRecordingSamples[0].time < secondRecordingStartTime,
"The second recorded sample times were normalized.");
ok(secondRecordingSamples[0].time > 0,
"The second recorded sample times were normalized correctly.");
ok(!secondRecordingSamples.find(e => e.time + secondRecordingStartTime <= firstRecording.getDuration()),
"There should be no samples from the first recording in the second one, " +
"even though the total number of frames did not overflow.");
target.client.request = request;
yield teardown(panel);
finish();
}

View File

@ -1,27 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the performance tool can jump to the debugger.
*/
function spawnTest () {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
let { viewSourceInDebugger } = panel.panelWin;
yield viewSourceInDebugger(SIMPLE_URL, 14);
let debuggerPanel = toolbox.getPanel("jsdebugger");
ok(debuggerPanel, "The debugger panel was opened.");
let { DebuggerView } = debuggerPanel.panelWin;
let Sources = DebuggerView.Sources;
is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
"The correct source is shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 14,
"The correct line is highlighted in the debugger's source editor.");
yield teardown(panel);
finish();
}

View File

@ -1,41 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the performance tool can jump to the debugger, when the source was
* already loaded in that tool.
*/
function spawnTest() {
let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL, "jsdebugger");
let debuggerWin = panel.panelWin;
let debuggerEvents = debuggerWin.EVENTS;
let { DebuggerView } = debuggerWin;
let Sources = DebuggerView.Sources;
yield debuggerWin.once(debuggerEvents.SOURCE_SHOWN);
ok("A source was shown in the debugger.");
is(Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
"The correct source is initially shown in the debugger.");
is(DebuggerView.editor.getCursor().line, 0,
"The correct line is initially highlighted in the debugger's source editor.");
yield toolbox.selectTool("performance");
let perfPanel = toolbox.getCurrentPanel();
let perfWin = perfPanel.panelWin;
let { viewSourceInDebugger } = perfWin;
yield viewSourceInDebugger(SIMPLE_URL, 14);
panel = toolbox.getPanel("jsdebugger");
ok(panel, "The debugger panel was reselected.");
is(DebuggerView.Sources.selectedValue, getSourceActor(Sources, SIMPLE_URL),
"The correct source is still shown in the debugger.");
is(DebuggerView.editor.getCursor().line + 1, 14,
"The correct line is now highlighted in the debugger's source editor.");
yield teardown(perfPanel);
finish();
}

View File

@ -0,0 +1,26 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that setting the `devtools.performance.profiler.` prefs propagate to the profiler actor.
*/
function spawnTest () {
let { panel } = yield initPerformance(SIMPLE_URL);
let { gFront } = panel.panelWin;
Services.prefs.setIntPref(PROFILER_BUFFER_SIZE_PREF, 1000);
Services.prefs.setIntPref(PROFILER_SAMPLE_RATE_PREF, 2);
yield startRecording(panel);
let { entries, interval } = yield gFront._request("profiler", "getStartOptions");
yield stopRecording(panel);
is(entries, 1000, "profiler entries option is set on profiler");
is(interval, 0.5, "profiler interval option is set on profiler");
yield teardown(panel);
finish();
}

View File

@ -5,7 +5,7 @@
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
let { Preferences } = Cu.import("resource://gre/modules/Preferences.jsm", {});
let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
@ -25,6 +25,8 @@ const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
const MEMORY_SAMPLE_PROB_PREF = "devtools.performance.memory.sample-probability"; const MEMORY_SAMPLE_PROB_PREF = "devtools.performance.memory.sample-probability";
const MEMORY_MAX_LOG_LEN_PREF = "devtools.performance.memory.max-log-length"; const MEMORY_MAX_LOG_LEN_PREF = "devtools.performance.memory.max-log-length";
const PROFILER_BUFFER_SIZE_PREF = "devtools.performance.profiler.buffer-size";
const PROFILER_SAMPLE_RATE_PREF = "devtools.performance.profiler.sample-frequency-khz";
const FRAMERATE_PREF = "devtools.performance.ui.enable-framerate"; const FRAMERATE_PREF = "devtools.performance.ui.enable-framerate";
const MEMORY_PREF = "devtools.performance.ui.enable-memory"; const MEMORY_PREF = "devtools.performance.ui.enable-memory";
@ -50,8 +52,12 @@ let DEFAULT_PREFS = [
"devtools.performance.ui.enable-memory", "devtools.performance.ui.enable-memory",
"devtools.performance.ui.enable-framerate", "devtools.performance.ui.enable-framerate",
"devtools.performance.ui.show-jit-optimizations", "devtools.performance.ui.show-jit-optimizations",
"devtools.performance.memory.sample-probability",
"devtools.performance.memory.max-log-length",
"devtools.performance.profiler.buffer-size",
"devtools.performance.profiler.sample-frequency-khz",
].reduce((prefs, pref) => { ].reduce((prefs, pref) => {
prefs[pref] = Services.prefs.getBoolPref(pref); prefs[pref] = Preferences.get(pref);
return prefs; return prefs;
}, {}); }, {});
@ -78,7 +84,7 @@ registerCleanupFunction(() => {
// Rollback any pref changes // Rollback any pref changes
Object.keys(DEFAULT_PREFS).forEach(pref => { Object.keys(DEFAULT_PREFS).forEach(pref => {
Services.prefs.setBoolPref(pref, DEFAULT_PREFS[pref]); Preferences.set(pref, DEFAULT_PREFS[pref]);
}); });
// Make sure the profiler module is stopped when the test finishes. // Make sure the profiler module is stopped when the test finishes.
@ -473,11 +479,6 @@ function dropSelection(graph) {
graph.emit("selecting"); graph.emit("selecting");
} }
function getSourceActor(aSources, aURL) {
let item = aSources.getItemForAttachment(a => a.source.url === aURL);
return item && item.value;
}
/** /**
* Fires a key event, like "VK_UP", "VK_DOWN", etc. * Fires a key event, like "VK_UP", "VK_DOWN", etc.
*/ */

View File

@ -61,9 +61,13 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
*/ */
_onLink: function (_, treeItem) { _onLink: function (_, treeItem) {
let { url, line } = treeItem.frame.getInfo(); let { url, line } = treeItem.frame.getInfo();
viewSourceInDebugger(url, line).then( gToolbox.viewSourceInDebugger(url, line).then(success => {
() => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER), if (success) {
() => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER)); this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
} else {
this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
}
});
}, },
/** /**
@ -121,31 +125,3 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
toString: () => "[object JsCallTreeView]" toString: () => "[object JsCallTreeView]"
}); });
/**
* Opens/selects the debugger in this toolbox and jumps to the specified
* file name and line number.
* @param string url
* @param number line
*/
let viewSourceInDebugger = Task.async(function *(url, line) {
// If the Debugger was already open, switch to it and try to show the
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = gToolbox.getPanel("jsdebugger");
let { panelWin: dbg } = yield gToolbox.selectTool("jsdebugger");
if (!debuggerAlreadyOpen) {
yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
}
let { DebuggerView } = dbg;
let { Sources } = DebuggerView;
let item = Sources.getItemForAttachment(a => a.source.url === url);
if (item) {
return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
}
return Promise.reject("Couldn't find the specified source in the debugger.");
});

View File

@ -51,9 +51,13 @@ let MemoryCallTreeView = Heritage.extend(DetailsSubview, {
*/ */
_onLink: function (_, treeItem) { _onLink: function (_, treeItem) {
let { url, line } = treeItem.frame.getInfo(); let { url, line } = treeItem.frame.getInfo();
viewSourceInDebugger(url, line).then( gToolbox.viewSourceInDebugger(url, line).then(success => {
() => this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER), if (success) {
() => this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER)); this.emit(EVENTS.SOURCE_SHOWN_IN_JS_DEBUGGER);
} else {
this.emit(EVENTS.SOURCE_NOT_FOUND_IN_JS_DEBUGGER);
}
});
}, },
/** /**

View File

@ -29,10 +29,12 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this._onMarkerSelected = this._onMarkerSelected.bind(this); this._onMarkerSelected = this._onMarkerSelected.bind(this);
this._onResize = this._onResize.bind(this); this._onResize = this._onResize.bind(this);
this._onViewSource = this._onViewSource.bind(this);
this.waterfall.on("selected", this._onMarkerSelected); this.waterfall.on("selected", this._onMarkerSelected);
this.waterfall.on("unselected", this._onMarkerSelected); this.waterfall.on("unselected", this._onMarkerSelected);
this.details.on("resize", this._onResize); this.details.on("resize", this._onResize);
this.details.on("view-source", this._onViewSource);
let blueprint = PerformanceController.getTimelineBlueprint(); let blueprint = PerformanceController.getTimelineBlueprint();
this.waterfall.setBlueprint(blueprint); this.waterfall.setBlueprint(blueprint);
@ -48,6 +50,7 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this.waterfall.off("selected", this._onMarkerSelected); this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected); this.waterfall.off("unselected", this._onMarkerSelected);
this.details.off("resize", this._onResize); this.details.off("resize", this._onResize);
this.details.off("view-source", this._onViewSource);
}, },
/** /**
@ -97,5 +100,12 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
this.waterfall.setBlueprint(blueprint); this.waterfall.setBlueprint(blueprint);
}, },
/**
* Called when MarkerDetails view emits an event to view source.
*/
_onViewSource: function (_, file, line) {
gToolbox.viewSourceInDebugger(file, line);
},
toString: () => "[object WaterfallView]" toString: () => "[object WaterfallView]"
}); });

View File

@ -350,7 +350,7 @@ let JITOptimizationsView = {
fileName = url.slice(url.lastIndexOf("/") + 1); fileName = url.slice(url.lastIndexOf("/") + 1);
node.classList.add("debugger-link"); node.classList.add("debugger-link");
node.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + url); node.setAttribute("tooltiptext", URL_LABEL_TOOLTIP + " → " + url);
node.addEventListener("click", () => viewSourceInDebugger(url, line)); node.addEventListener("click", () => gToolbox.viewSourceInDebugger(url, line));
} }
fileName = fileName || url || ""; fileName = fileName || url || "";
node.textContent = fileName ? `@${fileName}` : ""; node.textContent = fileName ? `@${fileName}` : "";

View File

@ -22,6 +22,14 @@ var RenamePlugin = Class({
}); });
}, },
onContextMenuOpen: function(resource) {
if (resource.isRoot) {
this.contextMenuItem.setAttribute("hidden", "true");
} else {
this.contextMenuItem.removeAttribute("hidden");
}
},
onCommand: function(cmd) { onCommand: function(cmd) {
if (cmd === "cmd-rename") { if (cmd === "cmd-rename") {
let tree = this.host.projectTree; let tree = this.host.projectTree;

View File

@ -54,6 +54,7 @@ EXTRA_JS_MODULES.devtools.shared += [
'inplace-editor.js', 'inplace-editor.js',
'observable-object.js', 'observable-object.js',
'options-view.js', 'options-view.js',
'source-utils.js',
'telemetry.js', 'telemetry.js',
'theme-switching.js', 'theme-switching.js',
'theme.js', 'theme.js',
@ -66,6 +67,7 @@ EXTRA_JS_MODULES.devtools.shared.widgets += [
'widgets/FastListWidget.js', 'widgets/FastListWidget.js',
'widgets/FilterWidget.js', 'widgets/FilterWidget.js',
'widgets/FlameGraph.js', 'widgets/FlameGraph.js',
'widgets/MdnDocsWidget.js',
'widgets/Spectrum.js', 'widgets/Spectrum.js',
'widgets/TableWidget.js', 'widgets/TableWidget.js',
'widgets/Tooltip.js', 'widgets/Tooltip.js',

View File

@ -0,0 +1,128 @@
/* 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";
loader.lazyRequireGetter(this, "Services");
loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
loader.lazyImporter(this, "Task", "resource://gre/modules/Task.jsm");
/**
* Tries to open a Stylesheet file in the Style Editor. If the file is not found,
* it is opened in source view instead.
* Returns a promise resolving to a boolean indicating whether or not
* the source was able to be displayed in the StyleEditor, as the built-in Firefox
* View Source is the fallback.
*
* @param {Toolbox} toolbox
* @param {string} sourceURL
* @param {number} sourceLine
*
* @return {Promise<boolean>}
*/
exports.viewSourceInStyleEditor = Task.async(function *(toolbox, sourceURL, sourceLine) {
let panel = yield toolbox.loadTool("styleeditor");
try {
let selected = panel.UI.once("editor-selected");
yield panel.selectStyleSheet(sourceURL, sourceLine);
yield toolbox.selectTool("styleeditor");
yield selected;
return true;
} catch (e) {
exports.viewSource(toolbox, sourceURL, sourceLine);
return false;
}
});
/**
* Tries to open a JavaScript file in the Debugger. If the file is not found,
* it is opened in source view instead.
* Returns a promise resolving to a boolean indicating whether or not
* the source was able to be displayed in the Debugger, as the built-in Firefox
* View Source is the fallback.
*
* @param {Toolbox} toolbox
* @param {string} sourceURL
* @param {number} sourceLine
*
* @return {Promise<boolean>}
*/
exports.viewSourceInDebugger = Task.async(function *(toolbox, sourceURL, sourceLine) {
// If the Debugger was already open, switch to it and try to show the
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
let { panelWin: dbg } = yield toolbox.loadTool("jsdebugger");
if (!debuggerAlreadyOpen) {
yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
}
let { DebuggerView } = dbg;
let { Sources } = DebuggerView;
let item = Sources.getItemForAttachment(a => a.source.url === sourceURL);
if (item) {
yield toolbox.selectTool("jsdebugger");
yield DebuggerView.setEditorLocation(item.attachment.source.actor, sourceLine, { noDebug: true });
return true;
}
// If not found, still attempt to open in View Source
exports.viewSource(toolbox, sourceURL, sourceLine);
return false;
});
/**
* Tries to open a JavaScript file in the corresponding Scratchpad.
*
* @param {string} sourceURL
* @param {number} sourceLine
*
* @return {Promise}
*/
exports.viewSourceInScratchpad = Task.async(function *(sourceURL, sourceLine) {
// Check for matching top level scratchpad window.
let wins = Services.wm.getEnumerator("devtools:scratchpad");
while (wins.hasMoreElements()) {
let win = wins.getNext();
if (!win.closed && win.Scratchpad.uniqueName === sourceURL) {
win.focus();
win.Scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
return;
}
}
// For scratchpads within toolbox
for (let [, toolbox] of gDevTools) {
let scratchpadPanel = toolbox.getPanel("scratchpad");
if (scratchpadPanel) {
let { scratchpad } = scratchpadPanel;
if (scratchpad.uniqueName === sourceURL) {
toolbox.selectTool("scratchpad");
toolbox.raise();
scratchpad.editor.focus();
scratchpad.editor.setCursor({ line: sourceLine, ch: 0 });
return;
}
}
}
});
/**
* Open a link in Firefox's View Source.
*
* @param {Toolbox} toolbox
* @param {string} sourceURL
* @param {number} sourceLine
*
* @return {Promise}
*/
exports.viewSource = Task.async(function *(toolbox, sourceURL, sourceLine) {
let utils = toolbox.gViewSourceUtils;
utils.viewSource(sourceURL, null, toolbox.doc, sourceLine || 0);
});

View File

@ -11,6 +11,11 @@ support-files =
browser_devices.json browser_devices.json
doc_options-view.xul doc_options-view.xul
head.js head.js
html-mdn-css-basic-testing.html
html-mdn-css-no-summary.html
html-mdn-css-no-summary-or-syntax.html
html-mdn-css-no-syntax.html
html-mdn-css-syntax-old-style.html
leakhunt.js leakhunt.js
[browser_css_color.js] [browser_css_color.js]
@ -70,6 +75,8 @@ support-files =
skip-if = e10s # Layouthelpers test should not run in a content page. skip-if = e10s # Layouthelpers test should not run in a content page.
[browser_layoutHelpers-getBoxQuads.js] [browser_layoutHelpers-getBoxQuads.js]
skip-if = e10s # Layouthelpers test should not run in a content page. skip-if = e10s # Layouthelpers test should not run in a content page.
[browser_mdn-docs-01.js]
[browser_mdn-docs-02.js]
[browser_num-l10n.js] [browser_num-l10n.js]
[browser_observableobject.js] [browser_observableobject.js]
[browser_options-view-01.js] [browser_options-view-01.js]

View File

@ -0,0 +1,174 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This file tests the MdnDocsWidget object, and specifically its
* loadCssDocs() function.
*
* The MdnDocsWidget is initialized with a document which has a specific
* structure. You then call loadCssDocs(), passing in a CSS property name.
* MdnDocsWidget then fetches docs for that property by making an XHR to
* a docs page, and loads the results into the document. While the XHR is
* still not resolved the document is put into an "initializing" state in
* which the devtools throbber is displayed.
*
* In this file we test:
* - the initial state of the document before the docs have loaded
* - the state of the document after the docs have loaded
*/
"use strict";
const {CssDocsTooltip} = require("devtools/shared/widgets/Tooltip");
const {setBaseCssDocsUrl, MdnDocsWidget} = devtools.require("devtools/shared/widgets/MdnDocsWidget");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
// frame to load the tooltip into
const MDN_DOCS_TOOLTIP_FRAME = "chrome://browser/content/devtools/mdn-docs-frame.xhtml";
/**
* Test properties
*
* In the real tooltip, a CSS property name is used to look up an MDN page
* for that property.
* In the test code, the names defined here is used to look up a page
* served by the test server.
*/
const BASIC_TESTING_PROPERTY = "html-mdn-css-basic-testing.html";
const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
const BASIC_EXPECTED_SYNTAX = "/* The part we want */\nthis: is-the-part-we-want";
const URI_PARAMS = "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default";
add_task(function*() {
setBaseCssDocsUrl(TEST_URI_ROOT);
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", MDN_DOCS_TOOLTIP_FRAME);
let widget = new MdnDocsWidget(win.document);
yield testTheBasics(widget);
host.destroy();
gBrowser.removeCurrentTab();
});
/**
* Test all the basics
* - initial content, before docs have loaded, is as expected
* - throbber is set before docs have loaded
* - contents are as expected after docs have loaded
* - throbber is gone after docs have loaded
* - mdn link text is correct and onclick behavior is correct
*/
function* testTheBasics(widget) {
info("Test all the basic functionality in the widget");
info("Get the widget state before docs have loaded");
let promise = widget.loadCssDocs(BASIC_TESTING_PROPERTY);
info("Check initial contents before docs have loaded");
checkTooltipContents(widget.elements, {
propertyName: BASIC_TESTING_PROPERTY,
summary: "",
syntax: ""
});
// throbber is set
ok(widget.elements.info.classList.contains("devtools-throbber"),
"Throbber is set");
info("Now let the widget finish loading");
yield promise;
info("Check contents after docs have loaded");
checkTooltipContents(widget.elements, {
propertyName: BASIC_TESTING_PROPERTY,
summary: BASIC_EXPECTED_SUMMARY,
syntax: BASIC_EXPECTED_SYNTAX
});
// throbber is gone
ok(!widget.elements.info.classList.contains("devtools-throbber"),
"Throbber is not set");
info("Check that MDN link text is correct and onclick behavior is correct");
let mdnLink = widget.elements.linkToMdn;
let expectedHref = TEST_URI_ROOT + BASIC_TESTING_PROPERTY + URI_PARAMS;
is(mdnLink.href, expectedHref, "MDN link href is correct");
let uri = yield checkLinkClick(mdnLink);
is(uri, expectedHref, "New tab opened with the expected URI");
}
/**
* Clicking the "Visit MDN Page" in the tooltip panel
* should open a new browser tab with the page loaded.
*
* To test this we'll listen for a new tab opening, and
* when it does, add a listener to that new tab to tell
* us when it has loaded.
*
* Then we click the link.
*
* In the tab's load listener, we'll resolve the promise
* with the URI, which is expected to match the href
* in the orginal link.
*
* One complexity is that when you open a new tab,
* "about:blank" is first loaded into the tab before the
* actual page. So we ignore that first load event, and keep
* listening until "load" is triggered for a different URI.
*/
function checkLinkClick(link) {
function loadListener(e) {
let tab = e.target;
var browser = getBrowser().getBrowserForTab(tab);
var uri = browser.currentURI.spec;
// this is horrible, and it's because when we open a new tab
// "about:blank: is first loaded into it, before the actual
// document we want to load.
if (uri != "about:blank") {
info("New browser tab has loaded");
tab.removeEventListener("load", loadListener);
gBrowser.removeTab(tab);
info("Resolve promise with new tab URI");
deferred.resolve(uri);
}
}
function newTabListener(e) {
gBrowser.tabContainer.removeEventListener("TabOpen", newTabListener);
var tab = e.target;
tab.addEventListener("load", loadListener, false);
}
let deferred = promise.defer();
info("Check that clicking the link opens a new tab with the correct URI");
gBrowser.tabContainer.addEventListener("TabOpen", newTabListener, false);
info("Click the link to MDN");
link.click();
return deferred.promise;
}
/**
* Utility function to check content of the tooltip.
*/
function checkTooltipContents(doc, expected) {
is(doc.heading.textContent,
expected.propertyName,
"Property name is correct");
is(doc.summary.textContent,
expected.summary,
"Summary is correct");
is(doc.syntax.textContent,
expected.syntax,
"Syntax is correct");
}

View File

@ -0,0 +1,134 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This file tests the MdnDocsWidget object, and specifically its
* loadCssDocs() function.
*
* The MdnDocsWidget is initialized with a document which has a specific
* structure. You then call loadCssDocs(), passing in a CSS property name.
* MdnDocsWidget then fetches docs for that property by making an XHR to
* a docs page, and loads the results into the document.
*
* In this file we test that the tooltip can properly handle the different
* structures that the docs page might have, including variant structures and
* error conditions like parts of the document being missing.
*
* We also test that the tooltip properly handles the case where the page
* doesn't exist at all.
*/
"use strict";
const {CssDocsTooltip} = require("devtools/shared/widgets/Tooltip");
const {setBaseCssDocsUrl, MdnDocsWidget} = devtools.require("devtools/shared/widgets/MdnDocsWidget");
// frame to load the tooltip into
const MDN_DOCS_TOOLTIP_FRAME = "chrome://browser/content/devtools/mdn-docs-frame.xhtml";
const BASIC_EXPECTED_SUMMARY = "A summary of the property.";
const BASIC_EXPECTED_SYNTAX = "/* The part we want */\nthis: is-the-part-we-want";
const ERROR_MESSAGE = "Could not load docs page.";
/**
* Test properties
*
* In the real tooltip, a CSS property name is used to look up an MDN page
* for that property.
* In the test code, the names defined here are used to look up a page
* served by the test server. We have different properties to test
* different ways that the docs pages might be constructed, including errors
* like pages that don't include docs where we expect.
*/
const SYNTAX_OLD_STYLE = "html-mdn-css-syntax-old-style.html";
const NO_SUMMARY = "html-mdn-css-no-summary.html";
const NO_SYNTAX = "html-mdn-css-no-syntax.html";
const NO_SUMMARY_OR_SYNTAX = "html-mdn-css-no-summary-or-syntax.html";
const TEST_DATA = [{
desc: "Test a property for which we don't have a page",
docsPageUrl: "i-dont-exist.html",
expectedContents: {
propertyName: "i-dont-exist.html",
summary: ERROR_MESSAGE,
syntax: ""
}
}, {
desc: "Test a property whose syntax section is specified using an old-style page",
docsPageUrl: SYNTAX_OLD_STYLE,
expectedContents: {
propertyName: SYNTAX_OLD_STYLE,
summary: BASIC_EXPECTED_SUMMARY,
syntax: BASIC_EXPECTED_SYNTAX
}
}, {
desc: "Test a property whose page doesn't have a summary",
docsPageUrl: NO_SUMMARY,
expectedContents: {
propertyName: NO_SUMMARY,
summary: "",
syntax: BASIC_EXPECTED_SYNTAX
}
}, {
desc: "Test a property whose page doesn't have a syntax",
docsPageUrl: NO_SYNTAX,
expectedContents: {
propertyName: NO_SYNTAX,
summary: BASIC_EXPECTED_SUMMARY,
syntax: ""
}
}, {
desc: "Test a property whose page doesn't have a summary or a syntax",
docsPageUrl: NO_SUMMARY_OR_SYNTAX,
expectedContents: {
propertyName: NO_SUMMARY_OR_SYNTAX,
summary: ERROR_MESSAGE,
syntax: ""
}
}
];
add_task(function*() {
setBaseCssDocsUrl(TEST_URI_ROOT);
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", MDN_DOCS_TOOLTIP_FRAME);
let widget = new MdnDocsWidget(win.document);
for (let {desc, docsPageUrl, expectedContents} of TEST_DATA) {
info(desc);
yield widget.loadCssDocs(docsPageUrl);
checkTooltipContents(widget.elements, expectedContents);
}
host.destroy();
gBrowser.removeCurrentTab();
});
function* testNonExistentPage(widget) {
info("Test a property for which we don't have a page");
yield widget.loadCssDocs("i-dont-exist.html");
checkTooltipContents(widget.elements, {
propertyName: "i-dont-exist.html",
summary: ERROR_MESSAGE,
syntax: ""
});
}
/*
* Utility function to check content of the tooltip.
*/
function checkTooltipContents(doc, expected) {
is(doc.heading.textContent,
expected.propertyName,
"Property name is correct");
is(doc.summary.textContent,
expected.summary,
"Summary is correct");
is(doc.syntax.textContent,
expected.syntax,
"Syntax is correct");
}

View File

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h2 id="Summary">Summary</h2>
<p>A summary of the property.</p>
<h2 id="Syntax">Syntax</h2>
<pre>/* The part we want */
this: is-the-part-we-want</pre>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p>This is not the summary or the syntax.</p>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p>This is not the summary.</p>
<h2 id="Syntax">Syntax</h2>
<pre>To be ignored.</pre>
<pre>/* The part we want */
this: is-the-part-we-want</pre>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h2 id="Summary">Summary</h2>
<p>A summary of the property.</p>
<p>This is not the syntax.</p>
</body>
</html>

View File

@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<h2 id="Summary">Summary</h2>
<p>A summary of the property.</p>
<h2 id="Syntax">Syntax</h2>
<pre>The part we should ignore</pre>
<pre>/* The part we want */
this: is-the-part-we-want</pre>
</body>
</html>

View File

@ -138,6 +138,9 @@ MarkerDetails.prototype = {
case "DOMEvent": case "DOMEvent":
this.renderDOMEventMarker(this._parent, marker); this.renderDOMEventMarker(this._parent, marker);
break; break;
case "Javascript":
this.renderJavascriptMarker(this._parent, marker);
break;
default: default:
} }
@ -219,7 +222,7 @@ MarkerDetails.prototype = {
aNode.addEventListener("click", (event) => { aNode.addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
viewSourceInDebugger(toolbox, url, line); this.emit("view-source", url, line);
}); });
} }
@ -284,36 +287,21 @@ MarkerDetails.prototype = {
} }
}, },
/**
* Render details of a Javascript marker.
*
* @param nsIDOMNode parent
* The parent node holding the view.
* @param object marker
* The marker to display.
*/
renderJavascriptMarker: function(parent, marker) {
if ("causeName" in marker) {
let cause = this.buildNameValueLabel("timeline.markerDetail.causeName", marker.causeName);
this._parent.appendChild(cause);
}
},
}; };
/**
* Opens/selects the debugger in this toolbox and jumps to the specified
* file name and line number.
* @param object toolbox
* The toolbox.
* @param string url
* @param number line
*/
let viewSourceInDebugger = Task.async(function *(toolbox, url, line) {
// If the Debugger was already open, switch to it and try to show the
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
let { panelWin: dbg } = yield toolbox.selectTool("jsdebugger");
if (!debuggerAlreadyOpen) {
yield dbg.once(dbg.EVENTS.SOURCES_ADDED);
}
let { DebuggerView } = dbg;
let { Sources } = DebuggerView;
let item = Sources.getItemForAttachment(a => a.source.url === url);
if (item) {
return DebuggerView.setEditorLocation(item.attachment.source.actor, line, { noDebug: true });
}
return Promise.reject("Couldn't find the specified source in the debugger.");
});
exports.MarkerDetails = MarkerDetails; exports.MarkerDetails = MarkerDetails;

View File

@ -0,0 +1,409 @@
/* 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/. */
/**
* This file contains functions to retrieve docs content from
* MDN (developer.mozilla.org) for particular items, and to display
* the content in a tooltip.
*
* At the moment it only supports fetching content for CSS properties,
* but it might support other types of content in the future
* (Web APIs, for example).
*
* It's split into two parts:
*
* - functions like getCssDocs that just fetch content from MDN,
* without any constraints on what to do with the content. If you
* want to embed the content in some custom way, use this.
*
* - the MdnDocsWidget class, that manages and updates a tooltip
* document whose content is taken from MDN. If you want to embed
* the content in a tooltip, use this in conjunction with Tooltip.js.
*/
"use strict";
const {Cc, Cu, Ci} = require("chrome");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
// Parameters for the XHR request
// see https://developer.mozilla.org/en-US/docs/MDN/Kuma/API#Document_parameters
const XHR_PARAMS = "?raw&macros";
// URL for the XHR request
var XHR_CSS_URL = "https://developer.mozilla.org/en-US/docs/Web/CSS/";
// Parameters for the link to MDN in the tooltip, so
// so we know which MDN visits come from this feature
const PAGE_LINK_PARAMS = "?utm_source=mozilla&utm_medium=firefox-inspector&utm_campaign=default"
// URL for the page link
// omits locale, so a locale-specific page will be loaded
var PAGE_LINK_URL = "https://developer.mozilla.org/docs/Web/CSS/";
const BROWSER_WINDOW = 'navigator:browser';
/**
* Fetch an MDN page.
*
* @param {string} pageUrl
* URL of the page to fetch.
*
* @return {promise}
* The promise is resolved with the page as an XML document.
*
* The promise is rejected with an error message if
* we could not load the page.
*/
function getMdnPage(pageUrl) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.addEventListener("load", onLoaded, false);
xhr.addEventListener("error", onError, false);
xhr.open("GET", pageUrl);
xhr.responseType = "document";
xhr.send();
function onLoaded(e) {
if (xhr.status != 200) {
deferred.reject({page: pageUrl, status: xhr.status});
}
else {
deferred.resolve(xhr.responseXML);
}
}
function onError(e) {
deferred.reject({page: pageUrl, status: xhr.status});
}
return deferred.promise;
}
/**
* Gets some docs for the given CSS property.
* Loads an MDN page for the property and gets some
* information about the property.
*
* @param {string} cssProperty
* The property for which we want docs.
*
* @return {promise}
* The promise is resolved with an object containing:
* - summary: a short summary of the property
* - syntax: some example syntax
*
* The promise is rejected with an error message if
* we could not load the page.
*/
function getCssDocs(cssProperty) {
let deferred = Promise.defer();
let pageUrl = XHR_CSS_URL + cssProperty + XHR_PARAMS;
getMdnPage(pageUrl).then(parseDocsFromResponse, handleRejection);
function parseDocsFromResponse(responseDocument) {
let theDocs = {};
theDocs.summary = getSummary(responseDocument);
theDocs.syntax = getSyntax(responseDocument);
if (theDocs.summary || theDocs.syntax) {
deferred.resolve(theDocs);
}
else {
deferred.reject("Couldn't find the docs in the page.");
}
}
function handleRejection(e) {
deferred.reject(e.status);
}
return deferred.promise;
}
exports.getCssDocs = getCssDocs;
/**
* The MdnDocsWidget is used by tooltip code that needs to display docs
* from MDN in a tooltip. The tooltip code loads a document that contains the
* basic structure of a docs tooltip (loaded from mdn-docs-frame.xhtml),
* and passes this document into the widget's constructor.
*
* In the constructor, the widget does some general setup that's not
* dependent on the particular item we need docs for.
*
* After that, when the tooltip code needs to display docs for an item, it
* asks the widget to retrieve the docs and update the document with them.
*
* @param {Document} tooltipDocument
* A DOM document. The widget expects the document to have a particular
* structure.
*/
function MdnDocsWidget(tooltipDocument) {
// fetch all the bits of the document that we will manipulate later
this.elements = {
heading: tooltipDocument.getElementById("property-name"),
summary: tooltipDocument.getElementById("summary"),
syntax: tooltipDocument.getElementById("syntax"),
info: tooltipDocument.getElementById("property-info"),
linkToMdn: tooltipDocument.getElementById("visit-mdn-page")
};
// get the localized string for the link text
this.elements.linkToMdn.textContent =
l10n.strings.GetStringFromName("docsTooltip.visitMDN");
// listen for clicks and open in the browser window instead
let browserWindow = Services.wm.getMostRecentWindow(BROWSER_WINDOW);
this.elements.linkToMdn.addEventListener("click", function(e) {
e.stopPropagation();
e.preventDefault();
let link = e.target.href;
browserWindow.gBrowser.addTab(link);
});
}
exports.MdnDocsWidget = MdnDocsWidget;
MdnDocsWidget.prototype = {
/**
* This is called just before the tooltip is displayed, and is
* passed the CSS property for which we want to display help.
*
* Its job is to make sure the document contains the docs
* content for that CSS property.
*
* First, it initializes the document, setting the things it can
* set synchronously, resetting the things it needs to get
* asynchronously, and making sure the throbber is throbbing.
*
* Then it tries to get the content asynchronously, updating
* the document with the content or with an error message.
*
* It returns immediately, so the caller can display the tooltip
* without waiting for the asynch operation to complete.
*
* @param {string} propertyName
* The name of the CSS property for which we need to display help.
*/
loadCssDocs: function(propertyName) {
/**
* Do all the setup we can do synchronously, and get the document in
* a state where it can be displayed while we are waiting for the
* MDN docs content to be retrieved.
*/
function initializeDocument(propertyName) {
// set property name heading
elements.heading.textContent = propertyName;
// set link target
elements.linkToMdn.setAttribute("href",
PAGE_LINK_URL + propertyName + PAGE_LINK_PARAMS);
// clear docs summary and syntax
elements.summary.textContent = "";
elements.syntax.textContent = "";
// reset the scroll position
elements.info.scrollTop = 0;
elements.info.scrollLeft = 0;
// show the throbber
elements.info.classList.add("devtools-throbber");
}
/**
* This is called if we successfully got the docs content.
* Finishes setting up the tooltip content, and disables the throbber.
*/
function finalizeDocument({summary, syntax}) {
// set docs summary and syntax
elements.summary.textContent = summary;
elements.syntax.textContent = syntax;
// hide the throbber
elements.info.classList.remove("devtools-throbber");
deferred.resolve(this);
}
/**
* This is called if we failed to get the docs content.
* Sets the content to contain an error message, and disables the throbber.
*/
function gotError(error) {
// show error message
elements.summary.textContent = l10n.strings.GetStringFromName("docsTooltip.loadDocsError");
// hide the throbber
elements.info.classList.remove("devtools-throbber");
// although gotError is called when there's an error, we have handled
// the error, so call resolve not reject.
deferred.resolve(this);
}
let deferred = Promise.defer();
let elements = this.elements;
initializeDocument(propertyName);
getCssDocs(propertyName).then(finalizeDocument, gotError);
return deferred.promise;
}
}
/**
* L10N utility class
*/
function L10N() {}
L10N.prototype = {};
let l10n = new L10N();
loader.lazyGetter(L10N.prototype, "strings", () => {
return Services.strings.createBundle(
"chrome://browser/locale/devtools/inspector.properties");
});
/**
* Test whether a node is all whitespace.
*
* @return {boolean}
* True if the node all whitespace, otherwise false.
*/
function isAllWhitespace(node) {
return !(/[^\t\n\r ]/.test(node.textContent));
}
/**
* Test whether a node is a comment or whitespace node.
*
* @return {boolean}
* True if the node is a comment node or is all whitespace, otherwise false.
*/
function isIgnorable(node) {
return (node.nodeType == 8) || // A comment node
((node.nodeType == 3) && isAllWhitespace(node)); // text node, all ws
}
/**
* Get the next node, skipping comments and whitespace.
*
* @return {node}
* The next sibling node that is not a comment or whitespace, or null if
* there isn't one.
*/
function nodeAfter(sib) {
while ((sib = sib.nextSibling)) {
if (!isIgnorable(sib)) return sib;
}
return null;
}
/**
* Test whether the argument `node` is a node whose tag is `tagName`.
*
* @param {node} node
* The code to test. May be null.
*
* @param {string} tagName
* The tag name to test against.
*
* @return {boolean}
* True if the node is not null and has the tag name `tagName`,
* otherwise false.
*/
function hasTagName(node, tagName) {
return node && node.tagName &&
node.tagName.toLowerCase() == tagName.toLowerCase();
}
/**
* Given an MDN page, get the "summary" portion.
*
* This is the textContent of the first non-whitespace
* element in the #Summary section of the document.
*
* It's expected to be a <P> element.
*
* @param {Document} mdnDocument
* The document in which to look for the "summary" section.
*
* @return {string}
* The summary section as a string, or null if it could not be found.
*/
function getSummary(mdnDocument) {
let summary = mdnDocument.getElementById("Summary");
if (!hasTagName(summary, "H2")) {
return null;
}
let firstParagraph = nodeAfter(summary);
if (!hasTagName(firstParagraph, "P")) {
return null;
}
return firstParagraph.textContent;
}
/**
* Given an MDN page, get the "syntax" portion.
*
* First we get the #Syntax section of the document. The syntax
* section we want is somewhere inside there.
*
* If the page is in the old structure, then the *first two*
* non-whitespace elements in the #Syntax section will be <PRE>
* nodes, and the second of these will be the syntax section.
*
* If the page is in the new structure, then the only the *first*
* non-whitespace element in the #Syntax section will be a <PRE>
* node, and it will be the syntax section.
*
* @param {Document} mdnDocument
* The document in which to look for the "syntax" section.
*
* @return {string}
* The syntax section as a string, or null if it could not be found.
*/
function getSyntax(mdnDocument) {
let syntax = mdnDocument.getElementById("Syntax");
if (!hasTagName(syntax, "H2")) {
return null;
}
let firstParagraph = nodeAfter(syntax);
if (!hasTagName(firstParagraph, "PRE")) {
return null;
}
let secondParagraph = nodeAfter(firstParagraph);
if (hasTagName(secondParagraph, "PRE")) {
return secondParagraph.textContent;
}
else {
return firstParagraph.textContent;
}
}
/**
* Use a different URL for CSS docs pages. Used only for testing.
*
* @param {string} baseUrl
* The baseURL to use.
*/
function setBaseCssDocsUrl(baseUrl) {
PAGE_LINK_URL = baseUrl;
XHR_CSS_URL = baseUrl;
}
exports.setBaseCssDocsUrl = setBaseCssDocsUrl;

View File

@ -10,6 +10,7 @@ const IOService = Cc["@mozilla.org/network/io-service;1"]
.getService(Ci.nsIIOService); .getService(Ci.nsIIOService);
const {Spectrum} = require("devtools/shared/widgets/Spectrum"); const {Spectrum} = require("devtools/shared/widgets/Spectrum");
const {CubicBezierWidget} = require("devtools/shared/widgets/CubicBezierWidget"); const {CubicBezierWidget} = require("devtools/shared/widgets/CubicBezierWidget");
const {MdnDocsWidget} = require("devtools/shared/widgets/MdnDocsWidget");
const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget"); const {CSSFilterEditorWidget} = require("devtools/shared/widgets/FilterWidget");
const EventEmitter = require("devtools/toolkit/event-emitter"); const EventEmitter = require("devtools/toolkit/event-emitter");
const {colorUtils} = require("devtools/css-color"); const {colorUtils} = require("devtools/css-color");
@ -40,6 +41,7 @@ const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XHTML_NS = "http://www.w3.org/1999/xhtml";
const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml"; const SPECTRUM_FRAME = "chrome://browser/content/devtools/spectrum-frame.xhtml";
const CUBIC_BEZIER_FRAME = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml"; const CUBIC_BEZIER_FRAME = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
const MDN_DOCS_FRAME = "chrome://browser/content/devtools/mdn-docs-frame.xhtml";
const FILTER_FRAME = "chrome://browser/content/devtools/filter-frame.xhtml"; const FILTER_FRAME = "chrome://browser/content/devtools/filter-frame.xhtml";
const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE; const ESCAPE_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE;
const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN; const RETURN_KEYCODE = Ci.nsIDOMKeyEvent.DOM_VK_RETURN;
@ -366,6 +368,8 @@ Tooltip.prototype = {
* A function that accepts a node argument and returns true or false * A function that accepts a node argument and returns true or false
* (or a promise that resolves or rejects) to signify if the tooltip * (or a promise that resolves or rejects) to signify if the tooltip
* should be shown on that node or not. * should be shown on that node or not.
* If the promise rejects, it must reject `false` as value.
* Any other value is going to be logged as unexpected error.
* Additionally, the function receives a second argument which is the * Additionally, the function receives a second argument which is the
* tooltip instance itself, to be used to add/modify the content of the * tooltip instance itself, to be used to add/modify the content of the
* tooltip if needed. If omitted, the tooltip will be shown everytime. * tooltip if needed. If omitted, the tooltip will be shown everytime.
@ -733,30 +737,71 @@ Tooltip.prototype = {
_getImageDimensionLabel: (w, h) => w + " \u00D7 " + h, _getImageDimensionLabel: (w, h) => w + " \u00D7 " + h,
/**
* Load a document into an iframe, and set the iframe
* to be the tooltip's content.
*
* Used by tooltips that want to load their interface
* into an iframe from a URL.
*
* @param {string} width
* Width of the iframe.
* @param {string} height
* Height of the iframe.
* @param {string} url
* URL of the document to load into the iframe.
*
* @return {promise} A promise which is resolved with
* the iframe.
*
* This function creates an iframe, loads the specified document
* into it, sets the tooltip's content to the iframe, and returns
* a promise.
*
* When the document is loaded, the function gets the content window
* and resolves the promise with the content window.
*/
setIFrameContent: function({width, height}, url) {
let def = promise.defer();
// Create an iframe
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
iframe.setAttribute("transparent", true);
iframe.setAttribute("width", width);
iframe.setAttribute("height", height);
iframe.setAttribute("flex", "1");
iframe.setAttribute("class", "devtools-tooltip-iframe");
// Wait for the load to initialize the widget
function onLoad() {
iframe.removeEventListener("load", onLoad, true);
def.resolve(iframe);
}
iframe.addEventListener("load", onLoad, true);
// load the document from url into the iframe
iframe.setAttribute("src", url);
// Put the iframe in the tooltip
this.content = iframe;
return def.promise;
},
/** /**
* Fill the tooltip with a new instance of the spectrum color picker widget * Fill the tooltip with a new instance of the spectrum color picker widget
* initialized with the given color, and return a promise that resolves to * initialized with the given color, and return a promise that resolves to
* the instance of spectrum * the instance of spectrum
*/ */
setColorPickerContent: function(color) { setColorPickerContent: function(color) {
let def = promise.defer(); let dimensions = {width: "210", height: "216"};
// Create an iframe to contain spectrum
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
iframe.setAttribute("transparent", true);
iframe.setAttribute("width", "210");
iframe.setAttribute("height", "216");
iframe.setAttribute("flex", "1");
iframe.setAttribute("class", "devtools-tooltip-iframe");
let panel = this.panel; let panel = this.panel;
let xulWin = this.doc.ownerGlobal; return this.setIFrameContent(dimensions, SPECTRUM_FRAME).then(onLoaded);
// Wait for the load to initialize spectrum function onLoaded(iframe) {
function onLoad() {
iframe.removeEventListener("load", onLoad, true);
let win = iframe.contentWindow.wrappedJSObject; let win = iframe.contentWindow.wrappedJSObject;
let def = promise.defer();
let container = win.document.getElementById("spectrum"); let container = win.document.getElementById("spectrum");
let spectrum = new Spectrum(container, color); let spectrum = new Spectrum(container, color);
@ -775,14 +820,8 @@ Tooltip.prototype = {
finalizeSpectrum(); finalizeSpectrum();
}, true); }, true);
} }
return def.promise;
} }
iframe.addEventListener("load", onLoad, true);
iframe.setAttribute("src", SPECTRUM_FRAME);
// Put the iframe in the tooltip
this.content = iframe;
return def.promise;
}, },
/** /**
@ -791,24 +830,13 @@ Tooltip.prototype = {
* the instance of the widget * the instance of the widget
*/ */
setCubicBezierContent: function(bezier) { setCubicBezierContent: function(bezier) {
let def = promise.defer(); let dimensions = {width: "410", height: "360"};
// Create an iframe to host the cubic-bezier widget
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
iframe.setAttribute("transparent", true);
iframe.setAttribute("width", "410");
iframe.setAttribute("height", "360");
iframe.setAttribute("flex", "1");
iframe.setAttribute("class", "devtools-tooltip-iframe");
let panel = this.panel; let panel = this.panel;
let xulWin = this.doc.ownerGlobal; return this.setIFrameContent(dimensions, CUBIC_BEZIER_FRAME).then(onLoaded);
// Wait for the load to initialize the widget function onLoaded(iframe) {
function onLoad() {
iframe.removeEventListener("load", onLoad, true);
let win = iframe.contentWindow.wrappedJSObject; let win = iframe.contentWindow.wrappedJSObject;
let def = promise.defer();
let container = win.document.getElementById("container"); let container = win.document.getElementById("container");
let widget = new CubicBezierWidget(container, bezier); let widget = new CubicBezierWidget(container, bezier);
@ -821,14 +849,8 @@ Tooltip.prototype = {
def.resolve(widget); def.resolve(widget);
}, true); }, true);
} }
return def.promise;
} }
iframe.addEventListener("load", onLoad, true);
iframe.setAttribute("src", CUBIC_BEZIER_FRAME);
// Put the iframe in the tooltip
this.content = iframe;
return def.promise;
}, },
/** /**
@ -837,29 +859,21 @@ Tooltip.prototype = {
* that resolves to the instance of the widget when ready. * that resolves to the instance of the widget when ready.
*/ */
setFilterContent: function(filter) { setFilterContent: function(filter) {
let def = promise.defer(); let dimensions = {width: "350", height: "350"};
// Create an iframe to host the filter widget
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
iframe.setAttribute("transparent", true);
iframe.setAttribute("width", "350");
iframe.setAttribute("flex", "1");
iframe.setAttribute("class", "devtools-tooltip-iframe");
let panel = this.panel; let panel = this.panel;
return this.setIFrameContent(dimensions, FILTER_FRAME).then(onLoaded);
function onLoad() { function onLoaded(iframe) {
iframe.removeEventListener("load", onLoad, true); let win = iframe.contentWindow.wrappedJSObject;
let win = iframe.contentWindow.wrappedJSObject, let doc = win.document.documentElement;
doc = win.document.documentElement; let def = promise.defer();
let container = win.document.getElementById("container"); let container = win.document.getElementById("container");
let widget = new CSSFilterEditorWidget(container, filter); let widget = new CSSFilterEditorWidget(container, filter);
iframe.height = doc.offsetHeight iframe.height = doc.offsetHeight;
widget.on("render", e => { widget.on("render", e => {
iframe.height = doc.offsetHeight iframe.height = doc.offsetHeight;
}); });
// Resolve to the widget instance whenever the popup becomes visible // Resolve to the widget instance whenever the popup becomes visible
@ -871,14 +885,8 @@ Tooltip.prototype = {
def.resolve(widget); def.resolve(widget);
}, true); }, true);
} }
return def.promise;
} }
iframe.addEventListener("load", onLoad, true);
iframe.setAttribute("src", FILTER_FRAME);
// Put the iframe in the tooltip
this.content = iframe;
return def.promise;
}, },
/** /**
@ -909,7 +917,33 @@ Tooltip.prototype = {
let str = yield data.string(); let str = yield data.string();
this.setImageContent(str, { hideDimensionLabel: true, maxDim: size }); this.setImageContent(str, { hideDimensionLabel: true, maxDim: size });
} }
}) }),
/**
* Set the content of this tooltip to the MDN docs widget.
*
* This is called when the tooltip is first constructed.
*
* @return {promise} A promise which is resolved with an MdnDocsWidget.
*
* It loads the tooltip's structure from a separate XHTML file
* into an iframe. When the iframe is loaded it constructs
* an MdnDocsWidget and passes that into resolve.
*
* The caller can use the MdnDocsWidget to update the tooltip's
* UI with new content each time the tooltip is shown.
*/
setMdnDocsContent: function() {
let dimensions = {width: "410", height: "300"};
return this.setIFrameContent(dimensions, MDN_DOCS_FRAME).then(onLoaded);
function onLoaded(iframe) {
let win = iframe.contentWindow.wrappedJSObject;
// create an MdnDocsWidget, initializing it with the content document
let widget = new MdnDocsWidget(win.document);
return widget;
}
}
}; };
/** /**
@ -1535,6 +1569,46 @@ SwatchCubicBezierTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
} }
}); });
/**
* Tooltip for displaying docs for CSS properties from MDN.
*
* @param {XULDocument} doc
*/
function CssDocsTooltip(doc) {
this.tooltip = new Tooltip(doc, {
consumeOutsideClick: true,
closeOnKeys: [ESCAPE_KEYCODE, RETURN_KEYCODE],
noAutoFocus: false
});
this.widget = this.tooltip.setMdnDocsContent();
}
module.exports.CssDocsTooltip = CssDocsTooltip;
CssDocsTooltip.prototype = {
/**
* Load CSS docs for the given property,
* then display the tooltip.
*/
show: function(anchor, propertyName) {
function loadCssDocs(widget) {
return widget.loadCssDocs(propertyName);
}
this.widget.then(loadCssDocs);
this.tooltip.show(anchor, "topcenter bottomleft");
},
hide: function() {
this.tooltip.hide();
},
destroy: function() {
this.tooltip.destroy();
}
};
/** /**
* The swatch-based css filter tooltip class is a specific class meant to be used * The swatch-based css filter tooltip class is a specific class meant to be used
* along with rule-view's generated css filter swatches. * along with rule-view's generated css filter swatches.

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/content/devtools/mdn-docs.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
</head>
<body class="theme-body">
<div id = "container">
<header>
<h1 id="property-name" class="theme-fg-color5"></h1>
</header>
<div id="property-info">
<div id="summary"></div>
<pre id="syntax" class="devtools-monospace"></pre>
</div>
<footer>
<a id="visit-mdn-page" class="theme-link" href="#">Visit MDN (placeholder)</a>
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
/* 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/. */
#visit-mdn-page {
display: inline-block;
padding: 1em 0;
}
html, body, #container {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
#container {
display: flex;
flex-direction: column;
}
header, footer {
flex: 1;
padding: 0 1em;
}
#property-info {
flex: 10;
padding: 0 1em;
overflow: auto;
transition: opacity 400ms ease-in;
}
#syntax {
margin-top: 1em;
}
.devtools-throbber {
opacity: 0;
align-self: center;
}

View File

@ -1561,8 +1561,8 @@ SelectorView.prototype = {
contentDoc = rawNode.ownerDocument; contentDoc = rawNode.ownerDocument;
} }
} }
let viewSourceUtils = inspector.viewSourceUtils; let toolbox = gDevTools.getToolbox(inspector.target);
viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line); toolbox.viewSource(rule.href, rule.line);
return; return;
} }

View File

@ -25,6 +25,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
const PROPERTY_NAME_CLASS = "ruleview-propertyname";
const FILTER_CHANGED_TIMEOUT = 150; const FILTER_CHANGED_TIMEOUT = 150;
/** /**
@ -1126,6 +1127,7 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this._onCopy = this._onCopy.bind(this); this._onCopy = this._onCopy.bind(this);
this._onCopyColor = this._onCopyColor.bind(this); this._onCopyColor = this._onCopyColor.bind(this);
this._onToggleOrigSources = this._onToggleOrigSources.bind(this); this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this); this._onFilterStyles = this._onFilterStyles.bind(this);
this._onClearSearch = this._onClearSearch.bind(this); this._onClearSearch = this._onClearSearch.bind(this);
this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this); this._onFilterTextboxContextMenu = this._onFilterTextboxContextMenu.bind(this);
@ -1216,6 +1218,12 @@ CssRuleView.prototype = {
type: "checkbox" type: "checkbox"
}); });
this.menuitemShowMdnDocs = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.showMdnDocs",
accesskey: "ruleView.contextmenu.showMdnDocs.accessKey",
command: this._onShowMdnDocs
});
let popupset = doc.documentElement.querySelector("popupset"); let popupset = doc.documentElement.querySelector("popupset");
if (!popupset) { if (!popupset) {
popupset = doc.createElementNS(XUL_NS, "popupset"); popupset = doc.createElementNS(XUL_NS, "popupset");
@ -1342,6 +1350,9 @@ CssRuleView.prototype = {
var showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); var showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
this.menuitemSources.setAttribute("checked", showOrig); this.menuitemSources.setAttribute("checked", showOrig);
this.menuitemShowMdnDocs.hidden = !this.doc.popupNode.parentNode
.classList.contains(PROPERTY_NAME_CLASS);
this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode(); this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode();
}, },
@ -1363,7 +1374,7 @@ CssRuleView.prototype = {
let classes = node.classList; let classes = node.classList;
let prop = getParentTextProperty(node); let prop = getParentTextProperty(node);
if (classes.contains("ruleview-propertyname") && prop) { if (classes.contains(PROPERTY_NAME_CLASS) && prop) {
type = overlays.VIEW_NODE_PROPERTY_TYPE; type = overlays.VIEW_NODE_PROPERTY_TYPE;
value = { value = {
property: node.textContent, property: node.textContent,
@ -1518,6 +1529,16 @@ CssRuleView.prototype = {
Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
}, },
/**
* Show docs from MDN for a CSS property.
*/
_onShowMdnDocs: function() {
let cssPropertyName = this.doc.popupNode.textContent;
let anchor = this.doc.popupNode.parentNode;
let cssDocsTooltip = this.tooltips.cssDocs;
cssDocsTooltip.show(anchor, cssPropertyName);
},
/** /**
* Add a new rule to the current element. * Add a new rule to the current element.
*/ */
@ -2410,7 +2431,7 @@ RuleEditor.prototype = {
}); });
this.newPropSpan = createChild(this.newPropItem, "span", { this.newPropSpan = createChild(this.newPropItem, "span", {
class: "ruleview-propertyname", class: PROPERTY_NAME_CLASS,
tabindex: "0" tabindex: "0"
}); });

View File

@ -17,6 +17,7 @@ const {
Tooltip, Tooltip,
SwatchColorPickerTooltip, SwatchColorPickerTooltip,
SwatchCubicBezierTooltip, SwatchCubicBezierTooltip,
CssDocsTooltip,
SwatchFilterTooltip SwatchFilterTooltip
} = require("devtools/shared/widgets/Tooltip"); } = require("devtools/shared/widgets/Tooltip");
const {CssLogic} = require("devtools/styleinspector/css-logic"); const {CssLogic} = require("devtools/styleinspector/css-logic");
@ -263,6 +264,8 @@ TooltipsOverlay.prototype = {
this.colorPicker = new SwatchColorPickerTooltip(this.view.inspector.panelDoc); this.colorPicker = new SwatchColorPickerTooltip(this.view.inspector.panelDoc);
// Cubic bezier tooltip // Cubic bezier tooltip
this.cubicBezier = new SwatchCubicBezierTooltip(this.view.inspector.panelDoc); this.cubicBezier = new SwatchCubicBezierTooltip(this.view.inspector.panelDoc);
// MDN CSS help tooltip
this.cssDocs = new CssDocsTooltip(this.view.inspector.panelDoc);
// Filter editor tooltip // Filter editor tooltip
this.filterEditor = new SwatchFilterTooltip(this.view.inspector.panelDoc); this.filterEditor = new SwatchFilterTooltip(this.view.inspector.panelDoc);
} }
@ -290,6 +293,10 @@ TooltipsOverlay.prototype = {
this.cubicBezier.destroy(); this.cubicBezier.destroy();
} }
if (this.cssDocs) {
this.cssDocs.destroy();
}
if (this.filterEditor) { if (this.filterEditor) {
this.filterEditor.destroy(); this.filterEditor.destroy();
} }
@ -334,13 +341,13 @@ TooltipsOverlay.prototype = {
let nodeInfo = this.view.getNodeInfo(target); let nodeInfo = this.view.getNodeInfo(target);
if (!nodeInfo) { if (!nodeInfo) {
// The hovered node isn't something we care about // The hovered node isn't something we care about
return promise.reject(); return promise.reject(false);
} }
let type = this._getTooltipType(nodeInfo); let type = this._getTooltipType(nodeInfo);
if (!type) { if (!type) {
// There is no tooltip type defined for the hovered node // There is no tooltip type defined for the hovered node
return promise.reject(); return promise.reject(false);
} }
if (this.isRuleView && this.colorPicker.tooltip.isShown()) { if (this.isRuleView && this.colorPicker.tooltip.isShown()) {
@ -353,6 +360,10 @@ TooltipsOverlay.prototype = {
this.cubicBezier.hide(); this.cubicBezier.hide();
} }
if (this.isRuleView && this.cssDocs.tooltip.isShown()) {
this.cssDocs.hide();
}
if (this.isRuleView && this.filterEditor.tooltip.isShown()) { if (this.isRuleView && this.filterEditor.tooltip.isShown()) {
this.filterEditor.revert(); this.filterEditor.revert();
this.filterEdtior.hide(); this.filterEdtior.hide();
@ -387,6 +398,10 @@ TooltipsOverlay.prototype = {
this.cubicBezier.hide(); this.cubicBezier.hide();
} }
if (this.cssDocs) {
this.cssDocs.hide();
}
if (this.filterEditor) { if (this.filterEditor) {
this.filterEditor.hide(); this.filterEditor.hide();
} }

View File

@ -110,9 +110,9 @@ RuleViewTool.prototype = {
// these sheets in the view source window instead. // these sheets in the view source window instead.
if (!sheet || sheet.isSystem) { if (!sheet || sheet.isSystem) {
let contentDoc = this.inspector.selection.document; let contentDoc = this.inspector.selection.document;
let viewSourceUtils = this.inspector.viewSourceUtils;
let href = rule.nodeHref || rule.href; let href = rule.nodeHref || rule.href;
viewSourceUtils.viewSource(href, null, contentDoc, rule.line || 0); let toolbox = gDevTools.getToolbox(this.inspector.target);
toolbox.viewSource(href, rule.line);
return; return;
} }

View File

@ -70,6 +70,8 @@ support-files =
[browser_ruleview_content_01.js] [browser_ruleview_content_01.js]
[browser_ruleview_content_02.js] [browser_ruleview_content_02.js]
skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work with e10s skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work with e10s
[browser_ruleview_context-menu-show-mdn-docs-01.js]
[browser_ruleview_context-menu-show-mdn-docs-02.js]
[browser_ruleview_cubicbezier-appears-on-swatch-click.js] [browser_ruleview_cubicbezier-appears-on-swatch-click.js]
[browser_ruleview_cubicbezier-commit-on-ENTER.js] [browser_ruleview_cubicbezier-commit-on-ENTER.js]
[browser_ruleview_cubicbezier-revert-on-ESC.js] [browser_ruleview_cubicbezier-revert-on-ESC.js]

View File

@ -0,0 +1,99 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This file tests the code that integrates the Style Inspector's rule view
* with the MDN docs tooltip.
*
* If you display the context click on a property name in the rule view, you
* should see a menu item "Show MDN Docs". If you click that item, the MDN
* docs tooltip should be shown, containing docs from MDN for that property.
*
* This file tests that the context menu item is shown when it should be
* shown and hidden when it should be hidden.
*/
"use strict";
const {setBaseCssDocsUrl} = devtools.require("devtools/shared/widgets/MdnDocsWidget");
/**
* The test document tries to confuse the context menu
* code by having a tag called "padding" and a property
* value called "margin".
*/
const TEST_DOC =`
<html>
<head>
<style>
padding {font-family: margin;}
</style>
</head>
<body>
<padding>MDN tooltip testing</padding>
</body>
</html>`;
add_task(function* () {
yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
let {inspector, view} = yield openRuleView();
yield selectNode("padding", inspector);
yield testMdnContextMenuItemVisibility(view);
});
/**
* Test that the MDN context menu item is shown when it should be,
* and hidden when it should be.
* - iterate through every node in the rule view
* - set that node as popupNode (the node that the context menu
* is shown for)
* - update the context menu's state
* - test that the MDN context menu item is hidden, or not,
* depending on popupNode
*/
function* testMdnContextMenuItemVisibility(view) {
info("Test that MDN context menu item is shown only when it should be.");
let root = rootElement(view);
for (let node of iterateNodes(root)) {
info("Setting " + node + " as popupNode");
view.doc.popupNode = node;
info("Updating context menu state");
view._contextMenuUpdate();
let isVisible = !view.menuitemShowMdnDocs.hidden;
let shouldBeVisible = isPropertyNameNode(node);
let message = shouldBeVisible? "shown": "hidden";
is(isVisible, shouldBeVisible,
"The MDN context menu item is " + message);
}
}
/**
* Check if a node is a property name.
*/
function isPropertyNameNode(node) {
return ((node.nodeType === node.TEXT_NODE) &&
(node.textContent === "font-family"));
}
/**
* A generator that iterates recursively through all child nodes of baseNode.
*/
function* iterateNodes(baseNode) {
yield baseNode;
for (let child of baseNode.childNodes) {
yield* iterateNodes(child);
}
}
/**
* Returns the root element for the rule view.
*/
let rootElement = view => (view.element) ? view.element : view.styleDocument;

View File

@ -0,0 +1,89 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* This file tests the code that integrates the Style Inspector's rule view
* with the MDN docs tooltip.
*
* If you display the context click on a property name in the rule view, you
* should see a menu item "Show MDN Docs". If you click that item, the MDN
* docs tooltip should be shown, containing docs from MDN for that property.
*
* This file tests that:
* - clicking the context menu item shows the tooltip
* - pressing "Escape" while the tooltip is showing hides the tooltip
*/
"use strict";
const {setBaseCssDocsUrl} = devtools.require("devtools/shared/widgets/MdnDocsWidget");
const PROPERTYNAME = "color";
const TEST_DOC = `
<html>
<body>
<div style="color: red">
Test "Show MDN Docs" context menu option
</div>
</body>
</html>`;
add_task(function* () {
yield addTab("data:text/html;charset=utf8," + encodeURIComponent(TEST_DOC));
let {inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
yield testShowAndHideMdnTooltip(view);
});
function* testShowMdnTooltip(view) {
setBaseCssDocsUrl(TEST_URL_ROOT);
info("Setting the popupNode for the MDN docs tooltip");
let {nameSpan} = getRuleViewProperty(view, "element", PROPERTYNAME);
view.doc.popupNode = nameSpan.firstChild;
view._contextMenuUpdate();
let cssDocs = view.tooltips.cssDocs;
info("Showing the MDN docs tooltip");
let onShown = cssDocs.tooltip.once("shown");
view.menuitemShowMdnDocs.click();
yield onShown;
ok(true, "The MDN docs tooltip was shown");
}
/**
* Test that:
* - the MDN tooltip is shown when we click the context menu item
* - the tooltip's contents have been initialized (we don't fully
* test this here, as it's fully tested with the tooltip test code)
* - the tooltip is hidden when we press Escape
*/
function* testShowAndHideMdnTooltip(view) {
yield testShowMdnTooltip(view);
info("Quick check that the tooltip contents are set");
let cssDocs = view.tooltips.cssDocs;
let tooltipDocument = cssDocs.tooltip.content.contentDocument;
let h1 = tooltipDocument.getElementById("property-name");
is(h1.textContent, PROPERTYNAME, "The MDN docs tooltip h1 is correct");
info("Simulate pressing the 'Escape' key");
let onHidden = cssDocs.tooltip.once("hidden");
EventUtils.sendKey("escape");
yield onHidden;
ok(true, "The MDN docs tooltip was hidden on pressing 'escape'");
}
/**
* Returns the root element for the rule view.
*/
let rootElement = view => (view.element) ? view.element : view.styleDocument;

View File

@ -20,6 +20,7 @@ loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm"); loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm"); loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
loader.lazyGetter(this, "showDoorhanger", () => require("devtools/shared/doorhanger").showDoorhanger); loader.lazyGetter(this, "showDoorhanger", () => require("devtools/shared/doorhanger").showDoorhanger);
loader.lazyRequireGetter(this, "sourceUtils", "devtools/shared/source-utils");
const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
let l10n = new WebConsoleUtils.l10n(STRINGS_URI); let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
@ -432,10 +433,8 @@ WebConsole.prototype = {
* @param integer aSourceLine * @param integer aSourceLine
* The line number which should be highlighted. * The line number which should be highlighted.
*/ */
viewSource: function WC_viewSource(aSourceURL, aSourceLine) viewSource: function WC_viewSource(aSourceURL, aSourceLine) {
{ this.gViewSourceUtils.viewSource(aSourceURL, null, this.iframeWindow.document, aSourceLine || 0);
this.gViewSourceUtils.viewSource(aSourceURL, null,
this.iframeWindow.document, aSourceLine);
}, },
/** /**
@ -443,30 +442,20 @@ WebConsole.prototype = {
* instance in the Style Editor. If the file is not found, it is opened in * instance in the Style Editor. If the file is not found, it is opened in
* source view instead. * source view instead.
* *
* Manually handle the case where toolbox does not exist (Browser Console).
*
* @param string aSourceURL * @param string aSourceURL
* The URL of the file. * The URL of the file.
* @param integer aSourceLine * @param integer aSourceLine
* The line number which you want to place the caret. * The line number which you want to place the caret.
* TODO: This function breaks the client-server boundaries.
* To be fixed in bug 793259.
*/ */
viewSourceInStyleEditor: viewSourceInStyleEditor: function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine) {
function WC_viewSourceInStyleEditor(aSourceURL, aSourceLine)
{
let toolbox = gDevTools.getToolbox(this.target); let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) { if (!toolbox) {
this.viewSource(aSourceURL, aSourceLine); this.viewSource(aSourceURL, aSourceLine);
return; return;
} }
toolbox.viewSourceInStyleEditor(aSourceURL, aSourceLine);
gDevTools.showToolbox(this.target, "styleeditor").then(function(toolbox) {
try {
toolbox.getCurrentPanel().selectStyleSheet(aSourceURL, aSourceLine);
} catch(e) {
// Open view source if style editor fails.
this.viewSource(aSourceURL, aSourceLine);
}
});
}, },
/** /**
@ -474,49 +463,24 @@ WebConsole.prototype = {
* instance in the Script Debugger. If the file is not found, it is opened in * instance in the Script Debugger. If the file is not found, it is opened in
* source view instead. * source view instead.
* *
* Manually handle the case where toolbox does not exist (Browser Console).
*
* @param string aSourceURL * @param string aSourceURL
* The URL of the file. * The URL of the file.
* @param integer aSourceLine * @param integer aSourceLine
* The line number which you want to place the caret. * The line number which you want to place the caret.
*/ */
viewSourceInDebugger: viewSourceInDebugger: function WC_viewSourceInDebugger(aSourceURL, aSourceLine) {
function WC_viewSourceInDebugger(aSourceURL, aSourceLine)
{
let toolbox = gDevTools.getToolbox(this.target); let toolbox = gDevTools.getToolbox(this.target);
if (!toolbox) { if (!toolbox) {
this.viewSource(aSourceURL, aSourceLine); this.viewSource(aSourceURL, aSourceLine);
return; return;
} }
toolbox.viewSourceInDebugger(aSourceURL, aSourceLine).then(() => {
let showSource = ({ DebuggerView }) => { this.ui.emit("source-in-debugger-opened");
let item = DebuggerView.Sources.getItemForAttachment( })
a => a.source.url === aSourceURL
);
if (item) {
DebuggerView.setEditorLocation(item.attachment.source.actor, aSourceLine,
{ noDebug: true }).then(() => {
this.ui.emit("source-in-debugger-opened");
});
return;
}
toolbox.selectTool("webconsole")
.then(() => this.viewSource(aSourceURL, aSourceLine));
}
// If the Debugger was already open, switch to it and try to show the
// source immediately. Otherwise, initialize it and wait for the sources
// to be added first.
let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger");
toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => {
if (debuggerAlreadyOpen) {
showSource(dbg);
} else {
dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg));
}
});
}, },
/** /**
* Tries to open a JavaScript file related to the web page for the web console * Tries to open a JavaScript file related to the web page for the web console
* instance in the corresponding Scratchpad. * instance in the corresponding Scratchpad.
@ -524,33 +488,8 @@ WebConsole.prototype = {
* @param string aSourceURL * @param string aSourceURL
* The URL of the file which corresponds to a Scratchpad id. * The URL of the file which corresponds to a Scratchpad id.
*/ */
viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL) viewSourceInScratchpad: function WC_viewSourceInScratchpad(aSourceURL, aSourceLine) {
{ sourceUtils.viewSourceInScratchpad(aSourceURL, aSourceLine);
// Check for matching top level Scratchpad window.
let wins = Services.wm.getEnumerator("devtools:scratchpad");
while (wins.hasMoreElements()) {
let win = wins.getNext();
if (!win.closed && win.Scratchpad.uniqueName === aSourceURL) {
win.focus();
return;
}
}
// Check for matching Scratchpad toolbox tab.
for (let [, toolbox] of gDevTools) {
let scratchpadPanel = toolbox.getPanel("scratchpad");
if (scratchpadPanel) {
let { scratchpad } = scratchpadPanel;
if (scratchpad.uniqueName === aSourceURL) {
toolbox.selectTool("scratchpad");
toolbox.raise();
scratchpad.editor.focus();
return;
}
}
}
}, },
/** /**

View File

@ -76,6 +76,7 @@ support-files =
test-console-extras.html test-console-extras.html
test-console-replaced-api.html test-console-replaced-api.html
test-console.html test-console.html
test-console-workers.html
test-console-table.html test-console-table.html
test-console-output-02.html test-console-output-02.html
test-console-output-03.html test-console-output-03.html
@ -317,6 +318,7 @@ skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
[browser_webconsole_completion.js] [browser_webconsole_completion.js]
[browser_webconsole_console_extras.js] [browser_webconsole_console_extras.js]
[browser_webconsole_console_logging_api.js] [browser_webconsole_console_logging_api.js]
[browser_webconsole_console_logging_workers_api.js]
[browser_webconsole_count.js] [browser_webconsole_count.js]
[browser_webconsole_dont_navigate_on_doubleclick.js] [browser_webconsole_dont_navigate_on_doubleclick.js]
[browser_webconsole_execution_scope.js] [browser_webconsole_execution_scope.js]

View File

@ -67,8 +67,8 @@ function test()
let viewSource = browserconsole.viewSource; let viewSource = browserconsole.viewSource;
let URL = null; let URL = null;
let clickPromise = promise.defer(); let clickPromise = promise.defer();
browserconsole.viewSource = (aURL) => { browserconsole.viewSourceInDebugger = (aURL) => {
info("browserconsole.viewSource() was invoked: " + aURL); info("browserconsole.viewSourceInDebugger() was invoked: " + aURL);
URL = aURL; URL = aURL;
clickPromise.resolve(null); clickPromise.resolve(null);
}; };
@ -85,6 +85,6 @@ function test()
isnot(URL.indexOf("toolbox.js"), -1, "we have the expected view source URL"); isnot(URL.indexOf("toolbox.js"), -1, "we have the expected view source URL");
is(URL.indexOf("->"), -1, "no -> in the URL given to view-source"); is(URL.indexOf("->"), -1, "no -> in the URL given to view-source");
browserconsole.viewSource = viewSource; browserconsole.viewSourceInDebugger = viewSource;
} }
} }

View File

@ -52,7 +52,7 @@ function test()
{ {
let viewSource = hud.viewSource; let viewSource = hud.viewSource;
let viewSourceCalled = false; let viewSourceCalled = false;
hud.viewSource = () => viewSourceCalled = true; hud.viewSourceInDebugger = () => viewSourceCalled = true;
for (let result of results) { for (let result of results) {
viewSourceCalled = false; viewSourceCalled = false;
@ -67,7 +67,7 @@ function test()
ok(viewSourceCalled, "view source opened"); ok(viewSourceCalled, "view source opened");
} }
hud.viewSource = viewSource; hud.viewSourceInDebugger = viewSource;
finishTest(); finishTest();
} }
} }

View File

@ -22,7 +22,10 @@ const prefs = {
"error", "error",
"warn", "warn",
"info", "info",
"log" "log",
"serviceworkers",
"sharedworkers",
"windowlessworkers"
] ]
}; };

View File

@ -0,0 +1,44 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the basic console.log()-style APIs and filtering work for sharedWorkers
"use strict";
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-console-workers.html";
function pushPrefEnv()
{
let deferred = promise.defer();
let options = {'set': [["dom.workers.sharedWorkers.enabled", true]]};
SpecialPowers.pushPrefEnv(options, deferred.resolve);
return deferred.promise;
}
let test = asyncTest(function*() {
yield pushPrefEnv();
yield loadTab(TEST_URI);
let hud = yield openConsole();
yield waitForMessages({
webconsole: hud,
messages: [{
text: "foo-bar-shared-worker"
}],
});
hud.setFilterState('sharedworkers', false);
is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 1,
"1 message hidden for sharedworkers (logging turned off)")
hud.setFilterState('sharedworkers', true);
is(hud.outputNode.querySelectorAll(".filtered-by-type").length, 0,
"1 message shown for sharedworkers (logging turned on)")
hud.setFilterState('sharedworkers', false);
hud.jsterm.clearOutput(true);
});

View File

@ -0,0 +1,13 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE HTML>
<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
<meta charset="utf-8">
<title>Console test</title>
</head>
<body>
<script type="text/javascript">
var sw = new SharedWorker('data:application/javascript,console.log("foo-bar-shared-worker");');
</script>
</body>
</html>

View File

@ -8,7 +8,7 @@
const {Cc, Ci, Cu} = require("chrome"); const {Cc, Ci, Cu} = require("chrome");
let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils; const {Utils: WebConsoleUtils, CONSOLE_WORKER_IDS} = require("devtools/toolkit/webconsole/utils");
loader.lazyServiceGetter(this, "clipboardHelper", loader.lazyServiceGetter(this, "clipboardHelper",
"@mozilla.org/widget/clipboardhelper;1", "@mozilla.org/widget/clipboardhelper;1",
@ -138,6 +138,10 @@ const LEVELS = {
count: SEVERITY_LOG count: SEVERITY_LOG
}; };
// This array contains the prefKey for the workers and it must keep them in the
// same order as CONSOLE_WORKER_IDS
const WORKERTYPES_PREFKEYS = [ 'sharedworkers', 'serviceworkers', 'windowlessworkers' ];
// The lowest HTTP response code (inclusive) that is considered an error. // The lowest HTTP response code (inclusive) that is considered an error.
const MIN_HTTP_ERROR_CODE = 400; const MIN_HTTP_ERROR_CODE = 400;
// The highest HTTP response code (inclusive) that is considered an error. // The highest HTTP response code (inclusive) that is considered an error.
@ -649,7 +653,8 @@ WebConsoleFrame.prototype = {
{ {
let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog", let prefs = ["network", "networkinfo", "csserror", "cssparser", "csslog",
"exception", "jswarn", "jslog", "error", "info", "warn", "log", "exception", "jswarn", "jslog", "error", "info", "warn", "log",
"secerror", "secwarn", "netwarn", "netxhr"]; "secerror", "secwarn", "netwarn", "netxhr", "sharedworkers",
"serviceworkers", "windowlessworkers"];
for (let pref of prefs) { for (let pref of prefs) {
this.filterPrefs[pref] = Services.prefs this.filterPrefs[pref] = Services.prefs
.getBoolPref(this._filterPrefsPrefix + pref); .getBoolPref(this._filterPrefsPrefix + pref);
@ -1026,8 +1031,11 @@ WebConsoleFrame.prototype = {
// (filter="error", filter="cssparser", etc.) and add or remove the // (filter="error", filter="cssparser", etc.) and add or remove the
// "filtered-by-type" class, which turns on or off the display. // "filtered-by-type" class, which turns on or off the display.
let attribute = WORKERTYPES_PREFKEYS.indexOf(aPrefKey) == -1
? 'filter' : 'workerType';
let xpath = ".//*[contains(@class, 'message') and " + let xpath = ".//*[contains(@class, 'message') and " +
"@filter='" + aPrefKey + "']"; "@" + attribute + "='" + aPrefKey + "']";
let result = doc.evaluate(xpath, outputNode, null, let result = doc.evaluate(xpath, outputNode, null,
Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0; i < result.snapshotLength; i++) { for (let i = 0; i < result.snapshotLength; i++) {
@ -1088,6 +1096,12 @@ WebConsoleFrame.prototype = {
isFiltered = true; isFiltered = true;
} }
// Filter by worker type
if ("workerType" in aNode && !this.getFilterState(aNode.workerType)) {
aNode.classList.add("filtered-by-type");
isFiltered = true;
}
// Filter on the search string. // Filter on the search string.
let search = this.filterBox.value; let search = this.filterBox.value;
let text = aNode.clipboardText; let text = aNode.clipboardText;
@ -1365,6 +1379,12 @@ WebConsoleFrame.prototype = {
} }
} }
let workerTypeID = CONSOLE_WORKER_IDS.indexOf(aMessage.workerType);
if (workerTypeID != -1) {
node.workerType = WORKERTYPES_PREFKEYS[workerTypeID];
node.setAttribute('workerType', WORKERTYPES_PREFKEYS[workerTypeID]);
}
return node; return node;
}, },
@ -2720,7 +2740,7 @@ WebConsoleFrame.prototype = {
let onClick = () => { let onClick = () => {
let target = locationNode.target; let target = locationNode.target;
if (target == "scratchpad" || isScratchpad) { if (target == "scratchpad" || isScratchpad) {
this.owner.viewSourceInScratchpad(url); this.owner.viewSourceInScratchpad(url, line);
return; return;
} }

View File

@ -161,6 +161,13 @@ function goUpdateConsoleCommands() {
prefKey="info"/> prefKey="info"/>
<menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false" <menuitem label="&btnConsoleLog;" type="checkbox" autocheck="false"
prefKey="log"/> prefKey="log"/>
<menuseparator />
<menuitem label="&btnConsoleSharedWorkers;" type="checkbox"
autocheck="false" prefKey="sharedworkers"/>
<menuitem label="&btnConsoleServiceWorkers;" type="checkbox"
autocheck="false" prefKey="serviceworkers"/>
<menuitem label="&btnConsoleWindowlessWorkers;" type="checkbox"
autocheck="false" prefKey="windowlessworkers"/>
</menupopup> </menupopup>
</toolbarbutton> </toolbarbutton>
</hbox> </hbox>

View File

@ -10,9 +10,11 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Task.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
const {require} = devtools; const {require} = devtools;
const promise = require("promise"); const promise = require("promise");
const {AppProjects} = require("devtools/app-manager/app-projects"); const {AppProjects} = require("devtools/app-manager/app-projects");
gDevTools.testing = true;
let TEST_BASE; let TEST_BASE;
if (window.location === "chrome://browser/content/browser.xul") { if (window.location === "chrome://browser/content/browser.xul") {
@ -33,6 +35,7 @@ Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "template
Services.prefs.setCharPref("devtools.devices.url", TEST_BASE + "browser_devices.json"); Services.prefs.setCharPref("devtools.devices.url", TEST_BASE + "browser_devices.json");
SimpleTest.registerCleanupFunction(() => { SimpleTest.registerCleanupFunction(() => {
gDevTools.testing = false;
Services.prefs.clearUserPref("devtools.webide.enabled"); Services.prefs.clearUserPref("devtools.webide.enabled");
Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime"); Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper"); Services.prefs.clearUserPref("devtools.webide.autoinstallADBHelper");

View File

@ -698,7 +698,6 @@
@RESPATH@/browser/@PREF_DIR@/firefox.js @RESPATH@/browser/@PREF_DIR@/firefox.js
@RESPATH@/browser/@PREF_DIR@/firefox-branding.js @RESPATH@/browser/@PREF_DIR@/firefox-branding.js
@RESPATH@/greprefs.js @RESPATH@/greprefs.js
@RESPATH@/defaults/autoconfig/platform.js
@RESPATH@/defaults/autoconfig/prefcalls.js @RESPATH@/defaults/autoconfig/prefcalls.js
@RESPATH@/browser/defaults/profile/prefs.js @RESPATH@/browser/defaults/profile/prefs.js

View File

@ -72,6 +72,8 @@ InstallDirRegKey HKLM "Software\Mozilla\MaintenanceService" ""
SetOverwrite on SetOverwrite on
; serviceinstall.cpp also uses this key, in case the path is changed, update
; there too.
!define MaintUninstallKey \ !define MaintUninstallKey \
"Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService" "Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService"

View File

@ -69,20 +69,22 @@ addonsInstalledNeedsRestart=#1 will be installed after you restart #3.;#2 add-on
addonInstallRestartButton=Restart Now addonInstallRestartButton=Restart Now
addonInstallRestartButton.accesskey=R addonInstallRestartButton.accesskey=R
# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4): # LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5):
# #1 is the add-on name, #2 is the host name, #3 is the application name # #1 is the add-on name, #2 is the host name, #3 is the application name
# #4 is the application version # #4 is the application version
addonError-1=The add-on could not be downloaded because of a connection failure on #2. addonError-1=The add-on could not be downloaded because of a connection failure on #2.
addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected. addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected.
addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt. addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
addonError-4=#1 could not be installed because #3 cannot modify the needed file. addonError-4=#1 could not be installed because #3 cannot modify the needed file.
addonError-5=#3 has prevented this site from installing an unverified add-on.
# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonErrorIncompatible, addonErrorBlocklisted): # LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonLocalError-5, addonErrorIncompatible, addonErrorBlocklisted):
# #1 is the add-on name, #3 is the application name, #4 is the application version # #1 is the add-on name, #3 is the application name, #4 is the application version
addonLocalError-1=This add-on could not be installed because of a filesystem error. addonLocalError-1=This add-on could not be installed because of a filesystem error.
addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected. addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected.
addonLocalError-3=This add-on could not be installed because it appears to be corrupt. addonLocalError-3=This add-on could not be installed because it appears to be corrupt.
addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file. addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file.
addonLocalError-5=This add-on could not be installed because it has not been verified.
addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4. addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4.
addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems. addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems.

View File

@ -55,6 +55,14 @@ previewTooltip.image.brokenImage=Could not load the image
#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded #LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
eventsTooltip.openInDebugger=Open in Debugger eventsTooltip.openInDebugger=Open in Debugger
# LOCALIZATION NOTE (docsTooltip.visitMDN): Shown in the tooltip that displays
# help from MDN. This is a link to the complete MDN documentation page.
docsTooltip.visitMDN=Visit MDN page
# LOCALIZATION NOTE (docsTooltip.visitMDN): Shown in the docs tooltip when the MDN page
# could not be loaded (for example, because of a connectivity problem).
docsTooltip.loadDocsError=Could not load docs page.
# LOCALIZATION NOTE (inspector.collapsePane): This is the tooltip for the button # LOCALIZATION NOTE (inspector.collapsePane): This is the tooltip for the button
# that collapses the right panel (rules, computed, box-model, etc...) in the # that collapses the right panel (rules, computed, box-model, etc...) in the
# inspector UI. # inspector UI.

View File

@ -73,3 +73,4 @@ timeline.markerDetail.startStack=Stack at start:
timeline.markerDetail.endStack=Stack at end: timeline.markerDetail.endStack=Stack at end:
timeline.markerDetail.unknownFrame=<unknown location> timeline.markerDetail.unknownFrame=<unknown location>
timeline.markerDetail.asyncStack=(Async: %S) timeline.markerDetail.asyncStack=(Async: %S)
timeline.markerDetail.causeName=Cause:

View File

@ -76,6 +76,16 @@
<!ENTITY btnConsoleXhr "XHR"> <!ENTITY btnConsoleXhr "XHR">
<!ENTITY btnConsoleReflows "Reflows"> <!ENTITY btnConsoleReflows "Reflows">
<!-- LOCALIZATION NODE (btnConsoleSharedWorkers) the term "Shared Workers"
- should not be translated. -->
<!ENTITY btnConsoleSharedWorkers "Shared Workers">
<!-- LOCALIZATION NODE (btnConsoleServiceWorkers) the term "Service Workers"
- should not be translated. -->
<!ENTITY btnConsoleServiceWorkers "Service Workers">
<!-- LOCALIZATION NODE (btnConsoleWindowlessWorkers) the term "Workers"
- should not be translated. -->
<!ENTITY btnConsoleWindowlessWorkers "Add-on or Chrome Workers">
<!ENTITY filterOutput.placeholder "Filter output"> <!ENTITY filterOutput.placeholder "Filter output">
<!ENTITY btnClear.label "Clear"> <!ENTITY btnClear.label "Clear">
<!ENTITY btnClear.tooltip "Clear the Web Console output"> <!ENTITY btnClear.tooltip "Clear the Web Console output">

View File

@ -20,7 +20,7 @@ def test(mod, path, entity = None):
if mod == "extensions/spellcheck": if mod == "extensions/spellcheck":
return "ignore" return "ignore"
# browser # browser
if (re.match(r"searchplugins\/.+\.xml", path): if (re.match(r"searchplugins\/.+\.xml", path)):
return "ignore" return "ignore"
return "error" return "error"
if mod == "extensions/spellcheck": if mod == "extensions/spellcheck":

View File

@ -13,6 +13,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
@ -24,6 +25,7 @@ let ReaderParent = {
MESSAGES: [ MESSAGES: [
"Reader:AddToList", "Reader:AddToList",
"Reader:AddToPocket",
"Reader:ArticleGet", "Reader:ArticleGet",
"Reader:FaviconRequest", "Reader:FaviconRequest",
"Reader:ListStatusRequest", "Reader:ListStatusRequest",
@ -44,7 +46,7 @@ let ReaderParent = {
receiveMessage: function(message) { receiveMessage: function(message) {
switch (message.name) { switch (message.name) {
case "Reader:AddToList": case "Reader:AddToList": {
let article = message.data.article; let article = message.data.article;
ReadingList.getMetadataFromBrowser(message.target).then(function(metadata) { ReadingList.getMetadataFromBrowser(message.target).then(function(metadata) {
if (metadata.previews.length > 0) { if (metadata.previews.length > 0) {
@ -59,6 +61,25 @@ let ReaderParent = {
}); });
}); });
break; break;
}
case "Reader:AddToPocket": {
let doc = message.target.ownerDocument;
let pocketWidget = doc.getElementById("pocket-button");
let placement = CustomizableUI.getPlacementOfWidget("pocket-button");
if (placement) {
if (placement.area == CustomizableUI.AREA_PANEL) {
doc.defaultView.PanelUI.show().then(function() {
// The DOM node might not exist yet if the panel wasn't opened before.
pocketWidget = doc.getElementById("pocket-button");
pocketWidget.doCommand();
});
} else {
pocketWidget.doCommand();
}
}
break;
}
case "Reader:ArticleGet": case "Reader:ArticleGet":
this._getArticle(message.data.url, message.target).then((article) => { this._getArticle(message.data.url, message.target).then((article) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Some files were not shown because too many files have changed in this diff Show More