Merge autoland to mozilla-central. a=merge

This commit is contained in:
Noemi Erli 2018-08-02 11:51:57 +03:00
commit 63b27e89bf
164 changed files with 2187 additions and 683 deletions

File diff suppressed because one or more lines are too long

View File

@ -216,14 +216,15 @@ panelview[mainview] > .panel-header {
display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
}
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected] {
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[selected],
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[multiselected] {
position: relative;
z-index: 2;
pointer-events: none; /* avoid blocking dragover events on scroll buttons */
}
.tabbrowser-tab[tabdrop-samewindow],
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]) {
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[fadein]:not([selected]):not([multiselected]) {
transition: transform 200ms var(--animation-easing-function);
}

View File

@ -149,6 +149,8 @@ window._gBrowser = {
_clearMultiSelectionLocked: false,
_clearMultiSelectionLockedOnce: false,
/**
* Tab close requests are ignored if the window is closing anyway,
* e.g. when holding Ctrl+W.
@ -3695,6 +3697,10 @@ window._gBrowser = {
clearMultiSelectedTabs(updatePositionalAttributes) {
if (this._clearMultiSelectionLocked) {
if (this._clearMultiSelectionLockedOnce) {
this._clearMultiSelectionLockedOnce = false;
this._clearMultiSelectionLocked = false;
}
return;
}
@ -3713,6 +3719,11 @@ window._gBrowser = {
}
},
lockClearMultiSelectionOnce() {
this._clearMultiSelectionLockedOnce = true;
this._clearMultiSelectionLocked = true;
},
/**
* Remove the active tab from the multiselection if it's the only one left there.
*/

View File

@ -557,7 +557,8 @@
if (this.getAttribute("movingtab") != "true") {
this.setAttribute("movingtab", "true");
this.parentNode.setAttribute("movingtab", "true");
this.selectedItem = draggedTab;
if (!draggedTab.multiselected)
this.selectedItem = draggedTab;
}
if (!("animLastScreenX" in draggedTab._dragData))
@ -567,6 +568,9 @@
if (screenX == draggedTab._dragData.animLastScreenX)
return;
// Direction of the mouse movement.
let ltrMove = screenX > draggedTab._dragData.animLastScreenX;
draggedTab._dragData.animLastScreenX = screenX;
let rtl = (window.getComputedStyle(this).direction == "rtl");
@ -575,46 +579,61 @@
let tabs = this._getVisibleTabs()
.slice(pinned ? 0 : numPinned,
pinned ? numPinned : undefined);
let movingTabs = draggedTab._dragData.movingTabs;
if (rtl) {
tabs.reverse();
// Copy moving tabs array to avoid infinite reversing.
movingTabs = [...movingTabs].reverse();
}
let tabWidth = draggedTab.getBoundingClientRect().width;
let shiftWidth = tabWidth * movingTabs.length;
draggedTab._dragData.tabWidth = tabWidth;
// Move the dragged tab based on the mouse position.
let leftTab = tabs[0];
let rightTab = tabs[tabs.length - 1];
let tabScreenX = draggedTab.boxObject.screenX;
let rightMovingTabScreenX = movingTabs[movingTabs.length - 1].boxObject.screenX;
let leftMovingTabScreenX = movingTabs[0].boxObject.screenX;
let translateX = screenX - draggedTab._dragData.screenX;
if (!pinned) {
translateX += this.arrowScrollbox._scrollbox.scrollLeft - draggedTab._dragData.scrollX;
}
let leftBound = leftTab.boxObject.screenX - tabScreenX;
let leftBound = leftTab.boxObject.screenX - leftMovingTabScreenX;
let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
(tabScreenX + tabWidth);
translateX = Math.max(translateX, leftBound);
translateX = Math.min(translateX, rightBound);
draggedTab.style.transform = "translateX(" + translateX + "px)";
(rightMovingTabScreenX + tabWidth);
translateX = Math.min(Math.max(translateX, leftBound), rightBound);
for (let tab of movingTabs) {
tab.style.transform = "translateX(" + translateX + "px)";
}
draggedTab._dragData.translateX = translateX;
// Determine what tab we're dragging over.
// * Point of reference is the center of the dragged tab. If that
// * Single tab dragging: Point of reference is the center of the dragged tab. If that
// point touches a background tab, the dragged tab would take that
// tab's position when dropped.
// * Multiple tabs dragging: All dragged tabs are one "giant" tab with two
// points of reference (center of tabs on the extremities). When
// mouse is moving from left to right, the right reference gets activated,
// otherwise the left reference will be used. Everything else works the same
// as single tab dragging.
// * We're doing a binary search in order to reduce the amount of
// tabs we need to check.
let tabCenter = tabScreenX + translateX + tabWidth / 2;
tabs = tabs.filter(t => !movingTabs.includes(t) || t == draggedTab);
let leftTabCenter = leftMovingTabScreenX + translateX + tabWidth / 2;
let rightTabCenter = rightMovingTabScreenX + translateX + tabWidth / 2;
let tabCenter = ltrMove ? rightTabCenter : leftTabCenter;
let newIndex = -1;
let oldIndex = "animDropIndex" in draggedTab._dragData ?
draggedTab._dragData.animDropIndex : draggedTab._tPos;
draggedTab._dragData.animDropIndex : movingTabs[0]._tPos;
let low = 0;
let high = tabs.length - 1;
while (low <= high) {
let mid = Math.floor((low + high) / 2);
if (tabs[mid] == draggedTab &&
++mid > high)
if (tabs[mid] == draggedTab && ++mid > high)
break;
let boxObject = tabs[mid].boxObject;
screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex);
@ -645,9 +664,9 @@
function getTabShift(tab, dropIndex) {
if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
return rtl ? -tabWidth : tabWidth;
return (rtl ? -shiftWidth : shiftWidth);
if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
return rtl ? tabWidth : -tabWidth;
return (rtl ? shiftWidth : -shiftWidth);
return 0;
}
]]></body>
@ -1168,6 +1187,29 @@
// node to deliver the `dragend` event. See bug 1345473.
dt.addElement(tab);
// Regroup all selected tabs around the dragged tab
// for multiple tabs dragging
if (tab.multiselected) {
let selectedTabs = gBrowser.selectedTabs;
let draggedTabPos = tab._tPos;
// Move left selected tabs
let insertAtPos = draggedTabPos - 1;
for (let i = selectedTabs.indexOf(tab) - 1; i > -1; i--) {
let movingTab = selectedTabs[i];
gBrowser.moveTabTo(movingTab, insertAtPos);
insertAtPos--;
}
// Move right selected tabs
insertAtPos = draggedTabPos + 1;
for (let i = selectedTabs.indexOf(tab) + 1; i < selectedTabs.length; i++) {
let movingTab = selectedTabs[i];
gBrowser.moveTabTo(movingTab, insertAtPos);
insertAtPos++;
}
}
// Create a canvas to which we capture the current tab.
// Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
// canvas size (in CSS pixels) to the window's backing resolution in order
@ -1241,7 +1283,9 @@
offsetX: event.screenX - window.screenX - tabOffsetX,
offsetY: event.screenY - window.screenY,
scrollX: this.arrowScrollbox._scrollbox.scrollLeft,
screenX: event.screenX
screenX: event.screenX,
movingTabs: (tab.multiselected ? gBrowser.selectedTabs : [tab])
.filter(t => t.pinned == tab.pinned)
};
event.stopPropagation();
@ -1344,11 +1388,13 @@
var dt = event.dataTransfer;
var dropEffect = dt.dropEffect;
var draggedTab;
let movingTabs;
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
// not our drop then
if (!draggedTab)
return;
movingTabs = draggedTab._dragData.movingTabs;
}
this._tabDropIndicator.collapsed = true;
@ -1374,34 +1420,45 @@
let dropIndex = "animDropIndex" in draggedTab._dragData &&
draggedTab._dragData.animDropIndex;
if (dropIndex && dropIndex > draggedTab._tPos)
let incrementDropIndex = true;
if (dropIndex && dropIndex > movingTabs[0]._tPos) {
dropIndex--;
incrementDropIndex = false;
}
let animate = gBrowser.animationsEnabled;
if (oldTranslateX && oldTranslateX != newTranslateX && animate) {
draggedTab.setAttribute("tabdrop-samewindow", "true");
draggedTab.style.transform = "translateX(" + newTranslateX + "px)";
let onTransitionEnd = transitionendEvent => {
if (transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != draggedTab) {
return;
}
draggedTab.removeEventListener("transitionend", onTransitionEnd);
for (let tab of movingTabs) {
tab.setAttribute("tabdrop-samewindow", "true");
tab.style.transform = "translateX(" + newTranslateX + "px)";
let onTransitionEnd = transitionendEvent => {
if (transitionendEvent.propertyName != "transform" ||
transitionendEvent.originalTarget != tab) {
return;
}
tab.removeEventListener("transitionend", onTransitionEnd);
draggedTab.removeAttribute("tabdrop-samewindow");
tab.removeAttribute("tabdrop-samewindow");
this._finishAnimateTabMove();
if (dropIndex !== false) {
gBrowser.moveTabTo(draggedTab, dropIndex);
}
this._finishAnimateTabMove();
if (dropIndex !== false) {
gBrowser.moveTabTo(tab, dropIndex);
if (incrementDropIndex)
dropIndex++;
}
gBrowser.syncThrobberAnimations(draggedTab);
};
draggedTab.addEventListener("transitionend", onTransitionEnd);
gBrowser.syncThrobberAnimations(tab);
};
tab.addEventListener("transitionend", onTransitionEnd);
}
} else {
this._finishAnimateTabMove();
if (dropIndex !== false) {
gBrowser.moveTabTo(draggedTab, dropIndex);
for (let tab of movingTabs) {
gBrowser.moveTabTo(tab, dropIndex);
if (incrementDropIndex)
dropIndex++;
}
}
}
} else if (draggedTab) {
@ -1973,6 +2030,10 @@
<handler event="mousedown" phase="capturing">
<![CDATA[
if (event.button == 0 && !this.selected && this.multiselected) {
gBrowser.lockClearMultiSelectionOnce();
}
let tabContainer = this.parentNode;
if (tabContainer._closeTabByDblclick &&
event.button == 0 &&

View File

@ -410,8 +410,6 @@ skip-if = verify
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tabDrop.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tabReorder.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tab_detach_restore.js]
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
[browser_tab_drag_drop_perwindow.js]

View File

@ -7,6 +7,6 @@
<meta charset="utf8">
</head>
<body>
<iframe src="http://tracking.example.com/"></iframe>
<iframe src="http://trackertest.org/"></iframe>
</body>
</html>

View File

@ -30,6 +30,7 @@ support-files =
[browser_multiselect_tabs_pin_unpin.js]
[browser_multiselect_tabs_positional_attrs.js]
[browser_multiselect_tabs_reload.js]
[browser_multiselect_tabs_reorder.js]
[browser_multiselect_tabs_using_Ctrl.js]
[browser_multiselect_tabs_using_selectedTabs.js]
[browser_multiselect_tabs_using_Shift_and_Ctrl.js]
@ -58,6 +59,7 @@ skip-if = (verify && (os == 'win' || os == 'mac'))
skip-if = (debug && os == 'mac') || (debug && os == 'linux' && bits == 64) #Bug 1421183, disabled on Linux/OSX for leaked windows
[browser_tabCloseProbes.js]
[browser_tabReorder_overflow.js]
[browser_tabReorder.js]
[browser_tabSpinnerProbe.js]
skip-if = !e10s # Tab spinner is e10s only.
[browser_tabSwitchPrintPreview.js]

View File

@ -0,0 +1,54 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(async function() {
let tab0 = gBrowser.selectedTab;
let tab1 = await addTab();
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
let tab5 = await addTab();
let tabs = [tab0, tab1, tab2, tab3, tab4, tab5];
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab3, { ctrlKey: true });
await triggerClickOn(tab5, { ctrlKey: true });
is(gBrowser.selectedTab, tab1, "Tab1 is active");
is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
for (let i of [1, 3, 5]) {
ok(tabs[i].multiselected, "Tab" + i + " is multiselected");
}
for (let i of [0, 2, 4]) {
ok(!tabs[i].multiselected, "Tab" + i + " is not multiselected");
}
for (let i of [0, 1, 2, 3, 4, 5]) {
is(tabs[i]._tPos, i, "Tab" + i + " position is :" + i);
}
await dragAndDrop(tab3, tab4, false);
is(gBrowser.selectedTab, tab3, "Dragged tab (tab3) is now active");
is(gBrowser.selectedTabs.length, 3, "Three selected tabs");
for (let i of [1, 3, 5]) {
ok(tabs[i].multiselected, "Tab" + i + " is still multiselected");
}
for (let i of [0, 2, 4]) {
ok(!tabs[i].multiselected, "Tab" + i + " is still not multiselected");
}
is(tab0._tPos, 0, "Tab0 position (0) doesn't change");
// Multiselected tabs gets grouped at the start of the slide.
is(tab1._tPos, tab3._tPos - 1, "Tab1 is located right at the left of the dragged tab (tab3)");
is(tab5._tPos, tab3._tPos + 1, "Tab5 is located right at the right of the dragged tab (tab3)");
is(tab3._tPos, 4, "Dragged tab (tab3) position is 4");
is(tab4._tPos, 2, "Drag target (tab4) has shifted to position 2");
for (let tab of tabs.filter(t => t != tab0))
BrowserTestUtils.removeTab(tab);
});

View File

@ -18,23 +18,6 @@ add_task(async function() {
is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
async function dragAndDrop(tab1, tab2, copy) {
let rect = tab2.getBoundingClientRect();
let event = {
ctrlKey: copy,
altKey: copy,
clientX: rect.left + rect.width / 2 + 10,
clientY: rect.top + rect.height / 2,
};
let originalTPos = tab1._tPos;
EventUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
if (!copy) {
await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
"Waiting for tab position to be updated");
}
}
await dragAndDrop(newTab1, newTab2, false);
is(gBrowser.tabs.length, initialTabsLength + 3, "tabs are still there");
is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");

View File

@ -12,11 +12,7 @@ add_task(async function() {
let tabs = gBrowser.tabs;
let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth);
let rect = ele => ele.getBoundingClientRect();
let width = ele => rect(ele).width;
let height = ele => rect(ele).height;
let left = ele => rect(ele).left;
let top = ele => rect(ele).top;
let width = ele => ele.getBoundingClientRect().width;
let tabCountForOverflow = Math.ceil(width(arrowScrollbox) / tabMinWidth);
@ -39,19 +35,7 @@ add_task(async function() {
is(gBrowser.tabs[initialTabsLength + 1], newTab2, "newTab2 position is correct");
is(gBrowser.tabs[initialTabsLength + 2], newTab3, "newTab3 position is correct");
async function dragAndDrop(tab1, tab2) {
let event = {
clientX: left(tab2) + width(tab2) / 2 + 10,
clientY: top(tab2) + height(tab2) / 2,
};
let originalTPos = tab1._tPos;
EventUtils.synthesizeDrop(tab1, tab2, null, "move", window, window, event);
await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
"Waiting for tab position to be updated");
}
await dragAndDrop(newTab1, newTab2);
await dragAndDrop(newTab1, newTab2, false);
is(gBrowser.tabs.length, tabCountForOverflow, "tabs are still there");
is(gBrowser.tabs[initialTabsLength], newTab2, "newTab2 and newTab1 are swapped");
is(gBrowser.tabs[initialTabsLength + 1], newTab1, "newTab1 and newTab2 are swapped");

View File

@ -157,3 +157,20 @@ async function test_mute_tab(tab, icon, expectMuted) {
return mutedPromise;
}
async function dragAndDrop(tab1, tab2, copy) {
let rect = tab2.getBoundingClientRect();
let event = {
ctrlKey: copy,
altKey: copy,
clientX: rect.left + rect.width / 2 + 10,
clientY: rect.top + rect.height / 2,
};
let originalTPos = tab1._tPos;
EventUtils.synthesizeDrop(tab1, tab2, null, copy ? "copy" : "move", window, window, event);
if (!copy) {
await BrowserTestUtils.waitForCondition(() => tab1._tPos != originalTPos,
"Waiting for tab position to be updated");
}
}

View File

@ -3,9 +3,24 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
[
{
"namespace": "manifest",
"types": [
{
"$extend": "OptionalPermission",
"choices": [{
"type": "string",
"enum": [
"search"
]
}]
}
]
},
{
"namespace": "search",
"description": "Use browser.search to interact with search engines.",
"permissions": ["search"],
"types": [
{
"id": "SearchEngine",

View File

@ -3,6 +3,7 @@ prefs =
dom.animations-api.core.enabled=true
dom.animations-api.timelines.enabled=true
support-files =
silence.ogg
head.js
head_pageAction.js
head_sessions.js
@ -40,6 +41,7 @@ support-files =
[browser_ExtensionControlledPopup.js]
[browser_ext_addon_debugging_netmonitor.js]
[browser_ext_autoplayInBackground.js]
[browser_ext_browserAction_area.js]
[browser_ext_browserAction_experiment.js]
[browser_ext_browserAction_context.js]
@ -108,7 +110,7 @@ skip-if = (verify && (os == 'linux' || os == 'mac'))
[browser_ext_menus_events.js]
[browser_ext_menus_refresh.js]
[browser_ext_omnibox.js]
skip-if = (debug && (os == 'linux' || os == 'mac')) || (verify && (os == 'linux' || os == 'mac')) # Bug 1417052
skip-if = (debug && (os == 'linux' || os == 'mac')) || (verify && (os == 'linux' || os == 'mac')) # Bug 1416103 (was bug 1417052)
[browser_ext_openPanel.js]
skip-if = (verify && !debug && (os == 'linux' || os == 'mac'))
[browser_ext_optionsPage_browser_style.js]

View File

@ -0,0 +1,48 @@
"use strict";
function setup_test_preference(enableScript) {
return SpecialPowers.pushPrefEnv({"set": [
["media.autoplay.default", 1],
["media.autoplay.enabled.user-gestures-needed", true],
["media.autoplay.ask-permission", true],
["media.autoplay.allow-extension-background-pages", enableScript],
]});
}
async function testAutoplayInBackgroundScript(enableScript) {
info(`- setup test preference, enableScript=${enableScript} -`);
await setup_test_preference(enableScript);
let extension = ExtensionTestUtils.loadExtension({
background() {
browser.test.log("- create audio in background page -");
let audio = new Audio();
audio.src = "https://example.com/browser/browser/components/extensions/test/browser/silence.ogg";
audio.play().then(function() {
browser.test.log("play succeed!");
browser.test.sendMessage("play-succeed");
}, function() {
browser.test.log("play promise was rejected!");
browser.test.sendMessage("play-failed");
});
},
});
await extension.startup();
if (enableScript) {
await extension.awaitMessage("play-succeed");
ok(true, "play promise was resolved!");
} else {
await extension.awaitMessage("play-failed");
ok(true, "play promise was rejected!");
}
await extension.unload();
}
add_task(async function testMain() {
await testAutoplayInBackgroundScript(true /* enable autoplay */);
await testAutoplayInBackgroundScript(false /* enable autoplay */);
});

View File

@ -3,7 +3,9 @@
"use strict";
add_task(async function() {
let keyword = "test";
// This keyword needs to be unique to prevent history entries from unrelated
// tests from appearing in the suggestions list.
let keyword = "VeryUniqueKeywordThatDoesNeverMatchAnyTestUrl";
let extension = ExtensionTestUtils.loadExtension({
manifest: {

View File

@ -28,7 +28,7 @@ add_task(async function test_search() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
permissions: ["search", "tabs"],
name: TEST_ID,
"browser_action": {},
"chrome_settings_overrides": {
@ -74,7 +74,7 @@ add_task(async function test_search_notab() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
permissions: ["search", "tabs"],
name: TEST_ID,
"browser_action": {},
"chrome_settings_overrides": {

Binary file not shown.

View File

@ -5,4 +5,3 @@ support-files =
tags = webextensions
[test_ext_all_apis.html]
skip-if = true

View File

@ -16,7 +16,6 @@ let expectedContentApisTargetSpecific = [
];
let expectedBackgroundApisTargetSpecific = [
"search.get",
"tabs.MutedInfoReason",
"tabs.TAB_ID_NONE",
"tabs.TabStatus",
@ -34,6 +33,7 @@ let expectedBackgroundApisTargetSpecific = [
"tabs.getCurrent",
"tabs.getZoom",
"tabs.getZoomSettings",
"tabs.highlight",
"tabs.insertCSS",
"tabs.move",
"tabs.onActivated",

View File

@ -17,6 +17,7 @@ rules:
mixins-before-declarations: [2, {exclude: [breakpoint, mq]}]
nesting-depth: [2, {max-depth: 4}]
no-debug: 1
no-disallowed-properties: [1, {properties: [text-transform]}]
no-duplicate-properties: 2
no-misspelled-properties: [2, {extra-properties: [-moz-context-properties]}]
no-url-domains: 0

View File

@ -308,7 +308,9 @@ AboutNewTabService.prototype = {
get activityStreamLocale() {
// Pick the best available locale to match the app locales
return Services.locale.negotiateLanguages(
Services.locale.getAppLocalesAsBCP47(),
// Fix up incorrect BCP47 that are actually lang tags as a workaround for
// bug 1479606 returning the wrong values in the content process
Services.locale.getAppLocalesAsBCP47().map(l => l.replace(/^(ja-JP-mac)$/, "$1os")),
ACTIVITY_STREAM_BCP47,
// defaultLocale's strings aren't necessarily packaged, but en-US' are
"en-US",

View File

@ -118,6 +118,7 @@ for (const type of [
// as call-to-action buttons in snippets, onboarding tour, etc.
const ASRouterActions = {};
for (const type of [
"INSTALL_ADDON_FROM_URL",
"OPEN_PRIVATE_BROWSER_WINDOW",
"OPEN_URL",
"OPEN_ABOUT_PAGE"

View File

@ -0,0 +1,29 @@
# Activity Stream Router
## Preferences `browser.newtab.activity-stream.asrouter.*`
Name | Used for | Type | Example value
--- | --- | --- | ---
`whitelistHosts` | Whitelist a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]`
`snippetsUrl` | The main remote endpoint that serves all snippet messages | `String` | `https://activity-stream-icons.services.mozilla.com/v1/messages.json.br`
## Admin Interface
* Navigate to `about:newtab#asrouter`
* See all available messages and message providers
* Block, unblock or force show a specific message
## Snippet Preview
* Whitelist the provider host that will serve the messages
* In `about:config`, `browser.newtab.activity-stream.asrouter.whitelistHosts` can contain a array of hosts
* Example value `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]`
* Errors are surfaced in the `Console` tab of the `Browser Toolbox` ([read how to enable](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox))
* Navigate to `about:newtab?endpoint=<URL>`
* Example `https://gist.githubusercontent.com/piatra/70234f08696c0a0509d7ba5568cd830f/raw/68370f34abc134142c64b6f0a9b9258a06de7aa3/messages.json`
* URL should be from an endpoint that was just whitelisted
* The snippet preview should imediately load
* The endpoint must be HTTPS, the host must be whitelisted
* Errors are surfaced in the `Console` tab of the `Browser Toolbox`
### [Snippet message format documentation](https://github.com/mozilla/activity-stream/blob/master/content-src/asrouter/schemas/message-format.md)

View File

@ -191,7 +191,7 @@ export class ASRouterUISurface extends React.PureComponent {
// If we are loading about:welcome we want to trigger the onboarding messages
if (this.props.document.location.href === "about:welcome") {
ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: "firstRun"}});
ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
} else {
ASRouterUtils.sendMessage({type: "CONNECT_UI_REQUEST", data: {endpoint}});
}

View File

@ -36,11 +36,27 @@
},
"targeting": {
"type": "string",
"description": "a JEXL expression representing targeting information"
"description": "A JEXL expression representing targeting information"
},
"trigger": {
"type": "string",
"description": "A string representing what the trigger to show this message is."
"type": "object",
"description": "An action to trigger potentially showing the message",
"properties": {
"id": {
"type": "string",
"description": "A string identifying the trigger action",
"enum": ["firstRun", "openURL"]
},
"params": {
"type": "array",
"description": "An optional array of string parameters for the trigger action",
"items": {
"type": "string",
"description": "A parameter for the trigger action"
}
}
},
"required": ["id"]
},
"frequency": {
"type": "object",

View File

@ -127,7 +127,7 @@
overflow: hidden;
padding-bottom: 4px;
text-overflow: ellipsis;
text-transform: uppercase;
text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties
white-space: nowrap;
}

View File

@ -8,7 +8,6 @@
font-size: $section-title-font-size;
font-weight: bold;
margin: 0;
text-transform: uppercase;
span {
color: var(--newtab-section-header-text-color);

View File

@ -134,7 +134,7 @@ $half-base-gutter: $base-gutter / 2;
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase;
text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties
&::before {
content: attr(data-fallback);
@ -299,7 +299,6 @@ $half-base-gutter: $base-gutter / 2;
}
.section-title {
text-transform: none;
font-size: 16px;
margin: 0 0 16px;
}

View File

@ -67,7 +67,7 @@ export const LinkMenuOptions = {
icon: "dismiss",
action: ac.AlsoToMain({
type: at.BLOCK_URL,
data: {url: site.url, pocket_id: site.pocket_id}
data: {url: site.open_url || site.url, pocket_id: site.pocket_id}
}),
impression: ac.ImpressionStats({
source: eventSource,

View File

@ -625,7 +625,6 @@ main {
margin-inline-start: 32px;
pointer-events: none; }
.topsite-form .form-input-container .section-title {
text-transform: none;
font-size: 16px;
margin: 0 0 16px; }
@ -1569,8 +1568,7 @@ a.firstrun-link {
.collapsible-section .section-title {
font-size: 13px;
font-weight: bold;
margin: 0;
text-transform: uppercase; }
margin: 0; }
.collapsible-section .section-title span {
color: var(--newtab-section-header-text-color);
display: inline-block;

File diff suppressed because one or more lines are too long

View File

@ -628,7 +628,6 @@ main {
margin-inline-start: 32px;
pointer-events: none; }
.topsite-form .form-input-container .section-title {
text-transform: none;
font-size: 16px;
margin: 0 0 16px; }
@ -1572,8 +1571,7 @@ a.firstrun-link {
.collapsible-section .section-title {
font-size: 13px;
font-weight: bold;
margin: 0;
text-transform: uppercase; }
margin: 0; }
.collapsible-section .section-title span {
color: var(--newtab-section-header-text-color);
display: inline-block;

File diff suppressed because one or more lines are too long

View File

@ -625,7 +625,6 @@ main {
margin-inline-start: 32px;
pointer-events: none; }
.topsite-form .form-input-container .section-title {
text-transform: none;
font-size: 16px;
margin: 0 0 16px; }
@ -1569,8 +1568,7 @@ a.firstrun-link {
.collapsible-section .section-title {
font-size: 13px;
font-weight: bold;
margin: 0;
text-transform: uppercase; }
margin: 0; }
.collapsible-section .section-title span {
color: var(--newtab-section-header-text-color);
display: inline-block;

File diff suppressed because one or more lines are too long

View File

@ -213,7 +213,7 @@ for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM
// as call-to-action buttons in snippets, onboarding tour, etc.
const ASRouterActions = {};
for (const type of ["OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE"]) {
for (const type of ["INSTALL_ADDON_FROM_URL", "OPEN_PRIVATE_BROWSER_WINDOW", "OPEN_URL", "OPEN_ABOUT_PAGE"]) {
ASRouterActions[type] = type;
}
@ -1132,7 +1132,7 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
// If we are loading about:welcome we want to trigger the onboarding messages
if (this.props.document.location.href === "about:welcome") {
ASRouterUtils.sendMessage({ type: "TRIGGER", data: { trigger: "firstRun" } });
ASRouterUtils.sendMessage({ type: "TRIGGER", data: { trigger: { id: "firstRun" } } });
} else {
ASRouterUtils.sendMessage({ type: "CONNECT_UI_REQUEST", data: { endpoint } });
}
@ -2751,7 +2751,7 @@ const LinkMenuOptions = {
icon: "dismiss",
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].BLOCK_URL,
data: { url: site.url, pocket_id: site.pocket_id }
data: { url: site.open_url || site.url, pocket_id: site.pocket_id }
}),
impression: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].ImpressionStats({
source: eventSource,

File diff suppressed because one or more lines are too long

View File

@ -539,3 +539,20 @@ This reports the user's interaction with Activity Stream Router.
"event": ["CLICK_BUTTION" | "BLOCK"]
}
```
### Targeting error pings
This reports when an error has occurred when parsing/evaluating a JEXL targeting string in a message.
```js
{
"client_id": "n/a",
"action": "asrouter_undesired_event",
"addon_version": "20180710100040",
"impression_id": "{005deed0-e3e4-4c02-a041-17405fd703f6}",
"locale": "en-US",
"message_id": "some_message_id",
"event": "TARGETING_EXPRESSION_ERROR",
"value": ["MALFORMED_EXPRESSION" | "OTHER_ERROR"]
}
```

View File

@ -140,7 +140,8 @@ module.exports = function(config) {
exclude: [
path.resolve("test"),
path.resolve("vendor"),
path.resolve("lib/ASRouterTargeting.jsm")
path.resolve("lib/ASRouterTargeting.jsm"),
path.resolve("lib/ASRouterTriggerListeners.jsm")
]
}
]

View File

@ -5,12 +5,15 @@
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
const {ASRouterActions: ra} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {ASRouterActions: ra, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm", {});
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
@ -105,6 +108,16 @@ const MessageLoaderUtils = {
.map(msg => ({...msg, provider: provider.id}));
const lastUpdated = Date.now();
return {messages, lastUpdated};
},
async installAddonFromURL(browser, url) {
try {
const aUri = Services.io.newURI(url);
const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
const install = await AddonManager.getInstallForURL(aUri.spec, "application/x-xpinstall");
await AddonManager.installAddonFromWebpage("application/x-xpinstall", browser,
systemPrincipal, install);
} catch (e) {}
}
};
@ -122,6 +135,7 @@ class _ASRouter {
constructor(initialState = {}) {
this.initialized = false;
this.messageChannel = null;
this.dispatchToAS = null;
this._storage = null;
this._resetInitialization();
this._state = {
@ -132,7 +146,9 @@ class _ASRouter {
messages: [],
...initialState
};
this._triggerHandler = this._triggerHandler.bind(this);
this.onMessage = this.onMessage.bind(this);
this._handleTargetingError = this._handleTargetingError.bind(this);
}
_addASRouterPrefListener() {
@ -212,7 +228,23 @@ class _ASRouter {
newState.messages = [...newState.messages, ...messages];
}
}
await this.setState(newState);
// Some messages have triggers that require us to initalise trigger listeners
const unseenListeners = new Set(ASRouterTriggerListeners.keys());
for (const {trigger} of newState.messages) {
if (trigger && ASRouterTriggerListeners.has(trigger.id)) {
ASRouterTriggerListeners.get(trigger.id).init(this._triggerHandler, trigger.params);
unseenListeners.delete(trigger.id);
}
}
// We don't need these listeners, but they may have previously been
// initialised, so uninitialise them
for (const triggerID of unseenListeners) {
ASRouterTriggerListeners.get(triggerID).uninit();
}
// We don't want to cache preview endpoints, remove them after messages are fetched
await this.setState(this._removePreviewEndpoint(newState));
await this.cleanupImpressions();
}
}
@ -223,14 +255,16 @@ class _ASRouter {
*
* @param {RemotePageManager} channel a RemotePageManager instance
* @param {obj} storage an AS storage instance
* @param {func} dispatchToAS dispatch an action the main AS Store
* @memberof _ASRouter
*/
async init(channel, storage) {
async init(channel, storage, dispatchToAS) {
this.messageChannel = channel;
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this._addASRouterPrefListener();
this._storage = storage;
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
this.dispatchToAS = dispatchToAS;
const blockList = await this._storage.get("blockList") || [];
const impressions = await this._storage.get("impressions") || {};
@ -245,11 +279,16 @@ class _ASRouter {
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
this.messageChannel.removeMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this.messageChannel = null;
this.dispatchToAS = null;
this.state.providers.forEach(provider => {
if (provider.endpointPref) {
Services.prefs.removeObserver(provider.endpointPref, this);
}
});
// Uninitialise all trigger listeners
for (const listener of ASRouterTriggerListeners.values()) {
listener.uninit();
}
this._resetInitialization();
}
@ -270,26 +309,31 @@ class _ASRouter {
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: state});
}
async _findMessage(messages, target, data = {}) {
let message;
const {trigger} = data;
_handleTargetingError(type, error, message) {
Cu.reportError(error);
if (this.dispatchToAS) {
this.dispatchToAS(ac.ASRouterUserEvent({
message_id: message.id,
action: "asrouter_undesired_event",
event: "TARGETING_EXPRESSION_ERROR",
value: type
}));
}
}
_findMessage(messages, target, trigger) {
const {impressions} = this.state;
if (trigger) {
// Find a message that matches the targeting context as well as the trigger context
message = await ASRouterTargeting.findMatchingMessageWithTrigger({messages, impressions, target, trigger});
}
if (!message) {
// If there was no messages with this trigger, try finding a regular targeted message
message = await ASRouterTargeting.findMatchingMessage({messages, impressions, target});
}
return message;
// Find a message that matches the targeting context as well as the trigger context (if one is provided)
// If no trigger is provided, we should find a message WITHOUT a trigger property defined.
return ASRouterTargeting.findMatchingMessage({messages, impressions, trigger, onError: this._handleTargetingError});
}
_orderBundle(bundle) {
return bundle.sort((a, b) => a.order - b.order);
}
async _getBundledMessages(originalMessage, target, data, force = false) {
async _getBundledMessages(originalMessage, target, trigger, force = false) {
let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
// First, find all messages of same template. These are potential matching targeting candidates
@ -308,7 +352,7 @@ class _ASRouter {
} else {
while (bundledMessagesOfSameTemplate.length) {
// Find a message that matches the targeting context - or break if there are no matching messages
const message = await this._findMessage(bundledMessagesOfSameTemplate, target, data);
const message = await this._findMessage(bundledMessagesOfSameTemplate, target, trigger);
if (!message) {
/* istanbul ignore next */ // Code coverage in mochitests
break;
@ -337,11 +381,11 @@ class _ASRouter {
return state.messages.filter(item => !state.blockList.includes(item.id));
}
async _sendMessageToTarget(message, target, data, force = false) {
async _sendMessageToTarget(message, target, trigger, force = false) {
let bundledMessages;
// If this message needs to be bundled with other messages of the same template, find them and bundle them together
if (message && message.bundled) {
bundledMessages = await this._getBundledMessages(message, target, data, force);
bundledMessages = await this._getBundledMessages(message, target, trigger, force);
}
if (message && !message.bundled) {
// If we only need to send 1 message, send the message
@ -423,8 +467,7 @@ class _ASRouter {
});
}
async sendNextMessage(target, action = {}) {
let {data} = action;
async sendNextMessage(target, trigger) {
const msgs = this._getUnblockedMessages();
let message = null;
const previewMsgs = this.state.messages.filter(item => item.provider === "preview");
@ -432,11 +475,19 @@ class _ASRouter {
if (previewMsgs.length) {
[message] = previewMsgs;
} else {
message = await this._findMessage(msgs, target, data);
message = await this._findMessage(msgs, target, trigger);
}
await this.setState({lastMessageId: message ? message.id : null});
await this._sendMessageToTarget(message, target, data);
if (previewMsgs.length) {
// We don't want to cache preview messages, remove them after we selected the message to show
await this.setState(state => ({
lastMessageId: message.id,
messages: state.messages.filter(m => m.id !== message.id)
}));
} else {
await this.setState({lastMessageId: message ? message.id : null});
}
await this._sendMessageToTarget(message, target, trigger);
}
async setMessageById(id, target, force = true, action = {}) {
@ -511,10 +562,19 @@ class _ASRouter {
}, {...DEFAULT_WHITELIST_HOSTS});
}
// To be passed to ASRouterTriggerListeners
async _triggerHandler(target, trigger) {
await this.onMessage({target, data: {type: "TRIGGER", trigger}});
}
_removePreviewEndpoint(state) {
state.providers = state.providers.filter(p => p.id !== "preview");
return state;
}
async _addPreviewEndpoint(url) {
const providers = [...this.state.providers];
if (this._validPreviewEndpoint(url) && !providers.find(p => p.url === url)) {
// Set update cycle to 0 to fetch new content on every page refresh
providers.push({id: "preview", type: "remote", url, updateCycleInMs: 0});
await this.setState({providers});
}
@ -532,7 +592,7 @@ class _ASRouter {
}
// Check if any updates are needed first
await this.loadMessagesFromAllProviders();
await this.sendNextMessage(target, action);
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
break;
case ra.OPEN_PRIVATE_BROWSER_WINDOW:
// Forcefully open about:privatebrowsing
@ -584,6 +644,9 @@ class _ASRouter {
case "IMPRESSION":
this.addImpression(action.data);
break;
case ra.INSTALL_ADDON_FROM_URL:
await MessageLoaderUtils.installAddonFromURL(target.browser, action.data.url);
break;
}
}
}

View File

@ -14,8 +14,11 @@ class ASRouterFeed {
}
async enable() {
await this.router.init(this.store._messageChannel.channel,
this.store.dbStorage.getDbTable("snippets"));
await this.router.init(
this.store._messageChannel.channel,
this.store.dbStorage.getDbTable("snippets"),
this.store.dispatch
);
// Disable onboarding
Services.prefs.setBoolPref(ONBOARDING_FINISHED_PREF, true);
}

View File

@ -11,6 +11,8 @@ ChromeUtils.defineModuleGetter(this, "ShellService",
const FXA_USERNAME_PREF = "services.sync.username";
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
const MOZ_JEXL_FILEPATH = "mozjexl";
// Max possible cap for any message
const MAX_LIFETIME_CAP = 100;
const ONE_DAY = 24 * 60 * 60 * 1000;
@ -141,10 +143,24 @@ const TargetingGetters = {
this.ASRouterTargeting = {
Environment: TargetingGetters,
isMatch(filterExpression, target, context = this.Environment) {
ERROR_TYPES: {
MALFORMED_EXPRESSION: "MALFORMED_EXPRESSION",
OTHER_ERROR: "OTHER_ERROR"
},
isMatch(filterExpression, context = this.Environment) {
return FilterExpressions.eval(filterExpression, context);
},
isTriggerMatch(trigger = {}, candidateMessageTrigger = {}) {
if (trigger.id !== candidateMessageTrigger.id) {
return false;
} else if (!candidateMessageTrigger.params) {
return true;
}
return candidateMessageTrigger.params.includes(trigger.param);
},
isBelowFrequencyCap(message, impressionsForMessage) {
if (!message.frequency || !impressionsForMessage || !impressionsForMessage.length) {
return true;
@ -174,44 +190,58 @@ this.ASRouterTargeting = {
return true;
},
/**
* checkMessageTargeting - Checks is a message's targeting parameters are satisfied
*
* @param {*} message An AS router message
* @param {obj} context A FilterExpression context
* @param {func} onError A function to handle errors (takes two params; error, message)
* @returns
*/
async checkMessageTargeting(message, context, onError) {
// If no targeting is specified,
if (!message.targeting) {
return true;
}
let result;
try {
result = await this.isMatch(message.targeting, context);
} catch (error) {
Cu.reportError(error);
if (onError) {
const type = error.fileName.includes(MOZ_JEXL_FILEPATH) ? this.ERROR_TYPES.MALFORMED_EXPRESSION : this.ERROR_TYPES.OTHER_ERROR;
onError(type, error, message);
}
result = false;
}
return result;
},
/**
* findMatchingMessage - Given an array of messages, returns one message
* whos targeting expression evaluates to true
*
* @param {Array} messages An array of AS router messages
* @param {obj} impressions An object containing impressions, where keys are message ids
* @param {trigger} string A trigger expression if a message for that trigger is desired
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
* @returns {obj} an AS router message
*/
async findMatchingMessage({messages, impressions = {}, target, context}) {
async findMatchingMessage({messages, impressions = {}, trigger, context, onError}) {
const arrayOfItems = [...messages];
let match;
let candidate;
while (!match && arrayOfItems.length) {
candidate = removeRandomItemFromArray(arrayOfItems);
if (
candidate &&
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
!candidate.trigger &&
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
) {
match = candidate;
}
}
return match;
},
async findMatchingMessageWithTrigger({messages, impressions = {}, target, trigger, context}) {
const arrayOfItems = [...messages];
let match;
let candidate;
while (!match && arrayOfItems.length) {
candidate = removeRandomItemFromArray(arrayOfItems);
if (
candidate &&
(trigger ? this.isTriggerMatch(trigger, candidate.trigger) : !candidate.trigger) &&
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
candidate.trigger === trigger &&
(!candidate.targeting || await this.isMatch(candidate.targeting, target, context))
// If a trigger expression was passed to this function, the message should match it.
// Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)
await this.checkMessageTargeting(candidate, context, onError)
) {
match = candidate;
}

View File

@ -0,0 +1,118 @@
/* 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";
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
/**
* A Map from trigger IDs to singleton trigger listeners. Each listener must
* have idempotent `init` and `uninit` methods.
*/
this.ASRouterTriggerListeners = new Map([
/**
* Attach listeners to every browser window to detect location changes, and
* notify the trigger handler whenever we navigate to a URL with a hostname
* we're looking for.
*/
["openURL", {
_initialized: false,
_triggerHandler: null,
_hosts: null,
/*
* If the listener is already initialised, `init` will replace the trigger
* handler and add any new hosts to `this._hosts`.
*/
init(triggerHandler, hosts = []) {
if (!this._initialized) {
this.onLocationChange = this.onLocationChange.bind(this);
// Listen for new windows being opened
Services.ww.registerNotification(this);
// Add listeners to all existing browser windows
const winEnum = Services.wm.getEnumerator("navigator:browser");
while (winEnum.hasMoreElements()) {
let win = winEnum.getNext();
if (win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {
continue;
}
win.gBrowser.addTabsProgressListener(this);
}
this._initialized = true;
}
this._triggerHandler = triggerHandler;
if (this._hosts) {
hosts.forEach(h => this._hosts.add(h));
} else {
this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour
}
},
uninit() {
if (this._initialized) {
Services.ww.unregisterNotification(this);
const winEnum = Services.wm.getEnumerator("navigator:browser");
while (winEnum.hasMoreElements()) {
let win = winEnum.getNext();
if (win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {
continue;
}
win.gBrowser.removeTabsProgressListener(this);
}
this._initialized = false;
this._triggerHandler = null;
this._hosts = null;
}
},
onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
const location = aLocationURI ? aLocationURI.spec : "";
if (location && aWebProgress.isTopLevel) {
try {
const host = (new URL(location)).hostname;
if (this._hosts.has(host)) {
this._triggerHandler(aBrowser.messageManager, {id: "openURL", param: host});
}
} catch (e) {} // Couldn't parse location URL
}
},
observe(win, topic, data) {
let onLoad;
switch (topic) {
case "domwindowopened":
if (!(win instanceof Ci.nsIDOMWindow) || win.closed || PrivateBrowsingUtils.isWindowPrivate(win)) {
break;
}
onLoad = () => {
// Ignore non-browser windows.
if (win.document.documentElement.getAttribute("windowtype") === "navigator:browser") {
win.gBrowser.addTabsProgressListener(this);
}
};
win.addEventListener("load", onLoad, {once: true});
break;
case "domwindowclosed":
if ((win instanceof Ci.nsIDOMWindow) &&
win.document.documentElement.getAttribute("windowtype") === "navigator:browser") {
win.gBrowser.removeTabsProgressListener(this);
}
break;
}
}
}]
]);
const EXPORTED_SYMBOLS = ["ASRouterTriggerListeners"];

View File

@ -153,6 +153,10 @@ const PREFS_CONFIG = new Map([
title: "The rendering order for the sections",
value: "topsites,topstories,highlights"
}],
["improvesearch.noDefaultSearchTile", {
title: "Experiment to remove tiles that are the same as the default search",
value: false
}],
["asrouterExperimentEnabled", {
title: "Is the message center experiment on?",
value: false

View File

@ -16,7 +16,7 @@ const ONBOARDING_MESSAGES = [
button_label: "Try It Now",
button_action: "OPEN_PRIVATE_BROWSER_WINDOW"
},
trigger: "firstRun"
trigger: {id: "firstRun"}
},
{
id: "ONBOARDING_2",
@ -31,7 +31,7 @@ const ONBOARDING_MESSAGES = [
button_action: "OPEN_URL",
button_action_params: "https://screenshots.firefox.com/#tour"
},
trigger: "firstRun"
trigger: {id: "firstRun"}
},
{
id: "ONBOARDING_3",
@ -47,7 +47,7 @@ const ONBOARDING_MESSAGES = [
button_action_params: "addons"
},
targeting: "isInExperimentCohort == 1",
trigger: "firstRun"
trigger: {id: "firstRun"}
},
{
id: "ONBOARDING_4",
@ -63,7 +63,7 @@ const ONBOARDING_MESSAGES = [
button_action_params: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/"
},
targeting: "isInExperimentCohort == 2",
trigger: "firstRun"
trigger: {id: "firstRun"}
}
];

View File

@ -3,7 +3,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
const {actionCreators: ac, actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {TippyTopProvider} = ChromeUtils.import("resource://activity-stream/lib/TippyTopProvider.jsm", {});
@ -32,6 +32,31 @@ const PINNED_FAVICON_PROPS_TO_MIGRATE = ["favicon", "faviconRef", "faviconSize"]
const SECTION_ID = "topsites";
const ROWS_PREF = "topSitesRows";
// Search experiment stuff
const NO_DEFAULT_SEARCH_TILE_EXP_PREF = "improvesearch.noDefaultSearchTile";
const SEARCH_HOST_FILTERS = [
{hostname: "google", identifierPattern: /^google/},
{hostname: "amazon", identifierPattern: /^amazon/}
];
/**
* isLinkDefaultSearch - does a given hostname match the user's default search engine?
*
* @param {string} hostname a top site hostname, such as "amazon" or "foo"
* @returns {bool}
*/
function isLinkDefaultSearch(hostname) {
for (const searchProvider of SEARCH_HOST_FILTERS) {
if (
hostname === searchProvider.hostname &&
String(Services.search.defaultEngine.identifier).match(searchProvider.identifierPattern)
) {
return true;
}
}
return false;
}
this.TopSitesFeed = class TopSitesFeed {
constructor() {
this._tippyTopProvider = new TippyTopProvider();
@ -50,10 +75,20 @@ this.TopSitesFeed = class TopSitesFeed {
this.refreshDefaults(this.store.getState().Prefs.values[DEFAULT_SITES_PREF]);
this._storage = this.store.dbStorage.getDbTable("sectionPrefs");
this.refresh({broadcast: true});
Services.obs.addObserver(this, "browser-search-engine-modified");
}
uninit() {
PageThumbs.removeExpirationFilter(this);
Services.obs.removeObserver(this, "browser-search-engine-modified");
}
observe(subj, topic, data) {
// We should update the current top sites if the search engine has been changed since
// the search engine that gets filtered out of top sites has changed.
if (topic === "browser-search-engine-modified" && data === "engine-default" && this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
this.refresh({broadcast: true});
}
}
_dedupeKey(site) {
@ -89,15 +124,32 @@ this.TopSitesFeed = class TopSitesFeed {
}
async getLinksWithDefaults() {
const isExperimentOn = this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF];
const numItems = this.store.getState().Prefs.values[ROWS_PREF] * TOP_SITES_MAX_SITES_PER_ROW;
// Get all frecent sites from history
const frecent = (await this.frecentCache.request({
numItems,
topsiteFrecency: FRECENCY_THRESHOLD
})).map(link => Object.assign({}, link, {hostname: shortURL(link)}));
}))
.reduce((validLinks, link) => {
const hostname = shortURL(link);
if (!(isExperimentOn && isLinkDefaultSearch(hostname))) {
validLinks.push({...link, hostname});
}
return validLinks;
}, []);
// Remove any defaults that have been blocked
const notBlockedDefaultSites = DEFAULT_TOP_SITES.filter(link =>
!NewTabUtils.blockedLinks.isBlocked({url: link.url}));
const notBlockedDefaultSites = DEFAULT_TOP_SITES
.filter(link => {
if (NewTabUtils.blockedLinks.isBlocked({url: link.url})) {
return false;
} else if (isExperimentOn && isLinkDefaultSearch(link.hostname)) {
return false;
}
return true;
});
// Get pinned links augmented with desired properties
const plainPinned = await this.pinnedCache.request();
@ -418,7 +470,7 @@ this.TopSitesFeed = class TopSitesFeed {
case at.PREF_CHANGED:
if (action.data.name === DEFAULT_SITES_PREF) {
this.refreshDefaults(action.data.value);
} else if (action.data.name === ROWS_PREF) {
} else if ([ROWS_PREF, NO_DEFAULT_SEARCH_TILE_EXP_PREF].includes(action.data.name)) {
this.refresh({broadcast: true});
}
break;

View File

@ -94,7 +94,7 @@ prefs_home_description=Tacha' achike etamab'äl nawajo' pa ri Rutikirib'al Firef
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_section_rows_option={num} cholaj:{num} taq cholaj
prefs_section_rows_option={num} cholaj;{num} taq cholaj
prefs_search_header=Ajk'amaya'l Kanoxïk
prefs_topsites_description=Taq ruxaq yalan ye'atz'ët
prefs_topstories_description2=Nïm rupam chijun ri ajk'amaya'l, ichinan awuma rat

View File

@ -187,7 +187,7 @@ firstrun_learn_more_link=Dysgu rhagor am Gyfrif Firefox
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=Rhowch eich e-bost
firstrun_form_sub_header=i barhau i Firefox Sync.
firstrun_form_sub_header=i barhau i Firefox Sync
firstrun_email_input_placeholder=E-bost

View File

@ -195,7 +195,6 @@ firstrun_invalid_input=Iimeel gollotooɗo hatojinaa
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Fuɗɗaade, woni a jaɓii {kuule} ɗee kam e {suturo}oo.
firstrun_terms_of_service=Laabi Carwol
firstrun_privacy_notice=Tintinol Suturo

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=lai turpinātu Firefox Sync.
firstrun_email_input_placeholder=Epasts
firstrun_invalid_input=Nepieciešams derīgs epasts
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Turpinot jūs piekrītat {terms} un {privacy}.

View File

@ -50,10 +50,14 @@ menu_action_archive_pocket=Pocket मध्ये संग्रहित क
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_mac_os=Finder मध्ये दर्शवा
menu_action_open_file=फाइल उघडा
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
menu_action_copy_download_link=डाउनलोड दुव्याची प्रत बनवा
menu_action_go_to_download_page=डाउनलोड पृष्ठावर जा
menu_action_remove_download=इतिहासातून काढून टाका
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
@ -87,11 +91,13 @@ prefs_home_description=आपल्या फायरफॉक्सचा म
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_section_rows_option={num} ओळ;{num} ओळी
prefs_search_header=वेब शोध
prefs_topsites_description=आपण सर्वाधिक भेट देता त्या साइट
prefs_topstories_description2=आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री
prefs_topstories_sponsored_learn_more=अधिक जाणून घ्या
prefs_highlights_description=आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा
prefs_highlights_options_visited_label=भेट दिलेली पृष्ठे
prefs_snippets_description=Mozilla आणि Firefox कडून अद्यतने
settings_pane_button_label=आपले नवीन टॅब पृष्ठ सानुकूलित करा
settings_pane_topsites_header=शीर्ष साइट्स
@ -113,6 +119,7 @@ topsites_form_add_header=नवीन खास साइट
topsites_form_edit_header=खास साईट संपादित करा
topsites_form_title_label=शिर्षक
topsites_form_title_placeholder=शिर्षक प्रविष्ट करा
topsites_form_url_label=URL
topsites_form_url_placeholder=URL चिकटवा किंवा टाईप करा
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=पूर्वावलोकन
@ -149,19 +156,30 @@ manual_migration_import_button=आता आयात करा
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_remove_section=विभाग काढा
section_menu_action_collapse_section=विभाग ढासळा
section_menu_action_manage_webext=एक्सटेन्शन व्यवस्थापित करा
section_menu_action_move_up=वर जा
section_menu_action_move_down=खाली जा
section_menu_action_privacy_notice=गोपनीयता सूचना
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=Firefox सोबत न्या
firstrun_learn_more_link=Firefox खात्यांविषयी अधिक जाणून घ्या
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=ईमेल प्रविष्ट करा
firstrun_email_input_placeholder=ईमेल
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_terms_of_service=सेवा अटी
firstrun_privacy_notice=गोपनीयता सूचना
firstrun_continue_to_login=पुढे चला
firstrun_skip_login=ही पायरी वगळा

View File

@ -40,7 +40,7 @@ window.gActivityStreamStrings = {
"section_disclaimer_topstories_buttontext": "Ütz, xno' pa nuwi'",
"prefs_home_header": "Etamab'äl pa ri Rutikirib'al Firefox",
"prefs_home_description": "Tacha' achike etamab'äl nawajo' pa ri Rutikirib'al Firefox ruwäch.",
"prefs_section_rows_option": "{num} cholaj:{num} taq cholaj",
"prefs_section_rows_option": "{num} cholaj;{num} taq cholaj",
"prefs_search_header": "Ajk'amaya'l Kanoxïk",
"prefs_topsites_description": "Taq ruxaq yalan ye'atz'ët",
"prefs_topstories_description2": "Nïm rupam chijun ri ajk'amaya'l, ichinan awuma rat",

View File

@ -94,7 +94,7 @@ window.gActivityStreamStrings = {
"firstrun_content": "Cewch eich nodau tudalen, hanes, cyfrineiriau a gosodiadau eraill ar eich holl ddyfeisiau.",
"firstrun_learn_more_link": "Dysgu rhagor am Gyfrif Firefox",
"firstrun_form_header": "Rhowch eich e-bost",
"firstrun_form_sub_header": "i barhau i Firefox Sync.",
"firstrun_form_sub_header": "i barhau i Firefox Sync",
"firstrun_email_input_placeholder": "E-bost",
"firstrun_invalid_input": "Mae angen e-bost dilys",
"firstrun_extra_legal_links": "Gan barhau, rydych yn cytuno i delerau'r {terms} a'r {privacy}.",

View File

@ -97,7 +97,7 @@ window.gActivityStreamStrings = {
"firstrun_form_sub_header": "ngam jokkude to Firefox Sync",
"firstrun_email_input_placeholder": "Iimeel",
"firstrun_invalid_input": "Iimeel gollotooɗo hatojinaa",
"firstrun_extra_legal_links": "Fuɗɗaade, woni a jaɓii {kuule} ɗee kam e {suturo}oo.",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Laabi Carwol",
"firstrun_privacy_notice": "Tintinol Suturo",
"firstrun_continue_to_login": "Jokku",

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Ievadiet savu epastu",
"firstrun_form_sub_header": "lai turpinātu Firefox Sync.",
"firstrun_email_input_placeholder": "Epasts",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Nepieciešams derīgs epasts",
"firstrun_extra_legal_links": "Turpinot jūs piekrītat {terms} un {privacy}.",
"firstrun_terms_of_service": "Lietošanas noteikumiem",
"firstrun_privacy_notice": "Privātuma politikai",

View File

@ -28,10 +28,10 @@ window.gActivityStreamStrings = {
"menu_action_show_file_windows": "Open Containing Folder",
"menu_action_show_file_linux": "Open Containing Folder",
"menu_action_show_file_default": "Show File",
"menu_action_open_file": "Open File",
"menu_action_copy_download_link": "Copy Download Link",
"menu_action_go_to_download_page": "Go to Download Page",
"menu_action_remove_download": "Remove from History",
"menu_action_open_file": "फाइल उघडा",
"menu_action_copy_download_link": "डाउनलोड दुव्याची प्रत बनवा",
"menu_action_go_to_download_page": "डाउनलोड पृष्ठावर जा",
"menu_action_remove_download": "इतिहासातून काढून टाका",
"search_button": "शोधा",
"search_header": "{search_engine_name} शोध",
"search_web_placeholder": "वेबवर शोधा",
@ -40,14 +40,14 @@ window.gActivityStreamStrings = {
"section_disclaimer_topstories_buttontext": "ठीक आहे, समजले",
"prefs_home_header": "फायरफॉक्स होम वरील मजकूर",
"prefs_home_description": "आपल्या फायरफॉक्सचा मुख्यपृष्ठवर आपल्याला कोणती माहिती पाहिजे ते निवडा.",
"prefs_section_rows_option": "{num} row;{num} rows",
"prefs_section_rows_option": "{num} ओळ;{num} ओळी",
"prefs_search_header": "वेब शोध",
"prefs_topsites_description": "आपण सर्वाधिक भेट देता त्या साइट",
"prefs_topstories_description2": "आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
"prefs_topstories_sponsored_learn_more": "अधिक जाणून घ्या",
"prefs_highlights_description": "आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा",
"prefs_highlights_options_visited_label": "Visited Pages",
"prefs_highlights_options_visited_label": "भेट दिलेली पृष्ठे",
"prefs_highlights_options_download_label": "Most Recent Download",
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
"prefs_snippets_description": "Mozilla आणि Firefox कडून अद्यतने",
@ -81,25 +81,25 @@ window.gActivityStreamStrings = {
"manual_migration_import_button": "आता आयात करा",
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"section_menu_action_remove_section": "Remove Section",
"section_menu_action_collapse_section": "Collapse Section",
"section_menu_action_remove_section": "विभाग काढा",
"section_menu_action_collapse_section": "विभाग ढासळा",
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "Manage Extension",
"section_menu_action_manage_webext": "एक्सटेन्शन व्यवस्थापित करा",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_move_up": "वर जा",
"section_menu_action_move_down": "खाली जा",
"section_menu_action_privacy_notice": "गोपनीयता सूचना",
"firstrun_title": "Take Firefox with You",
"firstrun_title": "Firefox सोबत न्या",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_form_header": "Enter your email",
"firstrun_learn_more_link": "Firefox खात्यांविषयी अधिक जाणून घ्या",
"firstrun_form_header": "ईमेल प्रविष्ट करा",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "Email",
"firstrun_email_input_placeholder": "ईमेल",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_continue_to_login": "Continue",
"firstrun_skip_login": "Skip this step"
"firstrun_terms_of_service": "सेवा अटी",
"firstrun_privacy_notice": "गोपनीयता सूचना",
"firstrun_continue_to_login": "पुढे चला",
"firstrun_skip_login": "ही पायरी वगळा"
};

View File

@ -1,6 +1,7 @@
[DEFAULT]
support-files =
blue_page.html
red_page.html
head.js
prefs =
browser.newtabpage.activity-stream.debug=false
@ -9,6 +10,7 @@ prefs =
[browser_as_load_location.js]
[browser_as_render.js]
[browser_asrouter_targeting.js]
[browser_asrouter_trigger_listeners.js]
[browser_enabled_newtabpage.js]
[browser_highlights_section.js]
[browser_getScreenshots.js]

View File

@ -15,13 +15,13 @@ ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
// ASRouterTargeting.isMatch
add_task(async function should_do_correct_targeting() {
is(await ASRouterTargeting.isMatch("FOO", {}, {FOO: true}), true, "should return true for a matching value");
is(await ASRouterTargeting.isMatch("!FOO", {}, {FOO: true}), false, "should return false for a non-matching value");
is(await ASRouterTargeting.isMatch("FOO", {FOO: true}), true, "should return true for a matching value");
is(await ASRouterTargeting.isMatch("!FOO", {FOO: true}), false, "should return false for a non-matching value");
});
add_task(async function should_handle_async_getters() {
const context = {get FOO() { return Promise.resolve(true); }};
is(await ASRouterTargeting.isMatch("FOO", {}, context), true, "should return true for a matching async value");
is(await ASRouterTargeting.isMatch("FOO", context), true, "should return true for a matching async value");
});
// ASRouterTargeting.findMatchingMessage
@ -32,7 +32,7 @@ add_task(async function find_matching_message() {
];
const context = {FOO: true};
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
const match = await ASRouterTargeting.findMatchingMessage({messages, context});
is(match, messages[0], "should match and return the correct message");
});
@ -41,11 +41,52 @@ add_task(async function return_nothing_for_no_matching_message() {
const messages = [{id: "bar", targeting: "!FOO"}];
const context = {FOO: true};
const match = await ASRouterTargeting.findMatchingMessage({messages, target: {}, context});
const match = await ASRouterTargeting.findMatchingMessage({messages, context});
is(match, undefined, "should return nothing since no matching message exists");
});
add_task(async function check_syntax_error_handling() {
let result;
function onError(...args) {
result = args;
}
const messages = [{id: "foo", targeting: "foo === 0"}];
const match = await ASRouterTargeting.findMatchingMessage({messages, onError});
is(match, undefined, "should return nothing since no valid matching message exists");
// Note that in order for the following test to pass, we are expecting a particular filepath for mozjexl.
// If the location of this file has changed, the MOZ_JEXL_FILEPATH constant should be updated om ASRouterTargeting.jsm
is(result[0], ASRouterTargeting.ERROR_TYPES.MALFORMED_EXPRESSION,
"should recognize the error as coming from mozjexl and call onError with the MALFORMED_EXPRESSION error type");
ok(result[1].message,
"should call onError with the error from mozjexl");
is(result[2], messages[0],
"should call onError with the invalid message");
});
add_task(async function check_other_error_handling() {
let result;
function onError(...args) {
result = args;
}
const messages = [{id: "foo", targeting: "foo"}];
const context = {get foo() { throw new Error("test error"); }};
const match = await ASRouterTargeting.findMatchingMessage({messages, context, onError});
is(match, undefined, "should return nothing since no valid matching message exists");
// Note that in order for the following test to pass, we are expecting a particular filepath for mozjexl.
// If the location of this file has changed, the MOZ_JEXL_FILEPATH constant should be updated om ASRouterTargeting.jsm
is(result[0], ASRouterTargeting.ERROR_TYPES.OTHER_ERROR,
"should not recognize the error as being an other error, not a mozjexl one");
is(result[1].message, "test error",
"should call onError with the error thrown in the context");
is(result[2], messages[0],
"should call onError with the invalid message");
});
// ASRouterTargeting.Environment
add_task(async function checkProfileAgeCreated() {
let profileAccessor = new ProfileAge();
@ -53,7 +94,7 @@ add_task(async function checkProfileAgeCreated() {
"should return correct profile age creation date");
const message = {id: "foo", targeting: `profileAgeCreated > ${await profileAccessor.created - 100}`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by profile age created");
});
@ -63,7 +104,7 @@ add_task(async function checkProfileAgeReset() {
"should return correct profile age reset");
const message = {id: "foo", targeting: `profileAgeReset == ${await profileAccessor.reset}`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by profile age reset");
});
@ -73,7 +114,7 @@ add_task(async function checkhasFxAccount() {
"should return true if a fx account is set");
const message = {id: "foo", targeting: "hasFxAccount"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by hasFxAccount");
});
@ -93,11 +134,11 @@ add_task(async function checksearchEngines() {
"searchEngines.current should be the current engine name");
const message = {id: "foo", targeting: `searchEngines[.current == ${Services.search.currentEngine.identifier}]`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by searchEngines.current");
const message2 = {id: "foo", targeting: `searchEngines[${Services.search.getVisibleEngines()[0].identifier} in .installed]`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message2], target: {}}), message2,
is(await ASRouterTargeting.findMatchingMessage({messages: [message2]}), message2,
"should select correct item by searchEngines.installed");
});
@ -109,7 +150,7 @@ add_task(async function checkisDefaultBrowser() {
is(result, expected,
"isDefaultBrowser should be equal to ShellService.isDefaultBrowser()");
const message = {id: "foo", targeting: `isDefaultBrowser == ${expected.toString()}`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by isDefaultBrowser");
});
@ -118,7 +159,7 @@ add_task(async function checkdevToolsOpenedCount() {
is(ASRouterTargeting.Environment.devToolsOpenedCount, 5,
"devToolsOpenedCount should be equal to devtools.selfxss.count pref value");
const message = {id: "foo", targeting: "devToolsOpenedCount >= 5"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by devToolsOpenedCount");
});
@ -200,31 +241,31 @@ add_task(async function checkFrecentSites() {
await PlacesTestUtils.addVisits(visits);
let message = {id: "foo", targeting: "'mozilla3.com' in topFrecentSites|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item by host in topFrecentSites");
message = {id: "foo", targeting: "'non-existent.com' in topFrecentSites|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), undefined,
"should not select incorrect item by host in topFrecentSites");
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 400]|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item when filtering by frecency");
message = {id: "foo", targeting: "'mozilla2.com' in topFrecentSites[.frecency >= 600]|mapToProperty('host')"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), undefined,
"should not select incorrect item when filtering by frecency");
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host')`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item when filtering by lastVisitDate");
message = {id: "foo", targeting: `'mozilla2.com' in topFrecentSites[.lastVisitDate >= ${timeDaysAgo(0) - 1}]|mapToProperty('host')`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), undefined,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), undefined,
"should not select incorrect item when filtering by lastVisitDate");
message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains");
// Cleanup

View File

@ -0,0 +1,58 @@
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "TestUtils",
"resource://testing-common/TestUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
async function openURLInWindow(window, url) {
const {selectedBrowser} = window.gBrowser;
await BrowserTestUtils.loadURI(selectedBrowser, url);
await BrowserTestUtils.browserLoaded(selectedBrowser);
}
add_task(async function check_openURL_listener() {
const TEST_URL = "https://example.com/browser/browser/components/newtab/test/browser/red_page.html";
let urlVisitCount = 0;
const triggerHandler = () => urlVisitCount++;
const normalWindow = await BrowserTestUtils.openNewBrowserWindow();
const privateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
// Initialise listener
const openURLListener = ASRouterTriggerListeners.get("openURL");
openURLListener.init(triggerHandler, ["example.com"]);
await openURLInWindow(normalWindow, TEST_URL);
is(urlVisitCount, 1, "should receive page visits from existing windows");
await openURLInWindow(normalWindow, "http://www.example.com/abc");
is(urlVisitCount, 1, "should not receive page visits for different domains");
await openURLInWindow(privateWindow, TEST_URL);
is(urlVisitCount, 1, "should not receive page visits from existing private windows");
const secondNormalWindow = await BrowserTestUtils.openNewBrowserWindow();
await openURLInWindow(secondNormalWindow, TEST_URL);
is(urlVisitCount, 2, "should receive page visits from newly opened windows");
const secondPrivateWindow = await BrowserTestUtils.openNewBrowserWindow({private: true});
await openURLInWindow(secondPrivateWindow, TEST_URL);
is(urlVisitCount, 2, "should not receive page visits from newly opened private windows");
// Uninitialise listener
openURLListener.uninit();
await openURLInWindow(normalWindow, TEST_URL);
is(urlVisitCount, 2, "should now not receive page visits from existing windows");
const thirdNormalWindow = await BrowserTestUtils.openNewBrowserWindow();
await openURLInWindow(thirdNormalWindow, TEST_URL);
is(urlVisitCount, 2, "should now not receive page visits from newly opened windows");
// Cleanup
const windows = [normalWindow, privateWindow, secondNormalWindow, secondPrivateWindow, thirdNormalWindow];
await Promise.all(windows.map(win => BrowserTestUtils.closeWindow(win)));
});

View File

@ -0,0 +1,6 @@
<html>
<head>
<meta charset="utf-8">
</head>
<body style="background-color: red" />
</html>

View File

@ -1,3 +1,4 @@
import {_ASRouter, MessageLoaderUtils} from "lib/ASRouter.jsm";
import {
CHILD_TO_PARENT_MESSAGE_NAME,
FAKE_LOCAL_MESSAGES,
@ -7,7 +8,7 @@ import {
FakeRemotePageManager,
PARENT_TO_CHILD_MESSAGE_NAME
} from "./constants";
import {_ASRouter} from "lib/ASRouter.jsm";
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
@ -30,6 +31,7 @@ describe("ASRouter", () => {
let clock;
let getStringPrefStub;
let addObserverStub;
let dispatchStub;
function createFakeStorage() {
const getStub = sandbox.stub();
@ -44,7 +46,8 @@ describe("ASRouter", () => {
async function createRouterAndInit(providers = FAKE_PROVIDERS) {
channel = new FakeRemotePageManager();
Router = new _ASRouter({providers});
await Router.init(channel, createFakeStorage());
dispatchStub = sandbox.stub();
await Router.init(channel, createFakeStorage(), dispatchStub);
}
beforeEach(async () => {
@ -86,7 +89,7 @@ describe("ASRouter", () => {
it("should set state.blockList to the block list in persistent storage", async () => {
blockList = ["foo"];
Router = new _ASRouter({providers: FAKE_PROVIDERS});
await Router.init(channel, createFakeStorage());
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.blockList, ["foo"]);
});
@ -97,7 +100,7 @@ describe("ASRouter", () => {
impressions = {foo: [0, 1, 2]};
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
await Router.init(channel, createFakeStorage());
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.impressions, impressions);
});
@ -105,7 +108,7 @@ describe("ASRouter", () => {
Router = new _ASRouter({providers: FAKE_PROVIDERS});
const loadMessagesSpy = sandbox.spy(Router, "loadMessagesFromAllProviders");
await Router.init(channel, createFakeStorage());
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.calledOnce(loadMessagesSpy);
assert.isArray(Router.state.messages);
@ -144,6 +147,9 @@ describe("ASRouter", () => {
assert.propertyVal(Router.WHITELIST_HOSTS, "snippets-admin.mozilla.org", "preview");
assert.propertyVal(Router.WHITELIST_HOSTS, "activity-stream-icons.services.mozilla.com", "production");
});
it("should set this.dispatchToAS to the third parameter passed to .init()", async () => {
assert.equal(Router.dispatchToAS, dispatchStub);
});
});
describe("#loadMessagesFromAllProviders", () => {
@ -207,6 +213,25 @@ describe("ASRouter", () => {
// These are the local messages that should not have been deleted
assertRouterContainsMessages(FAKE_LOCAL_MESSAGES);
});
it("should parse the triggers in the messages and register the trigger listeners", async () => {
sandbox.spy(ASRouterTriggerListeners.get("openURL"), "init");
/* eslint-disable object-curly-newline */ /* eslint-disable object-property-newline */
await createRouterAndInit([
{id: "foo", type: "local", messages: [
{id: "foo", template: "simple_template", trigger: {id: "firstRun"}, content: {title: "Foo", body: "Foo123"}},
{id: "bar1", template: "simple_template", trigger: {id: "openURL", params: ["www.mozilla.org", "www.mozilla.com"]}, content: {title: "Bar1", body: "Bar123"}},
{id: "bar2", template: "simple_template", trigger: {id: "openURL", params: ["www.example.com"]}, content: {title: "Bar2", body: "Bar123"}}
]}
]);
/* eslint-enable object-curly-newline */ /* eslint-enable object-property-newline */
assert.calledTwice(ASRouterTriggerListeners.get("openURL").init);
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
Router._triggerHandler, ["www.mozilla.org", "www.mozilla.com"]);
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
Router._triggerHandler, ["www.example.com"]);
});
});
describe("blocking", () => {
@ -240,6 +265,21 @@ describe("ASRouter", () => {
assert.calledWith(channel.removeMessageListener, CHILD_TO_PARENT_MESSAGE_NAME, listenerAdded);
});
it("should unregister the trigger listeners", () => {
for (const listener of ASRouterTriggerListeners.values()) {
sandbox.spy(listener, "uninit");
}
Router.uninit();
for (const listener of ASRouterTriggerListeners.values()) {
assert.calledOnce(listener.uninit);
}
});
it("should set .dispatchToAS to null", () => {
Router.uninit();
assert.isNull(Router.dispatchToAS);
});
});
describe("#onMessage: CONNECT_UI_REQUEST", () => {
@ -298,27 +338,21 @@ describe("ASRouter", () => {
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should add the endpoint provided on CONNECT_UI_REQUEST", async () => {
it("should make a request to the provided endpoint on CONNECT_UI_REQUEST", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.isDefined(Router.state.providers.find(p => p.url === url));
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should add the endpoint provided on ADMIN_CONNECT_STATE", async () => {
it("should make a request to the provided endpoint on ADMIN_CONNECT_STATE and remove the endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.isDefined(Router.state.providers.find(p => p.url === url));
});
it("should not add the same endpoint twice", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 1);
assert.calledWith(global.fetch, url);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should not add a url that is not from a whitelisted host", async () => {
const url = "https://mozilla.org";
@ -410,7 +444,7 @@ describe("ASRouter", () => {
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {type: "CONNECT_UI_REQUEST"});
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
});
it("should call sendNextMessage on GET_NEXT_MESSAGE", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
@ -419,15 +453,16 @@ describe("ASRouter", () => {
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {type: "GET_NEXT_MESSAGE"});
assert.calledWithExactly(Router.sendNextMessage, sinon.match.instanceOf(FakeRemotePageManager), {});
});
it("should return the preview message if that's available", async () => {
it("should return the preview message if that's available and remove it from Router.state", async () => {
const expectedObj = {provider: "preview"};
Router.setState({messages: [expectedObj]});
await Router.sendNextMessage(channel);
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: expectedObj});
assert.isUndefined(Router.state.messages.find(m => m.provider === "preview"));
});
it("should call _getBundledMessages if we request a message that needs to be bundled", async () => {
sandbox.stub(Router, "_getBundledMessages").resolves();
@ -480,30 +515,30 @@ describe("ASRouter", () => {
describe("#onMessage: TRIGGER", () => {
it("should pass the trigger to ASRouterTargeting on TRIGGER message", async () => {
sandbox.stub(Router, "_findMessage").resolves();
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: "firstRun"}});
const msg = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
await Router.onMessage(msg);
assert.calledOnce(Router._findMessage);
assert.deepEqual(Router._findMessage.firstCall.args[2], {trigger: "firstRun"});
assert.deepEqual(Router._findMessage.firstCall.args[2], {id: "firstRun"});
});
it("consider the trigger when picking a message", async () => {
let messages = [
{id: "foo1", template: "simple_template", bundled: 1, trigger: "foo", content: {title: "Foo1", body: "Foo123-1"}}
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}}
];
const {target, data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: "foo"}});
let message = await Router._findMessage(messages, target, data.data);
const {target, data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let message = await Router._findMessage(messages, target, data.data.trigger);
assert.equal(message, messages[0]);
});
it("should pick a message with the right targeting and trigger", async () => {
let messages = [
{id: "foo1", template: "simple_template", bundled: 2, trigger: "foo", content: {title: "Foo1", body: "Foo123-1"}},
{id: "foo2", template: "simple_template", bundled: 2, trigger: "bar", content: {title: "Foo2", body: "Foo123-2"}},
{id: "foo3", template: "simple_template", bundled: 2, trigger: "foo", content: {title: "Foo3", body: "Foo123-3"}}
{id: "foo1", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}},
{id: "foo2", template: "simple_template", bundled: 2, trigger: {id: "bar"}, content: {title: "Foo2", body: "Foo123-2"}},
{id: "foo3", template: "simple_template", bundled: 2, trigger: {id: "foo"}, content: {title: "Foo3", body: "Foo123-3"}}
];
await Router.setState({messages});
const {target, data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: "foo"}});
let {bundle} = await Router._getBundledMessages(messages[0], target, data.data);
const {target, data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let {bundle} = await Router._getBundledMessages(messages[0], target, data.data.trigger);
assert.equal(bundle.length, 2);
// it should have picked foo1 and foo3 only
assert.isTrue(bundle.every(elem => elem.id === "foo1" || elem.id === "foo3"));
@ -557,6 +592,29 @@ describe("ASRouter", () => {
});
});
describe("#onMessage: INSTALL_ADDON_FROM_URL", () => {
it("should call installAddonFromURL with correct arguments", async () => {
sandbox.stub(MessageLoaderUtils, "installAddonFromURL").resolves(null);
const msg = fakeAsyncMessage({type: "INSTALL_ADDON_FROM_URL", data: {url: "foo.com"}});
await Router.onMessage(msg);
assert.calledOnce(MessageLoaderUtils.installAddonFromURL);
assert.calledWithExactly(MessageLoaderUtils.installAddonFromURL, msg.target.browser, "foo.com");
});
});
describe("_triggerHandler", () => {
it("should call #onMessage with the correct trigger", () => {
sinon.spy(Router, "onMessage");
const target = {};
const trigger = {id: "FAKE_TRIGGER", param: "some fake param"};
Router._triggerHandler(target, trigger);
assert.calledOnce(Router.onMessage);
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", trigger}});
});
});
describe("valid preview endpoint", () => {
it("should report an error if url protocol is not https", () => {
sandbox.stub(Cu, "reportError");
@ -659,4 +717,18 @@ describe("ASRouter", () => {
});
});
});
describe("handle targeting errors", () => {
it("should dispatch an event when a targeting expression throws an error", async () => {
sandbox.stub(global.FilterExpressions, "eval").returns(Promise.reject(new Error("fake error")));
await Router.setState({messages: [{id: "foo", targeting: "foo2.[[("}]});
const msg = fakeAsyncMessage({type: "GET_NEXT_MESSAGE"});
await Router.onMessage(msg);
assert.calledOnce(dispatchStub);
const [action] = dispatchStub.firstCall.args;
assert.equal(action.type, "AS_ROUTER_TELEMETRY_USER_EVENT");
assert.equal(action.data.message_id, "foo");
});
});
});

View File

@ -68,7 +68,7 @@ describe("ASRouterFeed", () => {
});
it("should not re-initialize the ASRouter if it is already initialized", async () => {
// Router starts initialized
await Router.init(new FakeRemotePageManager(), storage);
await Router.init(new FakeRemotePageManager(), storage, () => {});
sinon.stub(Router, "init");
prefs[EXPERIMENT_PREF] = true;
@ -81,7 +81,7 @@ describe("ASRouterFeed", () => {
describe("#onAction: PREF_CHANGE", () => {
it("should return early if the pref changed does not enable/disable the router", async () => {
// Router starts initialized
await Router.init(new FakeRemotePageManager(), storage);
await Router.init(new FakeRemotePageManager(), storage, () => {});
sinon.stub(Router, "uninit");
prefs[EXPERIMENT_PREF] = false;
@ -92,7 +92,7 @@ describe("ASRouterFeed", () => {
});
it("should uninitialize the ASRouter if it is already initialized and the experiment pref is false", async () => {
// Router starts initialized
await Router.init(new FakeRemotePageManager(), storage);
await Router.init(new FakeRemotePageManager(), storage, () => {});
sinon.stub(Router, "uninit");
prefs[EXPERIMENT_PREF] = false;
@ -104,7 +104,7 @@ describe("ASRouterFeed", () => {
});
describe("#onAction: UNINIT", () => {
it("should uninitialize the ASRouter and restore onboarding", async () => {
await Router.init(new FakeRemotePageManager(), storage);
await Router.init(new FakeRemotePageManager(), storage, () => {});
sinon.stub(Router, "uninit");
feed.onAction({type: at.UNINIT});

View File

@ -0,0 +1,133 @@
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
describe("ASRouterTriggerListeners", () => {
let sandbox;
let registerWindowNotificationStub;
let unregisterWindoNotificationStub;
let windowEnumeratorStub;
let existingWindow;
const triggerHandler = () => {};
const openURLListener = ASRouterTriggerListeners.get("openURL");
const hosts = ["www.mozilla.com", "www.mozilla.org"];
function resetEnumeratorStub(windows) {
windowEnumeratorStub
.withArgs("navigator:browser")
.returns({
_count: -1,
hasMoreElements() { this._count++; return this._count < windows.length; },
getNext() { return windows[this._count]; }
});
}
beforeEach(async () => {
sandbox = sinon.sandbox.create();
registerWindowNotificationStub = sandbox.stub(global.Services.ww, "registerNotification");
unregisterWindoNotificationStub = sandbox.stub(global.Services.ww, "unregisterNotification");
existingWindow = {gBrowser: {addTabsProgressListener: sandbox.stub(), removeTabsProgressListener: sandbox.stub()}};
windowEnumeratorStub = sandbox.stub(global.Services.wm, "getEnumerator");
resetEnumeratorStub([existingWindow]);
sandbox.spy(openURLListener, "init");
sandbox.spy(openURLListener, "uninit");
});
afterEach(() => {
sandbox.restore();
});
describe("openURL listener", () => {
it("should exist and initially be uninitialised", () => {
assert.ok(openURLListener);
assert.notOk(openURLListener._initialized);
});
describe("#init", () => {
beforeEach(() => {
openURLListener.init(triggerHandler, hosts);
});
afterEach(() => {
openURLListener.uninit();
});
it("should set ._initialized to true and save the triggerHandler and hosts", () => {
assert.ok(openURLListener._initialized);
assert.deepEqual(openURLListener._hosts, new Set(hosts));
assert.equal(openURLListener._triggerHandler, triggerHandler);
});
it("should register an open-window notification", () => {
assert.calledOnce(registerWindowNotificationStub);
assert.calledWith(registerWindowNotificationStub, openURLListener);
});
it("should add tab progress listeners to all existing browser windows", () => {
assert.calledOnce(existingWindow.gBrowser.addTabsProgressListener);
assert.calledWithExactly(existingWindow.gBrowser.addTabsProgressListener, openURLListener);
});
it("if already initialised, should only update the trigger handler and add the new hosts", () => {
const newHosts = ["www.example.com"];
const newTriggerHandler = () => {};
resetEnumeratorStub([existingWindow]);
registerWindowNotificationStub.reset();
existingWindow.gBrowser.addTabsProgressListener.reset();
openURLListener.init(newTriggerHandler, newHosts);
assert.ok(openURLListener._initialized);
assert.deepEqual(openURLListener._hosts, new Set([...hosts, ...newHosts]));
assert.equal(openURLListener._triggerHandler, newTriggerHandler);
assert.notCalled(registerWindowNotificationStub);
assert.notCalled(existingWindow.gBrowser.addTabsProgressListener);
});
});
describe("#uninit", () => {
beforeEach(() => {
openURLListener.init(triggerHandler, hosts);
// Ensure that the window enumerator will return the existing window for uninit as well
resetEnumeratorStub([existingWindow]);
openURLListener.uninit();
});
it("should set ._initialized to false and clear the triggerHandler and hosts", () => {
assert.notOk(openURLListener._initialized);
assert.equal(openURLListener._hosts, null);
assert.equal(openURLListener._triggerHandler, null);
});
it("should remove an open-window notification", () => {
assert.calledOnce(unregisterWindoNotificationStub);
assert.calledWith(unregisterWindoNotificationStub, openURLListener);
});
it("should remove tab progress listeners from all existing browser windows", () => {
assert.calledOnce(existingWindow.gBrowser.removeTabsProgressListener);
assert.calledWithExactly(existingWindow.gBrowser.removeTabsProgressListener, openURLListener);
});
it("should do nothing if already uninitialised", () => {
unregisterWindoNotificationStub.reset();
existingWindow.gBrowser.removeTabsProgressListener.reset();
resetEnumeratorStub([existingWindow]);
openURLListener.uninit();
assert.notOk(openURLListener._initialized);
assert.notCalled(unregisterWindoNotificationStub);
assert.notCalled(existingWindow.gBrowser.removeTabsProgressListener);
});
});
describe("#onLocationChange", () => {
it("should call the ._triggerHandler with the right arguments", () => {
const newTriggerHandler = sinon.stub();
openURLListener.init(newTriggerHandler, hosts);
const browser = {messageManager: {}};
const webProgress = {isTopLevel: true};
const location = "https://www.mozilla.org/something";
openURLListener.onLocationChange(browser, webProgress, undefined, {spec: location});
assert.calledOnce(newTriggerHandler);
assert.calledWithExactly(newTriggerHandler, browser.messageManager, {id: "openURL", param: "www.mozilla.org"});
});
});
});
});

View File

@ -1,3 +1,4 @@
import {GlobalOverrider} from "test/unit/utils";
import {MessageLoaderUtils} from "lib/ASRouter.jsm";
describe("MessageLoaderUtils", () => {
@ -135,4 +136,42 @@ describe("MessageLoaderUtils", () => {
assert.isFalse(MessageLoaderUtils.shouldProviderUpdate({id: "foo", lastUpdated: 0, updateCycleInMs: 300}));
});
});
describe("#installAddonFromURL", () => {
let globals;
let sandbox;
let getInstallStub;
let installAddonStub;
beforeEach(() => {
globals = new GlobalOverrider();
sandbox = sinon.sandbox.create();
getInstallStub = sandbox.stub();
installAddonStub = sandbox.stub();
globals.set("AddonManager", {
getInstallForURL: getInstallStub,
installAddonFromWebpage: installAddonStub
});
});
afterEach(() => {
sandbox.restore();
globals.restore();
});
it("should call the Addons API when passed a valid URL", async () => {
getInstallStub.resolves(null);
installAddonStub.resolves(null);
await MessageLoaderUtils.installAddonFromURL({}, "foo.com");
assert.calledOnce(getInstallStub);
assert.calledOnce(installAddonStub);
});
it("should not call the Addons API on invalid URLs", async () => {
sandbox.stub(global.Services.scriptSecurityManager, "getSystemPrincipal").throws();
await MessageLoaderUtils.installAddonFromURL({}, "https://foo.com");
assert.notCalled(getInstallStub);
assert.notCalled(installAddonStub);
});
});
});

View File

@ -66,7 +66,7 @@ describe("Top Sites Feed", () => {
_shouldGetScreenshots: sinon.stub().returns(true)
};
filterAdultStub = sinon.stub().returns([]);
shortURLStub = sinon.stub().callsFake(site => site.url);
shortURLStub = sinon.stub().callsFake(site => site.url.replace(".com", ""));
const fakeDedupe = function() {};
fakePageThumbs = {
addExpirationFilter: sinon.stub(),
@ -1083,4 +1083,49 @@ describe("Top Sites Feed", () => {
assert.equal(feed.store.dispatch.secondCall.args[0].data.links[0].url, FAKE_LINKS[0].url);
});
});
describe("improvesearch.noDefaultSearchTile experiment", () => {
const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
let cachedDefaultSearch;
beforeEach(() => {
cachedDefaultSearch = global.Services.search.defaultEngine;
global.Services.search.defaultEngine = {identifier: "google"};
feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
});
afterEach(() => {
global.Services.search.defaultEngine = cachedDefaultSearch;
});
it("should not filter out google from the query results if the experiment pref is off", async () => {
links = [{url: "google.com"}, {url: "foo.com"}];
feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "google.com");
});
it("should filter out google from the default sites if it matches the current default search", async () => {
feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "google.com,amazon.com"}});
links = [{url: "foo.com"}];
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "amazon.com");
assert.notInclude(urlsReturned, "google.com");
});
it("should not filter out google from pinned sites even if it matches the current default search", async () => {
links = [{url: "foo.com"}];
fakeNewTabUtils.pinnedLinks.links = [{url: "google.com"}];
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "google.com");
});
it("should call refresh when the the default search engine has been set", () => {
sinon.stub(feed, "refresh");
feed.observe(null, "browser-search-engine-modified", "engine-default");
});
it("should call refresh when the experiment pref has changed", () => {
sinon.stub(feed, "refresh");
feed.onAction({type: at.PREF_CHANGED, data: {name: NO_DEFAULT_SEARCH_TILE_PREF, value: true}});
assert.calledOnce(feed.refresh);
feed.onAction({type: at.PREF_CHANGED, data: {name: NO_DEFAULT_SEARCH_TILE_PREF, value: false}});
assert.calledTwice(feed.refresh);
});
});
});

View File

@ -102,6 +102,7 @@ const TEST_GLOBAL = {
},
PluralForm: {get() {}},
Preferences: FakePrefs,
PrivateBrowsingUtils: {isWindowPrivate: () => false},
DownloadsViewUI: {DownloadElementShell},
Services: {
locale: {
@ -178,7 +179,8 @@ const TEST_GLOBAL = {
createNullPrincipal() {},
getSystemPrincipal() {}
},
wm: {getMostRecentWindow: () => window},
wm: {getMostRecentWindow: () => window, getEnumerator: () => ({hasMoreElements: () => false})},
ww: {registerNotification() {}, unregisterNotification() {}},
appinfo: {appBuildID: "20180710100040"}
},
XPCOMUtils: {

View File

@ -685,7 +685,7 @@
.tabbrowser-tab:hover::after,
#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[beforehovered]::after,
.tabbrowser-tab[multiselected]::after,
.tabbrowser-tab[before-multiselected]::after {
#tabbrowser-tabs:not([movingtab]) > .tabbrowser-tab[before-multiselected]::after {
margin-top: var(--tabs-top-border-width);
margin-bottom: 0;
}

View File

@ -16,6 +16,7 @@ Important Concepts
files-metadata
Profile Guided Optimization <pgo>
slow
tup
environment-variables
build-targets
python

View File

@ -142,7 +142,7 @@ Please note that clobber and incremental builds are different. A clobber
build with make will likely be as fast as a clobber build with e.g. Tup.
However, Tup should vastly outperform make when it comes to incremental
builds. Therefore, this issue is mostly seen when performing incremental
builds.
builds. For more information, see :ref:`tup`.
C++ header dependency hell
==========================

106
build/docs/tup.rst Normal file
View File

@ -0,0 +1,106 @@
.. _tup:
===========
Tup Backend
===========
The Tup build backend is an alternative to the default Make backend. The `Tup
build system <http://gittup.org/tup/>`_ is designed for fast and correct
incremental builds. A top-level no-op build should be under 2 seconds, and
clobbers should rarely be required. It is currently only available for Linux
Desktop builds -- other platforms like Windows or OSX are planned for the
future.
As part of the mozbuild architecture, the Tup backend shares a significant
portion of frontend (developer-facing) code in the build system. When using the
Tup backend, ``mach build`` is still the entry point to run the build system,
and moz.build files are still used for the build description. Familiar parts of
the build system like configure and generating the build files (the
``Reticulating splines...`` step) are virtually identical in both backends. The
difference is that ``mach`` invokes Tup instead of Make under the hood to do
the actual work of determining what needs to be rebuilt. Tup is able to perform
this work more efficiently by loading only the parts of the DAG that are
required for an incremental build. Additionally, Tup instruments processes to
see what files are read from and written to in order to verify that
dependencies are correct.
For more detailed information on the rationale behind Tup, see the `Build
System Rules and Algorithms
<http://gittup.org/tup/build_system_rules_and_algorithms.pdf>`_ paper.
Installation
============
You'll need to install the Tup executable, as well as the nightly rust/cargo
toolchain::
cd ~/.mozbuild && mach artifact toolchain --from-build linux64-tup
rustup install nightly
rustup default nightly
Configuration
=============
Your mozconfig needs to describe how to find the executable if it's not in your
PATH, and enable the Tup backend::
export TUP=~/.mozbuild/tup/tup
ac_add_options --enable-build-backends=Tup
What Works
==========
You should expect a Linux desktop build to generate a working Firefox binary
from a ``mach build``, and be able to run test suites against it (eg:
mochitests, xpcshell, gtest). Top-level incremental builds should be fast
enough to use them during a regular compile/edit/test cycle. If you wish to
stop compilation partway through the build to more quickly iterate on a
particular file, you can expect ``mach build objdir/path/to/file.o`` to
correctly produce all inputs required to build file.o before compiling it. For
example, you don't have to run the build system in various subdirectories to
get generated headers built in the right order.
Currently Unsupported / Future Work
===================================
There are a number of features that you may use in the Make backend that are
currently unsupported for the Tup backend. We plan to add support for these in
the future according to developer demand and build team availability.
* sccache - This is currently under active development to support icecream-like
functionality, which likely impacts the same parts that would affect Tup's
dependency checking mechanisms. Note that icecream itself should work with
Tup.
* Incremental Rust compilation - see `bug 1468527 <https://bugzilla.mozilla.org/show_bug.cgi?id=1468527>`_
* Watchman integration - This will allow Tup to skip the initial ``Scanning
filesystem...`` step, saving 1-2 seconds of startup overhead.
* More platform support (Windows, OSX, etc.)
* Packaging in automation - This is still quite intertwined with Makefiles
* Tests in automation - Requires packaging
How to Contribute
=================
At the moment we're looking for early adopters who are developing on the Linux
desktop to try out the Tup backend, and share your experiences with the build
team (see `Contact`_).
* Are there particular issues or missing features that prevent you from using
the Tup backend at this time?
* Do you find that top-level incremental builds are fast enough to use for
every build invocation?
* Have you needed to perform a clobber build to fix an issue?
Contact
========
If you have any issues, feel free to file a bug blocking `buildtup
<https://bugzilla.mozilla.org/show_bug.cgi?id=827343>`_, or contact mshal or
chmanchester in #build on IRC.

View File

@ -5,16 +5,17 @@
# file, You can obtain one at http://mozilla.og/MPL/2.0/.
import sys
import buildconfig
from mozbuild.preprocessor import Preprocessor
def main(output, input_file):
def main(output, input_file, version):
pp = Preprocessor()
pp.context.update({
'VERSION': 'xul%s' % buildconfig.substs['MOZILLA_SYMBOLVERSION'],
'VERSION': version,
})
pp.out = output
pp.do_include(input_file)
if __name__ == '__main__':
main(*sys.agv[1:])

View File

@ -1296,11 +1296,6 @@ if CONFIG['MOZ_ENABLE_CONTENTMANAGER']:
'SelectSingleContentItemPage.h',
]
if not CONFIG['MOZ_TREE_PIXMAN']:
system_headers += [
'pixman.h',
]
if CONFIG['MOZ_SYSTEM_LIBVPX']:
system_headers += [
'vpx_mem/vpx_mem.h',

View File

@ -15,6 +15,7 @@ const MenuList = createFactory(
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const { hr } = dom;
const { openDocLink } = require("devtools/client/shared/link");
const { assert } = require("devtools/shared/DevToolsUtils");
class MeatballMenu extends PureComponent {
static get propTypes() {
@ -94,14 +95,35 @@ class MeatballMenu extends PureComponent {
// Dock options
for (const hostType of this.props.hostTypes) {
const l10nkey =
hostType.position === "window" ? "separateWindow" : hostType.position;
// This is more verbose than it needs to be but lets us easily search for
// l10n entities.
let l10nkey;
switch (hostType.position) {
case "window":
l10nkey = "toolbox.meatballMenu.dock.separateWindow.label";
break;
case "bottom":
l10nkey = "toolbox.meatballMenu.dock.bottom.label";
break;
case "left":
l10nkey = "toolbox.meatballMenu.dock.left.label";
break;
case "right":
l10nkey = "toolbox.meatballMenu.dock.right.label";
break;
default:
assert(false, `Unexpected hostType.position: ${hostType.position}`);
break;
}
items.push(
MenuItem({
id: `toolbox-meatball-menu-dock-${hostType.position}`,
label: this.props.L10N.getStr(
`toolbox.meatballMenu.dock.${l10nkey}.label`
),
label: this.props.L10N.getStr(l10nkey),
onClick: () => hostType.switchHost(),
checked: hostType.position === this.props.currentHostType,
className: "iconic",
@ -115,14 +137,13 @@ class MeatballMenu extends PureComponent {
// Split console
if (this.props.currentToolId !== "webconsole") {
const l10nkey = this.props.isSplitConsoleActive
? "toolbox.meatballMenu.hideconsole.label"
: "toolbox.meatballMenu.splitconsole.label";
items.push(
MenuItem({
id: "toolbox-meatball-menu-splitconsole",
label: this.props.L10N.getStr(
`toolbox.meatballMenu.${
this.props.isSplitConsoleActive ? "hideconsole" : "splitconsole"
}.label`
),
label: this.props.L10N.getStr(l10nkey),
accelerator: "Esc",
onClick: this.props.toggleSplitConsole,
className: "iconic",

View File

@ -52,7 +52,6 @@ function onInspect(aState, aResponse)
let expectedProps = {
"addBroadcastListenerFor": { value: { type: "object" } },
"commandDispatcher": { get: { type: "object" } },
"getBoxObjectFor": { value: { type: "object" } },
};
let props = aResponse.ownProperties;

View File

@ -6019,7 +6019,7 @@ nsDocShell::SetCurScrollPosEx(int32_t aCurHorizontalPos,
NS_ENSURE_TRUE(sf, NS_ERROR_FAILURE);
nsIScrollableFrame::ScrollMode scrollMode = nsIScrollableFrame::INSTANT;
if (sf->GetScrollbarStyles().mScrollBehavior ==
if (sf->GetScrollStyles().mScrollBehavior ==
NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
scrollMode = nsIScrollableFrame::SMOOTH_MSD;
}
@ -11751,7 +11751,10 @@ nsDocShell::AddState(JS::Handle<JS::Value> aData, const nsAString& aTitle,
// notification is allowed only when we know docshell is not loading a new
// document and it requires LOCATION_CHANGE_SAME_DOCUMENT flag. Otherwise,
// FireOnLocationChange(...) breaks security UI.
if (!equalURIs) {
//
// If the docshell is shutting down, don't update the document URI, as we
// can't load into a docshell that is being destroyed.
if (!equalURIs && !mIsBeingDestroyed) {
document->SetDocumentURI(newURI);
// We can't trust SetCurrentURI to do always fire locationchange events
// when we expect it to, so we hack around that by doing it ourselves...

View File

@ -0,0 +1,16 @@
<html>
<head>
<script>
function go() {
var a = window.history.state;
window.history.replaceState(a,"","1");
var ok = opener.ok;
var SimpleTest = opener.SimpleTest;
ok("Addition of history in unload did not crash browser");
SimpleTest.finish();
}
</script>
</head>
<body onunload="go()">
</body>
</html>

View File

@ -43,6 +43,7 @@ support-files =
file_bug1121701_2.html
file_bug1186774.html
file_bug1151421.html
file_bug1450164.html
file_close_onpagehide1.html
file_close_onpagehide2.html
file_pushState_after_document_open.html
@ -104,6 +105,7 @@ support-files = file_bug675587.html
[test_bug1121701.html]
[test_bug1151421.html]
[test_bug1186774.html]
[test_bug1450164.html]
[test_close_onpagehide_by_history_back.html]
[test_close_onpagehide_by_window_close.html]
[test_forceinheritprincipal_overrule_owner.html]

View File

@ -0,0 +1,31 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1450164
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1450164</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="application/javascript">
/** Test for Bug 1450164 **/
function runTest() {
child = window.open("file_bug1450164.html", "", "width=100,height=100");
child.onload = function() {
// After the window loads, close it. If we don't crash in debug, consider that a pass.
child.close();
}
}
SimpleTest.waitForExplicitFinish();
addLoadEvent(runTest);
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1450164">Mozilla Bug 1450164</a>
</body>
</html>

View File

@ -1346,7 +1346,7 @@ KeyframeEffect::CanThrottleOverflowChangesInScrollable(nsIFrame& aFrame) const
return true;
}
ScrollbarStyles ss = scrollable->GetScrollbarStyles();
ScrollStyles ss = scrollable->GetScrollStyles();
if (ss.mVertical == NS_STYLE_OVERFLOW_HIDDEN &&
ss.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN &&
scrollable->GetLogicalScrollPosition() == nsPoint(0, 0)) {

View File

@ -805,7 +805,7 @@ Element::Scroll(const CSSIntPoint& aScroll, const ScrollOptions& aOptions)
if (aOptions.mBehavior == ScrollBehavior::Smooth) {
scrollMode = nsIScrollableFrame::SMOOTH_MSD;
} else if (aOptions.mBehavior == ScrollBehavior::Auto) {
ScrollbarStyles styles = sf->GetScrollbarStyles();
ScrollStyles styles = sf->GetScrollStyles();
if (styles.mScrollBehavior == NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
scrollMode = nsIScrollableFrame::SMOOTH_MSD;
}
@ -902,7 +902,7 @@ Element::SetScrollTop(int32_t aScrollTop)
nsIScrollableFrame* sf = GetScrollFrame(nullptr, flushType);
if (sf) {
nsIScrollableFrame::ScrollMode scrollMode = nsIScrollableFrame::INSTANT;
if (sf->GetScrollbarStyles().mScrollBehavior == NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
if (sf->GetScrollStyles().mScrollBehavior == NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
scrollMode = nsIScrollableFrame::SMOOTH_MSD;
}
sf->ScrollToCSSPixels(CSSIntPoint(sf->GetScrollPositionCSSPixels().x,
@ -927,7 +927,7 @@ Element::SetScrollLeft(int32_t aScrollLeft)
nsIScrollableFrame* sf = GetScrollFrame();
if (sf) {
nsIScrollableFrame::ScrollMode scrollMode = nsIScrollableFrame::INSTANT;
if (sf->GetScrollbarStyles().mScrollBehavior == NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
if (sf->GetScrollStyles().mScrollBehavior == NS_STYLE_SCROLL_BEHAVIOR_SMOOTH) {
scrollMode = nsIScrollableFrame::SMOOTH_MSD;
}
@ -1970,7 +1970,7 @@ Element::UnbindFromTree(bool aDeep, bool aNullParent)
nsPresContext* presContext = document->GetPresContext();
if (presContext) {
MOZ_ASSERT(this !=
presContext->GetViewportScrollbarStylesOverrideElement(),
presContext->GetViewportScrollStylesOverrideElement(),
"Leaving behind a raw pointer to this element (as having "
"propagated scrollbar styles) - that's dangerous...");
}

View File

@ -7291,7 +7291,7 @@ nsIDocument::UpdateViewportOverflowType(nscoord aScrolledWidth,
#ifdef DEBUG
MOZ_ASSERT(mPresShell);
nsPresContext* pc = GetPresContext();
MOZ_ASSERT(pc->GetViewportScrollbarStylesOverride().mHorizontal ==
MOZ_ASSERT(pc->GetViewportScrollStylesOverride().mHorizontal ==
NS_STYLE_OVERFLOW_HIDDEN,
"Should only be called when viewport has overflow-x: hidden");
MOZ_ASSERT(aScrolledWidth > aScrollportWidth,
@ -10696,7 +10696,7 @@ static void
UpdateViewportScrollbarOverrideForFullscreen(nsIDocument* aDoc)
{
if (nsPresContext* presContext = aDoc->GetPresContext()) {
presContext->UpdateViewportScrollbarStylesOverride();
presContext->UpdateViewportScrollStylesOverride();
}
}
@ -12638,6 +12638,13 @@ nsIDocument::HasBeenUserGestureActivated()
return mUserGestureActivated;
}
bool
nsIDocument::IsExtensionPage() const
{
return Preferences::GetBool("media.autoplay.allow-extension-background-pages", true) &&
BasePrincipal::Cast(NodePrincipal())->AddonPolicy();
}
nsIDocument*
nsIDocument::GetSameTypeParentDocument()
{

View File

@ -3924,7 +3924,7 @@ nsGlobalWindowInner::ScrollTo(const CSSIntPoint& aScroll,
scroll.y = maxpx;
}
bool smoothScroll = sf->GetScrollbarStyles().IsSmoothScroll(aOptions.mBehavior);
bool smoothScroll = sf->GetScrollStyles().IsSmoothScroll(aOptions.mBehavior);
sf->ScrollToCSSPixels(scroll, smoothScroll
? nsIScrollableFrame::SMOOTH_MSD
@ -3978,7 +3978,7 @@ nsGlobalWindowInner::ScrollByLines(int32_t numLines,
// It seems like it would make more sense for ScrollByLines to use
// SMOOTH mode, but tests seem to depend on the synchronous behaviour.
// Perhaps Web content does too.
bool smoothScroll = sf->GetScrollbarStyles().IsSmoothScroll(aOptions.mBehavior);
bool smoothScroll = sf->GetScrollStyles().IsSmoothScroll(aOptions.mBehavior);
sf->ScrollBy(nsIntPoint(0, numLines), nsIScrollableFrame::LINES,
smoothScroll
@ -3997,7 +3997,7 @@ nsGlobalWindowInner::ScrollByPages(int32_t numPages,
// It seems like it would make more sense for ScrollByPages to use
// SMOOTH mode, but tests seem to depend on the synchronous behaviour.
// Perhaps Web content does too.
bool smoothScroll = sf->GetScrollbarStyles().IsSmoothScroll(aOptions.mBehavior);
bool smoothScroll = sf->GetScrollStyles().IsSmoothScroll(aOptions.mBehavior);
sf->ScrollBy(nsIntPoint(0, numPages), nsIScrollableFrame::PAGES,
smoothScroll

View File

@ -3439,6 +3439,10 @@ public:
// document in the document tree.
bool HasBeenUserGestureActivated();
// This document is a WebExtension page, it might be a background page, a
// popup, a visible tab, a visible iframe ...e.t.c.
bool IsExtensionPage() const;
bool HasScriptsBlockedBySandbox();
bool InlineScriptAllowedByCSP();

View File

@ -2683,7 +2683,7 @@ EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent(
}
}
ScrollbarStyles ss = scrollableFrame->GetScrollbarStyles();
ScrollStyles ss = scrollableFrame->GetScrollStyles();
bool hiddenForV = (NS_STYLE_OVERFLOW_HIDDEN == ss.mVertical);
bool hiddenForH = (NS_STYLE_OVERFLOW_HIDDEN == ss.mHorizontal);
if ((hiddenForV && hiddenForH) ||
@ -2796,7 +2796,7 @@ EventStateManager::DoScrollText(nsIScrollableFrame* aScrollableFrame,
ComputeScrollAmountForDefaultAction(aEvent, scrollAmountInDevPixels);
// Don't scroll around the axis whose overflow style is hidden.
ScrollbarStyles overflowStyle = aScrollableFrame->GetScrollbarStyles();
ScrollStyles overflowStyle = aScrollableFrame->GetScrollStyles();
if (overflowStyle.mHorizontal == NS_STYLE_OVERFLOW_HIDDEN) {
actualDevPixelScrollAmount.x = 0;
}

View File

@ -2522,6 +2522,12 @@ HTMLMediaElement::ResumeLoad(PreloadAction aAction)
}
}
bool
HTMLMediaElement::AllowedToPlay() const
{
return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
}
void
HTMLMediaElement::UpdatePreloadAction()
{

View File

@ -640,6 +640,12 @@ public:
mIsCasting = aShow;
}
// Returns whether a call to Play() would be rejected with NotAllowedError.
// This assumes "worst case" for unknowns. So if prompting for permission is
// enabled and no permission is stored, this behaves as if the user would
// opt to block.
bool AllowedToPlay() const;
already_AddRefed<MediaSource> GetMozMediaSourceObject() const;
// Returns a string describing the state of the media player internal
// data. Used for debugging purposes.

View File

@ -532,7 +532,7 @@ TabChild::DoUpdateZoomConstraints(const uint32_t& aPresShellId,
const ViewID& aViewId,
const Maybe<ZoomConstraints>& aConstraints)
{
if (!mApzcTreeManager) {
if (!mApzcTreeManager || mDestroyed) {
return false;
}

View File

@ -71,6 +71,11 @@ IsWindowAllowedToPlay(nsPIDOMWindowInner* aWindow)
return true;
}
if (approver->IsExtensionPage()) {
// Always allow extension page to autoplay.
return true;
}
return false;
}

View File

@ -5,13 +5,13 @@ function playAndPostResult(muted, parent_window) {
element.src = "short.mp4";
element.id = "video";
document.body.appendChild(element);
let allowedToPlay = element.allowedToPlay;
element.play().then(
() => {
parent_window.postMessage({played: true}, "*");
parent_window.postMessage({played: true, allowedToPlay}, "*");
},
() => {
parent_window.postMessage({played: false}, "*");
parent_window.postMessage({played: false, allowedToPlay}, "*");
}
);
}

View File

@ -143,7 +143,8 @@
await once(child, "load");
child.postMessage(test_case, window.origin);
let result = await nextWindowMessage();
SimpleTest.is(result.data.played, test_case.should_play, test_case.name);
SimpleTest.is(result.data.allowedToPlay, test_case.should_play, "allowed - " + test_case.name);
SimpleTest.is(result.data.played, test_case.should_play, "played - " + test_case.name);
child.close();
}
SimpleTest.finish();

View File

@ -34,7 +34,8 @@
child.postMessage("play-audible", testCase.origin);
// Wait for the window to tell us whether it could play video.
let result = await nextWindowMessage();
is(result.data.played, testCase.shouldPlay, testCase.message);
is(result.data.allowedToPlay, testCase.shouldPlay, "allowedToPlay - " + testCase.message);
is(result.data.played, testCase.shouldPlay, "played - " + testCase.message);
child.close();
}

View File

@ -175,6 +175,8 @@ partial interface Document {
* etc.
*/
[Func="IsChromeOrXBL"] readonly attribute boolean mozSyntheticDocument;
[Throws, Func="IsChromeOrXBL"]
BoxObject? getBoxObjectFor(Element? element);
/**
* Returns the script element whose script is currently being processed.
*

View File

@ -219,3 +219,13 @@ partial interface HTMLMediaElement {
[Pref="media.test.video-suspend"]
boolean hasSuspendTaint();
};
/*
* API that exposes whether a call to HTMLMediaElement.play() would be
* blocked by autoplay policies; whether the promise returned by play()
* would be rejected with NotAllowedError.
*/
partial interface HTMLMediaElement {
[Pref="media.allowed-to-play.enabled"]
readonly attribute boolean allowedToPlay;
};

View File

@ -31,7 +31,4 @@ interface XULDocument : Document {
DOMString attr);
void removeBroadcastListenerFor(Element broadcaster, Element observer,
DOMString attr);
[Throws]
BoxObject? getBoxObjectFor(Element? element);
};

View File

@ -151,7 +151,6 @@ public:
const nsAString& aAttr, ErrorResult& aRv);
void RemoveBroadcastListenerFor(Element& aBroadcaster, Element& aListener,
const nsAString& aAttr);
using nsDocument::GetBoxObjectFor;
protected:
virtual ~XULDocument();

View File

@ -345,8 +345,12 @@ function moveMouseAndScrollWheelOver(element, dx, dy, testDriver, waitForScroll
// Synthesizes events to drag |element|'s vertical scrollbar by the distance
// specified, synthesizing a mousemove for each increment as specified.
// Returns false if the element doesn't have a vertical scrollbar, or true after
// all the events have been synthesized.
// Returns false if the element doesn't have a vertical scrollbar. Otherwise,
// returns a generator that should be invoked after the mousemoves have been
// processed by the widget code, to end the scrollbar drag. Mousemoves being
// processed by the widget code can be detected by listening for the mousemove
// events in the caller, or for some other event that is triggered by the
// mousemove, such as the scroll event resulting from the scrollbar drag.
function* dragVerticalScrollbar(element, testDriver, distance = 20, increment = 5) {
var boundingClientRect = element.getBoundingClientRect();
var verticalScrollbarWidth = boundingClientRect.width - element.clientWidth;
@ -369,8 +373,10 @@ function* dragVerticalScrollbar(element, testDriver, distance = 20, increment =
yield synthesizeNativeMouseEvent(element, mouseX, mouseY + y, nativeMouseMoveEventMsg(), testDriver);
}
yield synthesizeNativeMouseEvent(element, mouseX, mouseY + distance, nativeMouseMoveEventMsg(), testDriver);
// and release
yield synthesizeNativeMouseEvent(element, mouseX, mouseY + distance, nativeMouseUpEventMsg(), testDriver);
return true;
// and return a generator to call afterwards to finish up the drag
return function*() {
dump("Finishing drag of #" + element.id + "\n");
yield synthesizeNativeMouseEvent(element, mouseX, mouseY + distance, nativeMouseUpEventMsg(), testDriver);
};
}

View File

@ -237,6 +237,8 @@ function runSubtestsSeriallyInFreshWindows(aSubtests) {
return;
}
SimpleTest.ok(true, "Starting subtest " + test.file);
if (typeof test.dp_suppression != 'undefined') {
// Normally during a test, the displayport will get suppressed during page
// load, and unsuppressed at a non-deterministic time during the test. The

View File

@ -24,8 +24,8 @@ function* test(testDriver) {
var scrollableDiv = document.getElementById('scrollable');
scrollableDiv.addEventListener('scroll', () => setTimeout(testDriver, 0), {once: true});
var scrolled = yield* dragVerticalScrollbar(scrollableDiv, testDriver);
if (!scrolled) {
var dragFinisher = yield* dragVerticalScrollbar(scrollableDiv, testDriver);
if (!dragFinisher) {
ok(true, "No scrollbar, can't do this test");
return;
}
@ -35,6 +35,8 @@ function* test(testDriver) {
// triggered.
yield;
yield* dragFinisher();
// Flush everything just to be safe
yield flushApzRepaints(testDriver);

View File

@ -13,8 +13,8 @@ function* test(testDriver) {
var scrollableDiv = document.getElementById('scrollable');
scrollableDiv.addEventListener('scroll', () => setTimeout(testDriver, 0), {once: true});
var scrolled = yield* dragVerticalScrollbar(scrollableDiv, testDriver);
if (!scrolled) {
var dragFinisher = yield* dragVerticalScrollbar(scrollableDiv, testDriver);
if (!dragFinisher) {
ok(true, "No scrollbar, can't do this test");
return;
}
@ -24,6 +24,8 @@ function* test(testDriver) {
// triggered.
yield;
yield* dragFinisher();
// Flush everything just to be safe
yield flushApzRepaints(testDriver);

View File

@ -25,30 +25,13 @@
}
</style>
<script type="text/javascript">
var root;
var scrollPos;
var haveScrolled = false;
var generatedAll = false;
// Be careful not to call subtestDone() until we've scrolled AND generated
// all of the events.
function maybeDone() {
if (haveScrolled && generatedAll) {
subtestDone();
}
}
function scrolled(e) {
// Test that we have scrolled
ok(root.scrollTop > scrollPos, "document scrolled after dragging scrollbar");
haveScrolled = true;
maybeDone();
}
function* test(testDriver) {
root = document.scrollingElement;
scrollPos = root.scrollTop;
document.addEventListener('scroll', scrolled);
var root = document.scrollingElement;
var scrollPos = root.scrollTop;
document.addEventListener('scroll', () => {
ok(root.scrollTop > scrollPos, "document scrolled after dragging scrollbar");
setTimeout(testDriver, 0);
}, {once: true});
var scrollbarX = (window.innerWidth + root.clientWidth) / 2;
// Move the mouse to the scrollbar
@ -57,15 +40,15 @@ function* test(testDriver) {
yield synthesizeNativeMouseEvent(root, scrollbarX, 100, nativeMouseDownEventMsg(), testDriver);
// drag vertically
yield synthesizeNativeMouseEvent(root, scrollbarX, 150, nativeMouseMoveEventMsg(), testDriver);
// wait for the scroll listener to fire, which will resume this function
yield;
// and release
yield synthesizeNativeMouseEvent(root, scrollbarX, 150, nativeMouseUpEventMsg(), testDriver);
generatedAll = true;
maybeDone();
}
waitUntilApzStable()
.then(runContinuation(test));
.then(runContinuation(test))
.then(subtestDone);
</script>
</head>

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