Merge mozilla-central to inbound. a=merge CLOSED TREE
This commit is contained in:
@@ -1502,6 +1502,9 @@ pref("browser.contentblocking.reportBreakage.enabled", true);
|
||||
pref("browser.contentblocking.reportBreakage.enabled", false);
|
||||
#endif
|
||||
pref("browser.contentblocking.reportBreakage.url", "https://tracking-protection-issues.herokuapp.com/new");
|
||||
// Content Blocking has a separate pref for the intro count, since the former TP intro
|
||||
// was updated when we introduced content blocking and we want people to see it again.
|
||||
pref("browser.contentblocking.introCount", 0);
|
||||
|
||||
pref("privacy.trackingprotection.introCount", 0);
|
||||
pref("privacy.trackingprotection.introURL", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/tracking-protection/start/");
|
||||
|
||||
@@ -134,11 +134,17 @@ var ContentBlocking = {
|
||||
PREF_ANIMATIONS_ENABLED: "toolkit.cosmeticAnimations.enabled",
|
||||
PREF_REPORT_BREAKAGE_ENABLED: "browser.contentblocking.reportBreakage.enabled",
|
||||
PREF_REPORT_BREAKAGE_URL: "browser.contentblocking.reportBreakage.url",
|
||||
PREF_INTRO_COUNT_CB: "browser.contentblocking.introCount",
|
||||
PREF_INTRO_COUNT_TP: "privacy.trackingprotection.introCount",
|
||||
content: null,
|
||||
icon: null,
|
||||
activeTooltipText: null,
|
||||
disabledTooltipText: null,
|
||||
|
||||
get prefIntroCount() {
|
||||
return this.contentBlockingUIEnabled ? this.PREF_INTRO_COUNT_CB : this.PREF_INTRO_COUNT_TP;
|
||||
},
|
||||
|
||||
get appMenuLabel() {
|
||||
delete this.appMenuLabel;
|
||||
return this.appMenuLabel = document.getElementById("appMenu-tp-label");
|
||||
@@ -429,14 +435,11 @@ var ContentBlocking = {
|
||||
} else if (active && webProgress.isTopLevel) {
|
||||
this.iconBox.setAttribute("animate", "true");
|
||||
|
||||
// Open the tracking protection introduction panel, if applicable.
|
||||
if (TrackingProtection.enabledGlobally) {
|
||||
let introCount = Services.prefs.getIntPref("privacy.trackingprotection.introCount");
|
||||
if (introCount < this.MAX_INTROS) {
|
||||
Services.prefs.setIntPref("privacy.trackingprotection.introCount", ++introCount);
|
||||
Services.prefs.savePrefFile(null);
|
||||
this.showIntroPanel();
|
||||
}
|
||||
let introCount = Services.prefs.getIntPref(this.prefIntroCount);
|
||||
if (introCount < this.MAX_INTROS) {
|
||||
Services.prefs.setIntPref(this.prefIntroCount, ++introCount);
|
||||
Services.prefs.savePrefFile(null);
|
||||
this.showIntroPanel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,11 +496,8 @@ var ContentBlocking = {
|
||||
},
|
||||
|
||||
dontShowIntroPanelAgain() {
|
||||
// This function may be called in private windows, but it does not change
|
||||
// any preference unless Tracking Protection is enabled globally.
|
||||
if (TrackingProtection.enabledGlobally) {
|
||||
Services.prefs.setIntPref("privacy.trackingprotection.introCount",
|
||||
this.MAX_INTROS);
|
||||
if (!PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
|
||||
Services.prefs.setIntPref(this.prefIntroCount, this.MAX_INTROS);
|
||||
Services.prefs.savePrefFile(null);
|
||||
}
|
||||
},
|
||||
@@ -506,13 +506,39 @@ var ContentBlocking = {
|
||||
let brandBundle = document.getElementById("bundle_brand");
|
||||
let brandShortName = brandBundle.getString("brandShortName");
|
||||
|
||||
let introTitle;
|
||||
let introDescription;
|
||||
// This will be sent to the onboarding website to let them know which
|
||||
// UI variation we're showing.
|
||||
let variation;
|
||||
|
||||
if (this.contentBlockingUIEnabled) {
|
||||
introTitle = gNavigatorBundle.getFormattedString("contentBlocking.intro.title",
|
||||
[brandShortName]);
|
||||
// We show a different UI tour variation for users that already have TP
|
||||
// enabled globally.
|
||||
if (TrackingProtection.enabledGlobally) {
|
||||
introDescription = gNavigatorBundle.getString("contentBlocking.intro.v2.description");
|
||||
variation = 2;
|
||||
} else {
|
||||
introDescription = gNavigatorBundle.getFormattedString("contentBlocking.intro.v1.description",
|
||||
[brandShortName]);
|
||||
variation = 1;
|
||||
}
|
||||
} else {
|
||||
introTitle = gNavigatorBundle.getString("trackingProtection.intro.title");
|
||||
introDescription = gNavigatorBundle.getFormattedString("trackingProtection.intro.description2",
|
||||
[brandShortName]);
|
||||
variation = 0;
|
||||
}
|
||||
|
||||
let openStep2 = () => {
|
||||
// When the user proceeds in the tour, adjust the counter to indicate that
|
||||
// the user doesn't need to see the intro anymore.
|
||||
this.dontShowIntroPanelAgain();
|
||||
|
||||
let nextURL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
|
||||
"?step=2&newtab=true";
|
||||
`?step=2&newtab=true&variation=${variation}`;
|
||||
switchToTabHavingURI(nextURL, true, {
|
||||
// Ignore the fragment in case the intro is shown on the tour page
|
||||
// (e.g. if the user manually visited the tour or clicked the link from
|
||||
@@ -536,11 +562,7 @@ var ContentBlocking = {
|
||||
|
||||
let panelTarget = await UITour.getTarget(window, "trackingProtection");
|
||||
UITour.initForBrowser(gBrowser.selectedBrowser, window);
|
||||
UITour.showInfo(window, panelTarget,
|
||||
gNavigatorBundle.getString("trackingProtection.intro.title"),
|
||||
gNavigatorBundle.getFormattedString("trackingProtection.intro.description2",
|
||||
[brandShortName]),
|
||||
undefined, buttons,
|
||||
UITour.showInfo(window, panelTarget, introTitle, introDescription, undefined, buttons,
|
||||
{ closeButtonCallback: () => this.dontShowIntroPanelAgain() });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1538,10 +1538,21 @@ var BookmarkingUI = {
|
||||
let isStarred = !forceReset && this._itemGuids.size > 0;
|
||||
let label = this.stringbundleset.getAttribute(
|
||||
isStarred ? "string-editthisbookmark" : "string-bookmarkthispage");
|
||||
|
||||
let panelMenuToolbarButton =
|
||||
document.getElementById("panelMenuBookmarkThisPage");
|
||||
if (!panelMenuToolbarButton) {
|
||||
// We don't have the star UI or context menu (e.g. we're the hidden
|
||||
// window). So we just set the bookmarks menu item label and exit.
|
||||
document.getElementById("menu_bookmarkThisPage")
|
||||
.setAttribute("label", label);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let element of [
|
||||
document.getElementById("menu_bookmarkThisPage"),
|
||||
document.getElementById("context-bookmarkpage"),
|
||||
document.getElementById("panelMenuBookmarkThisPage"),
|
||||
panelMenuToolbarButton,
|
||||
]) {
|
||||
element.setAttribute("label", label);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ skip-if = (verify && debug && (os == 'win' || os == 'mac'))
|
||||
[browser_bookmarklet_windowOpen.js]
|
||||
support-files =
|
||||
bookmarklet_windowOpen_dummy.html
|
||||
[browser_bookmarkMenu_hiddenWindow.js]
|
||||
skip-if = os != 'mac' # Mac-only functionality
|
||||
[browser_bookmarks_change_title.js]
|
||||
[browser_bookmarks_sidebar_search.js]
|
||||
support-files =
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/* 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";
|
||||
|
||||
add_task(async function setup() {
|
||||
await PlacesUtils.bookmarks.insert({
|
||||
parentGuid: PlacesUtils.bookmarks.menuGuid,
|
||||
url: "http://example.com/",
|
||||
title: "Test1",
|
||||
});
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_menu_in_hidden_window() {
|
||||
let hwDoc = Services.appShell.hiddenDOMWindow.document;
|
||||
let bmPopup = hwDoc.getElementById("bookmarksMenuPopup");
|
||||
var popupEvent = hwDoc.createEvent("MouseEvent");
|
||||
popupEvent.initMouseEvent("popupshowing", true, true, Services.appShell.hiddenDOMWindow, 0,
|
||||
0, 0, 0, 0, false, false, false, false,
|
||||
0, null);
|
||||
bmPopup.dispatchEvent(popupEvent);
|
||||
|
||||
let testMenuitem = [...bmPopup.children].find(
|
||||
node => node.getAttribute("label") == "Test1");
|
||||
Assert.ok(testMenuitem,
|
||||
"Should have found the test bookmark in the hidden window bookmark menu");
|
||||
});
|
||||
@@ -8,6 +8,7 @@ support-files =
|
||||
[browser_backgroundTab.js]
|
||||
[browser_closeTab.js]
|
||||
skip-if = (verify && !debug && (os == 'linux'))
|
||||
[browser_contentBlocking.js]
|
||||
[browser_fxa.js]
|
||||
skip-if = debug || asan # updateUI leaks
|
||||
[browser_no_tabs.js]
|
||||
|
||||
136
browser/components/uitour/test/browser_contentBlocking.js
Normal file
136
browser/components/uitour/test/browser_contentBlocking.js
Normal file
@@ -0,0 +1,136 @@
|
||||
"use strict";
|
||||
|
||||
const PREF_INTRO_COUNT = "browser.contentblocking.introCount";
|
||||
const PREF_CB_UI_ENABLED = "browser.contentblocking.ui.enabled";
|
||||
const PREF_TP_ENABLED = "privacy.trackingprotection.enabled";
|
||||
const PREF_FB_ENABLED = "browser.fastblock.enabled";
|
||||
const PREF_FB_TIMEOUT = "browser.fastblock.timeout";
|
||||
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
|
||||
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
|
||||
const TOOLTIP_PANEL = document.getElementById("UITourTooltip");
|
||||
const TOOLTIP_ANCHOR = document.getElementById("tracking-protection-icon-animatable-box");
|
||||
|
||||
var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
UrlClassifierTestUtils.cleanupTestTrackers();
|
||||
Services.prefs.clearUserPref(PREF_CB_UI_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_TP_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_FB_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_FB_TIMEOUT);
|
||||
Services.prefs.clearUserPref(PREF_INTRO_COUNT);
|
||||
});
|
||||
|
||||
function allowOneIntro() {
|
||||
Services.prefs.setIntPref(PREF_INTRO_COUNT, window.ContentBlocking.MAX_INTROS - 1);
|
||||
}
|
||||
|
||||
add_task(async function setup_test() {
|
||||
Services.prefs.setBoolPref(PREF_CB_UI_ENABLED, true);
|
||||
Services.prefs.setBoolPref(PREF_TP_ENABLED, true);
|
||||
await UrlClassifierTestUtils.addTestTrackers();
|
||||
});
|
||||
|
||||
add_task(async function test_benignPage() {
|
||||
info("Load a test page not containing tracking elements");
|
||||
allowOneIntro();
|
||||
await BrowserTestUtils.withNewTab(BENIGN_PAGE, async function() {
|
||||
await Assert.rejects(waitForConditionPromise(() => {
|
||||
return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
|
||||
}, "timeout"), /timeout/, "Info panel shouldn't appear on a benign page");
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_tracking() {
|
||||
info("Load a test page containing tracking elements");
|
||||
allowOneIntro();
|
||||
await BrowserTestUtils.withNewTab({gBrowser, url: TRACKING_PAGE}, async function() {
|
||||
await new Promise((resolve, reject) => {
|
||||
waitForPopupAtAnchor(TOOLTIP_PANEL, TOOLTIP_ANCHOR, resolve,
|
||||
"Intro panel should appear");
|
||||
});
|
||||
|
||||
is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
|
||||
|
||||
let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
|
||||
"?step=2&newtab=true&variation=2";
|
||||
let buttons = document.getElementById("UITourTooltipButtons");
|
||||
|
||||
info("Click the step text and nothing should happen");
|
||||
let tabCount = gBrowser.tabs.length;
|
||||
await EventUtils.synthesizeMouseAtCenter(buttons.children[0], {});
|
||||
is(gBrowser.tabs.length, tabCount, "Same number of tabs should be open");
|
||||
|
||||
info("Resetting count to test that viewing the tour prevents future panels");
|
||||
allowOneIntro();
|
||||
|
||||
let panelHiddenPromise = promisePanelElementHidden(window, TOOLTIP_PANEL);
|
||||
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, step2URL);
|
||||
info("Clicking the main button");
|
||||
EventUtils.synthesizeMouseAtCenter(buttons.children[1], {});
|
||||
let tab = await tabPromise;
|
||||
is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS,
|
||||
"Check intro count is at the max after opening step 2");
|
||||
is(gBrowser.tabs.length, tabCount + 1, "Tour step 2 tab opened");
|
||||
await panelHiddenPromise;
|
||||
ok(true, "Panel hid when the button was clicked");
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
info("Open another tracking page and make sure we don't show the panel again");
|
||||
await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
|
||||
await Assert.rejects(waitForConditionPromise(() => {
|
||||
return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
|
||||
}, "timeout"), /timeout/, "Info panel shouldn't appear more than MAX_INTROS");
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_fastBlock() {
|
||||
Services.prefs.clearUserPref(PREF_INTRO_COUNT);
|
||||
|
||||
Services.prefs.setBoolPref(PREF_TP_ENABLED, false);
|
||||
Services.prefs.setBoolPref(PREF_FB_ENABLED, true);
|
||||
Services.prefs.setIntPref(PREF_FB_TIMEOUT, 0);
|
||||
|
||||
info("Load a test page containing tracking elements for FastBlock");
|
||||
allowOneIntro();
|
||||
await BrowserTestUtils.withNewTab({gBrowser, url: TRACKING_PAGE}, async function() {
|
||||
await new Promise((resolve, reject) => {
|
||||
waitForPopupAtAnchor(TOOLTIP_PANEL, TOOLTIP_ANCHOR, resolve,
|
||||
"Intro panel should appear");
|
||||
});
|
||||
|
||||
is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
|
||||
|
||||
let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
|
||||
"?step=2&newtab=true&variation=1";
|
||||
let buttons = document.getElementById("UITourTooltipButtons");
|
||||
|
||||
info("Click the step text and nothing should happen");
|
||||
let tabCount = gBrowser.tabs.length;
|
||||
await EventUtils.synthesizeMouseAtCenter(buttons.children[0], {});
|
||||
is(gBrowser.tabs.length, tabCount, "Same number of tabs should be open");
|
||||
|
||||
info("Resetting count to test that viewing the tour prevents future panels");
|
||||
allowOneIntro();
|
||||
|
||||
let panelHiddenPromise = promisePanelElementHidden(window, TOOLTIP_PANEL);
|
||||
let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, step2URL);
|
||||
info("Clicking the main button");
|
||||
EventUtils.synthesizeMouseAtCenter(buttons.children[1], {});
|
||||
let tab = await tabPromise;
|
||||
is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS,
|
||||
"Check intro count is at the max after opening step 2");
|
||||
is(gBrowser.tabs.length, tabCount + 1, "Tour step 2 tab opened");
|
||||
await panelHiddenPromise;
|
||||
ok(true, "Panel hid when the button was clicked");
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
info("Open another tracking page and make sure we don't show the panel again");
|
||||
await BrowserTestUtils.withNewTab(TRACKING_PAGE, async function() {
|
||||
await Assert.rejects(waitForConditionPromise(() => {
|
||||
return BrowserTestUtils.is_visible(TOOLTIP_PANEL);
|
||||
}, "timeout"), /timeout/, "Info panel shouldn't appear more than MAX_INTROS");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const PREF_INTRO_COUNT = "privacy.trackingprotection.introCount";
|
||||
const PREF_CB_ENABLED = "browser.contentblocking.enabled";
|
||||
const PREF_CB_UI_ENABLED = "browser.contentblocking.ui.enabled";
|
||||
const PREF_TP_ENABLED = "privacy.trackingprotection.enabled";
|
||||
const BENIGN_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/benignPage.html";
|
||||
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/trackingUI/trackingPage.html";
|
||||
@@ -12,7 +12,7 @@ var {UrlClassifierTestUtils} = ChromeUtils.import("resource://testing-common/Url
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
UrlClassifierTestUtils.cleanupTestTrackers();
|
||||
Services.prefs.clearUserPref(PREF_CB_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_CB_UI_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_TP_ENABLED);
|
||||
Services.prefs.clearUserPref(PREF_INTRO_COUNT);
|
||||
});
|
||||
@@ -22,7 +22,7 @@ function allowOneIntro() {
|
||||
}
|
||||
|
||||
add_task(async function setup_test() {
|
||||
Services.prefs.setBoolPref(PREF_CB_ENABLED, true);
|
||||
Services.prefs.setBoolPref(PREF_CB_UI_ENABLED, false);
|
||||
Services.prefs.setBoolPref(PREF_TP_ENABLED, true);
|
||||
await UrlClassifierTestUtils.addTestTrackers();
|
||||
});
|
||||
@@ -54,7 +54,7 @@ add_task(async function test_trackingPages() {
|
||||
is(Services.prefs.getIntPref(PREF_INTRO_COUNT), window.ContentBlocking.MAX_INTROS, "Check intro count increased");
|
||||
|
||||
let step2URL = Services.urlFormatter.formatURLPref("privacy.trackingprotection.introURL") +
|
||||
"?step=2&newtab=true";
|
||||
"?step=2&newtab=true&variation=0";
|
||||
let buttons = document.getElementById("UITourTooltipButtons");
|
||||
|
||||
info("Click the step text and nothing should happen");
|
||||
|
||||
@@ -539,6 +539,12 @@ trackingProtection.intro.description2=When you see the shield, %S is blocking so
|
||||
trackingProtection.intro.step1of3=1 of 3
|
||||
trackingProtection.intro.nextButton.label=Next
|
||||
|
||||
# LOCALIZATION NOTE (contentBlocking.intro.title): %S is brandShortName.
|
||||
contentBlocking.intro.title=New in %S: Content Blocking
|
||||
# LOCALIZATION NOTE (contentBlocking.v1.intro.description): %S is brandShortName.
|
||||
contentBlocking.intro.v1.description=When you see the shield, %S is blocking parts of the page that can slow your browsing or track you online.
|
||||
contentBlocking.intro.v2.description=The privacy benefits of Tracking Protection are now just one part of content blocking. When you see the shield, content blocking is on.
|
||||
|
||||
trackingProtection.toggle.enable.tooltip=Enable Tracking Protection
|
||||
trackingProtection.toggle.disable.tooltip=Disable Tracking Protection
|
||||
trackingProtection.toggle.enable.pbmode.tooltip=Enable Tracking Protection in Private Browsing
|
||||
|
||||
@@ -631,7 +631,7 @@ MarkupView.prototype = {
|
||||
* Highlights the node if needed, and make sure it is shown and selected in
|
||||
* the view.
|
||||
*/
|
||||
_onNewSelection: function() {
|
||||
_onNewSelection: function(nodeFront, reason) {
|
||||
const selection = this.inspector.selection;
|
||||
|
||||
if (this.htmlEditor) {
|
||||
@@ -656,20 +656,22 @@ MarkupView.prototype = {
|
||||
}
|
||||
|
||||
const slotted = selection.isSlotted();
|
||||
const onShow = this.showNode(selection.nodeFront, { slotted }).then(() => {
|
||||
// We could be destroyed by now.
|
||||
if (this._destroyer) {
|
||||
return promise.reject("markupview destroyed");
|
||||
}
|
||||
const smoothScroll = reason === "reveal-from-slot";
|
||||
const onShow = this.showNode(selection.nodeFront, { slotted, smoothScroll })
|
||||
.then(() => {
|
||||
// We could be destroyed by now.
|
||||
if (this._destroyer) {
|
||||
return promise.reject("markupview destroyed");
|
||||
}
|
||||
|
||||
// Mark the node as selected.
|
||||
const container = this.getContainer(selection.nodeFront, slotted);
|
||||
this._markContainerAsSelected(container);
|
||||
// Mark the node as selected.
|
||||
const container = this.getContainer(selection.nodeFront, slotted);
|
||||
this._markContainerAsSelected(container);
|
||||
|
||||
// Make sure the new selection is navigated to.
|
||||
this.maybeNavigateToNewSelection();
|
||||
return undefined;
|
||||
}).catch(this._handleRejectionIfNotDestroyed);
|
||||
// Make sure the new selection is navigated to.
|
||||
this.maybeNavigateToNewSelection();
|
||||
return undefined;
|
||||
}).catch(this._handleRejectionIfNotDestroyed);
|
||||
|
||||
promise.all([onShowBoxModel, onShow]).then(done);
|
||||
},
|
||||
@@ -1203,7 +1205,7 @@ MarkupView.prototype = {
|
||||
* Make sure the given node's parents are expanded and the
|
||||
* node is scrolled on to screen.
|
||||
*/
|
||||
showNode: function(node, {centered = true, slotted} = {}) {
|
||||
showNode: function(node, {centered = true, slotted, smoothScroll = false} = {}) {
|
||||
if (slotted && !this.hasContainer(node, slotted)) {
|
||||
throw new Error("Tried to show a slotted node not previously imported");
|
||||
} else {
|
||||
@@ -1217,7 +1219,7 @@ MarkupView.prototype = {
|
||||
return this._ensureVisible(node);
|
||||
}).then(() => {
|
||||
const container = this.getContainer(node, slotted);
|
||||
scrollIntoViewIfNeeded(container.editor.elt, centered);
|
||||
scrollIntoViewIfNeeded(container.editor.elt, centered, smoothScroll);
|
||||
}, this._handleRejectionIfNotDestroyed);
|
||||
},
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ add_task(async function() {
|
||||
// the scroll was performed.
|
||||
await waitUntil(() => isScrolledOut(slottedElement));
|
||||
is(isScrolledOut(slottedElement), true, "slotted element is scrolled out");
|
||||
await waitUntil(() => !isScrolledOut(realElement));
|
||||
is(isScrolledOut(realElement), false, "real element is not scrolled out");
|
||||
|
||||
info("Scroll back to see the slotted element");
|
||||
@@ -75,6 +76,7 @@ add_task(async function() {
|
||||
await clickOnRevealLink(inspector, slottedContainer);
|
||||
await waitUntil(() => isScrolledOut(slottedElement));
|
||||
is(isScrolledOut(slottedElement), true, "slotted element is scrolled out");
|
||||
await waitUntil(() => !isScrolledOut(realElement));
|
||||
is(isScrolledOut(realElement), false, "real element is not scrolled out");
|
||||
});
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ class FirefoxDataProvider {
|
||||
|
||||
switch (updateType) {
|
||||
case "securityInfo":
|
||||
this.pushRequestToQueue(actor, { securityState: networkInfo.securityInfo });
|
||||
this.pushRequestToQueue(actor, { securityState: networkInfo.securityState });
|
||||
break;
|
||||
case "responseStart":
|
||||
this.pushRequestToQueue(actor, {
|
||||
|
||||
@@ -87,7 +87,7 @@ function requestsReducer(state = Requests(), action) {
|
||||
|
||||
request = {
|
||||
...request,
|
||||
...processNetworkUpdates(action.data),
|
||||
...processNetworkUpdates(action.data, request),
|
||||
};
|
||||
const requestEndTime = request.startedMillis +
|
||||
(request.eventTimings ? request.eventTimings.totalTime : 0);
|
||||
|
||||
@@ -491,9 +491,9 @@ async function updateFormDataSections(props) {
|
||||
* incoming network update packets. It's used by Network and
|
||||
* Console panel reducers.
|
||||
*/
|
||||
function processNetworkUpdates(request = {}) {
|
||||
function processNetworkUpdates(update, request) {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(request)) {
|
||||
for (const [key, value] of Object.entries(update)) {
|
||||
if (UPDATE_PROPS.includes(key)) {
|
||||
result[key] = value;
|
||||
|
||||
@@ -501,8 +501,11 @@ function processNetworkUpdates(request = {}) {
|
||||
case "securityInfo":
|
||||
result.securityState = value.state;
|
||||
break;
|
||||
case "securityState":
|
||||
result.securityState = update.securityState || request.securityState;
|
||||
break;
|
||||
case "totalTime":
|
||||
result.totalTime = request.totalTime;
|
||||
result.totalTime = update.totalTime;
|
||||
break;
|
||||
case "requestPostData":
|
||||
result.requestHeadersFromUploadStream = value.uploadHeaders;
|
||||
|
||||
@@ -15,8 +15,11 @@ define(function(require, exports, module) {
|
||||
* true if you want it centered, false if you want it to appear on the
|
||||
* top of the viewport. It is true by default, and that is usually what
|
||||
* you want.
|
||||
* @param {Boolean} smooth
|
||||
* true if you want the scroll to happen smoothly, instead of instantly.
|
||||
* It is false by default.
|
||||
*/
|
||||
function scrollIntoViewIfNeeded(elem, centered = true) {
|
||||
function scrollIntoViewIfNeeded(elem, centered = true, smooth = false) {
|
||||
const win = elem.ownerDocument.defaultView;
|
||||
const clientRect = elem.getBoundingClientRect();
|
||||
|
||||
@@ -30,14 +33,23 @@ define(function(require, exports, module) {
|
||||
// We allow one translation on the y axis.
|
||||
let yAllowed = true;
|
||||
|
||||
// disable smooth scrolling when the user prefers reduced motion
|
||||
const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
|
||||
smooth = smooth && !reducedMotion;
|
||||
|
||||
const options = { behavior: smooth ? "smooth" : "auto" };
|
||||
|
||||
// Whatever `centered` is, the behavior is the same if the box is
|
||||
// (even partially) visible.
|
||||
if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
|
||||
win.scrollBy(0, topToBottom - elem.offsetHeight);
|
||||
win.scrollBy(Object.assign(
|
||||
{left: 0, top: topToBottom - elem.offsetHeight}, options));
|
||||
yAllowed = false;
|
||||
} else if ((bottomToTop < 0 || !centered) &&
|
||||
bottomToTop >= -elem.offsetHeight) {
|
||||
win.scrollBy(0, bottomToTop + elem.offsetHeight);
|
||||
win.scrollBy(Object.assign(
|
||||
{left: 0, top: bottomToTop + elem.offsetHeight}, options));
|
||||
|
||||
yAllowed = false;
|
||||
}
|
||||
|
||||
@@ -45,9 +57,10 @@ define(function(require, exports, module) {
|
||||
// then we center it explicitly.
|
||||
if (centered) {
|
||||
if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
|
||||
win.scroll(win.scrollX,
|
||||
win.scrollY + clientRect.top
|
||||
- (win.innerHeight - elem.offsetHeight) / 2);
|
||||
const x = win.scrollX;
|
||||
const y = win.scrollY + clientRect.top -
|
||||
(win.innerHeight - elem.offsetHeight) / 2;
|
||||
win.scroll(Object.assign({left: x, top: y}, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ const TEST_URI = TEST_URI_ROOT + "doc_layoutHelpers.html";
|
||||
|
||||
add_task(async function() {
|
||||
const [host, win] = await createHost("bottom", TEST_URI);
|
||||
runTest(win);
|
||||
await runTest(win);
|
||||
host.destroy();
|
||||
});
|
||||
|
||||
function runTest(win) {
|
||||
async function runTest(win) {
|
||||
const some = win.document.getElementById("some");
|
||||
|
||||
some.style.top = win.innerHeight + "px";
|
||||
@@ -90,4 +90,21 @@ function runTest(win) {
|
||||
"if parameter is false.");
|
||||
is(win.scrollX, xPos,
|
||||
"scrollX position has not changed.");
|
||||
|
||||
// Check smooth flag (scroll goes below the viewport)
|
||||
|
||||
info("Checking smooth flag");
|
||||
is(win.matchMedia("(prefers-reduced-motion)").matches, false,
|
||||
"Reduced motion is disabled");
|
||||
|
||||
const other = win.document.getElementById("other");
|
||||
other.style.top = win.innerHeight + "px";
|
||||
other.style.left = win.innerWidth + "px";
|
||||
win.scroll(0, 0);
|
||||
|
||||
scrollIntoViewIfNeeded(other, false, true);
|
||||
ok(win.scrollY < other.clientHeight,
|
||||
"Window has not instantly scrolled to the final position");
|
||||
await waitUntil(() => win.scrollY === other.clientHeight);
|
||||
ok(true, "Window did finish scrolling");
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
div#other {
|
||||
position: absolute;
|
||||
background: red;
|
||||
width: 2px;
|
||||
height: 300px;
|
||||
}
|
||||
iframe {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
@@ -22,3 +28,4 @@
|
||||
</style>
|
||||
|
||||
<div id=some></div>
|
||||
<div id="other"></div>
|
||||
|
||||
@@ -1226,13 +1226,11 @@ class JSTerm extends Component {
|
||||
completionText = selectedItem.label.substring(selectedItem.preLabel.length);
|
||||
}
|
||||
|
||||
if (!completionText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.insertStringAtCursor(completionText);
|
||||
this.clearCompletion();
|
||||
return true;
|
||||
|
||||
if (completionText) {
|
||||
this.insertStringAtCursor(completionText);
|
||||
}
|
||||
}
|
||||
|
||||
getInputValueBeforeCursor() {
|
||||
|
||||
@@ -314,7 +314,7 @@ function messages(state = MessageState(), action, filtersState, prefsState) {
|
||||
...networkMessagesUpdateById,
|
||||
[action.id]: {
|
||||
...request,
|
||||
...processNetworkUpdates(action.data),
|
||||
...processNetworkUpdates(action.data, request),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ stubPreparedMessages.set("GET request", new NetworkEventMessage({
|
||||
"indent": 0,
|
||||
"updates": [],
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"private": false,
|
||||
@@ -78,7 +77,6 @@ stubPreparedMessages.set("GET request update", new NetworkEventMessage({
|
||||
"totalTime": 16,
|
||||
"indent": 0,
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
|
||||
@@ -109,7 +107,6 @@ stubPreparedMessages.set("XHR GET request", new NetworkEventMessage({
|
||||
"indent": 0,
|
||||
"updates": [],
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"private": false,
|
||||
@@ -157,7 +154,6 @@ stubPreparedMessages.set("XHR GET request update", new NetworkEventMessage({
|
||||
"totalTime": 16,
|
||||
"indent": 0,
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
|
||||
@@ -188,7 +184,6 @@ stubPreparedMessages.set("XHR POST request", new NetworkEventMessage({
|
||||
"indent": 0,
|
||||
"updates": [],
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"private": false,
|
||||
@@ -236,7 +231,6 @@ stubPreparedMessages.set("XHR POST request update", new NetworkEventMessage({
|
||||
"totalTime": 10,
|
||||
"indent": 0,
|
||||
"openedOnce": false,
|
||||
"securityState": null,
|
||||
"securityInfo": null,
|
||||
"requestHeadersFromUploadStream": null,
|
||||
"url": "http://example.com/browser/devtools/client/webconsole/test/fixtures/stub-generators/inexistent.html",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
"use strict";
|
||||
|
||||
// See Bug 585991.
|
||||
// Test that the Enter keys works as expected. See Bug 585991 and 1483880.
|
||||
|
||||
const TEST_URI = `data:text/html;charset=utf-8,
|
||||
<head>
|
||||
@@ -19,6 +19,7 @@ const TEST_URI = `data:text/html;charset=utf-8,
|
||||
item1: "value1",
|
||||
item2: "value2",
|
||||
item3: "value3",
|
||||
item33: "value33",
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
@@ -37,7 +38,7 @@ async function performTests() {
|
||||
const { jsterm } = await openNewTabAndConsole(TEST_URI);
|
||||
const { autocompletePopup: popup } = jsterm;
|
||||
|
||||
const onPopUpOpen = popup.once("popup-opened");
|
||||
let onPopUpOpen = popup.once("popup-opened");
|
||||
|
||||
info("wait for completion suggestions: window.foobar.");
|
||||
|
||||
@@ -53,25 +54,45 @@ async function performTests() {
|
||||
"item1",
|
||||
"item2",
|
||||
"item3",
|
||||
"item33",
|
||||
];
|
||||
is(popup.itemCount, expectedPopupItems.length, "popup.itemCount is correct");
|
||||
is(popup.selectedIndex, 0, "First index from top is selected");
|
||||
|
||||
EventUtils.synthesizeKey("KEY_ArrowUp");
|
||||
|
||||
is(popup.selectedIndex, 3, "index 3 is selected");
|
||||
is(popup.selectedItem.label, "item3", "item3 is selected");
|
||||
is(popup.selectedIndex, expectedPopupItems.length - 1, "last index is selected");
|
||||
is(popup.selectedItem.label, "item33", "item33 is selected");
|
||||
const prefix = jsterm.getInputValue().replace(/[\S]/g, " ");
|
||||
checkJsTermCompletionValue(jsterm, prefix + "item3", "completeNode.value holds item3");
|
||||
checkJsTermCompletionValue(jsterm, prefix + "item33",
|
||||
"completeNode.value holds item33");
|
||||
|
||||
info("press Return to accept suggestion. wait for popup to hide");
|
||||
const onPopupClose = popup.once("popup-closed");
|
||||
let onPopupClose = popup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
|
||||
await onPopupClose;
|
||||
|
||||
ok(!popup.isOpen, "popup is not open after KEY_Enter");
|
||||
is(jsterm.getInputValue(), "window.foobar.item3",
|
||||
is(jsterm.getInputValue(), "window.foobar.item33",
|
||||
"completion was successful after KEY_Enter");
|
||||
ok(!getJsTermCompletionValue(jsterm), "completeNode is empty");
|
||||
|
||||
info("Test that hitting enter when the completeNode is empty closes the popup");
|
||||
onPopUpOpen = popup.once("popup-opened");
|
||||
info("wait for completion suggestions: window.foobar.item3");
|
||||
jsterm.setInputValue("window.foobar.item");
|
||||
EventUtils.sendString("3");
|
||||
await onPopUpOpen;
|
||||
|
||||
is(popup.selectedItem.label, "item3", "item3 is selected");
|
||||
ok(!getJsTermCompletionValue(jsterm), "completeNode is empty");
|
||||
|
||||
onPopupClose = popup.once("popup-closed");
|
||||
EventUtils.synthesizeKey("KEY_Enter");
|
||||
await onPopupClose;
|
||||
|
||||
ok(!popup.isOpen, "popup is not open after KEY_Enter");
|
||||
is(jsterm.getInputValue(), "window.foobar.item3",
|
||||
"completion was successful after KEY_Enter");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"use strict";
|
||||
|
||||
const TEST_FILE = "test-network-request.html";
|
||||
const TEST_PATH = "http://example.com/browser/devtools/client/webconsole/" +
|
||||
const TEST_PATH = "https://example.com/browser/devtools/client/webconsole/" +
|
||||
"test/mochitest/";
|
||||
const TEST_URI = TEST_PATH + TEST_FILE;
|
||||
|
||||
@@ -44,6 +44,10 @@ const tabs = [{
|
||||
id: "stack-trace",
|
||||
testEmpty: testEmptyStackTrace,
|
||||
testContent: testStackTrace,
|
||||
}, {
|
||||
id: "security",
|
||||
testEmpty: testEmptySecurity,
|
||||
testContent: testSecurity,
|
||||
}];
|
||||
|
||||
/**
|
||||
@@ -129,13 +133,18 @@ async function openRequestBeforeUpdates(target, hud, tab) {
|
||||
const urlNode = messageNode.querySelector(".url");
|
||||
urlNode.click();
|
||||
|
||||
// Make sure the current tab is the expected one.
|
||||
const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
|
||||
is(currentTab.getAttribute("aria-selected"), "true",
|
||||
"The correct tab is selected");
|
||||
// Except the security tab. It isn't available till the
|
||||
// "securityInfo" packet type is received, so doesn't
|
||||
// fit this part of the test.
|
||||
if (tab.id != "security") {
|
||||
// Make sure the current tab is the expected one.
|
||||
const currentTab = messageNode.querySelector(`#${tab.id}-tab`);
|
||||
is(currentTab.getAttribute("aria-selected"), "true",
|
||||
"The correct tab is selected");
|
||||
|
||||
// The tab should be empty now.
|
||||
tab.testEmpty(messageNode);
|
||||
// The tab should be empty now.
|
||||
tab.testEmpty(messageNode);
|
||||
}
|
||||
|
||||
// Wait till all updates and payload are received.
|
||||
await updates;
|
||||
@@ -158,6 +167,7 @@ async function testNetworkMessage(toolbox, messageNode) {
|
||||
await testResponse(messageNode);
|
||||
await testTimings(messageNode);
|
||||
await testStackTrace(messageNode);
|
||||
await testSecurity(messageNode);
|
||||
await waitForLazyRequests(toolbox);
|
||||
}
|
||||
|
||||
@@ -289,6 +299,24 @@ async function testStackTrace(messageNode) {
|
||||
});
|
||||
}
|
||||
|
||||
// Security
|
||||
|
||||
function testEmptySecurity(messageNode) {
|
||||
const panel = messageNode.querySelector("#security-panel .tab-panel");
|
||||
is(panel.textContent, "", "Security tab is empty");
|
||||
}
|
||||
|
||||
async function testSecurity(messageNode) {
|
||||
const securityTab = messageNode.querySelector("#security-tab");
|
||||
ok(securityTab, "Security tab is available");
|
||||
|
||||
// Select Timings tab and check the content.
|
||||
securityTab.click();
|
||||
await waitUntil(() => {
|
||||
return !!messageNode.querySelector("#security-panel .treeTable .treeRow");
|
||||
});
|
||||
}
|
||||
|
||||
// Waiting helpers
|
||||
|
||||
async function waitForPayloadReady(toolbox) {
|
||||
@@ -318,7 +346,7 @@ async function waitForRequestUpdates(toolbox) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until all lazily fetch requests in netmonitor get finsished.
|
||||
* Wait until all lazily fetch requests in netmonitor get finished.
|
||||
* Otherwise test will be shutdown too early and cause failure.
|
||||
*/
|
||||
async function waitForLazyRequests(toolbox) {
|
||||
|
||||
@@ -270,6 +270,7 @@ function transformNetworkEventPacket(packet) {
|
||||
updates: networkEvent.updates,
|
||||
cause: networkEvent.cause,
|
||||
private: networkEvent.private,
|
||||
securityState: networkEvent.securityState,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1495,6 +1495,11 @@ WebConsoleActor.prototype =
|
||||
return new Promise(resolve => {
|
||||
let messagesReceived = 0;
|
||||
const onMessage = ({ data }) => {
|
||||
// Resolve early if the console actor is destroyed
|
||||
if (!this.netmonitors) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (data.url != url) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ WebConsoleClient.prototype = {
|
||||
networkInfo.totalTime = packet.totalTime;
|
||||
break;
|
||||
case "securityInfo":
|
||||
networkInfo.securityInfo = packet.state;
|
||||
networkInfo.securityState = packet.state;
|
||||
break;
|
||||
case "responseCache":
|
||||
networkInfo.response.responseCache = packet.responseCache;
|
||||
|
||||
@@ -2894,10 +2894,13 @@ nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
static bool
|
||||
IsOrHasAncestorWithDisplayNone(Element* aElement)
|
||||
// https://html.spec.whatwg.org/#being-rendered
|
||||
//
|
||||
// With a gotcha for display contents:
|
||||
// https://github.com/whatwg/html/issues/3947
|
||||
static bool IsRendered(const Element& aElement)
|
||||
{
|
||||
return !aElement->HasServoData() || Servo_Element_IsDisplayNone(aElement);
|
||||
return aElement.GetPrimaryFrame() || aElement.IsDisplayContents();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -2958,7 +2961,7 @@ nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
|
||||
doc->FlushPendingNotifications(FlushType::Layout);
|
||||
}
|
||||
|
||||
if (IsOrHasAncestorWithDisplayNone(this)) {
|
||||
if (!IsRendered(*this)) {
|
||||
GetTextContentInternal(aValue, aError);
|
||||
} else {
|
||||
nsRange::GetInnerTextNoFlush(aValue, aError, this);
|
||||
|
||||
@@ -951,9 +951,19 @@ void
|
||||
MediaStreamGraphImpl::NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
|
||||
TrackRate aRate, uint32_t aChannels)
|
||||
{
|
||||
#ifdef ANDROID
|
||||
// On Android, mInputDeviceID is always null and represents the default
|
||||
// device.
|
||||
// The absence of an input consumer is enough to know we need to bail out
|
||||
// here.
|
||||
if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (!mInputDeviceID) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// When/if we decide to support multiple input devices per graph, this needs
|
||||
// to loop over them.
|
||||
nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
|
||||
@@ -967,6 +977,11 @@ void
|
||||
MediaStreamGraphImpl::NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
|
||||
TrackRate aRate, uint32_t aChannels)
|
||||
{
|
||||
#ifdef ANDROID
|
||||
if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
#ifdef DEBUG
|
||||
{
|
||||
MonitorAutoLock lock(mMonitor);
|
||||
@@ -979,6 +994,7 @@ MediaStreamGraphImpl::NotifyInputData(const AudioDataValue* aBuffer, size_t aFra
|
||||
if (!mInputDeviceID) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
nsTArray<RefPtr<AudioDataListener>>* listeners = mInputDeviceUsers.GetValue(mInputDeviceID);
|
||||
MOZ_ASSERT(listeners);
|
||||
for (auto& listener : *listeners) {
|
||||
@@ -990,9 +1006,15 @@ void MediaStreamGraphImpl::DeviceChangedImpl()
|
||||
{
|
||||
MOZ_ASSERT(OnGraphThread());
|
||||
|
||||
#ifdef ANDROID
|
||||
if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (!mInputDeviceID) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
nsTArray<RefPtr<AudioDataListener>>* listeners =
|
||||
mInputDeviceUsers.GetValue(mInputDeviceID);
|
||||
|
||||
@@ -468,14 +468,18 @@ public:
|
||||
{
|
||||
MOZ_ASSERT(OnGraphThreadOrNotRunning());
|
||||
|
||||
#ifdef ANDROID
|
||||
if (!mInputDeviceUsers.GetValue(mInputDeviceID)) {
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
if (!mInputDeviceID) {
|
||||
#ifndef ANDROID
|
||||
MOZ_ASSERT(mInputDeviceUsers.Count() == 0,
|
||||
"If running on a platform other than android,"
|
||||
"an explicit device id should be present");
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
uint32_t maxInputChannels = 0;
|
||||
// When/if we decide to support multiple input device per graph, this needs
|
||||
// loop over them.
|
||||
|
||||
@@ -24,9 +24,7 @@ MemoryOutputStream::Create(uint64_t aSize)
|
||||
|
||||
RefPtr<MemoryOutputStream> stream = new MemoryOutputStream();
|
||||
|
||||
char* dummy;
|
||||
uint32_t length = stream->mData.GetMutableData(&dummy, aSize, fallible);
|
||||
if (NS_WARN_IF(length != aSize)) {
|
||||
if (NS_WARN_IF(!stream->mData.SetLength(aSize, fallible))) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -1006,6 +1006,65 @@ protected: // Shouldn't be used by friend classes
|
||||
GetNextTableRowElement(Element& aTableRowElement,
|
||||
ErrorResult& aRv) const;
|
||||
|
||||
/**
|
||||
* CellIndexes store both row index and column index of a table cell.
|
||||
*/
|
||||
struct MOZ_STACK_CLASS CellIndexes final
|
||||
{
|
||||
int32_t mRow;
|
||||
int32_t mColumn;
|
||||
|
||||
/**
|
||||
* This constructor initializes mRowIndex and mColumnIndex with indexes of
|
||||
* aCellElement.
|
||||
*
|
||||
* @param aCellElement An <td> or <th> element.
|
||||
* @param aRv Returns error if layout information is not
|
||||
* available or given element is not a table cell.
|
||||
*/
|
||||
CellIndexes(Element& aCellElement, ErrorResult& aRv)
|
||||
: mRow(-1)
|
||||
, mColumn(-1)
|
||||
{
|
||||
MOZ_ASSERT(!aRv.Failed());
|
||||
Update(aCellElement, aRv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mRowIndex and mColumnIndex with indexes of aCellElement.
|
||||
*
|
||||
* @param See above.
|
||||
*/
|
||||
void Update(Element& aCellElement, ErrorResult& aRv);
|
||||
|
||||
/**
|
||||
* This constructor initializes mRowIndex and mColumnIndex with indexes of
|
||||
* cell element which contains anchor of Selection.
|
||||
*
|
||||
* @param aHTMLEditor The editor which creates the instance.
|
||||
* @param aSelection The Selection for the editor.
|
||||
* @param aRv Returns error if there is no cell element
|
||||
* which contains anchor of Selection, or layout
|
||||
* information is not available.
|
||||
*/
|
||||
CellIndexes(HTMLEditor& aHTMLEditor, Selection& aSelection,
|
||||
ErrorResult& aRv)
|
||||
: mRow(-1)
|
||||
, mColumn(-1)
|
||||
{
|
||||
Update(aHTMLEditor, aSelection, aRv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mRowIndex and mColumnIndex with indexes of cell element which
|
||||
* contains anchor of Selection.
|
||||
*
|
||||
* @param See above.
|
||||
*/
|
||||
void Update(HTMLEditor& aHTMLEditor, Selection& aSelection,
|
||||
ErrorResult& aRv);
|
||||
};
|
||||
|
||||
/**
|
||||
* PasteInternal() pasts text with replacing selected content.
|
||||
* This tries to dispatch ePaste event first. If its defaultPrevent() is
|
||||
|
||||
@@ -762,18 +762,20 @@ HTMLEditor::DeleteTableCell(int32_t aNumber)
|
||||
rv = GetFirstSelectedCell(nullptr, getter_AddRefs(firstCell));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// When 2 or more cells are selected, ignore aNumber and use selected cells.
|
||||
if (firstCell && selection->RangeCount() > 1) {
|
||||
// When > 1 selected cell,
|
||||
// ignore aNumber and use selected cells
|
||||
cell = firstCell;
|
||||
|
||||
int32_t rowCount, colCount;
|
||||
rv = GetTableSize(table, &rowCount, &colCount);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Get indexes -- may be different than original cell
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
ErrorResult error;
|
||||
CellIndexes firstCellIndexes(*firstCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
cell = firstCell;
|
||||
startRowIndex = firstCellIndexes.mRow;
|
||||
startColIndex = firstCellIndexes.mColumn;
|
||||
|
||||
// The setCaret object will call AutoSelectionSetterAfterTableEdit in its
|
||||
// destructor
|
||||
@@ -804,8 +806,12 @@ HTMLEditor::DeleteTableCell(int32_t aNumber)
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
rv = GetCellIndexes(cell, &nextRow, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes nextSelectedCellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
nextRow = nextSelectedCellIndexes.mRow;
|
||||
startColIndex = nextSelectedCellIndexes.mColumn;
|
||||
}
|
||||
// Delete entire row
|
||||
rv = DeleteRow(table, startRowIndex);
|
||||
@@ -836,8 +842,12 @@ HTMLEditor::DeleteTableCell(int32_t aNumber)
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes nextSelectedCellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = nextSelectedCellIndexes.mRow;
|
||||
nextCol = nextSelectedCellIndexes.mColumn;
|
||||
}
|
||||
// Delete entire Col
|
||||
rv = DeleteColumn(table, startColIndex);
|
||||
@@ -863,11 +873,15 @@ HTMLEditor::DeleteTableCell(int32_t aNumber)
|
||||
}
|
||||
|
||||
// The next cell to delete
|
||||
cell = nextCell;
|
||||
if (cell) {
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (nextCell) {
|
||||
CellIndexes nextCellIndexes(*nextCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = nextCellIndexes.mRow;
|
||||
startColIndex = nextCellIndexes.mColumn;
|
||||
}
|
||||
cell = nextCell;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -957,9 +971,14 @@ HTMLEditor::DeleteTableCellContents()
|
||||
|
||||
|
||||
if (firstCell) {
|
||||
ErrorResult error;
|
||||
CellIndexes firstCellIndexes(*firstCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
cell = firstCell;
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
startRowIndex = firstCellIndexes.mRow;
|
||||
startColIndex = firstCellIndexes.mColumn;
|
||||
}
|
||||
|
||||
AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
|
||||
@@ -1037,10 +1056,14 @@ HTMLEditor::DeleteTableColumn(int32_t aNumber)
|
||||
|
||||
uint32_t rangeCount = selection->RangeCount();
|
||||
|
||||
ErrorResult error;
|
||||
if (firstCell && rangeCount > 1) {
|
||||
// Fetch indexes again - may be different for selected cells
|
||||
rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes firstCellIndexes(*firstCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = firstCellIndexes.mRow;
|
||||
startColIndex = firstCellIndexes.mColumn;
|
||||
}
|
||||
//We control selection resetting after the insert...
|
||||
AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
|
||||
@@ -1053,8 +1076,12 @@ HTMLEditor::DeleteTableColumn(int32_t aNumber)
|
||||
|
||||
while (cell) {
|
||||
if (cell != firstCell) {
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = cellIndexes.mRow;
|
||||
startColIndex = cellIndexes.mColumn;
|
||||
}
|
||||
// Find the next cell in a different column
|
||||
// to continue after we delete this column
|
||||
@@ -1065,8 +1092,12 @@ HTMLEditor::DeleteTableColumn(int32_t aNumber)
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &nextCol);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = cellIndexes.mRow;
|
||||
nextCol = cellIndexes.mColumn;
|
||||
}
|
||||
rv = DeleteColumn(table, startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
@@ -1201,10 +1232,15 @@ HTMLEditor::DeleteTableRow(int32_t aNumber)
|
||||
|
||||
uint32_t rangeCount = selection->RangeCount();
|
||||
|
||||
ErrorResult error;
|
||||
if (firstCell && rangeCount > 1) {
|
||||
// Fetch indexes again - may be different for selected cells
|
||||
rv = GetCellIndexes(firstCell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes firstCellIndexes(*firstCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = firstCellIndexes.mRow;
|
||||
startColIndex = firstCellIndexes.mColumn;
|
||||
}
|
||||
|
||||
//We control selection resetting after the insert...
|
||||
@@ -1220,8 +1256,12 @@ HTMLEditor::DeleteTableRow(int32_t aNumber)
|
||||
|
||||
while (cell) {
|
||||
if (cell != firstCell) {
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
startRowIndex = cellIndexes.mRow;
|
||||
startColIndex = cellIndexes.mColumn;
|
||||
}
|
||||
// Find the next cell in a different row
|
||||
// to continue after we delete this row
|
||||
@@ -1229,9 +1269,15 @@ HTMLEditor::DeleteTableRow(int32_t aNumber)
|
||||
while (nextRow == startRowIndex) {
|
||||
rv = GetNextSelectedCell(nullptr, getter_AddRefs(cell));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (!cell) break;
|
||||
rv = GetCellIndexes(cell, &nextRow, &startColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
nextRow = cellIndexes.mRow;
|
||||
startColIndex = cellIndexes.mColumn;
|
||||
}
|
||||
// Delete entire row
|
||||
rv = DeleteRow(table, startRowIndex);
|
||||
@@ -1434,17 +1480,14 @@ HTMLEditor::SelectBlockOfCells(Element* aStartCell,
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
|
||||
|
||||
// Get starting and ending cells' location in the cellmap
|
||||
nsresult rv = GetCellIndexes(aStartCell, &startRowIndex, &startColIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
ErrorResult error;
|
||||
CellIndexes startCellIndexes(*aStartCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
rv = GetCellIndexes(aEndCell, &endRowIndex, &endColIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
CellIndexes endCellIndexes(*aEndCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
// Suppress nsISelectionListener notification
|
||||
@@ -1453,26 +1496,34 @@ HTMLEditor::SelectBlockOfCells(Element* aStartCell,
|
||||
|
||||
// Examine all cell nodes in current selection and
|
||||
// remove those outside the new block cell region
|
||||
int32_t minColumn = std::min(startColIndex, endColIndex);
|
||||
int32_t minRow = std::min(startRowIndex, endRowIndex);
|
||||
int32_t maxColumn = std::max(startColIndex, endColIndex);
|
||||
int32_t maxRow = std::max(startRowIndex, endRowIndex);
|
||||
int32_t minColumn =
|
||||
std::min(startCellIndexes.mColumn, endCellIndexes.mColumn);
|
||||
int32_t minRow =
|
||||
std::min(startCellIndexes.mRow, endCellIndexes.mRow);
|
||||
int32_t maxColumn =
|
||||
std::max(startCellIndexes.mColumn, endCellIndexes.mColumn);
|
||||
int32_t maxRow =
|
||||
std::max(startCellIndexes.mRow, endCellIndexes.mRow);
|
||||
|
||||
RefPtr<Element> cell;
|
||||
int32_t currentRowIndex, currentColIndex;
|
||||
RefPtr<nsRange> range;
|
||||
rv = GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
|
||||
nsresult rv =
|
||||
GetFirstSelectedCell(getter_AddRefs(range), getter_AddRefs(cell));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (rv == NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
while (cell) {
|
||||
rv = GetCellIndexes(cell, ¤tRowIndex, ¤tColIndex);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (currentRowIndex < maxRow || currentRowIndex > maxRow ||
|
||||
currentColIndex < maxColumn || currentColIndex > maxColumn) {
|
||||
CellIndexes currentCellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
if (currentCellIndexes.mRow < maxRow ||
|
||||
currentCellIndexes.mRow > maxRow ||
|
||||
currentCellIndexes.mColumn < maxColumn ||
|
||||
currentCellIndexes.mColumn > maxColumn) {
|
||||
selection->RemoveRange(*range, IgnoreErrors());
|
||||
// Since we've removed the range, decrement pointer to next range
|
||||
mSelectedCellIndex--;
|
||||
@@ -2625,43 +2676,85 @@ HTMLEditor::NormalizeTable(Element* aTable)
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
HTMLEditor::GetCellIndexes(Element* aCell,
|
||||
HTMLEditor::GetCellIndexes(Element* aCellElement,
|
||||
int32_t* aRowIndex,
|
||||
int32_t* aColIndex)
|
||||
int32_t* aColumnIndex)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(aRowIndex);
|
||||
*aColIndex=0; // initialize out params
|
||||
NS_ENSURE_ARG_POINTER(aColIndex);
|
||||
*aRowIndex=0;
|
||||
// Needs to stay alive while we're using aCell, since it may be keeping it
|
||||
// alive.
|
||||
// XXX Looks like it's safe to use raw pointer here. However, layout code
|
||||
// change won't be handled by editor developers so that it must be safe
|
||||
// to keep using RefPtr here.
|
||||
RefPtr<Element> cell;
|
||||
if (!aCell) {
|
||||
if (NS_WARN_IF(!aRowIndex) || NS_WARN_IF(!aColumnIndex)) {
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
}
|
||||
*aRowIndex = 0;
|
||||
*aColumnIndex = 0;
|
||||
|
||||
if (!aCellElement) {
|
||||
// Use cell element which contains anchor of Selection when aCellElement is
|
||||
// nullptr.
|
||||
RefPtr<Selection> selection = GetSelection();
|
||||
if (NS_WARN_IF(!selection)) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
// Get the selected cell or the cell enclosing the selection anchor
|
||||
cell = GetElementOrParentByTagNameAtSelection(*selection, *nsGkAtoms::td);
|
||||
if (!cell) {
|
||||
return NS_ERROR_FAILURE;
|
||||
ErrorResult error;
|
||||
CellIndexes cellIndexes(*this, *selection, error);
|
||||
if (error.Failed()) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
aCell = cell;
|
||||
*aRowIndex = cellIndexes.mRow;
|
||||
*aColumnIndex = cellIndexes.mColumn;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIPresShell> ps = GetPresShell();
|
||||
NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED);
|
||||
ErrorResult error;
|
||||
CellIndexes cellIndexes(*aCellElement, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
*aRowIndex = cellIndexes.mRow;
|
||||
*aColumnIndex = cellIndexes.mColumn;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// frames are not ref counted, so don't use an nsCOMPtr
|
||||
nsIFrame *layoutObject = aCell->GetPrimaryFrame();
|
||||
NS_ENSURE_TRUE(layoutObject, NS_ERROR_FAILURE);
|
||||
void
|
||||
HTMLEditor::CellIndexes::Update(HTMLEditor& aHTMLEditor,
|
||||
Selection& aSelection,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
MOZ_ASSERT(!aRv.Failed());
|
||||
|
||||
nsITableCellLayout *cellLayoutObject = do_QueryFrame(layoutObject);
|
||||
NS_ENSURE_TRUE(cellLayoutObject, NS_ERROR_FAILURE);
|
||||
return cellLayoutObject->GetCellIndexes(*aRowIndex, *aColIndex);
|
||||
// Guarantee the life time of the cell element since Init() will access
|
||||
// layout methods.
|
||||
RefPtr<Element> cellElement =
|
||||
aHTMLEditor.GetElementOrParentByTagNameAtSelection(aSelection,
|
||||
*nsGkAtoms::td);
|
||||
if (!cellElement) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
Update(*cellElement, aRv);
|
||||
}
|
||||
|
||||
void
|
||||
HTMLEditor::CellIndexes::Update(Element& aCellElement,
|
||||
ErrorResult& aRv)
|
||||
{
|
||||
MOZ_ASSERT(!aRv.Failed());
|
||||
|
||||
// XXX If the table cell is created immediately before this call, e.g.,
|
||||
// using innerHTML, frames have not been created yet. In such case,
|
||||
// shouldn't we flush pending layout?
|
||||
nsIFrame* frameOfCell = aCellElement.GetPrimaryFrame();
|
||||
if (NS_WARN_IF(!frameOfCell)) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
nsITableCellLayout* tableCellLayout = do_QueryFrame(frameOfCell);
|
||||
if (!tableCellLayout) {
|
||||
aRv.Throw(NS_ERROR_FAILURE); // not a cell element.
|
||||
return;
|
||||
}
|
||||
|
||||
aRv = tableCellLayout->GetCellIndexes(mRow, mColumn);
|
||||
NS_WARNING_ASSERTION(!aRv.Failed(), "Failed to get cell indexes");
|
||||
}
|
||||
|
||||
nsTableWrapperFrame*
|
||||
@@ -2884,7 +2977,7 @@ HTMLEditor::GetCellContext(Selection** aSelection,
|
||||
nsINode** aCellParent,
|
||||
int32_t* aCellOffset,
|
||||
int32_t* aRowIndex,
|
||||
int32_t* aColIndex)
|
||||
int32_t* aColumnIndex)
|
||||
{
|
||||
// Initialize return pointers
|
||||
if (aSelection) {
|
||||
@@ -2905,8 +2998,8 @@ HTMLEditor::GetCellContext(Selection** aSelection,
|
||||
if (aRowIndex) {
|
||||
*aRowIndex = 0;
|
||||
}
|
||||
if (aColIndex) {
|
||||
*aColIndex = 0;
|
||||
if (aColumnIndex) {
|
||||
*aColumnIndex = 0;
|
||||
}
|
||||
|
||||
RefPtr<Selection> selection = GetSelection();
|
||||
@@ -2969,18 +3062,17 @@ HTMLEditor::GetCellContext(Selection** aSelection,
|
||||
}
|
||||
|
||||
// Get the rest of the related data only if requested
|
||||
if (aRowIndex || aColIndex) {
|
||||
int32_t rowIndex, colIndex;
|
||||
// Get current cell location so we can put caret back there when done
|
||||
nsresult rv = GetCellIndexes(cell, &rowIndex, &colIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
if (aRowIndex || aColumnIndex) {
|
||||
ErrorResult error;
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
if (aRowIndex) {
|
||||
*aRowIndex = rowIndex;
|
||||
*aRowIndex = cellIndexes.mRow;
|
||||
}
|
||||
if (aColIndex) {
|
||||
*aColIndex = colIndex;
|
||||
if (aColumnIndex) {
|
||||
*aColumnIndex = cellIndexes.mColumn;
|
||||
}
|
||||
}
|
||||
if (aCellParent) {
|
||||
@@ -3135,7 +3227,7 @@ HTMLEditor::GetNextSelectedCell(nsRange** aRange,
|
||||
|
||||
NS_IMETHODIMP
|
||||
HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
|
||||
int32_t* aColIndex,
|
||||
int32_t* aColumnIndex,
|
||||
Element** aCell)
|
||||
{
|
||||
NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
|
||||
@@ -3143,8 +3235,8 @@ HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
|
||||
if (aRowIndex) {
|
||||
*aRowIndex = 0;
|
||||
}
|
||||
if (aColIndex) {
|
||||
*aColIndex = 0;
|
||||
if (aColumnIndex) {
|
||||
*aColumnIndex = 0;
|
||||
}
|
||||
|
||||
RefPtr<Element> cell;
|
||||
@@ -3155,22 +3247,22 @@ HTMLEditor::GetFirstSelectedCellInTable(int32_t* aRowIndex,
|
||||
// We don't want to cell.forget() here, because we use "cell" below.
|
||||
*aCell = do_AddRef(cell).take();
|
||||
|
||||
// Also return the row and/or column if requested
|
||||
if (aRowIndex || aColIndex) {
|
||||
int32_t startRowIndex, startColIndex;
|
||||
rv = GetCellIndexes(cell, &startRowIndex, &startColIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
if (aRowIndex) {
|
||||
*aRowIndex = startRowIndex;
|
||||
}
|
||||
if (aColIndex) {
|
||||
*aColIndex = startColIndex;
|
||||
}
|
||||
if (!aRowIndex && !aColumnIndex) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Also return the row and/or column if requested
|
||||
ErrorResult error;
|
||||
CellIndexes cellIndexes(*cell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
if (aRowIndex) {
|
||||
*aRowIndex = cellIndexes.mRow;
|
||||
}
|
||||
if (aColumnIndex) {
|
||||
*aColumnIndex = cellIndexes.mColumn;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -3371,19 +3463,18 @@ HTMLEditor::GetSelectedCellsType(Element* aElement,
|
||||
// Store indexes of each row/col to avoid duplication of searches
|
||||
nsTArray<int32_t> indexArray;
|
||||
|
||||
ErrorResult error;
|
||||
bool allCellsInRowAreSelected = false;
|
||||
bool allCellsInColAreSelected = false;
|
||||
while (NS_SUCCEEDED(rv) && selectedCell) {
|
||||
// Get the cell's location in the cellmap
|
||||
int32_t startRowIndex, startColIndex;
|
||||
rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
CellIndexes selectedCellIndexes(*selectedCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
if (!indexArray.Contains(startColIndex)) {
|
||||
indexArray.AppendElement(startColIndex);
|
||||
allCellsInRowAreSelected = AllCellsInRowSelected(table, startRowIndex, colCount);
|
||||
if (!indexArray.Contains(selectedCellIndexes.mColumn)) {
|
||||
indexArray.AppendElement(selectedCellIndexes.mColumn);
|
||||
allCellsInRowAreSelected =
|
||||
AllCellsInRowSelected(table, selectedCellIndexes.mRow, colCount);
|
||||
// We're done as soon as we fail for any row
|
||||
if (!allCellsInRowAreSelected) {
|
||||
break;
|
||||
@@ -3404,16 +3495,15 @@ HTMLEditor::GetSelectedCellsType(Element* aElement,
|
||||
// Start at first cell again
|
||||
rv = GetFirstSelectedCell(nullptr, getter_AddRefs(selectedCell));
|
||||
while (NS_SUCCEEDED(rv) && selectedCell) {
|
||||
// Get the cell's location in the cellmap
|
||||
int32_t startRowIndex, startColIndex;
|
||||
rv = GetCellIndexes(selectedCell, &startRowIndex, &startColIndex);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
CellIndexes selectedCellIndexes(*selectedCell, error);
|
||||
if (NS_WARN_IF(error.Failed())) {
|
||||
return error.StealNSResult();
|
||||
}
|
||||
|
||||
if (!indexArray.Contains(startRowIndex)) {
|
||||
indexArray.AppendElement(startColIndex);
|
||||
allCellsInColAreSelected = AllCellsInColumnSelected(table, startColIndex, rowCount);
|
||||
if (!indexArray.Contains(selectedCellIndexes.mRow)) {
|
||||
indexArray.AppendElement(selectedCellIndexes.mColumn);
|
||||
allCellsInColAreSelected =
|
||||
AllCellsInColumnSelected(table, selectedCellIndexes.mColumn, rowCount);
|
||||
// We're done as soon as we fail for any column
|
||||
if (!allCellsInRowAreSelected) {
|
||||
break;
|
||||
|
||||
@@ -289,6 +289,7 @@ skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure
|
||||
skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure of non-related test
|
||||
[test_nsIHTMLEditor_setCaretAfterElement.html]
|
||||
skip-if = toolkit == 'android' && debug # bug 1480702, causes permanent failure of non-related test
|
||||
[test_nsITableEditor_getCellIndexes.html]
|
||||
[test_nsITableEditor_getFirstRow.html]
|
||||
[test_resizers_appearance.html]
|
||||
[test_resizers_resizing_elements.html]
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test for nsITableEditor.getCellIndexes()</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" href="/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="display">
|
||||
</div>
|
||||
<div id="content" contenteditable></div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
|
||||
<script class="testbody" type="application/javascript">
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
SimpleTest.waitForFocus(function() {
|
||||
let editor = document.getElementById("content");
|
||||
let selection = document.getSelection();
|
||||
let rowIndex = {}, columnIndex = {};
|
||||
|
||||
try {
|
||||
getTableEditor().getCellIndexes(undefined, rowIndex, columnIndex);
|
||||
ok(false, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
|
||||
} catch (e) {
|
||||
ok(true, "nsITableEditor.getCellIndexes(undefined) should cause throwing an exception");
|
||||
}
|
||||
|
||||
try {
|
||||
getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
|
||||
ok(false, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
|
||||
} catch (e) {
|
||||
ok(true, "nsITableEditor.getCellIndexes(null) should cause throwing an exception");
|
||||
}
|
||||
|
||||
try {
|
||||
getTableEditor().getCellIndexes(editor, rowIndex, columnIndex);
|
||||
ok(false, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
|
||||
} catch (e) {
|
||||
ok(true, "nsITableEditor.getCellIndexes() should cause throwing an exception if given node is not a <td> nor a <th>");
|
||||
}
|
||||
|
||||
// Set id to "test" for the argument for getCellIndexes().
|
||||
// Set data-row and data-col to expected indexes.
|
||||
kTests = [
|
||||
'<table><tr><td id="test" data-row="0" data-col="0">cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td id="test" data-row="0" data-col="2">cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td id="test" data-row="2" data-col="0">cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td id="test" data-row="2" data-col="2">cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1" rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td rowspan="2">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td id="test" data-row="2" data-col="1">cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td id="test" data-row="0" data-col="1">cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" rowspan="2">cell2-2</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0" colspan="2">cell2-1</td><td>cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td colspan="2">cell2-1</td><td id="test" data-row="1" data-col="2">cell2-3</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td id="test" data-row="1" data-col="0">cell2-1</td><td colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><td>cell1-1</td><td>cell1-2</td><td>cell1-3</tr><tr><td>cell2-1</td><td id="test" data-row="1" data-col="1" colspan="2">cell2-2</td></tr><tr><td>cell3-1</td><td>cell3-2</td><td>cell3-3</td></tr></table>',
|
||||
'<table><tr><th id="test" data-row="0" data-col="0">cell1-1</th><th>cell1-2</th><th>cell1-3</tr><tr><th>cell2-1</th><th>cell2-2</th><th>cell2-3</th></tr><tr><th>cell3-1</th><th>cell3-2</th><th>cell3-3</th></tr></table>',
|
||||
]
|
||||
|
||||
for (const kTest of kTests) {
|
||||
editor.innerHTML = kTest;
|
||||
editor.scrollTop; // compute layout now.
|
||||
let cell = document.getElementById("test");
|
||||
getTableEditor().getCellIndexes(cell, rowIndex, columnIndex);
|
||||
is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Specified cell element directly, row Index value of ${kTest}`);
|
||||
is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Specified cell element directly, column Index value of ${kTest}`);
|
||||
selection.collapse(cell.firstChild, 0);
|
||||
getTableEditor().getCellIndexes(null, rowIndex, columnIndex);
|
||||
is(rowIndex.value.toString(10), cell.getAttribute("data-row"), `Selection is collapsed in the cell element, row Index value of ${kTest}`);
|
||||
is(columnIndex.value.toString(10), cell.getAttribute("data-col"), `Selection is collapsed in the cell element, column Index value of ${kTest}`);
|
||||
}
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
|
||||
function getTableEditor() {
|
||||
var Ci = SpecialPowers.Ci;
|
||||
var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
|
||||
return editingSession.getEditorForWindow(window).QueryInterface(Ci.nsITableEditor);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -138,12 +138,25 @@ interface nsITableEditor : nsISupports
|
||||
*/
|
||||
void normalizeTable(in Element aTable);
|
||||
|
||||
/** Get the row an column index from the layout's cellmap
|
||||
* If aCell is null, it will try to find enclosing table of selection anchor
|
||||
*
|
||||
*/
|
||||
void getCellIndexes(in Element aCell,
|
||||
out long aRowIndex, out long aColIndex);
|
||||
/**
|
||||
* getCellIndexes() computes row index and column index of a table cell.
|
||||
* Note that this depends on layout information. Therefore, all pending
|
||||
* layout should've been flushed before calling this.
|
||||
*
|
||||
* @param aCellElement If not null, this computes indexes of the cell.
|
||||
* If null, this computes indexes of a cell which
|
||||
* contains anchor of Selection.
|
||||
* @param aRowIndex Must be an object, whose .value will be set
|
||||
* to row index of the cell. 0 is the first row.
|
||||
* If rowspan is set to 2 or more, the start
|
||||
* row index is used.
|
||||
* @param aColumnIndex Must be an object, whose .value will be set
|
||||
* to column index of the cell. 0 is the first
|
||||
* column. If colspan is set to 2 or more, the
|
||||
* start column index is used.
|
||||
*/
|
||||
void getCellIndexes(in Element aCellElement,
|
||||
out long aRowIndex, out long aColumnIndex);
|
||||
|
||||
/** Get the number of rows and columns in a table from the layout's cellmap
|
||||
* If aTable is null, it will try to find enclosing table of selection ancho
|
||||
|
||||
@@ -63,6 +63,9 @@ AddMesaSysfsPaths(SandboxBroker::Policy* aPolicy)
|
||||
// Bug 1384178: Mesa driver loader
|
||||
aPolicy->AddPrefix(rdonly, "/sys/dev/char/226:");
|
||||
|
||||
// Bug 1480755: Mesa tries to probe /sys paths in turn
|
||||
aPolicy->AddAncestors("/sys/dev/char/");
|
||||
|
||||
// Bug 1401666: Mesa driver loader part 2: Mesa <= 12 using libudev
|
||||
if (auto dir = opendir("/dev/dri")) {
|
||||
while (auto entry = readdir(dir)) {
|
||||
@@ -84,10 +87,22 @@ AddMesaSysfsPaths(SandboxBroker::Policy* aPolicy)
|
||||
// broker. To match this, allow the canonical paths.
|
||||
UniqueFreePtr<char[]> realSysPath(realpath(sysPath.get(), nullptr));
|
||||
if (realSysPath) {
|
||||
nsPrintfCString ueventPath("%s/uevent", realSysPath.get());
|
||||
nsPrintfCString configPath("%s/config", realSysPath.get());
|
||||
aPolicy->AddPath(rdonly, ueventPath.get());
|
||||
aPolicy->AddPath(rdonly, configPath.get());
|
||||
static const Array<const char*, 7> kMesaAttrSuffixes = {
|
||||
"revision",
|
||||
"vendor",
|
||||
"device",
|
||||
"subsystem_vendor",
|
||||
"subsystem_device",
|
||||
"uevent",
|
||||
"config"
|
||||
};
|
||||
for (const auto attrSuffix : kMesaAttrSuffixes) {
|
||||
nsPrintfCString attrPath("%s/%s", realSysPath.get(), attrSuffix);
|
||||
aPolicy->AddPath(rdonly, attrPath.get());
|
||||
}
|
||||
// Allowing stat-ing the parent dirs
|
||||
nsPrintfCString basePath("%s/", realSysPath.get());
|
||||
aPolicy->AddAncestors(basePath.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1498,6 +1498,7 @@ or run without that action (ie: --no-{action})"
|
||||
'value': hits,
|
||||
'extraOptions': self.perfherder_resource_options(),
|
||||
'subtests': [],
|
||||
'lowerIsBetter': False
|
||||
}
|
||||
|
||||
yield {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
[getter.html]
|
||||
[<canvas><div id='target'> contents ok for element not being rendered ("<canvas><div id='target'>abc")]
|
||||
expected: FAIL
|
||||
|
||||
[<rp> ("<div><ruby>abc<rp>(</rp><rt>def</rt><rp>)</rp></ruby>")]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ testText("<div style='display:none'>abc def", "abc def", "No whitespace compre
|
||||
testText("<div style='display:none'> abc def ", " abc def ", "No removal of leading/trailing whitespace in display:none container");
|
||||
testText("<div>123<span style='display:none'>abc", "123", "display:none child not rendered");
|
||||
testText("<div style='display:none'><span id='target'>abc", "abc", "display:none container with non-display-none target child");
|
||||
testTextInSVG("<div id='target'>abc", "", "non-display-none child of svg");
|
||||
testTextInSVG("<div id='target'>abc", "abc", "non-display-none child of svg");
|
||||
testTextInSVG("<div style='display:none' id='target'>abc", "abc", "display:none child of svg");
|
||||
testTextInSVG("<div style='display:none'><div id='target'>abc", "abc", "child of display:none child of svg");
|
||||
|
||||
@@ -132,13 +132,13 @@ testText("<iframe>abc", "", "<iframe> contents ignored");
|
||||
testText("<iframe><div id='target'>abc", "", "<iframe> contents ignored");
|
||||
testText("<iframe src='data:text/html,abc'>", "","<iframe> subdocument ignored");
|
||||
testText("<audio style='display:block'>abc", "", "<audio> contents ignored");
|
||||
testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "", "<audio> contents ignored");
|
||||
testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok if display:none");
|
||||
testText("<audio style='display:block'><source id='target' class='poke' style='display:block'>", "abc", "<audio> contents ok for element not being rendered");
|
||||
testText("<audio style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<audio> contents ok for element not being rendered");
|
||||
testText("<video>abc", "", "<video> contents ignored");
|
||||
testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "", "<video> contents ignored");
|
||||
testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok if display:none");
|
||||
testText("<video style='display:block'><source id='target' class='poke' style='display:block'>", "abc", "<video> contents ok for element not being rendered");
|
||||
testText("<video style='display:block'><source id='target' class='poke' style='display:none'>", "abc", "<video> contents ok for element not being rendered");
|
||||
testText("<canvas>abc", "", "<canvas> contents ignored");
|
||||
testText("<canvas><div id='target'>abc", "", "<canvas><div id='target'> contents ignored");
|
||||
testText("<canvas><div id='target'>abc", "abc", "<canvas><div id='target'> contents ok for element not being rendered");
|
||||
testText("<img alt='abc'>", "", "<img> alt text ignored");
|
||||
testText("<img src='about:blank' class='poke'>", "", "<img> contents ignored");
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ var DownloadHistory = {
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
getList({type = Downloads.PUBLIC, maxHistoryResults} = {}) {
|
||||
DownloadCache.ensureInitialized();
|
||||
|
||||
let key = `${type}|${maxHistoryResults ? maxHistoryResults : -1}`;
|
||||
if (!this._listPromises[key]) {
|
||||
this._listPromises[key] = Downloads.getList(type).then(list => {
|
||||
@@ -92,33 +94,7 @@ var DownloadHistory = {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetFile = new FileUtils.File(download.target.path);
|
||||
let targetUri = Services.io.newFileURI(targetFile);
|
||||
|
||||
let originalPageInfo = await PlacesUtils.history.fetch(download.source.url);
|
||||
|
||||
let pageInfo = await PlacesUtils.history.insert({
|
||||
url: download.source.url,
|
||||
// In case we are downloading a file that does not correspond to a web
|
||||
// page for which the title is present, we populate the otherwise empty
|
||||
// history title with the name of the destination file, to allow it to be
|
||||
// visible and searchable in history results.
|
||||
title: (originalPageInfo && originalPageInfo.title) || targetFile.leafName,
|
||||
visits: [{
|
||||
// The start time is always available when we reach this point.
|
||||
date: download.startTime,
|
||||
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
|
||||
referrer: download.source.referrer,
|
||||
}]
|
||||
});
|
||||
|
||||
await PlacesUtils.history.update({
|
||||
annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]),
|
||||
// XXX Bug 1479445: We shouldn't have to supply both guid and url here,
|
||||
// but currently we do.
|
||||
guid: pageInfo.guid,
|
||||
url: pageInfo.url,
|
||||
});
|
||||
await DownloadCache.addDownload(download);
|
||||
|
||||
await this._updateHistoryListData(download.source.url);
|
||||
},
|
||||
@@ -163,28 +139,74 @@ var DownloadHistory = {
|
||||
download.error.reputationCheckVerdict;
|
||||
}
|
||||
|
||||
try {
|
||||
await PlacesUtils.history.update({
|
||||
annotations: new Map([[METADATA_ANNO, JSON.stringify(metaData)]]),
|
||||
url: download.source.url,
|
||||
});
|
||||
// This should be executed before any async parts, to ensure the cache is
|
||||
// updated before any notifications are activated.
|
||||
await DownloadCache.setMetadata(download.source.url, metaData);
|
||||
|
||||
await this._updateHistoryListData(download.source.url);
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
await this._updateHistoryListData(download.source.url);
|
||||
},
|
||||
|
||||
async _updateHistoryListData(sourceUrl) {
|
||||
for (let key of Object.getOwnPropertyNames(this._listPromises)) {
|
||||
let downloadHistoryList = await this._listPromises[key];
|
||||
downloadHistoryList.updateForMetadataChange(sourceUrl);
|
||||
downloadHistoryList.updateForMetaDataChange(sourceUrl,
|
||||
DownloadCache.get(sourceUrl));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This cache exists:
|
||||
* - in order to optimize the load of DownloadsHistoryList, when Places
|
||||
* annotations for history downloads must be read. In fact, annotations are
|
||||
* stored in a single table, and reading all of them at once is much more
|
||||
* efficient than an individual query.
|
||||
* - to avoid needing to do asynchronous reading of the database during download
|
||||
* list updates, which are designed to be synchronous (to improve UI
|
||||
* responsiveness).
|
||||
*
|
||||
* The cache is initialized the first time DownloadHistory.getList is called, or
|
||||
* when data is added.
|
||||
*/
|
||||
var DownloadCache = {
|
||||
_data: new Map(),
|
||||
_initialized: false,
|
||||
|
||||
/**
|
||||
* Initializes the cache, loading the data from the places database.
|
||||
*/
|
||||
ensureInitialized() {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
this._initialized = true;
|
||||
|
||||
PlacesUtils.history.addObserver(this, true);
|
||||
|
||||
// Read the metadata annotations first, but ignore invalid JSON.
|
||||
for (let result of PlacesUtils.annotations.getAnnotationsWithName(
|
||||
METADATA_ANNO)) {
|
||||
try {
|
||||
this._data.set(result.uri.spec, JSON.parse(result.annotationValue));
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
// Add the target file annotations to the metadata.
|
||||
for (let result of PlacesUtils.annotations.getAnnotationsWithName(
|
||||
DESTINATIONFILEURI_ANNO)) {
|
||||
let newData = this.get(result.uri.spec);
|
||||
newData.targetFileSpec = result.annotationValue;
|
||||
this._data.set(result.uri.spec, newData);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads current metadata from Places annotations for the specified URI, and
|
||||
* returns an object with the format:
|
||||
* This returns an object containing the meta data for the supplied URL.
|
||||
*
|
||||
* @param {String} url The url to get the meta data for.
|
||||
* @return {Object|null} Returns an empty object if there is no meta data found, or
|
||||
* an object containing the meta data. The meta data
|
||||
* will look like:
|
||||
*
|
||||
* { targetFileSpec, state, endTime, fileSize, ... }
|
||||
*
|
||||
@@ -192,71 +214,103 @@ var DownloadHistory = {
|
||||
* while the other properties are taken from "downloads/metaData". Any of the
|
||||
* properties may be missing from the object.
|
||||
*/
|
||||
getPlacesMetaDataFor(spec) {
|
||||
let metaData = {};
|
||||
|
||||
try {
|
||||
let uri = Services.io.newURI(spec);
|
||||
try {
|
||||
metaData = JSON.parse(PlacesUtils.annotations.getPageAnnotation(
|
||||
uri, METADATA_ANNO));
|
||||
} catch (ex) {}
|
||||
metaData.targetFileSpec = PlacesUtils.annotations.getPageAnnotation(
|
||||
uri, DESTINATIONFILEURI_ANNO);
|
||||
} catch (ex) {}
|
||||
|
||||
return metaData;
|
||||
get(url) {
|
||||
return this._data.get(url) || {};
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* This cache exists in order to optimize the load of DownloadsHistoryList, when
|
||||
* Places annotations for history downloads must be read. In fact, annotations
|
||||
* are stored in a single table, and reading all of them at once is much more
|
||||
* efficient than an individual query.
|
||||
*
|
||||
* When this property is first requested, it reads the annotations for all the
|
||||
* history downloads and stores them indefinitely.
|
||||
*
|
||||
* The historical annotations are not expected to change for the duration of the
|
||||
* session, except in the case where a session download is running for the same
|
||||
* URI as a history download. To avoid using stale data, consumers should
|
||||
* permanently remove from the cache any URI corresponding to a session
|
||||
* download. This is a very small mumber compared to history downloads.
|
||||
*
|
||||
* This property returns a Map from each download source URI found in Places
|
||||
* annotations to an object with the format:
|
||||
*
|
||||
* { targetFileSpec, state, endTime, fileSize, ... }
|
||||
*
|
||||
* The targetFileSpec property is the value of "downloads/destinationFileURI",
|
||||
* while the other properties are taken from "downloads/metaData". Any of the
|
||||
* properties may be missing from the object.
|
||||
*/
|
||||
XPCOMUtils.defineLazyGetter(this, "gCachedPlacesMetaData", function() {
|
||||
let placesMetaData = new Map();
|
||||
/**
|
||||
* Adds a download to the cache and the places database.
|
||||
*
|
||||
* @param {Download} download The download to add to the database and cache.
|
||||
*/
|
||||
async addDownload(download) {
|
||||
this.ensureInitialized();
|
||||
|
||||
// Read the metadata annotations first, but ignore invalid JSON.
|
||||
for (let result of PlacesUtils.annotations.getAnnotationsWithName(
|
||||
METADATA_ANNO)) {
|
||||
try {
|
||||
placesMetaData.set(result.uri.spec, JSON.parse(result.annotationValue));
|
||||
} catch (ex) {}
|
||||
}
|
||||
let targetFile = new FileUtils.File(download.target.path);
|
||||
let targetUri = Services.io.newFileURI(targetFile);
|
||||
|
||||
// Add the target file annotations to the metadata.
|
||||
for (let result of PlacesUtils.annotations.getAnnotationsWithName(
|
||||
DESTINATIONFILEURI_ANNO)) {
|
||||
let metaData = placesMetaData.get(result.uri.spec);
|
||||
if (!metaData) {
|
||||
metaData = {};
|
||||
placesMetaData.set(result.uri.spec, metaData);
|
||||
// This should be executed before any async parts, to ensure the cache is
|
||||
// updated before any notifications are activated.
|
||||
// Note: this intentionally overwrites any metadata as this is
|
||||
// the start of a new download.
|
||||
this._data.set(download.source.url, { targetFileSpec: targetUri.spec });
|
||||
|
||||
let originalPageInfo = await PlacesUtils.history.fetch(download.source.url);
|
||||
|
||||
let pageInfo = await PlacesUtils.history.insert({
|
||||
url: download.source.url,
|
||||
// In case we are downloading a file that does not correspond to a web
|
||||
// page for which the title is present, we populate the otherwise empty
|
||||
// history title with the name of the destination file, to allow it to be
|
||||
// visible and searchable in history results.
|
||||
title: (originalPageInfo && originalPageInfo.title) || targetFile.leafName,
|
||||
visits: [{
|
||||
// The start time is always available when we reach this point.
|
||||
date: download.startTime,
|
||||
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
|
||||
referrer: download.source.referrer,
|
||||
}]
|
||||
});
|
||||
|
||||
await PlacesUtils.history.update({
|
||||
annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]),
|
||||
// XXX Bug 1479445: We shouldn't have to supply both guid and url here,
|
||||
// but currently we do.
|
||||
guid: pageInfo.guid,
|
||||
url: pageInfo.url,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the metadata for a given url. If the cache already contains meta data
|
||||
* for the given url, it will be overwritten (note: the targetFileSpec will be
|
||||
* maintained).
|
||||
*
|
||||
* @param {String} url The url to set the meta data for.
|
||||
* @param {Object} metadata The new metaData to save in the cache.
|
||||
*/
|
||||
async setMetadata(url, metadata) {
|
||||
this.ensureInitialized();
|
||||
|
||||
// This should be executed before any async parts, to ensure the cache is
|
||||
// updated before any notifications are activated.
|
||||
let existingData = this.get(url);
|
||||
let newData = { ...metadata };
|
||||
if ("targetFileSpec" in existingData) {
|
||||
newData.targetFileSpec = existingData.targetFileSpec;
|
||||
}
|
||||
metaData.targetFileSpec = result.annotationValue;
|
||||
}
|
||||
this._data.set(url, newData);
|
||||
|
||||
return placesMetaData;
|
||||
});
|
||||
try {
|
||||
await PlacesUtils.history.update({
|
||||
annotations: new Map([[METADATA_ANNO, JSON.stringify(metadata)]]),
|
||||
url,
|
||||
});
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
Ci.nsINavHistoryObserver,
|
||||
Ci.nsISupportsWeakReference
|
||||
]),
|
||||
|
||||
// nsINavHistoryObserver
|
||||
onDeleteURI(uri) {
|
||||
this._data.delete(uri.spec);
|
||||
},
|
||||
onClearHistory() {
|
||||
this._data.clear();
|
||||
},
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onTitleChanged() {},
|
||||
onFrecencyChanged() {},
|
||||
onManyFrecenciesChanged() {},
|
||||
onPageChanged() {},
|
||||
onDeleteVisits() {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a download from the browser history. This object implements part
|
||||
@@ -510,8 +564,9 @@ this.DownloadHistoryList.prototype = {
|
||||
* changes.
|
||||
*
|
||||
* @param {String} sourceUrl The sourceUrl which was updated.
|
||||
* @param {Object} metaData The new meta data for the sourceUrl.
|
||||
*/
|
||||
updateForMetadataChange(sourceUrl) {
|
||||
updateForMetaDataChange(sourceUrl, metaData) {
|
||||
let slotsForUrl = this._slotsForUrl.get(sourceUrl);
|
||||
if (!slotsForUrl) {
|
||||
return;
|
||||
@@ -522,8 +577,7 @@ this.DownloadHistoryList.prototype = {
|
||||
// The visible data doesn't change, so we don't have to notify views.
|
||||
return;
|
||||
}
|
||||
slot.historyDownload.updateFromMetaData(
|
||||
DownloadHistory.getPlacesMetaDataFor(sourceUrl));
|
||||
slot.historyDownload.updateFromMetaData(metaData);
|
||||
this._notifyAllViews("onDownloadChanged", slot.download);
|
||||
}
|
||||
},
|
||||
@@ -602,9 +656,7 @@ this.DownloadHistoryList.prototype = {
|
||||
// Since the history download is visible in the slot, we also have to update
|
||||
// the object using the Places metadata.
|
||||
let historyDownload = new HistoryDownload(placesNode);
|
||||
historyDownload.updateFromMetaData(
|
||||
gCachedPlacesMetaData.get(placesNode.uri) ||
|
||||
DownloadHistory.getPlacesMetaDataFor(placesNode.uri));
|
||||
historyDownload.updateFromMetaData(DownloadCache.get(placesNode.uri));
|
||||
let slot = new DownloadSlot(this);
|
||||
slot.historyDownload = historyDownload;
|
||||
this._insertSlot({ slot, slotsForUrl, index: this._firstSessionSlotIndex });
|
||||
@@ -680,16 +732,6 @@ this.DownloadHistoryList.prototype = {
|
||||
let url = download.source.url;
|
||||
let slotsForUrl = this._slotsForUrl.get(url) || new Set();
|
||||
|
||||
// When a session download is attached to a slot, we ensure not to keep
|
||||
// stale metadata around for the corresponding history download. This
|
||||
// prevents stale state from being used if the view is rebuilt.
|
||||
//
|
||||
// Note that we will eagerly load the data in the cache at this point, even
|
||||
// if we have seen no history download. The case where no history download
|
||||
// will appear at all is rare enough in normal usage, so we can apply this
|
||||
// simpler solution rather than keeping a list of cache items to ignore.
|
||||
gCachedPlacesMetaData.delete(url);
|
||||
|
||||
// For every source URL, there can be at most one slot containing a history
|
||||
// download without an associated session download. If we find one, then we
|
||||
// can reuse it for the current session download, although we have to move
|
||||
@@ -719,6 +761,8 @@ this.DownloadHistoryList.prototype = {
|
||||
let slot = this._slotForDownload.get(download);
|
||||
this._removeSlot({ slot, slotsForUrl });
|
||||
|
||||
this._slotForDownload.delete(download);
|
||||
|
||||
// If there was only one slot for this source URL and it also contained a
|
||||
// history download, we should resurrect it in the correct area of the list.
|
||||
if (slotsForUrl.size == 0 && slot.historyDownload) {
|
||||
@@ -728,14 +772,12 @@ this.DownloadHistoryList.prototype = {
|
||||
// by the session download. Since this is no longer the case, we have to
|
||||
// read the latest metadata before resurrecting the history download.
|
||||
slot.historyDownload.updateFromMetaData(
|
||||
DownloadHistory.getPlacesMetaDataFor(url));
|
||||
DownloadCache.get(url));
|
||||
slot.sessionDownload = null;
|
||||
// Place the resurrected history slot after all the session slots.
|
||||
this._insertSlot({ slot, slotsForUrl,
|
||||
index: this._firstSessionSlotIndex });
|
||||
}
|
||||
|
||||
this._slotForDownload.delete(download);
|
||||
},
|
||||
|
||||
// DownloadList
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.import("resource://gre/modules/DownloadHistory.jsm");
|
||||
ChromeUtils.import("resource://testing-common/PlacesTestUtils.jsm");
|
||||
|
||||
let baseDate = new Date("2000-01-01");
|
||||
|
||||
/**
|
||||
* This test is designed to ensure the cache of download history is correctly
|
||||
* loaded and initialized. We do this by having the test as the only test in
|
||||
* this file, and injecting data into the places database before we start.
|
||||
*/
|
||||
add_task(async function test_DownloadHistory_initialization() {
|
||||
// Clean up at the beginning and at the end of the test.
|
||||
async function cleanup() {
|
||||
await PlacesUtils.history.clear();
|
||||
}
|
||||
registerCleanupFunction(cleanup);
|
||||
await cleanup();
|
||||
|
||||
let testDownloads = [];
|
||||
for (let i = 10; i <= 30; i += 10) {
|
||||
let targetFile = getTempFile(`${TEST_TARGET_FILE_NAME}${i}`);
|
||||
let download = {
|
||||
source: {
|
||||
url: httpUrl(`source${i}`),
|
||||
isPrivate: false
|
||||
},
|
||||
target: { path: targetFile.path },
|
||||
endTime: baseDate.getTime() + i,
|
||||
fileSize: 100 + i,
|
||||
state: i / 10,
|
||||
};
|
||||
|
||||
await PlacesTestUtils.addVisits([{
|
||||
uri: download.source.url,
|
||||
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
|
||||
}]);
|
||||
|
||||
let targetUri = Services.io.newFileURI(new FileUtils.File(download.target.path));
|
||||
|
||||
await PlacesUtils.history.update({
|
||||
annotations: new Map([
|
||||
[ "downloads/destinationFileURI", targetUri.spec ],
|
||||
[ "downloads/metaData", JSON.stringify({
|
||||
state: download.state,
|
||||
endTime: download.endTime,
|
||||
fileSize: download.fileSize,
|
||||
})]
|
||||
]),
|
||||
url: download.source.url,
|
||||
});
|
||||
|
||||
testDownloads.push(download);
|
||||
}
|
||||
|
||||
// Initialize DownloadHistoryList only after having added the history and
|
||||
// session downloads.
|
||||
let historyList = await DownloadHistory.getList();
|
||||
let downloads = await historyList.getAll();
|
||||
Assert.equal(downloads.length, testDownloads.length);
|
||||
|
||||
for (let expected of testDownloads) {
|
||||
let download = downloads.find(d => d.source.url == expected.source.url);
|
||||
|
||||
info(`Checking download ${expected.source.url}`);
|
||||
Assert.ok(download, "Should have found the expected download");
|
||||
Assert.equal(download.endTime, expected.endTime,
|
||||
"Should have the correct end time");
|
||||
Assert.equal(download.target.size, expected.fileSize,
|
||||
"Should have the correct file size");
|
||||
Assert.equal(download.succeeded, expected.state == 1,
|
||||
"Should have the correct succeeded value");
|
||||
Assert.equal(download.canceled, expected.state == 3,
|
||||
"Should have the correct canceled value");
|
||||
Assert.equal(download.target.path, expected.target.path,
|
||||
"Should have the correct target path");
|
||||
}
|
||||
});
|
||||
@@ -9,6 +9,7 @@ support-files =
|
||||
|
||||
[test_DownloadCore.js]
|
||||
[test_DownloadHistory.js]
|
||||
[test_DownloadHistory_initialization.js]
|
||||
[test_DownloadIntegration.js]
|
||||
[test_DownloadLegacy.js]
|
||||
[test_DownloadList.js]
|
||||
|
||||
Reference in New Issue
Block a user