Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-02-24 12:21:22 +01:00
commit cf4d06f3b0
157 changed files with 3530 additions and 1426 deletions

View File

@ -9,6 +9,8 @@ var StarUI = {
_itemId: -1,
uri: null,
_batching: false,
_isNewBookmark: false,
_autoCloseTimer: 0,
_element: function(aID) {
return document.getElementById(aID);
@ -21,8 +23,11 @@ var StarUI = {
// initially the panel is hidden
// to avoid impacting startup / new window performance
element.hidden = false;
element.addEventListener("popuphidden", this, false);
element.addEventListener("keypress", this, false);
element.addEventListener("mouseout", this, false);
element.addEventListener("mouseover", this, false);
element.addEventListener("popuphidden", this, false);
element.addEventListener("popupshown", this, false);
return this.panel = element;
},
@ -58,7 +63,11 @@ var StarUI = {
// nsIDOMEventListener
handleEvent(aEvent) {
switch (aEvent.type) {
case "mouseover":
clearTimeout(this._autoCloseTimer);
break;
case "popuphidden":
clearTimeout(this._autoCloseTimer);
if (aEvent.originalTarget == this.panel) {
if (!this._element("editBookmarkPanelContent").hidden)
this.quitEditMode();
@ -72,44 +81,42 @@ var StarUI = {
if (this._batching)
this.endBatch();
switch (this._actionOnHide) {
case "cancel": {
if (!PlacesUIUtils.useAsyncTransactions) {
if (this._uriForRemoval) {
if (this._isNewBookmark) {
if (!PlacesUtils.useAsyncTransactions) {
PlacesUtils.transactionManager.undoTransaction();
break;
}
PlacesTransactions.undo().catch(Cu.reportError);
PlacesTransactions().undo().catch(Cu.reportError);
break;
}
case "remove": {
// Remove all bookmarks for the bookmark's url, this also removes
// the tags for the url.
if (!PlacesUIUtils.useAsyncTransactions) {
let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
for (let itemId of itemIds) {
let txn = new PlacesRemoveItemTransaction(itemId);
PlacesUtils.transactionManager.doTransaction(txn);
}
break;
// Remove all bookmarks for the bookmark's url, this also removes
// the tags for the url.
if (!PlacesUIUtils.useAsyncTransactions) {
let itemIds = PlacesUtils.getBookmarksForURI(this._uriForRemoval);
for (let itemId of itemIds) {
let txn = new PlacesRemoveItemTransaction(itemId);
PlacesUtils.transactionManager.doTransaction(txn);
}
PlacesTransactions.RemoveBookmarksForUrls(this._uriForRemoval)
.transact().catch(Cu.reportError);
break;
}
PlacesTransactions.RemoveBookmarksForUrls([this._uriForRemoval])
.transact().catch(Cu.reportError);
}
this._actionOnHide = "";
}
break;
case "keypress":
clearTimeout(this._autoCloseTimer);
if (aEvent.defaultPrevented) {
// The event has already been consumed inside of the panel.
break;
}
switch (aEvent.keyCode) {
case KeyEvent.DOM_VK_ESCAPE:
if (!this._element("editBookmarkPanelContent").hidden)
this.cancelButtonOnCommand();
this.panel.hidePopup();
break;
case KeyEvent.DOM_VK_RETURN:
if (aEvent.target.classList.contains("expander-up") ||
@ -123,12 +130,40 @@ var StarUI = {
break;
}
break;
case "mouseout": {
// Don't handle events for descendent elements.
if (aEvent.target != aEvent.currentTarget) {
break;
}
// Explicit fall-through
}
case "popupshown":
// auto-close if new and not interacted with
if (this._isNewBookmark) {
// 3500ms matches the timeout that Pocket uses in
// browser/extensions/pocket/content/panels/js/saved.js
let delay = 3500;
if (this._closePanelQuickForTesting) {
delay /= 10;
}
this._autoCloseTimer = setTimeout(() => this.panel.hidePopup(), delay, this);
}
break;
}
},
_overlayLoaded: false,
_overlayLoading: false,
showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition) {
showEditBookmarkPopup: Task.async(function* (aNode, aAnchorElement, aPosition, aIsNewBookmark) {
// Slow double-clicks (not true double-clicks) shouldn't
// cause the panel to flicker.
if (this.panel.state == "showing" ||
this.panel.state == "open") {
return;
}
this._isNewBookmark = aIsNewBookmark;
this._uriForRemoval = "";
// TODO: Deprecate this once async transactions are enabled and the legacy
// transactions code is gone (bug 1131491) - we don't want addons to to use
// the completeNodeLikeObjectForItemId, so it's better if they keep passing
@ -177,26 +212,18 @@ var StarUI = {
if (this.panel.state != "closed")
return;
this._blockCommands(); // un-done in the popuphiding handler
this._blockCommands(); // un-done in the popuphidden handler
// Set panel title:
// if we are batching, i.e. the bookmark has been added now,
// then show Page Bookmarked, else if the bookmark did already exist,
// we are about editing it, then use Edit This Bookmark.
this._element("editBookmarkPanelTitle").value =
this._batching ?
this._isNewBookmark ?
gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
// No description; show the Done, Cancel;
// No description; show the Done, Remove;
this._element("editBookmarkPanelDescription").textContent = "";
this._element("editBookmarkPanelBottomButtons").hidden = false;
this._element("editBookmarkPanelContent").hidden = false;
// The remove button is shown only if we're not already batching, i.e.
// if the cancel button/ESC does not remove the bookmark.
this._element("editBookmarkPanelRemoveButton").hidden = this._batching;
// The label of the remove button differs if the URI is bookmarked
// multiple times.
let bookmarks = PlacesUtils.getBookmarksForURI(gBrowser.currentURI);
@ -250,14 +277,8 @@ var StarUI = {
gEditItemOverlay.uninitPanel(true);
},
cancelButtonOnCommand: function SU_cancelButtonOnCommand() {
this._actionOnHide = "cancel";
this.panel.hidePopup(true);
},
removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
this._uriForRemoval = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
this._actionOnHide = "remove";
this.panel.hidePopup();
},
@ -325,7 +346,8 @@ var PlacesCommandHook = {
var uri = aBrowser.currentURI;
var itemId = PlacesUtils.getMostRecentBookmarkForURI(uri);
if (itemId == -1) {
let isNewBookmark = itemId == -1;
if (isNewBookmark) {
// Bug 1148838 - Make this code work for full page plugins.
var title;
var description;
@ -342,10 +364,10 @@ var PlacesCommandHook = {
}
catch (e) { }
if (aShowEditUI) {
// If we bookmark the page here (i.e. page was not "starred" already)
// but open right into the "edit" state, start batching here, so
// "Cancel" in that state removes the bookmark.
if (aShowEditUI && isNewBookmark) {
// If we bookmark the page here but open right into a cancelable
// state (i.e. new bookmark in Library), start batching here so
// all of the actions can be undone in a single undo step.
StarUI.beginBatch();
}
@ -376,16 +398,16 @@ var PlacesCommandHook = {
// 3. the content area
if (BookmarkingUI.anchor) {
StarUI.showEditBookmarkPopup(itemId, BookmarkingUI.anchor,
"bottomcenter topright");
"bottomcenter topright", isNewBookmark);
return;
}
let identityIcon = document.getElementById("identity-icon");
if (isElementVisible(identityIcon)) {
StarUI.showEditBookmarkPopup(itemId, identityIcon,
"bottomcenter topright");
"bottomcenter topright", isNewBookmark);
} else {
StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap");
StarUI.showEditBookmarkPopup(itemId, aBrowser, "overlap", isNewBookmark);
}
}),
@ -394,6 +416,7 @@ var PlacesCommandHook = {
_bookmarkPagePT: Task.async(function* (aBrowser, aParentId, aShowEditUI) {
let url = new URL(aBrowser.currentURI.spec);
let info = yield PlacesUtils.bookmarks.fetch({ url });
let isNewBookmark = !info;
if (!info) {
let parentGuid = aParentId !== undefined ?
yield PlacesUtils.promiseItemGuid(aParentId) :
@ -417,10 +440,10 @@ var PlacesCommandHook = {
Components.utils.reportError(e);
}
if (aShowEditUI) {
// If we bookmark the page here (i.e. page was not "starred" already)
// but open right into the "edit" state, start batching here, so
// "Cancel" in that state removes the bookmark.
if (aShowEditUI && isNewBookmark) {
// If we bookmark the page here but open right into a cancelable
// state (i.e. new bookmark in Library), start batching here so
// all of the actions can be undone in a single undo step.
StarUI.beginBatch();
}
@ -452,16 +475,16 @@ var PlacesCommandHook = {
// 3. the content area
if (BookmarkingUI.anchor) {
StarUI.showEditBookmarkPopup(node, BookmarkingUI.anchor,
"bottomcenter topright");
"bottomcenter topright", isNewBookmark);
return;
}
let identityIcon = document.getElementById("identity-icon");
if (isElementVisible(identityIcon)) {
StarUI.showEditBookmarkPopup(node, identityIcon,
"bottomcenter topright");
"bottomcenter topright", isNewBookmark);
} else {
StarUI.showEditBookmarkPopup(node, aBrowser, "overlap");
StarUI.showEditBookmarkPopup(node, aBrowser, "overlap", isNewBookmark);
}
}),
@ -1703,19 +1726,15 @@ var BookmarkingUI = {
let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID)
.forWindow(window);
if (widget.overflowed) {
// Allow to close the panel if the page is already bookmarked, cause
// we are going to open the edit bookmark panel.
if (isBookmarked)
widget.node.removeAttribute("closemenu");
else
widget.node.setAttribute("closemenu", "none");
// Close the overflow panel because the Edit Bookmark panel will appear.
widget.node.removeAttribute("closemenu");
}
// Ignore clicks on the star if we are updating its state.
if (!this._pendingStmt) {
if (!isBookmarked)
this._showBookmarkedNotification();
PlacesCommandHook.bookmarkCurrentPage(isBookmarked);
PlacesCommandHook.bookmarkCurrentPage(true);
}
},

View File

@ -2345,7 +2345,12 @@ function URLBarSetURI(aURI) {
checkEmptyPageOrigin(gBrowser.selectedBrowser, uri)) {
value = "";
} else {
value = losslessDecodeURI(uri);
// We should deal with losslessDecodeURI throwing for exotic URIs
try {
value = losslessDecodeURI(uri);
} catch (ex) {
value = "about:blank";
}
}
valid = !isBlankPageURL(uri.spec);

View File

@ -180,12 +180,6 @@
<vbox>
<label id="editBookmarkPanelTitle"/>
<description id="editBookmarkPanelDescription"/>
<hbox>
<button id="editBookmarkPanelRemoveButton"
class="editBookmarkPanelHeaderButton"
oncommand="StarUI.removeBookmarkButtonCommand();"
accesskey="&editBookmark.removeBookmark.accessKey;"/>
</hbox>
</vbox>
</row>
<vbox id="editBookmarkPanelContent" flex="1" hidden="true"/>
@ -196,15 +190,15 @@
label="&editBookmark.done.label;"
default="true"
oncommand="StarUI.panel.hidePopup();"/>
<button id="editBookmarkPanelDeleteButton"
<button id="editBookmarkPanelRemoveButton"
class="editBookmarkPanelBottomButton"
label="&editBookmark.cancel.label;"
oncommand="StarUI.cancelButtonOnCommand();"/>
oncommand="StarUI.removeBookmarkButtonCommand();"
accesskey="&editBookmark.removeBookmark.accessKey;"/>
#else
<button id="editBookmarkPanelDeleteButton"
<button id="editBookmarkPanelRemoveButton"
class="editBookmarkPanelBottomButton"
label="&editBookmark.cancel.label;"
oncommand="StarUI.cancelButtonOnCommand();"/>
oncommand="StarUI.removeBookmarkButtonCommand();"
accesskey="&editBookmark.removeBookmark.accessKey;"/>
<button id="editBookmarkPanelDoneButton"
class="editBookmarkPanelBottomButton"
label="&editBookmark.done.label;"

View File

@ -1026,11 +1026,16 @@
if (!aForceUpdate) {
TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
if (!Services.appinfo.browserTabsRemoteAutostart) {
// old way of measuring tab paint which is not
// valid with e10s.
window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
.beginTabSwitch();
if (!gMultiProcessBrowser) {
// old way of measuring tab paint which is not valid with e10s.
// Waiting until the next MozAfterPaint ensures that we capture
// the time it takes to paint, upload the textures to the compositor,
// and then composite.
TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
window.addEventListener("MozAfterPaint", function onMozAfterPaint() {
TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
window.removeEventListener("MozAfterPaint", onMozAfterPaint);
});
}
}
@ -5891,7 +5896,7 @@
bgLoad = !bgLoad;
let tab = this._getDragTargetTab(event, true);
if (!tab || dropEffect == "copy") {
if (!tab) {
// We're adding a new tab.
let newIndex = this._getDropIndex(event, true);
let newTab = this.tabbrowser.loadOneTab(url, {inBackground: bgLoad, allowThirdPartyFixup: true});

View File

@ -8,5 +8,6 @@ support-files =
[browser_notification_open_settings.js]
[browser_notification_remove_permission.js]
[browser_notification_permission_migration.js]
[browser_notification_replace.js]
[browser_notification_tab_switching.js]
skip-if = buildapp == 'mulet'

View File

@ -28,7 +28,7 @@ add_task(function* test_notificationClose() {
let alertTitleLabel = alertWindow.document.getElementById("alertTitleLabel");
is(alertTitleLabel.value, "Test title", "Title text of notification should be present");
let alertTextLabel = alertWindow.document.getElementById("alertTextLabel");
is(alertTextLabel.textContent, "Test body", "Body text of notification should be present");
is(alertTextLabel.textContent, "Test body 2", "Body text of notification should be present");
let alertCloseButton = alertWindow.document.querySelector(".alertCloseButton");
is(alertCloseButton.localName, "toolbarbutton", "close button found");

View File

@ -0,0 +1,38 @@
"use strict";
let notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
add_task(function* test_notificationReplace() {
let pm = Services.perms;
pm.add(makeURI(notificationURL), "desktop-notification", pm.ALLOW_ACTION);
yield BrowserTestUtils.withNewTab({
gBrowser,
url: notificationURL
}, function* dummyTabTask(aBrowser) {
yield ContentTask.spawn(aBrowser, {}, function* () {
let win = content.window.wrappedJSObject;
let notification = win.showNotification1();
let promiseCloseEvent = ContentTaskUtils.waitForEvent(notification, "close");
let showEvent = yield ContentTaskUtils.waitForEvent(notification, "show");
is(showEvent.target.body, "Test body 1", "Showed tagged notification");
let newNotification = win.showNotification2();
let newShowEvent = yield ContentTaskUtils.waitForEvent(newNotification, "show");
is(newShowEvent.target.body, "Test body 2", "Showed new notification with same tag");
let closeEvent = yield promiseCloseEvent;
is(closeEvent.target.body, "Test body 1", "Closed previous tagged notification");
let promiseNewCloseEvent = ContentTaskUtils.waitForEvent(newNotification, "close");
newNotification.close();
let newCloseEvent = yield promiseNewCloseEvent;
is(newCloseEvent.target.body, "Test body 2", "Closed new notification");
});
});
});
add_task(function* cleanup() {
Services.perms.remove(makeURI(notificationURL), "desktop-notification");
});

View File

@ -8,7 +8,7 @@ function showNotification1() {
var options = {
dir: undefined,
lang: undefined,
body: "Test body",
body: "Test body 1",
tag: "Test tag",
icon: undefined,
};
@ -23,7 +23,7 @@ function showNotification2() {
var options = {
dir: undefined,
lang: undefined,
body: "Test body",
body: "Test body 2",
tag: "Test tag",
icon: undefined,
};

View File

@ -154,6 +154,7 @@ skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir
skip-if = os == "mac" # The Fitt's Law back button is not supported on OS X
[browser_beforeunload_duplicate_dialogs.js]
[browser_blob-channelname.js]
[browser_bookmark_popup.js]
[browser_bookmark_titles.js]
skip-if = buildapp == 'mulet' || toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341)
[browser_bug304198.js]

View File

@ -0,0 +1,271 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* Test opening and closing the bookmarks panel.
*/
let bookmarkPanel = document.getElementById("editBookmarkPanel");
let bookmarkStar = document.getElementById("bookmarks-menu-button");
let bookmarkPanelTitle = document.getElementById("editBookmarkPanelTitle");
StarUI._closePanelQuickForTesting = true;
Services.prefs.setBoolPref("browser.bookmarks.closePanelQuickForTesting", true);
function* test_bookmarks_popup({isNewBookmark, popupShowFn, popupEditFn,
shouldAutoClose, popupHideFn, isBookmarkRemoved}) {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
try {
if (!isNewBookmark) {
yield PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "about:home",
title: "Home Page"
});
}
is(bookmarkStar.hasAttribute("starred"), !isNewBookmark,
"Page should only be starred prior to popupshown if editing bookmark");
is(bookmarkPanel.state, "closed", "Panel should be 'closed' to start test");
let shownPromise = promisePopupShown(bookmarkPanel);
yield popupShowFn(tab.linkedBrowser);
yield shownPromise;
is(bookmarkPanel.state, "open", "Panel should be 'open' after shownPromise is resolved");
if (popupEditFn) {
yield popupEditFn();
}
let bookmarks = [];
yield PlacesUtils.bookmarks.fetch({url: "about:home"}, bm => bookmarks.push(bm));
is(bookmarks.length, 1, "Only one bookmark should exist");
is(bookmarkStar.getAttribute("starred"), "true", "Page is starred");
is(bookmarkPanel.state, "open", "Check that panel state is 'open'");
is(bookmarkPanelTitle.value,
isNewBookmark ?
gNavigatorBundle.getString("editBookmarkPanel.pageBookmarkedTitle") :
gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"),
"title should match isEditingBookmark state");
if (!shouldAutoClose) {
yield new Promise(resolve => setTimeout(resolve, 400));
}
let hiddenPromise = promisePopupHidden(bookmarkPanel);
if (popupHideFn) {
yield popupHideFn();
}
yield hiddenPromise;
is(bookmarkStar.hasAttribute("starred"), !isBookmarkRemoved,
"Page is starred after closing");
} finally {
let bookmark = yield PlacesUtils.bookmarks.fetch({url: "about:home"});
is(!!bookmark, !isBookmarkRemoved,
"bookmark should not be present if a panel action should've removed it");
if (bookmark) {
yield PlacesUtils.bookmarks.remove(bookmark);
}
gBrowser.removeTab(tab);
}
}
add_task(function* panel_shown_for_new_bookmarks_and_autocloses() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn() {
bookmarkStar.click();
},
shouldAutoClose: true,
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_once_for_doubleclick_on_new_bookmark_star_and_autocloses() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn() {
EventUtils.synthesizeMouse(bookmarkStar, 10, 10, { clickCount: 2 },
window);
},
shouldAutoClose: true,
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_once_for_slow_doubleclick_on_new_bookmark_star_and_autocloses() {
todo(false, "bug 1250267, may need to add some tracking state to " +
"browser-places.js for this.");
return;
yield test_bookmarks_popup({
isNewBookmark: true,
*popupShowFn() {
EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
yield new Promise(resolve => setTimeout(resolve, 300));
EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
},
shouldAutoClose: true,
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_keyboardshortcut_on_new_bookmark_star_and_autocloses() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn() {
EventUtils.synthesizeKey("D", {accelKey: true}, window);
},
shouldAutoClose: true,
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_new_bookmarks_mouseover_mouseout() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn() {
bookmarkStar.click();
},
*popupEditFn() {
let mouseOverPromise = new Promise(resolve => {
bookmarkPanel.addEventListener("mouseover", function onmouseover() {
bookmarkPanel.removeEventListener("mouseover", onmouseover);
resolve();
});
});
yield new Promise(resolve => {
EventUtils.synthesizeNativeMouseMove(bookmarkPanel, 0, 0, resolve, window);
});
info("Waiting for mouseover event");
yield mouseOverPromise;
info("Got mouseover event");
yield new Promise(resolve => setTimeout(resolve, 400));
is(bookmarkPanel.state, "open", "Panel should still be open on mouseover");
let mouseOutPromise = new Promise(resolve => {
bookmarkPanel.addEventListener("mouseout", function onmouseout() {
bookmarkPanel.removeEventListener("mouseout", onmouseout);
resolve();
});
});
yield new Promise(resolve => {
EventUtils.synthesizeNativeMouseMove(bookmarkStar, 0, 0, resolve, window);
});
info("Waiting for mouseout event");
yield mouseOutPromise;
info("Got mouseout event, should autoclose now");
},
shouldAutoClose: true,
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_new_bookmark_no_autoclose_close_with_ESC() {
yield test_bookmarks_popup({
isNewBookmark: false,
popupShowFn() {
bookmarkStar.click();
},
shouldAutoClose: false,
popupHideFn() {
EventUtils.synthesizeKey("VK_ESCAPE", {accelKey: true}, window);
},
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_editing_no_autoclose_close_with_ESC() {
yield test_bookmarks_popup({
isNewBookmark: false,
popupShowFn() {
bookmarkStar.click();
},
shouldAutoClose: false,
popupHideFn() {
EventUtils.synthesizeKey("VK_ESCAPE", {accelKey: true}, window);
},
isBookmarkRemoved: false,
});
});
add_task(function* panel_shown_for_new_bookmark_keypress_no_autoclose() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn() {
bookmarkStar.click();
},
popupEditFn() {
EventUtils.sendChar("VK_TAB", window);
},
shouldAutoClose: false,
popupHideFn() {
bookmarkPanel.hidePopup();
},
isBookmarkRemoved: false,
});
});
add_task(function* contextmenu_new_bookmark_click_no_autoclose() {
yield test_bookmarks_popup({
isNewBookmark: true,
*popupShowFn(browser) {
let contextMenu = document.getElementById("contentAreaContextMenu");
let awaitPopupShown = BrowserTestUtils.waitForEvent(contextMenu,
"popupshown");
let awaitPopupHidden = BrowserTestUtils.waitForEvent(contextMenu,
"popuphidden");
yield BrowserTestUtils.synthesizeMouseAtCenter("body", {
type: "contextmenu",
button: 2
}, browser);
yield awaitPopupShown;
document.getElementById("context-bookmarkpage").click();
contextMenu.hidePopup();
yield awaitPopupHidden;
},
popupEditFn() {
bookmarkPanelTitle.click();
},
shouldAutoClose: false,
popupHideFn() {
bookmarkPanel.hidePopup();
},
isBookmarkRemoved: false,
});
});
add_task(function* bookmarks_menu_new_bookmark_remove_bookmark() {
yield test_bookmarks_popup({
isNewBookmark: true,
popupShowFn(browser) {
document.getElementById("menu_bookmarkThisPage").doCommand();
},
shouldAutoClose: true,
popupHideFn() {
document.getElementById("editBookmarkPanelRemoveButton").click();
},
isBookmarkRemoved: true,
});
});
add_task(function* ctrl_d_edit_bookmark_remove_bookmark() {
yield test_bookmarks_popup({
isNewBookmark: false,
popupShowFn(browser) {
EventUtils.synthesizeKey("D", {accelKey: true}, window);
},
shouldAutoClose: true,
popupHideFn() {
document.getElementById("editBookmarkPanelRemoveButton").click();
},
isBookmarkRemoved: true,
});
});
registerCleanupFunction(function() {
Services.prefs.clearUserPref("browser.bookmarks.closePanelQuickForTesting");
delete StarUI._closePanelQuickForTesting;
})

View File

@ -60,6 +60,9 @@ function test() {
testVal("<user:pass@sub1.sub2.sub3.>mozilla.org");
testVal("<user:pass@>mozilla.org");
testVal("<https://>mozilla.org< >");
testVal("mozilla.org< >");
testVal("<https://>mozilla.org</file.ext>");
testVal("<https://>mozilla.org</sub/file.ext>");
testVal("<https://>mozilla.org</sub/file.ext?foo>");

View File

@ -1,3 +1,4 @@
[browser_moz_action_link.js]
[browser_urlbar_blanking.js]
support-files =
file_blank_but_not_blank.html

View File

@ -0,0 +1,31 @@
"use strict";
const kURIs = [
"moz-action:foo,",
"moz-action:foo",
];
add_task(function*() {
for (let uri of kURIs) {
let dataURI = `data:text/html,<a id=a href="${uri}" target=_blank>Link</a>`;
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, dataURI);
let tabSwitchPromise = BrowserTestUtils.switchTab(gBrowser, function() {});
yield ContentTask.spawn(tab.linkedBrowser, null, function*() {
content.document.getElementById("a").click();
});
yield tabSwitchPromise;
isnot(gBrowser.selectedTab, tab, "Switched to new tab!");
is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank");
let newTab = gBrowser.selectedTab;
yield BrowserTestUtils.switchTab(gBrowser, tab);
yield BrowserTestUtils.switchTab(gBrowser, newTab);
is(gBrowser.selectedTab, newTab, "Switched to new tab again!");
is(gURLBar.value, "about:blank", "URL bar should be displaying about:blank after tab switch");
// Finally, check that directly setting it produces the right results, too:
URLBarSetURI(makeURI(uri));
is(gURLBar.value, "about:blank", "URL bar should still be displaying about:blank");
yield BrowserTestUtils.removeTab(newTab);
yield BrowserTestUtils.removeTab(tab);
}
});

View File

@ -261,7 +261,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
trimmedLength = "http://".length;
}
let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(.+?)(?::\d+)?(?:[\/#?]|$)/);
let matchedURL = value.match(/^((?:[a-z]+:\/\/)(?:[^\/#?]+@)?)(\S+?)(?::\d+)?\s*(?:[\/#?]|$)/);
if (!matchedURL)
return;
@ -839,12 +839,13 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<method name="_parseActionUrl">
<parameter name="aUrl"/>
<body><![CDATA[
if (!aUrl.startsWith("moz-action:"))
const MOZ_ACTION_REGEX = /^moz-action:([^,]+),(.*)$/;
if (!MOZ_ACTION_REGEX.test(aUrl))
return null;
// URL is in the format moz-action:ACTION,PARAMS
// Where PARAMS is a JSON encoded object.
let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
let [, type, params] = aUrl.match(MOZ_ACTION_REGEX);
let action = {
type: type,

View File

@ -33,7 +33,6 @@ skip-if = os == "mac"
skip-if = os == "linux"
[browser_901207_searchbar_in_panel.js]
skip-if = e10s # bug 1090656
[browser_913972_currentset_overflow.js]
skip-if = os == "linux"
@ -69,9 +68,9 @@ skip-if = os == "linux"
[browser_947914_button_addons.js]
skip-if = os == "linux" # Intermittent failures
[browser_947914_button_copy.js]
skip-if = os == "linux" || e10s # Intermittent failures on Linux, e10s issues are bug 1091561
skip-if = os == "linux" # Intermittent failures on Linux
[browser_947914_button_cut.js]
skip-if = os == "linux" || e10s # Intermittent failures on Linux, e10s issues are bug 1091561
skip-if = os == "linux" # Intermittent failures on Linux
[browser_947914_button_find.js]
skip-if = os == "linux" # Intermittent failures
[browser_947914_button_history.js]
@ -81,9 +80,9 @@ skip-if = os == "linux" # Intermittent failures
[browser_947914_button_newWindow.js]
skip-if = os == "linux" # Intermittent failures
[browser_947914_button_paste.js]
skip-if = os == "linux" || e10s # Intermittent failures on Linux, e10s issues are bug 1091561
skip-if = os == "linux" # Intermittent failures on Linux
[browser_947914_button_print.js]
skip-if = os == "linux" || (os == "win" && e10s) # Intermittent failures on Linux, e10s issues on Windows (bug 1088714)
skip-if = os == "linux" # Intermittent failures on Linux
[browser_947914_button_savePage.js]
skip-if = os == "linux" # Intermittent failures
[browser_947914_button_zoomIn.js]
@ -123,7 +122,6 @@ skip-if = os == "linux"
[browser_984455_bookmarks_items_reparenting.js]
skip-if = os == "linux"
[browser_985815_propagate_setToolbarVisibility.js]
skip-if = e10s # bug 1090635
[browser_987177_destroyWidget_xul.js]
[browser_987177_xul_wrapper_updating.js]
[browser_987185_syncButton.js]
@ -133,7 +131,6 @@ skip-if = e10s # Bug 1088710
[browser_988072_sidebar_events.js]
[browser_989338_saved_placements_not_resaved.js]
[browser_989751_subviewbutton_class.js]
skip-if = os == "linux" && e10s # Bug 1102900, bug 1104745, bug 1104761
[browser_992747_toggle_noncustomizable_toolbar.js]
[browser_993322_widget_notoolbar.js]
[browser_995164_registerArea_during_customize_mode.js]

View File

@ -8,57 +8,52 @@ var initialLocation = gBrowser.currentURI.spec;
var globalClipboard;
add_task(function*() {
info("Check copy button existence and functionality");
yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() {
info("Check copy button existence and functionality");
let testText = "copy text test";
let testText = "copy text test";
gURLBar.focus();
info("The URL bar was focused");
yield PanelUI.show();
info("Menu panel was opened");
gURLBar.focus();
info("The URL bar was focused");
yield PanelUI.show();
info("Menu panel was opened");
let copyButton = document.getElementById("copy-button");
ok(copyButton, "Copy button exists in Panel Menu");
ok(copyButton.getAttribute("disabled"), "Copy button is initially disabled");
let copyButton = document.getElementById("copy-button");
ok(copyButton, "Copy button exists in Panel Menu");
ok(copyButton.getAttribute("disabled"), "Copy button is initially disabled");
// copy text from URL bar
gURLBar.value = testText;
gURLBar.focus();
gURLBar.select();
yield PanelUI.show();
info("Menu panel was opened");
// copy text from URL bar
gURLBar.value = testText;
gURLBar.focus();
gURLBar.select();
yield PanelUI.show();
info("Menu panel was opened");
ok(!copyButton.hasAttribute("disabled"), "Copy button is enabled when selecting");
ok(!copyButton.hasAttribute("disabled"), "Copy button is enabled when selecting");
copyButton.click();
is(gURLBar.value, testText, "Selected text is unaltered when clicking copy");
copyButton.click();
is(gURLBar.value, testText, "Selected text is unaltered when clicking copy");
// check that the text was added to the clipboard
let clipboard = Services.clipboard;
let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
globalClipboard = clipboard.kGlobalClipboard;
// check that the text was added to the clipboard
let clipboard = Services.clipboard;
let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
globalClipboard = clipboard.kGlobalClipboard;
transferable.init(null);
transferable.addDataFlavor("text/unicode");
clipboard.getData(transferable, globalClipboard);
let str = {}, strLength = {};
transferable.getTransferData("text/unicode", str, strLength);
let clipboardValue = "";
transferable.init(null);
transferable.addDataFlavor("text/unicode");
clipboard.getData(transferable, globalClipboard);
let str = {}, strLength = {};
transferable.getTransferData("text/unicode", str, strLength);
let clipboardValue = "";
if (str.value) {
str.value.QueryInterface(Ci.nsISupportsString);
clipboardValue = str.value.data;
}
is(clipboardValue, testText, "Data was copied to the clipboard.");
if (str.value) {
str.value.QueryInterface(Ci.nsISupportsString);
clipboardValue = str.value.data;
}
is(clipboardValue, testText, "Data was copied to the clipboard.");
});
});
add_task(function* asyncCleanup() {
// clear the clipboard
registerCleanupFunction(function cleanup() {
Services.clipboard.emptyClipboard(globalClipboard);
info("Clipboard was cleared");
// restore the tab as it was at the begining of the test
gBrowser.addTab(initialLocation);
gBrowser.removeTab(gBrowser.selectedTab);
info("Tabs were restored");
});

View File

@ -8,55 +8,50 @@ var initialLocation = gBrowser.currentURI.spec;
var globalClipboard;
add_task(function*() {
info("Check cut button existence and functionality");
yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() {
info("Check cut button existence and functionality");
let testText = "cut text test";
let testText = "cut text test";
gURLBar.focus();
yield PanelUI.show();
info("Menu panel was opened");
gURLBar.focus();
yield PanelUI.show();
info("Menu panel was opened");
let cutButton = document.getElementById("cut-button");
ok(cutButton, "Cut button exists in Panel Menu");
ok(cutButton.hasAttribute("disabled"), "Cut button is disabled");
let cutButton = document.getElementById("cut-button");
ok(cutButton, "Cut button exists in Panel Menu");
ok(cutButton.hasAttribute("disabled"), "Cut button is disabled");
// cut text from URL bar
gURLBar.value = testText;
gURLBar.focus();
gURLBar.select();
yield PanelUI.show();
info("Menu panel was opened");
// cut text from URL bar
gURLBar.value = testText;
gURLBar.focus();
gURLBar.select();
yield PanelUI.show();
info("Menu panel was opened");
ok(!cutButton.hasAttribute("disabled"), "Cut button is enabled when selecting");
cutButton.click();
is(gURLBar.value, "", "Selected text is removed from source when clicking on cut");
ok(!cutButton.hasAttribute("disabled"), "Cut button is enabled when selecting");
cutButton.click();
is(gURLBar.value, "", "Selected text is removed from source when clicking on cut");
// check that the text was added to the clipboard
let clipboard = Services.clipboard;
let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
globalClipboard = clipboard.kGlobalClipboard;
// check that the text was added to the clipboard
let clipboard = Services.clipboard;
let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable);
globalClipboard = clipboard.kGlobalClipboard;
transferable.init(null);
transferable.addDataFlavor("text/unicode");
clipboard.getData(transferable, globalClipboard);
let str = {}, strLength = {};
transferable.getTransferData("text/unicode", str, strLength);
let clipboardValue = "";
transferable.init(null);
transferable.addDataFlavor("text/unicode");
clipboard.getData(transferable, globalClipboard);
let str = {}, strLength = {};
transferable.getTransferData("text/unicode", str, strLength);
let clipboardValue = "";
if (str.value) {
str.value.QueryInterface(Ci.nsISupportsString);
clipboardValue = str.value.data;
}
is(clipboardValue, testText, "Data was copied to the clipboard.");
if (str.value) {
str.value.QueryInterface(Ci.nsISupportsString);
clipboardValue = str.value.data;
}
is(clipboardValue, testText, "Data was copied to the clipboard.");
});
});
add_task(function* asyncCleanup() {
// clear the clipboard
registerCleanupFunction(function cleanup() {
Services.clipboard.emptyClipboard(globalClipboard);
info("Clipboard was cleared");
// restore the tab as it was at the begining of the test
gBrowser.addTab(initialLocation);
gBrowser.removeTab(gBrowser.selectedTab);
info("Tabs were restored");
});

View File

@ -8,40 +8,34 @@ var initialLocation = gBrowser.currentURI.spec;
var globalClipboard;
add_task(function*() {
info("Check paste button existence and functionality");
yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() {
info("Check paste button existence and functionality");
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
globalClipboard = Services.clipboard.kGlobalClipboard;
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
globalClipboard = Services.clipboard.kGlobalClipboard;
yield PanelUI.show();
info("Menu panel was opened");
yield PanelUI.show();
info("Menu panel was opened");
let pasteButton = document.getElementById("paste-button");
ok(pasteButton, "Paste button exists in Panel Menu");
let pasteButton = document.getElementById("paste-button");
ok(pasteButton, "Paste button exists in Panel Menu");
// add text to clipboard
let text = "Sample text for testing";
clipboard.copyString(text);
// add text to clipboard
let text = "Sample text for testing";
clipboard.copyString(text);
// test paste button by pasting text to URL bar
gURLBar.focus();
yield PanelUI.show();
info("Menu panel was opened");
// test paste button by pasting text to URL bar
gURLBar.focus();
yield PanelUI.show();
info("Menu panel was opened");
ok(!pasteButton.hasAttribute("disabled"), "Paste button is enabled");
pasteButton.click();
ok(!pasteButton.hasAttribute("disabled"), "Paste button is enabled");
pasteButton.click();
is(gURLBar.value, text, "Text pasted successfully");
is(gURLBar.value, text, "Text pasted successfully");
});
});
add_task(function* asyncCleanup() {
// clear the clipboard
registerCleanupFunction(function cleanup() {
Services.clipboard.emptyClipboard(globalClipboard);
info("Clipboard was cleared");
// restore the tab as it was at the begining of the test
gBrowser.addTab(initialLocation);
gBrowser.removeTab(gBrowser.selectedTab);
info("Tabs were restored");
});

View File

@ -234,8 +234,21 @@ if (typeof Mozilla == 'undefined') {
});
};
Mozilla.UITour.showFirefoxAccounts = function() {
_sendEvent('showFirefoxAccounts');
/**
* Request the browser open the Firefox Accounts page.
*
* @param {Object} extraURLCampaignParams - An object containing additional
* paramaters for the URL opened by the browser for reasons of promotional
* campaign tracking. Each attribute of the object must have a name that
* begins with "utm_" and a value that is a string. Both the name and value
* must contain only alphanumeric characters, dashes or underscores (meaning
* that you are limited to values that don't need encoding, as any such
* characters in the name or value will be rejected.)
*/
Mozilla.UITour.showFirefoxAccounts = function(extraURLCampaignParams) {
_sendEvent('showFirefoxAccounts', {
extraURLCampaignParams: JSON.stringify(extraURLCampaignParams),
});
};
Mozilla.UITour.resetFirefox = function() {

View File

@ -610,8 +610,15 @@ this.UITour = {
case "showFirefoxAccounts": {
// 'signup' is the only action that makes sense currently, so we don't
// accept arbitrary actions just to be safe...
let p = new URLSearchParams("action=signup&entrypoint=uitour");
// Call our helper to validate extraURLCampaignParams and populate URLSearchParams
if (!this._populateCampaignParams(p, data.extraURLCampaignParams)) {
log.warn("showFirefoxAccounts: invalid campaign args specified");
return false;
}
// We want to replace the current tab.
browser.loadURI("about:accounts?action=signup&entrypoint=uitour");
browser.loadURI("about:accounts?" + p.toString());
break;
}
@ -805,6 +812,52 @@ this.UITour = {
}
},
// Given a string that is a JSONified represenation of an object with
// additional utm_* URL params that should be appended, validate and append
// them to the passed URLSearchParams object. Returns true if the params
// were validated and appended, and false if the request should be ignored.
_populateCampaignParams: function(urlSearchParams, extraURLCampaignParams) {
// We are extra paranoid about what params we allow to be appended.
if (typeof extraURLCampaignParams == "undefined") {
// no params, so it's all good.
return true;
}
if (typeof extraURLCampaignParams != "string") {
log.warn("_populateCampaignParams: extraURLCampaignParams is not a string");
return false;
}
let campaignParams;
try {
if (extraURLCampaignParams) {
campaignParams = JSON.parse(extraURLCampaignParams);
if (typeof campaignParams != "object") {
log.warn("_populateCampaignParams: extraURLCampaignParams is not a stringified object");
return false;
}
}
} catch (ex) {
log.warn("_populateCampaignParams: extraURLCampaignParams is not a JSON object");
return false;
}
if (campaignParams) {
// The regex that both the name and value of each param must match.
let reSimpleString = /^[-_a-zA-Z0-9]*$/;
for (let name in campaignParams) {
let value = campaignParams[name];
if (typeof name != "string" || typeof value != "string" ||
!name.startsWith("utm_") ||
value.length == 0 ||
!reSimpleString.test(name) ||
!reSimpleString.test(value)) {
log.warn("_populateCampaignParams: invalid campaign param specified");
return false;
}
urlSearchParams.append(name, value);
}
}
return true;
},
setTelemetryBucket: function(aPageID) {
let bucket = BUCKET_NAME + BrowserUITelemetry.BUCKET_SEPARATOR + aPageID;
BrowserUITelemetry.setBucket(bucket);
@ -1700,7 +1753,7 @@ this.UITour = {
// An event object is expected but we don't want to toggle the panel with a click if the panel
// is already open.
aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }, "rooms").then(() => {
aWindow.LoopUI.openPanel({ target: toolbarButton.node, }, "rooms").then(() => {
if (aOpenCallback) {
aOpenCallback();
}

View File

@ -37,6 +37,7 @@ skip-if = e10s # Bug 1240747 - UITour.jsm not e10s friendly
skip-if = e10s # Bug 1240747 - UITour.jsm not e10s friendly.
[browser_UITour_loop.js]
skip-if = true # Bug 1225832 - New Loop architecture is not compatible with test.
[browser_UITour_loop_panel.js]
[browser_UITour_modalDialog.js]
skip-if = os != "mac" || e10s # modal dialog disabling only working on OS X. Bug 1240747 - UITour.jsm not e10s friendly
[browser_UITour_observe.js]

View File

@ -112,29 +112,6 @@ var tests = [
checkLoopPanelIsHidden();
}),
taskify(function* test_menu_show_hide() {
// The targets to highlight only appear after getting started is launched.
// Set latestFTUVersion to lower number to show FTU panel.
Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 0);
is(loopButton.open, false, "Menu should initially be closed");
gContentAPI.showMenu("loop");
yield waitForConditionPromise(() => {
return loopButton.open;
}, "Menu should be visible after showMenu()");
ok(loopPanel.hasAttribute("noautohide"), "@noautohide should be on the loop panel");
ok(loopPanel.hasAttribute("panelopen"), "The panel should have @panelopen");
is(loopPanel.state, "open", "The panel should be open");
ok(loopButton.hasAttribute("open"), "Loop button should know that the menu is open");
gContentAPI.hideMenu("loop");
yield waitForConditionPromise(() => {
return !loopButton.open;
}, "Menu should be hidden after hideMenu()");
checkLoopPanelIsHidden();
}),
// Test the menu was cleaned up in teardown.
taskify(function* setup_menu_cleanup() {
gContentAPI.showMenu("loop");

View File

@ -0,0 +1,68 @@
"use strict";
var gTestTab;
var gContentAPI;
var gContentWindow;
var gMessageHandlers;
var loopButton;
var fakeRoom;
var loopPanel = document.getElementById("loop-notification-panel");
const { LoopAPI } = Cu.import("chrome://loop/content/modules/MozLoopAPI.jsm", {});
const { LoopRooms } = Cu.import("chrome://loop/content/modules/LoopRooms.jsm", {});
if (!Services.prefs.getBoolPref("loop.enabled")) {
ok(true, "Loop is disabled so skip the UITour Loop tests");
} else {
function checkLoopPanelIsHidden() {
ok(!loopPanel.hasAttribute("noautohide"), "@noautohide on the loop panel should have been cleaned up");
ok(!loopPanel.hasAttribute("panelopen"), "The panel shouldn't have @panelopen");
isnot(loopPanel.state, "open", "The panel shouldn't be open");
is(loopButton.hasAttribute("open"), false, "Loop button should know that the panel is closed");
}
add_task(setup_UITourTest);
add_task(function() {
loopButton = window.LoopUI.toolbarButton.node;
registerCleanupFunction(() => {
Services.prefs.clearUserPref("loop.gettingStarted.latestFTUVersion");
Services.io.offline = false;
// Copied from browser/components/loop/test/mochitest/head.js
// Remove the iframe after each test. This also avoids mochitest complaining
// about leaks on shutdown as we intentionally hold the iframe open for the
// life of the application.
let frameId = loopButton.getAttribute("notificationFrameId");
let frame = document.getElementById(frameId);
if (frame) {
frame.remove();
}
});
});
add_UITour_task(function* test_menu_show_hide() {
// The targets to highlight only appear after getting started is launched.
// Set latestFTUVersion to lower number to show FTU panel.
Services.prefs.setIntPref("loop.gettingStarted.latestFTUVersion", 0);
is(loopButton.open, false, "Menu should initially be closed");
gContentAPI.showMenu("loop");
yield waitForConditionPromise(() => {
return loopButton.open;
}, "Menu should be visible after showMenu()");
ok(loopPanel.hasAttribute("noautohide"), "@noautohide should be on the loop panel");
ok(loopPanel.hasAttribute("panelopen"), "The panel should have @panelopen");
is(loopPanel.state, "open", "The panel should be open");
ok(loopButton.hasAttribute("open"), "Loop button should know that the menu is open");
gContentAPI.hideMenu("loop");
yield waitForConditionPromise(() => {
return !loopButton.open;
}, "Menu should be hidden after hideMenu()");
checkLoopPanelIsHidden();
});
}

View File

@ -22,8 +22,50 @@ add_UITour_task(function* test_checkSyncSetup_enabled() {
});
// The showFirefoxAccounts API is sync related, so we test that here too...
add_UITour_task(function* test_firefoxAccounts() {
add_UITour_task(function* test_firefoxAccountsNoParams() {
yield gContentAPI.showFirefoxAccounts();
yield BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
"about:accounts?action=signup&entrypoint=uitour");
});
add_UITour_task(function* test_firefoxAccountsValidParams() {
yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", utm_bar: "bar" });
yield BrowserTestUtils.browserLoaded(gTestTab.linkedBrowser, false,
"about:accounts?action=signup&entrypoint=uitour&utm_foo=foo&utm_bar=bar");
});
// A helper to check the request was ignored due to invalid params.
function* checkAboutAccountsNotLoaded() {
try {
yield waitForConditionPromise(() => {
return gBrowser.selectedBrowser.currentURI.spec.startsWith("about:accounts");
}, "Check if about:accounts opened");
ok(false, "No about:accounts tab should have opened");
} catch (ex) {
ok(true, "No about:accounts tab opened");
}
}
add_UITour_task(function* test_firefoxAccountsNonObject() {
// non-string should be rejected.
yield gContentAPI.showFirefoxAccounts(99);
yield checkAboutAccountsNotLoaded();
});
add_UITour_task(function* test_firefoxAccountsNonUtmPrefix() {
// Any non "utm_" name should should be rejected.
yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", bar: "bar" });
yield checkAboutAccountsNotLoaded();
});
add_UITour_task(function* test_firefoxAccountsNonAlphaName() {
// Any "utm_" name which includes non-alpha chars should be rejected.
yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo", "utm_bar=": "bar" });
yield checkAboutAccountsNotLoaded();
});
add_UITour_task(function* test_firefoxAccountsNonAlphaValue() {
// Any non-alpha value should be rejected.
yield gContentAPI.showFirefoxAccounts({ utm_foo: "foo&" });
yield checkAboutAccountsNotLoaded();
});

View File

@ -28,6 +28,7 @@ LOOPDIR=browser/extensions/loop
TESTS="
${LOOPDIR}/chrome/test/mochitest
browser/components/uitour/test/browser_UITour_loop_panel.js
browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
browser/base/content/test/general/browser_parsable_css.js
"

View File

@ -357,7 +357,7 @@ var PocketOverlay = {
PocketContextMenu.init();
PocketReader.startup();
if (reason == ADDON_ENABLE) {
if (reason != APP_STARTUP) {
for (let win of allBrowserWindows()) {
this.setWindowScripts(win);
this.addStyles(win);
@ -529,7 +529,7 @@ function startup(data, reason) {
function shutdown(data, reason) {
// For speed sake, we should only do a shutdown if we're being disabled.
// On an app shutdown, just let it fade away...
if (reason == ADDON_DISABLE) {
if (reason != APP_SHUTDOWN) {
Services.prefs.removeObserver("extensions.pocket.enabled", prefObserver);
PocketOverlay.shutdown(reason);
}

View File

@ -744,7 +744,6 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY spellAddDictionaries.accesskey "A">
<!ENTITY editBookmark.done.label "Done">
<!ENTITY editBookmark.cancel.label "Cancel">
<!ENTITY editBookmark.removeBookmark.accessKey "R">
<!ENTITY identity.connectionSecure "Secure Connection">

View File

@ -265,8 +265,8 @@
// Disallow unreachable statements after a return, throw, continue, or break
// statement.
"no-unreachable": 2,
// Disallow declaration of variables that are not used in the code
"no-unused-vars": 2,
// Disallow global and local variables that aren't used, but allow unused function arguments.
"no-unused-vars": [2, {"vars": "all", "args": "none"}],
// Allow using variables before they are defined.
"no-use-before-define": 0,
// We use var-only-at-top-level instead of no-var as we allow top level

View File

@ -43,13 +43,6 @@ button {
max-width: 800px;
}
/* Prefs */
label {
display: block;
margin-bottom: 5px;
}
/* Targets */
.targets {
@ -88,3 +81,8 @@ label {
.addons-options {
flex: 1;
}
.addons-debugging-label {
display: inline-block;
margin: 0 5px 5px 0;
}

View File

@ -17,6 +17,9 @@ loader.lazyImporter(this, "AddonManager",
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const MORE_INFO_URL = "https://developer.mozilla.org/docs/Tools" +
"/about:debugging#Enabling_add-on_debugging";
exports.AddonsControls = React.createClass({
displayName: "AddonsControls",
@ -33,9 +36,14 @@ exports.AddonsControls = React.createClass({
onChange: this.onEnableAddonDebuggingChange,
}),
React.createElement("label", {
className: "addons-debugging-label",
htmlFor: "enable-addon-debugging",
title: Strings.GetStringFromName("addonDebugging.tooltip")
}, Strings.GetStringFromName("addonDebugging.label"))
}, Strings.GetStringFromName("addonDebugging.label")),
"(",
React.createElement("a", { href: MORE_INFO_URL, target: "_blank" },
Strings.GetStringFromName("addonDebugging.moreInfo")),
")"
),
React.createElement("button", {
id: "load-addon-from-file",
@ -47,6 +55,7 @@ exports.AddonsControls = React.createClass({
onEnableAddonDebuggingChange(event) {
let enabled = event.target.checked;
Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
},
loadAddonFromFile(event) {

View File

@ -23,6 +23,9 @@ const ExtensionIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
const Strings = Services.strings.createBundle(
"chrome://devtools/locale/aboutdebugging.properties");
const CHROME_ENABLED_PREF = "devtools.chrome.enabled";
const REMOTE_ENABLED_PREF = "devtools.debugger.remote-enabled";
exports.AddonsTab = React.createClass({
displayName: "AddonsTab",
@ -35,15 +38,21 @@ exports.AddonsTab = React.createClass({
componentDidMount() {
AddonManager.addAddonListener(this);
Services.prefs.addObserver("devtools.chrome.enabled",
Services.prefs.addObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus, false);
Services.prefs.addObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus, false);
this.updateDebugStatus();
this.updateAddonsList();
},
componentWillUnmount() {
AddonManager.removeAddonListener(this);
Services.prefs.removeObserver("devtools.chrome.enabled",
Services.prefs.removeObserver(CHROME_ENABLED_PREF,
this.updateDebugStatus);
Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
this.updateDebugStatus);
},
@ -68,9 +77,11 @@ exports.AddonsTab = React.createClass({
},
updateDebugStatus() {
this.setState({
debugDisabled: !Services.prefs.getBoolPref("devtools.chrome.enabled")
});
let debugDisabled =
!Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
!Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
this.setState({ debugDisabled });
},
updateAddonsList() {

View File

@ -8,6 +8,7 @@ support-files =
service-workers/empty-sw.html
service-workers/empty-sw.js
[browser_addons_debugging_initial_state.js]
[browser_addons_install.js]
[browser_addons_toggle_debug.js]
[browser_service_workers.js]

View File

@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that addons debugging controls are properly enabled/disabled depending
// on the values of the relevant preferences:
// - devtools.chrome.enabled
// - devtools.debugger.remote-enabled
const ADDON_ID = "test-devtools@mozilla.org";
const TEST_DATA = [
{
chromeEnabled: false,
debuggerRemoteEnable: false,
expected: false,
}, {
chromeEnabled: false,
debuggerRemoteEnable: true,
expected: false,
}, {
chromeEnabled: true,
debuggerRemoteEnable: false,
expected: false,
}, {
chromeEnabled: true,
debuggerRemoteEnable: true,
expected: true,
}
];
add_task(function* () {
for (let testData of TEST_DATA) {
yield testCheckboxState(testData);
}
});
function* testCheckboxState(testData) {
info("Set preferences as defined by the current test data.");
yield new Promise(resolve => {
let options = {"set": [
["devtools.chrome.enabled", testData.chromeEnabled],
["devtools.debugger.remote-enabled", testData.debuggerRemoteEnable],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});
let { tab, document } = yield openAboutDebugging("addons");
info("Install a test addon.");
yield installAddon(document, "addons/unpacked/install.rdf", "test-devtools");
info("Test checkbox checked state.");
let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
is(addonDebugCheckbox.checked, testData.expected,
"Addons debugging checkbox should be in expected state.");
info("Test debug buttons disabled state.");
let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
ok(debugButtons.every(b => b.disabled != testData.expected),
"Debug buttons should be in the expected state");
info("Uninstall test addon installed earlier.");
yield uninstallAddon(ADDON_ID);
yield closeAboutDebugging(tab);
}

View File

@ -13,6 +13,7 @@ add_task(function* () {
yield new Promise(resolve => {
let options = {"set": [
["devtools.chrome.enabled", false],
["devtools.debugger.remote-enabled", false],
]};
SpecialPowers.pushPrefEnv(options, resolve);
});

View File

@ -9,6 +9,11 @@ Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/inspector/test/head.js",
this);
Services.prefs.setBoolPref("devtools.fontinspector.enabled", true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("devtools.fontinspector.enabled");
});
/**
* Adds a new tab with the given URL, opens the inspector and selects the
* font-inspector tab.

View File

@ -598,6 +598,7 @@ MarkupView.prototype = {
*/
_onKeyDown: function(event) {
let handled = true;
let previousNode, nextNode;
// Ignore keystrokes that originated in editors.
if (this._isInputOrTextarea(event.target)) {
@ -649,26 +650,26 @@ MarkupView.prototype = {
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_UP:
let prev = this._selectionWalker().previousNode();
if (prev) {
this.navigate(prev.container);
previousNode = this._selectionWalker().previousNode();
if (previousNode) {
this.navigate(previousNode.container);
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_DOWN:
let next = this._selectionWalker().nextNode();
if (next) {
this.navigate(next.container);
nextNode = this._selectionWalker().nextNode();
if (nextNode) {
this.navigate(nextNode.container);
}
break;
case Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP: {
let walker = this._selectionWalker();
let selection = this._selectedContainer;
for (let i = 0; i < PAGE_SIZE; i++) {
let prev = walker.previousNode();
if (!prev) {
previousNode = walker.previousNode();
if (!previousNode) {
break;
}
selection = prev.container;
selection = previousNode.container;
}
this.navigate(selection);
break;
@ -677,11 +678,11 @@ MarkupView.prototype = {
let walker = this._selectionWalker();
let selection = this._selectedContainer;
for (let i = 0; i < PAGE_SIZE; i++) {
let next = walker.nextNode();
if (!next) {
nextNode = walker.nextNode();
if (!nextNode) {
break;
}
selection = next.container;
selection = nextNode.container;
}
this.navigate(selection);
break;
@ -955,8 +956,8 @@ MarkupView.prototype = {
// If there has been additions, flash the nodes if their associated
// container exist (so if their parent is expanded in the inspector).
added.forEach(added => {
let addedContainer = this.getContainer(added);
added.forEach(node => {
let addedContainer = this.getContainer(node);
if (addedContainer) {
addedOrEditedContainers.add(addedContainer);
@ -1501,55 +1502,56 @@ MarkupView.prototype = {
// If the dirty flag is re-set while we're fetching we'll need to fetch
// again.
container.childrenDirty = false;
let updatePromise = this._getVisibleChildren(container, centered).then(children => {
if (!this._containers) {
return promise.reject("markup view destroyed");
}
this._queuedChildUpdates.delete(container);
let updatePromise =
this._getVisibleChildren(container, centered).then(children => {
if (!this._containers) {
return promise.reject("markup view destroyed");
}
this._queuedChildUpdates.delete(container);
// If children are dirty, we got a change notification for this node
// while the request was in progress, we need to do it again.
if (container.childrenDirty) {
return this._updateChildren(container, {expand: centered});
}
// If children are dirty, we got a change notification for this node
// while the request was in progress, we need to do it again.
if (container.childrenDirty) {
return this._updateChildren(container, {expand: centered});
}
let fragment = this.doc.createDocumentFragment();
let fragment = this.doc.createDocumentFragment();
for (let child of children.nodes) {
let container = this.importNode(child, flash);
fragment.appendChild(container.elt);
}
for (let child of children.nodes) {
let childContainer = this.importNode(child, flash);
fragment.appendChild(childContainer.elt);
}
while (container.children.firstChild) {
container.children.removeChild(container.children.firstChild);
}
while (container.children.firstChild) {
container.children.removeChild(container.children.firstChild);
}
if (!(children.hasFirst && children.hasLast)) {
let data = {
showing: this.strings.GetStringFromName("markupView.more.showing"),
showAll: this.strings.formatStringFromName(
"markupView.more.showAll",
[container.node.numChildren.toString()], 1),
allButtonClick: () => {
container.maxChildren = -1;
container.childrenDirty = true;
this._updateChildren(container);
if (!(children.hasFirst && children.hasLast)) {
let data = {
showing: this.strings.GetStringFromName("markupView.more.showing"),
showAll: this.strings.formatStringFromName(
"markupView.more.showAll",
[container.node.numChildren.toString()], 1),
allButtonClick: () => {
container.maxChildren = -1;
container.childrenDirty = true;
this._updateChildren(container);
}
};
if (!children.hasFirst) {
let span = this.template("more-nodes", data);
fragment.insertBefore(span, fragment.firstChild);
}
if (!children.hasLast) {
let span = this.template("more-nodes", data);
fragment.appendChild(span);
}
};
if (!children.hasFirst) {
let span = this.template("more-nodes", data);
fragment.insertBefore(span, fragment.firstChild);
}
if (!children.hasLast) {
let span = this.template("more-nodes", data);
fragment.appendChild(span);
}
}
container.children.appendChild(fragment);
return container;
}).then(null, console.error);
container.children.appendChild(fragment);
return container;
}).then(null, console.error);
this._queuedChildUpdates.set(container, updatePromise);
return updatePromise;
},
@ -2867,8 +2869,8 @@ ElementEditor.prototype = {
editor.input.select();
}
},
done: (val, commit, direction) => {
if (!commit || val === initial) {
done: (newValue, commit, direction) => {
if (!commit || newValue === initial) {
return;
}
@ -2881,7 +2883,7 @@ ElementEditor.prototype = {
this.refocusOnEdit(attribute.name, attr, direction);
this._saveAttribute(attribute.name, undoMods);
doMods.removeAttribute(attribute.name);
this._applyAttributes(val, attr, doMods, undoMods);
this._applyAttributes(newValue, attr, doMods, undoMods);
this.container.undo.do(() => {
doMods.apply();
}, () => {
@ -2906,8 +2908,9 @@ ElementEditor.prototype = {
// it (make sure to pass a complete list of existing attributes to the
// parseAttribute function, by concatenating attribute, because this could
// be a newly added attribute not yet on this.node).
let attributes = this.node.attributes
.filter(({name}) => name !== attribute.name);
let attributes = this.node.attributes.filter(existingAttribute => {
return existingAttribute.name !== attribute.name;
});
attributes.push(attribute);
let parsedLinksData = parseAttribute(this.node.namespaceURI,
this.node.tagName, attributes, attribute.name);

View File

@ -216,6 +216,7 @@ devtools.jar:
skin/styleeditor.css (themes/styleeditor.css)
skin/webaudioeditor.css (themes/webaudioeditor.css)
skin/components-frame.css (themes/components-frame.css)
skin/components-h-split-box.css (themes/components-h-split-box.css)
skin/jit-optimizations.css (themes/jit-optimizations.css)
skin/images/magnifying-glass.png (themes/images/magnifying-glass.png)
skin/images/magnifying-glass@2x.png (themes/images/magnifying-glass@2x.png)

View File

@ -7,7 +7,7 @@
define(function(require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./reps/rep-utils");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { Headers } = createFactories(require("./headers"));
const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));

View File

@ -7,7 +7,7 @@
define(function(require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./reps/rep-utils");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { TreeView } = createFactories(require("./reps/tree-view"));
const { SearchBox } = createFactories(require("./search-box"));
const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));

View File

@ -9,7 +9,7 @@
define(function(require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./reps/rep-utils");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { JsonPanel } = createFactories(require("./json-panel"));
const { TextPanel } = createFactories(require("./text-panel"));
const { HeadersPanel } = createFactories(require("./headers-panel"));

View File

@ -1,189 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { Rep } = createFactories(require("./rep"));
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const DOM = React.DOM;
/**
* Renders an array. The array is enclosed by left and right bracket
* and the max number of rendered items depends on the current mode.
*/
var ArrayRep = React.createClass({
displayName: "ArrayRep",
render: function() {
var mode = this.props.mode || "short";
var object = this.props.object;
var hasTwisty = this.hasSpecialProperties(object);
var items;
if (mode == "tiny") {
items = DOM.span({className: "length"}, object.length);
} else {
var max = (mode == "short") ? 3 : 300;
items = this.arrayIterator(object, max);
}
return (
ObjectBox({className: "array", onClick: this.onToggleProperties},
DOM.a({className: "objectLink", onclick: this.onClickBracket},
DOM.span({className: "arrayLeftBracket", role: "presentation"}, "[")
),
items,
DOM.a({className: "objectLink", onclick: this.onClickBracket},
DOM.span({className: "arrayRightBracket", role: "presentation"}, "]")
),
DOM.span({className: "arrayProperties", role: "group"})
)
)
},
getTitle: function(object, context) {
return "[" + object.length + "]";
},
arrayIterator: function(array, max) {
var items = [];
for (var i=0; i<array.length && i<=max; i++) {
try {
var delim = (i == array.length-1 ? "" : ", ");
var value = array[i];
if (value === array) {
items.push(Reference({
key: i,
object: value,
delim: delim
}));
} else {
items.push(ItemRep({
key: i,
object: value,
delim: delim
}));
}
} catch (exc) {
items.push(ItemRep({object: exc, delim: delim, key: i}));
}
}
if (array.length > max + 1) {
items.pop();
items.push(Caption({
key: "more",
object: Locale.$STR("jsonViewer.reps.more"),
}));
}
return items;
},
/**
* Returns true if the passed object is an array with additional (custom)
* properties, otherwise returns false. Custom properties should be
* displayed in extra expandable section.
*
* Example array with a custom property.
* let arr = [0, 1];
* arr.myProp = "Hello";
*
* @param {Array} array The array object.
*/
hasSpecialProperties: function(array) {
function isInteger(x) {
var y = parseInt(x, 10);
if (isNaN(y)) {
return false;
}
return x === y.toString();
}
var n = 0;
var props = Object.getOwnPropertyNames(array);
for (var i=0; i<props.length; i++) {
var p = props[i];
// Valid indexes are skipped
if (isInteger(p)) {
continue;
}
// Ignore standard 'length' property, anything else is custom.
if (p != "length") {
return true;
}
}
return false;
},
// Event Handlers
onToggleProperties: function(event) {
},
onClickBracket: function(event) {
}
});
/**
* Renders array item. Individual values are separated by a comma.
*/
var ItemRep = React.createFactory(React.createClass({
displayName: "ItemRep",
render: function(){
var object = this.props.object;
var delim = this.props.delim;
return (
DOM.span({},
Rep({object: object}),
delim
)
)
}
}));
/**
* Renders cycle references in an array.
*/
var Reference = React.createFactory(React.createClass({
displayName: "Reference",
render: function(){
var tooltip = Locale.$STR("jsonView.reps.reference");
return (
span({title: tooltip},
"[...]")
)
}
}));
function supportsObject(object, type) {
return Array.isArray(object) ||
Object.prototype.toString.call(object) === "[object Arguments]";
}
// Exports from this module
exports.ArrayRep = {
rep: ArrayRep,
supportsObject: supportsObject
};
});

View File

@ -1,31 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a caption. This template is used by other components
* that needs to distinguish between a simple text/value and a label.
*/
const Caption = React.createClass({
displayName: "Caption",
render: function() {
return (
DOM.span({"className": "caption"}, this.props.object)
);
},
});
// Exports from this module
exports.Caption = Caption;
});

View File

@ -5,18 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'array.js',
'caption.js',
'null.js',
'number.js',
'object-box.js',
'object-link.js',
'object.js',
'rep-utils.js',
'rep.js',
'string.js',
'tabs.js',
'toolbar.js',
'tree-view.js',
'undefined.js',
)

View File

@ -1,46 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders null value
*/
const Null = React.createClass({
displayName: "NullRep",
render: function() {
return (
ObjectBox({className: "null"},
"null"
)
)
},
});
function supportsObject(object, type) {
if (object && object.type && object.type == "null") {
return true;
}
return (object == null);
}
// Exports from this module
exports.Null = {
rep: Null,
supportsObject: supportsObject
};
});

View File

@ -1,47 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders a number
*/
const Number = React.createClass({
displayName: "Number",
render: function() {
var value = this.props.object;
return (
ObjectBox({className: "number"},
this.stringify(value)
)
)
},
stringify: function(object) {
return (Object.is(object, -0) ? "-0" : String(object));
},
});
function supportsObject(object, type) {
return type == "boolean" || type == "number";
}
// Exports from this module
exports.Number = {
rep: Number,
supportsObject: supportsObject
};
});

View File

@ -1,35 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a box for given object.
*/
const ObjectBox = React.createClass({
displayName: "ObjectBox",
render: function() {
var className = this.props.className;
var boxClassName = className ? " objectBox-" + className : "";
return (
DOM.span({className: "objectBox" + boxClassName, role: "presentation"},
this.props.children
)
)
}
});
// Exports from this module
exports.ObjectBox = ObjectBox;
});

View File

@ -1,36 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a link for given object.
*/
const ObjectLink = React.createClass({
displayName: "ObjectLink",
render: function() {
var className = this.props.className;
var objectClassName = className ? " objectLink-" + className : "";
var linkClassName = "objectLink" + objectClassName + " a11yFocus";
return (
DOM.a({className: linkClassName, _repObject: this.props.object},
this.props.children
)
)
}
});
// Exports from this module
exports.ObjectLink = ObjectLink;
});

View File

@ -1,178 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const DOM = React.DOM;
/**
* Renders an object. An object is represented by a list of its
* properties enclosed in curly brackets.
*/
const Obj = React.createClass({
displayName: "Obj",
render: function() {
var object = this.props.object;
var props = this.shortPropIterator(object);
return (
ObjectBox({className: "object"},
DOM.span({className: "objectTitle"}, this.getTitle(object)),
DOM.span({className: "objectLeftBrace", role: "presentation"}, "{"),
props,
DOM.span({className: "objectRightBrace"}, "}")
)
)
},
getTitle: function() {
return ""; // Could also be "Object";
},
longPropIterator: function (object) {
try {
return this.propIterator(object, 100);
}
catch (err) {
console.error(err);
}
},
shortPropIterator: function (object) {
try {
return this.propIterator(object, /*could be a pref*/ 3);
}
catch (err) {
console.error(err);
}
},
propIterator: function(object, max) {
function isInterestingProp(t, value) {
return (t == "boolean" || t == "number" || (t == "string" && value) ||
(t == "object" && value && value.toString));
}
// Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
if (Object.prototype.toString.call(object) === "[object Generator]") {
object = Object.getPrototypeOf(object);
}
// Object members with non-empty values are preferred since it gives the
// user a better overview of the object.
var props = [];
this.getProps(props, object, max, isInterestingProp);
if (props.length <= max) {
// There are not enough props yet (or at least, not enough props to
// be able to know whether we should print "more..." or not).
// Let's display also empty members and functions.
this.getProps(props, object, max, function(t, value) {
return !isInterestingProp(t, value);
});
}
if (props.length > max) {
props.pop();
props.push(Caption({
key: "more",
object: Locale.$STR("jsonViewer.reps.more"),
}));
}
else if (props.length > 0) {
// Remove the last comma.
props[props.length-1] = React.cloneElement(
props[props.length-1], { delim: "" });
}
return props;
},
getProps: function (props, object, max, filter) {
max = max || 3;
if (!object) {
return [];
}
var len = 0;
var mode = this.props.mode;
try {
for (var name in object) {
if (props.length > max) {
return;
}
var value;
try {
value = object[name];
}
catch (exc) {
continue;
}
var t = typeof(value);
if (filter(t, value)) {
props.push(PropRep({
key: name,
mode: "short",
name: name,
object: value,
equal: ": ",
delim: ", ",
mode: mode,
}));
}
}
}
catch (exc) {
}
},
});
/**
* Renders object property, name-value pair.
*/
var PropRep = React.createFactory(React.createClass({
displayName: "PropRep",
render: function(){
var { Rep } = createFactories(require("./rep"));
var object = this.props.object;
var mode = this.props.mode;
return (
DOM.span({},
DOM.span({"className": "nodeName"}, this.props.name),
DOM.span({"className": "objectEqual", role: "presentation"}, this.props.equal),
Rep({object: object, mode: mode}),
DOM.span({"className": "objectComma", role: "presentation"}, this.props.delim)
)
);
}
}));
function supportsObject(object, type) {
return true;
}
// Exports from this module
exports.Obj = {
rep: Obj,
supportsObject: supportsObject
};
});

View File

@ -1,29 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
/**
* Create React factories for given arguments.
* Example:
* const { Rep } = createFactories(require("./rep"));
*/
function createFactories(args) {
var result = {};
for (var p in args) {
result[p] = React.createFactory(args[p]);
}
return result;
}
// Exports from this module
exports.createFactories = createFactories;
});

View File

@ -1,87 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
// Load all existing rep templates
const { Undefined } = require("./undefined");
const { Null } = require("./null");
const { StringRep } = require("./string");
const { Number } = require("./number");
const { ArrayRep } = require("./array");
const { Obj } = require("./object");
// List of all registered template.
// XXX there should be a way for extensions to register a new
// or modify an existing rep.
var reps = [Undefined, Null, StringRep, Number, ArrayRep, Obj];
var defaultRep;
/**
* Generic rep that is using for rendering native JS types or an object.
* The right template used for rendering is picked automatically according
* to the current value type. The value must be passed is as 'object'
* property.
*/
const Rep = React.createClass({
displayName: "Rep",
render: function() {
var rep = getRep(this.props.object);
return rep(this.props);
},
});
// Helpers
/**
* Return a rep object that is responsible for rendering given
* object.
*
* @param object {Object} Object to be rendered in the UI. This
* can be generic JS object as well as a grip (handle to a remote
* debuggee object).
*/
function getRep(object) {
var type = typeof(object);
if (type == "object" && object instanceof String) {
type = "string";
}
if (isGrip(object)) {
type = object.class;
}
for (var i=0; i<reps.length; i++) {
var rep = reps[i];
try {
// supportsObject could return weight (not only true/false
// but a number), which would allow to priorities templates and
// support better extensibility.
if (rep.supportsObject(object, type)) {
return React.createFactory(rep.rep);
}
}
catch (err) {
console.error("reps.getRep; EXCEPTION ", err, err);
}
}
return React.createFactory(defaultRep.rep);
}
function isGrip(object) {
return object && object.actor;
}
// Exports from this module
exports.Rep = Rep;
});

View File

@ -1,102 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders a string. String value is enclosed within quotes.
*/
const StringRep = React.createClass({
displayName: "StringRep",
render: function() {
var text = this.props.object;
var member = this.props.member;
if (member && member.open) {
return (
ObjectBox({className: "string"},
"\"" + text + "\""
)
)
} else {
return (
ObjectBox({className: "string"},
"\"" + cropMultipleLines(text) + "\""
)
)
}
},
});
// Helpers
function escapeNewLines(value) {
return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
};
function cropMultipleLines(text, limit) {
return escapeNewLines(cropString(text, limit));
};
function cropString(text, limit, alternativeText) {
if (!alternativeText) {
alternativeText = "...";
}
// Make sure it's a string.
text = text + "";
// Use default limit if necessary.
if (!limit) {
limit = 50;
}
// Crop the string only if a limit is actually specified.
if (limit <= 0) {
return text;
}
// Set the limit at least to the length of the alternative text
// plus one character of the original text.
if (limit <= alternativeText.length) {
limit = alternativeText.length + 1;
}
var halfLimit = (limit - alternativeText.length) / 2;
if (text.length > limit) {
return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
text.substr(text.length - Math.floor(halfLimit));
}
return text;
};
function isCropped(value) {
var cropLength = 50;
return typeof(value) == "string" && value.length > cropLength;
}
function supportsObject(object, type) {
return (type == "string");
}
// Exports from this module
exports.StringRep = {
rep: StringRep,
supportsObject: supportsObject,
isCropped: isCropped
};
});

View File

@ -8,9 +8,9 @@ define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { Rep } = createFactories(require("./rep"));
const { StringRep } = require("./string");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { Rep } = createFactories(require("devtools/client/shared/components/reps/rep"));
const { StringRep } = require("devtools/client/shared/components/reps/string");
const DOM = React.DOM;
var uid = 0;

View File

@ -1,46 +0,0 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders undefined value
*/
const Undefined = React.createClass({
displayName: "UndefinedRep",
render: function() {
return (
ObjectBox({className: "undefined"},
"undefined"
)
)
},
});
function supportsObject(object, type) {
if (object && object.type && object.type == "undefined") {
return true;
}
return (type == "undefined");
}
// Exports from this module
exports.Undefined = {
rep: Undefined,
supportsObject: supportsObject
};
});

View File

@ -7,7 +7,7 @@
define(function(require, exports, module) {
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./reps/rep-utils");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { Toolbar, ToolbarButton } = createFactories(require("./reps/toolbar"));
const DOM = React.DOM;

View File

@ -3,8 +3,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@import "resource://devtools/client/shared/components/reps/reps.css";
@import "general.css";
@import "reps.css";
@import "dom-tree.css";
@import "search-box.css";
@import "tabs.css";

View File

@ -14,7 +14,6 @@ DevToolsModules(
'json-panel.css',
'main.css',
'read-only-prop.svg',
'reps.css',
'search-box.css',
'search.svg',
'tabs.css',

View File

@ -8,7 +8,7 @@ define(function(require, exports, module) {
// ReactJS
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { createFactories } = require("./components/reps/rep-utils");
const { createFactories } = require("devtools/client/shared/components/reps/rep-utils");
const { MainTabbedArea } = createFactories(require("./components/main-tabbed-area"));
const json = document.getElementById("json");

View File

@ -7,6 +7,7 @@ debug = Debug
addons = Add-ons
addonDebugging.label = Enable add-on debugging
addonDebugging.tooltip = Turning this on will allow you to debug add-ons and various other parts of the browser chrome
addonDebugging.moreInfo = more info
loadTemporaryAddon = Load Temporary Add-on
extensions = Extensions
selectAddonFromFile = Select Add-on Directory or XPI File

View File

@ -368,3 +368,11 @@ heapview.field.name=Name
# LOCALIZATION NOTE (heapview.field.name.tooltip): The tooltip for the column
# header in the heap view for name.
heapview.field.name.tooltip=The name of this group
# LOCALIZATION NOTE (shortest-paths.header): The header label for the shortest
# paths pane.
shortest-paths.header=Retaining Paths from GC Roots
# LOCALIZATION NOTE (shortest-paths.select-node): The message displayed in the
# shortest paths pane when a node is not yet selected.
shortest-paths.select-node=Select a node to view its retaining paths

View File

@ -12,6 +12,7 @@ DevToolsModules(
'inverted.js',
'io.js',
'refresh.js',
'sizes.js',
'snapshot.js',
'view.js',
)

View File

@ -0,0 +1,13 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { actions } = require("../constants");
exports.resizeShortestPaths = function (newSize) {
return {
type: actions.RESIZE_SHORTEST_PATHS,
size: newSize,
};
};

View File

@ -33,6 +33,7 @@ const {
focusDominatorTreeNode,
} = require("./actions/snapshot");
const { changeViewAndRefresh } = require("./actions/view");
const { resizeShortestPaths } = require("./actions/sizes");
const {
breakdownNameToSpec,
getBreakdownDisplayData,
@ -113,7 +114,7 @@ const MemoryApp = createClass({
filter,
diffing,
view,
dominatorTreeBreakdown
sizes,
} = this.props;
const selectedSnapshot = snapshots.find(s => s.selected);
@ -237,6 +238,10 @@ const MemoryApp = createClass({
"...and that snapshot should have a dominator tree");
dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
},
onShortestPathsResize: newSize => {
dispatch(resizeShortestPaths(newSize));
},
sizes,
view,
})
)

View File

@ -25,11 +25,11 @@ const CensusTreeItem = module.exports = createClass({
depth,
arrow,
focused,
toolbox,
getPercentBytes,
getPercentCount,
showSign,
onViewSourceInDebugger,
inverted,
} = this.props;
const bytes = formatNumber(item.bytes, showSign);
@ -44,7 +44,14 @@ const CensusTreeItem = module.exports = createClass({
const totalCount = formatNumber(item.totalCount, showSign);
const percentTotalCount = formatPercent(getPercentCount(item.totalCount), showSign);
return dom.div({ className: `heap-tree-item ${focused ? "focused" :""}` },
let pointer;
if (inverted && depth > 0) {
pointer = dom.span({ className: "children-pointer" }, "↖");
} else if (!inverted && item.children && item.children.length) {
pointer = dom.span({ className: "children-pointer" }, "↘");
}
return dom.div({ className: `heap-tree-item ${focused ? "focused" : ""}` },
dom.span({ className: "heap-tree-item-field heap-tree-item-bytes" },
dom.span({ className: "heap-tree-number" }, bytes),
dom.span({ className: "heap-tree-percent" }, percentBytes)),
@ -60,6 +67,7 @@ const CensusTreeItem = module.exports = createClass({
dom.span({ className: "heap-tree-item-field heap-tree-item-name",
style: { marginLeft: depth * TREE_ROW_HEIGHT }},
arrow,
pointer,
this.toLabel(item.name, onViewSourceInDebugger)
)
);

View File

@ -66,6 +66,7 @@ const Census = module.exports = createClass({
getPercentBytes,
getPercentCount,
showSign: !!diffing,
inverted: census.inverted,
}),
getRoots: () => report.children || [],
getKey: node => node.id,

View File

@ -8,6 +8,8 @@ const Census = createFactory(require("./census"));
const CensusHeader = createFactory(require("./census-header"));
const DominatorTree = createFactory(require("./dominator-tree"));
const DominatorTreeHeader = createFactory(require("./dominator-tree-header"));
const HSplitBox = createFactory(require("devtools/client/shared/components/h-split-box"));
const ShortestPaths = createFactory(require("./shortest-paths"));
const { getStatusTextFull, L10N } = require("../utils");
const { snapshotState: states, diffingState, viewState, dominatorTreeState } = require("../constants");
const { snapshot: snapshotModel, diffingModel } = require("../models");
@ -145,10 +147,12 @@ const Heap = module.exports = createClass({
onDominatorTreeCollapse: PropTypes.func.isRequired,
onCensusFocus: PropTypes.func.isRequired,
onDominatorTreeFocus: PropTypes.func.isRequired,
onShortestPathsResize: PropTypes.func.isRequired,
snapshot: snapshotModel,
onViewSourceInDebugger: PropTypes.func.isRequired,
diffing: diffingModel,
view: PropTypes.string.isRequired,
sizes: PropTypes.object.isRequired,
},
render() {
@ -277,8 +281,13 @@ const Heap = module.exports = createClass({
},
_renderDominatorTree(state, onViewSourceInDebugger, dominatorTree, onLoadMoreSiblings) {
return this._renderHeapView(
state,
const tree = dom.div(
{
className: "vbox",
style: {
overflowY: "auto"
}
},
DominatorTreeHeader(),
DominatorTree({
onViewSourceInDebugger,
@ -289,5 +298,21 @@ const Heap = module.exports = createClass({
onFocus: this.props.onDominatorTreeFocus,
})
);
const shortestPaths = ShortestPaths({
graph: dominatorTree.focused
? dominatorTree.focused.shortestPaths
: null
});
return this._renderHeapView(
state,
HSplitBox({
start: tree,
end: shortestPaths,
startWidth: this.props.sizes.shortestPathsSize,
onResize: this.props.onShortestPathsResize,
})
);
},
});

View File

@ -12,6 +12,7 @@ DevToolsModules(
'dominator-tree.js',
'heap.js',
'list.js',
'shortest-paths.js',
'snapshot-list-item.js',
'toolbar.js',
)

View File

@ -0,0 +1,189 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
DOM: dom,
createClass,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
const { getSourceNames } = require("devtools/client/shared/source-utils");
const { L10N } = require("../utils");
const { ViewHelpers } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
const COMPONENTS_STRINGS_URI = "chrome://devtools/locale/components.properties";
const componentsL10N = new ViewHelpers.L10N(COMPONENTS_STRINGS_URI);
const UNKNOWN_SOURCE_STRING = componentsL10N.getStr("frame.unknownSource");
const GRAPH_DEFAULTS = {
translate: [20, 20],
scale: 1
};
const NO_STACK = "noStack";
const NO_FILENAME = "noFilename";
const ROOT_LIST = "JS::ubi::RootList";
function stringifyLabel(label, id) {
const sanitized = [];
for (let i = 0, length = label.length; i < length; i++) {
const piece = label[i];
if (isSavedFrame(piece)) {
const { short } = getSourceNames(piece.source, UNKNOWN_SOURCE_STRING);
sanitized[i] = `${piece.functionDisplayName} @ ${short}:${piece.line}:${piece.column}`;
} else if (piece === NO_STACK) {
sanitized[i] = L10N.getStr("tree-item.nostack");
} else if (piece === NO_FILENAME) {
sanitized[i] = L10N.getStr("tree-item.nofilename");
} else if (piece === ROOT_LIST) {
// Don't use the usual labeling machinery for root lists: replace it
// with the "GC Roots" string.
sanitized.splice(0, label.length);
sanitized.push(L10N.getStr("tree-item.rootlist"));
break;
} else {
sanitized[i] = "" + piece;
}
}
return `${sanitized.join(" ")} @ 0x${id.toString(16)}`;
}
module.exports = createClass({
displayName: "ShortestPaths",
propTypes: {
graph: PropTypes.shape({
nodes: PropTypes.arrayOf(PropTypes.object),
edges: PropTypes.arrayOf(PropTypes.object),
}),
},
getInitialState() {
return { zoom: null };
},
shouldComponentUpdate(nextProps) {
return this.props.graph != nextProps.graph;
},
componentDidMount() {
if (this.props.graph) {
this._renderGraph(this.refs.container, this.props.graph);
}
},
componentDidUpdate() {
if (this.props.graph) {
this._renderGraph(this.refs.container, this.props.graph);
}
},
componentWillUnmount() {
if (this.state.zoom) {
this.state.zoom.on("zoom", null);
}
},
render() {
let contents;
if (this.props.graph) {
// Let the componentDidMount or componentDidUpdate method draw the graph
// with DagreD3. We just provide the container for the graph here.
contents = dom.div({
ref: "container",
style: {
flex: 1,
height: "100%",
width: "100%",
}
});
} else {
contents = dom.div(
{
id: "shortest-paths-select-node-msg"
},
L10N.getStr("shortest-paths.select-node")
);
}
return dom.div(
{
id: "shortest-paths",
className: "vbox",
},
dom.label(
{
id: "shortest-paths-header",
className: "header",
},
L10N.getStr("shortest-paths.header")
),
contents
);
},
_renderGraph(container, { nodes, edges }) {
if (!container.firstChild) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("id", "graph-svg");
svg.setAttribute("xlink", "http://www.w3.org/1999/xlink");
svg.style.width = "100%";
svg.style.height = "100%";
const target = document.createElementNS("http://www.w3.org/2000/svg", "g");
target.setAttribute("id", "graph-target");
target.style.width = "100%";
target.style.height = "100%";
svg.appendChild(target);
container.appendChild(svg);
}
const graph = new dagreD3.Digraph();
for (let i = 0; i < nodes.length; i++) {
graph.addNode(nodes[i].id, {
id: nodes[i].id,
label: stringifyLabel(nodes[i].label, nodes[i].id),
});
}
for (let i = 0; i < edges.length; i++) {
graph.addEdge(null, edges[i].from, edges[i].to, {
label: edges[i].name
});
}
const renderer = new dagreD3.Renderer();
renderer.drawNodes();
renderer.drawEdgePaths();
const svg = d3.select("#graph-svg");
const target = d3.select("#graph-target");
let zoom = this.state.zoom;
if (!zoom) {
zoom = d3.behavior.zoom().on("zoom", function() {
target.attr(
"transform",
`translate(${d3.event.translate}) scale(${d3.event.scale})`
);
});
svg.call(zoom);
this.setState({ zoom });
}
const { translate, scale } = GRAPH_DEFAULTS;
zoom.scale(scale);
zoom.translate(translate);
target.attr("transform", `translate(${translate}) scale(${scale})`);
const layout = dagreD3.layout();
renderer.layout(layout).run(graph, target);
},
});

View File

@ -101,6 +101,8 @@ actions.FETCH_IMMEDIATELY_DOMINATED_END = "fetch-immediately-dominated-end";
actions.EXPAND_DOMINATOR_TREE_NODE = "expand-dominator-tree-node";
actions.COLLAPSE_DOMINATOR_TREE_NODE = "collapse-dominator-tree-node";
actions.RESIZE_SHORTEST_PATHS = "resize-shortest-paths";
/*** Breakdowns ***************************************************************/
const COUNT = { by: "count", count: true, bytes: true };

View File

@ -14,14 +14,29 @@
<link rel="stylesheet" href="chrome://devtools/skin/widgets.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/skin/memory.css" type="text/css"/>
<link rel="stylesheet" href="chrome://devtools/skin/components-frame.css" type="text/css"/>
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"/>
<script type="application/javascript;version=1.8"
src="initializer.js"></script>
<link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
</head>
<body class="theme-body">
<div id="app">
</div>
<div id="app"></div>
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"
defer="true">
</script>
<script type="application/javascript;version=1.8"
src="initializer.js"
defer="true">
</script>
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/d3.js"
defer="true">
</script>
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/dagre-d3.js"
defer="true">
</script>
</body>
</html>

View File

@ -10,5 +10,6 @@ exports.dominatorTreeBreakdown = require("./reducers/dominatorTreeBreakdown");
exports.errors = require("./reducers/errors");
exports.filter = require("./reducers/filter");
exports.inverted = require("./reducers/inverted");
exports.sizes = require("./reducers/sizes");
exports.snapshots = require("./reducers/snapshots");
exports.view = require("./reducers/view");

View File

@ -11,6 +11,7 @@ DevToolsModules(
'errors.js',
'filter.js',
'inverted.js',
'sizes.js',
'snapshots.js',
'view.js',
)

View File

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { actions } = require("../constants");
const { immutableUpdate } = require("devtools/shared/DevToolsUtils");
const handlers = Object.create(null);
handlers[actions.RESIZE_SHORTEST_PATHS] = function (sizes, { size }) {
return immutableUpdate(sizes, { shortestPathsSize: size });
};
module.exports = function (sizes = { shortestPathsSize: .5 }, action) {
const handler = handlers[action.type];
return handler ? handler(sizes, action) : sizes;
};

View File

@ -26,10 +26,19 @@ this.test = makeMemoryTest(TEST_URL, function* ({ tab, panel }) {
const nameElems = [...doc.querySelectorAll(".heap-tree-item-field.heap-tree-item-name")];
is(nameElems.length, 4, "Should get 4 items, one for each coarse type");
ok(nameElems.some(e => e.textContent.trim() === "objects"), "One for coarse type 'objects'");
ok(nameElems.some(e => e.textContent.trim() === "scripts"), "One for coarse type 'scripts'");
ok(nameElems.some(e => e.textContent.trim() === "strings"), "One for coarse type 'strings'");
ok(nameElems.some(e => e.textContent.trim() === "other"), "One for coarse type 'other'");
for (let el of nameElems) {
dumpn(`Found ${el.textContent.trim()}`);
}
ok(nameElems.some(e => e.textContent.indexOf("objects") >= 0),
"One for coarse type 'objects'");
ok(nameElems.some(e => e.textContent.indexOf("scripts") >= 0),
"One for coarse type 'scripts'");
ok(nameElems.some(e => e.textContent.indexOf("strings") >= 0),
"One for coarse type 'strings'");
ok(nameElems.some(e => e.textContent.indexOf("other") >= 0),
"One for coarse type 'other'");
for (let e of nameElems) {
is(e.style.marginLeft, "0px",

View File

@ -72,6 +72,10 @@ function makeMemoryTest(url, generator) {
});
}
function dumpn(msg) {
dump(`MEMORY-TEST: ${msg}\n`);
}
/**
* Returns a promise that will resolve when the provided store matches
* the expected array. expectedStates is an array of dominatorTree states.

View File

@ -2,6 +2,7 @@
support-files =
head.js
[test_CensusTreeItem_01.html]
[test_DominatorTree_01.html]
[test_DominatorTree_02.html]
[test_DominatorTree_03.html]
@ -10,5 +11,7 @@ support-files =
[test_Heap_02.html]
[test_Heap_03.html]
[test_Heap_04.html]
[test_ShortestPaths_01.html]
[test_ShortestPaths_02.html]
[test_Toolbar_01.html]
[test_Toolbar_02.html]

View File

@ -28,6 +28,7 @@ var {
const {
getBreakdownDisplayData,
getDominatorTreeBreakdownDisplayData,
L10N,
} = require("devtools/client/memory/utils");
var models = require("devtools/client/memory/models");
@ -35,8 +36,10 @@ var models = require("devtools/client/memory/models");
var React = require("devtools/client/shared/vendor/react");
var ReactDOM = require("devtools/client/shared/vendor/react-dom");
var Heap = React.createFactory(require("devtools/client/memory/components/heap"));
var CensusTreeItem = React.createFactory(require("devtools/client/memory/components/census-tree-item"));
var DominatorTreeComponent = React.createFactory(require("devtools/client/memory/components/dominator-tree"));
var DominatorTreeItem = React.createFactory(require("devtools/client/memory/components/dominator-tree-item"));
var ShortestPaths = React.createFactory(require("devtools/client/memory/components/shortest-paths"));
var Toolbar = React.createFactory(require("devtools/client/memory/components/toolbar"));
// All tests are asynchronous.
@ -44,6 +47,33 @@ SimpleTest.waitForExplicitFinish();
var noop = () => {};
var TEST_CENSUS_TREE_ITEM_PROPS = Object.freeze({
item: Object.freeze({
bytes: 10,
count: 1,
totalBytes: 10,
totalCount: 1,
name: "foo",
children: [
Object.freeze({
bytes: 10,
count: 1,
totalBytes: 10,
totalCount: 1,
name: "bar",
})
]
}),
depth: 0,
arrow: ">",
focused: true,
getPercentBytes: () => 50,
getPercentCount: () => 50,
showSign: false,
onViewSourceInDebugger: noop,
inverted: false,
});
// Counter for mock DominatorTreeNode ids.
var TEST_NODE_ID_COUNTER = 0;
@ -105,6 +135,21 @@ var TEST_DOMINATOR_TREE_PROPS = Object.freeze({
onCollapse: noop,
});
var TEST_SHORTEST_PATHS_PROPS = Object.freeze({
graph: Object.freeze({
nodes: [
{ id: 1, label: ["other", "SomeType"] },
{ id: 2, label: ["other", "SomeType"] },
{ id: 3, label: ["other", "SomeType"] },
],
edges: [
{ from: 1, to: 2, name: "1->2" },
{ from: 1, to: 3, name: "1->3" },
{ from: 2, to: 3, name: "2->3" },
],
}),
});
var TEST_HEAP_PROPS = Object.freeze({
onSnapshotClick: noop,
onLoadMoreSiblings: noop,
@ -146,6 +191,8 @@ var TEST_HEAP_PROPS = Object.freeze({
creationTime: 0,
state: snapshotState.SAVED_CENSUS,
}),
sizes: Object.freeze({ shortestPathsSize: .5 }),
onShortestPathsResize: noop,
});
var TEST_TOOLBAR_PROPS = Object.freeze({

View File

@ -0,0 +1,65 @@
<!DOCTYPE HTML>
<html>
<!--
Test that children pointers show up at the correct times.
-->
<head>
<meta charset="utf-8">
<title>Tree component test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<!-- Give the container height so that the whole tree is rendered. -->
<div id="container" style="height: 900px;"></div>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const container = document.getElementById("container");
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
inverted: true,
depth: 0,
})), container);
ok(!container.querySelector(".children-pointer"),
"Don't show children pointer for roots when we are inverted");
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
inverted: true,
depth: 1,
})), container);
ok(container.querySelector(".children-pointer"),
"Do show children pointer for non-roots when we are inverted");
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
inverted: false,
item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: undefined }),
})), container);
ok(!container.querySelector(".children-pointer"),
"Don't show children pointer when non-inverted and no children");
yield renderComponent(CensusTreeItem(immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS, {
inverted: false,
depth: 0,
item: immutableUpdate(TEST_CENSUS_TREE_ITEM_PROPS.item, { children: [{}] }),
})), container);
ok(container.querySelector(".children-pointer"),
"Do show children pointer when non-inverted and have children");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,112 @@
<!DOCTYPE HTML>
<html>
<!--
Test that the ShortestPaths component properly renders a graph of the merged shortest paths.
-->
<head>
<meta charset="utf-8">
<title>Tree component test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/d3.js">
</script>
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/dagre-d3.js">
</script>
</head>
<body>
<!-- Give the container height so that the whole tree is rendered. -->
<div id="container" style="height: 900px;"></div>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const container = document.getElementById("container");
yield renderComponent(ShortestPaths(TEST_SHORTEST_PATHS_PROPS), container);
let found1 = false;
let found2 = false;
let found3 = false;
let found1to2 = false;
let found1to3 = false;
let found2to3 = false;
const tspans = [...container.querySelectorAll("tspan")];
for (let el of tspans) {
const text = el.textContent.trim();
dumpn("tspan's text = " + text);
switch (text) {
// Nodes
case "other SomeType @ 0x1": {
ok(!found1, "Should only find node 1 once");
found1 = true;
break;
}
case "other SomeType @ 0x2": {
ok(!found2, "Should only find node 2 once");
found2 = true;
break;
}
case "other SomeType @ 0x3": {
ok(!found3, "Should only find node 3 once");
found3 = true;
break;
}
// Edges
case "1->2": {
ok(!found1to2, "Should only find edge 1->2 once");
found1to2 = true;
break;
}
case "1->3": {
ok(!found1to3, "Should only find edge 1->3 once");
found1to3 = true;
break;
}
case "2->3": {
ok(!found2to3, "Should only find edge 2->3 once");
found2to3 = true;
break;
}
// Unexpected
default: {
ok(false, `Unexpected tspan: ${text}`);
break;
}
}
}
ok(found1, "Should have rendered node 1");
ok(found2, "Should have rendered node 2");
ok(found3, "Should have rendered node 3");
ok(found1to2, "Should have rendered edge 1->2");
ok(found1to3, "Should have rendered edge 1->3");
ok(found2to3, "Should have rendered edge 2->3");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,45 @@
<!DOCTYPE HTML>
<html>
<!--
Test that the ShortestPaths component renders a suggestion to select a node when there is no graph.
-->
<head>
<meta charset="utf-8">
<title>Tree component test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/d3.js">
</script>
<script type="application/javascript"
src="chrome://devtools/content/shared/vendor/dagre-d3.js">
</script>
</head>
<body>
<!-- Give the container height so that the whole tree is rendered. -->
<div id="container" style="height: 900px;"></div>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
window.onload = Task.async(function* () {
try {
const container = document.getElementById("container");
yield renderComponent(ShortestPaths(immutableUpdate(TEST_SHORTEST_PATHS_PROPS,
{ graph: null })),
container);
ok(container.textContent.indexOf(L10N.getStr("shortest-paths.select-node")) !== -1,
"The node selection prompt is displayed");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
</script>
</pre>
</body>
</html>

View File

@ -212,8 +212,17 @@ const DOM = {
container.className = "marker-details-stack";
container.appendChild(labelName);
// Workaround for profiles that have looping stack traces. See
// bug 1246555.
let wasAsyncParent = false;
let seen = new Set();
while (frameIndex > 0) {
if (seen.has(frameIndex)) {
break;
}
seen.add(frameIndex);
let frame = frames[frameIndex];
let url = frame.source;
let displayName = frame.functionDisplayName;

View File

@ -58,9 +58,21 @@ function* spawnTest() {
return m.start;
}, 0);
// Override the timestamp marker's stack with our own recursive stack, which
// can happen for unknown reasons (bug 1246555); we should not cause a crash
// when attempting to render a recursive stack trace
let timestampMarker = markers.find(m => m.name === "ConsoleTime");
ok(typeof timestampMarker.stack === "number", "ConsoleTime marker has a stack before overwriting.");
let frames = PerformanceController.getCurrentRecording().getFrames();
let frameIndex = timestampMarker.stack = frames.length;
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 1});
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex + 2 });
frames.push({ line: 1, column: 1, source: "file.js", functionDisplayName: "test", parent: frameIndex });
const tests = {
ConsoleTime: function (marker) {
info("Got `ConsoleTime` marker with data: " + JSON.stringify(marker));
ok(marker.stack === frameIndex, "Should have the ConsoleTime marker with recursive stack");
shouldHaveStack($, "startStack", marker);
shouldHaveStack($, "endStack", marker);
shouldHaveLabel($, "Timer Name:", "!!!", marker);

View File

@ -325,7 +325,7 @@ pref("devtools.editor.enableCodeFolding", true);
pref("devtools.editor.autocomplete", true);
// Enable the Font Inspector
pref("devtools.fontinspector.enabled", true);
pref("devtools.fontinspector.enabled", false);
// Pref to store the browser version at the time of a telemetry ping for an
// opened developer tool. This allows us to ping telemetry just once per browser

View File

@ -0,0 +1,5 @@
{
"globals": {
"define": true,
}
}

View File

@ -0,0 +1,138 @@
/* 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/. */
// A box with a start and a end pane, separated by a dragable splitter that
// allows the user to resize the relative widths of the panes.
//
// +-----------------------+---------------------+
// | | |
// | | |
// | S |
// | Start Pane p End Pane |
// | l |
// | i |
// | t |
// | t |
// | e |
// | r |
// | | |
// | | |
// +-----------------------+---------------------+
const {
DOM: dom,
createClass,
PropTypes,
} = require("devtools/client/shared/vendor/react");
const { assert } = require("devtools/shared/DevToolsUtils");
module.exports = createClass({
displayName: "HSplitBox",
getDefaultProps() {
return {
startWidth: 0.5,
minStartWidth: "20px",
minEndWidth: "20px",
};
},
getInitialState() {
return {
mouseDown: false
};
},
propTypes: {
// The contents of the start pane.
start: PropTypes.any.isRequired,
// The contents of the end pane.
end: PropTypes.any.isRequired,
// The relative width of the start pane, expressed as a number between 0 and
// 1. The relative width of the end pane is 1 - startWidth. For example,
// with startWidth = .5, both panes are of equal width; with startWidth =
// .25, the start panel will take up 1/4 width and the end panel will take
// up 3/4 width.
startWidth: PropTypes.number,
// A minimum css width value for the start and end panes.
minStartWidth: PropTypes.any,
minEndWidth: PropTypes.any,
// A callback fired when the user drags the splitter to resize the relative
// pane widths. The function is passed the startWidth value that would put
// the splitter underneath the users mouse.
onResize: PropTypes.func.isRequired,
},
_onMouseDown(event) {
this.setState({ mouseDown: true });
event.preventDefault();
},
_onMouseUp(event) {
this.setState({ mouseDown: false });
event.preventDefault();
},
_onMouseMove(event) {
if (!this.state.mouseDown) {
return;
}
const rect = this.refs.box.getBoundingClientRect();
const { left, right } = rect;
const width = right - left;
const relative = event.clientX - left;
this.props.onResize(relative / width);
event.preventDefault();
},
componentDidMount() {
document.defaultView.top.addEventListener("mouseup", this._onMouseUp, false);
document.defaultView.top.addEventListener("mousemove", this._onMouseMove, false);
},
componentWillUnmount() {
document.defaultView.top.removeEventListener("mouseup", this._onMouseUp, false);
document.defaultView.top.removeEventListener("mousemove", this._onMouseMove, false);
},
render() {
const { start, end, startWidth, minStartWidth, minEndWidth } = this.props;
assert(0 <= startWidth && startWidth <= 1,
"0 <= this.props.startWidth <= 1");
return dom.div(
{
className: "h-split-box",
ref: "box",
},
dom.div(
{
className: "h-split-box-pane",
style: { flex: startWidth, minWidth: minStartWidth },
},
start
),
dom.div({
className: "h-split-box-splitter",
onMouseDown: this._onMouseDown,
}),
dom.div(
{
className: "h-split-box-pane",
style: { flex: 1 - startWidth, minWidth: minEndWidth },
},
end
)
);
}
});

View File

@ -4,8 +4,13 @@
# 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/.
DIRS += [
'reps',
]
DevToolsModules(
'frame.js',
'h-split-box.js',
'tree.js',
)

View File

@ -0,0 +1,208 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { Rep } = createFactories(require("./rep"));
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const DOM = React.DOM;
/**
* Renders an array. The array is enclosed by left and right bracket
* and the max number of rendered items depends on the current mode.
*/
let ArrayRep = React.createClass({
displayName: "ArrayRep",
render: function() {
let mode = this.props.mode || "short";
let object = this.props.object;
let items;
if (mode == "tiny") {
items = DOM.span({className: "length"}, object.length);
} else {
let max = (mode == "short") ? 3 : 300;
items = this.arrayIterator(object, max);
}
return (
ObjectBox({
className: "array",
onClick: this.onToggleProperties},
DOM.a({
className: "objectLink",
onclick: this.onClickBracket},
DOM.span({
className: "arrayLeftBracket",
role: "presentation"},
"["
)
),
items,
DOM.a({
className: "objectLink",
onclick: this.onClickBracket},
DOM.span({
className: "arrayRightBracket",
role: "presentation"},
"]"
)
),
DOM.span({
className: "arrayProperties",
role: "group"}
)
)
);
},
getTitle: function(object, context) {
return "[" + object.length + "]";
},
arrayIterator: function(array, max) {
let items = [];
let delim;
for (let i = 0; i < array.length && i <= max; i++) {
try {
let value = array[i];
delim = (i == array.length - 1 ? "" : ", ");
if (value === array) {
items.push(Reference({
key: i,
object: value,
delim: delim
}));
} else {
items.push(ItemRep({
key: i,
object: value,
delim: delim
}));
}
} catch (exc) {
items.push(ItemRep({
object: exc,
delim: delim,
key: i
}));
}
}
if (array.length > max + 1) {
items.pop();
items.push(Caption({
key: "more",
object: "more...",
}));
}
return items;
},
/**
* Returns true if the passed object is an array with additional (custom)
* properties, otherwise returns false. Custom properties should be
* displayed in extra expandable section.
*
* Example array with a custom property.
* let arr = [0, 1];
* arr.myProp = "Hello";
*
* @param {Array} array The array object.
*/
hasSpecialProperties: function(array) {
function isInteger(x) {
let y = parseInt(x, 10);
if (isNaN(y)) {
return false;
}
return x === y.toString();
}
let props = Object.getOwnPropertyNames(array);
for (let i = 0; i < props.length; i++) {
let p = props[i];
// Valid indexes are skipped
if (isInteger(p)) {
continue;
}
// Ignore standard 'length' property, anything else is custom.
if (p != "length") {
return true;
}
}
return false;
},
// Event Handlers
onToggleProperties: function(event) {
},
onClickBracket: function(event) {
}
});
/**
* Renders array item. Individual values are separated by a comma.
*/
let ItemRep = React.createFactory(React.createClass({
displayName: "ItemRep",
render: function() {
let object = this.props.object;
let delim = this.props.delim;
return (
DOM.span({},
Rep({object: object}),
delim
)
);
}
}));
/**
* Renders cycle references in an array.
*/
let Reference = React.createFactory(React.createClass({
displayName: "Reference",
render: function() {
let tooltip = "Circular reference";
return (
DOM.span({title: tooltip},
"[...]")
);
}
}));
function supportsObject(object, type) {
return Array.isArray(object) ||
Object.prototype.toString.call(object) === "[object Arguments]";
}
// Exports from this module
exports.ArrayRep = {
rep: ArrayRep,
supportsObject: supportsObject
};
});

View File

@ -0,0 +1,31 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a caption. This template is used by other components
* that needs to distinguish between a simple text/value and a label.
*/
const Caption = React.createClass({
displayName: "Caption",
render: function() {
return (
DOM.span({"className": "caption"}, this.props.object)
);
},
});
// Exports from this module
exports.Caption = Caption;
});

View File

@ -0,0 +1,20 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'array.js',
'caption.js',
'null.js',
'number.js',
'object-box.js',
'object-link.js',
'object.js',
'rep-utils.js',
'rep.js',
'reps.css',
'string.js',
'undefined.js',
)

View File

@ -0,0 +1,45 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders null value
*/
const Null = React.createClass({
displayName: "NullRep",
render: function() {
return (
ObjectBox({className: "null"},
"null"
)
);
},
});
function supportsObject(object, type) {
if (object && object.type && object.type == "null") {
return true;
}
return (object == null);
}
// Exports from this module
exports.Null = {
rep: Null,
supportsObject: supportsObject
};
});

View File

@ -0,0 +1,46 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders a number
*/
const Number = React.createClass({
displayName: "Number",
render: function() {
let value = this.props.object;
return (
ObjectBox({className: "number"},
this.stringify(value)
)
);
},
stringify: function(object) {
return (Object.is(object, -0) ? "-0" : String(object));
},
});
function supportsObject(object, type) {
return type == "boolean" || type == "number";
}
// Exports from this module
exports.Number = {
rep: Number,
supportsObject: supportsObject
};
});

View File

@ -0,0 +1,35 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a box for given object.
*/
const ObjectBox = React.createClass({
displayName: "ObjectBox",
render: function() {
let className = this.props.className;
let boxClassName = className ? " objectBox-" + className : "";
return (
DOM.span({className: "objectBox" + boxClassName, role: "presentation"},
this.props.children
)
);
}
});
// Exports from this module
exports.ObjectBox = ObjectBox;
});

View File

@ -0,0 +1,36 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const DOM = React.DOM;
/**
* Renders a link for given object.
*/
const ObjectLink = React.createClass({
displayName: "ObjectLink",
render: function() {
let className = this.props.className;
let objectClassName = className ? " objectLink-" + className : "";
let linkClassName = "objectLink" + objectClassName + " a11yFocus";
return (
DOM.a({className: linkClassName, _repObject: this.props.object},
this.props.children
)
);
}
});
// Exports from this module
exports.ObjectLink = ObjectLink;
});

View File

@ -0,0 +1,190 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
const { Caption } = createFactories(require("./caption"));
// Shortcuts
const DOM = React.DOM;
/**
* Renders an object. An object is represented by a list of its
* properties enclosed in curly brackets.
*/
const Obj = React.createClass({
displayName: "Obj",
render: function() {
let object = this.props.object;
let props = this.shortPropIterator(object);
return (
ObjectBox({className: "object"},
DOM.span({className: "objectTitle"}, this.getTitle(object)),
DOM.span({className: "objectLeftBrace", role: "presentation"}, "{"),
props,
DOM.span({className: "objectRightBrace"}, "}")
)
);
},
getTitle: function() {
return "";
},
longPropIterator: function(object) {
try {
return this.propIterator(object, 100);
} catch (err) {
console.error(err);
}
return [];
},
shortPropIterator: function(object) {
try {
return this.propIterator(object, 3);
} catch (err) {
console.error(err);
}
return [];
},
propIterator: function(object, max) {
function isInterestingProp(t, value) {
return (t == "boolean" || t == "number" || (t == "string" && value) ||
(t == "object" && value && value.toString));
}
// Work around https://bugzilla.mozilla.org/show_bug.cgi?id=945377
if (Object.prototype.toString.call(object) === "[object Generator]") {
object = Object.getPrototypeOf(object);
}
// Object members with non-empty values are preferred since it gives the
// user a better overview of the object.
let props = [];
this.getProps(props, object, max, isInterestingProp);
if (props.length <= max) {
// There are not enough props yet (or at least, not enough props to
// be able to know whether we should print "more..." or not).
// Let's display also empty members and functions.
this.getProps(props, object, max, function(t, value) {
return !isInterestingProp(t, value);
});
}
if (props.length > max) {
props.pop();
props.push(Caption({
key: "more",
object: "more...",
}));
} else if (props.length > 0) {
// Remove the last comma.
props[props.length - 1] = React.cloneElement(
props[props.length - 1], { delim: "" });
}
return props;
},
getProps: function(props, object, max, filter) {
max = max || 3;
if (!object) {
return [];
}
let mode = this.props.mode;
try {
for (let name in object) {
if (props.length > max) {
return [];
}
let value;
try {
value = object[name];
} catch (exc) {
continue;
}
let t = typeof value;
if (filter(t, value)) {
props.push(PropRep({
key: name,
mode: mode,
name: name,
object: value,
equal: ": ",
delim: ", ",
}));
}
}
} catch (err) {
console.error(err);
}
return [];
},
});
/**
* Renders object property, name-value pair.
*/
let PropRep = React.createFactory(React.createClass({
displayName: "PropRep",
render: function() {
let { Rep } = createFactories(require("./rep"));
let object = this.props.object;
let mode = this.props.mode;
return (
DOM.span({},
DOM.span({
"className": "nodeName"},
this.props.name
),
DOM.span({
"className": "objectEqual",
role: "presentation"},
this.props.equal
),
Rep({
object: object,
mode: mode
}),
DOM.span({
"className": "objectComma",
role: "presentation"},
this.props.delim
)
)
);
}
}));
function supportsObject(object, type) {
return true;
}
// Exports from this module
exports.Obj = {
rep: Obj,
supportsObject: supportsObject
};
});

View File

@ -0,0 +1,29 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
/**
* Create React factories for given arguments.
* Example:
* const { Rep } = createFactories(require("./rep"));
*/
function createFactories(args) {
let result = {};
for (let p in args) {
result[p] = React.createFactory(args[p]);
}
return result;
}
// Exports from this module
exports.createFactories = createFactories;
});

View File

@ -0,0 +1,86 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
// Load all existing rep templates
const { Undefined } = require("./undefined");
const { Null } = require("./null");
const { StringRep } = require("./string");
const { Number } = require("./number");
const { ArrayRep } = require("./array");
const { Obj } = require("./object");
// List of all registered template.
// XXX there should be a way for extensions to register a new
// or modify an existing rep.
let reps = [Undefined, Null, StringRep, Number, ArrayRep, Obj];
let defaultRep;
/**
* Generic rep that is using for rendering native JS types or an object.
* The right template used for rendering is picked automatically according
* to the current value type. The value must be passed is as 'object'
* property.
*/
const Rep = React.createClass({
displayName: "Rep",
render: function() {
let rep = getRep(this.props.object);
return rep(this.props);
},
});
// Helpers
/**
* Return a rep object that is responsible for rendering given
* object.
*
* @param object {Object} Object to be rendered in the UI. This
* can be generic JS object as well as a grip (handle to a remote
* debuggee object).
*/
function getRep(object) {
let type = typeof object;
if (type == "object" && object instanceof String) {
type = "string";
}
if (isGrip(object)) {
type = object.class;
}
for (let i = 0; i < reps.length; i++) {
let rep = reps[i];
try {
// supportsObject could return weight (not only true/false
// but a number), which would allow to priorities templates and
// support better extensibility.
if (rep.supportsObject(object, type)) {
return React.createFactory(rep.rep);
}
} catch (err) {
console.error("reps.getRep; EXCEPTION ", err, err);
}
}
return React.createFactory(defaultRep.rep);
}
function isGrip(object) {
return object && object.actor;
}
// Exports from this module
exports.Rep = Rep;
});

View File

@ -0,0 +1,101 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders a string. String value is enclosed within quotes.
*/
const StringRep = React.createClass({
displayName: "StringRep",
render: function() {
let text = this.props.object;
let member = this.props.member;
if (member && member.open) {
return (
ObjectBox({className: "string"},
"\"" + text + "\""
)
);
}
return (
ObjectBox({className: "string"},
"\"" + cropMultipleLines(text) + "\""
)
);
},
});
// Helpers
function escapeNewLines(value) {
return value.replace(/\r/gm, "\\r").replace(/\n/gm, "\\n");
}
function cropMultipleLines(text, limit) {
return escapeNewLines(cropString(text, limit));
}
function cropString(text, limit, alternativeText) {
if (!alternativeText) {
alternativeText = "...";
}
// Make sure it's a string.
text = text + "";
// Use default limit if necessary.
if (!limit) {
limit = 50;
}
// Crop the string only if a limit is actually specified.
if (limit <= 0) {
return text;
}
// Set the limit at least to the length of the alternative text
// plus one character of the original text.
if (limit <= alternativeText.length) {
limit = alternativeText.length + 1;
}
let halfLimit = (limit - alternativeText.length) / 2;
if (text.length > limit) {
return text.substr(0, Math.ceil(halfLimit)) + alternativeText +
text.substr(text.length - Math.floor(halfLimit));
}
return text;
}
function isCropped(value) {
let cropLength = 50;
return typeof value == "string" && value.length > cropLength;
}
function supportsObject(object, type) {
return (type == "string");
}
// Exports from this module
exports.StringRep = {
rep: StringRep,
supportsObject: supportsObject,
isCropped: isCropped
};
});

View File

@ -0,0 +1,45 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// Make this available to both AMD and CJS environments
define(function(require, exports, module) {
// Dependencies
const React = require("devtools/client/shared/vendor/react");
const { createFactories } = require("./rep-utils");
const { ObjectBox } = createFactories(require("./object-box"));
/**
* Renders undefined value
*/
const Undefined = React.createClass({
displayName: "UndefinedRep",
render: function() {
return (
ObjectBox({className: "undefined"},
"undefined"
)
);
},
});
function supportsObject(object, type) {
if (object && object.type && object.type == "undefined") {
return true;
}
return (type == "undefined");
}
// Exports from this module
exports.Undefined = {
rep: Undefined,
supportsObject: supportsObject
};
});

View File

@ -2,6 +2,7 @@
support-files =
head.js
[test_HSplitBox_01.html]
[test_frame_01.html]
[test_frame_02.html]
[test_tree_01.html]

View File

@ -0,0 +1,122 @@
<!DOCTYPE HTML>
<html>
<!--
Basic tests for the HSplitBox component.
-->
<head>
<meta charset="utf-8">
<title>Tree component test</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript "src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/components-h-split-box.css" type="text/css"/>
<style>
html {
--theme-splitter-color: black;
}
</style>
</head>
<body>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script type="application/javascript;version=1.8">
const FUDGE_FACTOR = .1;
function aboutEq(a, b) {
dumpn(`Checking ${a} ~= ${b}`);
return Math.abs(a - b) < FUDGE_FACTOR;
}
window.onload = Task.async(function* () {
try {
const React = browserRequire("devtools/client/shared/vendor/react");
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
let HSplitBox = React.createFactory(browserRequire("devtools/client/shared/components/h-split-box"));
ok(HSplitBox, "Should get HSplitBox");
const newSizes = [];
const box = ReactDOM.render(HSplitBox({
start: "hello!",
end: "world!",
startWidth: .5,
onResize(newSize) {
newSizes.push(newSize);
},
}), window.document.body);
// Test that we properly rendered our two panes.
let panes = document.querySelectorAll(".h-split-box-pane");
is(panes.length, 2, "Should get two panes");
is(panes[0].style.flexGrow, "0.5", "Each pane should have .5 width");
is(panes[1].style.flexGrow, "0.5", "Each pane should have .5 width");
is(panes[0].textContent.trim(), "hello!", "First pane should be hello");
is(panes[1].textContent.trim(), "world!", "Second pane should be world");
// Now change the left width and assert that the changes are reflected.
yield setProps(box, { startWidth: .25 });
panes = document.querySelectorAll(".h-split-box-pane");
is(panes.length, 2, "Should still have two panes");
is(panes[0].style.flexGrow, "0.25", "First pane's width should be .25");
is(panes[1].style.flexGrow, "0.75", "Second pane's width should be .75");
// Mouse moves without having grabbed the splitter should have no effect.
let container = document.querySelector(".h-split-box");
ok(container, "Should get our container .h-split-box");
const { left, top, width } = container.getBoundingClientRect();
const middle = left + width / 2;
const oneQuarter = left + width / 4;
const threeQuarters = left + 3 * width / 4;
synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
is(newSizes.length, 0, "Mouse moves without dragging the splitter should have no effect");
// Send a mouse down on the splitter, and then move the mouse a couple
// times. Now we should get resizes.
const splitter = document.querySelector(".h-split-box-splitter");
ok(splitter, "Should get our splitter");
synthesizeMouseAtCenter(splitter, { button: 1, type: "mousedown" }, window);
function mouseMove(clientX) {
const event = new MouseEvent("mousemove", { clientX });
document.defaultView.top.dispatchEvent(event);
}
mouseMove(middle);
is(newSizes.length, 1, "Should get 1 resize");
ok(aboutEq(newSizes[0], .5), "New size should be ~.5");
mouseMove(left);
is(newSizes.length, 2, "Should get 2 resizes");
ok(aboutEq(newSizes[1], 0), "New size should be ~0");
mouseMove(oneQuarter);
is(newSizes.length, 3, "Sould get 3 resizes");
ok(aboutEq(newSizes[2], .25), "New size should be ~.25");
mouseMove(threeQuarters);
is(newSizes.length, 4, "Should get 4 resizes");
ok(aboutEq(newSizes[3], .75), "New size should be ~.75");
synthesizeMouseAtCenter(splitter, { button: 1, type: "mouseup" }, window);
// Now that we have let go of the splitter, mouse moves should not result in resizes.
synthesizeMouse(container, middle, top, { type: "mousemove" }, window);
is(newSizes.length, 4, "Should still have 4 resizes");
} catch(e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
} finally {
SimpleTest.finish();
}
});
</script>
</pre>
</body>
</html>

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