Update On Sat Mar 25 19:47:48 CET 2023
This commit is contained in:
parent
8fbe161a32
commit
7deff43da8
977 changed files with 14892 additions and 37853 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
@ -194,7 +194,7 @@ dependencies = [
|
|||
"askama_escape",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
|
@ -327,7 +327,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "audioipc2"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=73c8a02da8f2ff022723307bfafa3a58a61448da#73c8a02da8f2ff022723307bfafa3a58a61448da"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=916f65cc92f6f2484183ff4681b0e9a2bfd60fe7#916f65cc92f6f2484183ff4681b0e9a2bfd60fe7"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"ashmem",
|
||||
|
@ -336,7 +336,7 @@ dependencies = [
|
|||
"byteorder",
|
||||
"bytes 1.4.0",
|
||||
"cc",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-queue 0.3.8",
|
||||
"cubeb",
|
||||
"error-chain",
|
||||
"iovec",
|
||||
|
@ -355,7 +355,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "audioipc2-client"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=73c8a02da8f2ff022723307bfafa3a58a61448da#73c8a02da8f2ff022723307bfafa3a58a61448da"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=916f65cc92f6f2484183ff4681b0e9a2bfd60fe7#916f65cc92f6f2484183ff4681b0e9a2bfd60fe7"
|
||||
dependencies = [
|
||||
"audio_thread_priority",
|
||||
"audioipc2",
|
||||
|
@ -366,7 +366,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "audioipc2-server"
|
||||
version = "0.5.0"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=73c8a02da8f2ff022723307bfafa3a58a61448da#73c8a02da8f2ff022723307bfafa3a58a61448da"
|
||||
source = "git+https://github.com/kinetiknz/audioipc-2?rev=916f65cc92f6f2484183ff4681b0e9a2bfd60fe7#916f65cc92f6f2484183ff4681b0e9a2bfd60fe7"
|
||||
dependencies = [
|
||||
"audio_thread_priority",
|
||||
"audioipc2",
|
||||
|
@ -392,7 +392,7 @@ dependencies = [
|
|||
"libudev",
|
||||
"log",
|
||||
"memoffset 0.6.99",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"nss-gk-api",
|
||||
"pkcs11-bindings",
|
||||
"rand 0.8.5",
|
||||
|
@ -613,7 +613,7 @@ name = "builtins-static"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bindgen 0.63.0",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
"pkcs11-bindings",
|
||||
"smallvec",
|
||||
]
|
||||
|
@ -730,7 +730,7 @@ version = "0.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1075,6 +1075,16 @@ dependencies = [
|
|||
"crossbeam-utils 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.6.6"
|
||||
|
@ -1404,7 +1414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9313f104b590510b46fc01c0a324fc76505c13871454d3c48490468d04c8d395"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2371,11 +2381,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
|||
|
||||
[[package]]
|
||||
name = "glsl"
|
||||
version = "6.0.1"
|
||||
version = "6.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd49bbe5e12dd5aed9d66a84899af422f3d4fcfdd20b2294c8b4ade11500b05"
|
||||
checksum = "65c80dbf169ac31dbe6e0a69a7cef0b09ec9805f955da206ff1ee2e47895f836"
|
||||
dependencies = [
|
||||
"nom 6.99.99",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3812,13 +3822,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "6.99.99"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
@ -5102,6 +5105,7 @@ name = "static_prefs"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mozbuild",
|
||||
"nsstring",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5577,7 +5581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f0c32ffea4827978e9aa392d2f743d973c1dfa3730a2ed3f22ce1e6984da848c"
|
||||
dependencies = [
|
||||
"crossbeam-deque 0.7.4",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-queue 0.1.2",
|
||||
"crossbeam-utils 0.6.6",
|
||||
"futures 0.1.31",
|
||||
"lazy_static",
|
||||
|
@ -6383,7 +6387,7 @@ version = "4.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -6466,6 +6470,7 @@ dependencies = [
|
|||
"nsstring",
|
||||
"parking_lot 0.11.2",
|
||||
"serde",
|
||||
"static_prefs",
|
||||
"wgpu-core",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
|
|
|
@ -127,9 +127,6 @@ bindgen = { path = "build/rust/bindgen" }
|
|||
# Patch memoffset 0.6 to 0.7
|
||||
memoffset = { path = "build/rust/memoffset" }
|
||||
|
||||
# Patch nom 6 to 7
|
||||
nom = { path = "build/rust/nom" }
|
||||
|
||||
# Patch nix 0.24 to 0.25
|
||||
nix = { path = "build/rust/nix" }
|
||||
|
||||
|
|
|
@ -49,18 +49,26 @@ bool EventQueue::PushEvent(AccEvent* aEvent) {
|
|||
(aEvent->mEventType == nsIAccessibleEvent::EVENT_NAME_CHANGE ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
|
||||
aEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED)) {
|
||||
PushNameOrDescriptionChange(aEvent->mAccessible);
|
||||
PushNameOrDescriptionChange(aEvent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EventQueue::PushNameOrDescriptionChange(LocalAccessible* aTarget) {
|
||||
bool EventQueue::PushNameOrDescriptionChange(AccEvent* aOrigEvent) {
|
||||
// Fire name/description change event on parent or related LocalAccessible
|
||||
// being labelled/described given that this event hasn't been coalesced, the
|
||||
// dependent's name/description was calculated from this subtree, and the
|
||||
// subtree was changed.
|
||||
const bool doName = aTarget->HasNameDependent();
|
||||
const bool doDesc = aTarget->HasDescriptionDependent();
|
||||
LocalAccessible* target = aOrigEvent->mAccessible;
|
||||
// If the text of a text leaf changed without replacing the leaf, the only
|
||||
// event we get is text inserted on the container. In this case, we might
|
||||
// need to fire a name change event on the target itself.
|
||||
const bool maybeTargetNameChanged =
|
||||
(aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_REMOVED ||
|
||||
aOrigEvent->mEventType == nsIAccessibleEvent::EVENT_TEXT_INSERTED) &&
|
||||
nsTextEquivUtils::HasNameRule(target, eNameFromSubtreeRule);
|
||||
const bool doName = target->HasNameDependent() || maybeTargetNameChanged;
|
||||
const bool doDesc = target->HasDescriptionDependent();
|
||||
if (!doName && !doDesc) {
|
||||
return false;
|
||||
}
|
||||
|
@ -69,11 +77,11 @@ bool EventQueue::PushNameOrDescriptionChange(LocalAccessible* aTarget) {
|
|||
// Only continue traversing up the tree if it's possible that the parent
|
||||
// LocalAccessible's name (or a LocalAccessible being labelled by this
|
||||
// LocalAccessible or an ancestor) can depend on this LocalAccessible's name.
|
||||
LocalAccessible* parent = aTarget;
|
||||
LocalAccessible* parent = target;
|
||||
do {
|
||||
// Test possible name dependent parent.
|
||||
if (doName) {
|
||||
if (nameCheckAncestor && parent != aTarget &&
|
||||
if (nameCheckAncestor && (maybeTargetNameChanged || parent != target) &&
|
||||
nsTextEquivUtils::HasNameRule(parent, eNameFromSubtreeRule)) {
|
||||
nsAutoString name;
|
||||
ENameValueFlag nameFlag = parent->Name(name);
|
||||
|
|
|
@ -28,7 +28,7 @@ class EventQueue {
|
|||
/**
|
||||
* Puts name and/or description change events into the queue, if needed.
|
||||
*/
|
||||
bool PushNameOrDescriptionChange(LocalAccessible* aTarget);
|
||||
bool PushNameOrDescriptionChange(AccEvent* aOrigEvent);
|
||||
|
||||
/**
|
||||
* Process events from the queue and fires events.
|
||||
|
|
|
@ -185,7 +185,6 @@ bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) {
|
|||
// or hidden children of a container. So either queue a new one, or move an
|
||||
// existing one to the end of the queue if the container already has a
|
||||
// reorder event.
|
||||
LocalAccessible* target = aEvent->GetAccessible();
|
||||
LocalAccessible* container = aEvent->GetAccessible()->LocalParent();
|
||||
RefPtr<AccReorderEvent> reorder;
|
||||
if (!container->ReorderEventTarget()) {
|
||||
|
@ -195,7 +194,7 @@ bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) {
|
|||
|
||||
// Since this is the first child of container that is changing, the name
|
||||
// and/or description of dependent Accessibles may be changing.
|
||||
if (PushNameOrDescriptionChange(target)) {
|
||||
if (PushNameOrDescriptionChange(aEvent)) {
|
||||
ScheduleProcessing();
|
||||
}
|
||||
} else {
|
||||
|
@ -231,6 +230,7 @@ bool NotificationController::QueueMutationEvent(AccTreeMutationEvent* aEvent) {
|
|||
return true;
|
||||
}
|
||||
|
||||
LocalAccessible* target = aEvent->GetAccessible();
|
||||
int32_t offset = container->AsHyperText()->GetChildOffset(target);
|
||||
AccTreeMutationEvent* prevEvent = aEvent->PrevEvent();
|
||||
while (prevEvent &&
|
||||
|
|
|
@ -711,7 +711,7 @@ ROLE(COLOR_CHOOSER,
|
|||
ROLE(DATE_EDITOR,
|
||||
"date editor",
|
||||
ATK_ROLE_DATE_EDITOR,
|
||||
@"AXDateField",
|
||||
@"AXGroup",
|
||||
NSAccessibilityUnknownSubrole,
|
||||
USE_ROLE_STRING,
|
||||
IA2_ROLE_DATE_EDITOR,
|
||||
|
|
|
@ -313,6 +313,9 @@ Class a11y::GetTypeFromRole(roles::Role aRole) {
|
|||
case roles::PAGETAB:
|
||||
return [mozTabAccessible class];
|
||||
|
||||
case roles::DATE_EDITOR:
|
||||
return [mozDatePickerAccessible class];
|
||||
|
||||
case roles::CHECKBUTTON:
|
||||
case roles::TOGGLE_BUTTON:
|
||||
case roles::SWITCH:
|
||||
|
|
|
@ -99,3 +99,10 @@
|
|||
- (void)changeValueBySteps:(int)factor;
|
||||
|
||||
@end
|
||||
|
||||
@interface mozDatePickerAccessible : mozAccessible
|
||||
|
||||
// override
|
||||
- (NSString*)moxTitle;
|
||||
|
||||
@end
|
||||
|
|
|
@ -229,3 +229,11 @@ using namespace mozilla::a11y;
|
|||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation mozDatePickerAccessible
|
||||
|
||||
- (NSString*)moxTitle {
|
||||
return utils::LocalizedString(u"dateField"_ns);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -74,6 +74,7 @@ addAccessibleTask(
|
|||
<div id="tooltip" role="tooltip"></div>
|
||||
<input type="radio" role="menuitemradio" id="menuitemradio">
|
||||
<input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox">
|
||||
<input type="datetime-local" id="datetime">
|
||||
|
||||
<!-- text entries -->
|
||||
<div id="textbox_multiline" role="textbox" aria-multiline="true"></div>
|
||||
|
@ -187,6 +188,14 @@ addAccessibleTask(
|
|||
testRoleAndSubRole(accDoc, "tooltip", "AXGroup", "AXUserInterfaceTooltip");
|
||||
testRoleAndSubRole(accDoc, "menuitemradio", "AXMenuItem", null);
|
||||
testRoleAndSubRole(accDoc, "menuitemcheckbox", "AXMenuItem", null);
|
||||
testRoleAndSubRole(accDoc, "datetime", "AXGroup", null);
|
||||
// XXX for datetime elements, we spoof the role via the title, since
|
||||
// providing the correct role results in the internal elements being
|
||||
// unreachable by VO
|
||||
is(
|
||||
getNativeInterface(accDoc, "datetime").getAttributeValue("AXTitle"),
|
||||
"date field"
|
||||
);
|
||||
|
||||
// Text boxes
|
||||
testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea");
|
||||
|
|
|
@ -132,10 +132,18 @@
|
|||
await nameChanged;
|
||||
|
||||
nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "listitem");
|
||||
info("Changing text of listitem child");
|
||||
info("Changing textContent of listitem child");
|
||||
// Changing textContent replaces the text leaf with a new one.
|
||||
getNode("listitem").textContent = "world";
|
||||
await nameChanged;
|
||||
|
||||
nameChanged = PromEvents.waitForEvent(EVENT_NAME_CHANGE, "button");
|
||||
info("Changing text of button's text leaf");
|
||||
// Changing the text node's data changes the text without replacing the
|
||||
// leaf.
|
||||
getNode("button").firstChild.data = "after";
|
||||
await nameChanged;
|
||||
|
||||
SimpleTest.finish();
|
||||
}
|
||||
|
||||
|
@ -170,6 +178,8 @@
|
|||
|
||||
<ul><li id="listitem">hello</li></ul>
|
||||
|
||||
<button id="button">before</button>
|
||||
|
||||
<div id="eventdump"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -5,21 +5,11 @@
|
|||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"AboutReader",
|
||||
"resource://gre/modules/AboutReader.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"ReaderMode",
|
||||
"resource://gre/modules/ReaderMode.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"Readerable",
|
||||
"resource://gre/modules/Readerable.jsm"
|
||||
);
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
AboutReader: "resource://gre/modules/AboutReader.sys.mjs",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
||||
Readerable: "resource://gre/modules/Readerable.sys.mjs",
|
||||
});
|
||||
|
||||
var gUrlsToDocContentType = new Map();
|
||||
var gUrlsToDocTitle = new Map();
|
||||
|
|
|
@ -7,12 +7,8 @@ const lazy = {};
|
|||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
||||
});
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"ReaderMode",
|
||||
"resource://gre/modules/ReaderMode.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"pktApi",
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
* 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/. */
|
||||
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
|
@ -9,6 +11,13 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
lazy,
|
||||
"serpEventsEnabled",
|
||||
"browser.search.serpEventTelemetry.enabled",
|
||||
false
|
||||
);
|
||||
|
||||
const SHARED_DATA_KEY = "SearchTelemetry:ProviderInfo";
|
||||
export const ADLINK_CHECK_TIMEOUT_MS = 1000;
|
||||
|
||||
|
@ -78,7 +87,413 @@ class SearchProviders {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans SERPs for ad components.
|
||||
*/
|
||||
class SearchAdImpression {
|
||||
/**
|
||||
* A reference to ad component information that is used if an anchor
|
||||
* element could not be categorized to a specific ad component.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
#defaultComponent = null;
|
||||
|
||||
/**
|
||||
* Maps DOM elements to AdData.
|
||||
*
|
||||
* @type {Map<Element, AdData>}
|
||||
*
|
||||
* @typedef AdData
|
||||
* @type {object}
|
||||
* @property {string} type
|
||||
* The type of ad component.
|
||||
* @property {number} adsLoaded
|
||||
* The number of ads counted as loaded for the component.
|
||||
* @property {boolean} countChildren
|
||||
* Whether all the children were counted for the component.
|
||||
*/
|
||||
#elementToAdDataMap = new Map();
|
||||
|
||||
/**
|
||||
* Height of the inner window in the browser.
|
||||
*/
|
||||
#innerWindowHeight = 0;
|
||||
|
||||
set innerWindowHeight(height) {
|
||||
this.#innerWindowHeight = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference the providerInfo for this SERP.
|
||||
*
|
||||
* @type {object}
|
||||
*/
|
||||
#providerInfo = null;
|
||||
|
||||
set providerInfo(providerInfo) {
|
||||
if (this.#providerInfo?.telemetryId == providerInfo.telemetryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#providerInfo = providerInfo;
|
||||
for (let component of this.#providerInfo.components) {
|
||||
if (component.included?.default) {
|
||||
this.#defaultComponent = component;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How far from the top the page has been scrolled.
|
||||
*/
|
||||
#scrollFromTop = 0;
|
||||
|
||||
set scrollFromTop(distance) {
|
||||
this.#scrollFromTop = distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of anchor elements, group them into ad components
|
||||
* and count the number of loaded, visible, and hidden ads.
|
||||
*
|
||||
* The first step in the process is to check if the anchor should be
|
||||
* inspected. This is based on whether it contains an href or a
|
||||
* data-attribute values that matches an ad link.
|
||||
*
|
||||
* If it looks like an ad link, determine which ad component it belongs to
|
||||
* and the number of ads for the component. The heuristic is described in
|
||||
* findAdDataForAnchor. If there was a result and we haven't seen it before,
|
||||
* save in elementToAdDataMap.
|
||||
*
|
||||
* Once all the links have been parsed, go through each component
|
||||
* in elementToAdDataMap and check if the ad was visible to the user.
|
||||
*
|
||||
* @param {HTMLCollectionOf<HTMLAnchorElement>} anchors
|
||||
* Anchors to inspect.
|
||||
* @returns {Map<string, object>}
|
||||
* A map where the key is a string containing the type of ad component
|
||||
* and the value is an object containing the number of adsLoaded,
|
||||
* adsVisible, and adsHidden within the component.
|
||||
*/
|
||||
resultFromAnchors(anchors) {
|
||||
for (let anchor of anchors) {
|
||||
if (this.#shouldInspectAnchor(anchor)) {
|
||||
let result = this.#findAdDataForAnchor(anchor);
|
||||
if (result) {
|
||||
this.#recordElementData(result.element, {
|
||||
type: result.type,
|
||||
count: result.count,
|
||||
countChildren: result.countChildren,
|
||||
childElements: result.childElements,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let componentToVisibilityMap = new Map();
|
||||
// Count the number of visible and hidden ads within each cached
|
||||
// element and save the results according to the component they
|
||||
// belonged to.
|
||||
for (let [element, data] of this.#elementToAdDataMap.entries()) {
|
||||
let count = this.#countVisibleAndHiddenAds(
|
||||
element,
|
||||
data.adsLoaded,
|
||||
data.childElements
|
||||
);
|
||||
if (componentToVisibilityMap.has(data.type)) {
|
||||
let componentInfo = componentToVisibilityMap.get(data.type);
|
||||
componentInfo.adsLoaded += data.adsLoaded;
|
||||
componentInfo.adsVisible += count.adsVisible;
|
||||
componentInfo.adsHidden += count.adsHidden;
|
||||
} else {
|
||||
componentToVisibilityMap.set(data.type, {
|
||||
adsLoaded: data.adsLoaded,
|
||||
adsVisible: count.adsVisible,
|
||||
adsHidden: count.adsHidden,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Release the DOM elements from the Map.
|
||||
this.#elementToAdDataMap.clear();
|
||||
|
||||
return componentToVisibilityMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates whether an anchor should be inspected based on matching
|
||||
* regular expressions on either its href or specified data-attribute values.
|
||||
*
|
||||
* @param {HTMLAnchorElement} anchor
|
||||
* @returns {boolean}
|
||||
*/
|
||||
#shouldInspectAnchor(anchor) {
|
||||
if (!anchor.href) {
|
||||
return false;
|
||||
}
|
||||
let adServerAttributes = this.#providerInfo.adServerAttributes ?? [];
|
||||
let regexps = this.#providerInfo.extraAdServersRegexps;
|
||||
// Anchors can contain ad links in a data-attribute.
|
||||
for (let name of adServerAttributes) {
|
||||
if (
|
||||
anchor.dataset[name] &&
|
||||
regexps.some(regexp => regexp.test(anchor.dataset[name]))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Anchors can contain ad links in a specific href.
|
||||
if (regexps.some(regexp => regexp.test(anchor.href))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the ad data for an anchor.
|
||||
*
|
||||
* To categorize the anchor, we iterate over the list of possible components
|
||||
* the anchor could be categorized. If the component is default, we skip
|
||||
* checking because the fallback option for all anchor links is the default.
|
||||
*
|
||||
* First, get the "parent" of the anchor which best represents the DOM element
|
||||
* that contains the anchor links for the component and no other component.
|
||||
* This parent will be cached so that other anchors that share the same
|
||||
* parent can be counted together.
|
||||
*
|
||||
* The check for a parent is a loop because we can define more than one best
|
||||
* parent since on certain SERPs, it's possible for a "better" DOM element
|
||||
* parent to appear occassionally.
|
||||
*
|
||||
* If no parent is found, skip this component.
|
||||
*
|
||||
* If a parent was found, check for specific child elements.
|
||||
*
|
||||
* Finding child DOM elements of a parent is optional. One reason to do so is
|
||||
* to use child elements instead of anchor links to count the number of ads for
|
||||
* a component via the `countChildren` property. This is provided because some ads
|
||||
* (i.e. carousels) have multiple ad links in a single child element that go to the
|
||||
* same location. In this scenario, all instances of the child are recorded as ads.
|
||||
* Subsequent anchor elements that map to the same parent are ignored.
|
||||
*
|
||||
* Whether or not a child was found, return the information that was found,
|
||||
* including whether or not all child elements were counted instead of anchors.
|
||||
*
|
||||
* If another anchor belonging to a parent that was previously recorded is the input
|
||||
* for this function, we either increment the ad count by 1 or don't increment the ad
|
||||
* count because the parent used `countChildren` completed the calculation in a
|
||||
* previous step.
|
||||
*
|
||||
*
|
||||
* @param {HTMLAnchorElement} anchor
|
||||
* The anchor to be inspected.
|
||||
* @returns {object}
|
||||
* An object containing the element representing the root DOM element for
|
||||
* the component, the type of component, how many ads were counted,
|
||||
* and whether or not the count was of all the children.
|
||||
*/
|
||||
#findAdDataForAnchor(anchor) {
|
||||
for (let component of this.#providerInfo.components) {
|
||||
// First, check various conditions for skipping a component.
|
||||
|
||||
// A component should always have at least one included statement,
|
||||
// and a included statement with a parent.
|
||||
if (!component.included || !component.included.parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The default component doesn't need to be checked,
|
||||
// as it will be the fallback option.
|
||||
if (component.included.default) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the parent of the anchor.
|
||||
let parent = anchor.closest(component.included.parent.selector);
|
||||
|
||||
// If no parent was found, this wasn't the right component.
|
||||
if (!parent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we've already inspected the parent, either:
|
||||
// Increment the number of ads seen for this element,
|
||||
// or if its child elements have already been counted, return null.
|
||||
if (this.#elementToAdDataMap.has(parent)) {
|
||||
return !this.#childElementsCounted(parent)
|
||||
? { element: parent, count: 1, childElements: [anchor] }
|
||||
: null;
|
||||
}
|
||||
|
||||
// If the component has no defined children, return the parent element.
|
||||
if (component.included.children) {
|
||||
// Look for the first instance of a matching child selector.
|
||||
for (let child of component.included.children) {
|
||||
// If counting by child, get all of them at once.
|
||||
if (child.countChildren) {
|
||||
let childElements = parent.querySelectorAll(child.selector);
|
||||
if (childElements.length) {
|
||||
return {
|
||||
element: parent,
|
||||
type: component.type,
|
||||
count: childElements.length,
|
||||
countChildren: child.countChildren,
|
||||
childElements,
|
||||
};
|
||||
}
|
||||
} else if (parent.querySelector(child.selector)) {
|
||||
return {
|
||||
element: parent,
|
||||
type: component.type,
|
||||
count: 1,
|
||||
childElements: [anchor],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no children were defined for this component, or none were found
|
||||
// in the DOM, use the default definition.
|
||||
return {
|
||||
element: parent,
|
||||
type: component.type,
|
||||
count: 1,
|
||||
childElements: [anchor],
|
||||
};
|
||||
}
|
||||
// If no component was found, use default values.
|
||||
return {
|
||||
element: anchor,
|
||||
type: this.#defaultComponent.type,
|
||||
count: 1,
|
||||
childElements: [anchor],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether or not an ad was visible or hidden.
|
||||
*
|
||||
* An ad is considered visible if it has non-zero dimensions, and is in
|
||||
* the possible viewing area of the users window at the time the ad
|
||||
* impression is recorded.
|
||||
*
|
||||
* @param {Element} element
|
||||
* Element to be inspected
|
||||
* @param {number} adsLoaded
|
||||
* Number of ads initially determined to be loaded for this element.
|
||||
* @param {NodeListOf<Element>} childElements
|
||||
* List of children belonging to element.
|
||||
* @returns {object}
|
||||
* Contains adsVisible which is the number of ads shown for the element
|
||||
* and adsHidden, the number of ads not visible to the user.
|
||||
*/
|
||||
#countVisibleAndHiddenAds(element, adsLoaded, childElements) {
|
||||
let elementRect = element.getBoundingClientRect();
|
||||
|
||||
// If the element lacks a dimension, assume all ads that
|
||||
// were contained within it are hidden.
|
||||
if (elementRect.width == 0 || elementRect.height == 0) {
|
||||
return {
|
||||
adsVisible: 0,
|
||||
adsHidden: adsLoaded,
|
||||
};
|
||||
}
|
||||
|
||||
let adsVisible = 0;
|
||||
let adsHidden = 0;
|
||||
for (let child of childElements) {
|
||||
let itemRect = child.getBoundingClientRect();
|
||||
|
||||
// If the element we're inspecting has no dimension, it is hidden.
|
||||
if (itemRect.height == 0 || itemRect.width == 0) {
|
||||
adsHidden += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the element is to the left of the containing element, or to the
|
||||
// right of the containing element, skip it.
|
||||
if (
|
||||
itemRect.x < elementRect.x ||
|
||||
itemRect.x + itemRect.width > elementRect.x + elementRect.width
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the element is too far down, skip it.
|
||||
if (this.#scrollFromTop + this.#innerWindowHeight < elementRect.y) {
|
||||
continue;
|
||||
}
|
||||
++adsVisible;
|
||||
}
|
||||
|
||||
return {
|
||||
adsVisible,
|
||||
adsHidden,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches ad data for a DOM element. The key of the map is by Element rather
|
||||
* than Component for fast lookup on whether an Element has been already been
|
||||
* categorized as a component.
|
||||
*
|
||||
* @param {Element} element
|
||||
* The element considered to be the root for the component.
|
||||
* @param {object} params
|
||||
* Various parameters that can be recorded. Whether the input values exist
|
||||
* or not depends on which component was found, which heuristic should be used
|
||||
* to determine whether an ad was visible, and whether we've already seen this
|
||||
* element.
|
||||
* @param {string | null} params.type
|
||||
* The type of component.
|
||||
* @param {number} params.count
|
||||
* The number of ads found for a component. The number represents either
|
||||
* the number of elements that match an ad expression or the number of DOM
|
||||
* elements containing an ad link.
|
||||
* @param {boolean | null} params.countChildren
|
||||
* Whether all the children were counted for the element.
|
||||
* @param {Array<Element> | null} params.childElements
|
||||
* An array of DOM elements to inspect.
|
||||
*/
|
||||
#recordElementData(
|
||||
element,
|
||||
{ type, count = 0, countChildren = false, childElements = null } = {}
|
||||
) {
|
||||
if (this.#elementToAdDataMap.has(element)) {
|
||||
let recordedValues = this.#elementToAdDataMap.get(element);
|
||||
recordedValues.adsLoaded = recordedValues.adsLoaded + count;
|
||||
if (childElements) {
|
||||
recordedValues.childElements = recordedValues.childElements.concat(
|
||||
childElements
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.#elementToAdDataMap.set(element, {
|
||||
type,
|
||||
adsLoaded: count,
|
||||
countChildren,
|
||||
childElements,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a DOM element, return whether or not this element was counted
|
||||
* by specific child elements rather than the number of anchor links.
|
||||
*
|
||||
* @param {Element} domElement
|
||||
* The element to lookup.
|
||||
* @returns {boolean}
|
||||
* Returns true if child elements were counted, false otherwise.
|
||||
*/
|
||||
#childElementsCounted(domElement) {
|
||||
return !!this.#elementToAdDataMap.get(domElement)?.countChildren;
|
||||
}
|
||||
}
|
||||
|
||||
const searchProviders = new SearchProviders();
|
||||
const searchAdImpression = new SearchAdImpression();
|
||||
|
||||
/**
|
||||
* SearchTelemetryChild monitors for pages that are partner searches, and
|
||||
|
@ -106,7 +521,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
* Checks to see if the page is a partner and has an ad link within it. If so,
|
||||
* it will notify SearchTelemetry.
|
||||
*/
|
||||
_checkForAdLink() {
|
||||
_checkForAdLink(eventType) {
|
||||
try {
|
||||
if (!this.contentWindow) {
|
||||
return;
|
||||
|
@ -144,11 +559,27 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAds) {
|
||||
this.sendAsyncMessage("SearchTelemetry:PageInfo", {
|
||||
hasAds: true,
|
||||
hasAds,
|
||||
url,
|
||||
});
|
||||
|
||||
if (
|
||||
lazy.serpEventsEnabled &&
|
||||
providerInfo?.components &&
|
||||
(eventType == "load" || eventType == "pageshow")
|
||||
) {
|
||||
searchAdImpression.providerInfo = providerInfo;
|
||||
searchAdImpression.scrollFromTop = this.contentWindow.scrollY;
|
||||
searchAdImpression.innerWindowHeight = this.contentWindow.innerHeight;
|
||||
let adImpressions = searchAdImpression.resultFromAnchors(anchors);
|
||||
this.sendAsyncMessage("SearchTelemetry:AdImpressions", {
|
||||
adImpressions,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,10 +595,10 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
}
|
||||
};
|
||||
|
||||
const check = () => {
|
||||
const check = eventType => {
|
||||
cancelCheck();
|
||||
this._waitForContentTimeout = lazy.setTimeout(() => {
|
||||
this._checkForAdLink();
|
||||
this._checkForAdLink(eventType);
|
||||
}, ADLINK_CHECK_TIMEOUT_MS);
|
||||
};
|
||||
|
||||
|
@ -178,12 +609,12 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
// so that we remain consistent with the *.in-content:sap* count for the
|
||||
// SEARCH_COUNTS histogram.
|
||||
if (event.persisted) {
|
||||
check();
|
||||
check(event.type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "DOMContentLoaded": {
|
||||
check();
|
||||
check(event.type);
|
||||
break;
|
||||
}
|
||||
case "load": {
|
||||
|
@ -192,7 +623,7 @@ export class SearchSERPTelemetryChild extends JSWindowActorChild {
|
|||
// We still check at DOMContentLoaded because if the page hasn't
|
||||
// finished loading and the user navigates away, we still want to know
|
||||
// if there were ads on the page or not at that time.
|
||||
check();
|
||||
check(event.type);
|
||||
break;
|
||||
}
|
||||
case "unload": {
|
||||
|
|
|
@ -12,8 +12,15 @@ export class SearchSERPTelemetryParent extends JSWindowActorParent {
|
|||
receiveMessage(msg) {
|
||||
let browser = this.browsingContext.top.embedderElement;
|
||||
|
||||
if (msg.name == "SearchTelemetry:PageInfo") {
|
||||
lazy.SearchSERPTelemetry.reportPageWithAds(msg.data, browser);
|
||||
switch (msg.name) {
|
||||
case "SearchTelemetry:PageInfo": {
|
||||
lazy.SearchSERPTelemetry.reportPageWithAds(msg.data, browser);
|
||||
break;
|
||||
}
|
||||
case "SearchTelemetry:AdImpressions": {
|
||||
lazy.SearchSERPTelemetry.reportPageWithAdImpressions(msg.data, browser);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -500,7 +500,7 @@ var gXPInstallObserver = {
|
|||
Services.console.logMessage(consoleMsg);
|
||||
},
|
||||
|
||||
observe(aSubject, aTopic, aData) {
|
||||
async observe(aSubject, aTopic, aData) {
|
||||
var brandBundle = document.getElementById("bundle_brand");
|
||||
var installInfo = aSubject.wrappedJSObject;
|
||||
var browser = installInfo.browser;
|
||||
|
@ -622,6 +622,7 @@ var gXPInstallObserver = {
|
|||
break;
|
||||
}
|
||||
case "addon-install-blocked": {
|
||||
await window.ensureCustomElements("moz-support-link");
|
||||
// Dismiss the progress notification. Note that this is bad if
|
||||
// there are multiple simultaneous installs happening, see
|
||||
// bug 1329884 for a longer explanation.
|
||||
|
@ -694,13 +695,7 @@ var gXPInstallObserver = {
|
|||
? "site-permission-addons"
|
||||
: "unlisted-extensions-risks";
|
||||
let learnMore = doc.getElementById("addon-install-blocked-info");
|
||||
learnMore.textContent = gNavigatorBundle.getString(
|
||||
"xpinstallPromptMessage.learnMore"
|
||||
);
|
||||
learnMore.setAttribute(
|
||||
"href",
|
||||
Services.urlFormatter.formatURLPref("app.support.baseURL") + article
|
||||
);
|
||||
learnMore.setAttribute("support-page", article);
|
||||
};
|
||||
|
||||
let secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
|
||||
|
@ -788,6 +783,7 @@ var gXPInstallObserver = {
|
|||
// from product about how to approach this for extensions.
|
||||
declineActions.push(neverAllowAndReportAction);
|
||||
}
|
||||
|
||||
let popup = PopupNotifications.show(
|
||||
browser,
|
||||
notificationID,
|
||||
|
|
|
@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
||||
PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
||||
Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
|
||||
ScreenshotsUtils: "resource:///modules/ScreenshotsUtils.sys.mjs",
|
||||
SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
|
||||
|
@ -52,6 +53,7 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
TabsSetupFlowManager:
|
||||
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs",
|
||||
TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
|
||||
TranslationsParent: "resource://gre/actors/TranslationsParent.sys.mjs",
|
||||
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
||||
UrlbarInput: "resource:///modules/UrlbarInput.sys.mjs",
|
||||
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
|
||||
|
@ -88,8 +90,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
|||
PanelView: "resource:///modules/PanelMultiView.jsm",
|
||||
Pocket: "chrome://pocket/content/Pocket.jsm",
|
||||
ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
|
||||
// TODO (Bug 1529552): Remove once old urlbar code goes away.
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.jsm",
|
||||
SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm",
|
||||
SaveToPocket: "chrome://pocket/content/SaveToPocket.jsm",
|
||||
SiteDataManager: "resource:///modules/SiteDataManager.jsm",
|
||||
|
@ -5364,6 +5364,7 @@ var XULBrowserWindow = {
|
|||
CFRPageActions.updatePageActions(gBrowser.selectedBrowser);
|
||||
|
||||
AboutReaderParent.updateReaderButton(gBrowser.selectedBrowser);
|
||||
TranslationsParent.updateButtonFromLocationChange(gBrowser.selectedBrowser);
|
||||
|
||||
PictureInPicture.updateUrlbarToggle(gBrowser.selectedBrowser);
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@
|
|||
<link rel="localization" href="toolkit/printing/printUI.ftl"/>
|
||||
<link rel="localization" href="browser/tabbrowser.ftl"/>
|
||||
<link rel="localization" href="preview/firefoxSuggest.ftl"/>
|
||||
<link rel="localization" href="locales-preview/translations.ftl"/>
|
||||
<link rel="localization" href="browser/toolbarContextMenu.ftl"/>
|
||||
<link rel="localization" href="browser/screenshots.ftl"/>
|
||||
<link rel="localization" href="browser/firefoxView.ftl"/>
|
||||
|
|
|
@ -374,6 +374,14 @@
|
|||
<image id="picture-in-picture-button-icon"
|
||||
class="urlbar-icon"/>
|
||||
</hbox>
|
||||
<hbox id="translations-button"
|
||||
class="urlbar-page-action"
|
||||
role="button"
|
||||
data-l10n-id="urlbar-translations-button"
|
||||
hidden="true"
|
||||
onclick="TranslationsParent.urlBarButtonClick(event);">
|
||||
<image class="urlbar-icon"/>
|
||||
</hbox>
|
||||
<toolbarbutton id="urlbar-zoom-button"
|
||||
onclick="FullZoom.reset(); FullZoom.resetScalingZoom();"
|
||||
tooltip="dynamic-shortcut-tooltip"
|
||||
|
|
|
@ -128,7 +128,12 @@
|
|||
<popupnotificationcontent id="addon-install-blocked-content" orient="vertical">
|
||||
<description id="addon-install-blocked-message" class="popup-notification-description"></description>
|
||||
<hbox>
|
||||
<label id="addon-install-blocked-info" class="popup-notification-learnmore-link" is="text-link"/>
|
||||
<html:a
|
||||
is="moz-support-link"
|
||||
id="addon-install-blocked-info"
|
||||
class="popup-notification-learnmore-link"
|
||||
data-l10n-id="popup-notification-xpinstall-prompt-learn-more"
|
||||
/>
|
||||
</hbox>
|
||||
</popupnotificationcontent>
|
||||
</popupnotification>
|
||||
|
|
|
@ -2619,7 +2619,7 @@
|
|||
userContextId,
|
||||
csp,
|
||||
skipLoad = createLazyBrowser,
|
||||
batchInsertingTabs,
|
||||
insertTab = true,
|
||||
globalHistoryOptions,
|
||||
triggeringRemoteType,
|
||||
} = {}
|
||||
|
@ -2701,8 +2701,7 @@
|
|||
noInitialLabel,
|
||||
skipBackgroundNotify,
|
||||
});
|
||||
if (!batchInsertingTabs) {
|
||||
// When we are not restoring a session, we need to know
|
||||
if (insertTab) {
|
||||
// insert the tab into the tab container in the correct position
|
||||
this._insertTabAtIndex(t, {
|
||||
index,
|
||||
|
@ -2745,10 +2744,11 @@
|
|||
);
|
||||
b.registeredOpenURI = lazyBrowserURI;
|
||||
}
|
||||
// If we're batch inserting, we can't set the tab state meaningfully
|
||||
// because the tab won't be in the DOM yet. The consumer (normally
|
||||
// session restore) will have to do this work itself.
|
||||
if (!batchInsertingTabs) {
|
||||
// If we're not inserting the tab into the DOM, we can't set the tab
|
||||
// state meaningfully. Session restore (the only caller who does this)
|
||||
// will have to do this work itself later, when the tabs have been
|
||||
// inserted.
|
||||
if (insertTab) {
|
||||
SessionStore.setTabState(t, {
|
||||
entries: [
|
||||
{
|
||||
|
@ -2788,7 +2788,7 @@
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!batchInsertingTabs) {
|
||||
if (insertTab) {
|
||||
// Fire a TabOpen event
|
||||
this._fireTabOpen(t, eventDetail);
|
||||
|
||||
|
@ -3119,7 +3119,7 @@
|
|||
}
|
||||
},
|
||||
|
||||
addMultipleTabs(restoreTabsLazily, selectTab, aPropertiesTabs) {
|
||||
createTabsForSessionRestore(restoreTabsLazily, selectTab, tabDataList) {
|
||||
let tabs = [];
|
||||
let tabsFragment = document.createDocumentFragment();
|
||||
let tabToSelect = null;
|
||||
|
@ -3130,8 +3130,8 @@
|
|||
// into a document fragment so that we can insert them all
|
||||
// together. This prevents synch reflow for each tab
|
||||
// insertion.
|
||||
for (var i = 0; i < aPropertiesTabs.length; i++) {
|
||||
let tabData = aPropertiesTabs[i];
|
||||
for (var i = 0; i < tabDataList.length; i++) {
|
||||
let tabData = tabDataList[i];
|
||||
|
||||
let userContextId = tabData.userContextId;
|
||||
let select = i == selectTab - 1;
|
||||
|
@ -3191,7 +3191,7 @@
|
|||
userContextId,
|
||||
skipBackgroundNotify: true,
|
||||
bulkOrderedOpen: true,
|
||||
batchInsertingTabs: true,
|
||||
insertTab: false,
|
||||
skipLoad: true,
|
||||
preferredRemoteType,
|
||||
});
|
||||
|
@ -6635,6 +6635,28 @@
|
|||
// created or re-created browser, e.g. because it just switched
|
||||
// remoteness or is a new tab/window).
|
||||
this.mBrowser.urlbarChangeTracker.startedLoad();
|
||||
|
||||
// To improve the user experience and perceived performance when
|
||||
// opening links in new tabs, we show the url and tab title sooner,
|
||||
// but only if it's safe (from a phishing point of view) to do so,
|
||||
// thus there's no session history and the load starts from a
|
||||
// non-web-controlled blank page.
|
||||
if (
|
||||
this.mBrowser.browsingContext.sessionHistory?.count === 0 &&
|
||||
BrowserUIUtils.checkEmptyPageOrigin(
|
||||
this.mBrowser,
|
||||
originalLocation
|
||||
)
|
||||
) {
|
||||
gBrowser.setInitialTabTitle(this.mTab, originalLocation.spec, {
|
||||
isURL: true,
|
||||
});
|
||||
|
||||
this.mBrowser._initialURI = originalLocation;
|
||||
if (this.mTab.selected && !gBrowser.userTypedValue) {
|
||||
gURLBar.setURI();
|
||||
}
|
||||
}
|
||||
}
|
||||
delete this.mBrowser.initialPageLoadedFromUserAction;
|
||||
// If the browser is loading it must not be crashed anymore
|
||||
|
|
|
@ -35,7 +35,7 @@ const known_scripts = {
|
|||
"resource:///actors/LinkHandlerChild.sys.mjs",
|
||||
"resource:///actors/SearchSERPTelemetryChild.sys.mjs",
|
||||
"resource://gre/actors/ContentMetaChild.sys.mjs",
|
||||
"resource://gre/modules/Readerable.jsm",
|
||||
"resource://gre/modules/Readerable.sys.mjs",
|
||||
|
||||
// Telemetry
|
||||
"resource://gre/modules/TelemetryControllerBase.sys.mjs", // bug 1470339
|
||||
|
|
|
@ -40,13 +40,6 @@ const knownUnshownImages = [
|
|||
platforms: ["linux", "win", "macosx"],
|
||||
},
|
||||
|
||||
{
|
||||
// bug 1817360 fixes this.
|
||||
file: "resource://gre-resources/broken-image.png",
|
||||
platforms: ["linux", "win", "macosx"],
|
||||
intermittentNotLoaded: ["linux", "win", "macosx"],
|
||||
},
|
||||
|
||||
{
|
||||
file: "chrome://global/skin/icons/chevron.svg",
|
||||
platforms: ["win", "linux", "macosx"],
|
||||
|
|
|
@ -48,6 +48,42 @@ skip-if = debug # Bug 1444565, Bug 1457887
|
|||
support-files = tab_that_closes.html
|
||||
[browser_hiddentab_contextmenu.js]
|
||||
[browser_lazy_tab_browser_events.js]
|
||||
[browser_link_in_tab_title_and_url_prefilled_blank_page.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_link_in_tab_title_and_url_prefilled_new_window.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_link_in_tab_title_and_url_prefilled_normal_page_blank_target.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_link_in_tab_title_and_url_prefilled_normal_page_by_script.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_link_in_tab_title_and_url_prefilled_normal_page_no_target.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_link_in_tab_title_and_url_prefilled_normal_page_other_target.js]
|
||||
support-files =
|
||||
common_link_in_tab_title_and_url_prefilled.js
|
||||
link_in_tab_title_and_url_prefilled.html
|
||||
request-timeout.sjs
|
||||
wait-a-bit.sjs
|
||||
[browser_long_data_url_label_truncation.js]
|
||||
[browser_multiselect_tabs_active_tab_selected_by_default.js]
|
||||
[browser_multiselect_tabs_bookmark.js]
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar when opening about:blank by clicking link.
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function blank_target__foreground() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: "",
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: "",
|
||||
history: [BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function blank_target__background() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function other_target__foreground() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
history: [BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function other_target__background() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function by_script() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--by-script",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
history: [BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function no_target() {
|
||||
await doTestInSameWindow({
|
||||
link: "blank-page--no-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
// Inherit the title and URL until finishing loading a new link when the
|
||||
// link is opened in same tab.
|
||||
tab: HOME_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
history: [HOME_URL, BLANK_URL],
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar on new window opened by clicking
|
||||
// link.
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function normal_page__blank_target() {
|
||||
await doTestWithNewWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
expectedSetURICalled: true,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target() {
|
||||
await doTestWithNewWindow({
|
||||
link: "wait-a-bit--other-target",
|
||||
expectedSetURICalled: false,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__by_script() {
|
||||
await doTestWithNewWindow({
|
||||
link: "wait-a-bit--by-script",
|
||||
expectedSetURICalled: false,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function blank_page__blank_target() {
|
||||
await doTestWithNewWindow({
|
||||
link: "blank-page--blank-target",
|
||||
expectedSetURICalled: false,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function blank_page__other_target() {
|
||||
await doTestWithNewWindow({
|
||||
link: "blank-page--other-target",
|
||||
expectedSetURICalled: false,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function blank_page__by_script() {
|
||||
await doTestWithNewWindow({
|
||||
link: "blank-page--by-script",
|
||||
expectedSetURICalled: false,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,180 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar when opening normal web page by
|
||||
// clicking link that the target is "_blank".
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function normal_page__foreground__click() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__foreground__contextmenu() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CONTEXT_MENU,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__foreground__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__foreground__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: REQUEST_TIMEOUT_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: REQUEST_TIMEOUT_URL,
|
||||
history: [REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__background__click() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__background__contextmenu() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CONTEXT_MENU,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__background__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__background__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--blank-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar when opening normal web page by
|
||||
// clicking link that opens by script.
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function normal_page__by_script() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--by-script",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__by_script__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--by-script",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
history: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__by_script__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--by-script",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: REQUEST_TIMEOUT_URL,
|
||||
history: [REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar when opening normal web page by
|
||||
// clicking link that has no target.
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function normal_page__no_target() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--no-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
// Inherit the title and URL until finishing loading a new link when the
|
||||
// link is opened in same tab.
|
||||
tab: HOME_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [HOME_URL, WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__no_target__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--no-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: HOME_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: HOME_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [HOME_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__no_target__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--no-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: HOME_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: REQUEST_TIMEOUT_URL,
|
||||
history: [HOME_URL, REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,138 @@
|
|||
/* 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/. */
|
||||
|
||||
// Test the behavior of the tab and the urlbar when opening normal web page by
|
||||
// clicking link that the target is "other".
|
||||
|
||||
/* import-globals-from common_link_in_tab_title_and_url_prefilled.js */
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochitests/content/browser/browser/base/content/test/tabs/common_link_in_tab_title_and_url_prefilled.js",
|
||||
this
|
||||
);
|
||||
|
||||
add_task(async function normal_page__other_target__foreground() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: WAIT_A_BIT_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target__foreground__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
history: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target__foreground__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.FOREGROUND,
|
||||
loadingState: {
|
||||
tab: BLANK_TITLE,
|
||||
urlbar: BLANK_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: REQUEST_TIMEOUT_URL,
|
||||
history: [REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target__background() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_PAGE_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [WAIT_A_BIT_URL],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target__background__abort() {
|
||||
await doTestInSameWindow({
|
||||
link: "wait-a-bit--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Abort loading");
|
||||
document.getElementById("stop-button").click();
|
||||
},
|
||||
finalState: {
|
||||
tab: WAIT_A_BIT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function normal_page__other_target__background__timeout() {
|
||||
await doTestInSameWindow({
|
||||
link: "request-timeout--other-target",
|
||||
openBy: OPEN_BY.CLICK,
|
||||
openAs: OPEN_AS.BACKGROUND,
|
||||
loadingState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
},
|
||||
async actionWhileLoading(onTabLoaded) {
|
||||
info("Wait until loading the link target");
|
||||
await onTabLoaded;
|
||||
},
|
||||
finalState: {
|
||||
tab: REQUEST_TIMEOUT_LOADING_TITLE,
|
||||
urlbar: HOME_URL,
|
||||
history: [REQUEST_TIMEOUT_URL],
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,195 @@
|
|||
/* 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/. */
|
||||
|
||||
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
|
||||
|
||||
const TEST_ROOT = getRootDirectory(gTestPath).replace(
|
||||
"chrome://mochitests/content",
|
||||
"https://example.com"
|
||||
);
|
||||
const HOME_URL = `${TEST_ROOT}link_in_tab_title_and_url_prefilled.html`;
|
||||
const HOME_TITLE = HOME_URL.substring("https://".length);
|
||||
const WAIT_A_BIT_URL = `${TEST_ROOT}wait-a-bit.sjs`;
|
||||
const WAIT_A_BIT_LOADING_TITLE = WAIT_A_BIT_URL.substring("https://".length);
|
||||
const WAIT_A_BIT_PAGE_TITLE = "wait a bit";
|
||||
const REQUEST_TIMEOUT_URL = `${TEST_ROOT}request-timeout.sjs`;
|
||||
const REQUEST_TIMEOUT_LOADING_TITLE = REQUEST_TIMEOUT_URL.substring(
|
||||
"https://".length
|
||||
);
|
||||
const BLANK_URL = "about:blank";
|
||||
const BLANK_TITLE = "New Tab";
|
||||
|
||||
const OPEN_BY = {
|
||||
CLICK: "click",
|
||||
CONTEXT_MENU: "context_menu",
|
||||
};
|
||||
|
||||
const OPEN_AS = {
|
||||
FOREGROUND: "foreground",
|
||||
BACKGROUND: "background",
|
||||
};
|
||||
|
||||
async function doTestInSameWindow({
|
||||
link,
|
||||
openBy,
|
||||
openAs,
|
||||
loadingState,
|
||||
actionWhileLoading,
|
||||
finalState,
|
||||
}) {
|
||||
await BrowserTestUtils.withNewTab("about:blank", async browser => {
|
||||
// NOTE: The behavior after the click <a href="about:blank">link</a>
|
||||
// (no target) is different when the URL is opened directly with
|
||||
// BrowserTestUtils.withNewTab() and when it is loaded later.
|
||||
// Specifically, if we load `about:blank`, expect to see `New Tab` as the
|
||||
// title of the tab, but the former will continue to display the URL that
|
||||
// was previously displayed. Therefore, use the latter way.
|
||||
BrowserTestUtils.loadURIString(browser, HOME_URL);
|
||||
await BrowserTestUtils.browserLoaded(
|
||||
gBrowser.selectedBrowser,
|
||||
false,
|
||||
HOME_URL
|
||||
);
|
||||
|
||||
const onLoadStarted = new Promise(resolve =>
|
||||
gBrowser.addTabsProgressListener({
|
||||
onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
|
||||
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
|
||||
gBrowser.removeTabsProgressListener(this);
|
||||
resolve(gBrowser.getTabForBrowser(aBrowser));
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
info(`Open link for ${link} by ${openBy} as ${openAs}`);
|
||||
const href = await openLink(browser, link, openBy, openAs);
|
||||
|
||||
info("Wait until starting to load in the target tab");
|
||||
const target = await onLoadStarted;
|
||||
Assert.equal(target.selected, openAs === OPEN_AS.FOREGROUND);
|
||||
Assert.equal(gURLBar.value, loadingState.urlbar);
|
||||
Assert.equal(target.textLabel.textContent, loadingState.tab);
|
||||
|
||||
await actionWhileLoading(
|
||||
BrowserTestUtils.browserLoaded(target.linkedBrowser, false, href)
|
||||
);
|
||||
|
||||
info("Check the final result");
|
||||
Assert.equal(gURLBar.value, finalState.urlbar);
|
||||
Assert.equal(target.textLabel.textContent, finalState.tab);
|
||||
const sessionHistory = await new Promise(r =>
|
||||
SessionStore.getSessionHistory(target, r)
|
||||
);
|
||||
Assert.deepEqual(
|
||||
sessionHistory.entries.map(e => e.url),
|
||||
finalState.history
|
||||
);
|
||||
|
||||
BrowserTestUtils.removeTab(target);
|
||||
});
|
||||
}
|
||||
|
||||
async function doTestWithNewWindow({ link, expectedSetURICalled }) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.link.open_newwindow", 2]],
|
||||
});
|
||||
|
||||
await BrowserTestUtils.withNewTab(HOME_URL, async browser => {
|
||||
const onNewWindowOpened = BrowserTestUtils.domWindowOpenedAndLoaded();
|
||||
|
||||
info(`Open link for ${link}`);
|
||||
const href = await openLink(
|
||||
browser,
|
||||
link,
|
||||
OPEN_BY.CLICK,
|
||||
OPEN_AS.FOREGROUND
|
||||
);
|
||||
|
||||
info("Wait until opening a new window");
|
||||
const win = await onNewWindowOpened;
|
||||
|
||||
info("Check whether gURLBar.setURI is called while loading the page");
|
||||
const sandbox = sinon.createSandbox();
|
||||
registerCleanupFunction(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
let isSetURIWhileLoading = false;
|
||||
sandbox.stub(win.gURLBar, "setURI").callsFake(uri => {
|
||||
if (!uri && win.gBrowser.selectedBrowser._initialURI) {
|
||||
isSetURIWhileLoading = true;
|
||||
}
|
||||
});
|
||||
await BrowserTestUtils.browserLoaded(
|
||||
win.gBrowser.selectedBrowser,
|
||||
false,
|
||||
href
|
||||
);
|
||||
sandbox.restore();
|
||||
|
||||
Assert.equal(isSetURIWhileLoading, expectedSetURICalled);
|
||||
Assert.equal(
|
||||
!!win.gBrowser.selectedBrowser._initialURI,
|
||||
expectedSetURICalled
|
||||
);
|
||||
|
||||
await BrowserTestUtils.closeWindow(win);
|
||||
});
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
}
|
||||
|
||||
async function openLink(browser, link, openBy, openAs) {
|
||||
let href;
|
||||
const openAsBackground = openAs === OPEN_AS.BACKGROUND;
|
||||
if (openBy === OPEN_BY.CLICK) {
|
||||
href = await synthesizeMouse(browser, link, {
|
||||
ctrlKey: openAsBackground,
|
||||
metaKey: openAsBackground,
|
||||
});
|
||||
} else if (openBy === OPEN_BY.CONTEXT_MENU) {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.tabs.loadInBackground", openAsBackground]],
|
||||
});
|
||||
|
||||
const contextMenu = document.getElementById("contentAreaContextMenu");
|
||||
const onPopupShown = BrowserTestUtils.waitForEvent(
|
||||
contextMenu,
|
||||
"popupshown"
|
||||
);
|
||||
|
||||
href = await synthesizeMouse(browser, link, {
|
||||
type: "contextmenu",
|
||||
button: 2,
|
||||
});
|
||||
|
||||
await onPopupShown;
|
||||
|
||||
const openLinkMenuItem = contextMenu.querySelector(
|
||||
"#context-openlinkintab"
|
||||
);
|
||||
contextMenu.activateItem(openLinkMenuItem);
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
} else {
|
||||
throw new Error("Invalid openBy");
|
||||
}
|
||||
|
||||
return href;
|
||||
}
|
||||
|
||||
async function synthesizeMouse(browser, link, event) {
|
||||
return SpecialPowers.spawn(
|
||||
browser,
|
||||
[link, event],
|
||||
(linkInContent, eventInContent) => {
|
||||
const { EventUtils } = ChromeUtils.importESModule(
|
||||
"resource://specialpowers/SpecialPowersEventUtils.sys.mjs"
|
||||
);
|
||||
const target = content.document.getElementById(linkInContent);
|
||||
EventUtils.synthesizeMouseAtCenter(target, eventInContent, content);
|
||||
return target.href;
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<style>
|
||||
a { display: block; }
|
||||
</style>
|
||||
|
||||
<a id="wait-a-bit--blank-target" href="wait-a-bit.sjs" target="_blank">wait-a-bit - _blank target</a>
|
||||
<a id="wait-a-bit--other-target" href="wait-a-bit.sjs" target="other">wait-a-bit - other target</a>
|
||||
<a id="wait-a-bit--by-script">wait-a-bit - script</a>
|
||||
<a id="wait-a-bit--no-target" href="wait-a-bit.sjs">wait-a-bit - no target</a>
|
||||
|
||||
<a id="request-timeout--blank-target" href="request-timeout.sjs" target="_blank">request-timeout - _blank target</a>
|
||||
<a id="request-timeout--other-target" href="request-timeout.sjs" target="other">request-timeout - other target</a>
|
||||
<a id="request-timeout--by-script">request-timeout - script</a>
|
||||
<a id="request-timeout--no-target" href="request-timeout.sjs">request-timeout - no target</a>
|
||||
|
||||
<a id="blank-page--blank-target" href="about:blank" target="_blank">about:blank - _blank target</a>
|
||||
<a id="blank-page--other-target" href="about:blank" target="other">about:blank - other target</a>
|
||||
<a id="blank-page--by-script">blank - script</a>
|
||||
<a id="blank-page--no-target" href="about:blank">about:blank - no target</a>
|
||||
|
||||
<script>
|
||||
document.getElementById("wait-a-bit--by-script").addEventListener("click", () => {
|
||||
window.open("wait-a-bit.sjs", "_blank");
|
||||
})
|
||||
document.getElementById("request-timeout--by-script").addEventListener("click", () => {
|
||||
window.open("request-timeout.sjs", "_blank");
|
||||
})
|
||||
document.getElementById("blank-page--by-script").addEventListener("click", () => {
|
||||
window.open("about:blank", "_blank");
|
||||
})
|
||||
</script>
|
8
browser/base/content/test/tabs/request-timeout.sjs
Normal file
8
browser/base/content/test/tabs/request-timeout.sjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
async function handleRequest(request, response) {
|
||||
response.setStatusLine("1.1", 408, "Request Timeout");
|
||||
}
|
23
browser/base/content/test/tabs/wait-a-bit.sjs
Normal file
23
browser/base/content/test/tabs/wait-a-bit.sjs
Normal file
|
@ -0,0 +1,23 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// eslint-disable-next-line mozilla/no-redeclare-with-import-autofix
|
||||
const { setTimeout } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/Timer.sys.mjs"
|
||||
);
|
||||
|
||||
async function handleRequest(request, response) {
|
||||
response.seizePower();
|
||||
|
||||
await new Promise(r => setTimeout(r, 2000));
|
||||
|
||||
response.write("HTTP/1.1 200 OK\r\n");
|
||||
const body = "<title>wait a bit</title><body>ok</body>";
|
||||
response.write("Content-Type: text/html\r\n");
|
||||
response.write(`Content-Length: ${body.length}\r\n`);
|
||||
response.write("\r\n");
|
||||
response.write(body);
|
||||
response.finish();
|
||||
}
|
|
@ -124,8 +124,6 @@ function getBrowser(panel) {
|
|||
};
|
||||
|
||||
browser.addEventListener("DidChangeBrowserRemoteness", initBrowser);
|
||||
// Potentially unnecessary: bug 1822037 will evaluate further.
|
||||
browser.browsingContext.isAppTab = true;
|
||||
return readyPromise.then(initBrowser);
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ add_task(async function testReopen() {
|
|||
* reopen container tab in regular tab
|
||||
* Close all the tabs
|
||||
* This tests that behaviour of reopening tabs is correct
|
||||
* */
|
||||
*/
|
||||
|
||||
let regularPage = await openURIInRegularTab("about:blank");
|
||||
var currRemoteType;
|
||||
|
|
|
@ -1031,7 +1031,7 @@ DownloadsViewUI.DownloadElementShell.prototype = {
|
|||
isCommandEnabled(aCommand) {
|
||||
switch (aCommand) {
|
||||
case "downloadsCmd_retry":
|
||||
return this.download.canceled || this.download.error;
|
||||
return this.download.canceled || !!this.download.error;
|
||||
case "downloadsCmd_pauseResume":
|
||||
return this.download.hasPartialData && !this.download.error;
|
||||
case "downloadsCmd_openReferrer":
|
||||
|
|
|
@ -446,10 +446,11 @@ var DownloadsPanel = {
|
|||
// If the last element in the list is selected, or the footer is already
|
||||
// focused, focus the footer.
|
||||
if (
|
||||
richListBox.selectedItem === richListBox.lastElementChild ||
|
||||
document
|
||||
.getElementById("downloadsFooter")
|
||||
.contains(document.activeElement)
|
||||
DownloadsView.canChangeSelectedItem &&
|
||||
(richListBox.selectedItem === richListBox.lastElementChild ||
|
||||
document
|
||||
.getElementById("downloadsFooter")
|
||||
.contains(document.activeElement))
|
||||
) {
|
||||
richListBox.selectedIndex = -1;
|
||||
DownloadsFooter.focus();
|
||||
|
@ -513,7 +514,11 @@ var DownloadsPanel = {
|
|||
return;
|
||||
}
|
||||
|
||||
if (document.activeElement && this.panel.contains(document.activeElement)) {
|
||||
if (
|
||||
document.activeElement &&
|
||||
(this.panel.contains(document.activeElement) ||
|
||||
this.panel.shadowRoot.contains(document.activeElement))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let focusOptions = {};
|
||||
|
@ -521,7 +526,9 @@ var DownloadsPanel = {
|
|||
focusOptions.focusVisible = false;
|
||||
}
|
||||
if (DownloadsView.richListBox.itemCount > 0) {
|
||||
DownloadsView.richListBox.selectedIndex = 0;
|
||||
if (DownloadsView.canChangeSelectedItem) {
|
||||
DownloadsView.richListBox.selectedIndex = 0;
|
||||
}
|
||||
DownloadsView.richListBox.focus(focusOptions);
|
||||
} else {
|
||||
DownloadsFooter.focus(focusOptions);
|
||||
|
@ -960,6 +967,15 @@ var DownloadsView = {
|
|||
return this.contextMenu.state != "closed";
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether it's possible to change the currently selected item.
|
||||
*/
|
||||
get canChangeSelectedItem() {
|
||||
// When the context menu or a subview are open, the selected item should
|
||||
// not change.
|
||||
return !this.contextMenuOpen && !this.subViewOpen;
|
||||
},
|
||||
|
||||
/**
|
||||
* Mouse listeners to handle selection on hover.
|
||||
*/
|
||||
|
@ -978,7 +994,7 @@ var DownloadsView = {
|
|||
aEvent.target.closest(".downloadMainArea")
|
||||
);
|
||||
|
||||
if (!this.contextMenuOpen && !this.subViewOpen) {
|
||||
if (this.canChangeSelectedItem) {
|
||||
this.richListBox.selectedItem = item;
|
||||
}
|
||||
},
|
||||
|
@ -995,21 +1011,19 @@ var DownloadsView = {
|
|||
|
||||
// If the destination element is outside of the richlistitem, clear the
|
||||
// selection.
|
||||
if (
|
||||
!this.contextMenuOpen &&
|
||||
!this.subViewOpen &&
|
||||
!item.contains(aEvent.relatedTarget)
|
||||
) {
|
||||
if (this.canChangeSelectedItem && !item.contains(aEvent.relatedTarget)) {
|
||||
this.richListBox.selectedIndex = -1;
|
||||
}
|
||||
},
|
||||
|
||||
onDownloadContextMenu(aEvent) {
|
||||
let element = this.richListBox.selectedItem;
|
||||
let element = aEvent.originalTarget.closest("richlistitem");
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the selected item is the expected one, so commands and the
|
||||
// context menu are updated appropriately.
|
||||
this.richListBox.selectedItem = element;
|
||||
DownloadsViewController.updateCommands();
|
||||
|
||||
DownloadsViewUI.updateContextMenuForElement(this.contextMenu, element);
|
||||
|
|
|
@ -3,51 +3,38 @@ support-files = head.js
|
|||
|
||||
[browser_about_downloads.js]
|
||||
[browser_basic_functionality.js]
|
||||
[browser_confirm_unblock_download.js]
|
||||
[browser_download_is_clickable.js]
|
||||
[browser_download_opens_on_click.js]
|
||||
[browser_download_opens_policy.js]
|
||||
[browser_download_overwrite.js]
|
||||
support-files =
|
||||
foo.txt
|
||||
foo.txt^headers^
|
||||
!/toolkit/content/tests/browser/common/mockTransfer.js
|
||||
[browser_download_starts_in_tmp.js]
|
||||
[browser_first_download_panel.js]
|
||||
skip-if =
|
||||
os == "linux" # Bug 949434
|
||||
[browser_image_mimetype_issues.js]
|
||||
https_first_disabled = true
|
||||
support-files =
|
||||
not-really-a-jpeg.jpeg
|
||||
not-really-a-jpeg.jpeg^headers^
|
||||
blank.JPG
|
||||
[browser_library_select_all.js]
|
||||
[browser_overflow_anchor.js]
|
||||
skip-if = os == "linux" # Bug 952422
|
||||
[browser_confirm_unblock_download.js]
|
||||
[browser_iframe_gone_mid_download.js]
|
||||
[browser_indicatorDrop.js]
|
||||
[browser_libraryDrop.js]
|
||||
skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1306510
|
||||
[browser_library_clearall.js]
|
||||
[browser_download_opens_on_click.js]
|
||||
[browser_download_opens_policy.js]
|
||||
[browser_download_spam_protection.js]
|
||||
skip-if =
|
||||
os == "linux" && bits == 64 # bug 1743263 & Bug 1742678
|
||||
os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
|
||||
support-files = test_spammy_page.html
|
||||
[browser_download_is_clickable.js]
|
||||
[browser_download_starts_in_tmp.js]
|
||||
[browser_downloads_autohide.js]
|
||||
[browser_downloads_context_menu_always_open_similar_files.js]
|
||||
[browser_downloads_context_menu_delete_file.js]
|
||||
[browser_downloads_context_menu_selection.js]
|
||||
[browser_downloads_keynav.js]
|
||||
[browser_downloads_panel_block.js]
|
||||
[browser_downloads_panel_context_menu.js]
|
||||
skip-if =
|
||||
os == "win" && os_version == "10.0" && bits == 64 && !debug # Bug 1719949
|
||||
win10_2004 && bits == 32 && debug # Bug 1727925
|
||||
[browser_downloads_context_menu_always_open_similar_files.js]
|
||||
[browser_downloads_panel_ctrl_click.js]
|
||||
[browser_downloads_panel_disable_items.js]
|
||||
support-files =
|
||||
foo.txt
|
||||
foo.txt^headers^
|
||||
[browser_downloads_panel_dontshow.js]
|
||||
[browser_downloads_panel_focus.js]
|
||||
[browser_downloads_panel_height.js]
|
||||
[browser_downloads_panel_opens.js]
|
||||
skip-if =
|
||||
|
@ -55,10 +42,24 @@ skip-if =
|
|||
support-files =
|
||||
foo.txt
|
||||
foo.txt^headers^
|
||||
[browser_downloads_autohide.js]
|
||||
[browser_go_to_download_page.js]
|
||||
[browser_pdfjs_preview.js]
|
||||
[browser_downloads_pauseResume.js]
|
||||
[browser_downloads_panel_focus.js]
|
||||
[browser_downloads_context_menu_delete_file.js]
|
||||
[browser_first_download_panel.js]
|
||||
skip-if =
|
||||
os == "linux" # Bug 949434
|
||||
[browser_go_to_download_page.js]
|
||||
[browser_iframe_gone_mid_download.js]
|
||||
[browser_image_mimetype_issues.js]
|
||||
https_first_disabled = true
|
||||
support-files =
|
||||
not-really-a-jpeg.jpeg
|
||||
not-really-a-jpeg.jpeg^headers^
|
||||
blank.JPG
|
||||
[browser_indicatorDrop.js]
|
||||
[browser_libraryDrop.js]
|
||||
skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1306510
|
||||
[browser_library_clearall.js]
|
||||
[browser_library_select_all.js]
|
||||
[browser_overflow_anchor.js]
|
||||
skip-if = os == "linux" # Bug 952422
|
||||
[browser_pdfjs_preview.js]
|
||||
[browser_tempfilename.js]
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test that the context menu refers to the triggering item, even if the
|
||||
* selection was not set preemptively.
|
||||
*/
|
||||
|
||||
async function createDownloadFiles() {
|
||||
let dir = await setDownloadDir();
|
||||
let downloads = [];
|
||||
downloads.push({
|
||||
state: DownloadsCommon.DOWNLOAD_FAILED,
|
||||
contentType: "text/plain",
|
||||
target: new FileUtils.File(PathUtils.join(dir, "does-not-exist.txt")),
|
||||
});
|
||||
downloads.push({
|
||||
state: DownloadsCommon.DOWNLOAD_FINISHED,
|
||||
contentType: "text/plain",
|
||||
target: await createDownloadedFile(PathUtils.join(dir, "file.txt"), "file"),
|
||||
});
|
||||
return downloads;
|
||||
}
|
||||
|
||||
add_setup(async function setup() {
|
||||
await PlacesUtils.history.clear();
|
||||
await startServer();
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
await task_resetState();
|
||||
await PlacesUtils.history.clear();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test() {
|
||||
// remove download files, empty out collections
|
||||
let downloadList = await Downloads.getList(Downloads.ALL);
|
||||
let downloadCount = (await downloadList.getAll()).length;
|
||||
Assert.equal(downloadCount, 0, "There should be 0 downloads");
|
||||
await task_resetState();
|
||||
let downloads = await createDownloadFiles();
|
||||
await task_addDownloads(downloads);
|
||||
await task_openPanel();
|
||||
let downloadsListBox = document.getElementById("downloadsListBox");
|
||||
await TestUtils.waitForCondition(() => {
|
||||
downloadsListBox.removeAttribute("disabled");
|
||||
return downloadsListBox.childElementCount == downloads.length;
|
||||
});
|
||||
|
||||
// Note we're not doing anything to set the selectedItem here, exactly to
|
||||
// check the context menu doesn't depend on some selection prerequisite.
|
||||
|
||||
let first = downloadsListBox.querySelector("richlistitem");
|
||||
let second = downloadsListBox.querySelector("richlistitem:nth-child(2)");
|
||||
|
||||
info("Check first item");
|
||||
let firstDownload = DownloadsView.itemForElement(first).download;
|
||||
is(
|
||||
DownloadsCommon.stateOfDownload(firstDownload),
|
||||
DownloadsCommon.DOWNLOAD_FINISHED,
|
||||
"Download states match up"
|
||||
);
|
||||
// mousemove to the _other_ download, to ensure it doesn't confuse code.
|
||||
EventUtils.synthesizeMouse(second, -5, -5, { type: "mousemove" });
|
||||
await checkCommandsWithContextMenu(first, {
|
||||
downloadsCmd_show: true,
|
||||
cmd_delete: true,
|
||||
});
|
||||
|
||||
info("Check second item");
|
||||
let secondDownload = DownloadsView.itemForElement(second).download;
|
||||
is(
|
||||
DownloadsCommon.stateOfDownload(secondDownload),
|
||||
DownloadsCommon.DOWNLOAD_FAILED,
|
||||
"Download states match up"
|
||||
);
|
||||
// mousemove to the _other_ download, to ensure it doesn't confuse code.
|
||||
EventUtils.synthesizeMouse(first, -5, -5, { type: "mousemove" });
|
||||
await checkCommandsWithContextMenu(second, {
|
||||
downloadsCmd_show: false,
|
||||
cmd_delete: true,
|
||||
});
|
||||
|
||||
let hiddenPromise = BrowserTestUtils.waitForEvent(
|
||||
DownloadsPanel.panel,
|
||||
"popuphidden"
|
||||
);
|
||||
DownloadsPanel.hidePanel();
|
||||
await hiddenPromise;
|
||||
});
|
||||
|
||||
async function checkCommandsWithContextMenu(element, commands) {
|
||||
let contextMenu = await openContextMenu(element);
|
||||
for (let command in commands) {
|
||||
let enabled = commands[command];
|
||||
let commandStatus = enabled ? "enabled" : "disabled";
|
||||
info(`Checking command ${command} is ${commandStatus}`);
|
||||
|
||||
let commandElt = contextMenu.querySelector(`[command="${command}"]`);
|
||||
Assert.equal(
|
||||
!BrowserTestUtils.is_hidden(commandElt),
|
||||
enabled,
|
||||
`${command} should be ${enabled ? "visible" : "hidden"}`
|
||||
);
|
||||
|
||||
Assert.strictEqual(
|
||||
DownloadsView.richListBox.selectedItem._shell.isCommandEnabled(command),
|
||||
enabled,
|
||||
`${command} should be ${commandStatus}`
|
||||
);
|
||||
}
|
||||
contextMenu.hidePopup();
|
||||
}
|
|
@ -591,5 +591,10 @@ async function prepareDownloads(downloads, overrideExtension = null) {
|
|||
}
|
||||
ok(props.target instanceof Ci.nsIFile, "download target is a nsIFile");
|
||||
}
|
||||
await task_addDownloads(downloads);
|
||||
// If we'd just insert downloads as defined in the test case, they would
|
||||
// appear reversed in the panel, because they will be in descending insertion
|
||||
// order (newest at the top). The problem is we define an itemIndex based on
|
||||
// the downloads array, and it would be weird to define it based on a
|
||||
// reversed order. Short, we just reverse the array to preserve the order.
|
||||
await task_addDownloads(downloads.reverse());
|
||||
}
|
||||
|
|
|
@ -403,8 +403,6 @@ class BasePopup {
|
|||
};
|
||||
|
||||
browser.addEventListener("DidChangeBrowserRemoteness", initBrowser); // eslint-disable-line mozilla/balanced-listeners
|
||||
// Potentially unnecessary: bug 1822037 will evaluate further.
|
||||
browser.browsingContext.isAppTab = true;
|
||||
|
||||
if (!popupURL) {
|
||||
// For remote browsers, we can't do any setup until the frame loader is
|
||||
|
|
|
@ -199,6 +199,7 @@ skip-if =
|
|||
os == "mac" && !debug # Bug 1775565
|
||||
win11_2009 # Bug 1775565
|
||||
[browser_ext_optionsPage_browser_style.js]
|
||||
[browser_ext_optionsPage_links_open_in_tabs.js]
|
||||
[browser_ext_optionsPage_modals.js]
|
||||
[browser_ext_optionsPage_popups.js]
|
||||
[browser_ext_optionsPage_privileges.js]
|
||||
|
@ -225,6 +226,7 @@ https_first_disabled = true
|
|||
[browser_ext_popup_background.js]
|
||||
[browser_ext_popup_corners.js]
|
||||
[browser_ext_popup_focus.js]
|
||||
[browser_ext_popup_links_open_in_tabs.js]
|
||||
[browser_ext_popup_requestPermission.js]
|
||||
[browser_ext_popup_select.js]
|
||||
skip-if =
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_options_links() {
|
||||
async function backgroundScript() {
|
||||
browser.runtime.openOptionsPage();
|
||||
}
|
||||
|
||||
function optionsScript() {
|
||||
browser.test.sendMessage("options-page:loaded", document.documentURI);
|
||||
}
|
||||
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
useAddonManager: "temporary",
|
||||
|
||||
manifest: {
|
||||
options_ui: {
|
||||
page: "options.html",
|
||||
},
|
||||
},
|
||||
files: {
|
||||
"options.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="options.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body style="height: 100px;">
|
||||
<h1>Extensions Options</h1>
|
||||
<a href="https://example.com/options-page-link">options page link</a>
|
||||
</body>
|
||||
</html>`,
|
||||
"options.js": optionsScript,
|
||||
},
|
||||
background: backgroundScript,
|
||||
});
|
||||
|
||||
const aboutAddonsTab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:addons"
|
||||
);
|
||||
|
||||
await extension.startup();
|
||||
|
||||
await extension.awaitMessage("options-page:loaded");
|
||||
|
||||
const optionsBrowser = getInlineOptionsBrowser(gBrowser.selectedBrowser);
|
||||
|
||||
const promiseNewTabOpened = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
"https://example.com/options-page-link"
|
||||
);
|
||||
await SpecialPowers.spawn(optionsBrowser, [], () =>
|
||||
content.document.querySelector("a").click()
|
||||
);
|
||||
info(
|
||||
"Expect a new tab to be opened when a link is clicked in the options_page embedded inside about:addons"
|
||||
);
|
||||
const newTab = await promiseNewTabOpened;
|
||||
ok(newTab, "Got a new tab created on the expected url");
|
||||
BrowserTestUtils.removeTab(newTab);
|
||||
|
||||
BrowserTestUtils.removeTab(aboutAddonsTab);
|
||||
|
||||
await extension.unload();
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_popup_links_open_tabs() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
browser_action: {
|
||||
default_popup: "popup.html",
|
||||
},
|
||||
},
|
||||
|
||||
files: {
|
||||
"popup.html": `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script src="popup.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body style="height: 100px;">
|
||||
<h1>Extension Popup</h1>
|
||||
<a href="https://example.com/popup-page-link">popup page link</a>
|
||||
</body>
|
||||
</html>`,
|
||||
"popup.js": function() {
|
||||
window.onload = () => {
|
||||
browser.test.sendMessage("from-popup", "popup-a");
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await extension.startup();
|
||||
|
||||
let widget = getBrowserActionWidget(extension);
|
||||
CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_NAVBAR, 0);
|
||||
|
||||
let promiseActionPopupBrowser = awaitExtensionPanel(extension);
|
||||
clickBrowserAction(extension);
|
||||
await extension.awaitMessage("from-popup");
|
||||
let popupBrowser = await promiseActionPopupBrowser;
|
||||
const promiseNewTabOpened = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
"https://example.com/popup-page-link"
|
||||
);
|
||||
await SpecialPowers.spawn(popupBrowser, [], () =>
|
||||
content.document.querySelector("a").click()
|
||||
);
|
||||
const newTab = await promiseNewTabOpened;
|
||||
ok(newTab, "Got a new tab created on the expected url");
|
||||
BrowserTestUtils.removeTab(newTab);
|
||||
|
||||
await closeBrowserAction(extension);
|
||||
await extension.unload();
|
||||
});
|
|
@ -169,7 +169,7 @@ export const TabsSetupFlowManager = new (class {
|
|||
this._currentSetupStateName = "not-signed-in";
|
||||
this._shouldShowSuccessConfirmation = false;
|
||||
this._didShowMobilePromo = false;
|
||||
this._waitingForTabs = false;
|
||||
this.abortWaitingForTabs();
|
||||
|
||||
this.syncHasWorked = false;
|
||||
|
||||
|
@ -178,6 +178,8 @@ export const TabsSetupFlowManager = new (class {
|
|||
mobileDeviceConnected: this.mobileDeviceConnected,
|
||||
secondaryDeviceConnected: this.secondaryDeviceConnected,
|
||||
};
|
||||
// keep track of tab-pickup-container instance visibilities
|
||||
this._viewVisibilityStates = new Map();
|
||||
}
|
||||
|
||||
get isPrimaryPasswordLocked() {
|
||||
|
@ -215,6 +217,14 @@ export const TabsSetupFlowManager = new (class {
|
|||
Services.obs.removeObserver(this, FXA_DEVICE_CONNECTED);
|
||||
Services.obs.removeObserver(this, FXA_DEVICE_DISCONNECTED);
|
||||
}
|
||||
get hasVisibleViews() {
|
||||
return Array.from(this._viewVisibilityStates.values()).reduce(
|
||||
(hasVisible, visibility) => {
|
||||
return hasVisible || visibility == "visible";
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
get currentSetupState() {
|
||||
return this.setupState.get(this._currentSetupStateName);
|
||||
}
|
||||
|
@ -296,12 +306,17 @@ export const TabsSetupFlowManager = new (class {
|
|||
break;
|
||||
case TOPIC_DEVICELIST_UPDATED:
|
||||
this.logger.debug("Handling observer notification:", topic, data);
|
||||
if (await this.refreshDevices()) {
|
||||
this.logger.debug(
|
||||
"refreshDevices made changes, calling maybeUpdateUI"
|
||||
);
|
||||
const { deviceStateChanged, deviceAdded } = await this.refreshDevices();
|
||||
if (deviceStateChanged) {
|
||||
this.maybeUpdateUI(true);
|
||||
}
|
||||
if (deviceAdded && this.secondaryDeviceConnected) {
|
||||
this.logger.debug("device was added");
|
||||
this._deviceAddedResultsNeverSeen = true;
|
||||
if (this.hasVisibleViews) {
|
||||
this.startWaitingForNewDeviceTabs();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FXA_DEVICE_CONNECTED:
|
||||
case FXA_DEVICE_DISCONNECTED:
|
||||
|
@ -311,19 +326,21 @@ export const TabsSetupFlowManager = new (class {
|
|||
case SYNC_SERVICE_ERROR:
|
||||
this.logger.debug(`Handling ${SYNC_SERVICE_ERROR}`);
|
||||
if (lazy.UIState.get().status == lazy.UIState.STATUS_SIGNED_IN) {
|
||||
this._waitingForTabs = false;
|
||||
this.abortWaitingForTabs();
|
||||
this.syncIsWorking = false;
|
||||
this.maybeUpdateUI(true);
|
||||
}
|
||||
break;
|
||||
case NETWORK_STATUS_CHANGED:
|
||||
this.networkIsOnline = data == "online";
|
||||
this._waitingForTabs = false;
|
||||
this.abortWaitingForTabs();
|
||||
this.maybeUpdateUI(true);
|
||||
break;
|
||||
case SYNC_SERVICE_FINISHED:
|
||||
this.logger.debug(`Handling ${SYNC_SERVICE_FINISHED}`);
|
||||
this._waitingForTabs = false;
|
||||
// We intentionally leave any empty-tabs timestamp
|
||||
// as we may be still waiting for a sync that delivers some tabs
|
||||
this._waitingForNextTabSync = false;
|
||||
if (!this.syncIsWorking) {
|
||||
this.syncIsWorking = true;
|
||||
this.syncHasWorked = true;
|
||||
|
@ -340,25 +357,67 @@ export const TabsSetupFlowManager = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
updateViewVisibility(instanceId, visibility) {
|
||||
const wasVisible = this.hasVisibleViews;
|
||||
this.logger.debug(
|
||||
`updateViewVisibility for instance: ${instanceId}, visibility: ${visibility}`
|
||||
);
|
||||
if (visibility == "unloaded") {
|
||||
this._viewVisibilityStates.delete(instanceId);
|
||||
} else {
|
||||
this._viewVisibilityStates.set(instanceId, visibility);
|
||||
}
|
||||
const isVisible = this.hasVisibleViews;
|
||||
if (isVisible && !wasVisible) {
|
||||
// If we're already timing waiting for tabs from a newly-added device
|
||||
// we might be able to stop
|
||||
if (this._noTabsVisibleFromAddedDeviceTimestamp) {
|
||||
return this.stopWaitingForNewDeviceTabs();
|
||||
}
|
||||
if (this._deviceAddedResultsNeverSeen) {
|
||||
// If this is the first time a view has been visible since a device was added
|
||||
// we may want to start the empty-tabs visible timer
|
||||
return this.startWaitingForNewDeviceTabs();
|
||||
}
|
||||
}
|
||||
if (!isVisible) {
|
||||
this.logger.debug(
|
||||
"Resetting timestamp and tabs pending flags as there are no visible views"
|
||||
);
|
||||
// if there's no view visible, we're not really waiting anymore
|
||||
this.abortWaitingForTabs();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get waitingForTabs() {
|
||||
return (
|
||||
// signed in & at least 1 other device is sycning indicates there's something to wait for
|
||||
this.secondaryDeviceConnected &&
|
||||
// last recent tabs request came back empty and we've not had a sync finish (or error) yet
|
||||
this._waitingForTabs
|
||||
// signed in & at least 1 other device is syncing indicates there's something to wait for
|
||||
this.secondaryDeviceConnected && this._waitingForNextTabSync
|
||||
);
|
||||
}
|
||||
|
||||
abortWaitingForTabs() {
|
||||
this._waitingForNextTabSync = false;
|
||||
// also clear out the device-added / tabs pending flags
|
||||
this._noTabsVisibleFromAddedDeviceTimestamp = 0;
|
||||
this._deviceAddedResultsNeverSeen = false;
|
||||
}
|
||||
|
||||
startWaitingForTabs() {
|
||||
if (!this._waitingForTabs) {
|
||||
this._waitingForTabs = true;
|
||||
if (!this._waitingForNextTabSync) {
|
||||
this._waitingForNextTabSync = true;
|
||||
Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
stopWaitingForTabs() {
|
||||
if (this._waitingForTabs) {
|
||||
this._waitingForTabs = false;
|
||||
async stopWaitingForTabs() {
|
||||
const wasWaiting = this.waitingForTabs;
|
||||
if (this.hasVisibleViews && this._deviceAddedResultsNeverSeen) {
|
||||
await this.stopWaitingForNewDeviceTabs();
|
||||
}
|
||||
this._waitingForNextTabSync = false;
|
||||
if (wasWaiting) {
|
||||
Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
|
||||
}
|
||||
}
|
||||
|
@ -369,7 +428,7 @@ export const TabsSetupFlowManager = new (class {
|
|||
this.maybeUpdateUI(true);
|
||||
if (!this.fxaSignedIn) {
|
||||
// As we just signed out, ensure the waiting flag is reset for next time around
|
||||
this._waitingForTabs = false;
|
||||
this.abortWaitingForTabs();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -388,7 +447,8 @@ export const TabsSetupFlowManager = new (class {
|
|||
|
||||
// When SyncedTabs has resolved the getRecentTabs promise,
|
||||
// we also know we can update devices-related internal state
|
||||
if (await this.refreshDevices()) {
|
||||
const { deviceStateChanged } = await this.refreshDevices();
|
||||
if (deviceStateChanged) {
|
||||
this.logger.debug(
|
||||
"onSignedInChange, after refreshDevices, calling maybeUpdateUI"
|
||||
);
|
||||
|
@ -424,6 +484,53 @@ export const TabsSetupFlowManager = new (class {
|
|||
}
|
||||
}
|
||||
|
||||
async startWaitingForNewDeviceTabs() {
|
||||
// if we're already waiting for tabs, don't reset
|
||||
if (this._noTabsVisibleFromAddedDeviceTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// take a timestamp whenever the latest device is added and we have 0 tabs to show,
|
||||
// allowing us to track how long we show an empty list after a new device is added
|
||||
const hasRecentTabs = (await lazy.SyncedTabs.getRecentTabs(1)).length;
|
||||
if (this.hasVisibleViews && !hasRecentTabs) {
|
||||
this._noTabsVisibleFromAddedDeviceTimestamp = Date.now();
|
||||
this.logger.debug(
|
||||
"New device added with 0 synced tabs to show, storing timestamp:",
|
||||
this._noTabsVisibleFromAddedDeviceTimestamp
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async stopWaitingForNewDeviceTabs() {
|
||||
if (!this._noTabsVisibleFromAddedDeviceTimestamp) {
|
||||
return;
|
||||
}
|
||||
const recentTabs = await lazy.SyncedTabs.getRecentTabs(1);
|
||||
if (recentTabs.length) {
|
||||
// We have been waiting for > 0 tabs after a newly-added device, record
|
||||
// the time elapsed
|
||||
const elapsed = Date.now() - this._noTabsVisibleFromAddedDeviceTimestamp;
|
||||
this.logger.debug(
|
||||
"stopWaitingForTabs, resetting _noTabsVisibleFromAddedDeviceTimestamp and recording telemetry:",
|
||||
Math.round(elapsed / 1000)
|
||||
);
|
||||
this._noTabsVisibleFromAddedDeviceTimestamp = 0;
|
||||
this._deviceAddedResultsNeverSeen = false;
|
||||
Services.telemetry.recordEvent(
|
||||
"firefoxview",
|
||||
"synced_tabs_empty",
|
||||
"since_device_added",
|
||||
Math.round(elapsed / 1000).toString()
|
||||
);
|
||||
} else {
|
||||
// we are still waiting for some tabs to show...
|
||||
this.logger.debug(
|
||||
"stopWaitingForTabs: Still no recent tabs, we are still waiting"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshDevices() {
|
||||
// If current device not found in recent device list, refresh device list
|
||||
if (
|
||||
|
@ -437,13 +544,15 @@ export const TabsSetupFlowManager = new (class {
|
|||
// compare new values to the previous values
|
||||
const mobileDeviceConnected = this.mobileDeviceConnected;
|
||||
const secondaryDeviceConnected = this.secondaryDeviceConnected;
|
||||
const oldDevicesCount = this._deviceStateSnapshot?.devicesCount ?? 0;
|
||||
const devicesCount = lazy.fxAccounts.device?.recentDeviceList?.length ?? 0;
|
||||
|
||||
this.logger.debug(
|
||||
`refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
|
||||
`secondaryDeviceConnected: ${secondaryDeviceConnected}`
|
||||
);
|
||||
|
||||
let didDeviceStateChange =
|
||||
let deviceStateChanged =
|
||||
this._deviceStateSnapshot.mobileDeviceConnected !=
|
||||
mobileDeviceConnected ||
|
||||
this._deviceStateSnapshot.secondaryDeviceConnected !=
|
||||
|
@ -465,8 +574,9 @@ export const TabsSetupFlowManager = new (class {
|
|||
this._deviceStateSnapshot = {
|
||||
mobileDeviceConnected,
|
||||
secondaryDeviceConnected,
|
||||
devicesCount,
|
||||
};
|
||||
if (didDeviceStateChange) {
|
||||
if (deviceStateChanged) {
|
||||
this.logger.debug("refreshDevices: device state did change");
|
||||
if (!secondaryDeviceConnected) {
|
||||
this.logger.debug(
|
||||
|
@ -477,7 +587,10 @@ export const TabsSetupFlowManager = new (class {
|
|||
} else {
|
||||
this.logger.debug("refreshDevices: no device state change");
|
||||
}
|
||||
return didDeviceStateChange;
|
||||
return {
|
||||
deviceStateChanged,
|
||||
deviceAdded: oldDevicesCount < devicesCount,
|
||||
};
|
||||
}
|
||||
|
||||
maybeUpdateUI(forceUpdate = false) {
|
||||
|
|
|
@ -20,6 +20,7 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
this._currentSetupStateIndex = -1;
|
||||
this.errorState = null;
|
||||
this.tabListAdded = null;
|
||||
this._id = Math.floor(Math.random() * 10e6);
|
||||
}
|
||||
get setupContainerElem() {
|
||||
return this.querySelector(".sync-setup-container");
|
||||
|
@ -41,7 +42,7 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
connectedCallback() {
|
||||
this.addEventListener("click", this);
|
||||
this.addEventListener("toggle", this);
|
||||
this.addEventListener("visibilitychange", this);
|
||||
this.ownerDocument.addEventListener("visibilitychange", this);
|
||||
Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
|
||||
|
||||
for (let elem of this.querySelectorAll("a[data-support-url]")) {
|
||||
|
@ -54,6 +55,7 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
// when its safe to assume the custom-element's methods will be available
|
||||
this.tabListAdded = this.promiseChildAdded();
|
||||
this.update();
|
||||
this.onVisibilityChange();
|
||||
}
|
||||
|
||||
promiseChildAdded() {
|
||||
|
@ -73,6 +75,8 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
}
|
||||
|
||||
cleanup() {
|
||||
TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
|
||||
this.ownerDocument?.removeEventListener("visibilitychange", this);
|
||||
Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
|
||||
}
|
||||
|
||||
|
@ -83,6 +87,7 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
handleEvent(event) {
|
||||
if (event.type == "toggle") {
|
||||
onToggleContainer(this);
|
||||
this.onVisibilityChange();
|
||||
return;
|
||||
}
|
||||
if (event.type == "click" && event.target.dataset.action) {
|
||||
|
@ -130,11 +135,21 @@ class TabPickupContainer extends HTMLDetailsElement {
|
|||
}
|
||||
}
|
||||
// Returning to fxview seems like a likely time for a device check
|
||||
if (
|
||||
event.type == "visibilitychange" &&
|
||||
document.visibilityState === "visible"
|
||||
) {
|
||||
if (event.type == "visibilitychange") {
|
||||
this.onVisibilityChange();
|
||||
}
|
||||
}
|
||||
onVisibilityChange() {
|
||||
const isVisible = document.visibilityState == "visible";
|
||||
const isOpen = this.open;
|
||||
if (isVisible && isOpen) {
|
||||
this.update();
|
||||
TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
|
||||
} else {
|
||||
TabsSetupFlowManager.updateViewVisibility(
|
||||
this._id,
|
||||
isVisible ? "closed" : "hidden"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -108,10 +108,15 @@ async function withFirefoxView(
|
|||
return result;
|
||||
}
|
||||
|
||||
function isFirefoxViewTabSelectedInWindow(win) {
|
||||
return win.gBrowser.selectedBrowser.currentURI.spec == "about:firefoxview";
|
||||
}
|
||||
|
||||
export {
|
||||
withFirefoxView,
|
||||
assertFirefoxViewTab,
|
||||
assertFirefoxViewTabSelected,
|
||||
openFirefoxViewTab,
|
||||
closeFirefoxViewTab,
|
||||
isFirefoxViewTabSelectedInWindow,
|
||||
};
|
||||
|
|
|
@ -27,5 +27,7 @@ skip-if = true # Bug 1783684
|
|||
[browser_sync_admin_disabled.js]
|
||||
[browser_tab_close_last_tab.js]
|
||||
[browser_tab_on_close_warning.js]
|
||||
[browser_tab_pickup_device_added_telemetry.js]
|
||||
[browser_tab_pickup_list.js]
|
||||
[browser_tab_pickup_visibility.js]
|
||||
[browser_ui_state.js]
|
||||
|
|
|
@ -26,10 +26,10 @@ add_task(async function feature_callout_is_accessible() {
|
|||
await waitForCalloutScreen(document, "FEATURE_CALLOUT_1");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => document.activeElement.id === calloutId,
|
||||
"Feature Callout is focused on page load"
|
||||
() => document.activeElement.value === "primary_button",
|
||||
`Feature Callout primary button is focused on page load}`
|
||||
);
|
||||
ok(true, "Feature Callout was focused on page load");
|
||||
ok(true, "Feature Callout primary button was focused on page load");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() =>
|
||||
|
@ -46,10 +46,10 @@ add_task(async function feature_callout_is_accessible() {
|
|||
|
||||
ok(true, "FEATURE_CALLOUT_2 was successfully displayed");
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => document.activeElement.id === calloutId,
|
||||
"Feature Callout is focused after advancing screens"
|
||||
() => document.activeElement.value == "primary_button",
|
||||
"Feature Callout primary button is focused after advancing screens"
|
||||
);
|
||||
ok(true, "Feature Callout was successfully focused");
|
||||
ok(true, "Feature Callout primary button was successfully focused");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -32,9 +32,9 @@ add_task(async function test_primary_password_locked() {
|
|||
const sandbox = setupMocks();
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
sandbox
|
||||
.stub(TabsSetupFlowManager, "syncTabs")
|
||||
.returns(Promise.resolve(null));
|
||||
sandbox.stub(TabsSetupFlowManager, "syncTabs").resolves(null);
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
syncedTabsMock.resolves(getMockTabData(syncedTabsData1));
|
||||
|
||||
const { document } = browser.contentWindow;
|
||||
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
|
||||
|
|
|
@ -132,7 +132,7 @@ add_task(async function test_tab_sync_loading() {
|
|||
|
||||
add_task(async function test_tab_no_sync() {
|
||||
// Ensure we take down the waiting message if SyncedTabs determines it doesnt need to sync
|
||||
const recentTabsData = [];
|
||||
const recentTabsData = structuredClone(syncedTabsData1[0].tabs);
|
||||
const sandbox = setupMocks(recentTabsData);
|
||||
// stub syncTabs so it resolves to false - meaning it will not trigger a sync, which is the case
|
||||
// we want to cover in this test.
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
await clearAllParentTelemetryEvents();
|
||||
cleanup_tab_pickup();
|
||||
});
|
||||
|
||||
function setupWithFxaDevices() {
|
||||
const sandbox = (gSandbox = setupSyncFxAMocks({
|
||||
state: UIState.STATUS_SIGNED_IN,
|
||||
fxaDevices: [
|
||||
{
|
||||
id: 1,
|
||||
name: "My desktop",
|
||||
isCurrentDevice: true,
|
||||
type: "desktop",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Other device",
|
||||
isCurrentDevice: false,
|
||||
type: "mobile",
|
||||
},
|
||||
],
|
||||
}));
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
const mockDesktopTab1 = {
|
||||
client: "6c12bonqXZh8",
|
||||
device: "My desktop",
|
||||
deviceType: "desktop",
|
||||
type: "tab",
|
||||
title: "Example2",
|
||||
url: "https://example.com",
|
||||
icon: "https://example/favicon.png",
|
||||
lastUsed: Math.floor((Date.now() - 1000 * 60) / 1000), // This is one minute from now, which is below the threshold for 'Just now'
|
||||
};
|
||||
|
||||
const mockDesktopTab2 = {
|
||||
client: "6c12bonqXZh8",
|
||||
device: "My desktop",
|
||||
deviceType: "desktop",
|
||||
type: "tab",
|
||||
title: "Sandboxes - Sinon.JS",
|
||||
url: "https://sinonjs.org/releases/latest/sandbox/",
|
||||
icon: "https://sinonjs.org/assets/images/favicon.png",
|
||||
lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000
|
||||
};
|
||||
|
||||
const mockMobileTab1 = {
|
||||
client: "9d0y686hBXel",
|
||||
device: "My phone",
|
||||
deviceType: "mobile",
|
||||
type: "tab",
|
||||
title: "Element",
|
||||
url: "https://chat.mozilla.org/#room:mozilla.org",
|
||||
icon: "https://chat.mozilla.org/vector-icons/favicon.ico",
|
||||
lastUsed: 1664571288,
|
||||
};
|
||||
|
||||
const NO_TABS_EVENTS = [
|
||||
["firefoxview", "entered", "firefoxview", undefined],
|
||||
["firefoxview", "synced_tabs", "tabs", undefined, { count: "0" }],
|
||||
];
|
||||
const SINGLE_TAB_EVENTS = [
|
||||
["firefoxview", "entered", "firefoxview", undefined],
|
||||
["firefoxview", "synced_tabs", "tabs", undefined, { count: "1" }],
|
||||
];
|
||||
const DEVICE_ADDED_NO_TABS_EVENTS = [
|
||||
["firefoxview", "synced_tabs", "tabs", undefined, undefined],
|
||||
["firefoxview", "synced_tabs_empty", "since_device_added", undefined],
|
||||
];
|
||||
const DEVICE_ADDED_TABS_EVENTS = [
|
||||
["firefoxview", "synced_tabs", "tabs", undefined, undefined],
|
||||
];
|
||||
|
||||
async function whenResolved(functionSpy, functionLabel) {
|
||||
info(`Waiting for ${functionLabel} to be called`);
|
||||
await TestUtils.waitForCondition(
|
||||
() => functionSpy.called,
|
||||
`Waiting for ${functionLabel} to be called`
|
||||
);
|
||||
is(
|
||||
functionSpy.getCall(0).returnValue.constructor.name,
|
||||
"Promise",
|
||||
`${functionLabel} returned a promise`
|
||||
);
|
||||
info(`Waiting for the promise returned by ${functionLabel} to be resolved`);
|
||||
await functionSpy.getCall(0).returnValue;
|
||||
info(`${functionLabel} promise resolved`);
|
||||
}
|
||||
|
||||
async function test_device_added({
|
||||
initialRecentTabsResult,
|
||||
expectedInitialTelementryEvents,
|
||||
expectedDeviceAddedTelementryEvents,
|
||||
}) {
|
||||
const recentTabsResult = initialRecentTabsResult;
|
||||
await clearAllParentTelemetryEvents();
|
||||
const sandbox = setupWithFxaDevices();
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${recentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(recentTabsResult);
|
||||
});
|
||||
|
||||
ok(
|
||||
!isFirefoxViewTabSelected(),
|
||||
"Before we call withFirefoxView, about:firefoxview tab is not selected"
|
||||
);
|
||||
ok(
|
||||
!TabsSetupFlowManager.hasVisibleViews,
|
||||
"Initially hasVisibleViews is false"
|
||||
);
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
info("inside withFirefoxView taskFn, waiting for setupListState");
|
||||
const { document } = browser.contentWindow;
|
||||
const stopWaitingSpy = sandbox.spy(
|
||||
TabsSetupFlowManager,
|
||||
"stopWaitingForTabs"
|
||||
);
|
||||
const signedInChangeSpy = sandbox.spy(
|
||||
TabsSetupFlowManager,
|
||||
"onSignedInChange"
|
||||
);
|
||||
|
||||
await setupListState(browser);
|
||||
info("setupListState finished");
|
||||
|
||||
// ensure any tab syncs triggered by Fxa sign-in are complete before proceeding
|
||||
await whenResolved(signedInChangeSpy, "onSignedInChange");
|
||||
if (!recentTabsResult.length) {
|
||||
info("No synced tabs so we wait for the result of the sync we trigger");
|
||||
await whenResolved(stopWaitingSpy, "stopWaitingForTabs");
|
||||
info("stopWaitingForTabs finished");
|
||||
}
|
||||
|
||||
const isTablistVisible = !!initialRecentTabsResult.length;
|
||||
testVisibility(browser, {
|
||||
expectedVisible: {
|
||||
"ol.synced-tabs-list": isTablistVisible,
|
||||
"#synced-tabs-placeholder": !isTablistVisible,
|
||||
},
|
||||
});
|
||||
const syncedTabsItems = document.querySelectorAll(
|
||||
"ol.synced-tabs-list > li:not(.synced-tab-li-placeholder)"
|
||||
);
|
||||
info(
|
||||
"list items: " +
|
||||
Array.from(syncedTabsItems)
|
||||
.map(li => `li.${li.className}`)
|
||||
.join(", ")
|
||||
);
|
||||
is(
|
||||
syncedTabsItems.length,
|
||||
initialRecentTabsResult.length,
|
||||
`synced-tabs-list should have initial count of ${initialRecentTabsResult.length} non-placeholder list items`
|
||||
);
|
||||
|
||||
// confirm telemetry is in expected state?
|
||||
info(
|
||||
"Checking telemetry against expectedInitialTelementryEvents: " +
|
||||
JSON.stringify(expectedInitialTelementryEvents, null, 2)
|
||||
);
|
||||
TelemetryTestUtils.assertEvents(
|
||||
expectedInitialTelementryEvents,
|
||||
{ category: "firefoxview" },
|
||||
{ clear: true, process: "parent" }
|
||||
);
|
||||
|
||||
// add a new mock device
|
||||
info("Adding a new mock fxa dedvice");
|
||||
gMockFxaDevices.push({
|
||||
id: 1,
|
||||
name: "My primary phone",
|
||||
isCurrentDevice: false,
|
||||
type: "mobile",
|
||||
});
|
||||
|
||||
const startWaitingSpy = sandbox.spy(
|
||||
TabsSetupFlowManager,
|
||||
"startWaitingForNewDeviceTabs"
|
||||
);
|
||||
// Notify of the newly added device
|
||||
info("Notifying devicelist_updated with the new mobile device");
|
||||
Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
|
||||
|
||||
// Some time passes here waiting for sync to get data from that device
|
||||
// we expect new-device handling to kick in. If there are 0 tabs we'll signal we're waiting,
|
||||
// create a timestamp and only clear it when there are > 0 tabs.
|
||||
// If there are already > 0 tabs, we'll basically do nothing, showing any new tabs when they arrive
|
||||
await whenResolved(startWaitingSpy, "startWaitingForNewDeviceTabs");
|
||||
|
||||
info(
|
||||
"Initial tabs count: " +
|
||||
recentTabsResult.length +
|
||||
", assert on _noTabsVisibleFromAddedDeviceTimestamp: " +
|
||||
TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp
|
||||
);
|
||||
if (recentTabsResult.length) {
|
||||
ok(
|
||||
!TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp,
|
||||
"Should not be waiting if there were > 0 tabs initially"
|
||||
);
|
||||
} else {
|
||||
ok(
|
||||
TabsSetupFlowManager._noTabsVisibleFromAddedDeviceTimestamp,
|
||||
"Should be waiting if there were 0 tabs initially"
|
||||
);
|
||||
}
|
||||
|
||||
// Add tab data from this new device and notify of the changed data
|
||||
recentTabsResult.push(mockMobileTab1);
|
||||
stopWaitingSpy.resetHistory();
|
||||
|
||||
info("Notifying tabs.changed with the new mobile device's tabs");
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
|
||||
// handling the tab.change and clearing the timestamp is necessarily async
|
||||
// as counting synced tabs via getRecentTabs() is async.
|
||||
// There may not be any outcome depending on the tab state, so we just wait
|
||||
// for stopWaitingForTabs to get called and its promise to resolve
|
||||
info("Waiting for the stopWaitingSpy to be called");
|
||||
await whenResolved(stopWaitingSpy, "stopWaitingForTabs");
|
||||
await TestUtils.waitForTick(); // allow time for the telemetry event to get recorded
|
||||
|
||||
info(
|
||||
"We've added a synced tab and updated the tab list, got snapshotEvents:" +
|
||||
JSON.stringify(
|
||||
Services.telemetry.snapshotEvents(
|
||||
Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
|
||||
false
|
||||
),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
// confirm no telemetry was recorded for tabs from the newly-added device
|
||||
// as the tab list was never empty
|
||||
info(
|
||||
"Checking telemetry against expectedDeviceAddedTelementryEvents: " +
|
||||
JSON.stringify(expectedDeviceAddedTelementryEvents, null, 2)
|
||||
);
|
||||
TelemetryTestUtils.assertEvents(
|
||||
expectedDeviceAddedTelementryEvents,
|
||||
{ category: "firefoxview" },
|
||||
{ clear: true, process: "parent" }
|
||||
);
|
||||
});
|
||||
sandbox.restore();
|
||||
cleanup_tab_pickup();
|
||||
}
|
||||
|
||||
add_task(async function test_device_added_with_existing_tabs() {
|
||||
/* Confirm that no telemetry is recorded when a new device is added while the synced tabs list has tabs */
|
||||
await test_device_added({
|
||||
initialRecentTabsResult: [mockDesktopTab1],
|
||||
expectedInitialTelementryEvents: SINGLE_TAB_EVENTS,
|
||||
expectedDeviceAddedTelementryEvents: DEVICE_ADDED_TABS_EVENTS,
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_device_added_with_empty_list() {
|
||||
/* Confirm that telemetry is recorded when a device is added and the synced tabs list
|
||||
is empty until its tabs get synced
|
||||
*/
|
||||
await test_device_added({
|
||||
initialRecentTabsResult: [],
|
||||
expectedInitialTelementryEvents: NO_TABS_EVENTS,
|
||||
expectedDeviceAddedTelementryEvents: DEVICE_ADDED_NO_TABS_EVENTS,
|
||||
});
|
||||
});
|
|
@ -152,11 +152,12 @@ add_task(async function test_tab_list_ordering() {
|
|||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData1);
|
||||
let mockTabs2 = getMockTabData(syncedTabsData2);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -189,7 +190,7 @@ add_task(async function test_tab_list_ordering() {
|
|||
"Last list item in synced-tabs-list is in the correct order"
|
||||
);
|
||||
|
||||
syncedTabsMock.returns(mockTabs2);
|
||||
getRecentTabsResult = mockTabs2;
|
||||
// Initiate a synced tabs update
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
|
||||
|
@ -230,11 +231,12 @@ add_task(async function test_empty_list_items() {
|
|||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData3);
|
||||
let mockTabs2 = getMockTabData(syncedTabsData4);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -274,7 +276,7 @@ add_task(async function test_empty_list_items() {
|
|||
"Last list item in synced-tabs-list should be a placeholder"
|
||||
);
|
||||
|
||||
syncedTabsMock.returns(mockTabs2);
|
||||
getRecentTabsResult = mockTabs2;
|
||||
// Initiate a synced tabs update
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
|
||||
|
@ -312,11 +314,12 @@ add_task(async function test_empty_list() {
|
|||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData([]);
|
||||
let mockTabs2 = getMockTabData(syncedTabsData4);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -357,12 +360,7 @@ add_task(async function test_empty_list() {
|
|||
{ clear: true, process: "parent" }
|
||||
);
|
||||
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs2.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs2);
|
||||
});
|
||||
getRecentTabsResult = mockTabs2;
|
||||
// Initiate a synced tabs update
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
|
||||
|
@ -394,11 +392,12 @@ add_task(async function test_time_updates_correctly() {
|
|||
const sandbox = setupRecentDeviceListMocks();
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData5);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -504,11 +503,12 @@ add_task(async function test_tabs_sync_on_user_page_reload() {
|
|||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData1);
|
||||
let expectedTabsAfterReload = getMockTabData(syncedTabsData3);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -525,9 +525,12 @@ add_task(async function test_tabs_sync_on_user_page_reload() {
|
|||
ok(true, "Firefox View has been reloaded");
|
||||
ok(TabsSetupFlowManager.waitingForTabs, "waitingForTabs is true");
|
||||
|
||||
syncedTabsMock.returns(expectedTabsAfterReload);
|
||||
let waitedForTabs = TestUtils.waitForCondition(() => {
|
||||
return !TabsSetupFlowManager.waitingForTabs;
|
||||
});
|
||||
|
||||
getRecentTabsResult = expectedTabsAfterReload;
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
ok(!TabsSetupFlowManager.waitingForTabs, "waitingForTabs is false");
|
||||
|
||||
const syncedTabsList = document.querySelector("ol.synced-tabs-list");
|
||||
// The tab pickup list has been updated
|
||||
|
@ -537,6 +540,7 @@ add_task(async function test_tabs_sync_on_user_page_reload() {
|
|||
() =>
|
||||
syncedTabsList.firstChild.textContent.includes("Sandboxes - Sinon.JS")
|
||||
);
|
||||
await waitedForTabs;
|
||||
|
||||
sandbox.restore();
|
||||
cleanup_tab_pickup();
|
||||
|
@ -549,11 +553,12 @@ add_task(async function test_keyboard_navigation() {
|
|||
const sandbox = setupRecentDeviceListMocks();
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData1);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -660,11 +665,12 @@ add_task(async function test_duplicate_tab_filter() {
|
|||
const sandbox = setupRecentDeviceListMocks();
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs6 = getMockTabData(syncedTabsData6);
|
||||
let getRecentTabsResult = mockTabs6;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs6.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs6);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -715,11 +721,12 @@ add_task(async function test_tabs_dont_update_unnecessarily() {
|
|||
const sandbox = setupRecentDeviceListMocks();
|
||||
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
|
||||
let mockTabs1 = getMockTabData(syncedTabsData1);
|
||||
let getRecentTabsResult = mockTabs1;
|
||||
syncedTabsMock.callsFake(() => {
|
||||
info(
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${mockTabs1.length} tabs\n`
|
||||
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
|
||||
);
|
||||
return Promise.resolve(mockTabs1);
|
||||
return Promise.resolve(getRecentTabsResult);
|
||||
});
|
||||
|
||||
await withFirefoxView({}, async browser => {
|
||||
|
@ -772,13 +779,15 @@ add_task(async function test_tabs_dont_update_unnecessarily() {
|
|||
|
||||
observer.observe(syncedTabsList, { childList: true, subtree: true });
|
||||
|
||||
syncedTabsMock.returns(mockTabs1);
|
||||
getRecentTabsResult = mockTabs1;
|
||||
const tabPickupList = document.querySelector("tab-pickup-list");
|
||||
const updateTabsListSpy = sandbox.spy(tabPickupList, "updateTabsList");
|
||||
|
||||
// Initiate a synced tabs update
|
||||
Services.obs.notifyObservers(null, "services.sync.tabs.changed");
|
||||
|
||||
await TestUtils.waitForCondition(() => {
|
||||
return !TabsSetupFlowManager.waitingForTabs;
|
||||
});
|
||||
await TestUtils.waitForCondition(() => updateTabsListSpy.called);
|
||||
Assert.ok(!wasMutated, "The synced tabs list was not mutated");
|
||||
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
registerCleanupFunction(async function() {
|
||||
Services.prefs.clearUserPref(TAB_PICKUP_STATE_PREF);
|
||||
});
|
||||
|
||||
async function setup({ open } = {}) {
|
||||
TabsSetupFlowManager.resetInternalState();
|
||||
// sanity check initial values
|
||||
ok(
|
||||
!TabsSetupFlowManager.hasVisibleViews,
|
||||
"Initially hasVisibleViews is false"
|
||||
);
|
||||
is(
|
||||
TabsSetupFlowManager._viewVisibilityStates.size,
|
||||
0,
|
||||
"Initially, there are no visible views"
|
||||
);
|
||||
ok(
|
||||
!isFirefoxViewTabSelected(),
|
||||
"During setup, the about:firefoxview tab is not selected"
|
||||
);
|
||||
|
||||
if (typeof open == "undefined") {
|
||||
Services.prefs.clearUserPref(TAB_PICKUP_STATE_PREF);
|
||||
} else {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [[TAB_PICKUP_STATE_PREF, open]],
|
||||
});
|
||||
}
|
||||
const sandbox = sinon.createSandbox();
|
||||
sandbox.stub(TabsSetupFlowManager, "isTabSyncSetupComplete").get(() => true);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
add_task(async function test_tab_pickup_visibility() {
|
||||
/* Confirm the correct number of tab-pickup views are registered as visible */
|
||||
const sandbox = await setup();
|
||||
|
||||
await withFirefoxView({ win: window }, async function(browser) {
|
||||
const { document } = browser.contentWindow;
|
||||
let tabPickupContainer = document.querySelector("#tab-pickup-container");
|
||||
|
||||
ok(tabPickupContainer.open, "Tab Pickup container should be open");
|
||||
ok(isFirefoxViewTabSelected(), "The firefox view tab is selected");
|
||||
ok(TabsSetupFlowManager.hasVisibleViews, "hasVisibleViews");
|
||||
is(TabsSetupFlowManager._viewVisibilityStates.size, 1, "One view");
|
||||
|
||||
info("Opening and switching to different tab to background fx-view");
|
||||
let newTab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:mozilla"
|
||||
);
|
||||
ok(!isFirefoxViewTabSelected(), "The firefox view tab is not selected");
|
||||
ok(
|
||||
!TabsSetupFlowManager.hasVisibleViews,
|
||||
"no view visible when fx-view is not active"
|
||||
);
|
||||
let newWin = await BrowserTestUtils.openNewBrowserWindow();
|
||||
await openFirefoxViewTab(newWin);
|
||||
|
||||
ok(
|
||||
isFirefoxViewTabSelected(newWin),
|
||||
"The firefox view tab in the new window is selected"
|
||||
);
|
||||
ok(
|
||||
TabsSetupFlowManager.hasVisibleViews,
|
||||
"view registered as visible when fx-view is opened in a new window"
|
||||
);
|
||||
is(TabsSetupFlowManager._viewVisibilityStates.size, 2, "2 tracked views");
|
||||
|
||||
await BrowserTestUtils.closeWindow(newWin);
|
||||
|
||||
ok(
|
||||
!isFirefoxViewTabSelected(),
|
||||
"The firefox view tab in the original window is not selected"
|
||||
);
|
||||
ok(
|
||||
!TabsSetupFlowManager.hasVisibleViews,
|
||||
"no visible views when fx-view is not the active tab in the remaining window"
|
||||
);
|
||||
is(
|
||||
TabsSetupFlowManager._viewVisibilityStates.size,
|
||||
1,
|
||||
"Back to one tracked view"
|
||||
);
|
||||
|
||||
// Switch back to FxView:
|
||||
await BrowserTestUtils.switchTab(
|
||||
gBrowser,
|
||||
gBrowser.getTabForBrowser(browser)
|
||||
);
|
||||
|
||||
ok(
|
||||
isFirefoxViewTabSelected(),
|
||||
"The firefox view tab in the original window is now selected"
|
||||
);
|
||||
ok(
|
||||
TabsSetupFlowManager.hasVisibleViews,
|
||||
"View visibility updated when we switch tab"
|
||||
);
|
||||
BrowserTestUtils.removeTab(newTab);
|
||||
});
|
||||
sandbox.restore();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
ok(
|
||||
!TabsSetupFlowManager.hasVisibleViews,
|
||||
"View visibility updated after withFirefoxView"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_instance_closed() {
|
||||
/* Confirm tab-pickup views are correctly accounted for when toggled closed */
|
||||
const sandbox = await setup({ open: false });
|
||||
await withFirefoxView({ win: window }, async function(browser) {
|
||||
const { document } = browser.contentWindow;
|
||||
info(
|
||||
"tab-pickup.open pref: " +
|
||||
Services.prefs.getBoolPref(
|
||||
"browser.tabs.firefox-view.ui-state.tab-pickup.open"
|
||||
)
|
||||
);
|
||||
info(
|
||||
"isTabSyncSetupComplete: " + TabsSetupFlowManager.isTabSyncSetupComplete
|
||||
);
|
||||
let tabPickupContainer = document.querySelector("#tab-pickup-container");
|
||||
ok(!tabPickupContainer.open, "Tab Pickup container should be closed");
|
||||
info(
|
||||
"_viewVisibilityStates" +
|
||||
JSON.stringify(
|
||||
Array.from(TabsSetupFlowManager._viewVisibilityStates.values()),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
ok(!TabsSetupFlowManager.hasVisibleViews, "no visible views");
|
||||
is(
|
||||
TabsSetupFlowManager._viewVisibilityStates.size,
|
||||
1,
|
||||
"One registered view"
|
||||
);
|
||||
|
||||
tabPickupContainer.open = true;
|
||||
await TestUtils.waitForTick();
|
||||
ok(TabsSetupFlowManager.hasVisibleViews, "view visible");
|
||||
});
|
||||
sandbox.restore();
|
||||
});
|
|
@ -7,6 +7,7 @@ const {
|
|||
assertFirefoxViewTabSelected,
|
||||
openFirefoxViewTab,
|
||||
closeFirefoxViewTab,
|
||||
isFirefoxViewTabSelectedInWindow,
|
||||
} = ChromeUtils.importESModule(
|
||||
"resource://testing-common/FirefoxViewTestUtils.sys.mjs"
|
||||
);
|
||||
|
@ -527,3 +528,18 @@ function cleanup_tab_pickup() {
|
|||
Services.prefs.clearUserPref("services.sync.lastTabFetch");
|
||||
Services.prefs.clearUserPref(TAB_PICKUP_STATE_PREF);
|
||||
}
|
||||
|
||||
function isFirefoxViewTabSelected(win = window) {
|
||||
return isFirefoxViewTabSelectedInWindow(win);
|
||||
}
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
is(
|
||||
typeof SyncedTabs._internal?._createRecentTabsList,
|
||||
"function",
|
||||
"in firefoxview/head.js, SyncedTabs._internal._createRecentTabsList is a function"
|
||||
);
|
||||
// ensure all the stubs are restored, regardless of any exceptions
|
||||
// that might have prevented it
|
||||
gSandbox?.restore();
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
|
||||
extends: ["plugin:mozilla/require-jsdoc"],
|
||||
rules: {
|
||||
"block-scoped-var": "error",
|
||||
complexity: ["error", { max: 22 }],
|
||||
|
|
|
@ -291,11 +291,10 @@ export class MigrationWizardParent extends JSWindowActorParent {
|
|||
);
|
||||
}
|
||||
case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY: {
|
||||
let quantity = MigrationUtils.getImportedCount("history");
|
||||
return lazy.gFluentStrings.formatValue(
|
||||
"migration-wizard-progress-success-history",
|
||||
{
|
||||
quantity,
|
||||
maxAgeInDays: MigrationUtils.HISTORY_MAX_AGE_IN_DAYS,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -308,6 +307,11 @@ export class MigrationWizardParent extends JSWindowActorParent {
|
|||
}
|
||||
);
|
||||
}
|
||||
case lazy.MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA: {
|
||||
return lazy.gFluentStrings.formatValue(
|
||||
"migration-wizard-progress-success-formdata"
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ const RESOURCE_TYPES_WITH_QUANTITIES = [
|
|||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.BOOKMARKS,
|
||||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY,
|
||||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.PASSWORDS,
|
||||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -209,11 +210,36 @@ function assertQuantitiesShown(wizard, expectedResourceTypes) {
|
|||
progressGroup.dataset.resourceType
|
||||
)
|
||||
) {
|
||||
Assert.notEqual(
|
||||
successText.indexOf(EXPECTED_QUANTITY),
|
||||
-1,
|
||||
`Found expected quantity in success string: ${successText}`
|
||||
);
|
||||
if (
|
||||
progressGroup.dataset.resourceType ==
|
||||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.HISTORY
|
||||
) {
|
||||
// HISTORY is a special case that doesn't show the number of imported
|
||||
// history entries, but instead shows the maximum number of days of history
|
||||
// that might have been imported.
|
||||
Assert.notEqual(
|
||||
successText.indexOf(MigrationUtils.HISTORY_MAX_AGE_IN_DAYS),
|
||||
-1,
|
||||
`Found expected maximum number of days of history: ${successText}`
|
||||
);
|
||||
} else if (
|
||||
progressGroup.dataset.resourceType ==
|
||||
MigrationWizardConstants.DISPLAYED_RESOURCE_TYPES.FORMDATA
|
||||
) {
|
||||
// FORMDATA is another special case, because we simply show "Form history" as
|
||||
// the success string, rather than a particular quantity.
|
||||
Assert.equal(
|
||||
successText,
|
||||
"Form history",
|
||||
`Found expected form data string: ${successText}`
|
||||
);
|
||||
} else {
|
||||
Assert.notEqual(
|
||||
successText.indexOf(EXPECTED_QUANTITY),
|
||||
-1,
|
||||
`Found expected quantity in success string: ${successText}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If you've found yourself here, and this is failing, it's probably because you've
|
||||
// updated MigrationWizardParent.#getStringForImportQuantity to return a string for
|
||||
|
|
|
@ -987,7 +987,8 @@ class ProtonScreen extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
|||
this.mainContentHeader = input;
|
||||
}
|
||||
}, isCenterPosition ? null : this.renderSecondarySection(content), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "section-main"
|
||||
className: "section-main",
|
||||
role: "document"
|
||||
}, content.secondary_button_top ? /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MultiStageAboutWelcome__WEBPACK_IMPORTED_MODULE_6__.SecondaryCTA, {
|
||||
content: content,
|
||||
handleAction: this.props.handleAction,
|
||||
|
|
|
@ -382,7 +382,7 @@ export class ProtonScreen extends React.PureComponent {
|
|||
}}
|
||||
>
|
||||
{isCenterPosition ? null : this.renderSecondarySection(content)}
|
||||
<div className="section-main">
|
||||
<div className="section-main" role="document">
|
||||
{content.secondary_button_top ? (
|
||||
<SecondaryCTA
|
||||
content={content}
|
||||
|
|
|
@ -48,12 +48,15 @@
|
|||
inset-inline-end: 0;
|
||||
z-index: 1001;
|
||||
padding: 16px;
|
||||
transition: transform 250ms $customize-menu-slide-bezier, visibility 250ms;
|
||||
overflow: auto;
|
||||
transform: translateX(435px);
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
transition: transform 250ms $customize-menu-slide-bezier, visibility 250ms;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
border-inline-start: solid 1px;
|
||||
}
|
||||
|
|
|
@ -1652,12 +1652,16 @@ main.has-snippet {
|
|||
inset-inline-end: 0;
|
||||
z-index: 1001;
|
||||
padding: 16px;
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
overflow: auto;
|
||||
transform: translateX(435px);
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.customize-menu {
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.customize-menu {
|
||||
border-inline-start: solid 1px;
|
||||
|
|
|
@ -1656,12 +1656,16 @@ main.has-snippet {
|
|||
inset-inline-end: 0;
|
||||
z-index: 1001;
|
||||
padding: 16px;
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
overflow: auto;
|
||||
transform: translateX(435px);
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.customize-menu {
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.customize-menu {
|
||||
border-inline-start: solid 1px;
|
||||
|
|
|
@ -1652,12 +1652,16 @@ main.has-snippet {
|
|||
inset-inline-end: 0;
|
||||
z-index: 1001;
|
||||
padding: 16px;
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
overflow: auto;
|
||||
transform: translateX(435px);
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.customize-menu {
|
||||
transition: transform 250ms cubic-bezier(0.46, 0.03, 0.52, 0.96), visibility 250ms;
|
||||
}
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.customize-menu {
|
||||
border-inline-start: solid 1px;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
|
||||
extends: ["plugin:mozilla/require-jsdoc"],
|
||||
|
||||
rules: {
|
||||
"mozilla/var-only-at-top-level": "error",
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
|
||||
extends: ["plugin:mozilla/require-jsdoc"],
|
||||
};
|
||||
|
|
|
@ -46,12 +46,8 @@
|
|||
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
||||
});
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"ReaderMode",
|
||||
"resource://gre/modules/ReaderMode.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"pktApi",
|
||||
|
|
|
@ -414,7 +414,7 @@
|
|||
<hbox>
|
||||
<description>
|
||||
<html:span id="cookieBannerReductionExplanation" class="tail-with-learn-more" data-l10n-id="cookie-banner-handling-description" ></html:span>
|
||||
<label id="cookieBannerHandlingLearnMore" class="learnMore" is="text-link" data-l10n-id="cookie-banner-learn-more"></label>
|
||||
<html:a is="moz-support-link" id="cookieBannerHandlingLearnMore" class="learnMore" data-l10n-id="cookie-banner-learn-more"/>
|
||||
</description>
|
||||
</hbox>
|
||||
<hbox>
|
||||
|
|
|
@ -234,9 +234,10 @@
|
|||
</vbox>
|
||||
</hbox>
|
||||
<vbox align="start">
|
||||
<label id="connect-another-device"
|
||||
<html:a id="connect-another-device"
|
||||
is="text-link"
|
||||
class="fxaMobilePromo"
|
||||
target="_blank"
|
||||
data-l10n-id="sync-connect-another-device"/>
|
||||
</vbox>
|
||||
</vbox>
|
||||
|
|
|
@ -17,7 +17,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=402788
|
|||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
/** Test for Bug 402788 **/
|
||||
/** Test for Bug 402788 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
// return false if an exception has been catched, true otherwise
|
||||
|
|
|
@ -10,7 +10,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1382545
|
|||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
|
||||
/** Test for Bug 1382545 **/
|
||||
/** Test for Bug 1382545 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
// Used by file_animation_api.html
|
||||
|
|
|
@ -10,7 +10,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1369319
|
|||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
|
||||
/** Test for Bug 1369319 **/
|
||||
/** Test for Bug 1369319 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = () => {
|
||||
SimpleTest.waitForFocus(() => {
|
||||
|
|
|
@ -12,8 +12,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1372069
|
|||
|
||||
const BASE_GEO_URL = "http://mochi.test:8888/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs";
|
||||
|
||||
/** Test for Bug 1372069 **/
|
||||
/** Modified for Bug 1441295 **/
|
||||
/** Test for Bug 1372069 */
|
||||
/** Modified for Bug 1441295 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = () => {
|
||||
SimpleTest.waitForFocus(() => {
|
||||
|
|
|
@ -11,7 +11,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1222285
|
|||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
|
||||
/** Test for Bug 1222285 **/
|
||||
/** Test for Bug 1222285 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
window.onload = () => {
|
||||
|
|
|
@ -15,7 +15,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1363508
|
|||
<div id="target1" style="width: 50px; height: 50px; background: black"></div>
|
||||
<script type="application/javascript">
|
||||
|
||||
/** Test for Bug 1363508 **/
|
||||
/** Test for Bug 1363508 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var target0 = window.document.getElementById("target0");
|
||||
|
|
|
@ -10,7 +10,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1333641
|
|||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<script type="application/javascript">
|
||||
|
||||
/** Test for Bug 1333641 **/
|
||||
/** Test for Bug 1333641 */
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
window.onload = setupSpeechSynthesis;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
|
||||
extends: ["plugin:mozilla/require-jsdoc"],
|
||||
|
||||
rules: {
|
||||
"mozilla/var-only-at-top-level": "error",
|
||||
|
|
|
@ -39,6 +39,10 @@ export var SearchSERPTelemetryUtils = {
|
|||
ACTIONS: {
|
||||
CLICKED: "clicked",
|
||||
},
|
||||
COMPONENTS: {
|
||||
AD_CAROUSEL: "ad_carousel",
|
||||
AD_LINK: "ad_link",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -230,6 +234,10 @@ class TelemetryHandler {
|
|||
this._contentHandler._reportPageWithAds(info, browser);
|
||||
}
|
||||
|
||||
reportPageWithAdImpressions(info, browser) {
|
||||
this._contentHandler._reportPageWithAdImpressions(info, browser);
|
||||
}
|
||||
|
||||
/**
|
||||
* This may start tracking a tab based on the URL. If the URL matches a search
|
||||
* partner, and it has a code, then we'll start tracking it. This will aid
|
||||
|
@ -291,6 +299,7 @@ class TelemetryHandler {
|
|||
if (item) {
|
||||
item.browserTelemetryStateMap.set(browser, {
|
||||
adsReported: false,
|
||||
adImpressionsReported: false,
|
||||
impressionId,
|
||||
});
|
||||
item.count++;
|
||||
|
@ -300,6 +309,7 @@ class TelemetryHandler {
|
|||
item = this._browserInfoByURL.set(url, {
|
||||
browserTelemetryStateMap: new WeakMap().set(browser, {
|
||||
adsReported: false,
|
||||
adImpressionsReported: false,
|
||||
impressionId,
|
||||
}),
|
||||
info,
|
||||
|
@ -911,6 +921,47 @@ class ContentHandler {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs ad impression telemetry for a page with adverts, if it is
|
||||
* one of the partner search provider pages that we're tracking.
|
||||
*
|
||||
* @param {object} info
|
||||
* The search provider information for the page.
|
||||
* @param {string} info.url
|
||||
* The url of the page.
|
||||
* @param {Map<string, object>} info.adImpressions
|
||||
* A map of ad impressions found for the page, where the key
|
||||
* is the type of ad component and the value is an object
|
||||
* containing the number of ads that were loaded, visible,
|
||||
* and hidden.
|
||||
* @param {object} browser
|
||||
* The browser associated with the page.
|
||||
*/
|
||||
_reportPageWithAdImpressions(info, browser) {
|
||||
let item = this._findBrowserItemForURL(info.url);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
let telemetryState = item.browserTelemetryStateMap.get(browser);
|
||||
if (
|
||||
lazy.serpEventsEnabled &&
|
||||
info.adImpressions &&
|
||||
!telemetryState.adImpressionsReported
|
||||
) {
|
||||
for (let [componentType, data] of info.adImpressions.entries()) {
|
||||
lazy.logConsole.debug("Counting ad:", { type: componentType, ...data });
|
||||
Glean.serp.adImpression.record({
|
||||
impression_id: telemetryState.impressionId,
|
||||
component: componentType,
|
||||
ads_loaded: data.adsLoaded,
|
||||
ads_visible: data.adsVisible,
|
||||
ads_hidden: data.adsHidden,
|
||||
});
|
||||
}
|
||||
telemetryState.adImpressionsReported = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export var SearchSERPTelemetry = new TelemetryHandler();
|
||||
|
|
|
@ -195,3 +195,41 @@ serp:
|
|||
The action taken on the page.
|
||||
Possible values are `clicked`.
|
||||
type: string
|
||||
|
||||
ad_impression:
|
||||
type: event
|
||||
description: >
|
||||
Recorded when a user loads a SERP and ads are detected.
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1816728
|
||||
data_reviews:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1816728
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
notification_emails:
|
||||
- fx-search-telemetry@mozilla.com
|
||||
- rev-data@mozilla.com
|
||||
expires: never
|
||||
extra_keys:
|
||||
impression_id: *impression_id
|
||||
component:
|
||||
description: >
|
||||
Type of components on a SERP. Possible values are
|
||||
`ad_carousel`, and `ad_link`. Defaults to `ad_link`.
|
||||
type: string
|
||||
ads_loaded:
|
||||
description: >
|
||||
Number of ads loaded for this component. They may or
|
||||
may not be visible on the page.
|
||||
type: quantity
|
||||
ads_visible:
|
||||
description: >
|
||||
Number of ads visible for this component. An ad can be
|
||||
considered visible if was within the browser window
|
||||
by the time the impression was recorded.
|
||||
type: quantity
|
||||
ads_hidden:
|
||||
description: >
|
||||
Number of ads hidden for this component. These are ads that
|
||||
are loaded in the DOM but hidden via CSS and/or Javascript.
|
||||
type: quantity
|
||||
|
|
|
@ -46,6 +46,16 @@ support-files =
|
|||
[browser_search_nimbus_reload.js]
|
||||
[browser_search_telemetry_aboutHome.js]
|
||||
tags = search-telemetry
|
||||
[browser_search_telemetry_adImpression_component.js]
|
||||
tags = search-telemetry
|
||||
support-files =
|
||||
searchTelemetryAd_components_carousel.html
|
||||
searchTelemetryAd_components_carousel_below_the_fold.html
|
||||
searchTelemetryAd_components_carousel_doubled.html
|
||||
searchTelemetryAd_components_carousel_first_element_non_visible.html
|
||||
searchTelemetryAd_components_carousel_hidden.html
|
||||
searchTelemetryAd_components_carousel_outer_container.html
|
||||
serp.css
|
||||
[browser_search_telemetry_content.js]
|
||||
tags = search-telemetry
|
||||
[browser_search_telemetry_searchbar.js]
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {
|
||||
SearchSERPTelemetry,
|
||||
SearchSERPTelemetryUtils,
|
||||
} = ChromeUtils.importESModule(
|
||||
"resource:///modules/SearchSERPTelemetry.sys.mjs"
|
||||
);
|
||||
|
||||
const WINDOW_HEIGHT = 768;
|
||||
const WINDOW_WIDTH = 1024;
|
||||
|
||||
const TEST_PROVIDER_INFO = [
|
||||
{
|
||||
telemetryId: "example",
|
||||
searchPageRegexp: /^http:\/\/mochi.test:.+\/browser\/browser\/components\/search\/test\/browser\/searchTelemetryAd_components_/,
|
||||
queryParamName: "s",
|
||||
codeParamName: "abc",
|
||||
taggedCodes: ["ff"],
|
||||
adServerAttributes: ["mozAttr"],
|
||||
extraAdServersRegexps: [/^https:\/\/example\.com\/ad$/],
|
||||
components: [
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
included: {
|
||||
parent: {
|
||||
selector: ".moz-carousel",
|
||||
},
|
||||
children: [
|
||||
{
|
||||
selector: ".moz-carousel-card",
|
||||
countChildren: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: SearchSERPTelemetryUtils.COMPONENTS.AD_LINK,
|
||||
included: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function getSERPUrl(page, organic = false) {
|
||||
let url =
|
||||
getRootDirectory(gTestPath).replace(
|
||||
"chrome://mochitests/content",
|
||||
"http://mochi.test:8888"
|
||||
) + page;
|
||||
return `${url}?s=test${organic ? "" : "&abc=ff"}`;
|
||||
}
|
||||
|
||||
async function promiseAdImpressionReceived() {
|
||||
return TestUtils.waitForCondition(() => {
|
||||
let adImpressions = Glean.serp.adImpression.testGetValue() ?? [];
|
||||
return adImpressions.length;
|
||||
}, "Should have received an ad impression.");
|
||||
}
|
||||
|
||||
async function promiseResize(width, height) {
|
||||
return TestUtils.waitForCondition(() => {
|
||||
return window.outerWidth === width && window.outerHeight === height;
|
||||
}, "Waiting for window to resize");
|
||||
}
|
||||
|
||||
// sharedData messages are only passed to the child on idle. Therefore
|
||||
// we wait for a few idles to try and ensure the messages have been able
|
||||
// to be passed across and handled.
|
||||
async function waitForIdle() {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
|
||||
}
|
||||
}
|
||||
|
||||
add_setup(async function() {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
|
||||
await waitForIdle();
|
||||
// Enable local telemetry recording for the duration of the tests.
|
||||
let oldCanRecord = Services.telemetry.canRecordExtended;
|
||||
Services.telemetry.canRecordExtended = true;
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.search.log", true],
|
||||
["browser.search.serpEventTelemetry.enabled", true],
|
||||
],
|
||||
});
|
||||
|
||||
// The tests evaluate whether or not ads are visible depending on whether
|
||||
// they are within the view of the window. To ensure the test results
|
||||
// are consistent regardless of where they are launched,
|
||||
// set the window size to something reasonable.
|
||||
let originalWidth = window.outerWidth;
|
||||
let originalHeight = window.outerHeight;
|
||||
window.resizeTo(WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
await promiseResize(WINDOW_WIDTH, WINDOW_HEIGHT);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
SearchSERPTelemetry.overrideSearchTelemetryForTests();
|
||||
Services.telemetry.canRecordExtended = oldCanRecord;
|
||||
window.resizeTo(originalWidth, originalHeight);
|
||||
await promiseResize(originalWidth, originalHeight);
|
||||
resetTelemetry();
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_ad_impressions_with_one_carousel() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl("searchTelemetryAd_components_carousel.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "4",
|
||||
ads_visible: "3",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
// This is to ensure we're not counting two carousel components as two
|
||||
// separate components but as one record with a sum of the results.
|
||||
add_task(async function test_ad_impressions_with_two_carousels() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl("searchTelemetryAd_components_carousel_doubled.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "8",
|
||||
ads_visible: "6",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_ad_impressions_with_carousels_with_outer_container() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl(
|
||||
"searchTelemetryAd_components_carousel_outer_container.html"
|
||||
);
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "4",
|
||||
ads_visible: "3",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_ad_impressions_with_carousels_tabhistory() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl("searchTelemetryAd_components_carousel.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
// Reset telemetry because we care about the telemetry upon going back.
|
||||
resetTelemetry();
|
||||
|
||||
let browserLoadedPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
BrowserTestUtils.loadURIString(
|
||||
tab.linkedBrowser,
|
||||
"https://www.example.com/some_url"
|
||||
);
|
||||
await browserLoadedPromise;
|
||||
|
||||
let pageShowPromise = BrowserTestUtils.waitForContentEvent(
|
||||
tab.linkedBrowser,
|
||||
"pageshow"
|
||||
);
|
||||
tab.linkedBrowser.goBack();
|
||||
await pageShowPromise;
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "4",
|
||||
ads_visible: "3",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_ad_impressions_with_hidden_carousels() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl("searchTelemetryAd_components_carousel_hidden.html");
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "3",
|
||||
ads_visible: "0",
|
||||
ads_hidden: "3",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_ad_impressions_with_carousel_scrolled_left() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl(
|
||||
"searchTelemetryAd_components_carousel_first_element_non_visible.html"
|
||||
);
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "4",
|
||||
ads_visible: "2",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
||||
add_task(async function test_ad_impressions_with_carousel_below_the_fold() {
|
||||
resetTelemetry();
|
||||
let url = getSERPUrl(
|
||||
"searchTelemetryAd_components_carousel_below_the_fold.html"
|
||||
);
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||
|
||||
await promiseAdImpressionReceived();
|
||||
|
||||
assertAdImpressionEvents([
|
||||
{
|
||||
component: SearchSERPTelemetryUtils.COMPONENTS.AD_CAROUSEL,
|
||||
ads_loaded: "4",
|
||||
ads_visible: "0",
|
||||
ads_hidden: "0",
|
||||
},
|
||||
]);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
|
@ -321,3 +321,28 @@ function assertImpressionEvents(expectedEvents) {
|
|||
"Should have equal number of engagements."
|
||||
);
|
||||
}
|
||||
|
||||
function assertAdImpressionEvents(expectedAdImpressions) {
|
||||
let adImpressions = Glean.serp.adImpression.testGetValue() ?? [];
|
||||
let impressions = Glean.serp.impression.testGetValue() ?? [];
|
||||
|
||||
Assert.equal(impressions.length, 1, "Should have a SERP impression event.");
|
||||
Assert.equal(
|
||||
adImpressions.length,
|
||||
expectedAdImpressions.length,
|
||||
"Should have equal number of ad impression events."
|
||||
);
|
||||
|
||||
expectedAdImpressions = expectedAdImpressions.map(expectedAdImpression => {
|
||||
expectedAdImpression.impression_id = impressions[0].extra.impression_id;
|
||||
return expectedAdImpression;
|
||||
});
|
||||
|
||||
for (let [index, expectedAdImpression] of expectedAdImpressions.entries()) {
|
||||
Assert.deepEqual(
|
||||
adImpressions[index]?.extra,
|
||||
expectedAdImpression,
|
||||
"Should have equal values for an ad impression."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top">
|
||||
<!--
|
||||
Carousels can have multiple hidden links.
|
||||
-->
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
Carousels can be used for non-ads.
|
||||
-->
|
||||
<h5 test-label="true">non_ad_carousel</h5>
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Giraffes</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Rhinos</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
<!--
|
||||
This is for testing a carousel below the fold.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top" style="padding-top: 1000px;">
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel-container">
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,182 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top">
|
||||
<!--
|
||||
Carousels can have multiple hidden links.
|
||||
-->
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
Carousels can be used for non-ads.
|
||||
-->
|
||||
<h5 test-label="true">non_ad_carousel</h5>
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Giraffes</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/some-normal-path"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Rhinos</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,85 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top">
|
||||
<!--
|
||||
If a user scrolls a carousel before the impression is snapped,
|
||||
we shouldn't count elements that aren't fully shown in the carousel
|
||||
as visible.
|
||||
-->
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel-container">
|
||||
<div class="moz-carousel" narrow="true">
|
||||
<div style="margin-left: -80px;" class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css"
|
||||
href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top">
|
||||
<h5 test-label="true">ad_carousel with display: none;</h5>
|
||||
<div class="moz-carousel" narrow="true" style="display: none;">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 test-label="true">ad_carousel with no width;</h5>
|
||||
<div class="moz-carousel" narrow="true" style="width: 0;">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 test-label="true">ad_carousel with no height;</h5>
|
||||
<div class="moz-carousel" narrow="true" style="height: 0;">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,83 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="./serp.css" />
|
||||
</head>
|
||||
<body>
|
||||
<section id="top">
|
||||
<!--
|
||||
Carousels can sometimes have an outer container that doesn't always show up.
|
||||
-->
|
||||
<h5 test-label="true">ad_carousel</h5>
|
||||
<div class="moz-carousel-container">
|
||||
<div class="moz-carousel" extra="true">
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moz-carousel-card">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/some-normal-path">
|
||||
<div class="moz-carousel-image">Image</div>
|
||||
</a>
|
||||
<div class="moz-carousel-card-inner">
|
||||
<div class="moz-carousel-card-inner-content">
|
||||
<a class="hidden" href="https://example.com/ad"></a>
|
||||
<a href="https://example.com/normal-path">
|
||||
<h3>Name of Product</h3>
|
||||
</a>
|
||||
<h3>$199.99</h3>
|
||||
<h3>Example.com</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
164
browser/components/search/test/browser/serp.css
Normal file
164
browser/components/search/test/browser/serp.css
Normal file
|
@ -0,0 +1,164 @@
|
|||
:root {
|
||||
--margin-left: 80px;
|
||||
--subtle: whitesmoke;
|
||||
--carousel-card-width: 180px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0 0 80px 0;
|
||||
}
|
||||
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
h5[test-label] {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
nav {
|
||||
border-bottom: 1px solid #ececec;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#searchform {
|
||||
padding-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
nav>div,
|
||||
#searchform,
|
||||
.moz-carousel,
|
||||
.factrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav>div,
|
||||
#searchform {
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
nav>div,
|
||||
#searchform,
|
||||
#searchresults,
|
||||
#top {
|
||||
margin-left: var(--margin-left);
|
||||
}
|
||||
|
||||
#searchbox {
|
||||
font-size: 14px;
|
||||
padding: 10px 20px;
|
||||
width: 300px;
|
||||
border-radius: 20px;
|
||||
border: 2px solid var(--subtle);
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.card-container>.card {
|
||||
height: 160px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--subtle);
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-container>.card:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.card-container>.card>a {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#searchresults {
|
||||
width: 900px;
|
||||
display: grid;
|
||||
grid-template-columns: 600px 300px;
|
||||
}
|
||||
|
||||
.moz-carousel,
|
||||
.factrow {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.moz-carousel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.moz-carousel[narrow],
|
||||
.moz-carousel-container {
|
||||
width: calc(var(--carousel-card-width) * 3 + (3 * 10px));
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.moz-carousel[extra] {
|
||||
width: calc(var(--carousel-card-width) * 4 + (3 * 10px));
|
||||
}
|
||||
|
||||
.moz-carousel>.moz-inner {
|
||||
border: 1px solid var(--subtle);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.moz-carousel>.moz-carousel-card {
|
||||
flex: 1 0 var(--carousel-card-width);
|
||||
border: 1px solid var(--subtle);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.moz-carousel-card .moz-carousel-image {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background-color: var(--subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.moz-carousel-card-inner-content {
|
||||
padding: 10px 20px 20px 20px;
|
||||
}
|
||||
|
||||
.multi-col {
|
||||
display: grid;
|
||||
padding: 10px 20px 20px 20px;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mock-image {
|
||||
height: 100px;
|
||||
background-color: var(--subtle);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Some SERPs hide anchors using CSS */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h2 {
|
||||
line-height: 100%;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
|
@ -80,7 +80,7 @@ class EventListener extends Handler {
|
|||
!content.document.body.classList.contains("loaded")
|
||||
) {
|
||||
// Don't restore the scroll position of an about:reader page at this
|
||||
// point; listen for the custom event dispatched from AboutReader.jsm.
|
||||
// point; listen for the custom event dispatched from AboutReader.sys.mjs.
|
||||
content.addEventListener("AboutReaderContentReady", this);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -4369,7 +4369,7 @@ var SessionStoreInternal = {
|
|||
this._restore_on_demand;
|
||||
|
||||
if (winData.tabs.length) {
|
||||
var tabs = tabbrowser.addMultipleTabs(
|
||||
var tabs = tabbrowser.createTabsForSessionRestore(
|
||||
restoreTabsLazily,
|
||||
selectTab,
|
||||
winData.tabs
|
||||
|
|
|
@ -436,7 +436,7 @@ let ShellServiceInternal = {
|
|||
* Determine if we're the default handler for the given file extension (like
|
||||
* ".pdf") or protocol (like "https"). Windows-only for now.
|
||||
*
|
||||
* @returns true if we are the default handler, false otherwise.
|
||||
* @returns {boolean} true if we are the default handler, false otherwise.
|
||||
*/
|
||||
isDefaultHandlerFor(aFileExtensionOrProtocol) {
|
||||
if (AppConstants.platform == "win") {
|
||||
|
|
|
@ -159,7 +159,8 @@ nsMacShellService::SetDesktopBackground(Element* aElement, int32_t aPosition,
|
|||
aElement->OwnerDoc()->CookieJarSettings();
|
||||
return wbp->SaveURI(imageURI, aElement->NodePrincipal(), 0, referrerInfo,
|
||||
cookieJarSettings, nullptr, nullptr, mBackgroundFile,
|
||||
nsIContentPolicy::TYPE_IMAGE, loadContext);
|
||||
nsIContentPolicy::TYPE_IMAGE,
|
||||
loadContext->UsePrivateBrowsing());
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
|
|
|
@ -36,6 +36,7 @@ XPCOMUtils.defineLazyServiceGetter(
|
|||
/**
|
||||
* Executes a XUL command on the top window. Called by the callbacks in each
|
||||
* TouchBarInput.
|
||||
*
|
||||
* @param {string} commandName
|
||||
* A XUL command.
|
||||
*/
|
||||
|
@ -52,6 +53,7 @@ function execCommand(commandName) {
|
|||
/**
|
||||
* Static helper function to convert a hexadecimal string to its integer
|
||||
* value. Used to convert colours to a format accepted by Apple's NSColor code.
|
||||
*
|
||||
* @param {string} hexString
|
||||
* A hexadecimal string, optionally beginning with '#'.
|
||||
*/
|
||||
|
@ -364,6 +366,7 @@ class TouchBarHelper {
|
|||
|
||||
/**
|
||||
* Fetches a specific Touch Bar Input by name and updates it on the Touch Bar.
|
||||
*
|
||||
* @param {...*} inputNames
|
||||
* A key/keys to a value/values in the gBuiltInInputs object in this file.
|
||||
*/
|
||||
|
@ -392,6 +395,7 @@ class TouchBarHelper {
|
|||
/**
|
||||
* Inserts a restriction token into the Urlbar ahead of the current typed
|
||||
* search term.
|
||||
*
|
||||
* @param {string} restrictionToken
|
||||
* The restriction token to be inserted into the Urlbar. Preferably
|
||||
* sourced from UrlbarTokenizer.RESTRICT.
|
||||
|
@ -509,6 +513,7 @@ helperProto._l10n = new Localization(["browser/touchbar/touchbar.ftl"]);
|
|||
|
||||
/**
|
||||
* A representation of a Touch Bar input.
|
||||
*
|
||||
* @param {object} input
|
||||
* An object representing a Touch Bar Input.
|
||||
* Contains listed properties.
|
||||
|
@ -617,6 +622,7 @@ class TouchBarInput {
|
|||
|
||||
/**
|
||||
* Apply Fluent l10n to child inputs.
|
||||
*
|
||||
* @param {Array} children
|
||||
* An array of initialized TouchBarInputs.
|
||||
*/
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
|
||||
extends: ["plugin:mozilla/require-jsdoc"],
|
||||
|
||||
rules: {
|
||||
"mozilla/var-only-at-top-level": "error",
|
||||
|
|
|
@ -17,6 +17,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
PartnerLinkAttribution: "resource:///modules/PartnerLinkAttribution.sys.mjs",
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs",
|
||||
SearchUIUtils: "resource:///modules/SearchUIUtils.sys.mjs",
|
||||
SearchUtils: "resource://gre/modules/SearchUtils.sys.mjs",
|
||||
UrlbarController: "resource:///modules/UrlbarController.sys.mjs",
|
||||
|
@ -34,7 +35,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
XPCOMUtils.defineLazyModuleGetters(lazy, {
|
||||
BrowserUIUtils: "resource:///modules/BrowserUIUtils.jsm",
|
||||
ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
|
||||
ReaderMode: "resource://gre/modules/ReaderMode.jsm",
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
|
@ -378,6 +378,9 @@ export class UrlbarInput {
|
|||
uri =
|
||||
this.window.gBrowser.selectedBrowser.currentAuthPromptURI ||
|
||||
uri ||
|
||||
(this.window.gBrowser.selectedBrowser.browsingContext.sessionHistory
|
||||
?.count === 0 &&
|
||||
this.window.gBrowser.selectedBrowser._initialURI) ||
|
||||
this.window.gBrowser.currentURI;
|
||||
// Strip off usernames and passwords for the location bar
|
||||
try {
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/valid-jsdoc"],
|
||||
|
||||
rules: {
|
||||
// Rules from the mozilla plugin
|
||||
"mozilla/balanced-listeners": "error",
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
extends: ["plugin:mozilla/valid-jsdoc"],
|
||||
|
||||
rules: {
|
||||
// Rules from the mozilla plugin
|
||||
"mozilla/balanced-listeners": "error",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* to make sure `FastClick.notNeeded` returns `true`.
|
||||
* This allows to disable FastClick and fix various breakage caused
|
||||
* by the library (mainly non-functioning drop-down lists).
|
||||
**/
|
||||
*/
|
||||
|
||||
/* globals exportFunction */
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue