Merge autoland to mozilla-central. a=merge

This commit is contained in:
Iulian Moraru 2024-05-23 07:08:36 +03:00
commit 010ccb86d4
184 changed files with 8567 additions and 6367 deletions

View file

@ -65,19 +65,19 @@ git = "https://github.com/mozilla/application-services"
rev = "e0563d725f852f617878ecc13a03cdf50c85cd5a"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/audioipc?rev=409e11f8de6288e9ddfe269654523735302e59e6"]
[source."git+https://github.com/mozilla/audioipc?rev=3495905752a4263827f5d43737f9ca3ed0243ce0"]
git = "https://github.com/mozilla/audioipc"
rev = "409e11f8de6288e9ddfe269654523735302e59e6"
rev = "3495905752a4263827f5d43737f9ca3ed0243ce0"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=4ca174cf83ebe32b3198478c2211d69678845bc7"]
[source."git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=0989726a1b9b640a30dfdf3ea005a12c73ab8155"]
git = "https://github.com/mozilla/cubeb-coreaudio-rs"
rev = "4ca174cf83ebe32b3198478c2211d69678845bc7"
rev = "0989726a1b9b640a30dfdf3ea005a12c73ab8155"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/cubeb-pulse-rs?rev=8ff972c8e2ec1782ff262ac4071c0415e69b1367"]
[source."git+https://github.com/mozilla/cubeb-pulse-rs?rev=8678dcab1c287de79c4c184ccc2e065bc62b70e2"]
git = "https://github.com/mozilla/cubeb-pulse-rs"
rev = "8ff972c8e2ec1782ff262ac4071c0415e69b1367"
rev = "8678dcab1c287de79c4c184ccc2e065bc62b70e2"
replace-with = "vendored-sources"
[source."git+https://github.com/mozilla/midir.git?rev=85156e360a37d851734118104619f86bd18e94c6"]

55
Cargo.lock generated
View file

@ -160,7 +160,7 @@ version = "0.38.0+1.3.281"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
dependencies = [
"libloading 0.8.3",
"libloading",
]
[[package]]
@ -260,7 +260,7 @@ dependencies = [
[[package]]
name = "audioipc2"
version = "0.6.0"
source = "git+https://github.com/mozilla/audioipc?rev=409e11f8de6288e9ddfe269654523735302e59e6#409e11f8de6288e9ddfe269654523735302e59e6"
source = "git+https://github.com/mozilla/audioipc?rev=3495905752a4263827f5d43737f9ca3ed0243ce0#3495905752a4263827f5d43737f9ca3ed0243ce0"
dependencies = [
"arrayvec",
"ashmem",
@ -288,7 +288,7 @@ dependencies = [
[[package]]
name = "audioipc2-client"
version = "0.6.0"
source = "git+https://github.com/mozilla/audioipc?rev=409e11f8de6288e9ddfe269654523735302e59e6#409e11f8de6288e9ddfe269654523735302e59e6"
source = "git+https://github.com/mozilla/audioipc?rev=3495905752a4263827f5d43737f9ca3ed0243ce0#3495905752a4263827f5d43737f9ca3ed0243ce0"
dependencies = [
"audio_thread_priority",
"audioipc2",
@ -299,7 +299,7 @@ dependencies = [
[[package]]
name = "audioipc2-server"
version = "0.6.0"
source = "git+https://github.com/mozilla/audioipc?rev=409e11f8de6288e9ddfe269654523735302e59e6#409e11f8de6288e9ddfe269654523735302e59e6"
source = "git+https://github.com/mozilla/audioipc?rev=3495905752a4263827f5d43737f9ca3ed0243ce0#3495905752a4263827f5d43737f9ca3ed0243ce0"
dependencies = [
"audio_thread_priority",
"audioipc2",
@ -746,13 +746,13 @@ checksum = "bb7bdea464ae038f09197b82430b921c53619fc8d2bcaf7b151013b3ca008017"
[[package]]
name = "clang-sys"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a"
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
dependencies = [
"glob",
"libc",
"libloading 0.7.999",
"libloading",
]
[[package]]
@ -921,7 +921,7 @@ dependencies = [
[[package]]
name = "coreaudio-sys-utils"
version = "0.1.0"
source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=4ca174cf83ebe32b3198478c2211d69678845bc7#4ca174cf83ebe32b3198478c2211d69678845bc7"
source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=0989726a1b9b640a30dfdf3ea005a12c73ab8155#0989726a1b9b640a30dfdf3ea005a12c73ab8155"
dependencies = [
"core-foundation-sys",
"coreaudio-sys",
@ -997,7 +997,7 @@ dependencies = [
"fluent",
"gtkbind",
"intl-memoizer",
"libloading 0.8.3",
"libloading",
"log",
"mozbuild",
"mozilla-central-workspace-hack",
@ -1140,27 +1140,27 @@ dependencies = [
[[package]]
name = "cubeb"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db57570f2617f0214c11721e8d2325816d9dc936c2c472661ac5d90a30fba98"
checksum = "3d105547cf8036cdb30e796ce0d06832af4766106a44574402fa2fd3c861a042"
dependencies = [
"cubeb-core",
]
[[package]]
name = "cubeb-backend"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00b0f3b84e315571bd8c4e18794180633066267a413f2f05bca65001adc8410"
checksum = "67361fe9b49b4599e2a230ce322529b6ddd91df14897c872dcede716f8fbca81"
dependencies = [
"cubeb-core",
]
[[package]]
name = "cubeb-core"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2380c03a7df0ea3744f6a210d6340f423935e53cbf2fd68ada84b5e808e46ac7"
checksum = "ac08d314dd1ec6d41d9ccdeec70899c98ed3b89845367000dd6096099481bc73"
dependencies = [
"bitflags 1.999.999",
"cubeb-sys",
@ -1169,7 +1169,7 @@ dependencies = [
[[package]]
name = "cubeb-coreaudio"
version = "0.1.0"
source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=4ca174cf83ebe32b3198478c2211d69678845bc7#4ca174cf83ebe32b3198478c2211d69678845bc7"
source = "git+https://github.com/mozilla/cubeb-coreaudio-rs?rev=0989726a1b9b640a30dfdf3ea005a12c73ab8155#0989726a1b9b640a30dfdf3ea005a12c73ab8155"
dependencies = [
"atomic",
"audio-mixer",
@ -1188,7 +1188,7 @@ dependencies = [
[[package]]
name = "cubeb-pulse"
version = "0.5.0"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8ff972c8e2ec1782ff262ac4071c0415e69b1367#8ff972c8e2ec1782ff262ac4071c0415e69b1367"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8678dcab1c287de79c4c184ccc2e065bc62b70e2#8678dcab1c287de79c4c184ccc2e065bc62b70e2"
dependencies = [
"cubeb-backend",
"pulse",
@ -1199,9 +1199,9 @@ dependencies = [
[[package]]
name = "cubeb-sys"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c20c457d7b34dad6e0c1a9c759c96b4420b9e9917a572998b81835799a07e1d"
checksum = "26073cd50c7b6ba4272204839f56921557609a0d67e092882cbb903df94cab39"
dependencies = [
"cmake",
"pkg-config",
@ -1213,7 +1213,7 @@ version = "0.20.0"
source = "git+https://github.com/gfx-rs/wgpu?rev=18b758e3889bdd6ffa769085de15e2b96a0c1eb5#18b758e3889bdd6ffa769085de15e2b96a0c1eb5"
dependencies = [
"bitflags 2.5.0",
"libloading 0.8.3",
"libloading",
"winapi",
]
@ -3265,13 +3265,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "libloading"
version = "0.7.999"
dependencies = [
"libloading 0.8.3",
]
[[package]]
name = "libloading"
version = "0.8.3"
@ -4292,7 +4285,7 @@ dependencies = [
"core-foundation",
"env_logger",
"lazy_static",
"libloading 0.8.3",
"libloading",
"log",
"mozilla-central-workspace-hack",
"pkcs11-bindings",
@ -4635,7 +4628,7 @@ dependencies = [
[[package]]
name = "pulse"
version = "0.3.0"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8ff972c8e2ec1782ff262ac4071c0415e69b1367#8ff972c8e2ec1782ff262ac4071c0415e69b1367"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8678dcab1c287de79c4c184ccc2e065bc62b70e2#8678dcab1c287de79c4c184ccc2e065bc62b70e2"
dependencies = [
"bitflags 2.5.0",
"pulse-ffi",
@ -4644,7 +4637,7 @@ dependencies = [
[[package]]
name = "pulse-ffi"
version = "0.1.0"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8ff972c8e2ec1782ff262ac4071c0415e69b1367#8ff972c8e2ec1782ff262ac4071c0415e69b1367"
source = "git+https://github.com/mozilla/cubeb-pulse-rs?rev=8678dcab1c287de79c4c184ccc2e065bc62b70e2#8678dcab1c287de79c4c184ccc2e065bc62b70e2"
dependencies = [
"libc",
]
@ -6674,7 +6667,7 @@ dependencies = [
"js-sys",
"khronos-egl",
"libc",
"libloading 0.8.3",
"libloading",
"log",
"metal",
"naga",

View file

@ -173,9 +173,6 @@ memoffset = { path = "build/rust/memoffset" }
# Patch `hashbrown` 0.12.* to depend on 0.14.*
hashbrown = { path = "build/rust/hashbrown" }
# Patch `libloading` 0.7 to depend on 0.8, which moves from `winapi` to `windows-sys`.
libloading = { path = "build/rust/libloading" }
# Patch `socket2` 0.4 to 0.5
socket2 = { path = "build/rust/socket2" }

View file

@ -403,12 +403,20 @@ int32_t AccessibleWrap::GetInputType(const nsString& aInputTypeAttr) {
}
void AccessibleWrap::GetTextEquiv(nsString& aText) {
if (nsTextEquivUtils::HasNameRule(this, eNameFromSubtreeIfReqRule)) {
// This is an accessible that normally doesn't get its name from its
// subtree, so we collect the text equivalent explicitly.
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
} else {
Name(aText);
// 1. Start with the name, since it might have been explicitly specified.
if (Name(aText) != eNameFromSubtree) {
// 2. If the name didn't come from the subtree, add the text from the
// subtree.
if (aText.IsEmpty()) {
nsTextEquivUtils::GetTextEquivFromSubtree(this, aText);
} else {
nsAutoString subtree;
nsTextEquivUtils::GetTextEquivFromSubtree(this, subtree);
if (!subtree.IsEmpty()) {
aText.Append(' ');
aText.Append(subtree);
}
}
}
}

View file

@ -5,6 +5,7 @@
#include "NotificationController.h"
#include "CssAltContent.h"
#include "DocAccessible-inl.h"
#include "DocAccessibleChild.h"
#include "LocalAccessible-inl.h"
@ -823,6 +824,14 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
}
#endif
if (CssAltContent(textNode)) {
// A11y doesn't care about the text rendered by layout if there is CSS
// content alt text. We skip this here rather than when the update is
// queued because the TextLeafAccessible might not exist yet and we
// might need to create it below.
continue;
}
TextUpdater::Run(mDocument, textAcc->AsTextLeaf(), text.mString);
continue;
}

View file

@ -20,7 +20,7 @@ using namespace mozilla::a11y;
/**
* The accessible for which we are computing a text equivalent. It is useful
* for bailing out during recursive text computation, or for special cases
* like step f. of the ARIA implementation guide.
* like the "Embedded Control" section of the AccName spec.
*/
static const Accessible* sInitiatorAcc = nullptr;
@ -252,7 +252,7 @@ nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
bool isEmptyTextEquiv = true;
// Attempt to find the value. If it's non-empty, append and return it. See the
// "embedded control" section of the name spec.
// "Embedded Control" section of the name spec.
nsAutoString val;
nsresult rv = AppendFromValue(aAccessible, &val);
NS_ENSURE_SUCCESS(rv, rv);
@ -261,16 +261,15 @@ nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
return NS_OK;
}
// If the name is from tooltip then append it to result string in the end
// (see h. step of name computation guide).
// If the name is from tooltip, we retrieve it now but only append it to the
// result string later as a last resort. Otherwise, we append the name now.
nsAutoString text;
if (aAccessible->Name(text) != eNameFromTooltip) {
isEmptyTextEquiv = !AppendString(aString, text);
}
// Implementation of g) step of text equivalent computation guide. Go down
// into subtree if accessible allows "text equivalent from subtree rule" or
// it's not root and not control.
// Implementation of the "Name From Content" step of the text alternative
// computation guide. Traverse the accessible's subtree if allowed.
if (isEmptyTextEquiv) {
if (ShouldIncludeInSubtreeCalculation(aAccessible)) {
rv = AppendFromAccessibleChildren(aAccessible, aString);
@ -280,7 +279,7 @@ nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
}
}
// Implementation of h. step
// Implementation of the "Tooltip" step
if (isEmptyTextEquiv && !text.IsEmpty()) {
AppendString(aString, text);
if (isHTMLBlock) {
@ -305,7 +304,6 @@ nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
// computation. If the given accessible is not the root accessible (the
// accessible the text alternative is computed for in the end) then append the
// accessible value.
if (aAccessible == sInitiatorAcc) {
return NS_OK_NO_NAME_CLAUSE_HANDLED;
}
@ -323,6 +321,7 @@ nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
return NS_ERROR_FAILURE;
}
// For other accessibles, get the value directly.
aAccessible->Value(text);
return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;

View file

@ -20,17 +20,19 @@ class LocalAccessible;
} // namespace mozilla
/**
* Text equivalent computation rules (see nsTextEquivUtils::gRoleToNameRulesMap)
* Text equivalent computation rules. These rules are mapped to accessible roles
* in RoleMap.h.
*/
enum ETextEquivRule {
// No rule.
// No rule. Equivalent to "name from author."
eNoNameRule = 0x00,
// Walk into subtree only if the currently navigated accessible is not root
// accessible (i.e. if the accessible is part of text equivalent computation).
eNameFromSubtreeIfReqRule = 0x01,
// Text equivalent computation from subtree is allowed.
// Text equivalent computation from subtree is allowed. Equivalent to "name
// from content."
eNameFromSubtreeRule = 0x03,
// The accessible allows to append its value to text equivalent.
@ -41,7 +43,9 @@ enum ETextEquivRule {
/**
* The class provides utils methods to compute the accessible name and
* description.
* description. Note that, as of the Accessible Name and Description Computation
* 1.2 specification, the phrases "text equivalent" and "text alternative" are
* used interchangably.
*/
class nsTextEquivUtils {
public:
@ -61,7 +65,7 @@ class nsTextEquivUtils {
}
/**
* Calculates the name from accessible subtree if allowed.
* Calculates the name from the given accessible's subtree, if allowed.
*
* @param aAccessible [in] the given accessible
* @param aName [out] accessible name
@ -70,8 +74,10 @@ class nsTextEquivUtils {
nsAString& aName);
/**
* Calculates text equivalent from the subtree. Similar to GetNameFromSubtree.
* However it returns not empty result for things like HTML p.
* Calculates text equivalent from the subtree. This function is similar to
* GetNameFromSubtree, but it returns a non-empty result for things like
* HTML:p, since it does not verify that the given accessible allows name
* from content.
*/
static void GetTextEquivFromSubtree(const Accessible* aAccessible,
nsString& aTextEquiv) {
@ -82,8 +88,8 @@ class nsTextEquivUtils {
}
/**
* Calculates text equivalent for the given accessible from its IDRefs
* attribute (like aria-labelledby or aria-describedby).
* Calculates the text equivalent for the given accessible from one of its
* IDRefs attributes (like aria-labelledby or aria-describedby).
*
* @param aAccessible [in] the accessible text equivalent is computed for
* @param aIDRefsAttr [in] IDRefs attribute on DOM node of the accessible
@ -94,8 +100,8 @@ class nsTextEquivUtils {
nsAString& aTextEquiv);
/**
* Calculates the text equivalent from the given content and its subtree if
* allowed and appends it to the given string.
* Calculates the text equivalent from the given content - and its subtree, if
* allowed - and appends it to the given string.
*
* @param aInitiatorAcc [in] the accessible text equivalent is computed for
* in the end (root accessible of text equivalent
@ -119,8 +125,8 @@ class nsTextEquivUtils {
nsAString* aString);
/**
* Iterates DOM children and calculates text equivalent from each child node.
* Then, appends found text to the given string.
* Iterates DOM children and calculates the text equivalent from each child
* node. Then, appends the calculated text to the given string.
*
* @param aContent [in] the node to fetch DOM children from
* @param aString [in, out] the string
@ -130,33 +136,35 @@ class nsTextEquivUtils {
private:
/**
* Iterates accessible children and calculates text equivalent from each
* child.
* Iterates the given accessible's children and calculates the text equivalent
* from each child. Then, appends the calculated text to the given string.
*/
static nsresult AppendFromAccessibleChildren(const Accessible* aAccessible,
nsAString* aString);
/**
* Calculates text equivalent from the given accessible and its subtree if
* allowed.
* Calculates the text equivalent from the given accessible - and its subtree,
* if allowed. Then, appends the calculated text to the given string.
*/
static nsresult AppendFromAccessible(Accessible* aAccessible,
nsAString* aString);
/**
* Calculates text equivalent from the value of given accessible.
* Calculates the text equivalent from the value of the given accessible.
* Then, appends the calculated text to the given string. This function
* implements the "Embedded Control" section of the AccName spec.
*/
static nsresult AppendFromValue(Accessible* aAccessible, nsAString* aString);
/**
* Calculates text equivalent from the given DOM node and its subtree if
* allowed.
* Calculates the text equivalent from the given DOM node - and its subtree,
* if allowed. Then, appends the calculated text to the given string.
*/
static nsresult AppendFromDOMNode(nsIContent* aContent, nsAString* aString);
/**
* Concatenates strings and appends space between them. Returns true if
* text equivalent string was appended.
* Concatenates the given strings and appends space between them. Returns true
* if the text equivalent string was appended.
*/
static bool AppendString(nsAString* aString,
const nsAString& aTextEquivalent);
@ -173,7 +181,7 @@ class nsTextEquivUtils {
static bool ShouldIncludeInSubtreeCalculation(Accessible* aAccessible);
/**
* Returns true if a given accessible is a text leaf containing only
* Returns true if the given accessible is a text leaf containing only
* whitespace.
*/
static bool IsWhitespaceLeaf(Accessible* aAccessible);

View file

@ -802,6 +802,9 @@ pref("browser.shopping.experience2023.sidebarClosedCount", 0);
// When conditions are met, shows a prompt on the shopping sidebar asking users if they want to disable auto-open behavior
pref("browser.shopping.experience2023.showKeepSidebarClosedMessage", true);
// Spin the cursor while the page is loading
pref("browser.spin_cursor_while_busy", false);
// Enable display of megalist option in browser sidebar
// Keep it hidden from about:config for now.
// pref("browser.megalist.enabled", false);

View file

@ -3435,6 +3435,10 @@ var XULBrowserWindow = {
) {
this.busyUI = true;
if (this.spinCursorWhileBusy) {
window.setCursor("progress");
}
// XXX: This needs to be based on window activity...
this.stopCommand.removeAttribute("disabled");
CombinedStopReload.switchToStop(aRequest, aWebProgress);
@ -3502,6 +3506,8 @@ var XULBrowserWindow = {
if (this.busyUI && aWebProgress.isTopLevel) {
this.busyUI = false;
window.setCursor("auto");
this.stopCommand.setAttribute("disabled", "true");
CombinedStopReload.switchToReload(aRequest, aWebProgress);
}
@ -3967,6 +3973,12 @@ var XULBrowserWindow = {
},
};
XPCOMUtils.defineLazyPreferenceGetter(
XULBrowserWindow,
"spinCursorWhileBusy",
"browser.spin_cursor_while_busy"
);
var LinkTargetDisplay = {
get DELAY_SHOW() {
delete this.DELAY_SHOW;

View file

@ -233,6 +233,34 @@ newtab:
send_in_pings:
- newtab
wallpaper_click:
type: event
description: >
Recorded when a user clicks on a wallpaper option
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1896004
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1896004
data_sensitivity:
- interaction
notification_emails:
- nbarrett@mozilla.com
expires: never
extra_keys:
selected_wallpaper:
description: >
Which wallpaper has been selected by the user
Will be the title of a Wallpaper or 'none' for users
that reset the background to default
type: string
had_previous_wallpaper:
description: >
Wheather or not user had a previously set wallpaper
type: boolean
newtab_visit_id: *newtab_visit_id
send_in_pings:
- newtab
weather_change_display:
type: event
description: >

View file

@ -130,7 +130,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "99c7f10cf8f804a1db6f875e9d0f668951fe1230"
"revision": "6a0ec19cbba9c1626509305a0af58c7925f4bbdd"
},
"bg": {
"pin": false,
@ -358,7 +358,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "941aa270f1bc978b45cd96192dd67beb2b53656b"
"revision": "39847764e40b764c0a3cfc5fe9e79a212b318f22"
},
"da": {
"pin": false,
@ -396,7 +396,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "df2005e53260bbf47b32c2f092c9b1d72707d011"
"revision": "5261e1350e923eaaf014c329a93e04fe292fec57"
},
"dsb": {
"pin": false,
@ -415,7 +415,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "757c97c67dbb5795e042146d99e36619ee81b897"
"revision": "4d0723a62e375d3c62909892735dce95f5b719a9"
},
"el": {
"pin": false,
@ -529,7 +529,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "b3e50d2e42a769cffb3ae30c953d09f9a435a6af"
"revision": "8dce7cc5172e4fec2ffa59a1743bbd5d903bb21f"
},
"es-ES": {
"pin": false,
@ -719,7 +719,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "f10afc727e91f871d898d31b5a9b0c6d56b092c0"
"revision": "39c000ffab7363ba9cc8c7e15c6ad14c581336f9"
},
"ga-IE": {
"pin": false,
@ -890,7 +890,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "3db9ee337797d21a90f164ed037bfae7d04e7609"
"revision": "f2bf14543268340a4a926e2312bebbf8685df5d3"
},
"hu": {
"pin": false,
@ -909,7 +909,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "43352212205f762577018f1b5fc8c796d77f82c7"
"revision": "8bbfe04b73c31a07a8e773db7c66eee0ca28ecc3"
},
"hy-AM": {
"pin": false,
@ -1257,7 +1257,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "a424b2c5b2a4aa0576a4ffb7a381091c1b212473"
"revision": "9477991e2e620b21d5bc07249c1221b76a5fc8f5"
},
"meh": {
"pin": false,
@ -1409,7 +1409,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "a3af3d2d9c45eeb055b21f769abf8dcc496898cf"
"revision": "deccbe803c4925d91fd4ca0ace0d3858af619c3d"
},
"nn-NO": {
"pin": false,
@ -1504,7 +1504,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "3e6a5b601d803f0782b9a39301d26cd2d02e2cda"
"revision": "53d7e4683243d8dd28b9f71a6b0c376de7402bc0"
},
"pt-PT": {
"pin": false,
@ -1580,7 +1580,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "565aa25b27feb6e54292a13f6ec95d6db0a899ca"
"revision": "cbfbe72a7049d2934b239628f15aed063d998880"
},
"sat": {
"pin": false,
@ -1694,7 +1694,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "c8cc5fa50635433e6b9945b2eaba48544dad80d3"
"revision": "67e2b9de3a6db4a9f354110ff7403df9e47195a7"
},
"skr": {
"pin": false,
@ -1884,7 +1884,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "23144faf9ad3ec97302d41ecfb0a3c307ce40987"
"revision": "e3c7eaf5ca18eaa4b2a841306bd71ff60ac45049"
},
"th": {
"pin": false,
@ -1941,7 +1941,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "81c113dcfdc12cc9d156e478978ef5abc7a4e34e"
"revision": "e76162369cf8cffca8ce5c307e6f5f0979a1e505"
},
"trs": {
"pin": false,
@ -1979,7 +1979,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "c71cbc1877a43b1c8013d29d512602efb9f72f3a"
"revision": "22962edbb5280103883a3a6b239e9cac575d65ce"
},
"ur": {
"pin": false,
@ -2112,6 +2112,6 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "2e36f22fa527800bfc302c000fd0255f43fd9293"
"revision": "2a77385c1f92f1974f7847d883a78431130d5066"
}
}

View file

@ -1,10 +0,0 @@
[package]
name = "libloading"
version = "0.7.999"
edition = "2021"
[lib]
path = "lib.rs"
[dependencies.libloading]
version = "0.8.3"

View file

@ -1,4 +0,0 @@
/* 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/. */
pub use libloading::*;

View file

@ -210,6 +210,15 @@ static void _relrhack_init(void) {
}
}
# ifdef ANDROID
// This creates a value that uses a relative relocation.
// In the case we've not set DT_RELRHACK_BIT on the RELR tags in the
// dynamic section, if the dynamic loader applied the relocation,
// the value will correctly point to the function address in memory.
__attribute__((visibility("hidden"))) void (*__relrhack_init)(void) =
_relrhack_init;
# endif
// The Android CRT doesn't contain an init function.
# ifndef ANDROID
extern __attribute__((visibility("hidden"))) void _init(int argc, char** argv,
@ -217,6 +226,13 @@ extern __attribute__((visibility("hidden"))) void _init(int argc, char** argv,
# endif
void _relrhack_wrap_init(int argc, char** argv, char** env) {
# ifdef ANDROID
// When the dynamic loader has applied the relocations itself, do nothing.
// (see comment above __relrhack_init)
if (__relrhack_init == _relrhack_init) {
return;
}
# endif
_relrhack_init();
# ifndef ANDROID
_init(argc, argv, env);

View file

@ -114,7 +114,7 @@ struct RelR : public Elf<bits> {
return 0;
}
static bool hack(std::fstream& f);
static bool hack(std::fstream& f, bool set_relrhack_bit);
};
template <typename T>
@ -150,7 +150,7 @@ void write_vector_at(std::ostream& out, off_t pos, const std::vector<T>& vec) {
}
template <int bits>
bool RelR<bits>::hack(std::fstream& f) {
bool RelR<bits>::hack(std::fstream& f, bool set_relrhack_bit) {
auto ehdr = read_one_at<Elf_Ehdr>(f, 0);
if (ehdr.e_phentsize != sizeof(Elf_Phdr)) {
throw std::runtime_error("Invalid ELF?");
@ -225,9 +225,11 @@ bool RelR<bits>::hack(std::fstream& f) {
return false;
}
// Change DT_RELR* tags to add DT_RELRHACK_BIT.
for (const auto tag : {DT_RELR, DT_RELRSZ, DT_RELRENT}) {
write_one_at(f, dyn_info.offset(tag), tag | DT_RELRHACK_BIT);
if (set_relrhack_bit) {
// Change DT_RELR* tags to add DT_RELRHACK_BIT.
for (const auto tag : {DT_RELR, DT_RELRSZ, DT_RELRENT}) {
write_one_at(f, dyn_info.offset(tag), tag | DT_RELRHACK_BIT);
}
}
bool is_glibc = false;
@ -611,10 +613,14 @@ int main(int argc, char* argv[]) {
f.exceptions(f.failbit);
auto elf_class = get_elf_class(f);
f.seekg(0, std::ios::beg);
// On Android, we don't set the relrhack bit so that the system linker
// can handle the RELR relocations when supported. On desktop, the
// glibc unfortunately rejects the binaries without the bit set.
// (see comment about GLIBC_ABI_DT_RELR)
if (elf_class == ELFCLASS32) {
hacked = RelR<32>::hack(f);
hacked = RelR<32>::hack(f, /* set_relrhack_bit = */ !is_android);
} else if (elf_class == ELFCLASS64) {
hacked = RelR<64>::hack(f);
hacked = RelR<64>::hack(f, /* set_relrhack_bit = */ !is_android);
}
} catch (const std::runtime_error& err) {
std::cerr << "Failed to hack " << output->string() << ": " << err.what()

View file

@ -0,0 +1,13 @@
<script>
window.addEventListener("DOMContentLoaded", () => {
window.find("A")
let s = window.getSelection()
s.extend(a.attachShadow({mode: "open"}), 0)
s.extend(b)
document.execCommand("insertUnorderedList", false)
})
</script>
A
<select autofocus=""></select>
<h2 id="a">
<del id="b" contenteditable="true">

View file

@ -277,4 +277,5 @@ load 1887963_1.html
load 1887963_2.html
asserts(0-1) load 1887974.html
load 1890888.html
load 1896225.html
load 1897248.html

View file

@ -474,15 +474,21 @@ bool nsFrameMessageManager::GetParamsForMessage(JSContext* aCx,
// properly cases when interface is implemented in JS and used
// as a dictionary.
nsAutoString json;
NS_ENSURE_TRUE(
nsContentUtils::StringifyJSON(aCx, v, json, UndefinedIsNullStringLiteral),
false);
if (!nsContentUtils::StringifyJSON(aCx, v, json,
UndefinedIsNullStringLiteral)) {
NS_WARNING("nsContentUtils::StringifyJSON() failed");
JS_ClearPendingException(aCx);
return false;
}
NS_ENSURE_TRUE(!json.IsEmpty(), false);
JS::Rooted<JS::Value> val(aCx, JS::NullValue());
NS_ENSURE_TRUE(JS_ParseJSON(aCx, static_cast<const char16_t*>(json.get()),
json.Length(), &val),
false);
if (!JS_ParseJSON(aCx, static_cast<const char16_t*>(json.get()),
json.Length(), &val)) {
NS_WARNING("JS_ParseJSON");
JS_ClearPendingException(aCx);
return false;
}
aData.Write(aCx, val, rv);
if (NS_WARN_IF(rv.Failed())) {

View file

@ -3580,7 +3580,7 @@ void EventStateManager::DecideGestureEvent(WidgetGestureNotifyEvent* aEvent,
displayPanFeedback = true;
}
} // scrollableFrame
} // ancestor chain
} // ancestor chain
aEvent->mDisplayPanFeedback = displayPanFeedback;
aEvent->mPanDirection = panDirection;
}
@ -4821,6 +4821,11 @@ nsresult EventStateManager::SetCursor(StyleCursorKind aCursor,
return NS_OK;
}
bool EventStateManager::CursorSettingManagerHasLockedCursor() {
return sCursorSettingManager &&
sCursorSettingManager->mLockCursor != kInvalidCursorKind;
}
class MOZ_STACK_CLASS ESMEventCB : public EventDispatchingCallback {
public:
explicit ESMEventCB(nsIContent* aTarget) : mTarget(aTarget) {}

View file

@ -425,6 +425,9 @@ class EventStateManager : public nsSupportsWeakReference, public nsIObserver {
static EventStateManager* sCursorSettingManager;
static void ClearCursorSettingManager() { sCursorSettingManager = nullptr; }
// Checks if the manager in this process has a locked cursor
static bool CursorSettingManagerHasLockedCursor();
static EventStateManager* GetActiveEventStateManager() { return sActiveESM; }
// Sets aNewESM to be the active event state manager, and

View file

@ -1652,8 +1652,7 @@ FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
// we need to strip Authentication headers for cross-origin requests
// Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch
bool skipAuthHeader =
(StaticPrefs::network_fetch_redirect_stripAuthHeader() &&
NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags));
NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags);
SetRequestHeaders(newHttpChannel, rewriteToGET, skipAuthHeader);
}

View file

@ -1456,8 +1456,10 @@ void BrowserParent::MouseEnterIntoWidget() {
// When we mouseenter the remote target, the remote target's cursor should
// become the current cursor. When we mouseexit, we stop.
mRemoteTargetSetsCursor = true;
widget->SetCursor(mCursor);
EventStateManager::ClearCursorSettingManager();
if (!EventStateManager::CursorSettingManagerHasLockedCursor()) {
widget->SetCursor(mCursor);
EventStateManager::ClearCursorSettingManager();
}
}
// Mark that we have missed a mouse enter event, so that
@ -1495,8 +1497,10 @@ void BrowserParent::SendRealMouseEvent(WidgetMouseEvent& aEvent) {
// become the current cursor. When we mouseexit, we stop.
if (eMouseEnterIntoWidget == aEvent.mMessage) {
mRemoteTargetSetsCursor = true;
widget->SetCursor(mCursor);
EventStateManager::ClearCursorSettingManager();
if (!EventStateManager::CursorSettingManagerHasLockedCursor()) {
widget->SetCursor(mCursor);
EventStateManager::ClearCursorSettingManager();
}
} else if (eMouseExitFromWidget == aEvent.mMessage) {
mRemoteTargetSetsCursor = false;
}
@ -2354,6 +2358,10 @@ mozilla::ipc::IPCResult BrowserParent::RecvSetCursor(
return IPC_OK();
}
if (EventStateManager::CursorSettingManagerHasLockedCursor()) {
return IPC_OK();
}
widget->SetCursor(mCursor);
return IPC_OK();
}

View file

@ -0,0 +1,36 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
/* 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/. */
#include "MediaInfo.h"
#include "gtest/gtest.h"
using namespace mozilla;
TEST(TestMediaInfo, CloneVideoInfo)
{
VideoInfo info;
info.mDisplay = info.mImage = gfx::IntSize{640, 360};
info.mStereoMode = StereoMode::BOTTOM_TOP;
info.mRotation = VideoRotation::kDegree_270;
info.mColorDepth = gfx::ColorDepth::COLOR_16;
info.mColorRange = gfx::ColorRange::FULL;
UniquePtr<TrackInfo> clone1 = info.Clone();
UniquePtr<TrackInfo> clone2 = info.Clone();
auto* info1 = clone1->GetAsVideoInfo();
auto* info2 = clone2->GetAsVideoInfo();
EXPECT_EQ(info1->mDisplay, info2->mDisplay);
EXPECT_EQ(info1->mImage, info2->mImage);
EXPECT_EQ(info1->mStereoMode, info2->mStereoMode);
EXPECT_EQ(info1->mRotation, info2->mRotation);
EXPECT_EQ(info1->mColorDepth, info2->mColorDepth);
EXPECT_EQ(info1->mColorRange, info2->mColorRange);
// They should have their own media byte buffers which have different address
EXPECT_NE(info1->mExtraData.get(), info2->mExtraData.get());
EXPECT_NE(info1->mCodecSpecificConfig.get(),
info2->mCodecSpecificConfig.get());
}

View file

@ -53,6 +53,7 @@ UNIFIED_SOURCES += [
"TestMediaDataDecoder.cpp",
"TestMediaDataEncoder.cpp",
"TestMediaEventSource.cpp",
"TestMediaInfo.cpp",
"TestMediaMIMETypes.cpp",
"TestMediaQueue.cpp",
"TestMediaSpan.cpp",

View file

@ -3542,7 +3542,6 @@ XMLHttpRequestMainThread::AsyncOnChannelRedirect(
// we need to strip Authentication headers for cross-origin requests
// Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch
bool stripAuth =
StaticPrefs::network_fetch_redirect_stripAuthHeader() &&
NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags);
OnRedirectVerifyCallback(NS_OK, stripAuth);

View file

@ -9,6 +9,7 @@
--blinterp-eager
--cache-ir-stubs=off
--cache-ir-stubs=on
--enable-json-parse-with-source
--ion-check-range-analysis
--ion-eager
--ion-edgecase-analysis=off

View file

@ -20,10 +20,10 @@ diff --git a/test/test_duplex.cpp b/test/test_duplex.cpp
#define STREAM_FORMAT CUBEB_SAMPLE_FLOAT32LE
#define INPUT_CHANNELS 1
#define INPUT_LAYOUT CUBEB_LAYOUT_MONO
@@ -201,16 +203,18 @@ TEST(cubeb, duplex_collection_change)
ASSERT_EQ(r, CUBEB_OK);
@@ -202,16 +204,18 @@ TEST(cubeb, duplex_collection_change)
}
#ifdef GTEST_HAS_DEATH_TEST
TEST(cubeb, duplex_collection_change_no_unregister)
{
cubeb * ctx;

View file

@ -1,33 +0,0 @@
From 68ef0eb5691aa7b9d634b4d1af85f9d66fdfc06e Mon Sep 17 00:00:00 2001
From: Mike Hommey <mh@glandium.org>
Date: Thu, 7 Mar 2024 08:01:32 +0900
Subject: [PATCH] Only build duplex_collection_change_no_unregister when death
tests are supported
---
test/test_duplex.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/test/test_duplex.cpp b/test/test_duplex.cpp
index 518f44f..98a6701 100644
--- a/test/test_duplex.cpp
+++ b/test/test_duplex.cpp
@@ -201,6 +201,7 @@ TEST(cubeb, duplex_collection_change)
ASSERT_EQ(r, CUBEB_OK);
}
+#ifdef GTEST_HAS_DEATH_TEST
TEST(cubeb, duplex_collection_change_no_unregister)
{
cubeb * ctx;
@@ -221,6 +222,7 @@ TEST(cubeb, duplex_collection_change_no_unregister)
duplex_collection_change_impl(ctx);
}
+#endif
long
data_cb_input(cubeb_stream * stream, void * user, const void * inputbuffer,
--
2.44.0.1.g9765aa7075

View file

@ -21,7 +21,7 @@ diff --git a/test/test_duplex.cpp b/test/test_duplex.cpp
#include "common.h"
#define SAMPLE_FREQUENCY 48000
@@ -294,16 +296,28 @@ TEST(cubeb, one_duplex_one_input)
@@ -294,16 +296,27 @@ TEST(cubeb, one_duplex_one_input)
cubeb * ctx;
cubeb_stream * duplex_stream;
cubeb_stream_params input_params;

View file

@ -1,57 +0,0 @@
From 19fcbefe1a9c5e22f8111af251df27b41658bc77 Mon Sep 17 00:00:00 2001
From: John Lin <jolin@mozilla.com>
Date: Mon, 29 Apr 2024 13:46:57 -0700
Subject: [PATCH] Invalidate timing info buffers when destorying AAudio stream.
aaudio_stream_get_position() returns incorrect result because
aaudio_stream_init() recycled destroyed stream where the
timing_info buffers contain stale data.
---
src/cubeb_aaudio.cpp | 2 ++
src/cubeb_triple_buffer.h | 7 +++++++
test/test_triple_buffer.cpp | 3 +++
3 files changed, 12 insertions(+)
diff --git a/src/cubeb_aaudio.cpp b/src/cubeb_aaudio.cpp
index cfae2d6f..8b5eb231 100644
--- a/src/cubeb_aaudio.cpp
+++ b/src/cubeb_aaudio.cpp
@@ -1049,6 +1049,8 @@ aaudio_stream_destroy_locked(cubeb_stream * stm, lock_guard<mutex> & lock)
stm->istream = nullptr;
}
+ stm->timing_info.invalidate();
+
if (stm->resampler) {
cubeb_resampler_destroy(stm->resampler);
stm->resampler = nullptr;
diff --git a/src/cubeb_triple_buffer.h b/src/cubeb_triple_buffer.h
index a5a5978f..759b92e6 100644
--- a/src/cubeb_triple_buffer.h
+++ b/src/cubeb_triple_buffer.h
@@ -42,6 +42,13 @@ template <typename T> class triple_buffer {
{
return (shared_state.load(std::memory_order_relaxed) & BACK_DIRTY_BIT) != 0;
}
+ // Reset state and indices to initial values.
+ void invalidate()
+ {
+ shared_state.store(0, std::memory_order_release);
+ input_idx = 1;
+ output_idx = 2;
+ }
private:
// Publish a value to the consumer. Returns true if the data was overwritten
diff --git a/test/test_triple_buffer.cpp b/test/test_triple_buffer.cpp
index a6e0049b..d463c07e 100644
--- a/test/test_triple_buffer.cpp
+++ b/test/test_triple_buffer.cpp
@@ -64,4 +64,7 @@ TEST(cubeb, triple_buffer)
}
t.join();
+
+ buffer.invalidate();
+ ASSERT_FALSE(buffer.updated());
}

View file

@ -9,8 +9,8 @@ origin:
description: "Cross platform audio library"
url: https://github.com/mozilla/cubeb
license: ISC
release: 74d6b0546576d9ee13a078ed51f87a6eede62014 (2024-02-15T12:42:53Z).
revision: 74d6b0546576d9ee13a078ed51f87a6eede62014
release: 6c1a6e151c1f981a2800d40af7c041cfcccc710e (2024-05-21T16:11:12Z).
revision: 6c1a6e151c1f981a2800d40af7c041cfcccc710e
vendoring:
url: https://github.com/mozilla/cubeb
@ -19,10 +19,8 @@ vendoring:
patches:
- 0001-disable-aaudio-before-android-31.patch
- 0002-disable-crash-reporter-death-test.patch
- 0003-Only-build-duplex_collection_change_no_unregister-wh.patch
- 0004-audiounit-ios-compile-fixes.patch
- 0005-aaudio-timing-fix.patch
- 0006-disable-cubeb_one_duplex_one_input-macos10.15.patch
- 0003-audiounit-ios-compile-fixes.patch
- 0004-disable-cubeb_one_duplex_one_input-macos10.15.patch
skip-vendoring-steps:
- update-moz-build
exclude:

View file

@ -95,7 +95,7 @@ validate_stream_params(cubeb_stream_params * input_stream_params,
XASSERT(input_stream_params || output_stream_params);
if (output_stream_params) {
if (output_stream_params->rate < 1000 ||
output_stream_params->rate > 384000 ||
output_stream_params->rate > 768000 ||
output_stream_params->channels < 1 ||
output_stream_params->channels > UINT8_MAX) {
return CUBEB_ERROR_INVALID_FORMAT;
@ -103,7 +103,7 @@ validate_stream_params(cubeb_stream_params * input_stream_params,
}
if (input_stream_params) {
if (input_stream_params->rate < 1000 ||
input_stream_params->rate > 384000 ||
input_stream_params->rate > 768000 ||
input_stream_params->channels < 1 ||
input_stream_params->channels > UINT8_MAX) {
return CUBEB_ERROR_INVALID_FORMAT;

View file

@ -245,13 +245,24 @@ shutdown_with_error(cubeb_stream * stm)
}
int64_t poll_frequency_ns = NS_PER_S * stm->out_frame_size / stm->sample_rate;
int rv;
if (stm->istream) {
wait_for_state_change(stm->istream, AAUDIO_STREAM_STATE_STOPPED,
poll_frequency_ns);
rv = wait_for_state_change(stm->istream, AAUDIO_STREAM_STATE_STOPPED,
poll_frequency_ns);
if (rv != CUBEB_OK) {
LOG("Failure when waiting for stream change on the input side when "
"shutting down in error");
// Not much we can do, carry on
}
}
if (stm->ostream) {
wait_for_state_change(stm->ostream, AAUDIO_STREAM_STATE_STOPPED,
poll_frequency_ns);
rv = wait_for_state_change(stm->ostream, AAUDIO_STREAM_STATE_STOPPED,
poll_frequency_ns);
if (rv != CUBEB_OK) {
LOG("Failure when waiting for stream change on the output side when "
"shutting down in error");
// Not much we can do, carry on
}
}
assert(!stm->in_data_callback.load());
@ -921,7 +932,7 @@ aaudio_error_cb(AAudioStream * astream, void * user_data, aaudio_result_t error)
assert(stm->ostream == astream || stm->istream == astream);
// Device change -- reinitialize on the new default device.
if (error == AAUDIO_ERROR_DISCONNECTED) {
if (error == AAUDIO_ERROR_DISCONNECTED || error == AAUDIO_ERROR_TIMEOUT) {
LOG("Audio device change, reinitializing stream");
reinitialize_stream(stm);
return;

View file

@ -0,0 +1,231 @@
/*
* Copyright © 2023 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#define NOMINMAX
#include "cubeb_audio_dump.h"
#include "cubeb/cubeb.h"
#include "cubeb_ringbuffer.h"
#include <chrono>
#include <limits>
#include <thread>
#include <vector>
using std::thread;
using std::vector;
uint32_t
bytes_per_sample(cubeb_stream_params params)
{
switch (params.format) {
case CUBEB_SAMPLE_S16LE:
case CUBEB_SAMPLE_S16BE:
return sizeof(int16_t);
case CUBEB_SAMPLE_FLOAT32LE:
case CUBEB_SAMPLE_FLOAT32BE:
return sizeof(float);
};
}
struct cubeb_audio_dump_stream {
public:
explicit cubeb_audio_dump_stream(cubeb_stream_params params)
: sample_size(bytes_per_sample(params)),
ringbuffer(
static_cast<int>(params.rate * params.channels * sample_size))
{
}
int open(const char * name)
{
file = fopen(name, "wb");
if (!file) {
return CUBEB_ERROR;
}
return CUBEB_OK;
}
int close()
{
if (fclose(file)) {
return CUBEB_ERROR;
}
return CUBEB_OK;
}
// Directly write to the file. Useful to write the header.
size_t write(uint8_t * data, uint32_t count)
{
return fwrite(data, count, 1, file);
}
size_t write_all()
{
size_t written = 0;
const int buf_sz = 16 * 1024;
uint8_t buf[buf_sz];
while (int rv = ringbuffer.dequeue(buf, buf_sz)) {
written += fwrite(buf, rv, 1, file);
}
return written;
}
int dump(void * samples, uint32_t count)
{
int bytes = static_cast<int>(count * sample_size);
int rv = ringbuffer.enqueue(static_cast<uint8_t *>(samples), bytes);
return rv == bytes;
}
private:
uint32_t sample_size;
FILE * file{};
lock_free_queue<uint8_t> ringbuffer;
};
struct cubeb_audio_dump_session {
public:
cubeb_audio_dump_session() = default;
~cubeb_audio_dump_session()
{
assert(streams.empty());
session_thread.join();
}
cubeb_audio_dump_session(const cubeb_audio_dump_session &) = delete;
cubeb_audio_dump_session &
operator=(const cubeb_audio_dump_session &) = delete;
cubeb_audio_dump_session & operator=(cubeb_audio_dump_session &&) = delete;
cubeb_audio_dump_stream_t create_stream(cubeb_stream_params params,
const char * name)
{
if (running) {
return nullptr;
}
auto * stream = new cubeb_audio_dump_stream(params);
streams.push_back(stream);
int rv = stream->open(name);
if (rv != CUBEB_OK) {
delete stream;
return nullptr;
}
struct riff_header {
char chunk_id[4] = {'R', 'I', 'F', 'F'};
int32_t chunk_size = 0;
char format[4] = {'W', 'A', 'V', 'E'};
char subchunk_id_1[4] = {'f', 'm', 't', 0x20};
int32_t subchunk_1_size = 16;
int16_t audio_format{};
int16_t num_channels{};
int32_t sample_rate{};
int32_t byte_rate{};
int16_t block_align{};
int16_t bits_per_sample{};
char subchunk_id_2[4] = {'d', 'a', 't', 'a'};
int32_t subchunkd_2_size = std::numeric_limits<int32_t>::max();
};
riff_header header;
// 1 is integer PCM, 3 is float PCM
header.audio_format = bytes_per_sample(params) == 2 ? 1 : 3;
header.num_channels = params.channels;
header.sample_rate = params.rate;
header.byte_rate = bytes_per_sample(params) * params.rate * params.channels;
header.block_align = params.channels * bytes_per_sample(params);
header.bits_per_sample = bytes_per_sample(params) * 8;
stream->write(reinterpret_cast<uint8_t *>(&header), sizeof(riff_header));
return stream;
}
int delete_stream(cubeb_audio_dump_stream * stream)
{
assert(!running);
stream->close();
streams.erase(std::remove(streams.begin(), streams.end(), stream),
streams.end());
return CUBEB_OK;
}
int start()
{
assert(!running);
running = true;
session_thread = std::thread([this] {
while (running) {
for (auto * stream : streams) {
stream->write_all();
}
const int DUMP_INTERVAL = 10;
std::this_thread::sleep_for(std::chrono::milliseconds(DUMP_INTERVAL));
}
});
return CUBEB_OK;
}
int stop()
{
assert(running);
running = false;
return CUBEB_OK;
}
private:
thread session_thread;
vector<cubeb_audio_dump_stream_t> streams{};
std::atomic<bool> running = false;
};
int
cubeb_audio_dump_init(cubeb_audio_dump_session_t * session)
{
*session = new cubeb_audio_dump_session;
return CUBEB_OK;
}
int
cubeb_audio_dump_shutdown(cubeb_audio_dump_session_t session)
{
delete session;
return CUBEB_OK;
}
int
cubeb_audio_dump_stream_init(cubeb_audio_dump_session_t session,
cubeb_audio_dump_stream_t * stream,
cubeb_stream_params stream_params,
const char * name)
{
*stream = session->create_stream(stream_params, name);
return CUBEB_OK;
}
int
cubeb_audio_dump_stream_shutdown(cubeb_audio_dump_session_t session,
cubeb_audio_dump_stream_t stream)
{
return session->delete_stream(stream);
}
int
cubeb_audio_dump_start(cubeb_audio_dump_session_t session)
{
return session->start();
}
int
cubeb_audio_dump_stop(cubeb_audio_dump_session_t session)
{
return session->stop();
}
int
cubeb_audio_dump_write(cubeb_audio_dump_stream_t stream, void * audio_samples,
uint32_t count)
{
stream->dump(audio_samples, count);
return CUBEB_OK;
}

View file

@ -0,0 +1,108 @@
/*
* Copyright © 2023 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#ifndef CUBEB_AUDIO_DUMP
#define CUBEB_AUDIO_DUMP
#include "cubeb/cubeb.h"
#if defined(__cplusplus)
extern "C" {
#endif
typedef struct cubeb_audio_dump_stream * cubeb_audio_dump_stream_t;
typedef struct cubeb_audio_dump_session * cubeb_audio_dump_session_t;
// Start audio dumping session
// This can only be called if the other API functions
// aren't currently being called: synchronized externally.
// This is not real-time safe.
//
// This is generally called when deciding to start logging some audio.
//
// Returns 0 in case of success.
int
cubeb_audio_dump_init(cubeb_audio_dump_session_t * session);
// End audio dumping session
// This can only be called if the other API functions
// aren't currently being called: synchronized externally.
//
// This is generally called when deciding to stop logging some audio.
//
// This is not real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_shutdown(cubeb_audio_dump_session_t session);
// Register a stream for dumping to a file
// This can only be called if cubeb_audio_dump_write
// isn't currently being called: synchronized externally.
//
// This is generally called when setting up a system-level stream side (either
// input or output).
//
// This is not real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_stream_init(cubeb_audio_dump_session_t session,
cubeb_audio_dump_stream_t * stream,
cubeb_stream_params stream_params,
const char * name);
// Unregister a stream for dumping to a file
// This can only be called if cubeb_audio_dump_write
// isn't currently being called: synchronized externally.
//
// This is generally called when a system-level audio stream side
// (input/output) has been stopped and drained, and the audio callback isn't
// going to be called.
//
// This is not real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_stream_shutdown(cubeb_audio_dump_session_t session,
cubeb_audio_dump_stream_t stream);
// Start dumping.
// cubeb_audio_dump_write can now be called.
//
// This starts dumping the audio to disk. Generally this is called when
// cubeb_stream_start is caled is called, but can be called at the beginning of
// the application.
//
// This is not real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_start(cubeb_audio_dump_session_t session);
// Stop dumping.
// cubeb_audio_dump_write can't be called at this point.
//
// This stops dumping the audio to disk cubeb_stream_stop is caled is called,
// but can be called before exiting the application.
//
// This is not real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_stop(cubeb_audio_dump_session_t session);
// Dump some audio samples for audio stream id.
//
// This is generally called from the real-time audio callback.
//
// This is real-time safe.
// Returns 0 in case of success.
int
cubeb_audio_dump_write(cubeb_audio_dump_stream_t stream, void * audio_samples,
uint32_t count);
#ifdef __cplusplus
};
#endif
#endif

View file

@ -0,0 +1,74 @@
/*
* Copyright © 2023 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#define NOMINMAX
#define _USE_MATH_DEFINES
#include "cubeb/cubeb.h"
#include <ratio>
#include "cubeb_audio_dump.h"
#include "gtest/gtest.h"
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <thread>
TEST(cubeb, audio_dump)
{
cubeb_audio_dump_session_t session;
int rv = cubeb_audio_dump_init(&session);
ASSERT_EQ(rv, 0);
cubeb_stream_params params;
params.rate = 44100;
params.channels = 2;
params.format = CUBEB_SAMPLE_FLOAT32NE;
cubeb_audio_dump_stream_t dump_stream;
rv = cubeb_audio_dump_stream_init(session, &dump_stream, params, "test.wav");
ASSERT_EQ(rv, 0);
rv = cubeb_audio_dump_start(session);
ASSERT_EQ(rv, 0);
float phase = 0;
const size_t buf_sz = 2 * 44100 / 50;
float buf[buf_sz];
for (uint32_t iteration = 0; iteration < 50; iteration++) {
uint32_t write_idx = 0;
for (uint32_t i = 0; i < buf_sz / params.channels; i++) {
for (uint32_t j = 0; j < params.channels; j++) {
buf[write_idx++] = sin(phase);
}
phase += 440 * M_PI * 2 / 44100;
if (phase > 2 * M_PI) {
phase -= 2 * M_PI;
}
}
rv = cubeb_audio_dump_write(dump_stream, buf, 2 * 44100 / 50);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
ASSERT_EQ(rv, 0);
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
rv = cubeb_audio_dump_stop(session);
ASSERT_EQ(rv, 0);
rv = cubeb_audio_dump_stream_shutdown(session, dump_stream);
ASSERT_EQ(rv, 0);
rv = cubeb_audio_dump_shutdown(session);
ASSERT_EQ(rv, 0);
std::ifstream file("test.wav");
ASSERT_TRUE(file.good());
}
#undef NOMINMAX

View file

@ -153,9 +153,10 @@ class PermissionsDialogFragment : AddonDialogFragment() {
},
addon.translateName(requireContext()),
)
rootView.findViewById<TextView>(R.id.optional_or_required_text).text = buildOptionalOrRequiredText()
val listPermissions = buildPermissionsList()
rootView.findViewById<TextView>(R.id.optional_or_required_text).text =
buildOptionalOrRequiredText(listPermissions.isNotEmpty())
val permissionsRecyclerView = rootView.findViewById<RecyclerView>(R.id.permissions)
val positiveButton = rootView.findViewById<Button>(R.id.allow_button)
val negativeButton = rootView.findViewById<Button>(R.id.deny_button)
@ -211,7 +212,10 @@ class PermissionsDialogFragment : AddonDialogFragment() {
}
@VisibleForTesting
internal fun buildOptionalOrRequiredText(): String {
internal fun buildOptionalOrRequiredText(hasPermissions: Boolean): String {
if (!hasPermissions) {
return ""
}
val optionalOrRequiredText = if (forOptionalPermissions) {
getString(R.string.mozac_feature_addons_optional_permissions_dialog_subtitle)
} else {

View file

@ -48,8 +48,8 @@ class PermissionsDialogFragmentTest {
val optionalOrRequiredTextView = dialog.findViewById<TextView>(R.id.optional_or_required_text)
val permissionsRecyclerView = dialog.findViewById<RecyclerView>(R.id.permissions)
val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
val permissionList = fragment.buildPermissionsList()
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText(hasPermissions = permissionList.isNotEmpty())
assertTrue(titleTextView.text.contains(name))
assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
@ -152,12 +152,12 @@ class PermissionsDialogFragmentTest {
val optionalOrRequiredTextView = dialog.findViewById<TextView>(R.id.optional_or_required_text)
val permissionsRecyclerView = dialog.findViewById<RecyclerView>(R.id.permissions)
val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
val permissionList = fragment.buildPermissionsList()
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText(hasPermissions = permissionList.isNotEmpty())
assertTrue(titleTextView.text.contains(name))
assertTrue(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
assertTrue(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
assertFalse(optionalOrRequiredText.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
assertFalse(optionalOrRequiredTextView.text.contains(testContext.getString(R.string.mozac_feature_addons_permissions_dialog_subtitle)))
assertEquals(0, recyclerAdapter.itemCount)
assertFalse(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_privacy_description)))
assertFalse(permissionList.contains(testContext.getString(R.string.mozac_feature_addons_permissions_all_urls_description)))
@ -184,8 +184,8 @@ class PermissionsDialogFragmentTest {
val recyclerAdapter = permissionsRecyclerView.adapter!! as RequiredPermissionsAdapter
val allowButton = dialog.findViewById<Button>(R.id.allow_button)
val denyButton = dialog.findViewById<Button>(R.id.deny_button)
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText()
val permissionsList = fragment.buildPermissionsList()
val optionalOrRequiredText = fragment.buildOptionalOrRequiredText(hasPermissions = permissionsList.isNotEmpty())
assertEquals(
titleTextView.text,

View file

@ -173,7 +173,7 @@ class AppLinksFeature(
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun isSameCallerAndApp(tab: SessionState, appIntent: Intent): Boolean {
return (tab.source as? SessionState.Source.External)?.let { externalSource ->
return (tab.source as? SessionState.Source.External.CustomTab)?.let { externalSource ->
when (externalSource.caller?.packageId) {
null -> false
appIntent.component?.packageName -> true

View file

@ -222,6 +222,42 @@ class AppLinksFeatureTest {
verify(mockDialog, never()).showNow(eq(mockFragmentManager), anyString())
}
@Test
fun `WHEN non-custom tab and caller is the same as external app THEN an external app dialog is shown`() {
feature = spy(
AppLinksFeature(
context = mockContext,
store = store,
fragmentManager = mockFragmentManager,
useCases = mockUseCases,
dialog = mockDialog,
loadUrlUseCase = mockLoadUrlUseCase,
shouldPrompt = { true },
),
).also {
it.start()
}
val tab =
createCustomTab(
id = "d",
url = webUrl,
source = SessionState.Source.External.ActionView(
ExternalPackage("com.zxing.app", PackageCategory.PRODUCTIVITY),
),
)
val appIntent: Intent = mock()
val componentName: ComponentName = mock()
doReturn(componentName).`when`(appIntent).component
doReturn("com.zxing.app").`when`(componentName).packageName
feature.handleAppIntent(tab, intentUrl, appIntent)
verify(mockDialog).showNow(eq(mockFragmentManager), anyString())
verify(mockOpenRedirect, never()).invoke(any(), anyBoolean(), any())
}
@Test
fun `WHEN should prompt and in private mode THEN an external app dialog is shown`() {
feature = spy(

View file

@ -39,6 +39,7 @@ import org.mozilla.fenix.components.menu.compose.TOOLS_MENU_ROUTE
import org.mozilla.fenix.components.menu.compose.ToolsSubmenu
import org.mozilla.fenix.components.menu.middleware.MenuDialogMiddleware
import org.mozilla.fenix.components.menu.middleware.MenuNavigationMiddleware
import org.mozilla.fenix.components.menu.middleware.MenuTelemetryMiddleware
import org.mozilla.fenix.components.menu.store.BrowserMenuState
import org.mozilla.fenix.components.menu.store.MenuAction
import org.mozilla.fenix.components.menu.store.MenuState
@ -109,6 +110,9 @@ class MenuDialogFragment : BottomSheetDialogFragment() {
openToBrowser = ::openToBrowser,
scope = coroutineScope,
),
MenuTelemetryMiddleware(
accessPoint = args.accesspoint,
),
),
)
}

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/. */
package org.mozilla.fenix.components.menu.middleware
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.AppMenu
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeMenu
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.Translations
import org.mozilla.fenix.components.menu.MenuAccessPoint
import org.mozilla.fenix.components.menu.store.MenuAction
import org.mozilla.fenix.components.menu.store.MenuState
import org.mozilla.fenix.components.menu.store.MenuStore
/**
* A [Middleware] for recording telemetry based on [MenuAction]s that are dispatch to the
* [MenuStore].
*
* @param accessPoint The [MenuAccessPoint] that was used to navigate to the menu dialog.
*/
class MenuTelemetryMiddleware(
private val accessPoint: MenuAccessPoint,
) : Middleware<MenuState, MenuAction> {
@Suppress("CyclomaticComplexMethod", "LongMethod")
override fun invoke(
context: MiddlewareContext<MenuState, MenuAction>,
next: (MenuAction) -> Unit,
action: MenuAction,
) {
next(action)
when (action) {
MenuAction.AddBookmark,
MenuAction.Navigate.EditBookmark,
-> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "bookmark",
),
)
MenuAction.Navigate.Bookmarks -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "bookmarks",
),
)
MenuAction.Navigate.CustomizeHomepage -> {
AppMenu.customizeHomepage.record(NoExtras())
HomeScreen.customizeHomeClicked.record(NoExtras())
}
MenuAction.Navigate.Downloads -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "downloads",
),
)
MenuAction.Navigate.Help -> HomeMenu.helpTapped.record(NoExtras())
MenuAction.Navigate.History -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "history",
),
)
MenuAction.Navigate.ManageExtensions -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "addons_manager",
),
)
is MenuAction.Navigate.MozillaAccount -> {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra(item = "sync_account"))
AppMenu.signIntoSync.add()
}
MenuAction.Navigate.NewTab -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "new_tab",
),
)
MenuAction.Navigate.Passwords -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "passwords",
),
)
MenuAction.Navigate.ReleaseNotes -> Events.whatsNewTapped.record(NoExtras())
MenuAction.Navigate.Settings -> {
when (accessPoint) {
MenuAccessPoint.Browser -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "settings",
),
)
MenuAccessPoint.Home -> HomeMenu.settingsItemClicked.record(NoExtras())
MenuAccessPoint.External -> Unit
}
}
MenuAction.Navigate.Share -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "share",
),
)
MenuAction.Navigate.Translate -> {
Translations.action.record(Translations.ActionExtra(item = "main_flow_browser"))
Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
item = "translate",
),
)
}
MenuAction.InitAction,
MenuAction.Navigate.Back,
MenuAction.Navigate.DiscoverMoreExtensions,
MenuAction.Navigate.Extensions,
MenuAction.Navigate.NewPrivateTab,
MenuAction.Navigate.Save,
MenuAction.Navigate.Tools,
is MenuAction.UpdateBookmarkState,
-> Unit
}
}
}

View file

@ -115,6 +115,7 @@ fun TextListItem(
* an optional [IconButton] at the end.
*
* @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param description An optional description text below the label.
* @param faviconPainter Optional painter to use when fetching a new favicon is unnecessary.
* @param onClick Called when the user clicks on the item.
@ -126,6 +127,7 @@ fun TextListItem(
@Composable
fun FaviconListItem(
label: String,
modifier: Modifier = Modifier,
description: String? = null,
faviconPainter: Painter? = null,
onClick: (() -> Unit)? = null,
@ -136,6 +138,7 @@ fun FaviconListItem(
) {
ListItem(
label = label,
modifier = modifier,
description = description,
onClick = onClick,
beforeListAction = {
@ -179,6 +182,7 @@ fun FaviconListItem(
* text and an optional [IconButton] or [Icon] at the end.
*
* @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param labelTextColor [Color] to be applied to the label.
* @param description An optional description text below the label.
* @param enabled Controls the enabled state of the list item. When `false`, the list item will not
@ -196,6 +200,7 @@ fun FaviconListItem(
@Composable
fun IconListItem(
label: String,
modifier: Modifier = Modifier,
labelTextColor: Color = FirefoxTheme.colors.textPrimary,
description: String? = null,
enabled: Boolean = true,
@ -210,6 +215,7 @@ fun IconListItem(
) {
ListItem(
label = label,
modifier = modifier,
labelTextColor = labelTextColor,
description = description,
enabled = enabled,
@ -256,6 +262,7 @@ fun IconListItem(
* text and an optional [TextButton] at the end.
*
* @param label The label in the list item.
* @param modifier [Modifier] to be applied to the layout.
* @param labelTextColor [Color] to be applied to the label.
* @param description An optional description text below the label.
* @param enabled Controls the enabled state of the list item. When `false`, the list item will not
@ -271,6 +278,7 @@ fun IconListItem(
@Composable
fun IconListItem(
label: String,
modifier: Modifier = Modifier,
labelTextColor: Color = FirefoxTheme.colors.textPrimary,
description: String? = null,
enabled: Boolean = true,
@ -284,6 +292,7 @@ fun IconListItem(
) {
ListItem(
label = label,
modifier = modifier,
labelTextColor = labelTextColor,
description = description,
enabled = enabled,

View file

@ -5,27 +5,22 @@
package org.mozilla.fenix.home.recentvisits.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowColumn
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -34,22 +29,22 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.support.ktx.kotlin.trimmed
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.ext.thenConditional
import org.mozilla.fenix.compose.list.FaviconListItem
import org.mozilla.fenix.compose.list.IconListItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
@ -58,28 +53,10 @@ import org.mozilla.fenix.theme.FirefoxTheme
// Number of recently visited items per column.
private const val VISITS_PER_COLUMN = 3
private val itemRowHeight = 56.dp
private val recentlyVisitedItemMaxWidth = 320.dp
private val horizontalArrangementSpacing = 32.dp
private val contentPadding = 16.dp
private val imageSize = 24.dp
private val imageSpacer = 16.dp
private val textSpacer = 2.dp
/**
* The [Dp] width of UI elements to deduct from the screen width for a single column.
*
* Box start padding, Row start padding, Box end padding, Row end padding.
*/
private val singleColumnWidth: Dp = contentPadding * 4
/**
* The [Dp] width of UI elements to deduct from the screen width for multiple columns to show (peek) the
* second column icons.
*
* Box start padding, Row start padding, Spacer, Image size, Image spacer.
*/
private val multipleColumnsWidth: Dp =
contentPadding + contentPadding + horizontalArrangementSpacing + imageSize + imageSpacer
/**
* A list of recently visited items.
@ -87,56 +64,82 @@ private val multipleColumnsWidth: Dp =
* @param recentVisits List of [RecentlyVisitedItem] to display.
* @param menuItems List of [RecentVisitMenuItem] shown long clicking a [RecentlyVisitedItem].
* @param backgroundColor The background [Color] of each item.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit. The first parameter is
* the [RecentlyVisitedItem] that was clicked and the second parameter is the "page" or column number
* the item resides in.
*/
@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun RecentlyVisited(
recentVisits: List<RecentlyVisitedItem>,
menuItems: List<RecentVisitMenuItem>,
backgroundColor: Color = FirefoxTheme.colors.layer2,
onRecentVisitClick: (RecentlyVisitedItem, Int) -> Unit = { _, _ -> },
onRecentVisitClick: (RecentlyVisitedItem, pageNumber: Int) -> Unit = { _, _ -> },
) {
val itemsMatrix: List<List<RecentlyVisitedItem>> = recentVisits.chunked(VISITS_PER_COLUMN)
val isSingleColumn by remember(recentVisits) { derivedStateOf { recentVisits.size <= VISITS_PER_COLUMN } }
BoxWithConstraints(
Row(
modifier = Modifier
.fillMaxWidth()
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits"
},
.thenConditional(
modifier = Modifier.horizontalScroll(state = rememberScrollState()),
predicate = { !isSingleColumn },
)
.padding(
horizontal = contentPadding,
vertical = 8.dp,
),
) {
val boxWithConstraintsScope = this
val isSingleColumn = itemsMatrix.size == 1
val widthToDeduct = if (isSingleColumn) {
singleColumnWidth
} else {
multipleColumnsWidth
}
val rowWidth = boxWithConstraintsScope.maxWidth - widthToDeduct
LazyRow(
Card(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = contentPadding),
shape = RoundedCornerShape(8.dp),
backgroundColor = backgroundColor,
elevation = 6.dp,
) {
item {
RecentlyVisitedCard(backgroundColor) {
Row(modifier = Modifier.padding(contentPadding)) {
itemsMatrix.mapIndexed { pageIndex, items ->
RecentlyVisitedColumn(
modifier = Modifier.width(rowWidth),
FlowColumn(
modifier = Modifier.fillMaxWidth(),
maxItemsInEachColumn = VISITS_PER_COLUMN,
horizontalArrangement = Arrangement.spacedBy(horizontalArrangementSpacing),
) {
recentVisits.forEachIndexed { index, recentVisit ->
// Don't display the divider when its the last item in a column or the last item
// in the table.
val showDivider = (index + 1) % VISITS_PER_COLUMN != 0 &&
index != recentVisits.lastIndex
val pageIndex = index / VISITS_PER_COLUMN
val pageNumber = pageIndex + 1
Box(
modifier = if (isSingleColumn) {
Modifier.fillMaxWidth()
} else {
Modifier.widthIn(max = recentlyVisitedItemMaxWidth)
},
) {
when (recentVisit) {
is RecentHistoryHighlight -> RecentlyVisitedHistoryHighlight(
recentVisit = recentVisit,
menuItems = menuItems,
items = items,
pageIndex = pageIndex,
onRecentVisitClick = onRecentVisitClick,
onRecentVisitClick = {
onRecentVisitClick(it, pageNumber)
},
)
val isLastColumn = pageIndex == itemsMatrix.lastIndex
if (!isLastColumn) {
Spacer(modifier = Modifier.width(horizontalArrangementSpacing))
}
is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
recentVisit = recentVisit,
menuItems = menuItems,
onRecentVisitClick = {
onRecentVisitClick(it, pageNumber)
},
)
}
if (showDivider) {
Divider(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(horizontal = contentPadding),
)
}
}
}
@ -145,57 +148,11 @@ fun RecentlyVisited(
}
}
@Composable
private fun RecentlyVisitedCard(backgroundColor: Color, content: @Composable () -> Unit) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
backgroundColor = backgroundColor,
elevation = 6.dp,
) {
content()
}
}
@Composable
private fun RecentlyVisitedColumn(
modifier: Modifier,
menuItems: List<RecentVisitMenuItem>,
items: List<RecentlyVisitedItem>,
pageIndex: Int,
onRecentVisitClick: (RecentlyVisitedItem, Int) -> Unit = { _, _ -> },
) {
Column(modifier = modifier) {
items.forEachIndexed { index, recentVisit ->
when (recentVisit) {
is RecentHistoryHighlight -> RecentlyVisitedHistoryHighlight(
recentVisit = recentVisit,
menuItems = menuItems,
showDividerLine = index < items.size - 1,
onRecentVisitClick = {
onRecentVisitClick(it, pageIndex + 1)
},
)
is RecentHistoryGroup -> RecentlyVisitedHistoryGroup(
recentVisit = recentVisit,
menuItems = menuItems,
showDividerLine = index < items.size - 1,
onRecentVisitClick = {
onRecentVisitClick(it, pageIndex + 1)
},
)
}
}
}
}
/**
* A recently visited history group.
*
* @param recentVisit The [RecentHistoryGroup] to display.
* @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
* @param showDividerLine Whether to show a divider line at the bottom.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
*/
@OptIn(
@ -206,130 +163,26 @@ private fun RecentlyVisitedColumn(
private fun RecentlyVisitedHistoryGroup(
recentVisit: RecentHistoryGroup,
menuItems: List<RecentVisitMenuItem>,
showDividerLine: Boolean,
onRecentVisitClick: (RecentHistoryGroup) -> Unit = { _ -> },
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.combinedClickable(
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = { isMenuExpanded = true },
)
.height(itemRowHeight)
.fillMaxWidth()
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits.group"
},
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(R.drawable.ic_multiple_tabs),
contentDescription = null,
modifier = Modifier.size(imageSize),
)
Spacer(modifier = Modifier.width(imageSpacer))
Box(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
) {
RecentlyVisitedTitle(
text = recentVisit.title,
modifier = Modifier
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits.group.title"
},
)
Spacer(modifier = Modifier.height(textSpacer))
RecentlyVisitedCaption(
count = recentVisit.historyMetadata.size,
modifier = Modifier
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits.group.caption"
},
)
}
if (showDividerLine) {
Divider(modifier = Modifier.align(Alignment.BottomCenter))
}
}
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { MenuItem(it.title) { it.onClick(recentVisit) } },
modifier = Modifier.semantics {
testTagsAsResourceId = true
testTag = "recent.visit.menu"
},
)
val captionId = if (recentVisit.historyMetadata.size == 1) {
R.string.history_search_group_site_1
} else {
R.string.history_search_group_sites_1
}
}
/**
* A recently visited history item.
*
* @param recentVisit The [RecentHistoryHighlight] to display.
* @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
* @param showDividerLine Whether to show a divider line at the bottom.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
*/
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalComposeUiApi::class,
)
@Composable
private fun RecentlyVisitedHistoryHighlight(
recentVisit: RecentHistoryHighlight,
menuItems: List<RecentVisitMenuItem>,
showDividerLine: Boolean,
onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
) {
var isMenuExpanded by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.combinedClickable(
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = { isMenuExpanded = true },
)
.height(itemRowHeight)
.fillMaxWidth()
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits.highlight"
},
verticalAlignment = Alignment.CenterVertically,
) {
Favicon(url = recentVisit.url, size = imageSize)
Spacer(modifier = Modifier.width(imageSpacer))
Box(modifier = Modifier.fillMaxSize()) {
RecentlyVisitedTitle(
text = recentVisit.title.trimmed(),
modifier = Modifier
.align(Alignment.CenterStart)
.semantics {
testTagsAsResourceId = true
testTag = "recent.visits.highlight.title"
},
)
if (showDividerLine) {
Divider(modifier = Modifier.align(Alignment.BottomCenter))
}
}
Box {
IconListItem(
label = recentVisit.title.trimmed(),
modifier = Modifier
.combinedClickable(
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = { isMenuExpanded = true },
),
beforeIconPainter = painterResource(R.drawable.ic_multiple_tabs),
description = stringResource(id = captionId, recentVisit.historyMetadata.size),
)
ContextualMenu(
showMenu = isMenuExpanded,
@ -344,69 +197,67 @@ private fun RecentlyVisitedHistoryHighlight(
}
/**
* The title of a recent visit.
* A recently visited history item.
*
* @param text [String] that will be display. Will be ellipsized if cannot fit on one line.
* @param modifier [Modifier] allowing to perfectly place this.
* @param recentVisit The [RecentHistoryHighlight] to display.
* @param menuItems List of [RecentVisitMenuItem] to display in a recent visit dropdown menu.
* @param onRecentVisitClick Invoked when the user clicks on a recent visit.
*/
@OptIn(
ExperimentalFoundationApi::class,
ExperimentalComposeUiApi::class,
)
@Composable
private fun RecentlyVisitedTitle(
text: String,
modifier: Modifier = Modifier,
private fun RecentlyVisitedHistoryHighlight(
recentVisit: RecentHistoryHighlight,
menuItems: List<RecentVisitMenuItem>,
onRecentVisitClick: (RecentHistoryHighlight) -> Unit = { _ -> },
) {
Text(
text = text,
modifier = modifier,
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
var isMenuExpanded by remember { mutableStateOf(false) }
/**
* The caption text for a recent visit.
*
* @param count Number of recently visited items to display in the caption.
* @param modifier [Modifier] allowing to perfectly place this.
*/
@Composable
private fun RecentlyVisitedCaption(
count: Int,
modifier: Modifier,
) {
val stringId = if (count == 1) {
R.string.history_search_group_site_1
} else {
R.string.history_search_group_sites_1
Box {
FaviconListItem(
label = recentVisit.title.trimmed(),
url = recentVisit.url,
modifier = Modifier
.combinedClickable(
onClick = { onRecentVisitClick(recentVisit) },
onLongClick = { isMenuExpanded = true },
),
)
ContextualMenu(
showMenu = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false },
menuItems = menuItems.map { item -> MenuItem(item.title) { item.onClick(recentVisit) } },
modifier = Modifier.semantics {
testTagsAsResourceId = true
testTag = "recent.visit.menu"
},
)
}
Text(
text = String.format(LocalContext.current.getString(stringId), count),
modifier = modifier,
color = when (isSystemInDarkTheme()) {
true -> FirefoxTheme.colors.textPrimary
false -> FirefoxTheme.colors.textSecondary
},
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
@Composable
@LightDarkPreview
private fun RecentlyVisitedMultipleColumnsPreview() {
FirefoxTheme {
RecentlyVisited(
recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"),
RecentHistoryGroup(title = "mozilla"),
RecentHistoryGroup(title = "firefox"),
RecentHistoryGroup(title = "pocket"),
),
menuItems = emptyList(),
)
Box(
modifier = Modifier
.background(color = FirefoxTheme.colors.layer1)
.padding(vertical = contentPadding),
) {
RecentlyVisited(
recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"),
RecentHistoryGroup(title = "mozilla"),
RecentHistoryGroup(title = "firefox"),
RecentHistoryGroup(title = "pocket"),
RecentHistoryHighlight(title = "Mozilla", url = "www.mozilla.com"),
),
menuItems = emptyList(),
)
}
}
}
@ -414,13 +265,24 @@ private fun RecentlyVisitedMultipleColumnsPreview() {
@LightDarkPreview
private fun RecentlyVisitedSingleColumnPreview() {
FirefoxTheme {
RecentlyVisited(
recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"),
RecentHistoryGroup(title = "mozilla"),
RecentHistoryGroup(title = "firefox"),
),
menuItems = emptyList(),
)
Box(
modifier = Modifier
.background(color = FirefoxTheme.colors.layer1)
.padding(vertical = contentPadding),
) {
RecentlyVisited(
recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"),
RecentHistoryHighlight(title = "Mozilla", url = "www.mozilla.com"),
),
menuItems = emptyList(),
)
}
}
}
@Composable
@Preview(widthDp = 250)
private fun RecentlyVisitedSingleColumnSmallPreview() {
RecentlyVisitedSingleColumnPreview()
}

View file

@ -29,6 +29,7 @@ import org.mozilla.fenix.components.menu.compose.EXTENSIONS_MENU_ROUTE
import org.mozilla.fenix.components.menu.compose.SAVE_MENU_ROUTE
import org.mozilla.fenix.components.menu.compose.TOOLS_MENU_ROUTE
import org.mozilla.fenix.components.menu.middleware.MenuNavigationMiddleware
import org.mozilla.fenix.components.menu.store.BookmarkState
import org.mozilla.fenix.components.menu.store.BrowserMenuState
import org.mozilla.fenix.components.menu.store.MenuAction
import org.mozilla.fenix.components.menu.store.MenuState
@ -258,6 +259,34 @@ class MenuNavigationMiddlewareTest {
verify { navHostController.popBackStack() }
}
@Test
fun `WHEN navigate to edit bookmark action is dispatched THEN navigate to bookmark edit fragment`() = runTest {
val tab = createTab(url = "https://www.mozilla.org")
val store = createStore(
menuState = MenuState(
browserMenuState = BrowserMenuState(
selectedTab = tab,
bookmarkState = BookmarkState(
guid = BookmarkRoot.Mobile.id,
isBookmarked = true,
),
),
),
)
store.dispatch(MenuAction.Navigate.EditBookmark).join()
verify {
navController.nav(
R.id.menuDialogFragment,
MenuDialogFragmentDirections.actionGlobalBookmarkEditFragment(
guidToEdit = BookmarkRoot.Mobile.id,
requiresSnackbarPaddingForToolbar = true,
),
)
}
}
@Test
fun `WHEN navigate to translate action is dispatched THEN navigate to translation dialog`() = runTest {
val tab = createTab(url = "https://www.mozilla.org")

View file

@ -0,0 +1,245 @@
/* 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/. */
package org.mozilla.fenix.components.menu
import mozilla.components.service.fxa.manager.AccountState
import mozilla.components.service.glean.private.EventMetricType
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.service.glean.testing.GleanTestRule
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.robolectric.testContext
import mozilla.telemetry.glean.internal.CounterMetric
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.AppMenu
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeMenu
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.Translations
import org.mozilla.fenix.components.menu.middleware.MenuTelemetryMiddleware
import org.mozilla.fenix.components.menu.store.MenuAction
import org.mozilla.fenix.components.menu.store.MenuState
import org.mozilla.fenix.components.menu.store.MenuStore
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class MenuTelemetryMiddlewareTest {
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
@Test
fun `WHEN adding a bookmark THEN record the bookmark browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.AddBookmark).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "bookmark")
}
@Test
fun `WHEN navigating to edit a bookmark THEN record the bookmark browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.EditBookmark).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "bookmark")
}
@Test
fun `WHEN navigating to bookmarks THEN record the bookmarks browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.Bookmarks).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "bookmarks")
}
@Test
fun `WHEN navigating to customize homepage THEN record the customize homepage telemetry`() {
val store = createStore()
assertNull(AppMenu.customizeHomepage.testGetValue())
assertNull(HomeScreen.customizeHomeClicked.testGetValue())
store.dispatch(MenuAction.Navigate.CustomizeHomepage).joinBlocking()
assertTelemetryRecorded(AppMenu.customizeHomepage)
assertTelemetryRecorded(HomeScreen.customizeHomeClicked)
}
@Test
fun `WHEN navigating to downloads THEN record the downloads browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.Downloads).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "downloads")
}
@Test
fun `WHEN navigating to the help SUMO page THEN record the help interaction telemetry`() {
val store = createStore()
assertNull(HomeMenu.helpTapped.testGetValue())
store.dispatch(MenuAction.Navigate.Help).joinBlocking()
assertTelemetryRecorded(HomeMenu.helpTapped)
}
@Test
fun `WHEN navigating to history THEN record the history browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.History).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "history")
}
@Test
fun `WHEN navigating to manage extensions THEN record the manage extensions browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.ManageExtensions).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "addons_manager")
}
@Test
fun `WHEN navigating to sync account THEN record the sync account browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(
MenuAction.Navigate.MozillaAccount(
accountState = AccountState.NotAuthenticated,
accesspoint = MenuAccessPoint.Browser,
),
).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "sync_account")
assertTelemetryRecorded(AppMenu.signIntoSync)
}
@Test
fun `WHEN opening a new tab THEN record the new tab browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.NewTab).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "new_tab")
}
@Test
fun `WHEN navigating to passwords THEN record the passwords browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.Passwords).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "passwords")
}
@Test
fun `WHEN navigating to the release notes page THEN record the whats new interaction telemetry`() {
val store = createStore()
assertNull(HomeMenu.helpTapped.testGetValue())
store.dispatch(MenuAction.Navigate.ReleaseNotes).joinBlocking()
assertTelemetryRecorded(Events.whatsNewTapped)
}
@Test
fun `WHEN navigating to the share sheet THEN record the share browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
store.dispatch(MenuAction.Navigate.Share).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "share")
}
@Test
fun `GIVEN the menu accesspoint is from the browser WHEN navigating to the settings THEN record the settings browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
assertNull(HomeMenu.settingsItemClicked.testGetValue())
store.dispatch(MenuAction.Navigate.Settings).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "settings")
assertNull(HomeMenu.settingsItemClicked.testGetValue())
}
@Test
fun `GIVEN the menu accesspoint is from the home screen WHEN navigating to the settings THEN record the home menu interaction telemetry`() {
val store = createStore(accessPoint = MenuAccessPoint.Home)
assertNull(Events.browserMenuAction.testGetValue())
assertNull(HomeMenu.settingsItemClicked.testGetValue())
store.dispatch(MenuAction.Navigate.Settings).joinBlocking()
assertTelemetryRecorded(HomeMenu.settingsItemClicked)
assertNull(Events.browserMenuAction.testGetValue())
}
@Test
fun `WHEN navigating to translations dialog THEN record the translate browser menu telemetry`() {
val store = createStore()
assertNull(Events.browserMenuAction.testGetValue())
assertNull(Translations.action.testGetValue())
store.dispatch(MenuAction.Navigate.Translate).joinBlocking()
assertTelemetryRecorded(Events.browserMenuAction, item = "translate")
val snapshot = Translations.action.testGetValue()!!
assertEquals(1, snapshot.size)
assertEquals("main_flow_browser", snapshot.single().extra?.getValue("item"))
}
private fun assertTelemetryRecorded(
event: EventMetricType<Events.BrowserMenuActionExtra>,
item: String,
) {
assertNotNull(event.testGetValue())
val snapshot = event.testGetValue()!!
assertEquals(1, snapshot.size)
assertEquals(item, snapshot.single().extra?.getValue("item"))
}
private fun assertTelemetryRecorded(event: EventMetricType<NoExtras>) {
assertNotNull(event.testGetValue())
assertEquals(1, event.testGetValue()!!.size)
}
private fun assertTelemetryRecorded(event: CounterMetric) {
assertNotNull(event.testGetValue())
assertEquals(1, event.testGetValue()!!)
}
private fun createStore(
menuState: MenuState = MenuState(),
accessPoint: MenuAccessPoint = MenuAccessPoint.Browser,
) = MenuStore(
initialState = menuState,
middleware = listOf(
MenuTelemetryMiddleware(
accessPoint = accessPoint,
),
),
)
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="status" role="status"></div>
</body>
</html>

View file

@ -1202,6 +1202,19 @@ class AccessibilityTest : BaseSessionTest() {
})
}
@Test fun testLiveRegionStatus() {
loadTestPage("test-live-region-status")
waitForInitialFocus()
mainSession.evaluateJS("document.querySelector('#status').textContent = 'hello';")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAnnouncement(event: AccessibilityEvent) {
assertThat("Announcement is correct", event.text[0].toString(), equalTo("hello"))
}
})
}
private fun screenContainsNode(nodeId: Int): Boolean {
var node = createNodeInfo(nodeId)
var nodeBounds = Rect()

View file

@ -11801,13 +11801,6 @@
value: true
mirror: always
# Whether to strip auth headers for redirected fetch/xhr channels
# https://fetch.spec.whatwg.org/#http-redirect-fetch
- name: network.fetch.redirect.stripAuthHeader
type: RelaxedAtomicBool
value: true
mirror: always
# If true, cross origin fetch (or XHR) requests would be keyed
# with a different cache key.
- name: network.fetch.cache_partition_cross_origin
@ -11822,13 +11815,6 @@
value: true
mirror: always
# Whether to strip auth headers for redirected http channels
# https://fetch.spec.whatwg.org/#http-redirect-fetch
- name: network.http.redirect.stripAuthHeader
type: RelaxedAtomicBool
value: true
mirror: always
# Prefs allowing granular control of referers.
# 0=don't send any, 1=send only on clicks, 2=send on image requests as well
- name: network.http.sendRefererHeader

View file

@ -173,11 +173,10 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext,
// Construct a LoadInfo object to use when creating the internal channel for an
// Object/Embed load.
static auto CreateObjectLoadInfo(nsDocShellLoadState* aLoadState,
uint64_t aInnerWindowId,
nsContentPolicyType aContentPolicyType,
uint32_t aSandboxFlags)
-> already_AddRefed<LoadInfo> {
static auto CreateObjectLoadInfo(
nsDocShellLoadState* aLoadState, uint64_t aInnerWindowId,
nsContentPolicyType aContentPolicyType,
uint32_t aSandboxFlags) -> already_AddRefed<LoadInfo> {
RefPtr<WindowGlobalParent> wgp =
WindowGlobalParent::GetByInnerWindowId(aInnerWindowId);
MOZ_RELEASE_ASSERT(wgp);
@ -810,6 +809,24 @@ auto DocumentLoadListener::Open(nsDocShellLoadState* aLoadState,
if (cos && aUrgentStart) {
cos->AddClassFlags(nsIClassOfService::UrgentStart);
}
// ClientChannelHelper below needs us to have finalized the principal for
// the channel because it will request that StoragePrincipalHelper mint us a
// principal that needs to match the same principal that a later call to
// StoragePrincipalHelper will mint when determining the right origin to
// look up the ServiceWorker.
//
// Because nsHttpChannel::AsyncOpen calls UpdateAntiTrackingInfoForChannel
// which potentially flips the third party bit/flag on the partition key on
// the cookie jar which impacts the principal that will be minted, it is
// essential that UpdateAntiTrackingInfoForChannel is called before
// AddClientChannelHelperInParent below.
//
// Because the call to UpdateAntiTrackingInfoForChannel is largely
// idempotent, we currently just make the call ourselves right now. The one
// caveat is that the RFPRandomKey may be spuriously regenerated for
// top-level documents.
AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(httpChannel);
}
// Setup a ClientChannelHelper to watch for redirects, and copy
@ -995,8 +1012,8 @@ auto DocumentLoadListener::OpenObject(
uint64_t aInnerWindowId, nsLoadFlags aLoadFlags,
nsContentPolicyType aContentPolicyType, bool aUrgentStart,
dom::ContentParent* aContentParent,
ObjectUpgradeHandler* aObjectUpgradeHandler, nsresult* aRv)
-> RefPtr<OpenPromise> {
ObjectUpgradeHandler* aObjectUpgradeHandler,
nsresult* aRv) -> RefPtr<OpenPromise> {
LOG(("DocumentLoadListener [%p] OpenObject [uri=%s]", this,
aLoadState->URI()->GetSpecOrDefault().get()));
@ -1203,10 +1220,9 @@ void DocumentLoadListener::CleanupParentLoadAttempt(uint64_t aLoadIdent) {
registrar->DeregisterChannels(aLoadIdent);
}
auto DocumentLoadListener::ClaimParentLoad(DocumentLoadListener** aListener,
uint64_t aLoadIdent,
Maybe<uint64_t> aChannelId)
-> RefPtr<OpenPromise> {
auto DocumentLoadListener::ClaimParentLoad(
DocumentLoadListener** aListener, uint64_t aLoadIdent,
Maybe<uint64_t> aChannelId) -> RefPtr<OpenPromise> {
nsCOMPtr<nsIRedirectChannelRegistrar> registrar =
RedirectChannelRegistrar::GetOrCreate();

View file

@ -513,6 +513,21 @@ networking:
- necko@mozilla.com
expires: 130
http_onstart_suspend_total_time:
type: timing_distribution
time_unit: millisecond
telemetry_mirror: HTTP_ONSTART_SUSPEND_TOTAL_TIME
description: >
Time in milliseconds that http channel spent suspended between AsyncOpen and OnStartRequest.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1897209
- https://bugzilla.mozilla.org/show_bug.cgi?id=1347948
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1897209
notification_emails:
- necko@mozilla.com
expires: never
http_1_download_throughput:
type: custom_distribution
unit: mbps

View file

@ -96,7 +96,11 @@ nsresult nsAboutCache::Channel::Init(nsIURI* aURI, nsILoadInfo* aLoadInfo) {
if (!mOverview) {
mBuffer.AppendLiteral(
"<a href=\"about:cache?storage=\">Back to overview</a>");
"<a href=\"about:cache?storage=\">Back to overview</a>\n");
mBuffer.AppendLiteral(
"<p id=\"explanation-dataSize\">Data sizes refer to the size of the "
"response body and do not reflect the amount of disk space that the "
"file occupies.</p>\n");
}
rv = FlushBuffer();

View file

@ -5191,14 +5191,12 @@ nsresult HttpBaseChannel::SetupReplacementChannel(nsIURI* newURI,
// we need to strip Authentication headers for cross-origin requests
// Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch
nsAutoCString authHeader;
if (StaticPrefs::network_http_redirect_stripAuthHeader() &&
NS_SUCCEEDED(
httpChannel->GetRequestHeader("Authorization"_ns, authHeader))) {
if (NS_ShouldRemoveAuthHeaderOnRedirect(static_cast<nsIChannel*>(this),
newChannel, redirectFlags)) {
rv = httpChannel->SetRequestHeader("Authorization"_ns, ""_ns, false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
if (NS_SUCCEEDED(
httpChannel->GetRequestHeader("Authorization"_ns, authHeader)) &&
NS_ShouldRemoveAuthHeaderOnRedirect(static_cast<nsIChannel*>(this),
newChannel, redirectFlags)) {
rv = httpChannel->SetRequestHeader("Authorization"_ns, ""_ns, false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
return NS_OK;

View file

@ -840,7 +840,6 @@ nsCORSListenerProxy::AsyncOnChannelRedirect(
// cross-origin redirects.
// See Bug 1874132
bool stripAuthHeader =
StaticPrefs::network_fetch_redirect_stripAuthHeader() &&
NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags);
nsCOMPtr<nsIHttpChannel> oldHttpChannel = do_QueryInterface(aOldChannel);

View file

@ -6086,8 +6086,7 @@ nsHttpChannel::Resume() {
LogCallingScriptLocation(this);
if (--mSuspendCount == 0) {
mSuspendTotalTime +=
(TimeStamp::NowLoRes() - mSuspendTimestamp).ToMilliseconds();
mSuspendTotalTime += TimeStamp::NowLoRes() - mSuspendTimestamp;
if (mCallOnResume) {
// Resume the interrupted procedure first, then resume
@ -7497,8 +7496,8 @@ nsHttpChannel::OnStartRequest(nsIRequest* request) {
mOnStartRequestTimestamp = TimeStamp::Now();
}
Telemetry::Accumulate(Telemetry::HTTP_ONSTART_SUSPEND_TOTAL_TIME,
mSuspendTotalTime);
mozilla::glean::networking::http_onstart_suspend_total_time
.AccumulateRawDuration(mSuspendTotalTime);
if (mTransaction) {
mProxyConnectResponseCode = mTransaction->GetProxyConnectResponseCode();

View file

@ -617,7 +617,7 @@ class nsHttpChannel final : public HttpBaseChannel,
// Total time the channel spent suspended. This value is reported to
// telemetry in nsHttpChannel::OnStartRequest().
uint32_t mSuspendTotalTime{0};
TimeDuration mSuspendTotalTime{0};
friend class AutoRedirectVetoNotifier;
friend class HttpAsyncAborter<nsHttpChannel>;

View file

@ -3360,15 +3360,6 @@ GeckoDriver.prototype.setPermission = async function (cmd) {
lazy.permissions.validateDescriptor(descriptor);
lazy.permissions.validateState(state);
// Bug 1878741: Allowing this permission causes timing related Android crash.
if (
descriptor.name === "notifications" &&
lazy.permissions.isNotificationPreferenceSet()
) {
// Okay, do nothing. The notifications module will work without permission.
return;
}
let params;
try {
params =

View file

@ -39,6 +39,12 @@ ChromeUtils.defineLazyGetter(lazy, "UNLOAD_TIMEOUT_MULTIPLIER", () => {
export const DEFAULT_UNLOAD_TIMEOUT = 200;
// Load flag for an error page from the DocShell (0x0001U << 16)
const LOAD_FLAG_ERROR_PAGE = 0x10000;
const STATE_START = Ci.nsIWebProgressListener.STATE_START;
const STATE_STOP = Ci.nsIWebProgressListener.STATE_STOP;
/**
* Returns the multiplier used for the unload timer. Useful for tests which
* assert the behavior of this timeout.
@ -107,7 +113,14 @@ export async function waitForInitialNavigationCompleted(
listener.stop();
}
await navigated;
try {
await navigated;
} catch (e) {
// Ignore any error if the initial navigation failed.
lazy.logger.debug(
lazy.truncate`[${browsingContext.id}] Initial Navigation to ${listener.currentURI?.spec} failed: ${e}`
);
}
return {
currentURI: listener.currentURI,
@ -126,6 +139,7 @@ export class ProgressListener {
#webProgress;
#deferredNavigation;
#errorName;
#seenStartFlag;
#targetURI;
#unloadTimerId;
@ -171,6 +185,7 @@ export class ProgressListener {
this.#webProgress = webProgress;
this.#deferredNavigation = null;
this.#errorName = null;
this.#seenStartFlag = false;
this.#targetURI = null;
this.#unloadTimerId = null;
@ -188,6 +203,15 @@ export class ProgressListener {
return this.#webProgress.browsingContext.currentURI;
}
get documentURI() {
return this.#webProgress.browsingContext.currentWindowGlobal.documentURI;
}
get isInitialDocument() {
return this.#webProgress.browsingContext.currentWindowGlobal
.isInitialDocument;
}
get isLoadingDocument() {
return this.#webProgress.isLoadingDocument;
}
@ -196,6 +220,10 @@ export class ProgressListener {
return !!this.#deferredNavigation;
}
get loadType() {
return this.#webProgress.loadType;
}
get targetURI() {
return this.#targetURI;
}
@ -203,13 +231,17 @@ export class ProgressListener {
#checkLoadingState(request, options = {}) {
const { isStart = false, isStop = false, status = 0 } = options;
this.#trace(`Check loading state: isStart=${isStart} isStop=${isStop}`);
this.#trace(
`Loading state: isStart=${isStart} isStop=${isStop} status=0x${status.toString(
16
)}, loadType=0x${this.loadType.toString(16)}`
);
if (isStart && !this.#seenStartFlag) {
this.#seenStartFlag = true;
this.#targetURI = this.#getTargetURI(request);
this.#trace(`state=start: ${this.targetURI?.spec}`);
this.#trace(lazy.truncate`Started loading ${this.targetURI?.spec}`);
if (this.#unloadTimerId !== null) {
lazy.clearTimeout(this.#unloadTimerId);
@ -231,29 +263,32 @@ export class ProgressListener {
!Components.isSuccessCode(status) &&
status != Cr.NS_ERROR_PARSED_DATA_CACHED
) {
if (
status == Cr.NS_BINDING_ABORTED &&
this.browsingContext.currentWindowGlobal.isInitialDocument
) {
const errorName = ChromeUtils.getXPCOMErrorName(status);
if (this.loadType & LOAD_FLAG_ERROR_PAGE) {
// Wait for the next location change notification to ensure that the
// real error page was loaded.
this.#trace(`Error=${errorName}, wait for redirect to error page`);
this.#errorName = errorName;
return;
}
// Handle an aborted navigation. While for an initial document another
// navigation to the real document will happen it's not the case for
// normal documents. Here we need to stop the listener immediately.
if (status == Cr.NS_BINDING_ABORTED && this.isInitialDocument) {
this.#trace(
"Ignore aborted navigation error to the initial document, real document will be loaded."
"Ignore aborted navigation error to the initial document."
);
return;
}
// The navigation request caused an error.
const errorName = ChromeUtils.getXPCOMErrorName(status);
this.#trace(
`state=stop: error=0x${status.toString(16)} (${errorName})`
);
this.stop({ error: new Error(errorName) });
return;
}
this.#trace(`state=stop: ${this.currentURI.spec}`);
// If a non initial page finished loading the navigation is done.
if (!this.browsingContext.currentWindowGlobal.isInitialDocument) {
if (!this.isInitialDocument) {
this.stop();
return;
}
@ -267,6 +302,18 @@ export class ProgressListener {
}
}
#getErrorName(documentURI) {
try {
// Otherwise try to retrieve it from the document URI if it is an
// error page like `about:neterror?e=contentEncodingError&u=http%3A//...`
const regex = /about:.*error\?e=([^&]*)/;
return documentURI.spec.match(regex)[1];
} catch (e) {
// Or return a generic name
return "Address rejected";
}
}
#getTargetURI(request) {
try {
return request.QueryInterface(Ci.nsIChannel).originalURI;
@ -296,8 +343,8 @@ export class ProgressListener {
onStateChange(progress, request, flag, status) {
this.#checkLoadingState(request, {
isStart: flag & Ci.nsIWebProgressListener.STATE_START,
isStop: flag & Ci.nsIWebProgressListener.STATE_STOP,
isStart: !!(flag & STATE_START),
isStop: !!(flag & STATE_STOP),
status,
});
}
@ -305,15 +352,18 @@ export class ProgressListener {
onLocationChange(progress, request, location, flag) {
// If an error page has been loaded abort the navigation.
if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
this.#trace(`location=errorPage: ${location.spec}`);
this.stop({ error: new Error("Address restricted") });
const errorName = this.#errorName || this.#getErrorName(this.documentURI);
this.#trace(
lazy.truncate`Location=errorPage, error=${errorName}, url=${this.documentURI.spec}`
);
this.stop({ error: new Error(errorName) });
return;
}
// If location has changed in the same document the navigation is done.
if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
this.#targetURI = location;
this.#trace(`location=sameDocument: ${this.targetURI?.spec}`);
this.#trace(`Location=sameDocument: ${this.targetURI?.spec}`);
this.stop();
}
}
@ -383,7 +433,9 @@ export class ProgressListener {
stop(options = {}) {
const { error } = options;
this.#trace(`Stop: has error=${!!error}`);
this.#trace(
lazy.truncate`Stop: has error=${!!error} url=${this.currentURI.spec}`
);
if (!this.#deferredNavigation) {
throw new Error("Progress listener not yet started");

View file

@ -42,24 +42,6 @@ permissions.getStorageAccessPermissionsType = function (uri) {
return "3rdPartyFrameStorage^" + thirdPartyPrincipalSite;
};
/**
* Check if the notification preference is set.
*
* @returns {boolean}
* Returns `true` if the notification preference is set.
*
* @throws {UnsupportedOperationError}
* If the notification preference is not set.
*/
permissions.isNotificationPreferenceSet = function () {
if (Services.prefs.getBoolPref("notification.prompt.testing", false)) {
return true;
}
throw new lazy.error.UnsupportedOperationError(
`Setting "descriptor.name" "notifications" expected "notification.prompt.testing" preference to be set`
);
};
/**
* Set a permission given a permission descriptor, a permission state,
* an origin.

View file

@ -15,10 +15,12 @@ const {
"chrome://remote/content/shared/Navigate.sys.mjs"
);
const LOAD_FLAG_ERROR_PAGE = 0x10000;
const CURRENT_URI = Services.io.newURI("http://foo.bar/");
const INITIAL_URI = Services.io.newURI("about:blank");
const TARGET_URI = Services.io.newURI("http://foo.cheese/");
const TARGET_URI_IS_ERROR_PAGE = Services.io.newURI("doesnotexist://");
const TARGET_URI_ERROR_PAGE = Services.io.newURI("doesnotexist://");
const TARGET_URI_WITH_HASH = Services.io.newURI("http://foo.cheese/#foo");
function wait(time) {
@ -43,6 +45,7 @@ class MockWebProgress {
this.documentRequest = null;
this.isLoadingDocument = false;
this.listener = null;
this.loadType = 0;
this.progressListenerRemoved = false;
}
@ -69,15 +72,15 @@ class MockWebProgress {
this.documentRequest = null;
if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
this.browsingContext.currentURI = TARGET_URI_WITH_HASH;
this.browsingContext.updateURI(TARGET_URI_WITH_HASH);
} else if (flag & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
this.browsingContext.currentURI = TARGET_URI_IS_ERROR_PAGE;
this.browsingContext.updateURI(TARGET_URI_ERROR_PAGE, true);
}
this.listener?.onLocationChange(
this,
this.documentRequest,
TARGET_URI_WITH_HASH,
this.browsingContext.currentURI,
flag
);
@ -98,6 +101,7 @@ class MockWebProgress {
this.browsingContext.currentWindowGlobal.isInitialDocument = isInitial;
this.isLoadingDocument = true;
this.loadType = 0;
const uri = isInitial ? INITIAL_URI : TARGET_URI;
this.documentRequest = new MockRequest(uri);
@ -105,14 +109,14 @@ class MockWebProgress {
this,
this.documentRequest,
Ci.nsIWebProgressListener.STATE_START,
null
0
);
return new Promise(executeSoon);
}
sendStopState(options = {}) {
const { errorFlag = 0 } = options;
const { flag = 0, loadType = 0 } = options;
this.browsingContext.currentURI = this.documentRequest.originalURI;
@ -123,9 +127,16 @@ class MockWebProgress {
this,
this.documentRequest,
Ci.nsIWebProgressListener.STATE_STOP,
errorFlag
flag
);
if (loadType & LOAD_FLAG_ERROR_PAGE) {
this.loadType = 0x10000;
return this.sendLocationChange({
flag: Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE,
});
}
return new Promise(executeSoon);
}
}
@ -133,11 +144,23 @@ class MockWebProgress {
class MockTopContext {
constructor(webProgress = null) {
this.currentURI = CURRENT_URI;
this.currentWindowGlobal = { isInitialDocument: true };
this.currentWindowGlobal = {
isInitialDocument: true,
documentURI: CURRENT_URI,
};
this.id = 7;
this.top = this;
this.webProgress = webProgress || new MockWebProgress(this);
}
updateURI(uri, isError = false) {
this.currentURI = uri;
if (isError) {
this.currentWindowGlobal.documentURI = "about:neterror?e=errorMessage";
} else {
this.currentWindowGlobal.documentURI = uri;
}
}
}
const hasPromiseResolved = async function (promise) {
@ -775,7 +798,7 @@ add_task(async function test_ProgressListener_ignoreCacheError() {
await webProgress.sendStartState();
await webProgress.sendStopState({
errorFlag: Cr.NS_ERROR_PARSED_DATA_CACHED,
flag: Cr.NS_ERROR_PARSED_DATA_CACHED,
});
ok(await hasPromiseResolved(navigated), "Listener has resolved");
@ -803,23 +826,50 @@ add_task(async function test_ProgressListener_navigationRejectedOnErrorPage() {
);
});
add_task(async function test_ProgressListener_navigationRejectedOnStopState() {
const browsingContext = new MockTopContext();
const webProgress = browsingContext.webProgress;
add_task(
async function test_ProgressListener_navigationRejectedOnStopStateErrorPage() {
const browsingContext = new MockTopContext();
const webProgress = browsingContext.webProgress;
const progressListener = new ProgressListener(webProgress, {
waitForExplicitStart: false,
});
const navigated = progressListener.start();
const progressListener = new ProgressListener(webProgress, {
waitForExplicitStart: false,
});
const navigated = progressListener.start();
await webProgress.sendStartState();
await webProgress.sendStopState({ errorFlag: Cr.NS_BINDING_ABORTED });
await webProgress.sendStartState();
await webProgress.sendStopState({
flag: Cr.NS_ERROR_MALWARE_URI,
loadType: LOAD_FLAG_ERROR_PAGE,
});
ok(
await hasPromiseRejected(navigated),
"Listener has rejected in stop state for erroneous navigation"
);
});
ok(
await hasPromiseRejected(navigated),
"Listener has rejected in stop state for erroneous navigation"
);
}
);
add_task(
async function test_ProgressListener_navigationRejectedOnStopStateAndAborted() {
const browsingContext = new MockTopContext();
const webProgress = browsingContext.webProgress;
for (const flag of [Cr.NS_BINDING_ABORTED, Cr.NS_ERROR_ABORT]) {
const progressListener = new ProgressListener(webProgress, {
waitForExplicitStart: false,
});
const navigated = progressListener.start();
await webProgress.sendStartState();
await webProgress.sendStopState({ flag });
ok(
await hasPromiseRejected(navigated),
"Listener has rejected in stop state for erroneous navigation"
);
}
}
);
add_task(async function test_ProgressListener_stopIfStarted() {
const browsingContext = new MockTopContext();

View file

@ -63,15 +63,6 @@ class PermissionsModule extends Module {
const permissionName = descriptor.name;
// Bug 1878741: Allowing this permission causes timing related Android crash.
if (
permissionName === "notifications" &&
lazy.permissions.isNotificationPreferenceSet()
) {
// Okay, do nothing. The notifications module will work without permission.
return;
}
if (permissionName === "storage-access") {
// TODO: Bug 1895457. Add support for "storage-access" permission.
throw new lazy.error.UnsupportedOperationError(

View file

@ -980,6 +980,16 @@ who = "Mike Hommey <mh+mozilla@glandium.org>"
criteria = "safe-to-deploy"
delta = "1.4.0 -> 1.6.0"
[[audits.clang-sys]]
who = "Erich Gubler <erichdongubler@gmail.com>"
criteria = "safe-to-deploy"
delta = "1.6.0 -> 1.7.0"
notes = """
Adds several new symbols for Clang versions 11.0, 12.0, 16.0, and 17.0, conditionally enabled based
on Cargo feature flags. Some other minor internal refactors were implemented that shouldn't change
functionality otherwise.
"""
[[audits.clap-verbosity-flag]]
who = "Kershaw Chang <kershaw@mozilla.com>"
criteria = "safe-to-run"
@ -1278,6 +1288,11 @@ who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.10.3 -> 0.12.0"
[[audits.cubeb]]
who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.12.0 -> 0.13.0"
[[audits.cubeb-backend]]
who = "Matthew Gregan <kinetik@flim.org>"
criteria = "safe-to-deploy"
@ -1309,6 +1324,11 @@ who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.10.7 -> 0.12.0"
[[audits.cubeb-backend]]
who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.12.0 -> 0.13.0"
[[audits.cubeb-core]]
who = "Matthew Gregan <kinetik@flim.org>"
criteria = "safe-to-deploy"
@ -1345,6 +1365,11 @@ who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.10.7 -> 0.12.0"
[[audits.cubeb-core]]
who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.12.0 -> 0.13.0"
[[audits.cubeb-sys]]
who = "Matthew Gregan <kinetik@flim.org>"
criteria = "safe-to-deploy"
@ -1376,6 +1401,11 @@ who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.10.7 -> 0.12.0"
[[audits.cubeb-sys]]
who = "Andreas Pehrson <apehrson@mozilla.com>"
criteria = "safe-to-deploy"
delta = "0.12.0 -> 0.13.0"
[[audits.d3d12]]
who = "Jim Blandy <jimb@red-bean.com>"
criteria = "safe-to-deploy"

View file

@ -49,6 +49,7 @@ firefox-ci:
- '.taskcluster.yml'
- 'taskcluster/kinds/**'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'tools/tryselect/selectors/auto.py'
fog:
@ -343,8 +344,9 @@ taskgraph-tests:
subsuite: taskgraph
when:
files-changed:
- 'taskcluster/**/*.py'
- 'python/mach/**/*.py'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
tryselect:
description: tools/tryselect unit tests
@ -362,6 +364,7 @@ tryselect:
- 'taskcluster/config.yml'
- 'taskcluster/kinds/test/**'
- 'taskcluster/gecko_taskgraph/transforms/**'
- 'third_party/python/taskcluster_taskgraph/**/*.py'
- 'tools/tryselect/**'
mozbuild:

View file

@ -40,3 +40,4 @@ diff:
files-changed:
- 'taskcluster/kinds/**'
- 'taskcluster/**/*.py'
- 'third_party/python/taskcluster_taskgraph/**/*.py'

View file

@ -349,6 +349,15 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
"help": "Whether to use the Http2 server",
},
],
[
["--mochitest-flavor"],
{
"action": "store",
"dest": "mochitest_flavor",
"help": "Specify which mochitest flavor to run."
"Examples: 'plain', 'browser'",
},
],
]
+ copy.deepcopy(testing_config_options)
+ copy.deepcopy(code_coverage_config_options)
@ -385,6 +394,7 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
self.binary_path = c.get("binary_path")
self.abs_app_dir = None
self.abs_res_dir = None
self.mochitest_flavor = c.get("mochitest_flavor", None)
# Construct an identifier to be used to identify Perfherder data
# for resource monitoring recording. This attempts to uniquely
@ -642,6 +652,9 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
"gtest_dir": os.path.join(dirs["abs_test_install_dir"], "gtest"),
}
if self.mochitest_flavor:
str_format_values.update({"mochitest_flavor": self.mochitest_flavor})
# TestingMixin._download_and_extract_symbols() will set
# self.symbols_path when downloading/extracting.
if self.symbols_path:
@ -701,6 +714,9 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
"mochitest."
)
if c.get("mochitest_flavor", None):
base_cmd.append("--flavor={}".format(c["mochitest_flavor"]))
if c["headless"]:
base_cmd.append("--headless")
@ -1210,7 +1226,14 @@ class DesktopUnittest(TestingMixin, MercurialScript, MozbaseMixin, CodeCoverageM
options_list = suites[suite]
tests_list = []
flavor = self._query_try_flavor(suite_category, suite)
flavor = (
self.mochitest_flavor
if self.mochitest_flavor
else self._query_try_flavor(suite_category, suite)
)
if self.mochitest_flavor:
replace_dict.update({"mochitest_flavor": flavor})
try_options, try_tests = self.try_args(flavor)
suite_name = suite_category + "-" + suite

View file

@ -151,6 +151,15 @@ class MarionetteTest(TestingMixin, MercurialScript, TransferMixin, CodeCoverageM
"help": "Run the browser without fission enabled",
},
],
[
["--subsuite"],
{
"action": "store",
"dest": "subsuite",
"default": "marionette",
"help": "Selects test paths from test-manifests.active",
},
],
]
+ copy.deepcopy(testing_config_options)
+ copy.deepcopy(code_coverage_config_options)
@ -188,6 +197,7 @@ class MarionetteTest(TestingMixin, MercurialScript, TransferMixin, CodeCoverageM
self.binary_path = c.get("binary_path")
self.test_url = c.get("test_url")
self.test_packages_url = c.get("test_packages_url")
self.subsuite = c.get("subsuite")
self.test_suite = self._get_test_suite(c.get("emulator"))
if self.test_suite not in self.config["suite_definitions"]:
@ -357,7 +367,7 @@ class MarionetteTest(TestingMixin, MercurialScript, TransferMixin, CodeCoverageM
test_paths = json.loads(os.environ.get("MOZHARNESS_TEST_PATHS", '""'))
confirm_paths = json.loads(os.environ.get("MOZHARNESS_CONFIRM_PATHS", '""'))
suite = "marionette"
suite = self.subsuite
if test_paths and suite in test_paths:
suite_test_paths = test_paths[suite]
if confirm_paths and suite in confirm_paths and confirm_paths[suite]:

View file

@ -1,2 +1,3 @@
lsan-allowed: [Alloc, NS_GetXPTCallStub, NewPage, nsXPCWrappedJS::GetNewOrUsed]
leak-threshold: [default:51200]
prefs: [marionette.setpermission.enabled:true]

View file

@ -1,5 +1,3 @@
prefs: [notification.prompt.testing:true]
[event-onclose.https.html]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1816427
expected:

View file

@ -1,5 +1,3 @@
prefs: [notification.prompt.testing:true]
[event-onshow.https.html]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1816427
expected:

View file

@ -1,4 +1,4 @@
prefs: [notification.prompt.testing:true, dom.webnotifications.loglevel:All]
prefs: [dom.webnotifications.loglevel:All]
[getnotifications-across-processes.https.window.html]
[Get notification created from window]
expected:

View file

@ -1,4 +1,3 @@
prefs: [notification.prompt.testing:true]
[instance.https.window.html]
expected:
if (os == "linux") and not debug and not asan and not tsan: [OK, TIMEOUT]

View file

@ -1,113 +1,172 @@
[lang.https.html]
expected:
if (os == "android") and fission: [OK, TIMEOUT]
if os == "android": TIMEOUT
[Roundtripping lang "". Expecting "".]
expected: NOTRUN
expected:
if os == "android": TIMEOUT
[Roundtripping lang "en". Expecting "en".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "en-US-x-hixie". Expecting "en-US-x-hixie".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-DE". Expecting "de-DE".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-de". Expecting "de-de".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-De". Expecting "de-De".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-dE". Expecting "de-dE".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-DE-1996". Expecting "de-DE-1996".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-Latn-DE". Expecting "de-Latn-DE".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-Latf-DE". Expecting "de-Latf-DE".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-Latn-DE-1996". Expecting "de-Latn-DE-1996".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "de-CH". Expecting "de-CH".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "it-CH". Expecting "it-CH".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "fr-CH". Expecting "fr-CH".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "rm-CH". Expecting "rm-CH".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "es-CH". Expecting "es-CH".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
[Roundtripping lang "Latn-de". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "Latf-de". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "tic-tac-tac-toe". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "cocoa-1-bar". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "cocoa-a-bar". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "en-". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "en--". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "foo--bar". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "id---Java". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "fr-x". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "fr-xenomorph". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "fr-x-xenomorph". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "a". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "a-fr-lang". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "b-fr-lang". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "es1-KK-aa-bb-cc-dd". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "es2-KL-aa-bb-cc-dd". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "es3-KM-aa-bb-cc-dd". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "fooÉ". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "foöÉ-bÁr". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL
[Roundtripping lang "foöÉbÁr". Expecting "".]
expected: NOTRUN
expected:
if os == "android": NOTRUN
FAIL

View file

@ -1,3 +1,4 @@
[permission.html]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1897999
expected:
if (os == "android") and fission: [OK, TIMEOUT]
if (os == "android"): CRASH

View file

@ -1 +0,0 @@
prefs: [notification.prompt.testing:true]

View file

@ -1,4 +1,3 @@
prefs: [notification.prompt.testing:true]
[getnotifications-across-processes.https.window.html]
[Get notification created from window]
expected:

View file

@ -1,5 +1,3 @@
prefs: [notification.prompt.testing:true]
[tag.https.html]
bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1891536
expected:

View file

@ -36,6 +36,15 @@ async def test_insecure_certificate(
expected_error=True,
)
# If the error page hasn't fully loaded yet, running a BiDi command
# in most of the cases will trigger an error like:
# `AbortError: Actor 'MessageHandlerFrame' destroyed before query`
result = await bidi_session.browsing_context.locate_nodes(
context=contexts[0]["context"],
locator={"type": "css", "value": "body"},
)
assert len(result["nodes"]) > 0
async def test_invalid_content_encoding(bidi_session, new_tab, inline):
await navigate_and_assert(
@ -44,3 +53,12 @@ async def test_invalid_content_encoding(bidi_session, new_tab, inline):
f"{inline('<div>foo')}&pipe=header(Content-Encoding,gzip)",
expected_error=True,
)
# If the error page hasn't fully loaded yet, running a BiDi command
# in most of the cases will trigger an error like:
# `AbortError: Actor 'MessageHandlerFrame' destroyed before query`
result = await bidi_session.browsing_context.locate_nodes(
context=new_tab["context"],
locator={"type": "css", "value": "body"},
)
assert len(result["nodes"]) > 0

View file

@ -26,6 +26,7 @@ idl_test(
Notification: ['notification'],
});
self.notification = new Notification('title');
self.notification.close();
}
}
);

View file

@ -4,9 +4,10 @@
<link rel="author" title="Apple Inc." href="http://www.apple.com/">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/helpers.js"></script>
<script>
setup({ explicit_done: true });
/* Validity and well-formedness was determined by using the BCP 47
Validator at http://schneegans.de/lv/ */
@ -29,20 +30,20 @@ var invalid_langs = ["en-", "en-\-", "foo-\-bar", "id-\-\-Java", "fr-x",
"es2-KL-aa-bb-cc-dd", "es3-KM-aa-bb-cc-dd", "fooÉ",
"foöÉ-bÁr", "foöÉbÁr"];
promise_setup(async () => {
// No need to show actual notification
await trySettingPermission("prompt");
});
function test_lang(language, should_passthrough) {
var expected = should_passthrough ? language : "";
test(function() {
if (Notification.permission != "granted") {
this.force_timeout()
this.set_status(this.NOTRUN, "You must allow notifications for this origin before running this test.")
}
promise_test(async () => {
var notification = new Notification("This is a notification.", {
lang: language
});
notification.close();
assert_equals(notification.lang, expected, "notification.lang");
notification.onshow = function() {
notification.close();
};
},
"Roundtripping lang \"" + language + "\". Expecting \"" + expected + "\".");
}
@ -58,6 +59,4 @@ for (var i=0; i<well_formed_langs.length; i++) {
for (var i=0; i<invalid_langs.length; i++) {
test_lang(invalid_langs[i], false);
}
done();
</script>

9
third_party/python/poetry.lock generated vendored
View file

@ -1161,7 +1161,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -1376,14 +1375,14 @@ test = ["aiofiles", "coverage", "flake8", "httmock", "httptest", "hypothesis", "
[[package]]
name = "taskcluster-taskgraph"
version = "8.0.1"
version = "8.2.0"
description = "Build taskcluster taskgraphs"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "taskcluster-taskgraph-8.0.1.tar.gz", hash = "sha256:21387537bbebab2a7b1890d03e20e49379bdda65efd45ca7fb8d01f5c29e1797"},
{file = "taskcluster_taskgraph-8.0.1-py3-none-any.whl", hash = "sha256:14500bc703f64eb002c0cd505caaf2d34ffc0ae66d109b108e738661da1ae09c"},
{file = "taskcluster-taskgraph-8.2.0.tar.gz", hash = "sha256:af146323402c2d5f67c65e3c232eda953da1ce319e465069e4d5c7aeb212b66e"},
{file = "taskcluster_taskgraph-8.2.0-py3-none-any.whl", hash = "sha256:410e9c9ef43eac1d0676f16867137de90f77eb0b4e0cbe746fe5512d1a626822"},
]
[package.dependencies]
@ -1625,4 +1624,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "a683c725217ca4a2d61b56b43b137217b5f8630ee0715e44637fd29599e0b0a2"
content-hash = "401a5bde4f28e456dc5369de933b4382bfc9cbf12da2a9ccd7dbfa1a592f34b9"

View file

@ -53,7 +53,7 @@ setuptools==68.0.0
six==1.16.0
slugid==2.0.0
taskcluster==44.2.2
taskcluster-taskgraph==8.0.1
taskcluster-taskgraph==8.2.0
taskcluster-urls==13.0.1
toml==0.10.2
tomlkit==0.12.3

View file

@ -492,7 +492,6 @@ pyyaml==6.0.1 ; python_version >= "3.8" and python_version < "4.0" \
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
@ -540,9 +539,9 @@ six==1.16.0 ; python_version >= "3.8" and python_version < "4.0" \
slugid==2.0.0 ; python_version >= "3.8" and python_version < "4.0" \
--hash=sha256:a950d98b72691178bdd4d6c52743c4a2aa039207cf7a97d71060a111ff9ba297 \
--hash=sha256:aec8b0e01c4ad32e38e12d609eab3ec912fd129aaf6b2ded0199b56a5f8fd67c
taskcluster-taskgraph==8.0.1 ; python_version >= "3.8" and python_version < "4.0" \
--hash=sha256:14500bc703f64eb002c0cd505caaf2d34ffc0ae66d109b108e738661da1ae09c \
--hash=sha256:21387537bbebab2a7b1890d03e20e49379bdda65efd45ca7fb8d01f5c29e1797
taskcluster-taskgraph==8.2.0 ; python_version >= "3.8" and python_version < "4.0" \
--hash=sha256:410e9c9ef43eac1d0676f16867137de90f77eb0b4e0cbe746fe5512d1a626822 \
--hash=sha256:af146323402c2d5f67c65e3c232eda953da1ce319e465069e4d5c7aeb212b66e
taskcluster-urls==13.0.1 ; python_version >= "3.8" and python_version < "4.0" \
--hash=sha256:5e25e7e6818e8877178b175ff43d2e6548afad72694aa125f404a7329ece0973 \
--hash=sha256:b25e122ecec249c4299ac7b20b08db76e3e2025bdaeb699a9d444556de5fd367 \

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: taskcluster-taskgraph
Version: 8.0.1
Version: 8.2.0
Summary: Build taskcluster taskgraphs
Home-page: https://github.com/taskcluster/taskgraph
Classifier: Development Status :: 5 - Production/Stable

View file

@ -1,12 +1,12 @@
taskgraph/__init__.py,sha256=hCl3NLzC-cVXlKhuzf0-_0wd0gYmNA3oshXfTaa9DNQ,729
taskgraph/__init__.py,sha256=Y7AMSO_xkN6zeyK0gagjJ7_kp0ra84C6-RPuLB6FH_A,729
taskgraph/config.py,sha256=8vntWUrPwGds22mFKYAgcsD4Mr8hoONTv2ssGBcClLw,5108
taskgraph/create.py,sha256=_zokjSM3ZaO04l2LiMhenE8qXDZVfYvueIIu5hGUhzc,5185
taskgraph/decision.py,sha256=sG0CIj9OSOdfN65LSt6dRYFWbns9_JraVC5fQU1_7oc,13012
taskgraph/docker.py,sha256=rk-tAMycHnapFyR2Q-XJXzC2A4uv0i-VykLZfwl-pRo,8417
taskgraph/decision.py,sha256=gIvVLfMTd6KtnrTFkmFTrky93mknB9dxtL7_aZwEtoA,13088
taskgraph/docker.py,sha256=Tw2L4A3Mb3P4BdSkVilhSf8Ob38j15xIYYxtUXSDT9s,8415
taskgraph/filter_tasks.py,sha256=R7tYXiaVPGIkQ6O1c9-QJrKZ59m9pFXCloUlPraVnZU,866
taskgraph/generator.py,sha256=zrH1zfy-8akksKTSOf6e4FEsdOd5y7-h1Jne_2Jabcc,15703
taskgraph/graph.py,sha256=bHUsv2pPa2SSaWgBY-ItIj7REPd0o4fFYrwoQbwFKTY,4680
taskgraph/main.py,sha256=tgfAEcNUJfmADteL24yJR5u7tzU4v3mzmxiogVSCK8Y,29072
taskgraph/main.py,sha256=n4p2LAN10Oo2yVv1G-cnWxK0FV2KcB9Q4H5m0K0qmw0,29171
taskgraph/morph.py,sha256=bwkaSGdTZLcK_rhF2st2mCGv9EHN5WdbnDeuZcqp9UA,9208
taskgraph/parameters.py,sha256=hrwUHHu4PS79w-fQ3qNnLSyjRto1EDlidE8e1GzIy8U,12272
taskgraph/target_tasks.py,sha256=9_v66bzmQFELPsfIDGITXrqzsmEiLq1EeuJFhycKL0M,3356
@ -24,8 +24,8 @@ taskgraph/loader/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
taskgraph/loader/default.py,sha256=_bBJG6l04v44Jm5HSIEnVndC05NpNmq5L28QfJHk0wo,1185
taskgraph/loader/transform.py,sha256=olUBPjxk3eEIg25sduxlcyqhjoig4ts5kPlT_zs6g9g,2147
taskgraph/optimize/__init__.py,sha256=Oqpq1RW8QzOcu7zaMlNQ3BHT9ws9e_93FWfCqzNcQps,123
taskgraph/optimize/base.py,sha256=wTViUwVmY9sZvlzSuGwkVrETCo0v2OfyNxFFgzJrDNc,18982
taskgraph/optimize/strategies.py,sha256=UryFI5TizzEF_2NO8MyuKwqVektHfJeG_t0_zZwxEds,2577
taskgraph/optimize/base.py,sha256=ckr0C2qzYTyp036oDInMDRaGmieAH7t93kOy-1hXPbg,20107
taskgraph/optimize/strategies.py,sha256=KTX9PJ846Z8Bpy7z2_JGlJtyIbpt8Di8qrM9oGcGElA,3241
taskgraph/run-task/fetch-content,sha256=G1aAvZlTg0yWHqxhSxi4RvfxW-KBJ5JwnGtWRqfH_bg,29990
taskgraph/run-task/hgrc,sha256=BybWLDR89bWi3pE5T05UqmDHs02CbLypE-omLZWU6Uk,896
taskgraph/run-task/robustcheckout.py,sha256=vPKvHb3fIIJli9ZVZG88XYoa8Sohy2JrpmH6pDgBDHI,30813
@ -62,7 +62,7 @@ taskgraph/util/readonlydict.py,sha256=XzTG-gqGqWVlSkDxSyOL6Ur7Z0ONhIJ9DVLWV3q4q1
taskgraph/util/schema.py,sha256=HmbbJ_i5uxZZHZSJ8sVWaD-VMhZI4ymx0STNcjO5t2M,8260
taskgraph/util/set_name.py,sha256=cha9awo2nMQ9jfSEcbyNkZkCq_1Yg_kKJTfvDzabHSc,1134
taskgraph/util/shell.py,sha256=nf__ly0Ikhj92AiEBCQtvyyckm8UfO_3DSgz0SU-7QA,1321
taskgraph/util/taskcluster.py,sha256=LScpZknMycOOneIcRMf236rCTMRHHGxFTc9Lh7mRKaI,13057
taskgraph/util/taskcluster.py,sha256=-BlQqkxxH5S2BbZ4X2c0lNd1msU2xLM1S5rr8qrLwkE,15961
taskgraph/util/taskgraph.py,sha256=ecKEvTfmLVvEKLPO_0g34CqVvc0iCzuNMh3064BZNrE,1969
taskgraph/util/templates.py,sha256=HGTaIKCpAwEzBDHq0cDai1HJjPJrdnHsjJz6N4LVpKI,2139
taskgraph/util/time.py,sha256=XauJ0DbU0fyFvHLzJLG4ehHv9KaKixxETro89GPC1yk,3350
@ -71,9 +71,9 @@ taskgraph/util/vcs.py,sha256=FjS82fiTsoQ_ArjTCDOtDGfNdVUp_8zvVKB9SoAG3Rs,18019
taskgraph/util/verify.py,sha256=htrNX7aXMMDzxymsFVcs0kaO5gErFHd62g9cQsZI_WE,8518
taskgraph/util/workertypes.py,sha256=1wgM6vLrlgtyv8854anVIs0Bx11kV8JJJaKcOHJc2j0,2498
taskgraph/util/yaml.py,sha256=-LaIf3RROuaSWckOOGN5Iviu-DHWxIChgHn9a7n6ec4,1059
taskcluster_taskgraph-8.0.1.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
taskcluster_taskgraph-8.0.1.dist-info/METADATA,sha256=qg-m62f4BGLh2jBAr_-OQZhraOSciTrv5EyNY0Wwq8I,4688
taskcluster_taskgraph-8.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
taskcluster_taskgraph-8.0.1.dist-info/entry_points.txt,sha256=2hxDzE3qq_sHh-J3ROqwpxgQgxO-196phWAQREl2-XA,50
taskcluster_taskgraph-8.0.1.dist-info/top_level.txt,sha256=3JNeYn_hNiNXC7DrdH_vcv-WYSE7QdgGjdvUYvSjVp0,10
taskcluster_taskgraph-8.0.1.dist-info/RECORD,,
taskcluster_taskgraph-8.2.0.dist-info/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
taskcluster_taskgraph-8.2.0.dist-info/METADATA,sha256=minv1wMCm1M-KJtSo85Cj_tUPkQEdc3OuqHt-HT4tjE,4688
taskcluster_taskgraph-8.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
taskcluster_taskgraph-8.2.0.dist-info/entry_points.txt,sha256=2hxDzE3qq_sHh-J3ROqwpxgQgxO-196phWAQREl2-XA,50
taskcluster_taskgraph-8.2.0.dist-info/top_level.txt,sha256=3JNeYn_hNiNXC7DrdH_vcv-WYSE7QdgGjdvUYvSjVp0,10
taskcluster_taskgraph-8.2.0.dist-info/RECORD,,

View file

@ -2,7 +2,7 @@
# 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/.
__version__ = "8.0.1"
__version__ = "8.2.0"
# Maximum number of dependencies a single task can have
# https://docs.taskcluster.net/reference/platform/taskcluster-queue/references/api#createTask

View file

@ -74,6 +74,8 @@ def taskgraph_decision(options, parameters=None):
* generating a set of artifacts to memorialize the graph
* calling TaskCluster APIs to create the graph
"""
if options.get("verbose"):
logging.root.setLevel(logging.DEBUG)
parameters = parameters or (
lambda graph_config: get_decision_parameters(graph_config, options)

View file

@ -16,7 +16,10 @@ except ImportError as e:
zstd = e
from taskgraph.util import docker
from taskgraph.util.taskcluster import get_artifact_url, get_session
from taskgraph.util.taskcluster import (
get_artifact_url,
get_session,
)
DEPLOY_WARNING = """
*****************************************************************
@ -59,10 +62,9 @@ def load_image_by_name(image_name, tag=None):
)
tasks = load_tasks_for_kind(params, "docker-image")
task = tasks[f"build-docker-image-{image_name}"]
deadline = None
task_id = IndexSearch().should_replace_task(
task, {}, deadline, task.optimization.get("index-search", [])
)
indexes = task.optimization.get("index-search", [])
task_id = IndexSearch().should_replace_task(task, {}, None, indexes)
if task_id in (True, False):
print(

View file

@ -697,6 +697,9 @@ def image_digest(args):
"--tasks-for", required=True, help="the tasks_for value used to generate this task"
)
@argument("--try-task-config-file", help="path to try task configuration file")
@argument(
"--verbose", "-v", action="store_true", help="include debug-level logging output"
)
def decision(options):
from taskgraph.decision import taskgraph_decision

View file

@ -22,6 +22,7 @@ from taskgraph.graph import Graph
from taskgraph.taskgraph import TaskGraph
from taskgraph.util.parameterization import resolve_task_references, resolve_timestamps
from taskgraph.util.python_path import import_sibling_modules
from taskgraph.util.taskcluster import find_task_id_batched, status_task_batched
logger = logging.getLogger(__name__)
registry = {}
@ -51,6 +52,9 @@ def optimize_task_graph(
Perform task optimization, returning a taskgraph and a map from label to
assigned taskId, including replacement tasks.
"""
# avoid circular import
from taskgraph.optimize.strategies import IndexSearch
label_to_taskid = {}
if not existing_tasks:
existing_tasks = {}
@ -70,6 +74,23 @@ def optimize_task_graph(
do_not_optimize=do_not_optimize,
)
# Gather each relevant task's index
indexes = set()
for label in target_task_graph.graph.visit_postorder():
if label in do_not_optimize:
continue
_, strategy, arg = optimizations(label)
if isinstance(strategy, IndexSearch) and arg is not None:
indexes.update(arg)
index_to_taskid = {}
taskid_to_status = {}
if indexes:
# Find their respective status using TC index/queue batch APIs
indexes = list(indexes)
index_to_taskid = find_task_id_batched(indexes)
taskid_to_status = status_task_batched(list(index_to_taskid.values()))
replaced_tasks = replace_tasks(
target_task_graph=target_task_graph,
optimizations=optimizations,
@ -78,6 +99,8 @@ def optimize_task_graph(
label_to_taskid=label_to_taskid,
existing_tasks=existing_tasks,
removed_tasks=removed_tasks,
index_to_taskid=index_to_taskid,
taskid_to_status=taskid_to_status,
)
return (
@ -259,12 +282,17 @@ def replace_tasks(
label_to_taskid,
removed_tasks,
existing_tasks,
index_to_taskid,
taskid_to_status,
):
"""
Implement the "Replacing Tasks" phase, returning a set of task labels of
all replaced tasks. The replacement taskIds are added to label_to_taskid as
a side-effect.
"""
# avoid circular import
from taskgraph.optimize.strategies import IndexSearch
opt_counts = defaultdict(int)
replaced = set()
dependents_of = target_task_graph.graph.reverse_links_dict()
@ -307,6 +335,10 @@ def replace_tasks(
deadline = max(
resolve_timestamps(now, task.task["deadline"]) for task in dependents
)
if isinstance(opt, IndexSearch):
arg = arg, index_to_taskid, taskid_to_status
repl = opt.should_replace_task(task, params, deadline, arg)
if repl:
if repl is True:
@ -316,7 +348,7 @@ def replace_tasks(
removed_tasks.add(label)
else:
logger.debug(
f"replace_tasks: {label} replaced by optimization strategy"
f"replace_tasks: {label} replaced with {repl} by optimization strategy"
)
label_to_taskid[label] = repl
replaced.add(label)

View file

@ -22,12 +22,30 @@ class IndexSearch(OptimizationStrategy):
fmt = "%Y-%m-%dT%H:%M:%S.%fZ"
def should_replace_task(self, task, params, deadline, index_paths):
def should_replace_task(self, task, params, deadline, arg):
"Look for a task with one of the given index paths"
batched = False
# Appease static checker that doesn't understand that this is not needed
label_to_taskid = {}
taskid_to_status = {}
if isinstance(arg, tuple) and len(arg) == 3:
# allow for a batched call optimization instead of two queries
# per index path
index_paths, label_to_taskid, taskid_to_status = arg
batched = True
else:
index_paths = arg
for index_path in index_paths:
try:
task_id = find_task_id(index_path)
status = status_task(task_id)
if batched:
task_id = label_to_taskid[index_path]
status = taskid_to_status[task_id]
else:
# 404 is raised as `KeyError` also end up here
task_id = find_task_id(index_path)
status = status_task(task_id)
# status can be `None` if we're in `testing` mode
# (e.g. test-action-callback)
if not status or status.get("state") in ("exception", "failed"):
@ -40,7 +58,7 @@ class IndexSearch(OptimizationStrategy):
return task_id
except KeyError:
# 404 will end up here and go on to the next index path
# go on to the next index path
pass
return False

View file

@ -193,6 +193,48 @@ def find_task_id(index_path, use_proxy=False):
return response.json()["taskId"]
def find_task_id_batched(index_paths, use_proxy=False):
"""Gets the task id of multiple tasks given their respective index.
Args:
index_paths (List[str]): A list of task indexes.
use_proxy (bool): Whether to use taskcluster-proxy (default: False)
Returns:
Dict[str, str]: A dictionary object mapping each valid index path
to its respective task id.
See the endpoint here:
https://docs.taskcluster.net/docs/reference/core/index/api#findTasksAtIndex
"""
endpoint = liburls.api(get_root_url(use_proxy), "index", "v1", "tasks/indexes")
task_ids = {}
continuation_token = None
while True:
response = _do_request(
endpoint,
json={
"indexes": index_paths,
},
params={"continuationToken": continuation_token},
)
response_data = response.json()
if not response_data["tasks"]:
break
response_tasks = response_data["tasks"]
if (len(task_ids) + len(response_tasks)) > len(index_paths):
# Sanity check
raise ValueError("more task ids were returned than were asked for")
task_ids.update((t["namespace"], t["taskId"]) for t in response_tasks)
continuationToken = response_data.get("continuationToken")
if continuationToken is None:
break
return task_ids
def get_artifact_from_index(index_path, artifact_path, use_proxy=False):
full_path = index_path + "/artifacts/" + artifact_path
response = _do_request(get_index_url(full_path, use_proxy))
@ -271,6 +313,49 @@ def status_task(task_id, use_proxy=False):
return status
def status_task_batched(task_ids, use_proxy=False):
"""Gets the status of multiple tasks given task_ids.
In testing mode, just logs that it would have retrieved statuses.
Args:
task_id (List[str]): A list of task ids.
use_proxy (bool): Whether to use taskcluster-proxy (default: False)
Returns:
dict: A dictionary object as defined here:
https://docs.taskcluster.net/docs/reference/platform/queue/api#statuses
"""
if testing:
logger.info(f"Would have gotten status for {len(task_ids)} tasks.")
return
endpoint = liburls.api(get_root_url(use_proxy), "queue", "v1", "tasks/status")
statuses = {}
continuation_token = None
while True:
response = _do_request(
endpoint,
json={
"taskIds": task_ids,
},
params={
"continuationToken": continuation_token,
},
)
response_data = response.json()
if not response_data["statuses"]:
break
response_tasks = response_data["statuses"]
if (len(statuses) + len(response_tasks)) > len(task_ids):
raise ValueError("more task statuses were returned than were asked for")
statuses.update((t["taskId"], t["status"]) for t in response_tasks)
continuationToken = response_data.get("continuationToken")
if continuationToken is None:
break
return statuses
def state_task(task_id, use_proxy=False):
"""Gets the state of a task given a task_id.

View file

@ -1 +1 @@
{"files":{"Cargo.toml":"e94c46bbd290f02adccc7ae932285416d7e021bfde80abb2fb31a2c05426e732","cbindgen.toml":"fb6abe1671497f432a06e40b1db7ed7cd2cceecbd9a2382193ad7534e8855e34","src/context.rs":"a0559e92b554ef3156ab2bf2f1424555c8ef4a7977b9f43ac8500a9f399f8d99","src/lib.rs":"c87d9d57a16a9286cde730978db692df0fbc70cc69dd4f4677198d6843031fd8","src/send_recv.rs":"859abe75b521eb4297c84b30423814b5b87f3c7741ad16fe72189212e123e1ac","src/stream.rs":"90dc6a85552f3569ab1847de4247a46bcff2f5aef0c4d43fa2376589df015b25"},"package":null}
{"files":{"Cargo.toml":"b4fad65749eb0988ce4e6b6a2aae51e58ae22eca97cf61dfb011e951a0909f0e","cbindgen.toml":"fb6abe1671497f432a06e40b1db7ed7cd2cceecbd9a2382193ad7534e8855e34","src/context.rs":"a0559e92b554ef3156ab2bf2f1424555c8ef4a7977b9f43ac8500a9f399f8d99","src/lib.rs":"c87d9d57a16a9286cde730978db692df0fbc70cc69dd4f4677198d6843031fd8","src/send_recv.rs":"859abe75b521eb4297c84b30423814b5b87f3c7741ad16fe72189212e123e1ac","src/stream.rs":"90dc6a85552f3569ab1847de4247a46bcff2f5aef0c4d43fa2376589df015b25"},"package":null}

View file

@ -21,7 +21,7 @@ description = "Cubeb Backend for talking to remote cubeb server."
license = "ISC"
[dependencies]
cubeb-backend = "0.12"
cubeb-backend = "0.13"
log = "0.4"
[dependencies.audio_thread_priority]

View file

@ -1 +1 @@
{"files":{"Cargo.toml":"77997660e305851d9c0e656aac7159b999452a36f3436d8b2f402edd36fef853","cbindgen.toml":"fb6abe1671497f432a06e40b1db7ed7cd2cceecbd9a2382193ad7534e8855e34","src/lib.rs":"d70079c66de72c3469504f1f0c9cf5e510644cac17f2d8300b8d12218740e07b","src/server.rs":"187e2236aa9f2fb6cc4a533d40714a71504afa5ef9d849ac28b7f26032859c29"},"package":null}
{"files":{"Cargo.toml":"62eab883f31c0c088ff865fe2e4305d987b7b534f6cdfe1e5812072a2ec13f8b","cbindgen.toml":"fb6abe1671497f432a06e40b1db7ed7cd2cceecbd9a2382193ad7534e8855e34","src/lib.rs":"d70079c66de72c3469504f1f0c9cf5e510644cac17f2d8300b8d12218740e07b","src/server.rs":"187e2236aa9f2fb6cc4a533d40714a71504afa5ef9d849ac28b7f26032859c29"},"package":null}

View file

@ -21,7 +21,7 @@ description = "Remote cubeb server"
license = "ISC"
[dependencies]
cubeb-core = "0.12.0"
cubeb-core = "0.13"
log = "0.4"
once_cell = "1.2.0"
slab = "0.4"

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