mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-09 19:35:51 +00:00
merge fx-team to mozilla-central a=merge
This commit is contained in:
commit
5969f2ac89
@ -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/");
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
@ -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);
|
||||
});
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
62
browser/base/content/test/general/browser_bug1070778.js
Normal file
62
browser/base/content/test/general/browser_bug1070778.js
Normal 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);
|
||||
});
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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});
|
||||
|
@ -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();
|
||||
},
|
||||
|
||||
|
@ -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();
|
||||
},
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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/"})
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
|
@ -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() {
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;"
|
||||
|
@ -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]
|
||||
|
@ -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.");
|
||||
|
326
browser/devtools/inspector/test/browser_inspector_menu-02.js
Normal file
326
browser/devtools/inspector/test/browser_inspector_menu-02.js
Normal 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}"`;
|
||||
}
|
||||
});
|
21
browser/devtools/inspector/test/doc_inspector_menu-02.html
Normal file
21
browser/devtools/inspector/test/doc_inspector_menu-02.html
Normal 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>
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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"},
|
||||
|
@ -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'
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
@ -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.
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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>
|
||||
|
184
browser/devtools/timeline/widgets/marker-details.js
Normal file
184
browser/devtools/timeline/widgets/marker-details.js
Normal 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;
|
@ -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));
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.");
|
||||
|
@ -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|.
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
76
toolkit/components/places/tests/PlacesTestUtils.jsm
Normal file
76
toolkit/components/places/tests/PlacesTestUtils.jsm
Normal 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();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
@ -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,
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
TEST_DIRS += ['cpp']
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'PlacesTestUtils.jsm',
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += [
|
||||
'autocomplete/xpcshell.ini',
|
||||
'bookmarks/xpcshell.ini',
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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() {},
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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"]],
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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"
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user