merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-11-24 14:06:00 +01:00
commit 5969f2ac89
85 changed files with 2320 additions and 578 deletions

View File

@ -1642,10 +1642,10 @@ pref("shumway.disabled", true);
pref("image.mem.max_decoded_image_kb", 256000);
pref("loop.enabled", true);
pref("loop.server", "https://loop.services.mozilla.com");
pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.seenToS", "unseen");
pref("loop.gettingStarted.seen", false);
pref("loop.gettingStarted.url", "https://bugzilla.mozilla.org/show_bug.cgi?id=1099462");
pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start");
pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://hello.firefox.com/legal/terms/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");

View File

@ -99,16 +99,6 @@ let wrapper = {
iframe: null,
init: function (url, urlParams) {
let weave = Cc["@mozilla.org/weave/service;1"]
.getService(Ci.nsISupports)
.wrappedJSObject;
// Don't show about:accounts with FxA disabled.
if (!weave.fxAccountsEnabled) {
document.body.remove();
return;
}
// If a master-password is enabled, we want to encourage the user to
// unlock it. Things still work if not, but the user will probably need
// to re-auth next startup (in which case we will get here again and

View File

@ -720,7 +720,6 @@
enablehistory="true"
maxrows="6"
newlines="stripsurroundingwhitespace"
oninput="gBrowser.userTypedValue = this.value;"
ontextentered="this.handleCommand(param);"
ontextreverted="return this.handleRevert();"
pageproxystate="invalid"

View File

@ -193,6 +193,9 @@
() => {
this.swapDocShells(cb);
chatbar.focus();
this.close();
// chatboxForURL is a map of URL -> chatbox used to avoid opening
// duplicate chat windows. Ensure reattached chat windows aren't
// registered with about:blank as their URL, otherwise reattaching
@ -200,8 +203,6 @@
chatbar.chatboxForURL.delete("about:blank");
chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
chatbar.focus();
this.close();
deferred.resolve(cb);
}
);
@ -527,7 +528,9 @@
let cb = this.chatboxForURL.get(aURL);
if (cb) {
cb = cb.get();
if (cb.parentNode) {
// A chatbox is still alive to us when it's parented and still has
// content.
if (cb.parentNode && cb.contentWindow) {
this.showChat(cb, aMode);
if (aCallback) {
if (cb._callbacks == null) {
@ -646,6 +649,7 @@
<parameter name="aOptions"/>
<body><![CDATA[
let deferred = Promise.defer();
let chatbar = this;
let options = "";
for (let name in aOptions)
options += "," + name + "=" + aOptions[name];
@ -661,6 +665,8 @@
let otherChatbox = otherWin.document.getElementById("chatter");
aChatbox.swapDocShells(otherChatbox);
aChatbox.close();
chatbar.chatboxForURL.set(aChatbox.src, Cu.getWeakReference(otherChatbox));
deferred.resolve(otherChatbox);
}, true);
return deferred.promise;

View File

@ -122,6 +122,10 @@ skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test un
[browser_alltabslistener.js]
[browser_autocomplete_a11y_label.js]
skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
[browser_autocomplete_no_title.js]
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_autocomplete_autoselect.js]
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_backButtonFitts.js]
skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
[browser_blob-channelname.js]
@ -288,6 +292,8 @@ skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and
[browser_bug1015721.js]
skip-if = os == 'win' || e10s # Bug 1056146 - zoom tests use FullZoomHelper and break in e10s
[browser_bug1064280_changeUrlInPinnedTab.js]
[browser_bug1070778.js]
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
[browser_canonizeURL.js]
skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only
[browser_contentAreaClick.js]

View File

@ -31,7 +31,7 @@ let tests = [
function revert(next) {
loadTabInWindow(window, function (tab) {
gURLBar.handleRevert();
is(gURLBar.value, "example.com", "URL bar had user/pass stripped after reverting");
is(gURLBar.textValue, "example.com", "URL bar had user/pass stripped after reverting");
gBrowser.removeTab(tab);
next();
});
@ -44,7 +44,7 @@ let tests = [
loadTabInWindow(win, function () {
openToolbarCustomizationUI(function () {
closeToolbarCustomizationUI(function () {
is(win.gURLBar.value, "example.com", "URL bar had user/pass stripped after customize");
is(win.gURLBar.textValue, "example.com", "URL bar had user/pass stripped after customize");
win.close();
next();
}, win);
@ -59,7 +59,7 @@ let tests = [
// error.
tab.linkedBrowser.loadURI("http://test1.example.com");
tab.linkedBrowser.stop();
is(gURLBar.value, "example.com", "URL bar had user/pass stripped after load error");
is(gURLBar.textValue, "example.com", "URL bar had user/pass stripped after load error");
gBrowser.removeTab(tab);
next();
});
@ -76,7 +76,7 @@ function loadTabInWindow(win, callback) {
return;
tab.linkedBrowser.removeEventListener("load", listener, true);
is(win.gURLBar.value, "example.com", "URL bar had user/pass stripped initially");
is(win.gURLBar.textValue, "example.com", "URL bar had user/pass stripped initially");
callback(tab);
}, true);
}

View File

@ -43,11 +43,14 @@ add_task(function* () {
let result = yield promise_first_result("open a search");
isnot(result, null, "Should have a result");
is(result.getAttribute("url"),
`moz-action:searchengine,{"engineName":"MozSearch","input":"open a search","searchQuery":"open a search"}`,
"Result should be a moz-action: for the correct search engine");
is(result.hasAttribute("image"), false, "Result shouldn't have an image attribute");
let tabPromise = promiseTabLoaded(gBrowser.selectedTab);
EventUtils.synthesizeMouseAtCenter(result, {});
yield tabPromise;
is(gBrowser.selectedBrowser.currentURI.spec, "http://example.com/?q=open+a+search");
is(gBrowser.selectedBrowser.currentURI.spec, "http://example.com/?q=open+a+search", "Correct URL should be loaded");
});

View File

@ -0,0 +1,59 @@
function repeat(limit, func) {
for (let i = 0; i < limit; i++) {
func(i);
}
}
function* promiseAutoComplete(inputText) {
gURLBar.focus();
gURLBar.value = inputText.slice(0, -1);
EventUtils.synthesizeKey(inputText.slice(-1), {});
yield promiseSearchComplete();
}
function is_selected(index) {
is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
}
add_task(function*() {
registerCleanupFunction(promiseClearHistory);
let visits = [];
repeat(10, i => {
visits.push({
uri: makeURI("http://example.com/autocomplete/?" + i),
});
});
yield PlacesTestUtils.addVisits(visits);
yield promiseAutoComplete("example.com/autocomplete");
let popup = gURLBar.popup;
let results = popup.richlistbox.children;
// 1 extra for the current search engine match
is(results.length, 11, "Should get 11 results");
is_selected(0);
info("Key Down to select the next item");
EventUtils.synthesizeKey("VK_DOWN", {});
is_selected(1);
info("Key Down 11 times should wrap around all the way around");
repeat(11, () => EventUtils.synthesizeKey("VK_DOWN", {}));
is_selected(1);
info("Key Up 11 times should wrap around the other way");
repeat(11, () => EventUtils.synthesizeKey("VK_UP", {}));
is_selected(1);
info("Page Up will go up the list, but not wrap");
EventUtils.synthesizeKey("VK_PAGE_UP", {})
is_selected(0);
info("Page Up again will wrap around to the end of the list");
EventUtils.synthesizeKey("VK_PAGE_UP", {})
is_selected(10);
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield promisePopupHidden(gURLBar.popup);
});

View File

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function* check_title(inputText, expectedTitle) {
gURLBar.focus();
gURLBar.value = inputText.slice(0, -1);
EventUtils.synthesizeKey(inputText.slice(-1) , {});
yield promiseSearchComplete();
ok(gURLBar.popup.richlistbox.children.length > 1, "Should get at least 2 results");
let result = gURLBar.popup.richlistbox.children[1];
is(result._title.textContent, expectedTitle, "Result title should be as expected");
}
add_task(function*() {
// This test is only relevant if UnifiedComplete is enabled.
if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete"))
return;
let uri = NetUtil.newURI("http://bug1060642.example.com/beards/are/pretty/great");
yield PlacesTestUtils.addVisits([{uri: uri, title: ""}]);
yield check_title("bug1060642", "bug1060642.example.com");
gURLBar.popup.hidePopup();
yield promisePopupHidden(gURLBar.popup);
});

View File

@ -35,7 +35,7 @@ add_task(function* test_switchtab_override() {
onSearchComplete.apply(gURLBar);
deferred.resolve();
}
gURLBar.focus();
gURLBar.value = "dummy_pag";
EventUtils.synthesizeKey("e" , {});
@ -43,7 +43,6 @@ add_task(function* test_switchtab_override() {
info("Select second autocomplete popup entry");
EventUtils.synthesizeKey("VK_DOWN" , {});
EventUtils.synthesizeKey("VK_DOWN" , {});
ok(/moz-action:switchtab/.test(gURLBar.value), "switch to tab entry found");
info("Override switch-to-tab");
@ -61,6 +60,7 @@ add_task(function* test_switchtab_override() {
EventUtils.synthesizeKey("VK_SHIFT" , { type: "keydown" });
EventUtils.synthesizeKey("VK_RETURN" , { });
info(`gURLBar.value = ${gURLBar.value}`);
EventUtils.synthesizeKey("VK_SHIFT" , { type: "keyup" });
yield deferred.promise;

View File

@ -21,26 +21,13 @@ add_task(function* test_switchtab_override_keynav() {
return promiseClearHistory();
});
info("Wait for autocomplete")
let searchDeferred = Promise.defer();
let onSearchComplete = gURLBar.onSearchComplete;
registerCleanupFunction(() => {
gURLBar.onSearchComplete = onSearchComplete;
});
gURLBar.onSearchComplete = function () {
ok(gURLBar.popupOpen, "The autocomplete popup is correctly open");
onSearchComplete.apply(gURLBar);
searchDeferred.resolve();
}
gURLBar.focus();
gURLBar.value = "dummy_pag";
EventUtils.synthesizeKey("e" , {});
yield searchDeferred.promise;
yield promiseSearchComplete();
info("Select second autocomplete popup entry");
EventUtils.synthesizeKey("VK_DOWN" , {});
EventUtils.synthesizeKey("VK_DOWN" , {});
ok(/moz-action:switchtab/.test(gURLBar.value), "switch to tab entry found");
info("Shift+left on switch-to-tab entry");

View File

@ -0,0 +1,62 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function* promiseAutoComplete(inputText) {
gURLBar.focus();
gURLBar.value = inputText.slice(0, -1);
EventUtils.synthesizeKey(inputText.slice(-1) , {});
yield promiseSearchComplete();
}
function is_selected(index) {
is(gURLBar.popup.richlistbox.selectedIndex, index, `Item ${index + 1} should be selected`);
}
add_task(function*() {
// This test is only relevant if UnifiedComplete is enabled.
if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete"))
return;
registerCleanupFunction(() => {
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
});
let itemId =
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
NetUtil.newURI("http://example.com/?q=%s"),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"test");
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
// This item only needed so we can select the keyword item, select something
// else, then select the keyword item again.
itemId =
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
NetUtil.newURI("http://example.com/keyword"),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"keyword abc");
yield promiseAutoComplete("keyword a");
// First item should already be selected
is_selected(0);
// Select next one (important!)
EventUtils.synthesizeKey("VK_DOWN", {});
is_selected(1);
// Re-select keyword item
EventUtils.synthesizeKey("VK_UP", {});
is_selected(0);
EventUtils.synthesizeKey("b", {});
yield promiseSearchComplete();
is(gURLBar.value, "keyword ab", "urlbar should have expected input");
let result = gURLBar.popup.richlistbox.firstChild;
isnot(result, null, "Should have first item");
let uri = NetUtil.newURI(result.getAttribute("url"));
is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=ab", input: "keyword ab"}).spec, "Expect correct url");
EventUtils.synthesizeKey("VK_ESCAPE", {});
yield promisePopupHidden(gURLBar.popup);
});

View File

@ -21,16 +21,15 @@ function test() {
function cycleTabs() {
gBrowser.selectedTab = fullURLTab;
is(gURLBar.value, testURL, 'gURLBar.value should be testURL after switching back to fullURLTab');
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
gBrowser.selectedTab = partialURLTab;
is(gURLBar.value, testPartialURL, 'gURLBar.value should be testPartialURL after switching back to partialURLTab');
is(gURLBar.textValue, testPartialURL, 'gURLBar.textValue should be testPartialURL after switching back to partialURLTab');
gBrowser.selectedTab = deletedURLTab;
is(gURLBar.value, '', 'gURLBar.value should be "" after switching back to deletedURLTab');
is(gURLBar.textValue, '', 'gURLBar.textValue should be "" after switching back to deletedURLTab');
gBrowser.selectedTab = fullURLTab;
is(gURLBar.value, testURL, 'gURLBar.value should be testURL after switching back to fullURLTab');
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after switching back to fullURLTab');
}
// function borrowed from browser_bug386835.js
@ -59,13 +58,13 @@ function test() {
function prepareDeletedURLTab(cb) {
gBrowser.selectedTab = deletedURLTab;
is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to deletedURLTab');
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to deletedURLTab');
// simulate the user removing the whole url from the location bar
gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", true);
urlbarBackspace(function () {
is(gURLBar.value, "", 'gURLBar.value should be "" (just set)');
is(gURLBar.textValue, "", 'gURLBar.textValue should be "" (just set)');
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll"))
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
cb();
@ -74,13 +73,13 @@ function test() {
function prepareFullURLTab(cb) {
gBrowser.selectedTab = fullURLTab;
is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to fullURLTab');
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to fullURLTab');
cb();
}
function preparePartialURLTab(cb) {
gBrowser.selectedTab = partialURLTab;
is(gURLBar.value, testURL, 'gURLBar.value should be testURL after initial switch to partialURLTab');
is(gURLBar.textValue, testURL, 'gURLBar.textValue should be testURL after initial switch to partialURLTab');
// simulate the user removing part of the url from the location bar
gPrefService.setBoolPref("browser.urlbar.clickSelectsAll", false);
@ -91,7 +90,7 @@ function test() {
if (deleted < charsToDelete) {
urlbarBackspace(arguments.callee);
} else {
is(gURLBar.value, testPartialURL, "gURLBar.value should be testPartialURL (just set)");
is(gURLBar.textValue, testPartialURL, "gURLBar.textValue should be testPartialURL (just set)");
if (gPrefService.prefHasUserValue("browser.urlbar.clickSelectsAll"))
gPrefService.clearUserPref("browser.urlbar.clickSelectsAll");
cb();

View File

@ -31,7 +31,8 @@ let tests = [
setup: function() {
gURLBar.value = testActionURL;
gURLBar.valueIsTyped = true;
is(gURLBar.value, testActionURL, "gURLBar.value starts with correct value");
is(gURLBar.value, testActionURL, "gURLBar starts with the correct real value");
is(gURLBar.textValue, testURL, "gURLBar starts with the correct display value");
// Focus the urlbar so we can select it all & copy
gURLBar.focus();
@ -73,7 +74,8 @@ let tests = [
gURLBar.value = testActionURL;
gURLBar.valueIsTyped = true;
// Sanity check that we have the right value
is(gURLBar.value, testActionURL, "gURLBar.value starts with correct value");
is(gURLBar.value, testActionURL, "gURLBar starts with the correct real value");
is(gURLBar.textValue, testURL, "gURLBar starts with the correct display value");
// Now just select part of the value & cut that.
gURLBar.selectionStart = testURL.length - 10;

View File

@ -45,10 +45,12 @@ function testNext() {
gURLBar.addEventListener("focus", function onFocus() {
gURLBar.removeEventListener("focus", onFocus);
gURLBar.inputField.value = inputValue.slice(0, -1);
EventUtils.synthesizeKey(inputValue.slice(-1) , {});
EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true });
});
gBrowser.selectedBrowser.focus();
gURLBar.inputField.value = inputValue;
gURLBar.focus();
}

View File

@ -37,10 +37,10 @@ function runShiftLeftClickTest() {
addPageShowListener(aWindow.gBrowser.selectedBrowser, function() {
executeSoon(function () {
info("URL should be loaded in a new window");
is(gURLBar.value, "", "Urlbar reverted to original value");
is(gURLBar.value, "", "Urlbar reverted to original value");
is(gFocusManager.focusedElement, null, "There should be no focused element");
is(gFocusManager.focusedWindow, aWindow.gBrowser.contentWindow, "Content window should be focused");
is(aWindow.gURLBar.value, TEST_VALUE, "New URL is loaded in new window");
is(aWindow.gURLBar.textValue, TEST_VALUE, "New URL is loaded in new window");
aWindow.close();
@ -61,7 +61,7 @@ function runNextTest() {
finish();
return;
}
info("Running test: " + test.desc);
// Tab will be blank if test.startValue is null
let tab = gBrowser.selectedTab = gBrowser.addTab(test.startValue);
@ -106,7 +106,7 @@ let gTests = [
is(gURLBar.value, "", "Urlbar reverted to original value");
ok(!gURLBar.focused, "Urlbar is no longer focused after urlbar command");
is(gBrowser.selectedTab, aTab, "Focus did not change to the new tab");
// Select the new background tab
gBrowser.selectedTab = gBrowser.selectedTab.nextSibling;
is(gURLBar.value, TEST_VALUE, "New URL is loaded in new tab");
@ -143,7 +143,7 @@ function triggerCommand(aClick, aEvent) {
if (aClick) {
is(gURLBar.getAttribute("pageproxystate"), "invalid",
"page proxy state must be invalid for go button to be visible");
EventUtils.synthesizeMouseAtCenter(gGoButton, aEvent);
EventUtils.synthesizeMouseAtCenter(gGoButton, aEvent);
}
else
EventUtils.synthesizeKey("VK_RETURN", aEvent);

View File

@ -41,7 +41,7 @@ function testNext() {
gURLBar.focus();
paste(inputValue, function() {
is(gURLBar.value, expectedURL, "entering '" + inputValue + "' strips relevant bits.");
is(gURLBar.textValue, expectedURL, "entering '" + inputValue + "' strips relevant bits.");
setTimeout(testNext, 0);
});

View File

@ -27,7 +27,7 @@ function test() {
};
let history = Cc["@mozilla.org/browser/history;1"]
.getService(Ci.mozIAsyncHistory);
history.updatePlaces({ uri: NetUtil.newURI("http://www.autofilltrimurl.com/")
history.updatePlaces({ uri: NetUtil.newURI("http://www.autofilltrimurl.com/whatever")
, visits: [ { transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED
, visitDate: Date.now() * 1000
} ]
@ -44,7 +44,8 @@ function continue_test() {
EventUtils.synthesizeKey(aTyped.substr(-1), {});
waitForSearchComplete(function () {
is(gURLBar.value, aExpected, "trim was applied correctly");
info(`Got value: ${gURLBar.value}`);
is(gURLBar.value, aExpected, "Autofilled value is as expected");
aCallback();
});
}
@ -53,9 +54,9 @@ function continue_test() {
test_autoFill("http://au", "http://autofilltrimurl.com/", function () {
test_autoFill("http://www.autofilltrimurl.com", "http://www.autofilltrimurl.com/", function () {
// Now ensure selecting from the popup correctly trims.
is(gURLBar.controller.matchCount, 1, "Found the expected number of matches");
is(gURLBar.controller.matchCount, 2, "Found the expected number of matches");
EventUtils.synthesizeKey("VK_DOWN", {});
is(gURLBar.value, "www.autofilltrimurl.com", "trim was applied correctly");
is(gURLBar.textValue, "www.autofilltrimurl.com/whatever", "trim was applied correctly");
gURLBar.closePopup();
waitForClearHistory(finish);
});

View File

@ -156,7 +156,7 @@ function runTest(test, cb) {
function doCheck() {
if (test.setURL || test.loadURL) {
gURLBar.valueIsTyped = !!test.setURL;
is(gURLBar.value, test.expectedURL, "url bar value set");
is(gURLBar.textValue, test.expectedURL, "url bar value set");
}
testCopy(test.copyVal, test.copyExpected, cb);
@ -180,7 +180,7 @@ function testCopy(copyVal, targetValue, cb) {
let endBracket = copyVal.indexOf(">");
if (startBracket == -1 || endBracket == -1 ||
startBracket > endBracket ||
copyVal.replace("<", "").replace(">", "") != gURLBar.value) {
copyVal.replace("<", "").replace(">", "") != gURLBar.textValue) {
ok(false, "invalid copyVal: " + copyVal);
}
gURLBar.selectionStart = startBracket;

View File

@ -51,12 +51,12 @@ let gTests = [
]
function checkCurrent(aTab) {
is(gURLBar.value, TEST_VALUE, "Urlbar should preserve the value on return keypress");
is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
is(gBrowser.selectedTab, aTab, "New URL was loaded in the current tab");
}
function checkNewTab(aTab) {
is(gURLBar.value, TEST_VALUE, "Urlbar should preserve the value on return keypress");
is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
isnot(gBrowser.selectedTab, aTab, "New URL was loaded in a new tab");
}

View File

@ -11,23 +11,23 @@ function test() {
function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
is(gURLBar.value, gURLBar.trimValue(goodURL), "location bar reflects loaded page");
is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page");
typeAndSubmit(badURL);
is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects loading page");
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects loading page");
gBrowser.contentWindow.stop();
is(gURLBar.value, gURLBar.trimValue(goodURL), "location bar reflects loaded page after stop()");
is(gURLBar.textValue, gURLBar.trimValue(goodURL), "location bar reflects loaded page after stop()");
gBrowser.removeCurrentTab();
gBrowser.selectedTab = gBrowser.addTab("about:blank");
is(gURLBar.value, "", "location bar is empty");
is(gURLBar.textValue, "", "location bar is empty");
typeAndSubmit(badURL);
is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects loading page");
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects loading page");
gBrowser.contentWindow.stop();
is(gURLBar.value, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
is(gURLBar.textValue, gURLBar.trimValue(badURL), "location bar reflects stopped page in an empty tab");
gBrowser.removeCurrentTab();
finish();

View File

@ -5,7 +5,7 @@
function testVal(originalValue, targetValue) {
gURLBar.value = originalValue;
gURLBar.valueIsTyped = false;
is(gURLBar.value, targetValue || originalValue, "url bar value set");
is(gURLBar.textValue, targetValue || originalValue, "url bar value set");
}
function test() {
@ -96,7 +96,7 @@ function test() {
function testCopy(originalValue, targetValue, cb) {
waitForClipboard(targetValue, function () {
is(gURLBar.value, originalValue, "url bar copy value set");
is(gURLBar.textValue, originalValue, "url bar copy value set");
gURLBar.focus();
gURLBar.select();

View File

@ -6,6 +6,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm");
function closeAllNotifications () {
let notificationBox = document.getElementById("global-notificationbox");

View File

@ -132,9 +132,7 @@
-->
<method name="onBeforeValueGet">
<body><![CDATA[
if (this.hasAttribute("actiontype"))
return {value: this._value};
return null;
return {value: this._value};
]]></body>
</method>
@ -285,31 +283,31 @@
var mayInheritPrincipal = false;
var postData = null;
var action = this._parseActionUrl(url);
let action = this._parseActionUrl(this._value);
let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
let matchLastLocationChange = true;
if (action) {
if (this.hasAttribute("actiontype")) {
if (action.type == "switchtab") {
url = action.params.url;
if (action.type == "switchtab") {
url = action.params.url;
if (this.hasAttribute("actiontype")) {
this.handleRevert();
let prevTab = gBrowser.selectedTab;
if (switchToTabHavingURI(url) &&
isTabEmpty(prevTab))
gBrowser.removeTab(prevTab);
return;
} else if (action.type == "keyword") {
url = action.params.url;
} else if (action.type == "searchengine") {
let engine = Services.search.getEngineByName(action.params.engineName);
let submission = engine.getSubmission(action.params.searchQuery);
url = submission.uri.spec;
postData = submission.postData;
} else if (action.type == "visiturl") {
url = action.params.url;
}
} else if (action.type == "keyword") {
url = action.params.url;
} else if (action.type == "searchengine") {
let engine = Services.search.getEngineByName(action.params.engineName);
let submission = engine.getSubmission(action.params.searchQuery);
url = submission.uri.spec;
postData = submission.postData;
} else if (action.type == "visiturl") {
url = action.params.url;
}
continueOperation.call(this);
}
@ -584,7 +582,11 @@
urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
urlbar.inputField.value.substring(end);
urlbar.selectionStart = urlbar.selectionEnd = start;
urlbar.removeAttribute("actiontype");
let event = document.createEvent("UIEvents");
event.initUIEvent("input", true, false, window, 0);
urlbar.dispatchEvent(event);
SetPageProxyState("invalid");
}
@ -704,8 +706,10 @@
]]></body>
</method>
<property name="textValue"
onget="return this.value;">
<property name="textValue">
<getter><![CDATA[
return this.inputField.value;
]]></getter>
<setter>
<![CDATA[
try {
@ -714,8 +718,10 @@
// Trim popup selected values, but never trim results coming from
// autofill.
if (this.popup.selectedIndex == -1)
if (this.popup.selectedIndex == -1 ||
this.mController.getStyleAt(this.popup.selectedIndex) == "autofill") {
this._disableTrim = true;
}
this.value = val;
this._disableTrim = false;
@ -740,7 +746,6 @@
// URL is in the format moz-action:ACTION,PARAMS
// Where PARAMS is a JSON encoded object.
aUrl = decodeURI(aUrl);
let [, type, params] = aUrl.match(/^moz-action:([^,]+),(.*)$/);
let action = {
@ -786,6 +791,19 @@
this.inputField.setSelectionRange(aStartIndex, aEndIndex);
]]></body>
</method>
<method name="onInput">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this.mIgnoreInput && this.mController.input == this) {
this._value = this.inputField.value;
gBrowser.userTypedValue = this.value;
this.valueIsTyped = true;
this.mController.handleText();
}
this.resetActionType();
]]></body>
</method>
</implementation>
<handlers>
@ -819,7 +837,7 @@
<handler event="dragstart" phase="capturing"><![CDATA[
// Drag only if the gesture starts from the input field.
if (this.inputField != event.originalTarget &&
if (this.inputField != event.originalTarget &&
!(this.inputField.compareDocumentPosition(event.originalTarget) &
Node.DOCUMENT_POSITION_CONTAINED_BY))
return;
@ -943,6 +961,35 @@
createBundle("chrome://browser/locale/places/places.properties");
</field>
<!-- Override this so that navigating between items results in an item
always being selected. This is contrary to the normal behaviour where
if you navigate beyond either end of the list, no item will be
selected. -->
<method name="getNextIndex">
<parameter name="reverse"/>
<parameter name="amount"/>
<parameter name="index"/>
<parameter name="maxRow"/>
<body><![CDATA[
if (maxRow < 0)
return -1;
let newIndex = index + (reverse ? -1 : 1) * amount;
// We don't want to wrap if navigation in any direction by one item.
// Otherwise we clamp to one end of the list.
// ie, hitting page-down will only cause is to wrap if we're already
// at one end of the list.
if (newIndex < 0) {
newIndex = index > 0 ? 0 : maxRow;
} else if (newIndex > maxRow) {
newIndex = index < maxRow ? maxRow : 0;
}
return newIndex;
]]></body>
</method>
<property name="maxResults" readonly="true">
<getter>
<![CDATA[
@ -1050,6 +1097,18 @@
</body>
</method>
<method name="onResultsAdded">
<body>
<![CDATA[
if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete"))
return;
if (this._matchCount > 0 && this.selectedIndex == -1)
this.selectedIndex = 0;
]]>
</body>
</method>
</implementation>
</binding>

View File

@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
XPCOMUtils.defineLazyModuleGetter(this, "LoopContacts",
"resource:///modules/loop/LoopContacts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
/**
* Attempts to open a websocket.
@ -326,6 +328,50 @@ let LoopCallsInternal = {
return true;
},
/**
* Block a caller so it will show up in the contacts list as a blocked contact.
* If the contact is not yet part of the users' contacts list, it will be added
* as a blocked contact directly.
*
* @param {String} callerId Email address or phone number that may identify
* the caller as an existing contact
* @param {Function} callback Function that will be invoked once the operation
* has completed. When an error occurs, it will be
* passed as its first argument
*/
blockDirectCaller: function(callerId, callback) {
let field = callerId.contains("@") ? "email" : "tel";
Task.spawn(function* () {
// See if we can find the caller in our database.
let contacts = yield LoopContacts.promise("search", {
q: callerId,
field: field
});
let contact;
if (contacts.length) {
for (contact of contacts) {
yield LoopContacts.promise("block", contact._guid);
}
} else {
// If the contact doesn't exist yet, add it as a blocked contact.
contact = {
id: MozLoopService.generateUUID(),
name: [callerId],
category: ["local"],
blocked: true
};
// Add the phone OR email field to the contact.
contact[field] = [{
pref: true,
value: callerId
}];
yield LoopContacts.promise("add", contact);
}
}).then(callback, callback);
},
/**
* Open call progress websocket and terminate with a reason of busy
* the server.
@ -400,6 +446,13 @@ this.LoopCalls = {
*/
startDirectCall: function(contact, callType) {
LoopCallsInternal.startDirectCall(contact, callType);
},
/**
* @see LoopCallsInternal#blockDirectCaller
*/
blockDirectCaller: function(callerId, callback) {
return LoopCallsInternal.blockDirectCaller(callerId, callback);
}
};
Object.freeze(LoopCalls);

View File

@ -774,7 +774,14 @@ let MozLoopServiceInternal = {
openChatWindow: function(conversationWindowData) {
// So I guess the origin is the loop server!?
let origin = this.loopServerUri;
let windowId = gLastWindowId++;
// Try getting a window ID that can (re-)identify this conversation, or resort
// to a globally unique one as a last resort.
// XXX We can clean this up once rooms and direct contact calling are the only
// two modes left.
let windowId = ("contact" in conversationWindowData) ?
conversationWindowData.contact._guid || gLastWindowId++ :
conversationWindowData.roomToken || conversationWindowData.callId ||
gLastWindowId++;
// Store the id as a string, as that's what we use elsewhere.
windowId = windowId.toString();
@ -1421,9 +1428,12 @@ this.MozLoopService = {
*/
openGettingStartedTour: Task.async(function(aSrc = null) {
try {
let url = new URL(Services.prefs.getCharPref("loop.gettingStarted.url"));
let urlStr = Services.prefs.getCharPref("loop.gettingStarted.url");
let url = new URL(Services.urlFormatter.formatURL(urlStr));
if (aSrc) {
url.searchParams.set("source", aSrc);
url.searchParams.set("utm_source", "firefox-browser");
url.searchParams.set("utm_medium", "firefox-browser");
url.searchParams.set("utm_campaign", aSrc);
}
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.switchToTabHavingURI(url, true, {replaceQueryString: true});

View File

@ -20,6 +20,9 @@ loop.conversation = (function(mozL10n) {
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
@ -505,14 +508,27 @@ loop.conversation = (function(mozL10n) {
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},

View File

@ -20,6 +20,9 @@ loop.conversation = (function(mozL10n) {
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
@ -505,14 +508,27 @@ loop.conversation = (function(mozL10n) {
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},

View File

@ -165,12 +165,11 @@ loop.panel = (function(_, mozL10n) {
});
var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
componentDidMount: function() {
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
},
handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour();
navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event);
},
render: function() {
@ -287,7 +286,7 @@ loop.panel = (function(_, mozL10n) {
},
openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settingsMenu");
navigator.mozLoop.openGettingStartedTour("settings-menu");
},
render: function() {
@ -694,6 +693,7 @@ loop.panel = (function(_, mozL10n) {
getInitialState: function() {
return {
userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
};
},
@ -741,6 +741,12 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors();
},
_gettingStartedSeen: function() {
this.setState({
gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
});
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be simplified.
@ -750,7 +756,6 @@ loop.panel = (function(_, mozL10n) {
return (
Tab({name: "call"},
React.DOM.div({className: "content-area"},
GettingStartedView(null),
CallUrlResult({client: this.props.client,
notifications: this.props.notifications,
callUrl: this.props.callUrl}),
@ -762,7 +767,6 @@ loop.panel = (function(_, mozL10n) {
return (
Tab({name: "rooms"},
GettingStartedView(null),
RoomList({dispatcher: this.props.dispatcher,
store: this.props.roomStore,
userDisplayName: this._getUserDisplayName()}),
@ -786,10 +790,12 @@ loop.panel = (function(_, mozL10n) {
componentDidMount: function() {
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
window.addEventListener("GettingStartedSeen", this._gettingStartedSeen);
},
componentWillUnmount: function() {
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen);
},
_getUserDisplayName: function() {
@ -800,6 +806,17 @@ loop.panel = (function(_, mozL10n) {
render: function() {
var NotificationListView = sharedViews.NotificationListView;
if (!this.state.gettingStartedSeen) {
return (
React.DOM.div(null,
NotificationListView({notifications: this.props.notifications,
clearOnDocumentHidden: true}),
GettingStartedView(null),
ToSView(null)
)
);
}
return (
React.DOM.div(null,
NotificationListView({notifications: this.props.notifications,

View File

@ -165,12 +165,11 @@ loop.panel = (function(_, mozL10n) {
});
var GettingStartedView = React.createClass({
componentDidMount: function() {
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
},
handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour();
navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event);
},
render: function() {
@ -287,7 +286,7 @@ loop.panel = (function(_, mozL10n) {
},
openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settingsMenu");
navigator.mozLoop.openGettingStartedTour("settings-menu");
},
render: function() {
@ -694,6 +693,7 @@ loop.panel = (function(_, mozL10n) {
getInitialState: function() {
return {
userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
};
},
@ -741,6 +741,12 @@ loop.panel = (function(_, mozL10n) {
this.updateServiceErrors();
},
_gettingStartedSeen: function() {
this.setState({
gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
});
},
/**
* The rooms feature is hidden by default for now. Once it gets mainstream,
* this method can be simplified.
@ -750,7 +756,6 @@ loop.panel = (function(_, mozL10n) {
return (
<Tab name="call">
<div className="content-area">
<GettingStartedView />
<CallUrlResult client={this.props.client}
notifications={this.props.notifications}
callUrl={this.props.callUrl} />
@ -762,7 +767,6 @@ loop.panel = (function(_, mozL10n) {
return (
<Tab name="rooms">
<GettingStartedView />
<RoomList dispatcher={this.props.dispatcher}
store={this.props.roomStore}
userDisplayName={this._getUserDisplayName()}/>
@ -786,10 +790,12 @@ loop.panel = (function(_, mozL10n) {
componentDidMount: function() {
window.addEventListener("LoopStatusChanged", this._onStatusChanged);
window.addEventListener("GettingStartedSeen", this._gettingStartedSeen);
},
componentWillUnmount: function() {
window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen);
},
_getUserDisplayName: function() {
@ -800,6 +806,17 @@ loop.panel = (function(_, mozL10n) {
render: function() {
var NotificationListView = sharedViews.NotificationListView;
if (!this.state.gettingStartedSeen) {
return (
<div>
<NotificationListView notifications={this.props.notifications}
clearOnDocumentHidden={true} />
<GettingStartedView />
<ToSView />
</div>
);
}
return (
<div>
<NotificationListView notifications={this.props.notifications}

View File

@ -107,7 +107,6 @@ body {
#fte-getstarted {
padding-top: 1em;
padding-bottom: 1em;
border-bottom: 1px solid #ccc;
margin-bottom: 1em;
}
@ -499,7 +498,7 @@ body[dir=rtl] .generate-url-spinner {
}
#powered-by-logo.en-GB,
#powered-by-logo.de-DE {
#powered-by-logo.de {
background-image: url("../img/02.png");
background-size: 21px 20px;
width: 21px;
@ -526,7 +525,7 @@ body[dir=rtl] .generate-url-spinner {
}
#powered-by-logo.en-GB,
#powered-by-logo.de-DE {
#powered-by-logo.de {
background-image: url("../img/02@2x.png");
}

View File

@ -75,6 +75,8 @@ loop.store.ConversationStore = (function() {
// Call Connection information
// The call id from the loop-server
callId: undefined,
// The caller id of the contacting side
callerId: undefined,
// The connection progress url to connect the websocket
progressURL: undefined,
// The websocket token that allows connection to the progress url

View File

@ -12,7 +12,7 @@
# Bug 1066176 tracks moving all functionality currently here
# to the Gruntfile and getting rid of this Makefile entirely.
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000/v0})
LOOP_FEEDBACK_API_URL := $(shell echo $${LOOP_FEEDBACK_API_URL-"https://input.allizom.org/api/v1/feedback"})
LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
LOOP_BRAND_WEBSITE_URL := $(shell echo $${LOOP_BRAND_WEBSITE_URL-"https://www.mozilla.org/firefox/"})

View File

@ -18,7 +18,7 @@ function getConfigFile(req, res) {
res.send([
"var loop = loop || {};",
"loop.config = loop.config || {};",
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';",
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "/v0';",
"loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
"loop.config.feedbackProductName = '" + feedbackProductName + "';",
// XXX Update with the real marketplace url once the FxOS Loop app is

View File

@ -175,7 +175,8 @@ describe("loop.panel", function() {
describe("loop.rooms.enabled on", function() {
beforeEach(function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref === "rooms.enabled") {
if (pref === "rooms.enabled" ||
pref === "gettingStarted.seen") {
return true;
}
};
@ -208,6 +209,8 @@ describe("loop.panel", function() {
navigator.mozLoop.getLoopPref = function(pref) {
if (pref === "rooms.enabled") {
return false;
} else if (pref === "gettingStarted.seen") {
return true;
}
};
@ -373,6 +376,9 @@ describe("loop.panel", function() {
});
it("should render a GettingStarted view", function() {
navigator.mozLoop.getLoopPref = function(pref) {
return false;
};
var view = createTestPanelView();
TestUtils.findRenderedComponentWithType(view, loop.panel.GettingStartedView);

View File

@ -40,6 +40,7 @@ function* checkFxA401() {
add_task(function* setup() {
Services.prefs.setCharPref("loop.server", BASE_URL);
Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
// Normally the same pushUrl would be registered but we change it in the test
// to be able to check for success on the second registration.
@ -51,6 +52,7 @@ add_task(function* setup() {
yield promiseDeletedOAuthParams(BASE_URL);
Services.prefs.clearUserPref("loop.server");
Services.prefs.clearUserPref("services.push.serverURL");
Services.prefs.clearUserPref("loop.gettingStarted.seen");
MozLoopServiceInternal.mocks.pushHandler = undefined;
delete mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
delete mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];

View File

@ -9,11 +9,13 @@
Components.utils.import("resource://gre/modules/Promise.jsm", this);
const {LoopRoomsInternal} = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
registerCleanupFunction(function*() {
MozLoopService.doNotDisturb = false;
MozLoopServiceInternal.fxAOAuthProfile = null;
yield MozLoopServiceInternal.clearError("testing");
Services.prefs.clearUserPref("loop.gettingStarted.seen");
});
add_task(function* test_doNotDisturb() {

View File

@ -299,8 +299,8 @@ add_task(function* test_openRoom() {
Assert.ok(openedUrl, "should open a chat window");
// Stop the busy kicking in for following tests.
let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
// Stop the busy kicking in for following tests. (note: windowId can be 'fakeToken')
let windowId = openedUrl.match(/about:loopconversation\#(\w+)$/)[1];
let windowData = MozLoopService.getConversationWindowData(windowId);
Assert.equal(windowData.type, "room", "window data should contain room as the type");

View File

@ -123,14 +123,16 @@ function doOnloadOnce(aCallback) {
}
function* promiseOnLoad() {
let deferred = Promise.defer();
gBrowser.addEventListener("load", function onLoadListener(aEvent) {
info("onLoadListener: " + aEvent.originalTarget.location);
gBrowser.removeEventListener("load", onLoadListener, true);
deferred.resolve(aEvent);
}, true);
return deferred.promise;
return new Promise(resolve => {
gBrowser.addEventListener("load", function onLoadListener(aEvent) {
let cw = aEvent.target.defaultView;
let tab = gBrowser._getTabForContentWindow(cw);
if (tab) {
info("onLoadListener: " + aEvent.originalTarget.location);
gBrowser.removeEventListener("load", onLoadListener, true);
resolve(aEvent);
}
}, true);
});
}

View File

@ -648,8 +648,21 @@ let SessionStoreInternal = {
let uri = activePageData ? activePageData.url || null : null;
browser.userTypedValue = uri;
// Update tab label and icon again after the tab history was updated.
this.updateTabLabelAndIcon(tab, tabData);
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
}
// Restore the tab icon.
if ("image" in tabData) {
win.gBrowser.setIcon(tab, tabData.image);
}
let event = win.document.createEvent("Events");
event.initEvent("SSTabRestoring", true, false);
@ -1860,26 +1873,6 @@ let SessionStoreInternal = {
}
},
updateTabLabelAndIcon(tab, tabData) {
let activePageData = tabData.entries[tabData.index - 1] || null;
// If the page has a title, set it.
if (activePageData) {
if (activePageData.title) {
tab.label = activePageData.title;
tab.crop = "end";
} else if (activePageData.url != "about:blank") {
tab.label = activePageData.url;
tab.crop = "center";
}
}
// Restore the tab icon.
if ("image" in tabData) {
tab.ownerDocument.defaultView.gBrowser.setIcon(tab, tabData.image);
}
},
/**
* Restores the session state stored in LastSession. This will attempt
* to merge data into the current session. If a window was opened at startup
@ -2552,17 +2545,9 @@ let SessionStoreInternal = {
this._windows[aWindow.__SSi].selected = aSelectTab;
}
// If we restore the selected tab, make sure it goes first.
let selectedIndex = aTabs.indexOf(tabbrowser.selectedTab);
if (selectedIndex > -1) {
this.restoreTab(tabbrowser.selectedTab, aTabData[selectedIndex]);
}
// Restore all tabs.
for (let t = 0; t < aTabs.length; t++) {
if (t != selectedIndex) {
this.restoreTab(aTabs[t], aTabData[t]);
}
this.restoreTab(aTabs[t], aTabData[t]);
}
},
@ -2668,10 +2653,6 @@ let SessionStoreInternal = {
browser.messageManager.sendAsyncMessage("SessionStore:restoreHistory",
{tabData: tabData, epoch: epoch});
// Update tab label and icon to show something
// while we wait for the messages to be processed.
this.updateTabLabelAndIcon(tab, tabData);
// Restore tab attributes.
if ("attributes" in tabData) {
TabAttributes.set(tab, tabData.attributes);

View File

@ -193,16 +193,16 @@ function test() {
is(browser.userTypedValue, null, "userTypedValue is empty to start");
is(browser.userTypedClear, 0, "userTypedClear is 0 to start");
gURLBar.value = "example.org";
let event = document.createEvent("Events");
event.initEvent("input", true, false);
gURLBar.dispatchEvent(event);
let inputText = "example.org";
gURLBar.focus();
gURLBar.value = inputText.slice(0, -1);
EventUtils.synthesizeKey(inputText.slice(-1) , {});
executeSoon(function () {
is(browser.userTypedValue, "example.org",
"userTypedValue was set when changing gURLBar.value");
"userTypedValue was set when changing URLBar value");
is(browser.userTypedClear, 0,
"userTypedClear was not changed when changing gURLBar.value");
"userTypedClear was not changed when changing URLBar value");
// Now make sure ss gets these values too
let newState = JSON.parse(ss.getBrowserState());
@ -235,7 +235,7 @@ function test() {
"userTypedValue was null after loading a URI");
is(browser.userTypedClear, 0,
"userTypeClear reset to 0");
is(gURLBar.value, gURLBar.trimValue("http://example.com/"),
is(gURLBar.textValue, gURLBar.trimValue("http://example.com/"),
"Address bar's value set after loading URI");
runNextTest();
});

View File

@ -251,7 +251,7 @@ Selection.prototype = {
isHTMLNode: function() {
let xhtml_ns = "http://www.w3.org/1999/xhtml";
return this.isNode() && this.node.namespaceURI == xhtml_ns;
return this.isNode() && this.nodeFront.namespaceURI == xhtml_ns;
},
// Node type
@ -300,6 +300,24 @@ Selection.prototype = {
return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_NODE;
},
/**
* @returns true if the selection is the <body> HTML element.
*/
isBodyNode: function() {
return this.isHTMLNode() &&
this.isConnected() &&
this.nodeFront.nodeName === "BODY";
},
/**
* @returns true if the selection is the <head> HTML element.
*/
isHeadNode: function() {
return this.isHTMLNode() &&
this.isConnected() &&
this.nodeFront.nodeName === "HEAD";
},
isDocumentTypeNode: function() {
return this.isNode() && this.nodeFront.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE;
},

View File

@ -57,7 +57,6 @@ function InspectorPanel(iframeWindow, toolbox) {
this.panelDoc = iframeWindow.document;
this.panelWin = iframeWindow;
this.panelWin.inspector = this;
this._inspector = null;
this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
this._target.on("will-navigate", this._onBeforeNavigate);
@ -109,6 +108,10 @@ InspectorPanel.prototype = {
return this._target.client.traits.getUniqueSelector;
},
get canPasteInnerOrAdjacentHTML() {
return this._target.client.traits.pasteHTML;
},
_deferredOpen: function(defaultSelection) {
let deferred = promise.defer();
@ -573,7 +576,7 @@ InspectorPanel.prototype = {
* Returns the clipboard content if it is appropriate for pasting
* into the current node's outer HTML, otherwise returns null.
*/
_getClipboardContentForOuterHTML: function Inspector_getClipboardContentForOuterHTML() {
_getClipboardContentForPaste: function Inspector_getClipboardContentForPaste() {
let flavors = clipboard.currentFlavors;
if (flavors.indexOf("text") != -1 ||
(flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
@ -642,15 +645,34 @@ InspectorPanel.prototype = {
editHTML.setAttribute("disabled", "true");
}
// Enable the "paste outer HTML" item if the selection is an element and
// the root actor has the appropriate trait (isOuterHTMLEditable) and if
// the clipbard content is appropriate.
let pasteOuterHTML = this.panelDoc.getElementById("node-menu-pasteouterhtml");
if (isEditableElement && this.isOuterHTMLEditable &&
this._getClipboardContentForOuterHTML()) {
pasteOuterHTML.removeAttribute("disabled");
let pasteInnerHTML = this.panelDoc.getElementById("node-menu-pasteinnerhtml");
let pasteBefore = this.panelDoc.getElementById("node-menu-pastebefore");
let pasteAfter = this.panelDoc.getElementById("node-menu-pasteafter");
let pasteFirstChild = this.panelDoc.getElementById("node-menu-pastefirstchild");
let pasteLastChild = this.panelDoc.getElementById("node-menu-pastelastchild");
// Is the clipboard content appropriate? Is the element editable?
if (isEditableElement && this._getClipboardContentForPaste()) {
pasteInnerHTML.disabled = !this.canPasteInnerOrAdjacentHTML;
// Enable the "paste outer HTML" item if the selection is an element and
// the root actor has the appropriate trait (isOuterHTMLEditable).
pasteOuterHTML.disabled = !this.isOuterHTMLEditable;
// Don't paste before / after a root or a BODY or a HEAD element.
pasteBefore.disabled = pasteAfter.disabled =
!this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() ||
this.selection.isBodyNode() || this.selection.isHeadNode();
// Don't paste as a first / last child of a HTML document element.
pasteFirstChild.disabled = pasteLastChild.disabled =
!this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() &&
this.selection.isRoot());
} else {
pasteOuterHTML.setAttribute("disabled", "true");
pasteOuterHTML.disabled = true;
pasteInnerHTML.disabled = true;
pasteBefore.disabled = true;
pasteAfter.disabled = true;
pasteFirstChild.disabled = true;
pasteLastChild.disabled = true;
}
// Enable the "copy image data-uri" item if the selection is previewable
@ -690,7 +712,7 @@ InspectorPanel.prototype = {
this._markupBox.setAttribute("collapsed", true);
this._markupBox.appendChild(this._markupFrame);
this._markupFrame.setAttribute("src", "chrome://browser/content/devtools/markup-view.xhtml");
this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView"))
this._markupFrame.setAttribute("aria-label", this.strings.GetStringFromName("inspector.panelLabel.markupView"));
},
_onMarkupFrameLoad: function InspectorPanel__onMarkupFrameLoad() {
@ -773,8 +795,7 @@ InspectorPanel.prototype = {
/**
* Edit the outerHTML of the selected Node.
*/
editHTML: function InspectorPanel_editHTML()
{
editHTML: function InspectorPanel_editHTML() {
if (!this.selection.isNode()) {
return;
}
@ -786,22 +807,49 @@ InspectorPanel.prototype = {
/**
* Paste the contents of the clipboard into the selected Node's outer HTML.
*/
pasteOuterHTML: function InspectorPanel_pasteOuterHTML()
{
let content = this._getClipboardContentForOuterHTML();
if (content) {
let node = this.selection.nodeFront;
this.markup.getNodeOuterHTML(node).then((oldContent) => {
this.markup.updateNodeOuterHTML(node, content, oldContent);
});
}
pasteOuterHTML: function InspectorPanel_pasteOuterHTML() {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.getNodeOuterHTML(node).then(oldContent => {
this.markup.updateNodeOuterHTML(node, content, oldContent);
});
},
/**
* Paste the contents of the clipboard into the selected Node's inner HTML.
*/
pasteInnerHTML: function InspectorPanel_pasteInnerHTML() {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.getNodeInnerHTML(node).then(oldContent => {
this.markup.updateNodeInnerHTML(node, content, oldContent);
});
},
/**
* Paste the contents of the clipboard as adjacent HTML to the selected Node.
* @param position The position as specified for Element.insertAdjacentHTML
* (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
*/
pasteAdjacentHTML: function InspectorPanel_pasteAdjacent(position) {
let content = this._getClipboardContentForPaste();
if (!content)
return promise.reject("No clipboard content for paste");
let node = this.selection.nodeFront;
return this.markup.insertAdjacentHTMLToNode(node, position, content);
},
/**
* Copy the innerHTML of the selected Node to the clipboard.
*/
copyInnerHTML: function InspectorPanel_copyInnerHTML()
{
copyInnerHTML: function InspectorPanel_copyInnerHTML() {
if (!this.selection.isNode()) {
return;
}

View File

@ -59,10 +59,36 @@
label="&inspectorShowDOMProperties.label;"
oncommand="inspector.showDOMProperties()"/>
<menuseparator/>
<menuitem id="node-menu-pasteinnerhtml"
label="&inspectorHTMLPasteInner.label;"
accesskey="&inspectorHTMLPasteInner.accesskey;"
oncommand="inspector.pasteInnerHTML()"/>
<menuitem id="node-menu-pasteouterhtml"
label="&inspectorHTMLPasteOuter.label;"
accesskey="&inspectorHTMLPasteOuter.accesskey;"
oncommand="inspector.pasteOuterHTML()"/>
<menu id="node-menu-paste-extra-submenu"
label="&inspectorHTMLPasteExtraSubmenu.label;"
accesskey="&inspectorHTMLPasteExtraSubmenu.accesskey;">
<menupopup>
<menuitem id="node-menu-pastebefore"
label="&inspectorHTMLPasteBefore.label;"
accesskey="&inspectorHTMLPasteBefore.accesskey;"
oncommand="inspector.pasteAdjacentHTML('beforeBegin')"/>
<menuitem id="node-menu-pasteafter"
label="&inspectorHTMLPasteAfter.label;"
accesskey="&inspectorHTMLPasteAfter.accesskey;"
oncommand="inspector.pasteAdjacentHTML('afterEnd')"/>
<menuitem id="node-menu-pastefirstchild"
label="&inspectorHTMLPasteFirstChild.label;"
accesskey="&inspectorHTMLPasteFirstChild.accesskey;"
oncommand="inspector.pasteAdjacentHTML('afterBegin')"/>
<menuitem id="node-menu-pastelastchild"
label="&inspectorHTMLPasteLastChild.label;"
accesskey="&inspectorHTMLPasteLastChild.accesskey;"
oncommand="inspector.pasteAdjacentHTML('beforeEnd')"/>
</menupopup>
</menu>
<menuseparator/>
<menuitem id="node-menu-delete"
label="&inspectorHTMLDelete.label;"

View File

@ -15,7 +15,8 @@ support-files =
doc_inspector_highlighter_rect_iframe.html
doc_inspector_infobar_01.html
doc_inspector_infobar_02.html
doc_inspector_menu.html
doc_inspector_menu-01.html
doc_inspector_menu-02.html
doc_inspector_remove-iframe-during-load.html
doc_inspector_search.html
doc_inspector_search-suggestions.html
@ -54,7 +55,8 @@ support-files =
[browser_inspector_inspect-object-element.js]
[browser_inspector_invalidate.js]
[browser_inspector_keyboard-shortcuts.js]
[browser_inspector_menu.js]
[browser_inspector_menu-01.js]
[browser_inspector_menu-02.js]
[browser_inspector_navigation.js]
[browser_inspector_picker-stop-on-destroy.js]
[browser_inspector_picker-stop-on-tool-change.js]

View File

@ -14,7 +14,7 @@ thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is
// 1) menu items are disabled/enabled depending on the clicked node
// 2) actions triggered by the items work correctly
const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu.html";
const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-01.html";
const MENU_SENSITIVITY_TEST_DATA = [
{
desc: "doctype node",
@ -28,41 +28,6 @@ const MENU_SENSITIVITY_TEST_DATA = [
}
];
const PASTE_OUTER_HTML_TEST_DATA = [
{
desc: "some text",
clipboardData: "some text",
clipboardDataType: undefined,
disabled: false
},
{
desc: "base64 encoded image data uri",
clipboardData:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
"AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
clipboardDataType: undefined,
disabled: true
},
{
desc: "html",
clipboardData: "<p>some text</p>",
clipboardDataType: "html",
disabled: false
},
{
desc: "empty string",
clipboardData: "",
clipboardDataType: undefined,
disabled: true
},
{
desc: "whitespace only",
clipboardData: " \n\n\t\n\n \n",
clipboardDataType: undefined,
disabled: true
},
];
const COPY_ITEMS_TEST_DATA = [
{
desc: "copy inner html",
@ -90,30 +55,25 @@ add_task(function* () {
let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
yield testMenuItemSensitivity();
yield testPasteOuterHTMLMenuItemSensitivity();
yield testCopyMenuItems();
yield testShowDOMProperties();
yield testPasteOuterHTMLMenu();
yield testDeleteNode();
yield testDeleteRootNode();
function* testMenuItemSensitivity() {
info("Testing sensitivity of menu items for different elements.");
// The sensibility for paste options are described in browser_inspector_menu-02.js
const MENU_ITEMS = [
"node-menu-copyinner",
"node-menu-copyouter",
"node-menu-copyuniqueselector",
"node-menu-delete",
"node-menu-pasteouterhtml",
"node-menu-pseudo-hover",
"node-menu-pseudo-active",
"node-menu-pseudo-focus"
];
// To ensure clipboard contains something to paste.
clipboard.set("<p>test</p>", "html");
for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) {
info("Testing context menu entries for " + desc);
@ -135,25 +95,6 @@ add_task(function* () {
}
}
function* testPasteOuterHTMLMenuItemSensitivity() {
info("Checking 'Paste Outer HTML' menu item sensitivity for different types" +
"of data");
let nodeFront = yield getNodeFront("p", inspector);
let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
for (let data of PASTE_OUTER_HTML_TEST_DATA) {
let { desc, clipboardData, clipboardDataType, disabled } = data;
info("Checking 'Paste Outer HTML' for " + desc);
clipboard.set(clipboardData, clipboardDataType);
yield selectNode(nodeFront, inspector);
contextMenuClick(markupTagLine);
checkMenuItem("node-menu-pasteouterhtml", disabled);
}
}
function* testCopyMenuItems() {
info("Testing various copy actions of context menu.");
for (let {desc, id, text} of COPY_ITEMS_TEST_DATA) {
@ -190,27 +131,6 @@ add_task(function* () {
yield toolbox.toggleSplitConsole();
}
function* testPasteOuterHTMLMenu() {
info("Testing that 'Paste Outer HTML' menu item works.");
clipboard.set("this was pasted");
let nodeFront = yield getNodeFront("h1", inspector);
yield selectNode(nodeFront, inspector);
contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
let onNodeReselected = inspector.markup.once("reselectedonremoved");
let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
dispatchCommandEvent(menu);
info("Waiting for inspector selection to update");
yield onNodeReselected;
ok(content.document.body.outerHTML.contains(clipboard.get()),
"Clipboard content was pasted into the node's outer HTML.");
ok(!getNode("h1", { expectNoMatch: true }), "The original node was removed.");
}
function* testDeleteNode() {
info("Testing 'Delete Node' menu item for normal elements.");

View File

@ -0,0 +1,326 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test context menu functionality:
// 1) menu items are disabled/enabled depending on the clicked node
// 2) actions triggered by the items work correctly
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: jsterm.focusInput is not a function");
const MENU_SENSITIVITY_TEST_DATA = [
{
desc: "doctype node",
selector: null,
disabled: true,
},
{
desc: "element node",
selector: "#sensitivity",
disabled: false,
},
{
desc: "document element",
selector: "html",
disabled: {
"node-menu-pastebefore": true,
"node-menu-pasteafter": true,
"node-menu-pastefirstchild": true,
"node-menu-pastelastchild": true,
}
},
{
desc: "body",
selector: "body",
disabled: {
"node-menu-pastebefore": true,
"node-menu-pasteafter": true,
}
},
{
desc: "head",
selector: "head",
disabled: {
"node-menu-pastebefore": true,
"node-menu-pasteafter": true,
}
}
];
const TEST_URL = TEST_URL_ROOT + "doc_inspector_menu-02.html";
const PASTE_HTML_TEST_SENSITIVITY_DATA = [
{
desc: "some text",
clipboardData: "some text",
clipboardDataType: undefined,
disabled: false
},
{
desc: "base64 encoded image data uri",
clipboardData:
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABC" +
"AAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==",
clipboardDataType: undefined,
disabled: true
},
{
desc: "html",
clipboardData: "<p>some text</p>",
clipboardDataType: "html",
disabled: false
},
{
desc: "empty string",
clipboardData: "",
clipboardDataType: undefined,
disabled: true
},
{
desc: "whitespace only",
clipboardData: " \n\n\t\n\n \n",
clipboardDataType: undefined,
disabled: true
},
];
const PASTE_ADJACENT_HTML_DATA = [
{
desc: "As First Child",
clipboardData: "2",
menuId: "node-menu-pastefirstchild",
},
{
desc: "As Last Child",
clipboardData: "4",
menuId: "node-menu-pastelastchild",
},
{
desc: "Before",
clipboardData: "1",
menuId: "node-menu-pastebefore",
},
{
desc: "After",
clipboardData: "<span>5</span>",
menuId: "node-menu-pasteafter",
},
];
let clipboard = require("sdk/clipboard");
registerCleanupFunction(() => {
clipboard = null;
});
add_task(function* () {
let { inspector, toolbox } = yield openInspectorForURL(TEST_URL);
yield testMenuItemSensitivity();
yield testPasteHTMLMenuItemsSensitivity();
yield testPasteOuterHTMLMenu();
yield testPasteInnerHTMLMenu();
yield testPasteAdjacentHTMLMenu();
function* testMenuItemSensitivity() {
info("Testing sensitivity of menu items for different elements.");
const MENU_ITEMS = [
"node-menu-pasteinnerhtml",
"node-menu-pasteouterhtml",
"node-menu-pastebefore",
"node-menu-pasteafter",
"node-menu-pastefirstchild",
"node-menu-pastelastchild",
];
// To ensure clipboard contains something to paste.
clipboard.set("<p>test</p>", "html");
for (let {desc, selector, disabled} of MENU_SENSITIVITY_TEST_DATA) {
info("Testing context menu entries for " + desc);
let front;
if (selector) {
front = yield getNodeFront(selector, inspector);
} else {
// Select the docType if no selector is provided
let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
front = nodes[0];
}
yield selectNode(front, inspector);
contextMenuClick(getContainerForNodeFront(front, inspector).tagLine);
for (let name of MENU_ITEMS) {
let disabledForMenu = typeof disabled === "object" ?
disabled[name] : disabled;
info(`${name} should be ${disabledForMenu ? "disabled" : "enabled"} ` +
`for ${desc}`);
checkMenuItem(name, disabledForMenu);
}
}
}
function* testPasteHTMLMenuItemsSensitivity() {
let menus = [
"node-menu-pasteinnerhtml",
"node-menu-pasteouterhtml",
"node-menu-pastebefore",
"node-menu-pasteafter",
"node-menu-pastefirstchild",
"node-menu-pastelastchild",
];
info("Checking Paste menu items sensitivity for different types" +
"of data");
let nodeFront = yield getNodeFront("#paste-area", inspector);
let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
for (let menuId of menus) {
for (let data of PASTE_HTML_TEST_SENSITIVITY_DATA) {
let { desc, clipboardData, clipboardDataType, disabled } = data;
let menuLabel = getLabelFor("#" + menuId);
info(`Checking ${menuLabel} for ${desc}`);
clipboard.set(clipboardData, clipboardDataType);
yield selectNode(nodeFront, inspector);
contextMenuClick(markupTagLine);
checkMenuItem(menuId, disabled);
}
}
}
function* testPasteOuterHTMLMenu() {
info("Testing that 'Paste Outer HTML' menu item works.");
clipboard.set("this was pasted (outerHTML)");
let outerHTMLSelector = "#paste-area h1";
let nodeFront = yield getNodeFront(outerHTMLSelector, inspector);
yield selectNode(nodeFront, inspector);
contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
let onNodeReselected = inspector.markup.once("reselectedonremoved");
let menu = inspector.panelDoc.getElementById("node-menu-pasteouterhtml");
dispatchCommandEvent(menu);
info("Waiting for inspector selection to update");
yield onNodeReselected;
ok(content.document.body.outerHTML.contains(clipboard.get()),
"Clipboard content was pasted into the node's outer HTML.");
ok(!getNode(outerHTMLSelector, { expectNoMatch: true }),
"The original node was removed.");
}
function* testPasteInnerHTMLMenu() {
info("Testing that 'Paste Inner HTML' menu item works.");
clipboard.set("this was pasted (innerHTML)");
let innerHTMLSelector = "#paste-area .inner";
let getInnerHTML = () => content.document.querySelector(innerHTMLSelector).innerHTML;
let origInnerHTML = getInnerHTML();
let nodeFront = yield getNodeFront(innerHTMLSelector, inspector);
yield selectNode(nodeFront, inspector);
contextMenuClick(getContainerForNodeFront(nodeFront, inspector).tagLine);
let onMutation = inspector.once("markupmutation");
let menu = inspector.panelDoc.getElementById("node-menu-pasteinnerhtml");
dispatchCommandEvent(menu);
info("Waiting for mutation to occur");
yield onMutation;
ok(getInnerHTML() === clipboard.get(),
"Clipboard content was pasted into the node's inner HTML.");
ok(getNode(innerHTMLSelector), "The original node has been preserved.");
yield undoChange(inspector);
ok(getInnerHTML() === origInnerHTML, "Previous innerHTML has been " +
"restored after undo");
}
function* testPasteAdjacentHTMLMenu() {
let refSelector = "#paste-area .adjacent .ref";
let adjacentNode = content.document.querySelector(refSelector).parentNode;
let nodeFront = yield getNodeFront(refSelector, inspector);
yield selectNode(nodeFront, inspector);
let markupTagLine = getContainerForNodeFront(nodeFront, inspector).tagLine;
for (let { desc, clipboardData, menuId } of PASTE_ADJACENT_HTML_DATA) {
let menu = inspector.panelDoc.getElementById(menuId);
info(`Testing ${getLabelFor(menu)} for ${clipboardData}`);
clipboard.set(clipboardData);
contextMenuClick(markupTagLine);
let onMutation = inspector.once("markupmutation");
dispatchCommandEvent(menu);
info("Waiting for mutation to occur");
yield onMutation;
}
ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>" +
"<span>5</span>", "The Paste as Last Child / as First Child / Before " +
"/ After worked as expected");
yield undoChange(inspector);
ok(adjacentNode.innerHTML.trim() === "1<span class=\"ref\">234</span>",
"Undo works for paste adjacent HTML");
}
function checkMenuItem(elementId, disabled) {
if (disabled) {
checkDisabled(elementId);
} else {
checkEnabled(elementId);
}
}
function checkEnabled(elementId) {
let elt = inspector.panelDoc.getElementById(elementId);
ok(!elt.hasAttribute("disabled"),
'"' + elt.label + '" context menu option is not disabled');
}
function checkDisabled(elementId) {
let elt = inspector.panelDoc.getElementById(elementId);
ok(elt.hasAttribute("disabled"),
'"' + elt.label + '" context menu option is disabled');
}
function dispatchCommandEvent(node) {
info("Dispatching command event on " + node);
let commandEvent = document.createEvent("XULCommandEvent");
commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
false, false, null);
node.dispatchEvent(commandEvent);
}
function contextMenuClick(element) {
info("Simulating contextmenu event on " + element);
let evt = element.ownerDocument.createEvent('MouseEvents');
let button = 2; // right click
evt.initMouseEvent('contextmenu', true, true,
element.ownerDocument.defaultView, 1, 0, 0, 0, 0, false,
false, false, false, button, null);
element.dispatchEvent(evt);
}
function getLabelFor(elt) {
if (typeof elt === "string")
elt = inspector.panelDoc.querySelector(elt);
let isInPasteSubMenu = elt.matches("#node-menu-paste-extra-submenu *");
return `"${isInPasteSubMenu ? "Paste > " : ""}${elt.label}"`;
}
});

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Inspector Tree Menu Test</title>
<meta charset="utf-8">
</head>
<body>
<div>
<div id="paste-area">
<h1>Inspector Tree Menu Test</h1>
<p class="inner">Unset</p>
<p class="adjacent">
<span class="ref">3</span>
</p>
</div>
<p data-id="copy">Paragraph for testing copy</p>
<p id="sensitivity">Paragraph for sensitivity</p>
<p id="delete">This has to be deleted</p>
</div>
</body>
</html>

View File

@ -664,3 +664,43 @@ function executeInContent(name, data={}, objects={}, expectResponse=true) {
return promise.resolve();
}
}
/**
* Undo the last markup-view action and wait for the corresponding mutation to
* occur
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when the markup-mutation has been treated or
* rejects if no undo action is possible
*/
function undoChange(inspector) {
let canUndo = inspector.markup.undo.canUndo();
ok(canUndo, "The last change in the markup-view can be undone");
if (!canUndo) {
return promise.reject();
}
let mutated = inspector.once("markupmutation");
inspector.markup.undo.undo();
return mutated;
}
/**
* Redo the last markup-view action and wait for the corresponding mutation to
* occur
* @param {InspectorPanel} inspector The instance of InspectorPanel currently
* loaded in the toolbox
* @return a promise that resolves when the markup-mutation has been treated or
* rejects if no redo action is possible
*/
function redoChange(inspector) {
let canRedo = inspector.markup.undo.canRedo();
ok(canRedo, "The last change in the markup-view can be redone");
if (!canRedo) {
return promise.reject();
}
let mutated = inspector.once("markupmutation");
inspector.markup.undo.redo();
return mutated;
}

View File

@ -33,7 +33,7 @@ loader.lazyGetter(this, "DOMParser", function() {
return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
});
loader.lazyGetter(this, "AutocompletePopup", () => {
return require("devtools/shared/autocomplete-popup").AutocompletePopup
return require("devtools/shared/autocomplete-popup").AutocompletePopup;
});
/**
@ -521,16 +521,17 @@ MarkupView.prototype = {
// Retain the node so we can undo this...
this.walker.retainNode(aNode).then(() => {
let parent = aNode.parentNode();
let sibling = null;
let nextSibling = null;
this.undo.do(() => {
if (container.selected) {
this.navigate(this.getContainer(parent));
}
this.walker.removeNode(aNode).then(nextSibling => {
sibling = nextSibling;
this.walker.removeNode(aNode).then(siblings => {
let focusNode = siblings.previousSibling || parent;
nextSibling = siblings.nextSibling;
if (container.selected) {
this.navigate(this.getContainer(focusNode));
}
});
}, () => {
this.walker.insertBefore(aNode, parent, sibling);
this.walker.insertBefore(aNode, parent, nextSibling);
});
}).then(null, console.error);
},
@ -701,16 +702,19 @@ MarkupView.prototype = {
removedContainers.add(container);
}
// If there has been additions, flash the nodes
// 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);
addedOrEditedContainers.add(addedContainer);
if (addedContainer) {
addedOrEditedContainers.add(addedContainer);
// The node may be added as a result of an append, in which case it
// it will have been removed from another container first, but in
// these cases we don't want to flash both the removal and the
// addition
removedContainers.delete(container);
// The node may be added as a result of an append, in which case
// it will have been removed from another container first, but in
// these cases we don't want to flash both the removal and the
// addition
removedContainers.delete(container);
}
});
}
}
@ -799,20 +803,46 @@ MarkupView.prototype = {
container.expanded = false;
},
/**
* Returns either the innerHTML or the outerHTML for a remote node.
* @param aNode The NodeFront to get the outerHTML / innerHTML for.
* @param isOuter A boolean that, if true, makes the function return the
* outerHTML, otherwise the innerHTML.
* @returns A promise that will be resolved with the outerHTML / innerHTML.
*/
_getNodeHTML: function(aNode, isOuter) {
let walkerPromise = null;
if (isOuter) {
walkerPromise = this.walker.outerHTML(aNode);
} else {
walkerPromise = this.walker.innerHTML(aNode);
}
return walkerPromise.then(longstr => {
return longstr.string().then(html => {
longstr.release().then(null, console.error);
return html;
});
});
},
/**
* Retrieve the outerHTML for a remote node.
* @param aNode The NodeFront to get the outerHTML for.
* @returns A promise that will be resolved with the outerHTML.
*/
getNodeOuterHTML: function(aNode) {
let def = promise.defer();
this.walker.outerHTML(aNode).then(longstr => {
longstr.string().then(outerHTML => {
longstr.release().then(null, console.error);
def.resolve(outerHTML);
});
});
return def.promise;
return this._getNodeHTML(aNode, true);
},
/**
* Retrieve the innerHTML for a remote node.
* @param aNode The NodeFront to get the innerHTML for.
* @returns A promise that will be resolved with the innerHTML.
*/
getNodeInnerHTML: function(aNode) {
return this._getNodeHTML(aNode);
},
/**
@ -885,31 +915,89 @@ MarkupView.prototype = {
/**
* Replace the outerHTML of any node displayed in the inspector with
* some other HTML code
* @param aNode node which outerHTML will be replaced.
* @param newValue The new outerHTML to set on the node.
* @param oldValue The old outerHTML that will be used if the user undos the update.
* @param {NodeFront} node node which outerHTML will be replaced.
* @param {string} newValue The new outerHTML to set on the node.
* @param {string} oldValue The old outerHTML that will be used if the
* user undoes the update.
* @returns A promise that will resolve when the outer HTML has been updated.
*/
updateNodeOuterHTML: function(aNode, newValue, oldValue) {
let container = this._containers.get(aNode);
updateNodeOuterHTML: function(node, newValue, oldValue) {
let container = this.getContainer(node);
if (!container) {
return promise.reject();
}
// Changing the outerHTML removes the node which outerHTML was changed.
// Listen to this removal to reselect the right node afterwards.
this.reselectOnRemoved(aNode, "outerhtml");
return this.walker.setOuterHTML(aNode, newValue).then(null, () => {
this.reselectOnRemoved(node, "outerhtml");
return this.walker.setOuterHTML(node, newValue).then(null, () => {
this.cancelReselectOnRemoved();
});
},
/**
* Replace the innerHTML of any node displayed in the inspector with
* some other HTML code
* @param {Node} node node which innerHTML will be replaced.
* @param {string} newValue The new innerHTML to set on the node.
* @param {string} oldValue The old innerHTML that will be used if the user
* undoes the update.
* @returns A promise that will resolve when the inner HTML has been updated.
*/
updateNodeInnerHTML: function(node, newValue, oldValue) {
let container = this.getContainer(node);
if (!container) {
return promise.reject();
}
let def = promise.defer();
container.undo.do(() => {
this.walker.setInnerHTML(node, newValue).then(def.resolve, def.reject);
}, () => {
this.walker.setInnerHTML(node, oldValue);
});
return def.promise;
},
/**
* Insert adjacent HTML to any node displayed in the inspector.
*
* @param {NodeFront} node The reference node.
* @param {string} position The position as specified for Element.insertAdjacentHTML
* (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
* @param {string} newValue The adjacent HTML.
* @returns A promise that will resolve when the adjacent HTML has
* been inserted.
*/
insertAdjacentHTMLToNode: function(node, position, value) {
let container = this.getContainer(node);
if (!container) {
return promise.reject();
}
let def = promise.defer();
let injectedNodes = [];
container.undo.do(() => {
this.walker.insertAdjacentHTML(node, position, value).then(nodeArray => {
injectedNodes = nodeArray.nodes;
return nodeArray;
}).then(def.resolve, def.reject);
}, () => {
this.walker.removeNodes(injectedNodes);
});
return def.promise;
},
/**
* Open an editor in the UI to allow editing of a node's outerHTML.
* @param aNode The NodeFront to edit.
*/
beginEditingOuterHTML: function(aNode) {
this.getNodeOuterHTML(aNode).then((oldValue)=> {
this.getNodeOuterHTML(aNode).then(oldValue => {
let container = this.getContainer(aNode);
if (!container) {
return;
@ -1216,7 +1304,7 @@ MarkupView.prototype = {
this._inspector.selection.off("new-node-front", this._boundOnNewSelection);
this._boundOnNewSelection = null;
this.walker.off("mutations", this._boundMutationObserver)
this.walker.off("mutations", this._boundMutationObserver);
this._boundMutationObserver = null;
this.walker.off("display-change", this._boundOnDisplayChange);
@ -1923,7 +2011,7 @@ function TextEditor(aContainer, aNode, aTemplate) {
}, () => {
this.node.setNodeValue(oldValue).then(() => {
this.markup.nodeChanged(this.node);
})
});
});
});
});
@ -2154,7 +2242,7 @@ ElementEditor.prototype = {
doMods.apply();
}, () => {
undoMods.apply();
})
});
} catch(ex) {
console.error(ex);
}

View File

@ -4,27 +4,41 @@
"use strict";
// Tests that a node can be deleted from the markup-view with the delete key
// Tests that a node can be deleted from the markup-view with the delete key.
// Also checks that after deletion the correct element is highlighted.
// The next sibling is preferred, but the parent is a fallback.
const TEST_URL = "data:text/html,<div id='delete-me'></div>";
const TEST_URL = "data:text/html,<div id='parent'><div id='first'></div><div id='second'></div><div id='third'></div></div>";
let test = asyncTest(function*() {
let {toolbox, inspector} = yield addTab(TEST_URL).then(openInspector);
function* checkDeleteAndSelection(inspector, nodeSelector, focusedNodeSelector) {
yield selectNode(nodeSelector, inspector);
yield clickContainer(nodeSelector, inspector);
info("Selecting the test node by clicking on it to make sure it receives focus");
let node = content.document.querySelector("#delete-me");
yield clickContainer("#delete-me", inspector);
info("Deleting the element with the keyboard");
info("Deleting the element \"" + nodeSelector + "\" with the keyboard");
let mutated = inspector.once("markupmutation");
EventUtils.sendKey("delete", inspector.panelWin);
yield mutated;
yield Promise.all([mutated, inspector.once("inspector-updated")]);
let nodeFront = yield getNodeFront(focusedNodeSelector, inspector);
is(inspector.selection.nodeFront, nodeFront,
focusedNodeSelector + " should be selected after " + nodeSelector + " node gets deleted.");
info("Checking that it's gone, baby gone!");
ok(!content.document.querySelector("#delete-me"), "The test node does not exist");
ok(!content.document.querySelector(nodeSelector), "The test node does not exist");
yield undoChange(inspector);
ok(content.document.querySelector("#delete-me"), "The test node is back!");
ok(content.document.querySelector(nodeSelector), "The test node is back!");
}
let test = asyncTest(function*() {
let {inspector} = yield addTab(TEST_URL).then(openInspector);
info("Selecting the test node by clicking on it to make sure it receives focus");
yield checkDeleteAndSelection(inspector, "#first", "#parent");
yield checkDeleteAndSelection(inspector, "#second", "#first");
yield checkDeleteAndSelection(inspector, "#third", "#second");
yield inspector.once("inspector-updated");
});

View File

@ -252,7 +252,7 @@ let clickContainer = Task.async(function*(selector, inspector) {
let nodeFront = yield getNodeFront(selector, inspector);
let container = getContainerForNodeFront(nodeFront, inspector);
let updated = inspector.once("inspector-updated");
let updated = container.selected ? promise.resolve() : inspector.once("inspector-updated");
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mousedown"},
inspector.markup.doc.defaultView);
EventUtils.synthesizeMouseAtCenter(container.tagLine, {type: "mouseup"},

View File

@ -6,6 +6,7 @@
EXTRA_JS_MODULES.devtools.timeline += [
'panel.js',
'widgets/global.js',
'widgets/marker-details.js',
'widgets/markers-overview.js',
'widgets/memory-overview.js',
'widgets/waterfall.js'

View File

@ -15,3 +15,4 @@ support-files =
[browser_timeline_waterfall-background.js]
[browser_timeline_waterfall-generic.js]
[browser_timeline_waterfall-styles.js]
[browser_timeline_waterfall-sidebar.js]

View File

@ -34,7 +34,7 @@ let test = Task.async(function*() {
is($("#record-button").hasAttribute("checked"), false,
"The record button should be unchecked again.");
is($("#timeline-pane").selectedPanel, $("#timeline-waterfall"),
is($("#timeline-pane").selectedPanel, $("#timeline-waterfall-container"),
"A waterfall view is now displayed.");
yield teardown(panel);

View File

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests if the sidebar is properly updated when a marker is selected.
*/
let test = Task.async(function*() {
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
let { $, $$, EVENTS, TimelineController, TimelineView } = panel.panelWin;
let { L10N } = devtools.require("devtools/timeline/global");
yield TimelineController.toggleRecording();
ok(true, "Recording has started.");
yield waitUntil(() => {
// Wait until we get 3 different markers.
let markers = TimelineController.getMarkers();
return markers.some(m => m.name == "Styles") &&
markers.some(m => m.name == "Reflow") &&
markers.some(m => m.name == "Paint");
});
yield TimelineController.toggleRecording();
ok(true, "Recording has ended.");
// Select everything
TimelineView.markersOverview.setSelection({ start: 0, end: TimelineView.markersOverview.width })
let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
let markers = TimelineController.getMarkers();
ok(bars.length > 2, "got at least 3 markers");
let sidebar = $("#timeline-waterfall-details");
for (let i = 0; i < bars.length; i++) {
let bar = bars[i];
bar.click();
let m = markers[i];
is($("#timeline-waterfall-details .marker-details-type").getAttribute("value"), m.name,
"sidebar title matches markers name");
let printedStartTime = $(".marker-details-start .marker-details-labelvalue").getAttribute("value");
let printedEndTime = $(".marker-details-end .marker-details-labelvalue").getAttribute("value");
let printedDuration= $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
// Values are rounded. We don't use a strict equality.
is(toMs(m.start), printedStartTime, "sidebar start time is valid");
is(toMs(m.end), printedEndTime, "sidebar end time is valid");
is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
}
yield teardown(panel);
finish();
});

View File

@ -10,12 +10,11 @@
<body>
<script type="text/javascript">
var x = 1;
function test() {
var a = "Hello world!";
document.body.style.backgroundColor = "rgba(" +
((Math.random() * 64)|0) + "," +
((Math.random() * 16)|0) + "," +
((Math.random() * 16)|0) + ",1)";
document.body.style.borderTop = x + "px solid red";
x = 1^x;
document.body.innerHeight; // flush pending reflows
}
// Prevent this script from being garbage collected.

View File

@ -18,6 +18,8 @@ devtools.lazyRequireGetter(this, "MemoryOverview",
"devtools/timeline/memory-overview", true);
devtools.lazyRequireGetter(this, "Waterfall",
"devtools/timeline/waterfall", true);
devtools.lazyRequireGetter(this, "MarkerDetails",
"devtools/timeline/marker-details", true);
devtools.lazyImporter(this, "CanvasGraphUtils",
"resource:///modules/devtools/Graphs.jsm");
@ -250,11 +252,17 @@ let TimelineView = {
initialize: Task.async(function*() {
this.markersOverview = new MarkersOverview($("#markers-overview"));
this.waterfall = new Waterfall($("#timeline-waterfall"));
this.markerDetails = new MarkerDetails($("#timeline-waterfall-details"));
this._onSelecting = this._onSelecting.bind(this);
this._onRefresh = this._onRefresh.bind(this);
this.markersOverview.on("selecting", this._onSelecting);
this.markersOverview.on("refresh", this._onRefresh);
this.markerDetails.on("resize", this._onRefresh);
this._onMarkerSelected = this._onMarkerSelected.bind(this);
this.waterfall.on("selected", this._onMarkerSelected);
this.waterfall.on("unselected", this._onMarkerSelected);
yield this.markersOverview.ready();
yield this.waterfall.recalculateBounds();
@ -264,6 +272,9 @@ let TimelineView = {
* Destruction function, called when the tool is closed.
*/
destroy: function() {
this.markerDetails.off("resize", this._onRefresh);
this.waterfall.off("selected", this._onMarkerSelected);
this.waterfall.off("unselected", this._onMarkerSelected);
this.markersOverview.off("selecting", this._onSelecting);
this.markersOverview.off("refresh", this._onRefresh);
this.markersOverview.destroy();
@ -300,6 +311,18 @@ let TimelineView = {
this.memoryOverview = null;
},
/**
* A marker has been selected in the waterfall.
*/
_onMarkerSelected: function(event, marker) {
if (event == "selected") {
this.markerDetails.render(marker);
}
if (event == "unselected") {
this.markerDetails.empty();
}
},
/**
* Signals that a recording session has started and triggers the appropriate
* changes in the UI.
@ -328,7 +351,7 @@ let TimelineView = {
handleRecordingEnded: function() {
$("#record-button").removeAttribute("checked");
$("#memory-checkbox").removeAttribute("disabled");
$("#timeline-pane").selectedPanel = $("#timeline-waterfall");
$("#timeline-pane").selectedPanel = $("#timeline-waterfall-container");
this.markersOverview.selectionEnabled = true;
@ -346,9 +369,9 @@ let TimelineView = {
let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
this.markersOverview.setSelection({ start, end });
} else {
let timeStart = interval.startTime;
let timeEnd = interval.endTime;
this.waterfall.setData(markers, timeStart, timeStart, timeEnd);
let startTime = interval.startTime;
let endTime = interval.endTime;
this.waterfall.setData(markers, startTime, startTime, endTime);
}
window.emit(EVENTS.RECORDING_ENDED);
@ -382,6 +405,14 @@ let TimelineView = {
this.waterfall.clearView();
return;
}
this.waterfall.resetSelection();
this.updateWaterfall();
},
/**
* Rebuild the waterfall.
*/
updateWaterfall: function() {
let selection = this.markersOverview.getSelection();
let start = selection.start / this.markersOverview.dataScaleX;
let end = selection.end / this.markersOverview.dataScaleX;
@ -389,9 +420,10 @@ let TimelineView = {
let markers = TimelineController.getMarkers();
let interval = TimelineController.getInterval();
let timeStart = interval.startTime + Math.min(start, end);
let timeEnd = interval.startTime + Math.max(start, end);
this.waterfall.setData(markers, interval.startTime, timeStart, timeEnd);
let startTime = interval.startTime + Math.min(start, end);
let endTime = interval.startTime + Math.max(start, end);
this.waterfall.setData(markers, interval.startTime, startTime, endTime);
},
/**
@ -399,7 +431,7 @@ let TimelineView = {
*/
_onRefresh: function() {
this.waterfall.recalculateBounds();
this._onSelecting();
this.updateWaterfall();
}
};

View File

@ -4,8 +4,10 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<?xml-stylesheet href="chrome://browser/skin/" type="text/css"?>
<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
<?xml-stylesheet href="chrome://browser/skin/devtools/timeline.css" type="text/css"?>
<!DOCTYPE window [
<!ENTITY % timelineDTD SYSTEM "chrome://browser/locale/devtools/timeline.dtd">
%timelineDTD;
@ -66,7 +68,11 @@
<label value="&timelineUI.stopNotice2;"/>
</hbox>
<vbox id="timeline-waterfall" flex="1"/>
<hbox id="timeline-waterfall-container" class="devtools-responsive-container" flex="1">
<vbox id="timeline-waterfall" flex="1"/>
<splitter class="devtools-side-splitter"/>
<vbox id="timeline-waterfall-details" class="theme-sidebar" width="150" height="150"/>
</hbox>
</deck>
</vbox>
</window>

View File

@ -0,0 +1,184 @@
/* 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";
let { Ci } = require("chrome");
/**
* This file contains the rendering code for the marker sidebar.
*/
loader.lazyRequireGetter(this, "L10N",
"devtools/timeline/global", true);
loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
"devtools/timeline/global", true);
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
/**
* A detailed view for one single marker.
*
* @param nsIDOMNode parent
* The parent node holding the view.
*/
function MarkerDetails(parent) {
EventEmitter.decorate(this);
this._document = parent.ownerDocument;
this._parent = parent;
this._splitter = this._document.querySelector("#timeline-waterfall-container > splitter");
this._splitter.addEventListener("mouseup", () => this.emit("resize"));
}
MarkerDetails.prototype = {
destroy: function() {
this.empty();
this._parent = null;
},
/**
* Clears the view.
*/
empty: function() {
this._parent.innerHTML = "";
},
/**
* Builds the label representing marker's type.
*
* @param string type
* Could be "Paint", "Reflow", "Styles", ...
* See TIMELINE_BLUEPRINT in widgets/global.js
*/
buildMarkerTypeLabel: function(type) {
let blueprint = TIMELINE_BLUEPRINT[type];
let hbox = this._document.createElement("hbox");
hbox.setAttribute("align", "center");
let bullet = this._document.createElement("hbox");
bullet.className = "marker-details-bullet";
bullet.style.backgroundColor = blueprint.fill;
bullet.style.borderColor = blueprint.stroke;
let label = this._document.createElement("label");
label.className = "marker-details-type";
label.setAttribute("value", blueprint.label);
hbox.appendChild(bullet);
hbox.appendChild(label);
return hbox;
},
/**
* Builds labels for name:value pairs. Like "Start: 100ms",
* "Duration: 200ms", ...
*
* @param string l10nName
* String identifier for label's name.
* @param string value
* Label's value.
*/
buildNameValueLabel: function(l10nName, value) {
let hbox = this._document.createElement("hbox");
let labelName = this._document.createElement("label");
let labelValue = this._document.createElement("label");
labelName.className = "marker-details-labelname";
labelValue.className = "marker-details-labelvalue";
labelName.setAttribute("value", L10N.getStr(l10nName));
labelValue.setAttribute("value", value);
hbox.appendChild(labelName);
hbox.appendChild(labelValue);
return hbox;
},
/**
* Populates view with marker's details.
*
* @param object marker
* The marker to display.
*/
render: function(marker) {
this.empty();
// UI for any marker
let title = this.buildMarkerTypeLabel(marker.name);
let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
let start = this.buildNameValueLabel("timeline.markerDetail.start", toMs(marker.start));
let end = this.buildNameValueLabel("timeline.markerDetail.end", toMs(marker.end));
let duration = this.buildNameValueLabel("timeline.markerDetail.duration", toMs(marker.end - marker.start));
start.classList.add("marker-details-start");
end.classList.add("marker-details-end");
duration.classList.add("marker-details-duration");
this._parent.appendChild(title);
this._parent.appendChild(start);
this._parent.appendChild(end);
this._parent.appendChild(duration);
// UI for specific markers
switch (marker.name) {
case "ConsoleTime":
this.renderConsoleTimeMarker(this._parent, marker);
break;
case "DOMEvent":
this.renderDOMEventMarker(this._parent, marker);
break;
default:
}
},
/**
* Render details of a console marker (console.time).
*
* @param nsIDOMNode parent
* The parent node holding the view.
* @param object marker
* The marker to display.
*/
renderConsoleTimeMarker: function(parent, marker) {
if ("causeName" in marker) {
let timerName = this.buildNameValueLabel("timeline.markerDetail.consoleTimerName", marker.causeName);
this._parent.appendChild(timerName);
}
},
/**
* Render details of a DOM Event marker.
*
* @param nsIDOMNode parent
* The parent node holding the view.
* @param object marker
* The marker to display.
*/
renderDOMEventMarker: function(parent, marker) {
if ("type" in marker) {
let type = this.buildNameValueLabel("timeline.markerDetail.DOMEventType", marker.type);
this._parent.appendChild(type);
}
if ("eventPhase" in marker) {
let phaseL10NProp;
if (marker.eventPhase == Ci.nsIDOMEvent.AT_TARGET) {
phaseL10NProp = "timeline.markerDetail.DOMEventTargetPhase";
}
if (marker.eventPhase == Ci.nsIDOMEvent.CAPTURING_PHASE) {
phaseL10NProp = "timeline.markerDetail.DOMEventCapturingPhase";
}
if (marker.eventPhase == Ci.nsIDOMEvent.BUBBLING_PHASE) {
phaseL10NProp = "timeline.markerDetail.DOMEventBubblingPhase";
}
let phase = this.buildNameValueLabel("timeline.markerDetail.DOMEventPhase", L10N.getStr(phaseL10NProp));
this._parent.appendChild(phase);
}
},
}
exports.MarkerDetails = MarkerDetails;

View File

@ -8,7 +8,7 @@
* of all the markers in the timeline data.
*/
const {Cc, Ci, Cu, Cr} = require("chrome");
const {Ci, Cu} = require("chrome");
loader.lazyRequireGetter(this, "L10N",
"devtools/timeline/global", true);
@ -19,6 +19,8 @@ loader.lazyImporter(this, "setNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyImporter(this, "clearNamedTimeout",
"resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyRequireGetter(this, "EventEmitter",
"devtools/toolkit/event-emitter");
const HTML_NS = "http://www.w3.org/1999/xhtml";
@ -39,6 +41,8 @@ const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
const WATERFALL_ROWCOUNT_ONPAGEUPDOWN = 10;
/**
* A detailed waterfall view for the timeline data.
*
@ -46,6 +50,7 @@ const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
* The parent node holding the waterfall.
*/
function Waterfall(parent) {
EventEmitter.decorate(this);
this._parent = parent;
this._document = parent.ownerDocument;
this._fragment = this._document.createDocumentFragment();
@ -60,6 +65,8 @@ function Waterfall(parent) {
this._listContents.setAttribute("flex", "1");
this._parent.appendChild(this._listContents);
this.setupKeys();
this._isRTL = this._getRTL();
// Lazy require is a bit slow, and these are hot objects.
@ -67,6 +74,10 @@ function Waterfall(parent) {
this._blueprint = TIMELINE_BLUEPRINT;
this._setNamedTimeout = setNamedTimeout;
this._clearNamedTimeout = clearNamedTimeout;
// Selected row index. By default, we want the first
// row to be selected.
this._selectedRowIdx = 0;
}
Waterfall.prototype = {
@ -77,20 +88,55 @@ Waterfall.prototype = {
* A list of markers received from the controller.
* @param number timeEpoch
* The absolute time (in milliseconds) when the recording started.
* @param number timeStart
* @param number startTime
* The time (in milliseconds) to start drawing from.
* @param number timeEnd
* @param number endTime
* The time (in milliseconds) to end drawing at.
*/
setData: function(markers, timeEpoch, timeStart, timeEnd) {
setData: function(markers, timeEpoch, startTime, endTime) {
this.clearView();
this._markers = markers;
let dataScale = this._waterfallWidth / (timeEnd - timeStart);
let dataScale = this._waterfallWidth / (endTime - startTime);
this._drawWaterfallBackground(dataScale);
// Label the header as if the first possible marker was at T=0.
this._buildHeader(this._headerContents, timeStart - timeEpoch, dataScale);
this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
this._buildHeader(this._headerContents, startTime - timeEpoch, dataScale);
this._buildMarkers(this._listContents, markers, startTime, endTime, dataScale);
this.selectRow(this._selectedRowIdx);
},
/**
* Keybindings.
*/
setupKeys: function() {
let pane = this._document.querySelector("#timeline-pane");
pane.parentNode.parentNode.addEventListener("keydown", e => {
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + 1);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_HOME) {
e.preventDefault();
this.selectNearestRow(0);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_END) {
e.preventDefault();
this.selectNearestRow(this._listContents.children.length);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx - WATERFALL_ROWCOUNT_ONPAGEUPDOWN);
}
if (e.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
e.preventDefault();
this.selectNearestRow(this._selectedRowIdx + WATERFALL_ROWCOUNT_ONPAGEUPDOWN);
}
}, true);
},
/**
@ -122,12 +168,12 @@ Waterfall.prototype = {
*
* @param nsIDOMNode parent
* The parent node holding the header.
* @param number timeStart
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildHeader: function(parent, timeStart, dataScale) {
_buildHeader: function(parent, startTime, dataScale) {
let container = this._document.createElement("hbox");
container.className = "waterfall-header-container";
container.setAttribute("flex", "1");
@ -159,7 +205,7 @@ Waterfall.prototype = {
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
let start = x + direction * WATERFALL_HEADER_TEXT_PADDING;
let time = Math.round(timeStart + x / dataScale);
let time = Math.round(startTime + x / dataScale);
let label = this._l10n.getFormatStr("timeline.tick", time);
let node = this._document.createElement("label");
@ -177,23 +223,25 @@ Waterfall.prototype = {
*
* @param nsIDOMNode parent
* The parent node holding the markers.
* @param number timeStart
* @param number startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* The time scale of the data source.
*/
_buildMarkers: function(parent, markers, timeStart, timeEnd, dataScale) {
let processed = 0;
_buildMarkers: function(parent, markers, startTime, endTime, dataScale) {
let rowsCount = 0;
let markerIdx = -1;
for (let marker of markers) {
if (!isMarkerInRange(marker, timeStart, timeEnd)) {
markerIdx++;
if (!isMarkerInRange(marker, startTime, endTime)) {
continue;
}
// Only build and display a finite number of markers initially, to
// preserve a snappy UI. After a certain delay, continue building the
// outstanding markers while there's (hopefully) no user interaction.
let arguments_ = [this._fragment, marker, timeStart, dataScale];
if (processed++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
let arguments_ = [this._fragment, marker, startTime, dataScale, markerIdx, rowsCount];
if (rowsCount++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
this._buildMarker.apply(this, arguments_);
} else {
this._outstandingMarkers.push(arguments_);
@ -228,6 +276,7 @@ Waterfall.prototype = {
}
this._outstandingMarkers.length = 0;
parent.appendChild(this._fragment);
this.selectRow(this._selectedRowIdx);
},
/**
@ -237,18 +286,24 @@ Waterfall.prototype = {
* The parent node holding the marker.
* @param object marker
* The { name, start, end } marker in the data source.
* @param timeStart
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
* @param number markerIdx
* Index of the marker in this._markers
* @param number rowIdx
* Index of current row
*/
_buildMarker: function(parent, marker, timeStart, dataScale) {
_buildMarker: function(parent, marker, startTime, dataScale, markerIdx, rowIdx) {
let container = this._document.createElement("hbox");
container.setAttribute("markerIdx", markerIdx);
container.className = "waterfall-marker-container";
if (marker) {
this._buildMarkerSidebar(container, marker);
this._buildMarkerWaterfall(container, marker, timeStart, dataScale);
this._buildMarkerWaterfall(container, marker, startTime, dataScale, markerIdx);
container.onclick = () => this.selectRow(rowIdx);
} else {
this._buildMarkerSpacer(container);
container.setAttribute("flex", "1");
@ -258,6 +313,83 @@ Waterfall.prototype = {
parent.appendChild(container);
},
/**
* Select first row.
*/
resetSelection: function() {
this.selectRow(0);
},
/**
* Select a marker in the waterfall.
*
* @param number idx
* Index of the row to select. -1 clears the selection.
*/
selectRow: function(idx) {
// Unselect
let prev = this._listContents.children[this._selectedRowIdx];
if (prev) {
prev.classList.remove("selected");
}
this._selectedRowIdx = idx;
let row = this._listContents.children[idx];
if (row && !row.hasAttribute("is-spacer")) {
row.focus();
row.classList.add("selected");
let markerIdx = row.getAttribute("markerIdx");
this.emit("selected", this._markers[markerIdx]);
this.ensureRowIsVisible(row);
} else {
this.emit("unselected");
}
},
/**
* Find a valid row to select.
*
* @param number idx
* Index of the row to select.
*/
selectNearestRow: function(idx) {
if (this._listContents.children.length == 0) {
return;
}
idx = Math.max(idx, 0);
idx = Math.min(idx, this._listContents.children.length - 1);
let row = this._listContents.children[idx];
if (row && row.hasAttribute("is-spacer")) {
if (idx > 0) {
return this.selectNearestRow(idx - 1);
} else {
return;
}
}
this.selectRow(idx);
},
/**
* Scroll waterfall to ensure row is in the viewport.
*
* @param number idx
* Index of the row to select.
*/
ensureRowIsVisible: function(row) {
let parent = row.parentNode;
let parentRect = parent.getBoundingClientRect();
let rowRect = row.getBoundingClientRect();
let yDelta = rowRect.top - parentRect.top;
if (yDelta < 0) {
parent.scrollTop += yDelta;
}
yDelta = parentRect.bottom - rowRect.bottom;
if (yDelta < 0) {
parent.scrollTop -= yDelta;
}
},
/**
* Creates the sidebar part of a marker in this view.
*
@ -308,12 +440,12 @@ Waterfall.prototype = {
* The container node representing the marker.
* @param object marker
* @see Waterfall.prototype._buildMarker
* @param timeStart
* @param startTime
* @see Waterfall.prototype.setData
* @param number dataScale
* @see Waterfall.prototype._buildMarkers
*/
_buildMarkerWaterfall: function(container, marker, timeStart, dataScale) {
_buildMarkerWaterfall: function(container, marker, startTime, dataScale) {
let blueprint = this._blueprint[marker.name];
let waterfall = this._document.createElement("hbox");
@ -321,7 +453,7 @@ Waterfall.prototype = {
waterfall.setAttribute("align", "center");
waterfall.setAttribute("flex", "1");
let start = (marker.start - timeStart) * dataScale;
let start = (marker.start - startTime) * dataScale;
let width = (marker.end - marker.start) * dataScale;
let offset = this._isRTL ? this._waterfallWidth : 0;
@ -329,6 +461,8 @@ Waterfall.prototype = {
bar.className = "waterfall-marker-bar";
bar.style.backgroundColor = blueprint.fill;
bar.style.borderColor = blueprint.stroke;
// Save border color. It will change when marker is selected.
bar.setAttribute("borderColor", blueprint.stroke);
bar.style.transform = "translateX(" + (start - offset) + "px)";
bar.setAttribute("type", marker.name);
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));

View File

@ -1,26 +1,96 @@
<!-- LOCALIZATION NOTE (inspectorHTMLEdit.label): This is the label shown
in the inspector contextual-menu for the item that lets users edit the
(outer) HTML of the current node -->
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
<!ENTITY inspectorHTMLEdit.accesskey "E">
<!-- LOCALIZATION NOTE (inspectorHTMLCopyInner.label): This is the label shown
in the inspector contextual-menu for the item that lets users copy the
inner HTML of the current node -->
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
<!ENTITY inspectorHTMLCopyInner.accesskey "I">
<!-- LOCALIZATION NOTE (inspectorHTMLCopyOuter.label): This is the label shown
in the inspector contextual-menu for the item that lets users copy the
outer HTML of the current node -->
<!ENTITY inspectorHTMLCopyOuter.label "Copy Outer HTML">
<!ENTITY inspectorHTMLCopyOuter.accesskey "O">
<!-- LOCALIZATION NOTE (inspectorCopyUniqueSelector.label): This is the label
shown in the inspector contextual-menu for the item that lets users copy
the CSS Selector of the current node -->
<!ENTITY inspectorCopyUniqueSelector.label "Copy Unique Selector">
<!ENTITY inspectorCopyUniqueSelector.accesskey "U">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteOuter.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste outer
HTML in the current node -->
<!ENTITY inspectorHTMLPasteOuter.label "Paste Outer HTML">
<!ENTITY inspectorHTMLPasteOuter.accesskey "P">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteInner.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste inner
HTML in the current node -->
<!ENTITY inspectorHTMLPasteInner.label "Paste Inner HTML">
<!ENTITY inspectorHTMLPasteInner.accesskey "N">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteExtraSubmenu.label): This is the label
shown in the inspector contextual-menu for the sub-menu of the other Paste
items, which allow to paste HTML:
- before the current node
- after the current node
- as the first child of the current node
- as the last child of the current node -->
<!ENTITY inspectorHTMLPasteExtraSubmenu.label "Paste ...">
<!ENTITY inspectorHTMLPasteExtraSubmenu.accesskey "T">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteBefore.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste
the HTML before the current node -->
<!ENTITY inspectorHTMLPasteBefore.label "Before">
<!ENTITY inspectorHTMLPasteBefore.accesskey "B">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteAfter.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste
the HTML after the current node -->
<!ENTITY inspectorHTMLPasteAfter.label "After">
<!ENTITY inspectorHTMLPasteAfter.accesskey "A">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteFirstChild.label): This is the label
shown in the inspector contextual-menu for the item that lets users paste
the HTML as the first child the current node -->
<!ENTITY inspectorHTMLPasteFirstChild.label "As First Child">
<!ENTITY inspectorHTMLPasteFirstChild.accesskey "F">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteLastChild.label): This is the label
shown in the inspector contextual-menu for the item that lets users paste
the HTML as the last child the current node -->
<!ENTITY inspectorHTMLPasteLastChild.label "As Last Child">
<!ENTITY inspectorHTMLPasteLastChild.accesskey "L">
<!-- LOCALIZATION NOTE (inspectorHTMLDelete.label): This is the label shown in
the inspector contextual-menu for the item that lets users delete the
current node -->
<!ENTITY inspectorHTMLDelete.label "Delete Node">
<!ENTITY inspectorHTMLDelete.accesskey "D">
<!ENTITY inspector.selectButton.tooltip "Select element with mouse">
<!-- LOCALIZATION NOTE (inspectorSearchHTML.label): This is the label shown as
the placeholder in inspector search box -->
<!ENTITY inspectorSearchHTML.label "Search HTML">
<!ENTITY inspectorSearchHTML.key "F">
<!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label
shown in the inspector contextual-menu for the item that lets users copy
the URL embedding the image data encoded in Base 64 (what we name
here Image Data URL). For more information:
https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs -->
<!ENTITY inspectorCopyImageDataUri.label "Copy Image Data-URL">
<!-- LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
shown in the inspector contextual-menu for the item that lets users see
the DOM properties of the current node. When triggered, this item
opens the split Console and displays the properties in its side panel. -->
<!ENTITY inspectorShowDOMProperties.label "Show DOM Properties">

View File

@ -53,3 +53,15 @@ graphs.memory=MB
# %1$S is replaced with one of the above label (timeline.label.*) and %2$S
# with the details. For examples: Paint (200x100), or console.time (FOO)
timeline.markerDetailFormat=%1$S (%2$S)
# LOCALIZATION NOTE (time.markerDetail.*):
# Strings used in the waterfall sidebar.
timeline.markerDetail.start=Start:
timeline.markerDetail.end=End:
timeline.markerDetail.duration=Duration:
timeline.markerDetail.consoleTimerName=Timer Name:
timeline.markerDetail.DOMEventType=Event Type:
timeline.markerDetail.DOMEventPhase=Phase:
timeline.markerDetail.DOMEventTargetPhase=Target
timeline.markerDetail.DOMEventCapturingPhase=Capture
timeline.markerDetail.DOMEventBubblingPhase=Bubbling

View File

@ -63,6 +63,10 @@
overflow-y: auto;
}
.waterfall-header-contents {
overflow-x: hidden;
}
.waterfall-background-ticks {
/* Background created on a <canvas> in js. */
/* @see browser/devtools/timeline/widgets/waterfall.js */
@ -153,3 +157,42 @@
border-radius: 1px;
transform-origin: left center;
}
.theme-light .waterfall-marker-container.selected > .waterfall-sidebar,
.theme-light .waterfall-marker-container.selected > .waterfall-marker-item {
background-color: #4c9ed9; /* Select Highlight Blue */
color: #f5f7fa; /* Light foreground text */
}
.theme-dark .waterfall-marker-container.selected > .waterfall-sidebar,
.theme-dark .waterfall-marker-container.selected > .waterfall-marker-item {
background-color: #1d4f73; /* Select Highlight Blue */
color: #f5f7fa; /* Light foreground text */
}
.waterfall-marker-container.selected .waterfall-marker-bullet,
.waterfall-marker-container.selected .waterfall-marker-bar {
border-color: initial!important;
}
#timeline-waterfall-details {
padding-top: 28px;
overflow: auto;
}
.marker-details-bullet {
width: 8px;
height: 8px;
margin: 0 8px;
border: 1px solid;
border-radius: 1px;
}
.marker-details-type {
font-size: 1.2em;
font-weight: bold;
}
.marker-details-duration {
font-weight: bold;
}

View File

@ -17,7 +17,11 @@ function test() {
newBrowser.contentWindow.location = chromeURL;
}
function checkPage() {
function checkPage(event) {
if (event.target != gBrowser.selectedBrowser.contentDocument) {
return;
}
window.removeEventListener("DOMContentLoaded", checkPage, false);
is(newBrowser.contentDocument.getElementById("test_span"), null, "Error message should not be parsed as HTML, and hence shouldn't include the 'test_span' element.");

View File

@ -480,6 +480,17 @@ static const dom::ConstantSpec gLibcProperties[] =
INT_CONSTANT(SEEK_END),
INT_CONSTANT(SEEK_SET),
// fcntl command values
#if defined(XP_UNIX)
INT_CONSTANT(F_GETLK),
INT_CONSTANT(F_SETLK),
INT_CONSTANT(F_SETLKW),
// flock type values
INT_CONSTANT(F_RDLCK),
INT_CONSTANT(F_WRLCK),
INT_CONSTANT(F_UNLCK),
#endif // defined(XP_UNIX)
// copyfile
#if defined(COPYFILE_DATA)
INT_CONSTANT(COPYFILE_DATA),
@ -586,6 +597,15 @@ static const dom::ConstantSpec gLibcProperties[] =
// Size
{ "OSFILE_SIZEOF_DIRENT", INT_TO_JSVAL(sizeof (dirent)) },
// Defining |flock|.
#if defined(XP_UNIX)
{ "OSFILE_SIZEOF_FLOCK", INT_TO_JSVAL(sizeof (struct flock)) },
{ "OSFILE_OFFSETOF_FLOCK_L_START", INT_TO_JSVAL(offsetof (struct flock, l_start)) },
{ "OSFILE_OFFSETOF_FLOCK_L_LEN", INT_TO_JSVAL(offsetof (struct flock, l_len)) },
{ "OSFILE_OFFSETOF_FLOCK_L_PID", INT_TO_JSVAL(offsetof (struct flock, l_pid)) },
{ "OSFILE_OFFSETOF_FLOCK_L_TYPE", INT_TO_JSVAL(offsetof (struct flock, l_type)) },
{ "OSFILE_OFFSETOF_FLOCK_L_WHENCE", INT_TO_JSVAL(offsetof (struct flock, l_whence)) },
#endif // defined(XP_UNIX)
// Offset of field |d_name|.
{ "OSFILE_OFFSETOF_DIRENT_D_NAME", INT_TO_JSVAL(offsetof (struct dirent, d_name)) },
// An upper bound to the length of field |d_name| of struct |dirent|.

View File

@ -141,6 +141,24 @@ function ensureModuleIsOpen() {
}
}
/**
* Sends a bookmarks notification through the given observers.
*
* @param observers
* array of nsINavBookmarkObserver objects.
* @param notification
* the notification name.
* @param args
* array of arguments to pass to the notification.
*/
function notify(observers, notification, args) {
for (let observer of observers) {
try {
observer[notification](...args);
} catch (ex) {}
}
}
this.History = Object.freeze({
/**
* Fetch the available information for one page.

View File

@ -17,7 +17,7 @@ const PREF_BRANCH = "browser.urlbar.";
const PREF_ENABLED = [ "autocomplete.enabled", true ];
const PREF_AUTOFILL = [ "autoFill", true ];
const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ];
const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", true ];
const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", false ];
const PREF_RESTYLESEARCHES = [ "restyleSearches", false ];
const PREF_DELAY = [ "delay", 50 ];
const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
@ -964,7 +964,6 @@ Search.prototype = {
comment: match.engineName,
icon: match.iconUrl,
style: "action searchengine",
finalCompleteValue: this._trimmedOriginalSearchString,
frecency: FRECENCY_SEARCHENGINES_DEFAULT,
});
},
@ -1017,7 +1016,6 @@ Search.prototype = {
value: value,
comment: uri.spec,
style: "action visiturl",
finalCompleteValue: this._originalSearchString,
frecency: 0,
};

View File

@ -1176,7 +1176,7 @@ interface nsINavHistoryQueryOptions : nsISupports
nsINavHistoryQueryOptions clone();
};
[scriptable, uuid(47f7b08b-71e0-492e-a2be-9a9fbfc75250)]
[scriptable, uuid(8a1f527e-c9d7-4a51-bf0c-d86f0379b701)]
interface nsINavHistoryService : nsISupports
{
/**
@ -1373,6 +1373,12 @@ interface nsINavHistoryService : nsISupports
*/
void removeObserver(in nsINavHistoryObserver observer);
/**
* Gets an array of registered nsINavHistoryObserver objects.
*/
void getObservers([optional] out unsigned long count,
[retval, array, size_is(count)] out nsINavHistoryObserver observers);
/**
* Runs the passed callback in batch mode. Use this when a lot of things
* are about to change. Calls can be nested, observers will only be

View File

@ -2237,9 +2237,6 @@ nsNavHistory::GetQueryResults(nsNavHistoryQueryResultNode *aResultNode,
return NS_OK;
}
// nsNavHistory::AddObserver
NS_IMETHODIMP
nsNavHistory::AddObserver(nsINavHistoryObserver* aObserver, bool aOwnsWeak)
{
@ -2249,9 +2246,6 @@ nsNavHistory::AddObserver(nsINavHistoryObserver* aObserver, bool aOwnsWeak)
return mObservers.AppendWeakElement(aObserver, aOwnsWeak);
}
// nsNavHistory::RemoveObserver
NS_IMETHODIMP
nsNavHistory::RemoveObserver(nsINavHistoryObserver* aObserver)
{
@ -2261,7 +2255,51 @@ nsNavHistory::RemoveObserver(nsINavHistoryObserver* aObserver)
return mObservers.RemoveWeakElement(aObserver);
}
// nsNavHistory::BeginUpdateBatch
NS_IMETHODIMP
nsNavHistory::GetObservers(uint32_t* _count,
nsINavHistoryObserver*** _observers)
{
NS_ENSURE_ARG_POINTER(_count);
NS_ENSURE_ARG_POINTER(_observers);
*_count = 0;
*_observers = nullptr;
// Clear any cached value, cause it's very likely the consumer has made
// changes to history and is now trying to notify them.
mDaysOfHistory = -1;
if (!mCanNotify)
return NS_OK;
nsCOMArray<nsINavHistoryObserver> observers;
// First add the category cache observers.
mCacheObservers.GetEntries(observers);
// Then add the other observers.
for (uint32_t i = 0; i < mObservers.Length(); ++i) {
const nsCOMPtr<nsINavHistoryObserver> &observer = mObservers.ElementAt(i);
// Skip nullified weak observers.
if (observer)
observers.AppendElement(observer);
}
if (observers.Count() == 0)
return NS_OK;
*_observers = static_cast<nsINavHistoryObserver**>
(nsMemory::Alloc(observers.Count() * sizeof(nsINavHistoryObserver*)));
NS_ENSURE_TRUE(*_observers, NS_ERROR_OUT_OF_MEMORY);
*_count = observers.Count();
for (uint32_t i = 0; i < *_count; ++i) {
NS_ADDREF((*_observers)[i] = observers[i]);
}
return NS_OK;
}
// See RunInBatchMode
nsresult
nsNavHistory::BeginUpdateBatch()

View File

@ -0,0 +1,76 @@
"use strict";
this.EXPORTED_SYMBOLS = [
"PlacesTestUtils",
];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
this.PlacesTestUtils = Object.freeze({
/**
* Asynchronously adds visits to a page.
*
* @param aPlaceInfo
* Can be an nsIURI, in such a case a single LINK visit will be added.
* Otherwise can be an object describing the visit to add, or an array
* of these objects:
* { uri: nsIURI of the page,
* [optional] transition: one of the TRANSITION_* from nsINavHistoryService,
* [optional] title: title of the page,
* [optional] visitDate: visit date in microseconds from the epoch
* [optional] referrer: nsIURI of the referrer for this visit
* }
*
* @return {Promise}
* @resolves When all visits have been added successfully.
* @rejects JavaScript exception.
*/
addVisits(placeInfo) {
return new Promise((resolve, reject) => {
let places = [];
if (placeInfo instanceof Ci.nsIURI) {
places.push({ uri: placeInfo });
}
else if (Array.isArray(placeInfo)) {
places = places.concat(placeInfo);
} else {
places.push(placeInfo)
}
// Create mozIVisitInfo for each entry.
let now = Date.now();
for (let place of places) {
if (typeof place.title != "string") {
place.title = "test visit for " + place.uri.spec;
}
place.visits = [{
transitionType: place.transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
: place.transition,
visitDate: place.visitDate || (now++) * 1000,
referrerURI: place.referrer
}];
}
PlacesUtils.asyncHistory.updatePlaces(
places,
{
handleError: function AAV_handleError(resultCode, placeInfo) {
let ex = new Components.Exception("Unexpected error in adding visits.",
resultCode);
reject(ex);
},
handleResult: function () {},
handleCompletion: function UP_handleCompletion() {
resolve();
}
}
);
});
},
});

View File

@ -29,10 +29,6 @@ add_task(function () {
yield promiseClearHistory();
// History database should be empty
is(PlacesUtils.history.hasHistoryEntries, false,
"History database should be empty");
// Ensure we wait for the default bookmarks import.
let bookmarksDeferred = Promise.defer();
waitForCondition(() => {
@ -53,10 +49,6 @@ add_task(function () {
{ uri: visitedURIs[7], transition: TRANSITION_DOWNLOAD }
]);
// History database should have entries
is(PlacesUtils.history.hasHistoryEntries, true,
"History database should have entries");
placeItemsCount += 7;
// We added 7 new items to history.
is(getPlacesItemsCount(), placeItemsCount,

View File

@ -6,6 +6,10 @@
TEST_DIRS += ['cpp']
TESTING_JS_MODULES += [
'PlacesTestUtils.jsm',
]
XPCSHELL_TESTS_MANIFESTS += [
'autocomplete/xpcshell.ini',
'bookmarks/xpcshell.ini',

View File

@ -31,6 +31,7 @@ add_task(function* test_trailing_space_noautofill() {
});
add_task(function* test_searchEngine_autofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("CakeSearch", "", "", "",
"GET", "http://cake.search/");
let engine = Services.search.getEngineByName("CakeSearch");
@ -48,6 +49,7 @@ add_task(function* test_searchEngine_autofill() {
});
add_task(function* test_searchEngine_prefix_space_noautofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("CupcakeSearch", "", "", "",
"GET", "http://cupcake.search/");
let engine = Services.search.getEngineByName("CupcakeSearch");
@ -65,6 +67,7 @@ add_task(function* test_searchEngine_prefix_space_noautofill() {
});
add_task(function* test_searchEngine_trailing_space_noautofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("BaconSearch", "", "", "",
"GET", "http://bacon.search/");
let engine = Services.search.getEngineByName("BaconSearch");
@ -82,6 +85,7 @@ add_task(function* test_searchEngine_trailing_space_noautofill() {
});
add_task(function* test_searchEngine_www_noautofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("HamSearch", "", "", "",
"GET", "http://ham.search/");
let engine = Services.search.getEngineByName("HamSearch");
@ -99,6 +103,7 @@ add_task(function* test_searchEngine_www_noautofill() {
});
add_task(function* test_searchEngine_different_scheme_noautofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("PieSearch", "", "", "",
"GET", "https://pie.search/");
let engine = Services.search.getEngineByName("PieSearch");
@ -116,6 +121,7 @@ add_task(function* test_searchEngine_different_scheme_noautofill() {
});
add_task(function* test_searchEngine_matching_prefix_autofill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("BeanSearch", "", "", "",
"GET", "http://www.bean.search/");
let engine = Services.search.getEngineByName("BeanSearch");

View File

@ -38,6 +38,7 @@ function* addTestEngines(items) {
add_task(function* test_searchEngine_autoFill() {
Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", true);
Services.search.addEngineWithDetails("MySearchEngine", "", "", "",
"GET", "http://my.search.com/");
let engine = Services.search.getEngineByName("MySearchEngine");

View File

@ -3,15 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
const Cc = Components.classes;
const Ci = Components.interfaces;
// Dummy boomark/history observer
function DummyObserver() {
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
os.notifyObservers(null, "dummy-observer-created", null);
Services.obs.notifyObservers(null, "dummy-observer-created", null);
}
DummyObserver.prototype = {
@ -19,9 +18,7 @@ DummyObserver.prototype = {
onBeginUpdateBatch: function () {},
onEndUpdateBatch: function () {},
onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, aTransitionType) {
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
os.notifyObservers(null, "dummy-observer-visited", null);
Services.obs.notifyObservers(null, "dummy-observer-visited", null);
},
onTitleChanged: function () {},
onDeleteURI: function () {},
@ -33,9 +30,7 @@ DummyObserver.prototype = {
//onBeginUpdateBatch: function() {},
//onEndUpdateBatch: function() {},
onItemAdded: function(aItemId, aParentId, aIndex, aItemType, aURI) {
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
os.notifyObservers(null, "dummy-observer-item-added", null);
Services.obs.notifyObservers(null, "dummy-observer-item-added", null);
},
onItemChanged: function () {},
onItemRemoved: function() {},

View File

@ -7,67 +7,59 @@
const TEST_URI = NetUtil.newURI("http://mozilla.com/");
const TEST_SUBDOMAIN_URI = NetUtil.newURI("http://foobar.mozilla.com/");
add_test(function test_addPage()
{
promiseAddVisits(TEST_URI).then(function () {
do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
add_task(function* test_addPage() {
yield promiseAddVisits(TEST_URI);
do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
});
add_test(function test_removePage()
{
add_task(function* test_removePage() {
PlacesUtils.bhistory.removePage(TEST_URI);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
add_test(function test_removePages()
{
add_task(function* test_removePages() {
let pages = [];
for (let i = 0; i < 8; i++) {
pages.push(NetUtil.newURI(TEST_URI.spec + i));
}
promiseAddVisits(pages.map(function (uri) ({ uri: uri }))).then(function () {
// Bookmarked item should not be removed from moz_places.
const ANNO_INDEX = 1;
const ANNO_NAME = "testAnno";
const ANNO_VALUE = "foo";
const BOOKMARK_INDEX = 2;
PlacesUtils.annotations.setPageAnnotation(pages[ANNO_INDEX],
ANNO_NAME, ANNO_VALUE, 0,
Ci.nsIAnnotationService.EXPIRE_NEVER);
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
pages[BOOKMARK_INDEX],
PlacesUtils.bookmarks.DEFAULT_INDEX,
"test bookmark");
PlacesUtils.annotations.setPageAnnotation(pages[BOOKMARK_INDEX],
ANNO_NAME, ANNO_VALUE, 0,
Ci.nsIAnnotationService.EXPIRE_NEVER);
PlacesUtils.bhistory.removePages(pages, pages.length);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
// Check that the bookmark and its annotation still exist.
do_check_true(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) > 0);
do_check_eq(PlacesUtils.annotations.getPageAnnotation(pages[BOOKMARK_INDEX], ANNO_NAME),
ANNO_VALUE);
// Check the annotation on the non-bookmarked page does not exist anymore.
try {
PlacesUtils.annotations.getPageAnnotation(pages[ANNO_INDEX], ANNO_NAME);
do_throw("did not expire expire_never anno on a not bookmarked item");
} catch(ex) {}
// Cleanup.
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
promiseClearHistory().then(run_next_test);
});
yield promiseAddVisits(pages.map(function (uri) ({ uri: uri })));
// Bookmarked item should not be removed from moz_places.
const ANNO_INDEX = 1;
const ANNO_NAME = "testAnno";
const ANNO_VALUE = "foo";
const BOOKMARK_INDEX = 2;
PlacesUtils.annotations.setPageAnnotation(pages[ANNO_INDEX],
ANNO_NAME, ANNO_VALUE, 0,
Ci.nsIAnnotationService.EXPIRE_NEVER);
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
pages[BOOKMARK_INDEX],
PlacesUtils.bookmarks.DEFAULT_INDEX,
"test bookmark");
PlacesUtils.annotations.setPageAnnotation(pages[BOOKMARK_INDEX],
ANNO_NAME, ANNO_VALUE, 0,
Ci.nsIAnnotationService.EXPIRE_NEVER);
PlacesUtils.bhistory.removePages(pages, pages.length);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
// Check that the bookmark and its annotation still exist.
do_check_true(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) > 0);
do_check_eq(PlacesUtils.annotations.getPageAnnotation(pages[BOOKMARK_INDEX], ANNO_NAME),
ANNO_VALUE);
// Check the annotation on the non-bookmarked page does not exist anymore.
try {
PlacesUtils.annotations.getPageAnnotation(pages[ANNO_INDEX], ANNO_NAME);
do_throw("did not expire expire_never anno on a not bookmarked item");
} catch(ex) {}
// Cleanup.
PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
yield promiseClearHistory();
});
add_test(function test_removePagesByTimeframe()
{
add_task(function* test_removePagesByTimeframe() {
let visits = [];
let startDate = Date.now() * 1000;
for (let i = 0; i < 10; i++) {
@ -77,49 +69,61 @@ add_test(function test_removePagesByTimeframe()
});
}
promiseAddVisits(visits).then(function () {
// Delete all pages except the first and the last.
PlacesUtils.bhistory.removePagesByTimeframe(startDate + 1, startDate + 8);
// Check that we have removed the correct pages.
for (let i = 0; i < 10; i++) {
do_check_eq(page_in_database(NetUtil.newURI(TEST_URI.spec + i)) == 0,
i > 0 && i < 9);
}
// Clear remaining items and check that all pages have been removed.
PlacesUtils.bhistory.removePagesByTimeframe(startDate, startDate + 9);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
yield promiseAddVisits(visits);
// Delete all pages except the first and the last.
PlacesUtils.bhistory.removePagesByTimeframe(startDate + 1, startDate + 8);
// Check that we have removed the correct pages.
for (let i = 0; i < 10; i++) {
do_check_eq(page_in_database(NetUtil.newURI(TEST_URI.spec + i)) == 0,
i > 0 && i < 9);
}
// Clear remaining items and check that all pages have been removed.
PlacesUtils.bhistory.removePagesByTimeframe(startDate, startDate + 9);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
});
add_test(function test_removePagesFromHost()
{
promiseAddVisits(TEST_URI).then(function () {
PlacesUtils.bhistory.removePagesFromHost("mozilla.com", true);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
add_task(function* test_removePagesFromHost() {
yield promiseAddVisits(TEST_URI);
PlacesUtils.bhistory.removePagesFromHost("mozilla.com", true);
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
});
add_test(function test_removePagesFromHost_keepSubdomains()
{
promiseAddVisits([{ uri: TEST_URI }, { uri: TEST_SUBDOMAIN_URI }]).then(function () {
PlacesUtils.bhistory.removePagesFromHost("mozilla.com", false);
do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
add_task(function* test_removePagesFromHost_keepSubdomains() {
yield promiseAddVisits([{ uri: TEST_URI }, { uri: TEST_SUBDOMAIN_URI }]);
PlacesUtils.bhistory.removePagesFromHost("mozilla.com", false);
do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
});
add_test(function test_removeAllPages()
{
add_task(function* test_removeAllPages() {
PlacesUtils.bhistory.removeAllPages();
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
run_next_test();
});
function run_test()
{
add_task(function* test_getObservers() {
// Ensure that getObservers() invalidates the hasHistoryEntries cache.
yield promiseAddVisits(TEST_URI);
do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
// This is just for testing purposes, never do it.
return new Promise((resolve, reject) => {
DBConn().executeSimpleSQLAsync("DELETE FROM moz_historyvisits", {
handleError: function(error) {
reject(error);
},
handleResult: function(result) {
},
handleCompletion: function(result) {
// Just invoking getObservers should be enough to invalidate the cache.
PlacesUtils.history.getObservers();
do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
resolve();
}
});
});
});
function run_test() {
run_next_test();
}

View File

@ -1,46 +1,55 @@
/* 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/. */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
// Get services.
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
let gDummyCreated = false;
let gDummyVisited = false;
let observer = {
observe: function(subject, topic, data) {
if (topic == "dummy-observer-created")
gDummyCreated = true;
else if (topic == "dummy-observer-visited")
gDummyVisited = true;
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
])
};
function verify() {
do_check_true(gDummyCreated);
do_check_true(gDummyVisited);
do_test_finished();
function run_test() {
run_next_test();
}
// main
function run_test() {
add_task(function* () {
do_load_manifest("nsDummyObserver.manifest");
os.addObserver(observer, "dummy-observer-created", true);
os.addObserver(observer, "dummy-observer-visited", true);
let dummyCreated = false;
let dummyReceivedOnVisit = false;
do_test_pending();
Services.obs.addObserver(function created() {
Services.obs.removeObserver(created, "dummy-observer-created");
dummyCreated = true;
}, "dummy-observer-created", false);
Services.obs.addObserver(function visited() {
Services.obs.removeObserver(visited, "dummy-observer-visited");
dummyReceivedOnVisit = true;
}, "dummy-observer-visited", false);
// Add a visit
promiseAddVisits(uri("http://typed.mozilla.org")).then(
function () do_timeout(1000, verify));
}
let initialObservers = PlacesUtils.history.getObservers();
// Add a common observer, it should be invoked after the category observer.
let notificationsPromised = new Promise((resolve, reject) => {
PlacesUtils.history.addObserver({
__proto__: NavHistoryObserver.prototype,
onVisit() {
let observers = PlacesUtils.history.getObservers();
Assert.equal(observers.length, initialObservers.length + 1);
// Check the common observer is the last one.
for (let i = 0; i < initialObservers.length; ++i) {
Assert.equal(initialObservers[i], observers[i]);
}
PlacesUtils.history.removeObserver(this);
observers = PlacesUtils.history.getObservers();
Assert.equal(observers.length, initialObservers.length);
// Check the category observer has been invoked before this one.
Assert.ok(dummyCreated);
Assert.ok(dummyReceivedOnVisit);
resolve();
}
}, false);
});
// Add a visit.
yield promiseAddVisits(uri("http://typed.mozilla.org"));
yield notificationsPromised;
});

View File

@ -13,7 +13,7 @@ let Cr = Components.results;
let testServices = [
["browser/nav-history-service;1", "nsINavHistoryService",
["queryStringToQueries", "removePagesByTimeframe", "removePagesFromHost",
"removeVisitsByTimeframe"]],
"removeVisitsByTimeframe", "getObservers"]],
["browser/nav-bookmarks-service;1","nsINavBookmarksService",
["createFolder", "getObservers"]],
["browser/livemark-service;2","mozIAsyncLivemarks", ["reloadLivemarks"]],

View File

@ -564,15 +564,22 @@
onEvent: function() {}
})
]]></field>
<method name="onInput">
<parameter name="aEvent"/>
<body><![CDATA[
if (!this.mIgnoreInput && this.mController.input == this) {
this.valueIsTyped = true;
this.mController.handleText();
}
this.resetActionType();
]]></body>
</method>
</implementation>
<handlers>
<handler event="input"><![CDATA[
if (!this.mIgnoreInput && this.mController.input == this) {
this.valueIsTyped = true;
this.mController.handleText();
}
this.resetActionType();
this.onInput(event);
]]></handler>
<handler event="keypress" phase="capturing"
@ -1122,7 +1129,7 @@ extends="chrome://global/content/bindings/popup.xml#popup">
// Process maxRows per chunk to improve performance and user experience
for (let i = 0; i < this.maxRows; i++) {
if (this._currentIndex >= matchCount)
return;
break;
var item;
@ -1134,9 +1141,6 @@ extends="chrome://global/content/bindings/popup.xml#popup">
getService(Components.interfaces.nsITextToSubURI).
unEscapeURIForUI("UTF-8", controller.getValueAt(this._currentIndex));
if (typeof this.input.trimValue == "function")
url = this.input.trimValue(url);
if (this._currentIndex < existingItemsCount) {
// re-use the existing item
item = this.richlistbox.childNodes[this._currentIndex];
@ -1178,8 +1182,14 @@ extends="chrome://global/content/bindings/popup.xml#popup">
this._currentIndex++;
}
// yield after each batch of items so that typing the url bar is responsive
setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
if (typeof this.onResultsAdded == "function")
this.onResultsAdded();
if (this._currentIndex < matchCount) {
// yield after each batch of items so that typing the url bar is
// responsive
setTimeout(function (self) { self._appendCurrentResult(); }, 0, this);
}
]]>
</body>
</method>
@ -1566,6 +1576,12 @@ extends="chrome://global/content/bindings/popup.xml#popup">
let title = this.getAttribute("title");
let type = this.getAttribute("type");
let displayUrl = url;
let input = this.parentNode.parentNode.input;
if (typeof input.trimValue == "function")
displayUrl = input.trimValue(url);
let emphasiseTitle = true;
let emphasiseUrl = true;
@ -1591,7 +1607,7 @@ extends="chrome://global/content/bindings/popup.xml#popup">
this.setAttribute("actiontype", action.type);
if (action.type == "switchtab") {
url = action.params.url;
displayUrl = action.params.url;
let desc = this._stringBundle.GetStringFromName("switchToTab");
this._setUpDescription(this._action, desc, true);
} else if (action.type == "searchengine") {
@ -1613,11 +1629,11 @@ extends="chrome://global/content/bindings/popup.xml#popup">
}
} else if (action.type == "visiturl") {
emphasiseUrl = false;
url = action.params.url;
displayUrl = action.params.url;
let sourceStr = this._stringBundle.GetStringFromName("visitURL");
title = this._generateEmphasisPairs(sourceStr, [
[trimURL(url), true],
[displayUrl, true],
]);
}
@ -1634,7 +1650,7 @@ extends="chrome://global/content/bindings/popup.xml#popup">
let searchEngine = "";
[title, searchEngine] = title.split(TITLE_SEARCH_ENGINE_SEPARATOR);
url = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1);
displayUrl = this._stringBundle.formatStringFromName("searchWithEngine", [searchEngine], 1);
// Remove the "search" substring so that the correct style, if any,
// is applied below.
@ -1647,7 +1663,7 @@ extends="chrome://global/content/bindings/popup.xml#popup">
let sourceStr = this._stringBundle.GetStringFromName("visitURL");
title = this._generateEmphasisPairs(sourceStr, [
[trimURL(url), true],
[displayUrl, true],
]);
types.delete("autofill");
@ -1717,9 +1733,16 @@ extends="chrome://global/content/bindings/popup.xml#popup">
this._typeImage.className = "ac-type-icon" +
(type ? " ac-result-type-" + type : "");
// Show the url as the title if we don't have a title
if (title == "")
title = url;
// Show the domain as the title if we don't have a title.
if (title == "") {
title = displayUrl;
try {
let uri = Services.io.newURI(url, null, null);
// Not all valid URLs have a domain.
if (uri.host)
title = uri.host;
} catch (e) {}
}
// Emphasize the matching search terms for the description
if (Array.isArray(title))
@ -1727,7 +1750,7 @@ extends="chrome://global/content/bindings/popup.xml#popup">
else
this._setUpDescription(this._title, title, !emphasiseTitle);
this._setUpDescription(this._url, url, !emphasiseUrl);
this._setUpDescription(this._url, displayUrl, !emphasiseUrl);
// Set up overflow on a timeout because the contents of the box
// might not have a width yet even though we just changed them

View File

@ -69,7 +69,6 @@ let DeviceActor = exports.DeviceActor = protocol.ActorClass({
let appInfo = Services.appinfo;
let win = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
desc = {
appid: appInfo.ID,
@ -83,19 +82,22 @@ let DeviceActor = exports.DeviceActor = protocol.ActorClass({
geckobuildid: appInfo.platformBuildID,
geckoversion: appInfo.platformVersion,
changeset: this._getAppIniString("App", "SourceStamp"),
useragent: win.navigator.userAgent,
locale: Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global"),
os: null,
hardware: "unknown",
processor: appInfo.XPCOMABI.split("-")[0],
compiler: appInfo.XPCOMABI.split("-")[1],
dpi: utils.displayDPI,
brandName: null,
channel: null,
profile: null,
width: win.screen.width,
height: win.screen.height
};
if (win) {
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
desc.dpi = utils.displayDPI;
desc.useragent = win.navigator.userAgent;
desc.width = win.screen.width;
desc.height = win.screen.height;
}
// Profile
let profd = Services.dirsvc.get("ProfD", Ci.nsILocalFile);

View File

@ -56,6 +56,7 @@ const protocol = require("devtools/server/protocol");
const {Arg, Option, method, RetVal, types} = protocol;
const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
const object = require("sdk/util/object");
const events = require("sdk/event/core");
const {Unknown} = require("sdk/platform/xpcom");
@ -1041,14 +1042,7 @@ var NodeListActor = exports.NodeListActor = protocol.ActorClass({
*/
items: method(function(start=0, end=this.nodeList.length) {
let items = [this.walker._ref(item) for (item of Array.prototype.slice.call(this.nodeList, start, end))];
let newParents = new Set();
for (let item of items) {
this.walker.ensurePathToRoot(item, newParents);
}
return {
nodes: items,
newParents: [node for (node of newParents)]
}
return this.walker.attachElements(items);
}, {
request: {
start: Arg(0, "nullable:number"),
@ -1298,12 +1292,43 @@ var WalkerActor = protocol.ActorClass({
cancelPick: method(function() {}),
highlight: method(function(node) {}, {request: {node: Arg(0, "nullable:domnode")}}),
/**
* Ensures that the node is attached and it can be accessed from the root.
*
* @param {(Node|NodeActor)} nodes The nodes
* @return {Object} An object compatible with the disconnectedNode type.
*/
attachElement: function(node) {
node = this._ref(node);
let newParents = this.ensurePathToRoot(node);
let { nodes, newParents } = this.attachElements([node]);
return {
node: node,
newParents: [parent for (parent of newParents)]
node: nodes[0],
newParents: newParents
};
},
/**
* Ensures that the nodes are attached and they can be accessed from the root.
*
* @param {(Node[]|NodeActor[])} nodes The nodes
* @return {Object} An object compatible with the disconnectedNodeArray type.
*/
attachElements: function(nodes) {
let nodeActors = [];
let newParents = new Set();
for (let node of nodes) {
// Be sure we deal with NodeActor only.
if (!(node instanceof NodeActor))
node = this._ref(node);
this.ensurePathToRoot(node, newParents);
// If nodes may be an array of raw nodes, we're sure to only have
// NodeActors with the following array.
nodeActors.push(node);
}
return {
nodes: nodeActors,
newParents: [...newParents]
};
},
@ -2052,8 +2077,29 @@ var WalkerActor = protocol.ActorClass({
}
}),
/**
* Set a node's innerHTML property.
*
* @param {NodeActor} node The node.
* @param {string} value The piece of HTML content.
*/
setInnerHTML: method(function(node, value) {
let rawNode = node.rawNode;
if (rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE)
throw new Error("Can only change innerHTML to element nodes");
rawNode.innerHTML = value;
}, {
request: {
node: Arg(0, "domnode"),
value: Arg(1, "string"),
},
response: {}
}),
/**
* Get a node's outerHTML property.
*
* @param {NodeActor} node The node.
*/
outerHTML: method(function(node) {
return LongStringActor(this.conn, node.rawNode.outerHTML);
@ -2068,6 +2114,9 @@ var WalkerActor = protocol.ActorClass({
/**
* Set a node's outerHTML property.
*
* @param {NodeActor} node The node.
* @param {string} value The piece of HTML content.
*/
setOuterHTML: method(function(node, value) {
let parsedDOM = DOMParser.parseFromString(value, "text/html");
@ -2116,27 +2165,104 @@ var WalkerActor = protocol.ActorClass({
}, {
request: {
node: Arg(0, "domnode"),
value: Arg(1),
value: Arg(1, "string"),
},
response: {}
}),
/**
* Insert adjacent HTML to a node.
*
* @param {Node} node
* @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
* "afterEnd" (see Element.insertAdjacentHTML).
* @param {string} value The HTML content.
*/
insertAdjacentHTML: method(function(node, position, value) {
let rawNode = node.rawNode;
// Don't insert anything adjacent to the document element,
// the head or the body.
if (node.isDocumentElement()) {
throw new Error("Can't insert adjacent element to the root.");
}
let isInsertAsSibling = position === "beforeBegin" ||
position === "afterEnd";
if ((rawNode.tagName === "BODY" || rawNode.tagName === "HEAD") &&
isInsertAsSibling) {
throw new Error("Can't insert element before or after the body " +
"or the head.");
}
let rawParentNode = rawNode.parentNode;
if (!rawParentNode && isInsertAsSibling) {
throw new Error("Can't insert as sibling without parent node.");
}
// We can't use insertAdjacentHTML, because we want to return the nodes
// being created (so the front can remove them if the user undoes
// the change). So instead, use Range.createContextualFragment().
let range = rawNode.ownerDocument.createRange();
if (position === "beforeBegin" || position === "afterEnd") {
range.selectNode(rawNode);
} else {
range.selectNodeContents(rawNode);
}
let docFrag = range.createContextualFragment(value);
let newRawNodes = Array.from(docFrag.childNodes);
switch (position) {
case "beforeBegin":
rawParentNode.insertBefore(docFrag, rawNode);
break;
case "afterEnd":
// Note: if the second argument is null, rawParentNode.insertBefore
// behaves like rawParentNode.appendChild.
rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
case "afterBegin":
rawNode.insertBefore(docFrag, rawNode.firstChild);
break;
case "beforeEnd":
rawNode.appendChild(docFrag);
break;
default:
throw new Error('Invalid position value. Must be either ' +
'"beforeBegin", "beforeEnd", "afterBegin" or "afterEnd".');
}
return this.attachElements(newRawNodes);
}, {
request: {
node: Arg(0, "domnode"),
position: Arg(1, "string"),
value: Arg(2, "string")
},
response: RetVal("disconnectedNodeArray")
}),
/**
* Test whether a node is a document or a document element.
*
* @param {NodeActor} node The node to remove.
* @return {boolean} True if the node is a document or a document element.
*/
isDocumentOrDocumentElementNode: function(node) {
return ((node.rawNode.ownerDocument &&
node.rawNode.ownerDocument.documentElement === this.rawNode) ||
node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE);
},
/**
* Removes a node from its parent node.
*
* @param {NodeActor} node The node to remove.
* @returns The node's nextSibling before it was removed.
*/
removeNode: method(function(node) {
if ((node.rawNode.ownerDocument &&
node.rawNode.ownerDocument.documentElement === this.rawNode) ||
node.rawNode.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
if (this.isDocumentOrDocumentElementNode(node))
throw Error("Cannot remove document or document elements.");
}
let nextSibling = this.nextSibling(node);
if (node.rawNode.parentNode) {
node.rawNode.parentNode.removeChild(node.rawNode);
// Mutation events will take care of the rest.
}
node.rawNode.remove();
// Mutation events will take care of the rest.
return nextSibling;
}, {
request: {
@ -2147,6 +2273,29 @@ var WalkerActor = protocol.ActorClass({
}
}),
/**
* Removes an array of nodes from their parent node.
*
* @param {NodeActor[]} nodes The nodes to remove.
*/
removeNodes: method(function(nodes) {
// Check that all nodes are valid before processing the removals.
for (let node of nodes) {
if (this.isDocumentOrDocumentElementNode(node))
throw Error("Cannot remove document or document elements.");
}
for (let node of nodes) {
node.rawNode.remove();
// Mutation events will take care of the rest.
}
}, {
request: {
node: Arg(0, "array:domnode")
},
response: {}
}),
/**
* Insert a node into the DOM.
*/
@ -2876,7 +3025,18 @@ var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
walkerActor._orphaned.add(this.conn._transport._serverConnection.getActor(top.actorID));
}
return returnNode;
}
},
removeNode: protocol.custom(Task.async(function* (node) {
let previousSibling = yield this.previousSibling(node);
let nextSibling = yield this._removeNode(node);
return {
previousSibling: previousSibling,
nextSibling: nextSibling,
};
}), {
impl: "_removeNode"
}),
});
/**

View File

@ -119,7 +119,11 @@ RootActor.prototype = {
traits: {
sources: true,
// Whether the inspector actor allows modifying outer HTML.
editOuterHTML: true,
// Whether the inspector actor allows modifying innerHTML and inserting
// adjacent HTML.
pasteHTML: true,
// Whether the server-side highlighter actor exists and can be used to
// remotely highlight nodes (see server/actors/highlighter.js)
highlightable: true,

View File

@ -28,6 +28,12 @@ function assertOwnership() {
return assertOwnershipTrees(gWalker);
}
function ignoreNode(node) {
// Duplicate the walker logic to skip blank nodes...
return node.nodeType === Components.interfaces.nsIDOMNode.TEXT_NODE &&
!/[^\s]/.test(node.nodeValue);
}
addTest(function setup() {
let url = document.getElementById("inspectorContent").href;
attachURL(url, function(err, client, tab, doc) {
@ -48,11 +54,15 @@ addTest(function testRemoveSubtree() {
let longlistID = null;
let nextSibling = gInspectee.querySelector("#longlist").nextSibling;
// Duplicate the walker logic to skip blank nodes...
while (nextSibling && nextSibling.nodeType === Components.interfaces.nsIDOMNode.TEXT_NODE && !/[^\s]/.exec(nextSibling.nodeValue)) {
while (nextSibling && ignoreNode(nextSibling)) {
nextSibling = nextSibling.nextSibling;
}
let previousSibling = gInspectee.querySelector("#longlist").previousSibling;
while (previousSibling && ignoreNode(previousSibling)) {
previousSibling = previousSibling.previousSibling;
}
promiseDone(gWalker.querySelector(gWalker.rootNode, "#longlist").then(listFront => {
longlist = listFront;
longlistID = longlist.actorID;
@ -62,13 +72,14 @@ addTest(function testRemoveSubtree() {
originalOwnershipSize = assertOwnership();
ok(originalOwnershipSize > 26, "Should have at least 26 items in our ownership tree");
return gWalker.removeNode(longlist);
}).then(nextSiblingFront => {
is(nextSiblingFront.rawNode(), nextSibling, "Should have returned the next sibling.");
}).then(siblings => {
is(siblings.previousSibling.rawNode(), previousSibling, "Should have returned the previous sibling.");
is(siblings.nextSibling.rawNode(), nextSibling, "Should have returned the next sibling.");
return waitForMutation(gWalker, isChildList);
}).then(() => {
// Our ownership size should now be 26 fewer (we forgot about #longlist + 26 children, but learned about #longlist's next sibling)
// Our ownership size should now be 25 fewer (we forgot about #longlist + 26 children, but learned about #longlist's prev/next sibling)
let newOwnershipSize = assertOwnership();
is(newOwnershipSize, originalOwnershipSize - 26, "Ownership tree should have dropped by 27 nodes");
is(newOwnershipSize, originalOwnershipSize - 25, "Ownership tree should have dropped by 25 nodes");
// Now verify that some nodes have gone away
return checkMissing(gClient, longlistID);
}).then(runNextTest));