Update On Sat Mar 25 19:47:48 CET 2023

This commit is contained in:
github-action[bot] 2023-03-25 19:47:49 +01:00
parent 8fbe161a32
commit 7deff43da8
977 changed files with 14892 additions and 37853 deletions

47
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -99,3 +99,10 @@
- (void)changeValueBySteps:(int)factor;
@end
@interface mozDatePickerAccessible : mozAccessible
// override
- (NSString*)moxTitle;
@end

View file

@ -229,3 +229,11 @@ using namespace mozilla::a11y;
}
@end
@implementation mozDatePickerAccessible
- (NSString*)moxTitle {
return utils::LocalizedString(u"dateField"_ns);
}
@end

View file

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

View file

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

View file

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

View file

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

View file

@ -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": {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,5 +5,5 @@
"use strict";
module.exports = {
extends: ["plugin:mozilla/require-jsdoc", "plugin:mozilla/valid-jsdoc"],
extends: ["plugin:mozilla/require-jsdoc"],
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,8 +5,6 @@
"use strict";
module.exports = {
extends: ["plugin:mozilla/valid-jsdoc"],
rules: {
// Rules from the mozilla plugin
"mozilla/balanced-listeners": "error",

View file

@ -5,8 +5,6 @@
"use strict";
module.exports = {
extends: ["plugin:mozilla/valid-jsdoc"],
rules: {
// Rules from the mozilla plugin
"mozilla/balanced-listeners": "error",

View file

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