Merge mozilla-central to inbound. a=merge CLOSED TREE

This commit is contained in:
Noemi Erli
2018-08-22 19:32:56 +03:00
41 changed files with 1076 additions and 346 deletions

View File

@@ -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/");

View File

@@ -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() });
},
};

View File

@@ -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);
}

View File

@@ -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 =

View File

@@ -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");
});

View File

@@ -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]

View 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");
});
});

View File

@@ -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");

View File

@@ -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

View File

@@ -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);
},

View File

@@ -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");
});

View File

@@ -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, {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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));
}
}
}

View File

@@ -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");
}

View File

@@ -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>

View File

@@ -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() {

View File

@@ -314,7 +314,7 @@ function messages(state = MessageState(), action, filtersState, prefsState) {
...networkMessagesUpdateById,
[action.id]: {
...request,
...processNetworkUpdates(action.data),
...processNetworkUpdates(action.data, request),
}
}
};

View File

@@ -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",

View File

@@ -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");
}

View File

@@ -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) {

View File

@@ -270,6 +270,7 @@ function transformNetworkEventPacket(packet) {
updates: networkEvent.updates,
cause: networkEvent.cause,
private: networkEvent.private,
securityState: networkEvent.securityState,
});
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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, &currentRowIndex, &currentColIndex);
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;

View File

@@ -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]

View File

@@ -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>

View File

@@ -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

View File

@@ -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());
}
}
}

View File

@@ -1498,6 +1498,7 @@ or run without that action (ie: --no-{action})"
'value': hits,
'extraOptions': self.perfherder_resource_options(),
'subtests': [],
'lowerIsBetter': False
}
yield {

View File

@@ -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

View File

@@ -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");

View File

@@ -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

View File

@@ -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");
}
});

View File

@@ -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]