mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 09:15:35 +00:00
Merge autoland to mozilla-central. a=merge
This commit is contained in:
commit
63b27e89bf
File diff suppressed because one or more lines are too long
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -557,6 +557,7 @@
|
||||
if (this.getAttribute("movingtab") != "true") {
|
||||
this.setAttribute("movingtab", "true");
|
||||
this.parentNode.setAttribute("movingtab", "true");
|
||||
if (!draggedTab.multiselected)
|
||||
this.selectedItem = draggedTab;
|
||||
}
|
||||
|
||||
@ -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)";
|
||||
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 != draggedTab) {
|
||||
transitionendEvent.originalTarget != tab) {
|
||||
return;
|
||||
}
|
||||
draggedTab.removeEventListener("transitionend", onTransitionEnd);
|
||||
tab.removeEventListener("transitionend", onTransitionEnd);
|
||||
|
||||
draggedTab.removeAttribute("tabdrop-samewindow");
|
||||
tab.removeAttribute("tabdrop-samewindow");
|
||||
|
||||
this._finishAnimateTabMove();
|
||||
if (dropIndex !== false) {
|
||||
gBrowser.moveTabTo(draggedTab, dropIndex);
|
||||
gBrowser.moveTabTo(tab, dropIndex);
|
||||
if (incrementDropIndex)
|
||||
dropIndex++;
|
||||
}
|
||||
|
||||
gBrowser.syncThrobberAnimations(draggedTab);
|
||||
gBrowser.syncThrobberAnimations(tab);
|
||||
};
|
||||
draggedTab.addEventListener("transitionend", onTransitionEnd);
|
||||
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 &&
|
||||
|
@ -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]
|
||||
|
@ -7,6 +7,6 @@
|
||||
<meta charset="utf8">
|
||||
</head>
|
||||
<body>
|
||||
<iframe src="http://tracking.example.com/"></iframe>
|
||||
<iframe src="http://trackertest.org/"></iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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");
|
@ -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");
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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]
|
||||
|
@ -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 */);
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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": {
|
||||
|
BIN
browser/components/extensions/test/browser/silence.ogg
Normal file
BIN
browser/components/extensions/test/browser/silence.ogg
Normal file
Binary file not shown.
@ -5,4 +5,3 @@ support-files =
|
||||
tags = webextensions
|
||||
|
||||
[test_ext_all_apis.html]
|
||||
skip-if = true
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
29
browser/components/newtab/content-src/asrouter/README.md
Normal file
29
browser/components/newtab/content-src/asrouter/README.md
Normal 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)
|
@ -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}});
|
||||
}
|
||||
|
@ -36,11 +36,27 @@
|
||||
},
|
||||
"targeting": {
|
||||
"type": "string",
|
||||
"description": "a JEXL expression representing targeting information"
|
||||
"description": "A JEXL expression representing targeting information"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
"description": "An action to trigger potentially showing the message",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "A string representing what the trigger to show this message is."
|
||||
"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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
@ -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
@ -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
@ -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
@ -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"]
|
||||
}
|
||||
```
|
||||
|
@ -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")
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
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, data);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
118
browser/components/newtab/lib/ASRouterTriggerListeners.jsm
Normal file
118
browser/components/newtab/lib/ASRouterTriggerListeners.jsm
Normal 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"];
|
@ -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
|
||||
|
@ -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"}
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}.
|
||||
|
@ -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=ही पायरी वगळा
|
||||
|
@ -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",
|
||||
|
@ -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}.",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "ही पायरी वगळा"
|
||||
};
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)));
|
||||
});
|
6
browser/components/newtab/test/browser/red_page.html
Normal file
6
browser/components/newtab/test/browser/red_page.html
Normal file
@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body style="background-color: red" />
|
||||
</html>
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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});
|
||||
|
@ -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"});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ Important Concepts
|
||||
files-metadata
|
||||
Profile Guided Optimization <pgo>
|
||||
slow
|
||||
tup
|
||||
environment-variables
|
||||
build-targets
|
||||
python
|
||||
|
@ -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
106
build/docs/tup.rst
Normal 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.
|
@ -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:])
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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...
|
||||
|
16
docshell/test/file_bug1450164.html
Normal file
16
docshell/test/file_bug1450164.html
Normal 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>
|
@ -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]
|
||||
|
31
docshell/test/test_bug1450164.html
Normal file
31
docshell/test/test_bug1450164.html
Normal 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>
|
@ -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)) {
|
||||
|
@ -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...");
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -2522,6 +2522,12 @@ HTMLMediaElement::ResumeLoad(PreloadAction aAction)
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
HTMLMediaElement::AllowedToPlay() const
|
||||
{
|
||||
return AutoplayPolicy::IsAllowedToPlay(*this) == nsIAutoplay::ALLOWED;
|
||||
}
|
||||
|
||||
void
|
||||
HTMLMediaElement::UpdatePreloadAction()
|
||||
{
|
||||
|
@ -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.
|
||||
|
@ -532,7 +532,7 @@ TabChild::DoUpdateZoomConstraints(const uint32_t& aPresShellId,
|
||||
const ViewID& aViewId,
|
||||
const Maybe<ZoomConstraints>& aConstraints)
|
||||
{
|
||||
if (!mApzcTreeManager) {
|
||||
if (!mApzcTreeManager || mDestroyed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,11 @@ IsWindowAllowedToPlay(nsPIDOMWindowInner* aWindow)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (approver->IsExtensionPage()) {
|
||||
// Always allow extension page to autoplay.
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -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}, "*");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -31,7 +31,4 @@ interface XULDocument : Document {
|
||||
DOMString attr);
|
||||
void removeBroadcastListenerFor(Element broadcaster, Element observer,
|
||||
DOMString attr);
|
||||
|
||||
[Throws]
|
||||
BoxObject? getBoxObjectFor(Element? element);
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user