Update On Tue Apr 23 20:46:52 CEST 2024

This commit is contained in:
github-action[bot] 2024-04-23 20:46:53 +02:00
parent 8a7be19130
commit 7cdd4eb6fd
1186 changed files with 42039 additions and 15273 deletions

39
Cargo.lock generated
View file

@ -2460,7 +2460,7 @@ checksum = "bb07a4ffed2093b118a525b1d8f5204ae274faed5604537caf7135d0f18d9887"
dependencies = [
"log",
"plain",
"scroll 0.12.0",
"scroll",
]
[[package]]
@ -3243,9 +3243,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]]
name = "libc"
version = "0.2.152"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "libdbus-sys"
@ -3609,38 +3609,39 @@ dependencies = [
[[package]]
name = "minidump-common"
version = "0.19.1"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dbc11dfb55b3b7b5684fb16d98e0fc9d1e93a64d6b00bf383eabfc4541aaac2"
checksum = "1bb6eaf88cc770fa58e6ae721cf2e40c2ca6a4c942ae8c7aa324d680bd3c6717"
dependencies = [
"bitflags 2.5.0",
"debugid",
"num-derive",
"num-traits",
"range-map",
"scroll 0.11.999",
"scroll",
"smart-default",
]
[[package]]
name = "minidump-writer"
version = "0.8.3"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "017101620fe5d413ac2d98224ab8b1fff0d4bacaf2803c130ad4a6db3e5d3e70"
checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d"
dependencies = [
"bitflags 2.5.0",
"byteorder",
"cfg-if 1.0.0",
"crash-context",
"goblin 0.7.999",
"goblin 0.8.0",
"libc",
"log",
"mach2",
"memmap2 0.8.999",
"memmap2 0.9.3",
"memoffset 0.9.0",
"minidump-common",
"nix 0.27.1",
"nix 0.28.0",
"procfs-core",
"scroll 0.11.999",
"scroll",
"tempfile",
"thiserror",
]
@ -4067,17 +4068,18 @@ checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
name = "nix"
version = "0.26.99"
dependencies = [
"nix 0.27.1",
"nix 0.28.0",
]
[[package]]
name = "nix"
version = "0.27.1"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.5.0",
"cfg-if 1.0.0",
"cfg_aliases",
"libc",
]
@ -5044,13 +5046,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "scroll"
version = "0.11.999"
dependencies = [
"scroll 0.12.0",
]
[[package]]
name = "scroll"
version = "0.12.0"

View file

@ -153,7 +153,7 @@ backtrace = { path = "build/rust/backtrace" }
# Patch bindgen 0.63 to 0.69
bindgen_0_63 = { package = "bindgen", path = "build/rust/bindgen-0.63" }
# Patch nix 0.26 to 0.27
# Patch nix 0.26 to 0.28
nix = { path = "build/rust/nix" }
# Patch indexmap 2.0 to 1.0
@ -168,9 +168,6 @@ autocfg = { path = "third_party/rust/autocfg" }
# Patch goblin 0.7.0 to 0.8
goblin = { path = "build/rust/goblin" }
# Patch scroll 0.11 to 0.12
scroll = { path = "build/rust/scroll" }
# Patch memoffset from 0.8.0 to 0.9.0 since it's compatible and it avoids duplication
memoffset = { path = "build/rust/memoffset" }

View file

@ -135,48 +135,6 @@ static LocalAccessible* MaybeCreateSpecificARIAAccessible(
return nullptr;
}
/**
* Return true if the element has an attribute (ARIA, title, or relation) that
* requires the creation of an Accessible for the element.
*/
static bool AttributesMustBeAccessible(nsIContent* aContent,
DocAccessible* aDocument) {
if (aContent->IsElement()) {
uint32_t attrCount = aContent->AsElement()->GetAttrCount();
for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) {
const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx);
if (attr->NamespaceEquals(kNameSpaceID_None)) {
nsAtom* attrAtom = attr->Atom();
if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) {
// If the author provided a title on an element that would not
// be accessible normally, assume an intent and make it accessible.
return true;
}
nsDependentAtomString attrStr(attrAtom);
if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // not ARIA
// A global state or a property and in case of token defined.
uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom);
if ((attrFlags & ATTR_GLOBAL) &&
(!(attrFlags & ATTR_VALTOKEN) ||
nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) {
return true;
}
}
}
// If the given ID is referred by relation attribute then create an
// Accessible for it.
nsAutoString id;
if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) {
return aDocument->IsDependentID(aContent->AsElement(), id);
}
}
return false;
}
/**
* Return true if the element must be a generic Accessible, even if it has been
* marked presentational with role="presentation", etc. MustBeAccessible causes
@ -224,19 +182,54 @@ static bool MustBeGenericAccessible(nsIContent* aContent,
* Return true if the element must be accessible.
*/
static bool MustBeAccessible(nsIContent* aContent, DocAccessible* aDocument) {
nsIFrame* frame = aContent->GetPrimaryFrame();
MOZ_ASSERT(frame);
// This document might be invisible when it first loads. Therefore, we must
// check focusability irrespective of visibility here. Otherwise, we might not
// create Accessibles for some focusable elements; e.g. a span with only a
// tabindex. Elements that are invisible within this document are excluded
// earlier in CreateAccessible.
if (frame->IsFocusable(/* aWithMouse */ false,
/* aCheckVisibility */ false)) {
return true;
if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
// This document might be invisible when it first loads. Therefore, we must
// check focusability irrespective of visibility here. Otherwise, we might
// not create Accessibles for some focusable elements; e.g. a span with only
// a tabindex. Elements that are invisible within this document are excluded
// earlier in CreateAccessible.
if (frame->IsFocusable(/* aWithMouse */ false,
/* aCheckVisibility */ false)) {
return true;
}
}
return AttributesMustBeAccessible(aContent, aDocument);
// Return true if the element has an attribute (ARIA, title, or relation) that
// requires the creation of an Accessible for the element.
if (aContent->IsElement()) {
uint32_t attrCount = aContent->AsElement()->GetAttrCount();
for (uint32_t attrIdx = 0; attrIdx < attrCount; attrIdx++) {
const nsAttrName* attr = aContent->AsElement()->GetAttrNameAt(attrIdx);
if (attr->NamespaceEquals(kNameSpaceID_None)) {
nsAtom* attrAtom = attr->Atom();
if (attrAtom == nsGkAtoms::title && aContent->IsHTMLElement()) {
// If the author provided a title on an element that would not
// be accessible normally, assume an intent and make it accessible.
return true;
}
nsDependentAtomString attrStr(attrAtom);
if (!StringBeginsWith(attrStr, u"aria-"_ns)) continue; // not ARIA
// A global state or a property and in case of token defined.
uint8_t attrFlags = aria::AttrCharacteristicsFor(attrAtom);
if ((attrFlags & ATTR_GLOBAL) &&
(!(attrFlags & ATTR_VALTOKEN) ||
nsAccUtils::HasDefinedARIAToken(aContent, attrAtom))) {
return true;
}
}
}
// If the given ID is referred by relation attribute then create an
// Accessible for it.
nsAutoString id;
if (nsCoreUtils::GetID(aContent, id) && !id.IsEmpty()) {
return aDocument->IsDependentID(aContent->AsElement(), id);
}
}
return false;
}
bool nsAccessibilityService::ShouldCreateImgAccessible(
@ -293,6 +286,38 @@ static bool MustSVGElementBeAccessible(nsIContent* aContent,
return MustBeAccessible(aContent, aDocument);
}
/**
* Return an accessible for the content if the SVG element requires the creation
* of an Accessible.
*/
static RefPtr<LocalAccessible> MaybeCreateSVGAccessible(
nsIContent* aContent, DocAccessible* aDocument) {
if (aContent->IsSVGGeometryElement() ||
aContent->IsSVGElement(nsGkAtoms::image)) {
// Shape elements: rect, circle, ellipse, line, path, polygon, and polyline.
// 'use' and 'text' graphic elements require special support.
if (MustSVGElementBeAccessible(aContent, aDocument)) {
return new EnumRoleAccessible<roles::GRAPHIC>(aContent, aDocument);
}
} else if (aContent->IsSVGElement(nsGkAtoms::text)) {
return new HyperTextAccessible(aContent->AsElement(), aDocument);
} else if (aContent->IsSVGElement(nsGkAtoms::svg)) {
// An <svg> element could contain <foreignObject>, which contains HTML but
// does not normally create its own Accessible. This means that the <svg>
// Accessible could have TextLeafAccessible children, so it must be a
// HyperTextAccessible.
return new EnumRoleHyperTextAccessible<roles::DIAGRAM>(aContent, aDocument);
} else if (aContent->IsSVGElement(nsGkAtoms::g) &&
MustSVGElementBeAccessible(aContent, aDocument)) {
// <g> can also contain <foreignObject>.
return new EnumRoleHyperTextAccessible<roles::GROUPING>(aContent,
aDocument);
} else if (aContent->IsSVGElement(nsGkAtoms::a)) {
return new HTMLLinkAccessible(aContent, aDocument);
}
return nullptr;
}
/**
* Used by XULMap.h to map both menupopup and popup elements
*/
@ -1138,13 +1163,19 @@ LocalAccessible* nsAccessibilityService::CreateAccessible(
}
}
// SVG elements are not in a markup map, but we may still need to create an
// accessible for one, even in the case of display:contents.
if (!newAcc && content->IsSVGElement()) {
newAcc = MaybeCreateSVGAccessible(content, document);
}
// Check whether this element has an ARIA role or attribute that requires
// us to create an Accessible.
const bool hasNonPresentationalARIARole =
roleMapEntry && !roleMapEntry->Is(nsGkAtoms::presentation) &&
!roleMapEntry->Is(nsGkAtoms::none);
if (!newAcc && (hasNonPresentationalARIARole ||
AttributesMustBeAccessible(content, document))) {
if (!newAcc &&
(hasNonPresentationalARIARole || MustBeAccessible(content, document))) {
newAcc = new HyperTextAccessible(content, document);
}
@ -1365,32 +1396,7 @@ LocalAccessible* nsAccessibilityService::CreateAccessible(
if (!newAcc) {
if (content->IsSVGElement()) {
if (content->IsSVGGeometryElement() ||
content->IsSVGElement(nsGkAtoms::image)) {
// Shape elements: rect, circle, ellipse, line, path, polygon,
// and polyline. 'use' and 'text' graphic elements require
// special support.
if (MustSVGElementBeAccessible(content, document)) {
newAcc = new EnumRoleAccessible<roles::GRAPHIC>(content, document);
}
} else if (content->IsSVGElement(nsGkAtoms::text)) {
newAcc = new HyperTextAccessible(content->AsElement(), document);
} else if (content->IsSVGElement(nsGkAtoms::svg)) {
// An <svg> element could contain <foreignObject>, which contains HTML
// but does not normally create its own Accessible. This means that the
// <svg> Accessible could have TextLeafAccessible children, so it must
// be a HyperTextAccessible.
newAcc =
new EnumRoleHyperTextAccessible<roles::DIAGRAM>(content, document);
} else if (content->IsSVGElement(nsGkAtoms::g) &&
MustSVGElementBeAccessible(content, document)) {
// <g> can also contain <foreignObject>.
newAcc =
new EnumRoleHyperTextAccessible<roles::GROUPING>(content, document);
} else if (content->IsSVGElement(nsGkAtoms::a)) {
newAcc = new HTMLLinkAccessible(content, document);
}
newAcc = MaybeCreateSVGAccessible(content, document);
} else if (content->IsMathMLElement()) {
const MarkupMapInfo* markupMap =
mMathMLMarkupMap.Get(content->NodeInfo()->NameAtom());

View file

@ -24,7 +24,9 @@ class AccShowEvent;
*/
class DocAccessibleChild : public PDocAccessibleChild {
public:
DocAccessibleChild(DocAccessible* aDoc, IProtocol* aManager) : mDoc(aDoc) {
DocAccessibleChild(DocAccessible* aDoc,
mozilla::ipc::IRefCountedProtocol* aManager)
: mDoc(aDoc) {
MOZ_COUNT_CTOR(DocAccessibleChild);
SetManager(aManager);
}

View file

@ -11,8 +11,11 @@ support-files = ["head.js"]
["browser_elementFromPoint.js"]
["browser_focus.js"]
["browser_generalProps.js"]
["browser_gridPatterns.js"]
["browser_simplePatterns.js"]
["browser_tree.js"]

View file

@ -0,0 +1,161 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* eslint-disable camelcase */
const RowOrColumnMajor_RowMajor = 0;
/* eslint-enable camelcase */
const SNIPPET = `
<table id="table">
<tr><th id="a">a</th><th id="b">b</th><th id="c">c</th></tr>
<tr><th id="dg" rowspan="2">dg</th><td id="ef" colspan="2" headers="b c">ef</td></tr>
<tr><th id="h">h</th><td id="i" headers="dg h">i</td></tr>
<tr><td id="jkl" colspan="3" headers="a b c">jkl</td></tr>
</table>
<button id="button">button</button>
`;
async function testGridGetItem(row, col, cellId) {
is(
await runPython(`pattern.GetItem(${row}, ${col}).CurrentAutomationId`),
cellId,
`GetItem with row ${row} and col ${col} returned ${cellId}`
);
}
async function testGridItemProps(id, row, col, rowSpan, colSpan, gridId) {
await assignPyVarToUiaWithId(id);
await definePyVar("pattern", `getUiaPattern(${id}, "GridItem")`);
ok(await runPython(`bool(pattern)`), `${id} has GridItem pattern`);
is(await runPython(`pattern.CurrentRow`), row, `${id} has correct Row`);
is(await runPython(`pattern.CurrentColumn`), col, `${id} has correct Column`);
is(
await runPython(`pattern.CurrentRowSpan`),
rowSpan,
`${id} has correct RowSpan`
);
is(
await runPython(`pattern.CurrentColumnSpan`),
colSpan,
`${id} has correct ColumnSpan`
);
is(
await runPython(`pattern.CurrentContainingGrid.CurrentAutomationId`),
gridId,
`${id} ContainingGridItem is ${gridId}`
);
}
async function testTableItemProps(id, rowHeaders, colHeaders) {
await assignPyVarToUiaWithId(id);
await definePyVar("pattern", `getUiaPattern(${id}, "TableItem")`);
ok(await runPython(`bool(pattern)`), `${id} has TableItem pattern`);
await isUiaElementArray(
`pattern.GetCurrentRowHeaderItems()`,
rowHeaders,
`${id} has correct RowHeaderItems`
);
await isUiaElementArray(
`pattern.GetCurrentColumnHeaderItems()`,
colHeaders,
`${id} has correct ColumnHeaderItems`
);
}
/**
* Test the Grid pattern.
*/
addUiaTask(SNIPPET, async function testGrid() {
await definePyVar("doc", `getDocUia()`);
await assignPyVarToUiaWithId("table");
await definePyVar("pattern", `getUiaPattern(table, "Grid")`);
ok(await runPython(`bool(pattern)`), "table has Grid pattern");
is(
await runPython(`pattern.CurrentRowCount`),
4,
"table has correct RowCount"
);
is(
await runPython(`pattern.CurrentColumnCount`),
3,
"table has correct ColumnCount"
);
await testGridGetItem(0, 0, "a");
await testGridGetItem(0, 1, "b");
await testGridGetItem(0, 2, "c");
await testGridGetItem(1, 0, "dg");
await testGridGetItem(1, 1, "ef");
await testGridGetItem(1, 2, "ef");
await testGridGetItem(2, 0, "dg");
await testGridGetItem(2, 1, "h");
await testGridGetItem(2, 2, "i");
await testPatternAbsent("button", "Grid");
});
/**
* Test the GridItem pattern.
*/
addUiaTask(SNIPPET, async function testGridItem() {
await definePyVar("doc", `getDocUia()`);
await testGridItemProps("a", 0, 0, 1, 1, "table");
await testGridItemProps("b", 0, 1, 1, 1, "table");
await testGridItemProps("c", 0, 2, 1, 1, "table");
await testGridItemProps("dg", 1, 0, 2, 1, "table");
await testGridItemProps("ef", 1, 1, 1, 2, "table");
await testGridItemProps("jkl", 3, 0, 1, 3, "table");
await testPatternAbsent("button", "GridItem");
});
/**
* Test the Table pattern.
*/
addUiaTask(
SNIPPET,
async function testTable() {
await definePyVar("doc", `getDocUia()`);
await assignPyVarToUiaWithId("table");
await definePyVar("pattern", `getUiaPattern(table, "Table")`);
ok(await runPython(`bool(pattern)`), "table has Table pattern");
await isUiaElementArray(
`pattern.GetCurrentRowHeaders()`,
["dg", "h"],
"table has correct RowHeaders"
);
await isUiaElementArray(
`pattern.GetCurrentColumnHeaders()`,
["a", "b", "c"],
"table has correct ColumnHeaders"
);
is(
await runPython(`pattern.CurrentRowOrColumnMajor`),
RowOrColumnMajor_RowMajor,
"table has correct RowOrColumnMajor"
);
await testPatternAbsent("button", "Table");
},
// The IA2 -> UIA proxy doesn't support the Row/ColumnHeaders properties.
{ uiaEnabled: true, uiaDisabled: false }
);
/**
* Test the TableItem pattern.
*/
addUiaTask(SNIPPET, async function testTableItem() {
await definePyVar("doc", `getDocUia()`);
await testTableItemProps("a", [], []);
await testTableItemProps("b", [], []);
await testTableItemProps("c", [], []);
await testTableItemProps("dg", [], ["a"]);
await testTableItemProps("ef", ["dg"], ["b", "c"]);
await testTableItemProps("h", ["dg"], ["b"]);
await testTableItemProps("i", ["dg", "h"], ["c"]);
await testTableItemProps("jkl", [], ["a", "b", "c"]);
await testPatternAbsent("button", "TableItem");
});

View file

@ -4,7 +4,7 @@
"use strict";
/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises */
/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises, isUiaElementArray */
// Load the shared-head file first.
Services.scriptloader.loadSubScript(
@ -126,3 +126,15 @@ async function testPythonRaises(expression, message) {
}
ok(failed, message);
}
/**
* Verify that an array of UIA elements contains (only) elements with the given
* DOM ids.
*/
async function isUiaElementArray(pyExpr, ids, message) {
const result = await runPython(`
uias = (${pyExpr})
return [uias.GetElement(i).CurrentAutomationId for i in range(uias.Length)]
`);
SimpleTest.isDeeply(result, ids, message);
}

View file

@ -21,7 +21,7 @@
using namespace mozilla::a11y;
TableAccessible* ia2AccessibleTable::TableAcc() {
Accessible* acc = Acc();
Accessible* acc = MsaaAccessible::Acc();
return acc ? acc->AsTable() : nullptr;
}
@ -46,6 +46,18 @@ ia2AccessibleTable::QueryInterface(REFIID iid, void** ppv) {
return S_OK;
}
if (IID_IGridProvider == iid) {
*ppv = static_cast<IGridProvider*>(this);
(reinterpret_cast<IUnknown*>(*ppv))->AddRef();
return S_OK;
}
if (IID_ITableProvider == iid) {
*ppv = static_cast<ITableProvider*>(this);
(reinterpret_cast<IUnknown*>(*ppv))->AddRef();
return S_OK;
}
return ia2AccessibleHypertext::QueryInterface(iid, ppv);
}

View file

@ -12,6 +12,7 @@
#include "AccessibleTable2.h"
#include "ia2AccessibleHypertext.h"
#include "IUnknownImpl.h"
#include "UiaGrid.h"
namespace mozilla {
namespace a11y {
@ -20,6 +21,7 @@ class TableAccessible;
class ia2AccessibleTable : public IAccessibleTable,
public IAccessibleTable2,
public UiaGrid,
public ia2AccessibleHypertext {
public:
// IUnknown

View file

@ -27,6 +27,8 @@ TableCellAccessible* ia2AccessibleTableCell::CellAcc() {
// IUnknown
IMPL_IUNKNOWN_QUERY_HEAD(ia2AccessibleTableCell)
IMPL_IUNKNOWN_QUERY_IFACE(IAccessibleTableCell)
IMPL_IUNKNOWN_QUERY_IFACE(IGridItemProvider)
IMPL_IUNKNOWN_QUERY_IFACE(ITableItemProvider)
IMPL_IUNKNOWN_QUERY_TAIL_INHERITED(ia2AccessibleHypertext)
////////////////////////////////////////////////////////////////////////////////

View file

@ -11,12 +11,14 @@
#include "AccessibleTableCell.h"
#include "ia2AccessibleHypertext.h"
#include "IUnknownImpl.h"
#include "UiaGridItem.h"
namespace mozilla {
namespace a11y {
class TableCellAccessible;
class ia2AccessibleTableCell : public IAccessibleTableCell,
public UiaGridItem,
public ia2AccessibleHypertext {
public:
// IUnknown

View file

@ -0,0 +1,151 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 "ia2AccessibleTable.h"
#include "mozilla/a11y/TableAccessible.h"
#include "nsIAccessiblePivot.h"
#include "Pivot.h"
#include "UiaGrid.h"
using namespace mozilla;
using namespace mozilla::a11y;
// Helpers
// Used to search for all row and column headers in a table. This could be slow,
// as it potentially walks all cells in the table. However, it's unclear if,
// when or how often clients will use this. If this proves to be a performance
// problem, we will need to add methods to TableAccessible to get all row and
// column headers in a faster way.
class HeaderRule : public PivotRule {
public:
explicit HeaderRule(role aRole) : mRole(aRole) {}
virtual uint16_t Match(Accessible* aAcc) override {
role accRole = aAcc->Role();
if (accRole == mRole) {
return nsIAccessibleTraversalRule::FILTER_MATCH |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
if (accRole == roles::CAPTION || aAcc->IsTableCell()) {
return nsIAccessibleTraversalRule::FILTER_IGNORE |
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
}
return nsIAccessibleTraversalRule::FILTER_IGNORE;
}
private:
role mRole;
};
static SAFEARRAY* GetAllHeaders(Accessible* aTable, role aRole) {
AutoTArray<Accessible*, 20> headers;
Pivot pivot(aTable);
HeaderRule rule(aRole);
for (Accessible* header = pivot.Next(aTable, rule); header;
header = pivot.Next(header, rule)) {
headers.AppendElement(header);
}
return AccessibleArrayToUiaArray(headers);
}
// UiaGrid
Accessible* UiaGrid::Acc() {
auto* ia2t = static_cast<ia2AccessibleTable*>(this);
return ia2t->MsaaAccessible::Acc();
}
TableAccessible* UiaGrid::TableAcc() {
Accessible* acc = Acc();
return acc ? acc->AsTable() : nullptr;
}
// IGridProvider methods
STDMETHODIMP
UiaGrid::GetItem(int aRow, int aColumn,
__RPC__deref_out_opt IRawElementProviderSimple** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
TableAccessible* table = TableAcc();
if (!table) {
return CO_E_OBJNOTCONNECTED;
}
Accessible* cell = table->CellAt(aRow, aColumn);
if (!cell) {
return E_INVALIDARG;
}
RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(cell);
uia.forget(aRetVal);
return S_OK;
}
STDMETHODIMP
UiaGrid::get_RowCount(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableAccessible* table = TableAcc();
if (!table) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = table->RowCount();
return S_OK;
}
STDMETHODIMP
UiaGrid::get_ColumnCount(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableAccessible* table = TableAcc();
if (!table) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = table->ColCount();
return S_OK;
}
// ITableProvider methods
STDMETHODIMP
UiaGrid::GetRowHeaders(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = GetAllHeaders(acc, roles::ROWHEADER);
return S_OK;
}
STDMETHODIMP
UiaGrid::GetColumnHeaders(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
Accessible* acc = Acc();
if (!acc) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = GetAllHeaders(acc, roles::COLUMNHEADER);
return S_OK;
}
STDMETHODIMP
UiaGrid::get_RowOrColumnMajor(__RPC__out enum RowOrColumnMajor* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
// HTML and ARIA tables are always in row major order.
*aRetVal = RowOrColumnMajor_RowMajor;
return S_OK;
}

View file

@ -0,0 +1,52 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_a11y_UiaGrid_h__
#define mozilla_a11y_UiaGrid_h__
#include "objbase.h"
#include "uiautomation.h"
namespace mozilla::a11y {
class Accessible;
class TableAccessible;
/**
* IGridProvider and ITableProvider implementations.
*/
class UiaGrid : public IGridProvider, public ITableProvider {
public:
// IGridProvider
virtual HRESULT STDMETHODCALLTYPE GetItem(
/* [in] */ int aRow,
/* [in] */ int aColumn,
/* [retval][out] */
__RPC__deref_out_opt IRawElementProviderSimple** aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowCount(
/* [retval][out] */ __RPC__out int* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ColumnCount(
/* [retval][out] */ __RPC__out int* aRetVal);
// ITableProvider
virtual HRESULT STDMETHODCALLTYPE GetRowHeaders(
/* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal);
virtual HRESULT STDMETHODCALLTYPE GetColumnHeaders(
/* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowOrColumnMajor(
/* [retval][out] */ __RPC__out enum RowOrColumnMajor* aRetVal);
private:
Accessible* Acc();
TableAccessible* TableAcc();
};
} // namespace mozilla::a11y
#endif

View file

@ -0,0 +1,130 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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 "ia2AccessibleTableCell.h"
#include "mozilla/a11y/TableAccessible.h"
#include "mozilla/a11y/TableCellAccessible.h"
#include "UiaGridItem.h"
using namespace mozilla;
using namespace mozilla::a11y;
// UiaGridItem
TableCellAccessible* UiaGridItem::CellAcc() {
auto* derived = static_cast<ia2AccessibleTableCell*>(this);
Accessible* acc = derived->Acc();
return acc ? acc->AsTableCell() : nullptr;
}
// IGridItemProvider methods
STDMETHODIMP
UiaGridItem::get_Row(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = cell->RowIdx();
return S_OK;
}
STDMETHODIMP
UiaGridItem::get_Column(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = cell->ColIdx();
return S_OK;
}
STDMETHODIMP
UiaGridItem::get_RowSpan(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = cell->RowExtent();
return S_OK;
}
STDMETHODIMP
UiaGridItem::get_ColumnSpan(__RPC__out int* aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
*aRetVal = cell->ColExtent();
return S_OK;
}
STDMETHODIMP
UiaGridItem::get_ContainingGrid(
__RPC__deref_out_opt IRawElementProviderSimple** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
TableAccessible* table = cell->Table();
if (!table) {
return E_FAIL;
}
Accessible* tableAcc = table->AsAccessible();
RefPtr<IRawElementProviderSimple> uia = MsaaAccessible::GetFrom(tableAcc);
uia.forget(aRetVal);
return S_OK;
}
// ITableItemProvider methods
STDMETHODIMP
UiaGridItem::GetRowHeaderItems(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
AutoTArray<Accessible*, 10> cells;
cell->RowHeaderCells(&cells);
*aRetVal = AccessibleArrayToUiaArray(cells);
return S_OK;
}
STDMETHODIMP
UiaGridItem::GetColumnHeaderItems(__RPC__deref_out_opt SAFEARRAY** aRetVal) {
if (!aRetVal) {
return E_INVALIDARG;
}
*aRetVal = nullptr;
TableCellAccessible* cell = CellAcc();
if (!cell) {
return CO_E_OBJNOTCONNECTED;
}
AutoTArray<Accessible*, 10> cells;
cell->ColHeaderCells(&cells);
*aRetVal = AccessibleArrayToUiaArray(cells);
return S_OK;
}

View file

@ -0,0 +1,51 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_a11y_UiaGridItem_h__
#define mozilla_a11y_UiaGridItem_h__
#include "objbase.h"
#include "uiautomation.h"
namespace mozilla::a11y {
class TableCellAccessible;
/**
* IGridItemProvider and ITableItemProvider implementations.
*/
class UiaGridItem : public IGridItemProvider, public ITableItemProvider {
public:
// IGridItemProvider
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Row(
/* [retval][out] */ __RPC__out int* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_Column(
/* [retval][out] */ __RPC__out int* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_RowSpan(
/* [retval][out] */ __RPC__out int* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ColumnSpan(
/* [retval][out] */ __RPC__out int* aRetVal);
virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ContainingGrid(
/* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple**
aRetVal);
// ITableItemProvider
virtual HRESULT STDMETHODCALLTYPE GetRowHeaderItems(
/* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal);
virtual HRESULT STDMETHODCALLTYPE GetColumnHeaderItems(
/* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRetVal);
private:
TableCellAccessible* CellAcc();
};
} // namespace mozilla::a11y
#endif

View file

@ -5,6 +5,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
SOURCES += [
"UiaGrid.cpp",
"UiaGridItem.cpp",
"uiaRawElmProvider.cpp",
"UiaRoot.cpp",
]
@ -13,6 +15,7 @@ LOCAL_INCLUDES += [
"/accessible/base",
"/accessible/generic",
"/accessible/html",
"/accessible/windows/ia2",
"/accessible/windows/msaa",
"/accessible/xpcom",
"/accessible/xul",

View file

@ -13,6 +13,8 @@
#include "AccessibleWrap.h"
#include "ApplicationAccessible.h"
#include "ARIAMap.h"
#include "ia2AccessibleTable.h"
#include "ia2AccessibleTableCell.h"
#include "LocalAccessible-inl.h"
#include "mozilla/a11y/RemoteAccessible.h"
#include "mozilla/StaticPrefs_accessibility.h"
@ -277,6 +279,19 @@ uiaRawElmProvider::GetPatternProvider(
expand.forget(aPatternProvider);
}
return S_OK;
case UIA_GridPatternId:
if (acc->IsTable()) {
auto grid = GetPatternFromDerived<ia2AccessibleTable, IGridProvider>();
grid.forget(aPatternProvider);
}
return S_OK;
case UIA_GridItemPatternId:
if (acc->IsTableCell()) {
auto item =
GetPatternFromDerived<ia2AccessibleTableCell, IGridItemProvider>();
item.forget(aPatternProvider);
}
return S_OK;
case UIA_InvokePatternId:
// Per the UIA documentation, we should only expose the Invoke pattern "if
// the same behavior is not exposed through another control pattern
@ -298,6 +313,20 @@ uiaRawElmProvider::GetPatternProvider(
scroll.forget(aPatternProvider);
return S_OK;
}
case UIA_TablePatternId:
if (acc->IsTable()) {
auto table =
GetPatternFromDerived<ia2AccessibleTable, ITableProvider>();
table.forget(aPatternProvider);
}
return S_OK;
case UIA_TableItemPatternId:
if (acc->IsTableCell()) {
auto item =
GetPatternFromDerived<ia2AccessibleTableCell, ITableItemProvider>();
item.forget(aPatternProvider);
}
return S_OK;
case UIA_TogglePatternId:
if (HasTogglePattern()) {
RefPtr<IToggleProvider> toggle = this;
@ -998,3 +1027,32 @@ bool uiaRawElmProvider::HasValuePattern() const {
const nsRoleMapEntry* roleMapEntry = acc->ARIARoleMap();
return roleMapEntry && roleMapEntry->Is(nsGkAtoms::textbox);
}
template <class Derived, class Interface>
RefPtr<Interface> uiaRawElmProvider::GetPatternFromDerived() {
// MsaaAccessible inherits from uiaRawElmProvider. Derived
// inherits from MsaaAccessible and Interface. The compiler won't let us
// directly static_cast to Interface, hence the intermediate casts.
auto* msaa = static_cast<MsaaAccessible*>(this);
auto* derived = static_cast<Derived*>(msaa);
return derived;
}
SAFEARRAY* a11y::AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs) {
if (aAccs.IsEmpty()) {
// The UIA documentation is unclear about this, but the UIA client
// framework seems to treat a null value the same as an empty array. This
// is also what Chromium does.
return nullptr;
}
SAFEARRAY* uias = SafeArrayCreateVector(VT_UNKNOWN, 0, aAccs.Length());
LONG indices[1] = {0};
for (Accessible* acc : aAccs) {
// SafeArrayPutElement calls AddRef on the element, so we use a raw pointer
// here.
IRawElementProviderSimple* uia = MsaaAccessible::GetFrom(acc);
SafeArrayPutElement(uias, indices, uia);
++indices[0];
}
return uias;
}

View file

@ -11,6 +11,11 @@
#include <stdint.h>
#include <uiautomation.h>
template <class T>
class nsTArray;
template <class T>
class RefPtr;
namespace mozilla {
namespace a11y {
@ -152,8 +157,12 @@ class uiaRawElmProvider : public IAccessibleEx,
bool HasTogglePattern();
bool HasExpandCollapsePattern();
bool HasValuePattern() const;
template <class Derived, class Interface>
RefPtr<Interface> GetPatternFromDerived();
};
SAFEARRAY* AccessibleArrayToUiaArray(const nsTArray<Accessible*>& aAccs);
} // namespace a11y
} // namespace mozilla

View file

@ -422,13 +422,7 @@ pref("browser.urlbar.suggest.engines", true);
pref("browser.urlbar.suggest.calculator", false);
pref("browser.urlbar.suggest.recentsearches", true);
#if defined(EARLY_BETA_OR_EARLIER)
// Enable QuickActions and its urlbar search mode button.
pref("browser.urlbar.quickactions.enabled", true);
pref("browser.urlbar.suggest.quickactions", true);
pref("browser.urlbar.shortcuts.quickactions", true);
pref("browser.urlbar.quickactions.showPrefs", true);
#endif
pref("browser.urlbar.secondaryActions.featureGate", false);
#if defined(EARLY_BETA_OR_EARLIER)
// Enable Trending suggestions.
@ -1985,7 +1979,13 @@ pref("browser.translations.newSettingsUI.enable", false);
// Enable Firefox Select translations powered by Bergamot translations
// engine https://browser.mt/.
pref("browser.translations.select.enable", false);
#if defined(EARLY_BETA_OR_EARLIER)
// Enables Select Translations for Early Beta and Nightly.
pref("browser.translations.select.enable", true);
#else
// Disables Select Translations for Late Beta and Release.
pref("browser.translations.select.enable", false);
#endif
// Telemetry settings.
// Determines if Telemetry pings can be archived locally.

View file

@ -205,10 +205,10 @@
<hbox id="this-profile-buttons">
<toolbarbutton id="profiles-edit-this-delete-button"
class="subviewbutton toolbarbutton-1"
oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
/>
<toolbarbutton id="profiles-delete-this-profile-button"
class="subviewbutton toolbarbutton-1"
oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
/>
</hbox>
</vbox>
<toolbarseparator/>
@ -218,15 +218,15 @@
class="subviewbutton"
data-l10n-id="appmenu-close-profile"
data-l10n-args='{ "profilename": "" }'
oncommand=""/>
/>
<toolbarbutton id="profiles-create-profile-button"
class="subviewbutton"
data-l10n-id="appmenu-create-profile"
oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
/>
<toolbarbutton id="profiles-manage-profiles-button"
class="subviewbutton"
data-l10n-id="appmenu-manage-profiles"
oncommand="switchToTabHavingURI('about:profilemanager', true)"/>
/>
</vbox>
</panelview>
@ -448,7 +448,6 @@
<toolbarbutton id="PanelUI-remotetabs-syncnow"
align="center"
class="subviewbutton"
onmouseover="gSync.refreshSyncButtonsTooltip();"
closemenu="none">
<hbox flex="1">
<image class="syncNowBtn"/>
@ -586,7 +585,6 @@
<toolbarbutton id="PanelUI-fxa-menu-syncnow-button"
align="center"
class="subviewbutton"
onmouseover="gSync.refreshSyncButtonsTooltip();"
closemenu="none">
<hbox flex="1">
<image id="PanelUI-appMenu-fxa-image-last-synced"
@ -696,7 +694,7 @@
<toolbarbutton id="PanelUI-fxa-menu-sendtab-not-configured-button"
class="PanelUI-fxa-signin-button"
data-l10n-id="appmenuitem-fxa-sign-in"
oncommand="gSync.openPrefsFromFxaMenu('send_tab', this);"/>
/>
</vbox>
</panelview>
@ -707,7 +705,7 @@
<toolbarbutton id="PanelUI-fxa-menu-sendtab-connect-device-button"
class="PanelUI-fxa-signin-button"
data-l10n-id="appmenu-remote-tabs-connectdevice"
oncommand="gSync.openConnectAnotherDeviceFromFxaMenu(this);"/>
/>
</vbox>
</panelview>
@ -743,12 +741,12 @@
<button id="reset-pbm-panel-cancel-button"
class="footer-button"
data-l10n-id="reset-pbm-panel-cancel-button"
oncommand="ResetPBMPanel.onCancel(this)"></button>
></button>
<button slot="primary"
id="reset-pbm-panel-confirm-button"
class="footer-button"
data-l10n-id="reset-pbm-panel-confirm-button"
oncommand="ResetPBMPanel.onConfirm(this)"></button>
></button>
</html:moz-button-group>
</vbox>
</panelview>

View file

@ -419,7 +419,6 @@ var gSync = {
"browser/accounts.ftl",
"browser/appmenu.ftl",
"browser/sync.ftl",
"toolkit/branding/accounts.ftl",
],
true
));
@ -569,6 +568,18 @@ var gSync = {
fxaPanelView.addEventListener("ViewShowing", this);
fxaPanelView.addEventListener("ViewHiding", this);
fxaPanelView.addEventListener("command", this);
PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-syncnow-button"
).addEventListener("mouseover", this);
PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-sendtab-not-configured-button"
).addEventListener("command", this);
PanelMultiView.getViewNode(
document,
"PanelUI-fxa-menu-sendtab-connect-device-button"
).addEventListener("command", this);
// If the experiment is enabled, we'll need to update the panels
// to show some different text to the user
@ -594,6 +605,9 @@ var gSync = {
handleEvent(event) {
switch (event.type) {
case "mouseover":
this.refreshSyncButtonsTooltip();
break;
case "command": {
this.onCommand(event.target);
break;
@ -648,24 +662,27 @@ var gSync = {
onCommand(button) {
switch (button.id) {
case "PanelUI-fxa-menu-sync-prefs-button":
// fall through
case "PanelUI-fxa-menu-setup-sync-button":
this.openPrefsFromFxaMenu("sync_settings", button);
break;
case "PanelUI-fxa-menu-sendtab-connect-device-button":
// fall through
case "PanelUI-fxa-menu-connect-device-button":
this.openConnectAnotherDeviceFromFxaMenu(button);
break;
case "fxa-manage-account-button":
this.clickFxAMenuHeaderButton(button);
break;
case "PanelUI-fxa-menu-syncnow-button":
this.doSyncFromFxaMenu(button);
break;
case "PanelUI-fxa-menu-setup-sync-button":
this.openPrefsFromFxaMenu("sync_settings", button);
break;
case "PanelUI-fxa-menu-connect-device-button":
this.openConnectAnotherDeviceFromFxaMenu(button);
break;
case "PanelUI-fxa-menu-sendtab-button":
this.showSendToDeviceViewFromFxaMenu(button);
break;
case "PanelUI-fxa-menu-sync-prefs-button":
this.openPrefsFromFxaMenu("sync_settings", button);
break;
case "PanelUI-fxa-menu-account-signout-button":
this.disconnect();
break;
@ -681,6 +698,9 @@ var gSync = {
case "PanelUI-fxa-menu-vpn-button":
this.openVPNLink(button);
break;
case "PanelUI-fxa-menu-sendtab-not-configured-button":
this.openPrefsFromFxaMenu("send_tab", button);
break;
}
},
@ -690,10 +710,11 @@ var gSync = {
return;
}
switch (topic) {
case UIState.ON_UPDATE:
case UIState.ON_UPDATE: {
const state = UIState.get();
this.updateAllUI(state);
break;
}
case "quit-application":
// Stop the animation timer on shutdown, since we can't update the UI
// after this.

View file

@ -83,7 +83,6 @@
<link rel="localization" href="browser/translations.ftl" />
<link rel="localization" href="browser/unifiedExtensions.ftl"/>
<link rel="localization" href="browser/webrtcIndicator.ftl"/>
<link rel="localization" href="toolkit/branding/accounts.ftl"/>
<link rel="localization" href="toolkit/branding/brandings.ftl"/>
<link rel="localization" href="toolkit/global/contextual-identity.ftl"/>
<link rel="localization" href="toolkit/global/textActions.ftl"/>

View file

@ -16,7 +16,6 @@
<html:link rel="localization" href="browser/menubar.ftl"/>
<html:link rel="localization" href="browser/reportBrokenSite.ftl"/>
<html:link rel="localization" href="browser/screenshots.ftl"/>
<html:link rel="localization" href="toolkit/branding/accounts.ftl"/>
<html:link rel="localization" href="toolkit/branding/brandings.ftl"/>
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
</linkset>

View file

@ -678,4 +678,14 @@
<menuitem data-l10n-id="translations-panel-settings-about2"
oncommand="FullPageTranslationsPanel.onAboutTranslations()"/>
</menupopup>
<menupopup id="select-translations-panel-settings-menupopup">
<menuitem id="select-translations-panel-open-settings-page-menuitem"
class="manage-languages-menuitem"
data-l10n-id="select-translations-panel-open-translations-settings-menuitem"
oncommand="SelectTranslationsPanel.openTranslationsSettingsPage()"/>
<menuitem id="select-translations-panel-about-translations-menuitem"
data-l10n-id="translations-panel-settings-about2"
oncommand="SelectTranslationsPanel.onAboutTranslations()"/>
</menupopup>
</popupset>

View file

@ -2541,7 +2541,7 @@ class nsContextMenu {
screenY,
this.#getTextToTranslate(),
this.#translationsLangPairPromise
);
).catch(console.error);
}
/**

View file

@ -112,6 +112,8 @@ add_task(async function test_xul_text_link_label() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
// Clean up so won't affect HTML element test cases.
@ -204,6 +206,8 @@ const kLinkItems = [
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
];
add_task(async function test_link() {
@ -234,6 +238,8 @@ add_task(async function test_mailto() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
});
@ -247,6 +253,8 @@ add_task(async function test_tel() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
});
@ -1336,6 +1344,8 @@ add_task(async function test_select_text() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
"---",
null,
"context-viewpartialsource-selection",
@ -1371,6 +1381,10 @@ add_task(async function test_select_text_search_service_not_initialized() {
true,
"---",
null,
"context-translate-selection",
true,
"---",
null,
"context-viewpartialsource-selection",
true,
],
@ -1423,6 +1437,8 @@ add_task(async function test_select_text_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
"---",
null,
"context-viewpartialsource-selection",
@ -1490,6 +1506,10 @@ add_task(async function test_imagelink() {
null,
"context-setDesktopBackground",
true,
"---",
null,
"context-translate-selection",
true,
]);
});
@ -1682,6 +1702,8 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
await test_contextmenu("#svg-with-link2 > a", [
@ -1711,6 +1733,8 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
await test_contextmenu("#svg-with-link3 > a", [
@ -1740,6 +1764,8 @@ add_task(async function test_svg_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
});
@ -1771,6 +1797,8 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
await test_contextmenu("#svg-with-relative-link2 > a", [
@ -1800,6 +1828,8 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
await test_contextmenu("#svg-with-relative-link3 > a", [
@ -1829,6 +1859,8 @@ add_task(async function test_svg_relative_link() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
});
@ -1898,6 +1930,8 @@ add_task(async function test_background_image() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
// Don't show image related context menu commands when there is a selection
@ -1921,6 +1955,8 @@ add_task(async function test_background_image() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
"---",
null,
"context-viewpartialsource-selection",
@ -1989,6 +2025,8 @@ add_task(async function test_strip_on_share_on_secure_about_page() {
true,
"context-searchselect-private",
true,
"context-translate-selection",
true,
]);
// Clean up

View file

@ -122,6 +122,7 @@ add_task(async function test_link_contextmenu() {
"context-sendlinktodevice",
"context-sep-sendlinktodevice",
"context-searchselect",
"context-translate-selection",
"frame-sep"
);

View file

@ -124,11 +124,7 @@ XPCOMUtils.defineLazyServiceGetters(lazy, {
ChromeUtils.defineLazyGetter(
lazy,
"accountsL10n",
() =>
new Localization(
["browser/accounts.ftl", "toolkit/branding/accounts.ftl"],
true
)
() => new Localization(["browser/accounts.ftl"], true)
);
if (AppConstants.ENABLE_WEBDRIVER) {
@ -3728,7 +3724,7 @@ BrowserGlue.prototype = {
_onThisDeviceConnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
"account-connection-title",
"account-connection-title-2",
"account-connection-connected",
]);
@ -4853,7 +4849,7 @@ BrowserGlue.prototype = {
_onDeviceConnected(deviceName) {
const [title, body] = lazy.accountsL10n.formatValuesSync([
{ id: "account-connection-title" },
{ id: "account-connection-title-2" },
deviceName
? { id: "account-connection-connected-with", args: { deviceName } }
: { id: "account-connection-connected-with-noname" },
@ -4890,7 +4886,7 @@ BrowserGlue.prototype = {
_onDeviceDisconnected() {
const [title, body] = lazy.accountsL10n.formatValuesSync([
"account-connection-title",
"account-connection-title-2",
"account-connection-disconnected",
]);

View file

@ -11,7 +11,6 @@
<title data-l10n-id="about-logins-page-title-name"></title>
<link rel="localization" href="branding/brand.ftl">
<link rel="localization" href="browser/aboutLogins.ftl">
<link rel="localization" href="toolkit/branding/accounts.ftl">
<link rel="localization" href="toolkit/branding/brandings.ftl">
<script type="module" src="chrome://browser/content/aboutlogins/components/confirmation-dialog.mjs"></script>
<script type="module" src="chrome://browser/content/aboutlogins/components/remove-logins-dialog.mjs"></script>

View file

@ -14,7 +14,6 @@
<title data-l10n-id="about-logins-import-report-page-title"></title>
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/aboutLogins.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<script
type="module"

View file

@ -27,7 +27,6 @@
<link rel="localization" href="browser/newtab/onboarding.ftl" />
<link rel="localization" href="browser/spotlight.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
</head>
<body>

View file

@ -52,7 +52,6 @@ const L10N = new Localization([
"branding/brand.ftl",
"browser/newtab/onboarding.ftl",
"toolkit/branding/brandings.ftl",
"toolkit/branding/accounts.ftl",
]);
const HOMEPAGE_PREF = "browser.startup.homepage";

View file

@ -37,7 +37,7 @@ const MESSAGES = () => [
},
sumo_path: "https://example.com",
},
text: { string_id: "cfr-doorhanger-bookmark-fxa-body" },
text: { string_id: "cfr-doorhanger-bookmark-fxa-body-2" },
icon: "chrome://branding/content/icon64.png",
icon_class: "cfr-doorhanger-large-icon",
persistent_doorhanger: true,

View file

@ -204,7 +204,6 @@ export class _RemoteL10n {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
"toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
],
false

View file

@ -81,7 +81,6 @@ describe("RemoteL10n", () => {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
"toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
]);
assert.isFalse(args[1]);
@ -103,7 +102,6 @@ describe("RemoteL10n", () => {
"branding/brand.ftl",
"browser/defaultBrowserNotification.ftl",
"browser/newtab/asrouter.ftl",
"toolkit/branding/accounts.ftl",
"toolkit/branding/brandings.ftl",
]);
assert.isFalse(args[1]);

View file

@ -146,6 +146,12 @@ export class BackupService {
}
}
/**
* @typedef {object} CreateBackupResult
* @property {string} stagingPath
* The staging path for where the backup was created.
*/
/**
* Create a backup of the user's profile.
*
@ -154,13 +160,15 @@ export class BackupService {
* @param {string} [options.profilePath=PathUtils.profileDir]
* The path to the profile to backup. By default, this is the current
* profile.
* @returns {Promise<undefined>}
* @returns {Promise<CreateBackupResult|null>}
* A promise that resolves to an object containing the path to the staging
* folder where the backup was created, or null if the backup failed.
*/
async createBackup({ profilePath = PathUtils.profileDir } = {}) {
// createBackup does not allow re-entry or concurrent backups.
if (this.#backupInProgress) {
lazy.logConsole.warn("Backup attempt already in progress");
return;
return null;
}
this.#backupInProgress = true;
@ -180,8 +188,15 @@ export class BackupService {
let stagingPath = await this.#prepareStagingFolder(backupDirPath);
// Sort resources be priority.
let sortedResources = Array.from(this.#resources.values()).sort(
(a, b) => {
return b.priority - a.priority;
}
);
// Perform the backup for each resource.
for (let resourceClass of this.#resources.values()) {
for (let resourceClass of sortedResources) {
try {
lazy.logConsole.debug(
`Backing up resource with key ${resourceClass.key}. ` +
@ -197,11 +212,19 @@ export class BackupService {
resourcePath,
profilePath
);
lazy.logConsole.debug(
`Backup of resource with key ${resourceClass.key} completed`,
manifestEntry
);
manifest.resources[resourceClass.key] = manifestEntry;
if (manifestEntry === undefined) {
lazy.logConsole.error(
`Backup of resource with key ${resourceClass.key} returned undefined
as its ManifestEntry instead of null or an object`
);
} else {
lazy.logConsole.debug(
`Backup of resource with key ${resourceClass.key} completed`,
manifestEntry
);
manifest.resources[resourceClass.key] = manifestEntry;
}
} catch (e) {
lazy.logConsole.error(
`Failed to backup resource: ${resourceClass.key}`,
@ -238,6 +261,13 @@ export class BackupService {
BackupService.MANIFEST_FILE_NAME
);
await IOUtils.writeJSON(manifestPath, manifest);
let renamedStagingPath = await this.#finalizeStagingFolder(stagingPath);
lazy.logConsole.log(
"Wrote backup to staging directory at ",
renamedStagingPath
);
return { stagingPath: renamedStagingPath };
} finally {
this.#backupInProgress = false;
}
@ -266,6 +296,62 @@ export class BackupService {
return stagingPath;
}
/**
* Renames the staging folder to an ISO 8601 date string with dashes replacing colons and fractional seconds stripped off.
* The ISO date string should be formatted from YYYY-MM-DDTHH:mm:ss.sssZ to YYYY-MM-DDTHH-mm-ssZ
*
* @param {string} stagingPath
* The path to the populated staging folder.
* @returns {Promise<string|null>}
* The path to the renamed staging folder, or null if the stagingPath was
* not pointing to a valid folder.
*/
async #finalizeStagingFolder(stagingPath) {
if (!(await IOUtils.exists(stagingPath))) {
// If we somehow can't find the specified staging folder, cancel this step.
lazy.logConsole.error(
`Failed to finalize staging folder. Cannot find ${stagingPath}.`
);
return null;
}
try {
lazy.logConsole.debug("Finalizing and renaming staging folder");
let currentDateISO = new Date().toISOString();
// First strip the fractional seconds
let dateISOStripped = currentDateISO.replace(/\.\d+\Z$/, "Z");
// Now replace all colons with dashes
let dateISOFormatted = dateISOStripped.replaceAll(":", "-");
let stagingPathParent = PathUtils.parent(stagingPath);
let renamedBackupPath = PathUtils.join(
stagingPathParent,
dateISOFormatted
);
await IOUtils.move(stagingPath, renamedBackupPath);
let existingBackups = await IOUtils.getChildren(stagingPathParent);
/**
* Bug 1892532: for now, we only support a single backup file.
* If there are other pre-existing backup folders, delete them.
*/
for (let existingBackupPath of existingBackups) {
if (existingBackupPath !== renamedBackupPath) {
await IOUtils.remove(existingBackupPath, {
recursive: true,
});
}
}
return renamedBackupPath;
} catch (e) {
lazy.logConsole.error(
`Something went wrong while finalizing the staging folder. ${e}`
);
throw e;
}
}
/**
* Creates and returns a backup manifest object with an empty resources
* property.

View file

@ -12,6 +12,7 @@ JAR_MANIFESTS += ["jar.mn"]
SPHINX_TREES["docs"] = "docs"
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
MARIONETTE_MANIFESTS += ["tests/marionette/manifest.toml"]
EXTRA_JS_MODULES.backup += [
"BackupResources.sys.mjs",

View file

@ -63,6 +63,8 @@ export class AddonsBackupResource extends BackupResource {
stagingPath,
databases
);
return null;
}
async measure(profilePath = PathUtils.profileDir) {

View file

@ -57,6 +57,19 @@ export class BackupResource {
);
}
/**
* This can be overridden to return a number indicating the priority the
* resource should have in the backup order.
*
* Resources with a higher priority will be backed up first.
* The default priority of 0 indicates it can be processed in any order.
*
* @returns {number}
*/
static get priority() {
return 0;
}
/**
* Get the size of a file.
*
@ -140,7 +153,8 @@ export class BackupResource {
/**
* Copy a set of SQLite databases safely from a source directory to a
* destination directory. A new read-only connection is opened for each
* database, and then a backup is created.
* database, and then a backup is created. If the source database does not
* exist, it is ignored.
*
* @param {string} sourcePath
* Path to the source directory of the SQLite databases.
@ -154,6 +168,11 @@ export class BackupResource {
static async copySqliteDatabases(sourcePath, destPath, sqliteDatabases) {
for (let fileName of sqliteDatabases) {
let sourceFilePath = PathUtils.join(sourcePath, fileName);
if (!(await IOUtils.exists(sourceFilePath))) {
continue;
}
let destFilePath = PathUtils.join(destPath, fileName);
let connection;
@ -215,12 +234,12 @@ export class BackupResource {
}
/**
* Perform a safe copy of the resource(s) and write them into the backup
* database. The Promise should resolve with an object that can be serialized
* to JSON, as it will be written to the manifest file. This same object will
* be deserialized and passed to restore() when restoring the backup. This
* object can be null if no additional information is needed to restore the
* backup.
* Perform a safe copy of the datastores that this resource manages and write
* them into the backup database. The Promise should resolve with an object
* that can be serialized to JSON, as it will be written to the manifest file.
* This same object will be deserialized and passed to restore() when
* restoring the backup. This object can be null if no additional information
* is needed to restore the backup.
*
* @param {string} stagingPath
* The path to the staging folder where copies of the datastores for this
@ -238,6 +257,60 @@ export class BackupResource {
async backup(stagingPath, profilePath = null) {
throw new Error("BackupResource::backup must be overridden");
}
/**
* Recovers the datastores that this resource manages from a backup archive
* that has been decompressed into the recoveryPath. A pre-existing unlocked
* user profile should be available to restore into, and destProfilePath
* should point at its location on the file system.
*
* This method is not expected to be running in an app connected to the
* destProfilePath. If the BackupResource needs to run some operations
* while attached to the recovery profile, it should do that work inside of
* postRecovery(). If data needs to be transferred to postRecovery(), it
* should be passed as a JSON serializable object in the return value of this
* method.
*
* @see BackupResource.postRecovery()
* @param {object|null} manifestEntry
* The object that was returned by the backup() method when the backup was
* created. This object can be null if no additional information was needed
* for recovery.
* @param {string} recoveryPath
* The path to the resource directory where the backup archive has been
* decompressed.
* @param {string} destProfilePath
* The path to the profile directory where the backup should be restored to.
* @returns {Promise<object|null>}
* This should return a JSON serializable object that will be passed to
* postRecovery() if any data needs to be passed to it. This object can be
* null if no additional information is needed for postRecovery().
*/
// eslint-disable-next-line no-unused-vars
async recover(manifestEntry, recoveryPath, destProfilePath) {
throw new Error("BackupResource::recover must be overridden");
}
/**
* Perform any post-recovery operations that need to be done after the
* recovery has been completed and the recovered profile has been attached
* to.
*
* This method is running in an app connected to the recovered profile. The
* profile is locked, but this postRecovery method can be used to insert
* data into connected datastores, or perform any other operations that can
* only occur within the context of the recovered profile.
*
* @see BackupResource.recover()
* @param {object|null} postRecoveryEntry
* The object that was returned by the recover() method when the recovery
* was originally done. This object can be null if no additional information
* is needed for post-recovery.
*/
// eslint-disable-next-line no-unused-vars
async postRecovery(postRecoveryEntry) {
// no-op by default
}
}
XPCOMUtils.defineLazyPreferenceGetter(

View file

@ -20,6 +20,7 @@ export class CookiesBackupResource extends BackupResource {
await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
"cookies.sqlite",
]);
return null;
}
async measure(profilePath = PathUtils.profileDir) {

View file

@ -20,6 +20,8 @@ export class FormHistoryBackupResource extends BackupResource {
await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
"formhistory.sqlite",
]);
return null;
}
async measure(profilePath = PathUtils.profileDir) {

View file

@ -37,8 +37,11 @@ export class PlacesBackupResource extends BackupResource {
return false;
}
static get priority() {
return 1;
}
async backup(stagingPath, profilePath = PathUtils.profileDir) {
const sqliteDatabases = ["places.sqlite", "favicons.sqlite"];
let canBackupHistory =
!lazy.PrivateBrowsingUtils.permanentPrivateBrowsing &&
!lazy.isSanitizeOnShutdownEnabled &&
@ -59,11 +62,18 @@ export class PlacesBackupResource extends BackupResource {
return { bookmarksOnly: true };
}
await BackupResource.copySqliteDatabases(
profilePath,
stagingPath,
sqliteDatabases
);
// These are copied in parallel because they're attached[1], and we don't
// want them to get out of sync with one another.
//
// [1]: https://www.sqlite.org/lang_attach.html
await Promise.all([
BackupResource.copySqliteDatabases(profilePath, stagingPath, [
"places.sqlite",
]),
BackupResource.copySqliteDatabases(profilePath, stagingPath, [
"favicons.sqlite",
]),
]);
return null;
}

View file

@ -41,6 +41,8 @@ export class SessionStoreBackupResource extends BackupResource {
await BackupResource.copyFiles(profilePath, stagingPath, [
"sessionstore-backups",
]);
return null;
}
async measure(profilePath = PathUtils.profileDir) {

View file

@ -0,0 +1,5 @@
[DEFAULT]
run-if = ["buildapp == 'browser'"]
prefs = ["browser.backup.enabled=true", "browser.backup.log=true"]
["test_backup.py"]

View file

@ -0,0 +1,98 @@
# 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/.
import json
import os
from marionette_harness import MarionetteTestCase
class BackupTest(MarionetteTestCase):
def setUp(self):
MarionetteTestCase.setUp(self)
# We need to quit the browser and restart with the browser.backup.log
# pref already set to true in order for it to be displayed.
self.marionette.quit()
self.marionette.instance.prefs = {
"browser.backup.log": True,
}
# Now restart the browser.
self.marionette.instance.switch_profile()
self.marionette.start_session()
def test_backup(self):
self.marionette.set_context("chrome")
# In bug 1892105, we'll update this test to write some values to various
# datastores, like bookmarks, passwords, cookies, etc. Then we'll run
# a backup and ensure that the data can be recovered from the backup.
resourceKeys = self.marionette.execute_script(
"""
const DefaultBackupResources = ChromeUtils.importESModule("resource:///modules/backup/BackupResources.sys.mjs");
let resourceKeys = [];
for (const resourceName in DefaultBackupResources) {
let resource = DefaultBackupResources[resourceName];
resourceKeys.push(resource.key);
}
return resourceKeys;
"""
)
stagingPath = self.marionette.execute_async_script(
"""
const { BackupService } = ChromeUtils.importESModule("resource:///modules/backup/BackupService.sys.mjs");
let bs = BackupService.init();
if (!bs) {
throw new Error("Could not get initialized BackupService.");
}
let [outerResolve] = arguments;
(async () => {
let { stagingPath } = await bs.createBackup();
if (!stagingPath) {
throw new Error("Could not create backup.");
}
return stagingPath;
})().then(outerResolve);
"""
)
# First, ensure that the staging path exists
self.assertTrue(os.path.exists(stagingPath))
# Now, ensure that the backup-manifest.json file exists within it.
manifestPath = os.path.join(stagingPath, "backup-manifest.json")
self.assertTrue(os.path.exists(manifestPath))
# For now, we just do a cursory check to ensure that for the resources
# that are listed in the manifest as having been backed up, that we
# have at least one file in their respective staging directories.
# We don't check the contents of the files, just that they exist.
# Read the JSON manifest file
with open(manifestPath, "r") as f:
manifest = json.load(f)
# Ensure that the manifest has a "resources" key
self.assertIn("resources", manifest)
resources = manifest["resources"]
self.assertTrue(isinstance(resources, dict))
self.assertTrue(len(resources) > 0)
# We don't have encryption capabilities wired up yet, so we'll check
# that all default resources are represented in the manifest.
self.assertEqual(len(resources), len(resourceKeys))
for resourceKey in resourceKeys:
self.assertIn(resourceKey, resources)
# Iterate the resources dict keys
for resourceKey in resources:
print("Checking resource: %s" % resourceKey)
# Ensure that there are staging directories created for each
# resource that was backed up
resourceStagingDir = os.path.join(stagingPath, resourceKey)
self.assertTrue(os.path.exists(resourceStagingDir))
# In bug 1892105, we'll update this test to then recover from this
# staging directory and ensure that the recovered data is what we
# expect.

View file

@ -50,6 +50,9 @@ class FakeBackupResource2 extends BackupResource {
static get requiresEncryption() {
return true;
}
static get priority() {
return 1;
}
}
/**
@ -62,6 +65,9 @@ class FakeBackupResource3 extends BackupResource {
static get requiresEncryption() {
return false;
}
static get priority() {
return 2;
}
}
/**

View file

@ -302,7 +302,7 @@ add_task(async function test_backup() {
"AddonsBackupResource-staging-test"
);
const files = [
const simpleCopyFiles = [
{ path: "extensions.json" },
{ path: "extension-settings.json" },
{ path: "extension-preferences.json" },
@ -316,20 +316,33 @@ add_task(async function test_backup() {
{ path: ["extension-store-permissions", "data.safe.bin"] },
{ path: ["extensions", "{11aa1234-f111-1234-abcd-a9b8c7654d32}.xpi"] },
];
await createTestFiles(sourcePath, files);
await createTestFiles(sourcePath, simpleCopyFiles);
const junkFiles = [{ path: ["extensions", "junk"] }];
await createTestFiles(sourcePath, junkFiles);
// Create a fake storage-sync-v2 database file. We don't expect this to
// be copied to the staging directory in this test due to our stubbing
// of the backup method, so we don't include it in `simpleCopyFiles`.
await createTestFiles(sourcePath, [{ path: "storage-sync-v2.sqlite" }]);
let fakeConnection = {
backup: sandbox.stub().resolves(true),
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await addonsBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await addonsBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"AddonsBackupResource.backup should return null as its ManifestEntry"
);
await assertFilesExist(stagingPath, files);
await assertFilesExist(stagingPath, simpleCopyFiles);
let junkFile = PathUtils.join(stagingPath, "extensions", "junk");
Assert.equal(

View file

@ -101,6 +101,10 @@ add_task(async function test_copySqliteDatabases() {
"BackupResource-dest-test"
);
let pretendDatabases = ["places.sqlite", "favicons.sqlite"];
await createTestFiles(
sourcePath,
pretendDatabases.map(f => ({ path: f }))
);
let fakeConnection = {
backup: sandbox.stub().resolves(true),

View file

@ -61,6 +61,10 @@ add_task(async function test_backup() {
"CookiesBackupResource-staging-test"
);
// Make sure this file exists in the source directory, otherwise
// BackupResource will skip attempting to back it up.
await createTestFiles(sourcePath, [{ path: "cookies.sqlite" }]);
// We have no need to test that Sqlite.sys.mjs's backup method is working -
// this is something that is tested in Sqlite's own tests. We can just make
// sure that it's being called using sinon. Unfortunately, we cannot do the
@ -71,7 +75,15 @@ add_task(async function test_backup() {
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await cookiesBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await cookiesBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"CookiesBackupResource.backup should return null as its ManifestEntry"
);
// Next, we'll make sure that the Sqlite connection had `backup` called on it
// with the right arguments.

View file

@ -104,6 +104,15 @@ add_task(async function test_backup() {
];
await createTestFiles(sourcePath, simpleCopyFiles);
// Create our fake database files. We don't expect these to be copied to the
// staging directory in this test due to our stubbing of the backup method, so
// we don't include it in `simpleCopyFiles`.
await createTestFiles(sourcePath, [
{ path: "cert9.db" },
{ path: "key4.db" },
{ path: "credentialstate.sqlite" },
]);
// We have no need to test that Sqlite.sys.mjs's backup method is working -
// this is something that is tested in Sqlite's own tests. We can just make
// sure that it's being called using sinon. Unfortunately, we cannot do the
@ -114,7 +123,16 @@ add_task(async function test_backup() {
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await credentialsAndSecurityBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await credentialsAndSecurityBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"CredentialsAndSecurityBackupResource.backup should return null as its ManifestEntry"
);
await assertFilesExist(stagingPath, simpleCopyFiles);

View file

@ -65,6 +65,10 @@ add_task(async function test_backup() {
"FormHistoryBackupResource-staging-test"
);
// Make sure this file exists in the source directory, otherwise
// BackupResource will skip attempting to back it up.
await createTestFiles(sourcePath, [{ path: "formhistory.sqlite" }]);
// We have no need to test that Sqlite.sys.mjs's backup method is working -
// this is something that is tested in Sqlite's own tests. We can just make
// sure that it's being called using sinon. Unfortunately, we cannot do the
@ -75,7 +79,15 @@ add_task(async function test_backup() {
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await formHistoryBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await formHistoryBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"FormHistoryBackupResource.backup should return null as its ManifestEntry"
);
// Next, we'll make sure that the Sqlite connection had `backup` called on it
// with the right arguments.

View file

@ -79,6 +79,11 @@ add_task(async function test_backup() {
];
await createTestFiles(sourcePath, simpleCopyFiles);
// Create our fake database files. We don't expect this to be copied to the
// staging directory in this test due to our stubbing of the backup method, so
// we don't include it in `simpleCopyFiles`.
await createTestFiles(sourcePath, [{ path: "protections.sqlite" }]);
// We have no need to test that Sqlite.sys.mjs's backup method is working -
// this is something that is tested in Sqlite's own tests. We can just make
// sure that it's being called using sinon. Unfortunately, we cannot do the
@ -101,7 +106,15 @@ add_task(async function test_backup() {
.withArgs("snippets")
.resolves(snippetsTableStub);
await miscDataBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await miscDataBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"MiscDataBackupResource.backup should return null as its ManifestEntry"
);
await assertFilesExist(stagingPath, simpleCopyFiles);

View file

@ -93,13 +93,28 @@ add_task(async function test_backup() {
"PlacesBackupResource-staging-test"
);
// Make sure these files exist in the source directory, otherwise
// BackupResource will skip attempting to back them up.
await createTestFiles(sourcePath, [
{ path: "places.sqlite" },
{ path: "favicons.sqlite" },
]);
let fakeConnection = {
backup: sandbox.stub().resolves(true),
close: sandbox.stub().resolves(true),
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await placesBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await placesBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"PlacesBackupResource.backup should return null as its ManifestEntry"
);
Assert.ok(
fakeConnection.backup.calledTwice,
@ -154,7 +169,16 @@ add_task(async function test_backup_no_saved_history() {
Services.prefs.setBoolPref(HISTORY_ENABLED_PREF, false);
Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, false);
await placesBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await placesBackupResource.backup(
stagingPath,
sourcePath
);
Assert.deepEqual(
manifestEntry,
{ bookmarksOnly: true },
"Should have gotten back a ManifestEntry indicating that we only copied " +
"bookmarks"
);
Assert.ok(
fakeConnection.backup.notCalled,
@ -171,7 +195,13 @@ add_task(async function test_backup_no_saved_history() {
Services.prefs.setBoolPref(SANITIZE_ON_SHUTDOWN_PREF, true);
fakeConnection.backup.resetHistory();
await placesBackupResource.backup(stagingPath, sourcePath);
manifestEntry = await placesBackupResource.backup(stagingPath, sourcePath);
Assert.deepEqual(
manifestEntry,
{ bookmarksOnly: true },
"Should have gotten back a ManifestEntry indicating that we only copied " +
"bookmarks"
);
Assert.ok(
fakeConnection.backup.notCalled,
@ -211,7 +241,16 @@ add_task(async function test_backup_private_browsing() {
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
sandbox.stub(PrivateBrowsingUtils, "permanentPrivateBrowsing").value(true);
await placesBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await placesBackupResource.backup(
stagingPath,
sourcePath
);
Assert.deepEqual(
manifestEntry,
{ bookmarksOnly: true },
"Should have gotten back a ManifestEntry indicating that we only copied " +
"bookmarks"
);
Assert.ok(
fakeConnection.backup.notCalled,

View file

@ -86,6 +86,14 @@ add_task(async function test_backup() {
];
await createTestFiles(sourcePath, simpleCopyFiles);
// Create our fake database files. We don't expect these to be copied to the
// staging directory in this test due to our stubbing of the backup method, so
// we don't include it in `simpleCopyFiles`.
await createTestFiles(sourcePath, [
{ path: "permissions.sqlite" },
{ path: "content-prefs.sqlite" },
]);
// We have no need to test that Sqlite.sys.mjs's backup method is working -
// this is something that is tested in Sqlite's own tests. We can just make
// sure that it's being called using sinon. Unfortunately, we cannot do the
@ -96,7 +104,15 @@ add_task(async function test_backup() {
};
sandbox.stub(Sqlite, "openConnection").returns(fakeConnection);
await preferencesBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await preferencesBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"PreferencesBackupResource.backup should return null as its ManifestEntry"
);
await assertFilesExist(stagingPath, simpleCopyFiles);

View file

@ -93,7 +93,15 @@ add_task(async function test_backup() {
await createTestFiles(sourcePath, simpleCopyFiles);
let sessionStoreState = SessionStore.getCurrentState(true);
await sessionStoreBackupResource.backup(stagingPath, sourcePath);
let manifestEntry = await sessionStoreBackupResource.backup(
stagingPath,
sourcePath
);
Assert.equal(
manifestEntry,
null,
"SessionStoreBackupResource.backup should return null as its ManifestEntry"
);
/**
* We don't expect the actual file sessionstore.jsonlz4 to exist in the profile directory before calling the backup method.

View file

@ -85,23 +85,45 @@ add_task(async function test_createBackup() {
await bs.createBackup({ profilePath: fakeProfilePath });
// For now, we expect a staging folder to exist under the fakeProfilePath,
// and we should find a folder for each fake BackupResource.
let stagingPath = PathUtils.join(fakeProfilePath, "backups", "staging");
Assert.ok(await IOUtils.exists(stagingPath), "Staging folder exists");
// We expect the staging folder to exist then be renamed under the fakeProfilePath.
// We should also find a folder for each fake BackupResource.
let backupsFolderPath = PathUtils.join(fakeProfilePath, "backups");
let stagingPath = PathUtils.join(backupsFolderPath, "staging");
// For now, we expect a single backup only to be saved.
let backups = await IOUtils.getChildren(backupsFolderPath);
Assert.equal(
backups.length,
1,
"There should only be 1 backup in the backups folder"
);
let renamedFilename = await PathUtils.filename(backups[0]);
let expectedFormatRegex = /^\d{4}(-\d{2}){2}T(\d{2}-){2}\d{2}Z$/;
Assert.ok(
renamedFilename.match(expectedFormatRegex),
"Renamed staging folder should have format YYYY-MM-DDTHH-mm-ssZ"
);
let stagingPathRenamed = PathUtils.join(backupsFolderPath, renamedFilename);
for (let backupResourceClass of [
FakeBackupResource1,
FakeBackupResource2,
FakeBackupResource3,
]) {
let expectedResourceFolder = PathUtils.join(
let expectedResourceFolderBeforeRename = PathUtils.join(
stagingPath,
backupResourceClass.key
);
let expectedResourceFolderAfterRename = PathUtils.join(
stagingPathRenamed,
backupResourceClass.key
);
Assert.ok(
await IOUtils.exists(expectedResourceFolder),
`BackupResource staging folder exists for ${backupResourceClass.key}`
await IOUtils.exists(expectedResourceFolderAfterRename),
`BackupResource folder exists for ${backupResourceClass.key} after rename`
);
Assert.ok(
backupResourceClass.prototype.backup.calledOnce,
@ -109,14 +131,21 @@ add_task(async function test_createBackup() {
);
Assert.ok(
backupResourceClass.prototype.backup.calledWith(
expectedResourceFolder,
expectedResourceFolderBeforeRename,
fakeProfilePath
),
`Backup was passed the right paths for ${backupResourceClass.key}`
`Backup was called in the staging folder for ${backupResourceClass.key} before rename`
);
}
let manifestPath = PathUtils.join(stagingPath, "backup-manifest.json");
// Check that resources were called from highest to lowest backup priority.
sinon.assert.callOrder(
FakeBackupResource3.prototype.backup,
FakeBackupResource2.prototype.backup,
FakeBackupResource1.prototype.backup
);
let manifestPath = PathUtils.join(stagingPathRenamed, "backup-manifest.json");
Assert.ok(await IOUtils.exists(manifestPath), "Manifest file exists");
let manifest = await IOUtils.readJSON(manifestPath);

View file

@ -485,35 +485,50 @@ if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
lazy.PanelMultiView.getViewNode(doc, "PanelUI-remotetabs-tabslist")
);
panelview.addEventListener("command", this);
let syncNowButton = lazy.PanelMultiView.getViewNode(
aEvent.target.ownerDocument,
"PanelUI-remotetabs-syncnow"
);
syncNowButton.addEventListener("mouseover", this);
},
onViewHiding(aEvent) {
aEvent.target.syncedTabsPanelList.destroy();
aEvent.target.syncedTabsPanelList = null;
aEvent.target.removeEventListener("command", this);
let panelview = aEvent.target;
panelview.syncedTabsPanelList.destroy();
panelview.syncedTabsPanelList = null;
panelview.removeEventListener("command", this);
let syncNowButton = lazy.PanelMultiView.getViewNode(
aEvent.target.ownerDocument,
"PanelUI-remotetabs-syncnow"
);
syncNowButton.removeEventListener("mouseover", this);
},
handleEvent(aEvent) {
if (aEvent.type != "command") {
return;
}
let button = aEvent.target;
let { gSync } = button.ownerGlobal;
switch (button.id) {
case "PanelUI-remotetabs-syncnow":
gSync.doSync();
break;
case "PanelUI-remotetabs-view-managedevices":
gSync.openDevicesManagementPage("syncedtabs-menupanel");
break;
case "PanelUI-remotetabs-tabsdisabledpane-button":
case "PanelUI-remotetabs-setupsync-button":
case "PanelUI-remotetabs-syncdisabled-button":
case "PanelUI-remotetabs-reauthsync-button":
case "PanelUI-remotetabs-unverified-button":
gSync.openPrefs("synced-tabs");
break;
case "PanelUI-remotetabs-connect-device-button":
gSync.openConnectAnotherDevice("synced-tabs");
switch (aEvent.type) {
case "mouseover":
gSync.refreshSyncButtonsTooltip();
break;
case "command": {
switch (button.id) {
case "PanelUI-remotetabs-syncnow":
gSync.doSync();
break;
case "PanelUI-remotetabs-view-managedevices":
gSync.openDevicesManagementPage("syncedtabs-menupanel");
break;
case "PanelUI-remotetabs-tabsdisabledpane-button":
case "PanelUI-remotetabs-setupsync-button":
case "PanelUI-remotetabs-syncdisabled-button":
case "PanelUI-remotetabs-reauthsync-button":
case "PanelUI-remotetabs-unverified-button":
gSync.openPrefs("synced-tabs");
break;
case "PanelUI-remotetabs-connect-device-button":
gSync.openConnectAnotherDevice("synced-tabs");
break;
}
}
}
},
});

View file

@ -24,7 +24,6 @@
rel="localization"
href="browser/policies/policies-descriptions.ftl"
/>
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<script src="chrome://browser/content/policies/aboutPolicies.js"></script>
</head>

View file

@ -294,6 +294,7 @@ function generateDocumentation() {
SanitizeOnShutdown: "SanitizeOnShutdown2",
WindowsSSO: "Windows10SSO",
SecurityDevices: "SecurityDevices2",
DisableFirefoxAccounts: "DisableFirefoxAccounts1",
};
for (let policyName in schema.properties) {

View file

@ -70,6 +70,8 @@ add_task(async function testPopupSelectPopup() {
});
const selectRect = await SpecialPowers.spawn(iframe, [], async () => {
await SpecialPowers.contentTransformsReceived(content.window);
await ContentTaskUtils.waitForCondition(() => {
return content.document.querySelector("select");
});

View file

@ -13,7 +13,6 @@
<meta name="color-scheme" content="light dark" />
<title data-l10n-id="firefoxview-page-title"></title>
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="browser/firefoxView.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />

View file

@ -74,7 +74,7 @@ add_task(async function testHistoryImportStrangeEntries() {
await PlacesUtils.history.clear();
let placesQuery = new PlacesQuery();
let emptyHistory = await placesQuery.getHistory();
let emptyHistory = await placesQuery.getHistory({ daysOld: Infinity });
Assert.equal(emptyHistory.size, 0, "Empty history should indeed be empty.");
const EXPECTED_MIGRATED_SITES = 10;
@ -94,7 +94,10 @@ add_task(async function testHistoryImportStrangeEntries() {
let migrator = await MigrationUtils.getMigrator("safari");
await promiseMigration(migrator, MigrationUtils.resourceTypes.HISTORY);
let migratedHistory = await placesQuery.getHistory({ sortBy: "site" });
let migratedHistory = await placesQuery.getHistory({
daysOld: Infinity,
sortBy: "site",
});
let siteCount = migratedHistory.size;
let visitCount = 0;
for (let [, visits] of migratedHistory) {

View file

@ -26,7 +26,6 @@
rel="localization"
href="browser/preferences/preferences.ftl"
/>
<html:link rel="localization" href="toolkit/branding/accounts.ftl" />
</linkset>
<script src="chrome://global/content/preferencesBindings.js" />
<script src="chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.js" />

View file

@ -28,7 +28,6 @@
rel="localization"
href="browser/preferences/fxaPairDevice.ftl"
/>
<html:link rel="localization" href="toolkit/branding/accounts.ftl" />
</linkset>
<script src="chrome://browser/content/preferences/fxaPairDevice.js" />

View file

@ -49,7 +49,6 @@
<link rel="localization" href="browser/preferences/fonts.ftl"/>
<link rel="localization" href="browser/preferences/moreFromMozilla.ftl"/>
<link rel="localization" href="browser/preferences/preferences.ftl"/>
<link rel="localization" href="toolkit/branding/accounts.ftl"/>
<link rel="localization" href="toolkit/branding/brandings.ftl"/>
<link rel="localization" href="toolkit/featuregates/features.ftl"/>

View file

@ -6,26 +6,26 @@
"use strict";
ChromeUtils.defineESModuleGetters(this, {
UrlbarProviderQuickActions:
"resource:///modules/UrlbarProviderQuickActions.sys.mjs",
ActionsProviderQuickActions:
"resource:///modules/ActionsProviderQuickActions.sys.mjs",
UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.sys.mjs",
});
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.suggest.quickactions", true],
["browser.urlbar.secondaryActions.featureGate", true],
["browser.urlbar.quickactions.enabled", true],
],
});
UrlbarProviderQuickActions.addAction("testaction", {
ActionsProviderQuickActions.addAction("testaction", {
commands: ["testaction"],
label: "quickactions-downloads2",
});
registerCleanupFunction(() => {
UrlbarProviderQuickActions.removeAction("testaction");
ActionsProviderQuickActions.removeAction("testaction");
});
});
@ -61,15 +61,23 @@ add_task(async function test_show_prefs() {
await BrowserTestUtils.removeTab(tab);
});
async function testActionIsShown(window) {
async function testActionIsShown(window, name) {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "testact",
waitForFocus: SimpleTest.waitForFocus,
});
try {
let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
return result.providerName == "quickactions";
await BrowserTestUtils.waitForMutationCondition(
window.document,
{},
() =>
!!window.document.querySelector(
`.urlbarView-action-btn[data-action=${name}]`
)
);
Assert.ok(true, `We found action "${name}"`);
return true;
} catch (e) {
return false;
}
@ -100,7 +108,7 @@ add_task(async function test_prefs() {
});
Assert.ok(
await testActionIsShown(window),
await testActionIsShown(window, "testaction"),
"Actions are shown after user clicks checkbox"
);

View file

@ -193,6 +193,10 @@ add_setup(function () {
SpecialPowers.pushPrefEnv({
set: [["privacy.sanitize.useOldClearHistoryDialog", false]],
});
// The tests in this file all test specific interactions with the new clear
// history dialog and can't be split up.
requestLongerTimeout(2);
});
// Test opening the "Clear All Data" dialog and cancelling.

View file

@ -45,6 +45,9 @@ export const ResetPBMPanel = {
onViewShowing(aEvent) {
ResetPBMPanel.onViewShowing(aEvent);
},
onViewHiding(aEvent) {
ResetPBMPanel.onViewHiding(aEvent);
},
};
if (this._enabled) {
@ -59,7 +62,8 @@ export const ResetPBMPanel = {
* the toolbar button.
*/
async onViewShowing(event) {
let triggeringWindow = event.target.ownerGlobal;
let panelview = event.target;
let triggeringWindow = panelview.ownerGlobal;
// We may skip the confirmation panel if disabled via pref.
if (!this._shouldConfirmClear) {
@ -68,7 +72,7 @@ export const ResetPBMPanel = {
// If the action is triggered from the overflow menu make sure that the
// panel gets hidden.
lazy.CustomizableUI.hidePanelForNode(event.target);
lazy.CustomizableUI.hidePanelForNode(panelview);
// Trigger the restart action.
await this._restartPBM(triggeringWindow);
@ -77,6 +81,8 @@ export const ResetPBMPanel = {
return;
}
panelview.addEventListener("command", this);
// Before the panel is shown, update checkbox state based on pref.
this._rememberCheck(triggeringWindow).checked = this._shouldConfirmClear;
@ -86,6 +92,23 @@ export const ResetPBMPanel = {
});
},
onViewHiding(event) {
let panelview = event.target;
panelview.removeEventListener("command", this);
},
handleEvent(event) {
let button = event.target;
switch (button.id) {
case "reset-pbm-panel-cancel-button":
this.onCancel(button);
break;
case "reset-pbm-panel-confirm-button":
this.onConfirm(button);
break;
}
},
/**
* Handles the confirmation panel cancel button.
* @param {MozButton} button - Cancel button that triggered the action.

View file

@ -13,7 +13,6 @@
<meta name="color-scheme" content="light dark" />
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/protections.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<!-- Temporary "en-US"-only l10n strings -->
<link rel="localization" href="preview/protections.ftl" />

View file

@ -82,11 +82,7 @@ const EXTRA_EVENTS = [
add_task(async function test_started_and_canceled_events() {
await SpecialPowers.pushPrefEnv({
set: [
["browser.urlbar.quickactions.enabled", true],
["browser.urlbar.suggest.quickactions", true],
["browser.urlbar.shortcuts.quickactions", true],
],
set: [["browser.urlbar.secondaryActions.featureGate", true]],
});
await BrowserTestUtils.withNewTab(
@ -163,12 +159,9 @@ add_task(async function test_started_and_canceled_events() {
value: "screenshot",
waitForFocus: SimpleTest.waitForFocus,
});
let { result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
Assert.equal(result.providerName, "quickactions");
info("Trigger the screenshot mode");
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await helper.waitForOverlay();
@ -177,13 +170,10 @@ add_task(async function test_started_and_canceled_events() {
value: "screenshot",
waitForFocus: SimpleTest.waitForFocus,
});
({ result } = await UrlbarTestUtils.getDetailsOfResultAt(window, 1));
Assert.equal(result.type, UrlbarUtils.RESULT_TYPE.DYNAMIC);
Assert.equal(result.providerName, "quickactions");
info("Trigger the screenshot mode");
screenshotExit = TestUtils.topicObserved("screenshots-exit");
EventUtils.synthesizeKey("KEY_ArrowDown", {}, window);
EventUtils.synthesizeKey("KEY_Tab", {}, window);
EventUtils.synthesizeKey("KEY_Enter", {}, window);
await helper.waitForOverlayClosed();
await screenshotExit;

View file

@ -0,0 +1,281 @@
/* -*- 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/. */
/**
* This file exists so that LaunchModernSettingsDialogDefaultApps can be called
* without linking to libxul.
*/
#include "Windows11LimitedAccessFeatures.h"
#include "mozilla/Logging.h"
static mozilla::LazyLogModule sLog("Windows11LimitedAccessFeatures");
#define LAF_LOG(level, msg, ...) MOZ_LOG(sLog, level, (msg, ##__VA_ARGS__))
// MINGW32 is not supported for these features
// Fall back function defined in the #else
#ifndef __MINGW32__
# include "nsString.h"
# include "nsWindowsHelpers.h"
# include "mozilla/Atomics.h"
# include <wrl.h>
# include <inspectable.h>
# include <roapi.h>
# include <windows.services.store.h>
# include <windows.foundation.h>
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::ApplicationModel;
using namespace mozilla;
/**
* To unlock features, we need:
* a feature identifier
* a token,
* an attestation string
* a token
*
* The token is generated by Microsoft and must
* match the publisher id Microsoft thinks we have, for a particular
* feature.
*
* To get a token, find the right microsoft email address by doing
* a search on the web for the feature you want unlocked and reach
* out to the right people at Microsoft.
*
* The token is generated from Microsoft.
* The jumbled code in the attestation string is a publisher id and
* must match the code in the resources / .rc file for the identity,
* looking like this for non-MSIX builds:
*
* Identity LimitedAccessFeature {{ L"MozillaFirefox_pcsmm0jrprpb2" }}
*
* Broken down:
* Identity LimitedAccessFeature {{ L"PRODUCTNAME_PUBLISHERID" }}
*
* That is injected into our build in create_rc.py and is necessary
* to unlock the taskbar pinning feature / APIs from an unpackaged
* build.
*
* In the above, the token is generated from the publisher id (pcsmm0jrprpb2)
* and the product name (MozillaFirefox)
*
* All tokens listed here were provided to us by Microsoft.
*
* Below and in create_rc.py, we used this set:
*
* Token: "kRFiWpEK5uS6PMJZKmR7MQ=="
* Product Name: "MozillaFirefox"
* Publisher ID: "pcsmm0jrprpb2"
*
* Microsoft also provided these other tokens, which will will
* work if accompanied by the matching changes to create_rc.py:
* -----
* Token: "RGEhsYgKhmPLKyzkEHnMhQ=="
* Product Name: "FirefoxBeta"
* Publisher ID: "pcsmm0jrprpb2"
*
* -----
*
* Token: "qbVzns/9kT+t15YbIwT4Jw=="
* Product Name: "FirefoxNightly"
* Publisher ID: "pcsmm0jrprpb2"
*
* To use those instead, you have to ensure that the LimitedAccessFeature
* generated in create_rc.py has the product name and publisher id
* matching the token used in this file.
*
* For non-packaged (non-MSIX) builds, any of the above sets will work.
* Just make sure the right (ProductName_PublisherID) value is in the
* generated resource data for the executable, and the matching
* (Token) and attestation string
*
* To get MSIX/packaged builds to work, the product name and publisher in
* the final manifest (searchfox.org/mozilla-central/search?q=APPX_PUBLISHER)
* should match the token in this file. For that case, the identity value
* in the resources does not matter.
*
* See here for Microsoft examples:
https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/TaskbarManager/CppUnpackagedDesktopTaskbarPin
*/
struct LimitedAccessFeatureInfo {
const char* debugName;
const WCHAR* feature;
const WCHAR* token;
const WCHAR* attestation;
};
static LimitedAccessFeatureInfo limitedAccessFeatureInfo[] = {
{// Win11LimitedAccessFeatureType::Taskbar
"Win11LimitedAccessFeatureType::Taskbar",
L"com.microsoft.windows.taskbar.pin", L"kRFiWpEK5uS6PMJZKmR7MQ==",
L"pcsmm0jrprpb2 has registered their use of "
L"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "
L"terms "
L"of use."}};
static_assert(mozilla::ArrayLength(limitedAccessFeatureInfo) ==
kWin11LimitedAccessFeatureTypeCount);
/**
Implementation of the Win11LimitedAccessFeaturesInterface.
*/
class Win11LimitedAccessFeatures : public Win11LimitedAccessFeaturesInterface {
public:
using AtomicState = Atomic<int, SequentiallyConsistent>;
Result<bool, HRESULT> Unlock(Win11LimitedAccessFeatureType feature) override;
private:
AtomicState& GetState(Win11LimitedAccessFeatureType feature);
Result<bool, HRESULT> UnlockImplementation(
Win11LimitedAccessFeatureType feature);
/**
* Store the state as an atomic so that it can be safely accessed from
* different threads.
*/
static AtomicState mTaskbarState;
static AtomicState mDefaultState;
enum State {
Uninitialized,
Locked,
Unlocked,
};
};
Win11LimitedAccessFeatures::AtomicState
Win11LimitedAccessFeatures::mTaskbarState(
Win11LimitedAccessFeatures::Uninitialized);
Win11LimitedAccessFeatures::AtomicState
Win11LimitedAccessFeatures::mDefaultState(
Win11LimitedAccessFeatures::Uninitialized);
RefPtr<Win11LimitedAccessFeaturesInterface>
CreateWin11LimitedAccessFeaturesInterface() {
RefPtr<Win11LimitedAccessFeaturesInterface> result(
new Win11LimitedAccessFeatures());
return result;
}
Result<bool, HRESULT> Win11LimitedAccessFeatures::Unlock(
Win11LimitedAccessFeatureType feature) {
AtomicState& atomicState = GetState(feature);
const auto& lafInfo = limitedAccessFeatureInfo[static_cast<int>(feature)];
LAF_LOG(
LogLevel::Debug, "Limited Access Feature Info for %s. Feature %S, %S, %S",
lafInfo.debugName, lafInfo.feature, lafInfo.token, lafInfo.attestation);
int state = atomicState;
if (state != Uninitialized) {
LAF_LOG(LogLevel::Debug, "%s already initialized! State = %s",
lafInfo.debugName, (state == Unlocked) ? "true" : "false");
return (state == Unlocked);
}
// If multiple threads read the state at the same time, and it's unitialized,
// both threads will unlock the feature. This situation is unlikely, but even
// if it happens, it's not a problem.
auto result = UnlockImplementation(feature);
int newState = Locked;
if (!result.isErr() && result.unwrap()) {
newState = Unlocked;
}
atomicState = newState;
return result;
}
Win11LimitedAccessFeatures::AtomicState& Win11LimitedAccessFeatures::GetState(
Win11LimitedAccessFeatureType feature) {
switch (feature) {
case Win11LimitedAccessFeatureType::Taskbar:
return mTaskbarState;
default:
LAF_LOG(LogLevel::Debug, "Missing feature type for %d",
static_cast<int>(feature));
MOZ_ASSERT(false,
"Unhandled feature type! Add a new atomic state variable, add "
"that entry to the switch statement above, and add the proper "
"entries for the feature and the token.");
return mDefaultState;
}
}
Result<bool, HRESULT> Win11LimitedAccessFeatures::UnlockImplementation(
Win11LimitedAccessFeatureType feature) {
ComPtr<ILimitedAccessFeaturesStatics> limitedAccessFeatures;
ComPtr<ILimitedAccessFeatureRequestResult> limitedAccessFeaturesResult;
const auto& lafInfo = limitedAccessFeatureInfo[static_cast<int>(feature)];
HRESULT hr = RoGetActivationFactory(
HStringReference(
RuntimeClass_Windows_ApplicationModel_LimitedAccessFeatures)
.Get(),
IID_ILimitedAccessFeaturesStatics, &limitedAccessFeatures);
if (!SUCCEEDED(hr)) {
LAF_LOG(LogLevel::Debug, "%s activation error. HRESULT = 0x%lx",
lafInfo.debugName, hr);
return Err(hr);
}
hr = limitedAccessFeatures->TryUnlockFeature(
HStringReference(lafInfo.feature).Get(),
HStringReference(lafInfo.token).Get(),
HStringReference(lafInfo.attestation).Get(),
&limitedAccessFeaturesResult);
if (!SUCCEEDED(hr)) {
LAF_LOG(LogLevel::Debug, "%s unlock error. HRESULT = 0x%lx",
lafInfo.debugName, hr);
return Err(hr);
}
LimitedAccessFeatureStatus status;
hr = limitedAccessFeaturesResult->get_Status(&status);
if (!SUCCEEDED(hr)) {
LAF_LOG(LogLevel::Debug, "%s get status error. HRESULT = 0x%lx",
lafInfo.debugName, hr);
return Err(hr);
}
int state = Unlocked;
if ((status != LimitedAccessFeatureStatus_Available) &&
(status != LimitedAccessFeatureStatus_AvailableWithoutToken)) {
LAF_LOG(LogLevel::Debug, "%s not available. HRESULT = 0x%lx",
lafInfo.debugName, hr);
state = Locked;
}
return (state == Unlocked);
}
#else // MINGW32 implementation
RefPtr<Win11LimitedAccessFeaturesInterface>
CreateWin11LimitedAccessFeaturesInterface() {
RefPtr<Win11LimitedAccessFeaturesInterface> result;
return result;
}
#endif

View file

@ -0,0 +1,53 @@
/* -*- 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/. */
#ifndef SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__
#define SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__
#include "nsISupportsImpl.h"
#include "mozilla/Result.h"
#include "mozilla/ResultVariant.h"
#include <winerror.h>
#include <windows.h> // for HRESULT
#include "mozilla/DefineEnum.h"
#include <winerror.h>
MOZ_DEFINE_ENUM_CLASS(Win11LimitedAccessFeatureType, (Taskbar));
/**
* Class to manage unlocking limited access features on Windows 11.
* Unless stubbing for testing purposes, create objects of this
* class with CreateWin11LimitedAccessFeaturesInterface.
*
* Windows 11 requires certain features to be unlocked in order to work
* (for instance, the Win11 Taskbar pinning APIs). Call Unlock()
* to unlock them. Generally, results will be cached in atomic variables
* and future calls to Unlock will be as long as it takes
* to fetch an atomic variable.
*/
class Win11LimitedAccessFeaturesInterface {
public:
/**
* Unlocks the limited access features, if possible.
*
* Returns an error code on error, true on successful unlock,
* false on unlock failed (but with no error).
*/
virtual mozilla::Result<bool, HRESULT> Unlock(
Win11LimitedAccessFeatureType feature) = 0;
/**
* Reference counting and cycle collection.
*/
NS_INLINE_DECL_REFCOUNTING(Win11LimitedAccessFeaturesInterface)
protected:
virtual ~Win11LimitedAccessFeaturesInterface() {}
};
RefPtr<Win11LimitedAccessFeaturesInterface>
CreateWin11LimitedAccessFeaturesInterface();
#endif // SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__

View file

@ -0,0 +1,344 @@
/* -*- 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 "Windows11TaskbarPinning.h"
#include "Windows11LimitedAccessFeatures.h"
#include "nsWindowsHelpers.h"
#include "MainThreadUtils.h"
#include "nsThreadUtils.h"
#include <strsafe.h>
#include "mozilla/Result.h"
#include "mozilla/ResultVariant.h"
#include "mozilla/Logging.h"
static mozilla::LazyLogModule sLog("Windows11TaskbarPinning");
#define TASKBAR_PINNING_LOG(level, msg, ...) \
MOZ_LOG(sLog, level, (msg, ##__VA_ARGS__))
#ifndef __MINGW32__ // WinRT headers not yet supported by MinGW
# include <wrl.h>
# include <inspectable.h>
# include <roapi.h>
# include <windows.services.store.h>
# include <windows.foundation.h>
# include <windows.ui.shell.h>
using namespace mozilla;
/**
* The Win32 SetEvent and WaitForSingleObject functions take HANDLE parameters
* which are typedefs of void*. When using nsAutoHandle, that means if you
* forget to call .get() first, everything still compiles and then doesn't work
* at runtime. For instance, calling SetEvent(mEvent) below would compile but
* not work at runtime and the waits would block forever.
* To ensure this isn't an issue, we wrap the event in a custom class here
* with the simple methods that we want on an event.
*/
class EventWrapper {
public:
EventWrapper() : mEvent(CreateEventW(nullptr, true, false, nullptr)) {}
void Set() { SetEvent(mEvent.get()); }
void Reset() { ResetEvent(mEvent.get()); }
void Wait() { WaitForSingleObject(mEvent.get(), INFINITE); }
private:
nsAutoHandle mEvent;
};
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows;
using namespace ABI::Windows::UI::Shell;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::ApplicationModel;
static Result<ComPtr<ITaskbarManager>, HRESULT> InitializeTaskbar() {
ComPtr<IInspectable> taskbarStaticsInspectable;
TASKBAR_PINNING_LOG(LogLevel::Debug, "Initializing taskbar");
HRESULT hr = RoGetActivationFactory(
HStringReference(RuntimeClass_Windows_UI_Shell_TaskbarManager).Get(),
IID_ITaskbarManagerStatics, &taskbarStaticsInspectable);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(LogLevel::Debug,
"Taskbar: Failed to activate. HRESULT = 0x%lx", hr);
return Err(hr);
}
ComPtr<ITaskbarManagerStatics> taskbarStatics;
hr = taskbarStaticsInspectable.As(&taskbarStatics);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(LogLevel::Debug, "Failed statistics. HRESULT = 0x%lx",
hr);
return Err(hr);
}
ComPtr<ITaskbarManager> taskbarManager;
hr = taskbarStatics->GetDefault(&taskbarManager);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(LogLevel::Debug,
"Error getting TaskbarManager. HRESULT = 0x%lx", hr);
return Err(hr);
}
TASKBAR_PINNING_LOG(LogLevel::Debug,
"TaskbarManager retrieved successfully!");
return taskbarManager;
}
Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
bool aCheckOnly, const nsAString& aAppUserModelId,
nsAutoString aShortcutPath) {
MOZ_DIAGNOSTIC_ASSERT(!NS_IsMainThread(),
"PinCurrentAppToTaskbarWin11 should be called off main "
"thread only. It blocks, waiting on things to execute "
"asynchronously on the main thread.");
{
RefPtr<Win11LimitedAccessFeaturesInterface> limitedAccessFeatures =
CreateWin11LimitedAccessFeaturesInterface();
auto result =
limitedAccessFeatures->Unlock(Win11LimitedAccessFeatureType::Taskbar);
if (result.isErr()) {
auto hr = result.unwrapErr();
TASKBAR_PINNING_LOG(LogLevel::Debug,
"Taskbar unlock: Error. HRESULT = 0x%lx", hr);
return {hr, Win11PinToTaskBarResultStatus::NotSupported};
}
if (result.unwrap() == false) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar unlock: failed. Not supported on this version of Windows.");
return {S_OK, Win11PinToTaskBarResultStatus::NotSupported};
}
}
HRESULT hr;
Win11PinToTaskBarResultStatus resultStatus =
Win11PinToTaskBarResultStatus::NotSupported;
EventWrapper event;
// Everything related to the taskbar and pinning must be done on the main /
// user interface thread or Windows will cause them to fail.
NS_DispatchToMainThread(NS_NewRunnableFunction(
"PinCurrentAppToTaskbarWin11", [&event, &hr, &resultStatus, aCheckOnly] {
auto CompletedOperations =
[&event, &resultStatus](Win11PinToTaskBarResultStatus status) {
resultStatus = status;
event.Set();
};
auto result = InitializeTaskbar();
if (result.isErr()) {
hr = result.unwrapErr();
return CompletedOperations(
Win11PinToTaskBarResultStatus::NotSupported);
}
ComPtr<ITaskbarManager> taskbar = result.unwrap();
boolean supported;
hr = taskbar->get_IsSupported(&supported);
if (FAILED(hr) || !supported) {
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: error checking if supported. HRESULT = 0x%lx", hr);
} else {
TASKBAR_PINNING_LOG(LogLevel::Debug, "Taskbar: not supported.");
}
return CompletedOperations(
Win11PinToTaskBarResultStatus::NotSupported);
}
if (aCheckOnly) {
TASKBAR_PINNING_LOG(LogLevel::Debug, "Taskbar: check succeeded.");
return CompletedOperations(Win11PinToTaskBarResultStatus::Success);
}
boolean isAllowed = false;
hr = taskbar->get_IsPinningAllowed(&isAllowed);
if (FAILED(hr) || !isAllowed) {
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: error checking if pinning is allowed. HRESULT = "
"0x%lx",
hr);
} else {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: is pinning allowed error or isn't allowed right now. "
"It's not clear when it will be allowed. Possibly after a "
"reboot.");
}
return CompletedOperations(
Win11PinToTaskBarResultStatus::NotCurrentlyAllowed);
}
ComPtr<IAsyncOperation<bool>> isPinnedOperation = nullptr;
hr = taskbar->IsCurrentAppPinnedAsync(&isPinnedOperation);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: is current app pinned operation failed. HRESULT = "
"0x%lx",
hr);
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
// Copy the taskbar; don't use it as a reference.
// With the async calls, it's not guaranteed to still be valid
// if sent as a reference.
// resultStatus and event are not defined on the main thread and will
// be alive until the async functions complete, so they can be used as
// references.
auto isPinnedCallback = Callback<IAsyncOperationCompletedHandler<
bool>>([taskbar, &event, &resultStatus, &hr](
IAsyncOperation<bool>* asyncInfo,
AsyncStatus status) mutable -> HRESULT {
auto CompletedOperations =
[&event,
&resultStatus](Win11PinToTaskBarResultStatus status) -> HRESULT {
resultStatus = status;
event.Set();
return S_OK;
};
bool asyncOpSucceeded = status == AsyncStatus::Completed;
if (!asyncOpSucceeded) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: is pinned operation failed to complete.");
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
unsigned char isCurrentAppPinned = false;
hr = asyncInfo->GetResults(&isCurrentAppPinned);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: is current app pinned check failed. HRESULT = 0x%lx",
hr);
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
if (isCurrentAppPinned) {
TASKBAR_PINNING_LOG(LogLevel::Debug,
"Taskbar: current app is already pinned.");
return CompletedOperations(
Win11PinToTaskBarResultStatus::AlreadyPinned);
}
ComPtr<IAsyncOperation<bool>> requestPinOperation = nullptr;
hr = taskbar->RequestPinCurrentAppAsync(&requestPinOperation);
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin current app operation creation failed. "
"HRESULT = 0x%lx",
hr);
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
auto pinAppCallback = Callback<IAsyncOperationCompletedHandler<
bool>>([CompletedOperations, &hr](
IAsyncOperation<bool>* asyncInfo,
AsyncStatus status) -> HRESULT {
bool asyncOpSucceeded = status == AsyncStatus::Completed;
if (!asyncOpSucceeded) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin current app operation did not "
"complete.");
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
unsigned char successfullyPinned = 0;
hr = asyncInfo->GetResults(&successfullyPinned);
if (FAILED(hr) || !successfullyPinned) {
if (FAILED(hr)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin current app operation failed to pin "
"due to error. HRESULT = 0x%lx",
hr);
} else {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin current app operation failed to pin");
}
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin current app operation succeeded");
return CompletedOperations(Win11PinToTaskBarResultStatus::Success);
});
HRESULT pinOperationHR =
requestPinOperation->put_Completed(pinAppCallback.Get());
if (FAILED(pinOperationHR)) {
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: request pin operation failed when setting completion "
"callback. HRESULT = 0x%lx",
hr);
hr = pinOperationHR;
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
// DO NOT SET event HERE. It will be set in the pin operation
// callback As in, operations are not completed, so don't call
// CompletedOperations
return S_OK;
});
HRESULT isPinnedOperationHR =
isPinnedOperation->put_Completed(isPinnedCallback.Get());
if (FAILED(isPinnedOperationHR)) {
hr = isPinnedOperationHR;
TASKBAR_PINNING_LOG(
LogLevel::Debug,
"Taskbar: is pinned operation failed when setting completion "
"callback. HRESULT = 0x%lx",
hr);
return CompletedOperations(Win11PinToTaskBarResultStatus::Failed);
}
// DO NOT SET event HERE. It will be set in the is pin operation
// callback As in, operations are not completed, so don't call
// CompletedOperations
}));
// block until the pinning is completed on the main thread
event.Wait();
return {hr, resultStatus};
}
#else // MINGW32 implementation below
Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
bool aCheckOnly, const nsAString& aAppUserModelId,
nsAutoString aShortcutPath) {
return {S_OK, Win11PinToTaskBarResultStatus::NotSupported};
}
#endif // #ifndef __MINGW32__ // WinRT headers not yet supported by MinGW

View file

@ -0,0 +1,35 @@
/* -*- 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/. */
/**
* This file exists to keep the Windows 11 Taskbar Pinning API
* related code as self-contained as possible.
*/
#ifndef SHELL_WINDOWS11TASKBARPINNING_H__
#define SHELL_WINDOWS11TASKBARPINNING_H__
#include "nsString.h"
#include <wrl.h>
#include <windows.h> // for HRESULT
enum class Win11PinToTaskBarResultStatus {
Failed,
NotCurrentlyAllowed,
AlreadyPinned,
Success,
NotSupported,
};
struct Win11PinToTaskBarResult {
HRESULT errorCode;
Win11PinToTaskBarResultStatus result;
};
Win11PinToTaskBarResult PinCurrentAppToTaskbarWin11(
bool aCheckOnly, const nsAString& aAppUserModelId,
nsAutoString aShortcutPath);
#endif // SHELL_WINDOWS11TASKBARPINNING_H__

View file

@ -50,6 +50,8 @@ elif CONFIG["OS_ARCH"] == "WINNT":
]
SOURCES += [
"nsWindowsShellService.cpp",
"Windows11LimitedAccessFeatures.cpp",
"Windows11TaskbarPinning.cpp",
"WindowsDefaultBrowser.cpp",
"WindowsUserChoice.cpp",
]
@ -82,6 +84,7 @@ for var in (
):
DEFINES[var] = '"%s"' % CONFIG[var]
if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]

View file

@ -39,6 +39,7 @@
#include "nsIXULAppInfo.h"
#include "nsINIParser.h"
#include "nsNativeAppSupportWin.h"
#include "Windows11TaskbarPinning.h"
#include <windows.h>
#include <shellapi.h>
@ -1626,7 +1627,7 @@ nsWindowsShellService::GetTaskbarTabPins(nsTArray<nsString>& aShortcutPaths) {
static nsresult PinCurrentAppToTaskbarWin10(bool aCheckOnly,
const nsAString& aAppUserModelId,
nsAutoString aShortcutPath) {
const nsAString& aShortcutPath) {
// The behavior here is identical if we're only checking or if we try to pin
// but the app is already pinned so we update the variable accordingly.
if (!aCheckOnly) {
@ -1695,6 +1696,28 @@ static nsresult PinCurrentAppToTaskbarImpl(
}
}
auto pinWithWin11TaskbarAPIResults =
PinCurrentAppToTaskbarWin11(aCheckOnly, aAppUserModelId, shortcutPath);
switch (pinWithWin11TaskbarAPIResults.result) {
case Win11PinToTaskBarResultStatus::NotSupported:
// Fall through to the win 10 mechanism
break;
case Win11PinToTaskBarResultStatus::Success:
case Win11PinToTaskBarResultStatus::AlreadyPinned:
return NS_OK;
case Win11PinToTaskBarResultStatus::NotCurrentlyAllowed:
case Win11PinToTaskBarResultStatus::Failed:
// return NS_ERROR_FAILURE;
// Fall through to the old mechanism for now
// In future, we should be sending telemetry for when
// an error occurs or for when pinning is not allowed
// with the Win 11 APIs.
break;
}
return PinCurrentAppToTaskbarWin10(aCheckOnly, aAppUserModelId, shortcutPath);
}
@ -1720,7 +1743,7 @@ static nsresult PinCurrentAppToTaskbarAsyncImpl(bool aCheckOnly,
}
nsAutoString aumid;
if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GenerateAppUserModelID(
if (NS_WARN_IF(!mozilla::widget::WinTaskbar::GetAppUserModelID(
aumid, aPrivateBrowsing))) {
return NS_ERROR_FAILURE;
}

View file

@ -95,7 +95,7 @@ class ShoppingMessageBar extends MozLitElement {
}
staleWarningTemplate() {
return html`<message-bar>
return html`<div>
<article id="message-bar-container" aria-labelledby="header">
<span
data-l10n-id="shopping-message-bar-warning-stale-analysis-message-2"
@ -107,7 +107,7 @@ class ShoppingMessageBar extends MozLitElement {
@click=${this.onClickAnalysisButton}
></button>
</article>
</message-bar>`;
</div>`;
}
genericErrorTemplate() {
@ -163,7 +163,7 @@ class ShoppingMessageBar extends MozLitElement {
}
analysisInProgressTemplate() {
return html`<message-bar
return html`<div
style=${styleMap({
"--analysis-progress-pcent": `${this.progress}%`,
})}
@ -184,11 +184,12 @@ class ShoppingMessageBar extends MozLitElement {
data-l10n-id="shopping-message-bar-analysis-in-progress-message2"
></span>
</article>
</message-bar>`;
</div>`;
}
reanalysisInProgressTemplate() {
return html`<message-bar
return html`<div
id="reanalysis-in-progress-message"
style=${styleMap({
"--analysis-progress-pcent": `${this.progress}%`,
})}
@ -206,7 +207,7 @@ class ShoppingMessageBar extends MozLitElement {
})}"
></span>
</article>
</message-bar>`;
</div>`;
}
pageNotSupportedTemplate() {

View file

@ -127,8 +127,9 @@ add_task(async function test_in_progress_analysis_stale() {
"shopping-message-bar should have progress"
);
let messageBarEl =
shoppingMessageBarEl?.shadowRoot.querySelector("message-bar");
let messageBarEl = shoppingMessageBarEl?.shadowRoot.getElementById(
"reanalysis-in-progress-message"
);
is(
messageBarEl?.getAttribute("style"),
"--analysis-progress-pcent: 50%;",

View file

@ -15,21 +15,19 @@ export default {
component: "shopping-message-bar",
argTypes: {
type: {
control: {
type: "select",
options: [
"stale",
"generic-error",
"not-enough-reviews",
"product-not-available",
"product-not-available-reported",
"thanks-for-reporting",
"analysis-in-progress",
"reanalysis-in-progress",
"page-not-supported",
"thank-you-for-feedback",
],
},
control: { type: "select" },
options: [
"stale",
"generic-error",
"not-enough-reviews",
"product-not-available",
"product-not-available-reported",
"thanks-for-reporting",
"analysis-in-progress",
"reanalysis-in-progress",
"page-not-supported",
"thank-you-for-feedback",
],
},
},
parameters: {

View file

@ -22,7 +22,6 @@
href="chrome://browser/skin/syncedtabs/sidebar.css"
/>
<link rel="localization" href="browser/syncedTabs.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<title data-l10n-id="synced-tabs-sidebar-title" />
</head>

View file

@ -18,6 +18,15 @@ export class TranslationsPanelShared {
*/
static #langListsInitState = new Map();
/**
* True if the next language-list initialization to fail for testing.
*
* @see TranslationsPanelShared.ensureLangListsBuilt
*
* @type {boolean}
*/
static #simulateLangListError = false;
/**
* Clears cached data regarding the initialization state of the
* FullPageTranslationsPanel or the SelectTranslationsPanel.
@ -59,14 +68,28 @@ export class TranslationsPanelShared {
}
}
/**
* Ensures that the next call to ensureLangListBuilt wil fail
* for the purpose of testing the error state.
*
* @see TranslationsPanelShared.ensureLangListsBuilt
*
* @type {boolean}
*/
static simulateLangListError() {
this.#simulateLangListError = true;
}
/**
* Retrieves the initialization state of language lists for the specified panel.
*
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
* - The panel for which to look up the state.
* @param {number} innerWindowId - The id of the current window.
*/
static getLangListsInitState(panel) {
return TranslationsPanelShared.#langListsInitState.get(panel.id);
static getLangListsInitState(panel, innerWindowId) {
const key = `${panel.id}-${innerWindowId}`;
return TranslationsPanelShared.#langListsInitState.get(key);
}
/**
@ -77,18 +100,19 @@ export class TranslationsPanelShared {
* @param {Document} document - The document object.
* @param {FullPageTranslationsPanel | SelectTranslationsPanel} panel
* - The panel for which to ensure language lists are built.
* @param {number} innerWindowId - The id of the current window.
*/
static async ensureLangListsBuilt(document, panel, innerWindowId) {
const { id } = panel;
switch (
TranslationsPanelShared.#langListsInitState.get(`${id}-${innerWindowId}`)
) {
const key = `${panel.id}-${innerWindowId}`;
switch (TranslationsPanelShared.#langListsInitState.get(key)) {
case "initialized":
// This has already been initialized.
return;
case "error":
case undefined:
// attempt to initialize
// Set the error state in case there is an early exit at any point.
// This will be set to "initialized" if everything succeeds.
TranslationsPanelShared.#langListsInitState.set(key, "error");
break;
default:
throw new Error(
@ -102,7 +126,8 @@ export class TranslationsPanelShared {
await lazy.TranslationsParent.getSupportedLanguages();
// Verify that we are in a proper state.
if (languagePairs.length === 0) {
if (languagePairs.length === 0 || this.#simulateLangListError) {
this.#simulateLangListError = false;
throw new Error("No translation languages were retrieved.");
}
@ -143,6 +168,6 @@ export class TranslationsPanelShared {
}
}
TranslationsPanelShared.#langListsInitState.set(id, "initialized");
TranslationsPanelShared.#langListsInitState.set(key, "initialized");
}
}

View file

@ -541,7 +541,12 @@ var FullPageTranslationsPanel = new (class {
// Unconditionally hide the intro text in case the panel is re-shown.
intro.hidden = true;
if (TranslationsPanelShared.getLangListsInitState(panel) === "error") {
if (
TranslationsPanelShared.getLangListsInitState(
panel,
gBrowser.selectedBrowser.innerWindowID
) === "error"
) {
// There was an error, display it in the view rather than the language
// dropdowns.
const { cancelButton, errorHintAction } = this.elements;

View file

@ -20,15 +20,27 @@
class="translations-panel-beta-icon">
</image>
</hbox>
<toolbarbutton id="select-translations-panel-settings"
<toolbarbutton id="select-translations-panel-settings-button"
class="panel-info-button translations-panel-settings-gear-icon"
data-l10n-id="translations-panel-settings-button"
closemenu="none" />
</hbox>
<html:div id="select-translations-panel-init-failure-content"
class="translations-panel-content"
hidden="true">
<html:moz-message-bar id="select-translations-panel-init-failure-message-bar"
type="error"
class="select-translations-panel-message-bar"
data-l10n-id="select-translations-panel-init-failure-message"
data-l10n-attrs="message">
</html:moz-message-bar>
</html:div>
<html:div id="select-translations-panel-unsupported-language-content" hidden="true">
<vbox flex="1" class="select-translations-panel-content">
<html:moz-message-bar id="select-translations-panel-unsupported-language-message-bar"
type="info"
class="select-translations-panel-message-bar"
data-l10n-id="select-translations-panel-unsupported-language-message-unknown"
data-l10n-attrs="message">
</html:moz-message-bar>
<label id="select-translations-panel-try-another-language-label"
@ -96,6 +108,12 @@
readonly="true"
tabindex="0">
</html:textarea>
<html:moz-message-bar id="select-translations-panel-translation-failure-message-bar"
type="error"
hidden="true"
data-l10n-id="select-translations-panel-translation-failure-message"
data-l10n-attrs="message">
</html:moz-message-bar>
</vbox>
</html:div>
<html:moz-button-group id="select-translations-panel-footer-button-group"
@ -107,6 +125,11 @@
</button>
</html:div>
<html:div id="select-translations-panel-end-button-group">
<button id="select-translations-panel-cancel-button"
class="footer-button"
hidden="true"
data-l10n-id="select-translations-panel-cancel-button">
</button>
<button id="select-translations-panel-translate-full-page-button"
class="footer-button"
data-l10n-id="select-translations-panel-translate-full-page-button">
@ -123,6 +146,12 @@
default="true"
disabled="true">
</button>
<button id="select-translations-panel-try-again-button"
class="footer-button"
data-l10n-id="select-translations-panel-try-again-button"
hidden="true"
default="true">
</button>
</html:div>
</html:moz-button-group>
</panel>

View file

@ -129,13 +129,6 @@ var SelectTranslationsPanel = new (class {
*/
#translationState = { phase: "closed" };
/**
* The Translator for the current language pair.
*
* @type {Translator}
*/
#translator;
/**
* An Id that increments with each translation, used to help keep track
* of whether an active translation request continue its progression or
@ -172,13 +165,18 @@ var SelectTranslationsPanel = new (class {
TranslationsPanelShared.defineLazyElements(document, this.#lazyElements, {
betaIcon: "select-translations-panel-beta-icon",
cancelButton: "select-translations-panel-cancel-button",
copyButton: "select-translations-panel-copy-button",
doneButton: "select-translations-panel-done-button",
fromLabel: "select-translations-panel-from-label",
fromMenuList: "select-translations-panel-from",
fromMenuPopup: "select-translations-panel-from-menupopup",
header: "select-translations-panel-header",
initFailureContent: "select-translations-panel-init-failure-content",
initFailureMessageBar:
"select-translations-panel-init-failure-message-bar",
mainContent: "select-translations-panel-main-content",
settingsButton: "select-translations-panel-settings-button",
textArea: "select-translations-panel-text-area",
toLabel: "select-translations-panel-to-label",
toMenuList: "select-translations-panel-to",
@ -186,6 +184,9 @@ var SelectTranslationsPanel = new (class {
translateButton: "select-translations-panel-translate-button",
translateFullPageButton:
"select-translations-panel-translate-full-page-button",
translationFailureMessageBar:
"select-translations-panel-translation-failure-message-bar",
tryAgainButton: "select-translations-panel-try-again-button",
tryAnotherSourceMenuList:
"select-translations-panel-try-another-language",
tryAnotherSourceMenuPopup:
@ -246,6 +247,19 @@ var SelectTranslationsPanel = new (class {
* The `fromLang` property is omitted if it is a language that is not currently supported by Firefox Translations.
*/
async getLangPairPromise(textToTranslate) {
if (
TranslationsParent.isInAutomation() &&
!TranslationsParent.isTranslationsEngineMocked()
) {
// If we are in automation, and the Translations Engine is NOT mocked, then that means
// we are in a test case in which we are not explicitly testing Select Translations,
// and the code to get the supported languages below will not be available. However,
// we still need to ensure that the translate-selection menuitem in the context menu
// is compatible with all code in other tests, so we will return "en" for the purpose
// of being able to localize and display the context-menu item in other test cases.
return { toLang: "en" };
}
const [fromLang, toLang] = await Promise.all([
SelectTranslationsPanel.getTopSupportedDetectedLanguage(textToTranslate),
TranslationsParent.getTopPreferredSupportedToLang(),
@ -269,15 +283,11 @@ var SelectTranslationsPanel = new (class {
* dropdowns have already been initialized.
*/
async #ensureLangListsBuilt() {
try {
await TranslationsPanelShared.ensureLangListsBuilt(
document,
this.elements.panel,
gBrowser.selectedBrowser.innerWindowID
);
} catch (error) {
this.console?.error(error);
}
await TranslationsPanelShared.ensureLangListsBuilt(
document,
this.elements.panel,
gBrowser.selectedBrowser.innerWindowID
);
}
/**
@ -363,16 +373,26 @@ var SelectTranslationsPanel = new (class {
return;
}
this.#initializeEventListeners();
await this.#ensureLangListsBuilt();
try {
this.#initializeEventListeners();
await this.#ensureLangListsBuilt();
await Promise.all([
this.#cachePlaceholderText(),
this.#initializeLanguageMenuLists(langPairPromise),
this.#registerSourceText(sourceText, langPairPromise),
]);
this.#maybeRequestTranslation();
} catch (error) {
this.console?.error(error);
this.#changeStateToInitFailure(
event,
screenX,
screenY,
sourceText,
langPairPromise
);
}
await Promise.all([
this.#cachePlaceholderText(),
this.#initializeLanguageMenuLists(langPairPromise),
this.#registerSourceText(sourceText, langPairPromise),
]);
this.#maybeRequestTranslation();
await this.#openPopup(event, screenX, screenY);
}
@ -441,6 +461,45 @@ var SelectTranslationsPanel = new (class {
this.#translatingPlaceholderText = translatingText;
}
/**
* Opens the settings menu popup at the settings button gear-icon.
*/
#openSettingsPopup() {
const { settingsButton } = this.elements;
const popup = settingsButton.ownerDocument.getElementById(
"select-translations-panel-settings-menupopup"
);
popup.openPopup(settingsButton, "after_start");
}
/**
* Opens the "About translation in Firefox" Mozilla support page in a new tab.
*/
onAboutTranslations() {
this.close();
const window =
gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
window.openTrustedLinkIn(
"https://support.mozilla.org/kb/website-translation",
"tab",
{
forceForeground: true,
triggeringPrincipal:
Services.scriptSecurityManager.getSystemPrincipal(),
}
);
}
/**
* Opens the Translations section of about:preferences in a new tab.
*/
openTranslationsSettingsPage() {
this.close();
const window =
gBrowser.selectedBrowser.browsingContext.top.embedderElement.ownerGlobal;
window.openTrustedLinkIn("about:preferences#general-translations", "tab");
}
/**
* Handles events when a command event is triggered within the panel.
*
@ -448,30 +507,39 @@ var SelectTranslationsPanel = new (class {
*/
#handleCommandEvent(target) {
const {
cancelButton,
copyButton,
doneButton,
fromMenuList,
fromMenuPopup,
settingsButton,
toMenuList,
toMenuPopup,
translateButton,
translateFullPageButton,
tryAgainButton,
tryAnotherSourceMenuList,
tryAnotherSourceMenuPopup,
} = this.elements;
switch (target.id) {
case copyButton.id: {
this.onClickCopyButton();
break;
}
case cancelButton.id:
case doneButton.id: {
this.close();
break;
}
case copyButton.id: {
this.onClickCopyButton();
break;
}
case fromMenuList.id:
case fromMenuPopup.id: {
this.onChangeFromLanguage();
break;
}
case settingsButton.id: {
this.#openSettingsPopup();
break;
}
case toMenuList.id:
case toMenuPopup.id: {
this.onChangeToLanguage();
@ -481,6 +549,14 @@ var SelectTranslationsPanel = new (class {
this.onClickTranslateButton();
break;
}
case translateFullPageButton.id: {
this.onClickTranslateFullPageButton();
break;
}
case tryAgainButton.id: {
this.onClickTryAgainButton();
break;
}
case tryAnotherSourceMenuList.id:
case tryAnotherSourceMenuPopup.id: {
this.onChangeTryAnotherSourceLanguage();
@ -605,10 +681,65 @@ var SelectTranslationsPanel = new (class {
onClickTranslateButton() {
const { fromMenuList, tryAnotherSourceMenuList } = this.elements;
fromMenuList.value = tryAnotherSourceMenuList.value;
this.#deselectLanguage(tryAnotherSourceMenuList);
this.#maybeRequestTranslation();
}
/**
* Handles events when the panel's translate-full-page button is clicked.
*/
onClickTranslateFullPageButton() {
const { panel } = this.elements;
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
const actor = TranslationsParent.getTranslationsActor(
gBrowser.selectedBrowser
);
panel.addEventListener(
"popuphidden",
() =>
actor.translate(
fromLanguage,
toLanguage,
false // reportAsAutoTranslate
),
{ once: true }
);
this.close();
}
/**
* Handles events when the panel's try-again button is clicked.
*/
onClickTryAgainButton() {
switch (this.phase()) {
case "translation-failure": {
// If the translation failed, we just need to try translating again.
this.#maybeRequestTranslation();
break;
}
case "init-failure": {
// If the initialization failed, we need to close the panel and try reopening it
// which will attempt to initialize everything again after failure.
const { panel } = this.elements;
const { event, screenX, screenY, sourceText, langPairPromise } =
this.#translationState;
panel.addEventListener(
"popuphidden",
() => this.open(event, screenX, screenY, sourceText, langPairPromise),
{ once: true }
);
this.close();
break;
}
default: {
this.console?.error(
`Unexpected state "${this.phase()}" on try-again button click.`
);
}
}
}
/**
* Changes the copy button's visual icon to checked, and its localized text to "Copied".
*/
@ -700,21 +831,6 @@ var SelectTranslationsPanel = new (class {
return fromLanguage === selectedFromLang && toLanguage === selectedToLang;
}
/**
* Checks if the translator's language configuration matches the given language pair.
*
* @param {string} fromLanguage - The from-language to compare.
* @param {string} toLanguage - The to-language to compare.
*
* @returns {boolean} - True if the translator's languages match the given pair, otherwise false.
*/
#translatorMatchesLangPair(fromLanguage, toLanguage) {
return (
this.#translator?.fromLanguage === fromLanguage &&
this.#translator?.toLanguage === toLanguage
);
}
/**
* Retrieves the currently selected language pair from the menu lists.
*
@ -783,6 +899,8 @@ var SelectTranslationsPanel = new (class {
switch (phase) {
case "closed":
case "idle":
case "init-failure":
case "translation-failure":
case "translatable":
case "translating":
case "translated":
@ -868,41 +986,79 @@ var SelectTranslationsPanel = new (class {
}
/**
* Transitions the phase of the state based on the given language pair.
* Changes the phase to "init-failure".
*/
#changeStateToInitFailure(
event,
screenX,
screenY,
sourceText,
langPairPromise
) {
this.#changeStateTo("init-failure", /* retainEntries */ true, {
event,
screenX,
screenY,
sourceText,
langPairPromise,
});
}
/**
* Changes the phase from "translating" to "translation-failure".
*/
#changeStateToTranslationFailure() {
const phase = this.phase();
if (phase !== "translating") {
this.console?.error(
`Invalid state change (${phase} => translation-failure)`
);
}
this.#changeStateTo("translation-failure", /* retainEntries */ true);
}
/**
* Transitions the phase to "translatable" if the proper conditions are met,
* otherwise retains the same phase as before.
*
* @param {string} fromLanguage - The BCP-47 from-language tag.
* @param {string} toLanguage - The BCP-47 to-language tag.
*
* @returns {SelectTranslationsPanelState} The new phase of the translation state.
*/
#changeStateByLanguagePair(fromLanguage, toLanguage) {
#maybeChangeStateToTranslatable(fromLanguage, toLanguage) {
const {
phase: previousPhase,
fromLanguage: previousFromLanguage,
toLanguage: previousToLanguage,
} = this.#translationState;
let nextPhase = "translatable";
const langSelectionChanged = () =>
previousFromLanguage !== fromLanguage ||
previousToLanguage !== toLanguage;
const shouldTranslateEvenIfLangSelectionHasNotChanged = () => {
const phase = this.phase();
return (
// The panel has just opened, and this is the initial translation.
phase === "idle" ||
// The previous translation failed and we are about to try again.
phase === "translation-failure"
);
};
if (
// No from-language is selected, so we cannot translate.
!fromLanguage ||
// No to-language is selected, so we cannot translate.
!toLanguage ||
// The languages have not changed, so there is nothing to do.
(this.phase() !== "idle" &&
previousFromLanguage === fromLanguage &&
previousToLanguage === toLanguage)
// A valid from-language is actively selected.
fromLanguage &&
// A valid to-language is actively selected.
toLanguage &&
// The language selection has changed, requiring a new translation.
(langSelectionChanged() ||
// We should try to translate even if the language selection has not changed.
shouldTranslateEvenIfLangSelectionHasNotChanged())
) {
nextPhase = previousPhase;
this.#changeStateTo("translatable", /* retainEntries */ true, {
fromLanguage,
toLanguage,
});
}
this.#changeStateTo(nextPhase, /* retainEntries */ true, {
fromLanguage,
toLanguage,
});
return nextPhase;
}
/**
@ -913,11 +1069,13 @@ var SelectTranslationsPanel = new (class {
#handleCopyButtonChanges(phase) {
switch (phase) {
case "closed":
case "translation-failure":
case "translated": {
this.#uncheckCopyButton();
break;
}
case "idle":
case "init-failure":
case "translatable":
case "translating":
case "unsupported": {
@ -944,6 +1102,8 @@ var SelectTranslationsPanel = new (class {
}
case "closed":
case "idle":
case "init-failure":
case "translation-failure":
case "translatable":
case "translated":
case "unsupported": {
@ -968,6 +1128,14 @@ var SelectTranslationsPanel = new (class {
this.#displayIdlePlaceholder();
break;
}
case "init-failure": {
this.#displayInitFailureMessage();
break;
}
case "translation-failure": {
this.#displayTranslationFailureMessage();
break;
}
case "translatable": {
// Do nothing.
break;
@ -1006,9 +1174,7 @@ var SelectTranslationsPanel = new (class {
// Continue only if the current translationId matches.
translationId === this.#translationId &&
// Continue only if the given language pair is still the actively selected pair.
this.#isSelectedLangPair(fromLanguage, toLanguage) &&
// Continue only if the given language pair matches the current translator.
this.#translatorMatchesLangPair(fromLanguage, toLanguage)
this.#isSelectedLangPair(fromLanguage, toLanguage)
);
}
@ -1119,23 +1285,36 @@ var SelectTranslationsPanel = new (class {
*/
#showMainContent() {
const {
cancelButton,
copyButton,
doneButton,
initFailureContent,
mainContent,
unsupportedLanguageContent,
textArea,
translateButton,
translateFullPageButton,
translationFailureMessageBar,
tryAgainButton,
} = this.elements;
this.#setPanelElementAttributes({
makeHidden: [unsupportedLanguageContent, translateButton],
makeHidden: [
cancelButton,
initFailureContent,
translateButton,
translationFailureMessageBar,
tryAgainButton,
unsupportedLanguageContent,
],
makeVisible: [
mainContent,
copyButton,
doneButton,
textArea,
translateFullPageButton,
],
addDefault: [doneButton],
removeDefault: [translateButton],
removeDefault: [translateButton, tryAgainButton],
});
}
@ -1144,18 +1323,96 @@ var SelectTranslationsPanel = new (class {
*/
#showUnsupportedLanguageContent() {
const {
cancelButton,
copyButton,
doneButton,
initFailureContent,
mainContent,
unsupportedLanguageContent,
translateButton,
translateFullPageButton,
tryAgainButton,
} = this.elements;
this.#setPanelElementAttributes({
makeHidden: [mainContent, copyButton, translateFullPageButton],
makeVisible: [unsupportedLanguageContent, doneButton, translateButton],
makeHidden: [
cancelButton,
copyButton,
initFailureContent,
mainContent,
translateFullPageButton,
tryAgainButton,
],
makeVisible: [doneButton, translateButton, unsupportedLanguageContent],
addDefault: [translateButton],
removeDefault: [doneButton],
removeDefault: [doneButton, tryAgainButton],
});
}
/**
* Displays the panel content for when the language dropdowns fail to populate.
*/
#displayInitFailureMessage() {
const {
cancelButton,
copyButton,
doneButton,
initFailureContent,
mainContent,
unsupportedLanguageContent,
translateButton,
translateFullPageButton,
tryAgainButton,
} = this.elements;
this.#setPanelElementAttributes({
makeHidden: [
doneButton,
copyButton,
mainContent,
translateButton,
translateFullPageButton,
unsupportedLanguageContent,
],
makeVisible: [initFailureContent, cancelButton, tryAgainButton],
addDefault: [tryAgainButton],
removeDefault: [doneButton, translateButton],
});
}
/**
* Displays the panel content for when a translation fails to complete.
*/
#displayTranslationFailureMessage() {
const {
cancelButton,
copyButton,
doneButton,
initFailureContent,
mainContent,
unsupportedLanguageContent,
textArea,
translateButton,
translateFullPageButton,
translationFailureMessageBar,
tryAgainButton,
} = this.elements;
this.#setPanelElementAttributes({
makeHidden: [
doneButton,
copyButton,
initFailureContent,
translateButton,
translateFullPageButton,
textArea,
unsupportedLanguageContent,
],
makeVisible: [
cancelButton,
mainContent,
translationFailureMessageBar,
tryAgainButton,
],
addDefault: [tryAgainButton],
removeDefault: [doneButton, translateButton],
});
}
@ -1243,24 +1500,16 @@ var SelectTranslationsPanel = new (class {
*
* @returns {Promise<Translator>} A promise that resolves to a `Translator` instance for the given language pair.
*/
async #getOrCreateTranslator(fromLanguage, toLanguage) {
if (this.#translatorMatchesLangPair(fromLanguage, toLanguage)) {
return this.#translator;
}
async #createTranslator(fromLanguage, toLanguage) {
this.console?.log(
`Creating new Translator (${fromLanguage}-${toLanguage})`
);
if (this.#translator) {
this.#translator.destroy();
this.#translator = null;
}
this.#translator = await Translator.create(fromLanguage, toLanguage, {
const translator = await Translator.create(fromLanguage, toLanguage, {
allowSameLanguage: true,
requestTranslationsPort: this.#requestTranslationsPort,
});
return this.#translator;
return translator;
}
/**
@ -1271,14 +1520,16 @@ var SelectTranslationsPanel = new (class {
if (this.#isClosed()) {
return;
}
const { fromLanguage, toLanguage } = this.#getSelectedLanguagePair();
const nextState = this.#changeStateByLanguagePair(fromLanguage, toLanguage);
if (nextState !== "translatable") {
this.#maybeChangeStateToTranslatable(fromLanguage, toLanguage);
if (this.phase() !== "translatable") {
return;
}
const translationId = ++this.#translationId;
this.#getOrCreateTranslator(fromLanguage, toLanguage)
this.#createTranslator(fromLanguage, toLanguage)
.then(translator => {
if (
this.#shouldContinueTranslation(
@ -1302,13 +1553,12 @@ var SelectTranslationsPanel = new (class {
)
) {
this.#changeStateToTranslated(translatedText);
} else if (this.#isOpen()) {
this.#changeStateTo("idle", /* retainEntires */ false, {
sourceText: this.getSourceText(),
});
}
})
.catch(error => this.console?.error(error));
.catch(error => {
this.console?.error(error);
this.#changeStateToTranslationFailure();
});
}
/**

View file

@ -62,6 +62,8 @@ skip-if = ["true"]
["browser_translations_full_page_panel_gear.js"]
["browser_translations_full_page_panel_init_failure.js"]
["browser_translations_full_page_panel_never_translate_language.js"]
["browser_translations_full_page_panel_never_translate_site_auto.js"]
@ -117,6 +119,8 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_select_panel_fallback_to_doc_language.js"]
["browser_translations_select_panel_init_failure.js"]
["browser_translations_select_panel_retranslate_on_change_language_directly.js"]
["browser_translations_select_panel_retranslate_on_change_language_from_dropdown_menu.js"]
@ -129,6 +133,10 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_select_panel_select_same_from_and_to_languages_from_dropdown_menu.js"]
["browser_translations_select_panel_settings_menu.js"]
["browser_translations_select_panel_translate_full_page_button.js"]
["browser_translations_select_panel_translate_on_change_language_directly.js"]
["browser_translations_select_panel_translate_on_change_language_from_dropdown_menu.js"]
@ -139,4 +147,10 @@ skip-if = ["os == 'linux' && !debug"] # Bug 1863227
["browser_translations_select_panel_translate_on_open.js"]
["browser_translations_select_panel_translation_failure_after_unsupported_language.js"]
["browser_translations_select_panel_translation_failure_on_open.js"]
["browser_translations_select_panel_translation_failure_on_retranslate.js"]
["browser_translations_select_panel_unsupported_language.js"]

View file

@ -0,0 +1,25 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case verifies that the proper error message is displayed in
* the FullPageTranslationsPanel if the panel tries to open, but the language
* dropdown menus fail to initialize.
*/
add_task(async function test_full_page_translations_panel_init_failure() {
const { cleanup } = await loadTestPage({
page: SPANISH_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
});
TranslationsPanelShared.simulateLangListError();
await FullPageTranslationsTestUtils.openPanel({
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewInitFailure,
});
await FullPageTranslationsTestUtils.clickCancelButton();
await cleanup();
});

View file

@ -0,0 +1,119 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case verifies the scenario of clicking the cancel button to close
* the SelectTranslationsPanel after the language lists fail to initialize upon
* opening the panel, and the proper error message is displayed.
*/
add_task(async function test_select_translations_panel_init_failure_cancel() {
const { cleanup, runInPage } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
TranslationsPanelShared.simulateLangListError();
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
});
await SelectTranslationsTestUtils.clickCancelButton();
await cleanup();
});
/**
* This test case verifies the scenario of opening the SelectTranslationsPanel to a valid
* language pair, but having the language lists fail to initialize, then clicking the try-again
* button multiple times until both initialization and translation succeed.
*/
add_task(
async function test_select_translations_panel_init_failure_try_again_into_translation() {
const { cleanup, runInPage, resolveDownloads, rejectDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
TranslationsPanelShared.simulateLangListError();
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
});
TranslationsPanelShared.simulateLangListError();
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
SelectTranslationsTestUtils.clickTryAgainButton,
SelectTranslationsTestUtils.assertPanelViewInitFailure
);
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
async () =>
SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: rejectDownloads,
}),
SelectTranslationsTestUtils.assertPanelViewTranslationFailure
);
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: resolveDownloads,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);
/**
* This test case verifies the scenario of opening the SelectTranslationsPanel to an unsupported
* language, but having the language lists fail to initialize, then clicking the try-again
* button multiple times until the unsupported-language view is shown.
*/
add_task(
async function test_select_translations_panel_init_failure_try_again_into_unsupported() {
const { cleanup, runInPage } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: [
// Do not include Spanish.
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
],
prefs: [["browser.translations.select.enable", true]],
});
TranslationsPanelShared.simulateLangListError();
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectSpanishSection: true,
openAtSpanishSection: true,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewInitFailure,
});
TranslationsPanelShared.simulateLangListError();
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
SelectTranslationsTestUtils.clickTryAgainButton,
SelectTranslationsTestUtils.assertPanelViewInitFailure
);
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popupshown",
SelectTranslationsTestUtils.clickTryAgainButton,
SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage
);
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);

View file

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case tests the scenario of clicking the settings menu item
* that leads to the translations section of the about:preferences settings
* page in Firefox.
*/
add_task(async function test_select_translations_panel_open_settings_page() {
const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.openPanelSettingsMenu();
SelectTranslationsTestUtils.clickTranslationsSettingsPageMenuItem();
await waitForCondition(
() => gBrowser.currentURI.spec === "about:preferences#general",
"Waiting for about:preferences to be opened."
);
info("Remove the about:preferences tab");
gBrowser.removeCurrentTab();
await cleanup();
});
/**
* This test case tests the scenario of opening the SelectTranslationsPanel
* settings menu from the unsupported-language panel state.
*/
add_task(
async function test_select_translations_panel_open_settings_menu_from_unsupported_language() {
const { cleanup, runInPage } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: [
// Do not include Spanish.
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
],
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectSpanishSection: true,
openAtSpanishSection: true,
onOpenPanel:
SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
});
await SelectTranslationsTestUtils.openPanelSettingsMenu();
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);

View file

@ -0,0 +1,83 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Simulates clicking the translate-full-page button with a from-language that
* matches the language of the given document.
*/
add_task(
async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() {
const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
openAtSpanishHyperlink: true,
expectedFromLanguage: "es",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickTranslateFullPageButton();
await FullPageTranslationsTestUtils.assertPageIsTranslated(
"es",
"en",
runInPage
);
await cleanup();
}
);
/**
* Simulates clicking the translate-full-page button after changing the from-language
* and to-language values to values that don't match the document language or the
* user's app locale, ensuring that the current selection is respected.
*/
add_task(
async function test_select_translations_panel_translat_full_page_button_matching_doc_lang() {
const { cleanup, runInPage, resolveDownloads } = await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectSpanishSection: true,
openAtSpanishSection: true,
expectedFromLanguage: "es",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["fr"], {
openDropdownMenu: false,
downloadHandler: resolveDownloads,
onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
openDropdownMenu: true,
downloadHandler: resolveDownloads,
pivotTranslation: true,
onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickTranslateFullPageButton();
await FullPageTranslationsTestUtils.assertPageIsTranslated(
"fr",
"uk",
runInPage
);
await cleanup();
}
);

View file

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case tests the scenario of encountering the translation failure message
* as a result of changing the source language from the unsupported-language state.
*/
add_task(
async function test_select_translations_panel_failure_after_unsupported_language() {
const { cleanup, runInPage, resolveDownloads, rejectDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: [
// Do not include Spanish.
{ fromLang: "fr", toLang: "en" },
{ fromLang: "en", toLang: "fr" },
],
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectSpanishSentence: true,
openAtSpanishSentence: true,
onOpenPanel:
SelectTranslationsTestUtils.assertPanelViewUnsupportedLanguage,
});
await SelectTranslationsTestUtils.changeSelectedTryAnotherSourceLanguage(
"fr"
);
await SelectTranslationsTestUtils.clickTranslateButton({
downloadHandler: rejectDownloads,
viewAssertion:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: rejectDownloads,
viewAssertion:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: resolveDownloads,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);

View file

@ -0,0 +1,92 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case tests the scenario of opening the SelectTranslationsPanel to a translation
* attempt that fails, followed by closing the panel via the cancel button, and then re-attempting
* the translation by re-opening the panel and having it succeed.
*/
add_task(
async function test_select_translations_panel_translation_failure_on_open_then_cancel_and_reopen() {
const { cleanup, runInPage, rejectDownloads, resolveDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: rejectDownloads,
onOpenPanel:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickCancelButton();
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);
/**
* This test case tests the scenario of opening the SelectTranslationsPanel to a translation
* attempt that fails, followed by clicking the try-again button multiple times to retry the
* translation until it finally succeeds.
*/
add_task(
async function test_select_translations_panel_translation_failure_on_open_then_try_again() {
const { cleanup, runInPage, rejectDownloads, resolveDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: rejectDownloads,
onOpenPanel:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: rejectDownloads,
viewAssertion:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: rejectDownloads,
viewAssertion:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: resolveDownloads,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);

View file

@ -0,0 +1,128 @@
/* Any copyright is dedicated to the Public Domain.
https://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* This test case tests the scenario of encountering the translation failure message
* as a result of changing the selected from-language, along with moving from the failure
* state to a successful translation also by changing the selected from-language.
*/
add_task(
async function test_select_translations_panel_translation_failure_on_change_from_language() {
const { cleanup, runInPage, rejectDownloads, resolveDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
openDropdownMenu: false,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
openDropdownMenu: true,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["es"], {
openDropdownMenu: true,
downloadHandler: resolveDownloads,
onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk"], {
openDropdownMenu: false,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: resolveDownloads,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);
/**
* This test case tests the scenario of encountering the translation failure message
* as a result of changing the selected to-language, along with moving from the failure
* state to a successful translation also by changing the selected to-language.
*/
add_task(
async function test_select_translations_panel_translation_failure_on_change_to_language() {
const { cleanup, runInPage, rejectDownloads, resolveDownloads } =
await loadTestPage({
page: SELECT_TEST_PAGE_URL,
languagePairs: LANGUAGE_PAIRS,
prefs: [["browser.translations.select.enable", true]],
});
await SelectTranslationsTestUtils.openPanel(runInPage, {
selectFrenchSentence: true,
openAtFrenchSentence: true,
expectedFromLanguage: "fr",
expectedToLanguage: "en",
downloadHandler: resolveDownloads,
onOpenPanel: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
openDropdownMenu: false,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
openDropdownMenu: true,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["es"], {
openDropdownMenu: true,
downloadHandler: resolveDownloads,
pivotTranslation: true,
onChangeLanguage: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk"], {
openDropdownMenu: false,
downloadHandler: rejectDownloads,
onChangeLanguage:
SelectTranslationsTestUtils.assertPanelViewTranslationFailure,
});
await SelectTranslationsTestUtils.clickTryAgainButton({
downloadHandler: resolveDownloads,
pivotTranslation: true,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.clickDoneButton();
await cleanup();
}
);

View file

@ -69,7 +69,7 @@ add_task(
);
await SelectTranslationsTestUtils.clickTranslateButton({
onTranslated: SelectTranslationsTestUtils.assertPanelViewTranslated,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedToLanguage(["uk", "fi"], {
@ -134,7 +134,7 @@ add_task(
await SelectTranslationsTestUtils.clickTranslateButton({
downloadHandler: resolveDownloads,
onTranslated: SelectTranslationsTestUtils.assertPanelViewTranslated,
viewAssertion: SelectTranslationsTestUtils.assertPanelViewTranslated,
});
await SelectTranslationsTestUtils.changeSelectedFromLanguage(["uk", "fi"], {

View file

@ -681,6 +681,9 @@ class FullPageTranslationsTestUtils {
changeSourceLanguageButton: false,
dismissErrorButton: false,
error: false,
errorMessage: false,
errorMessageHint: false,
errorHintAction: false,
fromMenuList: false,
fromLabel: false,
header: false,
@ -748,6 +751,34 @@ class FullPageTranslationsTestUtils {
);
}
/**
* Asserts that panel element visibility matches the initialization-failure view.
*/
static assertPanelViewInitFailure() {
info("Checking that the panel shows the default view");
const { translateButton } = FullPageTranslationsPanel.elements;
FullPageTranslationsTestUtils.#assertPanelMainViewId(
"full-page-translations-panel-view-default"
);
FullPageTranslationsTestUtils.#assertPanelElementVisibility({
cancelButton: true,
error: true,
errorMessage: true,
errorMessageHint: true,
errorHintAction: true,
header: true,
translateButton: true,
});
is(
translateButton.disabled,
true,
"The translate button should be disabled."
);
FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
"translations-panel-header"
);
}
/**
* Asserts that panel element visibility matches the panel error view.
*/
@ -758,6 +789,7 @@ class FullPageTranslationsTestUtils {
);
FullPageTranslationsTestUtils.#assertPanelElementVisibility({
error: true,
errorMessage: true,
...FullPageTranslationsTestUtils.#defaultViewVisibilityExpectations,
});
FullPageTranslationsTestUtils.#assertPanelHeaderL10nId(
@ -1085,7 +1117,7 @@ class FullPageTranslationsTestUtils {
static async #clickSettingsMenuItemByL10nId(l10nId) {
info(`Toggling the "${l10nId}" settings menu item.`);
click(getByL10nId(l10nId), `Clicking the "${l10nId}" settings menu item.`);
await closeSettingsMenuIfOpen();
await closeFullPagePanelSettingsMenuIfOpen();
}
/**
@ -1486,19 +1518,25 @@ class SelectTranslationsTestUtils {
SelectTranslationsPanel.elements,
{
betaIcon: false,
cancelButton: false,
copyButton: false,
doneButton: false,
fromLabel: false,
fromMenuList: false,
fromMenuPopup: false,
header: false,
initFailureContent: false,
initFailureMessageBar: false,
mainContent: false,
settingsButton: false,
textArea: false,
toLabel: false,
toMenuList: false,
toMenuPopup: false,
translateButton: false,
translateFullPageButton: false,
translationFailureMessageBar: false,
tryAgainButton: false,
tryAnotherSourceMenuList: false,
tryAnotherSourceMenuPopup: false,
unsupportedLanguageContent: false,
@ -1548,6 +1586,7 @@ class SelectTranslationsTestUtils {
fromMenuList: true,
header: true,
mainContent: true,
settingsButton: true,
textArea: true,
toLabel: true,
toMenuList: true,
@ -1573,6 +1612,44 @@ class SelectTranslationsTestUtils {
await SelectTranslationsTestUtils.#assertPanelTextAreaOverflow();
}
/**
* Asserts that the SelectTranslationsPanel UI matches the expected
* state when the language lists fail to initialize upon opening the panel.
*/
static async assertPanelViewInitFailure() {
await SelectTranslationsTestUtils.waitForPanelState("init-failure");
SelectTranslationsTestUtils.#assertPanelElementVisibility({
header: true,
betaIcon: true,
cancelButton: true,
initFailureContent: true,
initFailureMessageBar: true,
settingsButton: true,
tryAgainButton: true,
});
}
/**
* Asserts that the SelectTranslationsPanel UI matches the expected
* state when a translation has failed to complete.
*/
static async assertPanelViewTranslationFailure() {
await SelectTranslationsTestUtils.waitForPanelState("translation-failure");
SelectTranslationsTestUtils.#assertPanelElementVisibility({
header: true,
betaIcon: true,
cancelButton: true,
fromLabel: true,
fromMenuList: true,
mainContent: true,
settingsButton: true,
toLabel: true,
toMenuList: true,
translationFailureMessageBar: true,
tryAgainButton: true,
});
}
static #assertPanelTextAreaDirection(langTag = null) {
const expectedTextDirection = langTag
? Services.intl.getScriptDirection(langTag)
@ -1602,6 +1679,7 @@ class SelectTranslationsTestUtils {
betaIcon: true,
doneButton: true,
header: true,
settingsButton: true,
translateButton: true,
tryAnotherSourceMenuList: true,
unsupportedLanguageContent: true,
@ -1685,6 +1763,7 @@ class SelectTranslationsTestUtils {
fromMenuList: true,
header: true,
mainContent: true,
settingsButton: true,
textArea: true,
toLabel: true,
toMenuList: true,
@ -1836,6 +1915,21 @@ class SelectTranslationsTestUtils {
);
}
/**
* Simulates clicking the cancel button and waits for the panel to close.
*/
static async clickCancelButton() {
logAction();
const { cancelButton } = SelectTranslationsPanel.elements;
assertVisibility({ visible: { cancelButton } });
await SelectTranslationsTestUtils.waitForPanelPopupEvent(
"popuphidden",
() => {
click(cancelButton, "Clicking the cancel button");
}
);
}
/**
* Simulates clicking the copy button and asserts that all relevant states are correctly updated.
*/
@ -1881,13 +1975,13 @@ class SelectTranslationsTestUtils {
* @param {boolean} [config.pivotTranslation]
* - True if the expected translation is a pivot translation, otherwise false.
* Affects the number of expected downloads.
* @param {Function} [config.onTranslated]
* - An optional callback function to execute after the translation completes.
* @param {Function} [config.viewAssertion]
* - An optional callback function to execute for asserting the panel UI state.
*/
static async clickTranslateButton({
downloadHandler,
pivotTranslation,
onTranslated,
viewAssertion,
}) {
logAction();
const { translateButton } = SelectTranslationsPanel.elements;
@ -1898,11 +1992,98 @@ class SelectTranslationsTestUtils {
if (downloadHandler) {
await this.handleDownloads({ downloadHandler, pivotTranslation });
}
if (onTranslated) {
await onTranslated();
if (viewAssertion) {
await viewAssertion();
}
}
/**
* Simulates clicking the translate-full-page button.
*/
static async clickTranslateFullPageButton() {
logAction();
const { translateFullPageButton } = SelectTranslationsPanel.elements;
assertVisibility({ visible: { translateFullPageButton } });
click(translateFullPageButton);
await FullPageTranslationsTestUtils.assertTranslationsButton(
{ button: true, circleArrows: true, locale: false, icon: true },
"The icon presents the loading indicator."
);
}
/**
* Simulates clicking the try-again button.
*
* @param {object} config
* @param {Function} [config.downloadHandler]
* - The function handle expected downloads, resolveDownloads() or rejectDownloads()
* Leave as null to test more granularly, such as testing opening the loading view,
* or allowing for the automatic downloading of files.
* @param {boolean} [config.pivotTranslation]
* - True if the expected translation is a pivot translation, otherwise false.
* Affects the number of expected downloads.
* @param {Function} [config.viewAssertion]
* - An optional callback function to execute for asserting the panel UI state.
*/
static async clickTryAgainButton({
downloadHandler,
pivotTranslation,
viewAssertion,
} = {}) {
logAction();
const { tryAgainButton } = SelectTranslationsPanel.elements;
assertVisibility({ visible: { tryAgainButton } });
click(tryAgainButton, "Clicking the try-again button");
await SelectTranslationsTestUtils.waitForPanelState("translatable");
if (downloadHandler) {
await this.handleDownloads({ downloadHandler, pivotTranslation });
}
if (viewAssertion) {
await viewAssertion();
}
}
/**
* Opens the SelectTranslationsPanel settings menu.
* Requires that the translations panel is already open.
*/
static async openPanelSettingsMenu() {
logAction();
const { settingsButton } = SelectTranslationsPanel.elements;
assertVisibility({ visible: { settingsButton } });
await SharedTranslationsTestUtils._waitForPopupEvent(
"select-translations-panel-settings-menupopup",
"popupshown",
() => click(settingsButton, "Opening the settings menu")
);
const settingsPageMenuItem = document.getElementById(
"select-translations-panel-open-settings-page-menuitem"
);
const aboutTranslationsMenuItem = document.getElementById(
"select-translations-panel-about-translations-menuitem"
);
assertVisibility({
visible: {
settingsPageMenuItem,
aboutTranslationsMenuItem,
},
});
}
/**
* Clicks the SelectTranslationsPanel settings menu item
* that leads to the Translations Settings in about:preferences.
*/
static clickTranslationsSettingsPageMenuItem() {
logAction();
const settingsPageMenuItem = document.getElementById(
"select-translations-panel-open-settings-page-menuitem"
);
assertVisibility({ visible: { settingsPageMenuItem } });
click(settingsPageMenuItem);
}
/**
* Opens the context menu at a specified element on the page, based on the provided options.
*

View file

@ -0,0 +1,110 @@
/* 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/. */
/**
* A provider that matches the urlbar input to built in actions.
*/
export class ActionsProvider {
/**
* Unique name for the provider.
*
* @abstract
*/
get name() {
return "ActionsProviderBase";
}
/**
* Whether this provider should be invoked for the given context.
* If this method returns false, the providers manager won't start a query
* with this provider, to save on resources.
*
* @param {UrlbarQueryContext} _queryContext The query context object.
* @returns {boolean} Whether this provider should be invoked for the search.
* @abstract
*/
isActive(_queryContext) {
throw new Error("Not implemented.");
}
/**
* Query for actions based on the current users input.
*
* @param {UrlbarQueryContext} _queryContext The query context object.
* @param {UrlbarController} _controller The urlbar controller.
* @returns {ActionsResult}
* @abstract
*/
async queryAction(_queryContext, _controller) {
throw new Error("Not implemented.");
}
/**
* Pick an action.
*
* @param {UrlbarQueryContext} _queryContext The query context object.
* @param {UrlbarController} _controller The urlbar controller.
* @param {DOMElement} _element The element that was selected.
* @abstract
*/
pickAction(_queryContext, _controller, _element) {
throw new Error("Not implemented.");
}
}
/**
* Class used to create an Actions Result.
*/
export class ActionsResult {
providerName;
#key;
#l10nId;
#l10nArgs;
#icon;
#dataset;
/**
* @param {object} options
* An option object.
* @param { string } options.key
* A string key used to distinguish between different actions.
* @param { string } options.l10nId
* The id of the l10n string displayed in the action button.
* @param { string } options.l10nArgs
* Arguments passed to construct the above string
* @param { string } options.icon
* The icon displayed in the button.
* @param {object} options.dataset
* An object of properties we set on the action button that
* can be used to pass data when it is selected.
*/
constructor({ key, l10nId, l10nArgs, icon, dataset }) {
this.#key = key;
this.#l10nId = l10nId;
this.#l10nArgs = l10nArgs;
this.#icon = icon;
this.#dataset = dataset;
}
get key() {
return this.#key;
}
get l10nId() {
return this.#l10nId;
}
get l10nArgs() {
return this.#l10nArgs;
}
get icon() {
return this.#icon;
}
get dataset() {
return this.#dataset;
}
}

View file

@ -0,0 +1,119 @@
/* 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/. */
import { UrlbarUtils } from "resource:///modules/UrlbarUtils.sys.mjs";
import {
ActionsProvider,
ActionsResult,
} from "resource:///modules/ActionsProvider.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
OpenSearchEngine: "resource://gre/modules/OpenSearchEngine.sys.mjs",
loadAndParseOpenSearchEngine:
"resource://gre/modules/OpenSearchLoader.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
});
const ENABLED_PREF = "contextualSearch.enabled";
/**
* A provider that returns an option for using the search engine provided
* by the active view if it utilizes OpenSearch.
*/
class ProviderContextualSearch extends ActionsProvider {
constructor() {
super();
this.engines = new Map();
}
get name() {
return "ActionsProviderContextualSearch";
}
isActive(queryContext) {
return (
queryContext.trimmedSearchString &&
lazy.UrlbarPrefs.get(ENABLED_PREF) &&
!queryContext.searchMode
);
}
async queryAction(queryContext, controller) {
let instance = this.queryInstance;
const hostname = URL.parse(queryContext.currentPage)?.hostname;
// This happens on about pages, which won't have associated engines
if (!hostname) {
return null;
}
let engine = await this.fetchEngine(controller);
let icon = engine.icon || (await engine.getIconURL?.());
let defaultEngine = lazy.UrlbarSearchUtils.getDefaultEngine();
if (
!engine ||
engine.name === defaultEngine?.name ||
instance != this.queryInstance
) {
return null;
}
return new ActionsResult({
key: "contextual-search",
l10nId: "urlbar-result-search-with",
l10nArgs: { engine: engine.name || engine.title },
icon,
dataset: { input: queryContext.searchString },
});
}
async fetchEngine(controller) {
let browser = controller.browserWindow.gBrowser.selectedBrowser;
let hostname = browser?.currentURI.host;
if (this.engines.has(hostname)) {
return this.engines.get(hostname);
}
// Strip www. to allow for partial matches when looking for an engine.
const [host] = UrlbarUtils.stripPrefixAndTrim(hostname, {
stripWww: true,
});
let engines = await lazy.UrlbarSearchUtils.enginesForDomainPrefix(host, {
matchAllDomainLevels: true,
});
return engines[0] ?? browser?.engines?.[0];
}
async pickAction(_queryContext, controller, element) {
// If we have an engine to add, first create a new OpenSearchEngine, then
// get and open a url to execute a search for the term in the url bar.
let engine = await this.fetchEngine(controller);
if (engine.uri) {
let engineData = await lazy.loadAndParseOpenSearchEngine(
Services.io.newURI(engine.uri)
);
engine = new lazy.OpenSearchEngine({ engineData });
engine._setIcon(engine.icon, false);
}
const [url] = UrlbarUtils.getSearchQueryUrl(engine, element.dataset.input);
element.ownerGlobal.gBrowser.fixupAndLoadURIString(url, {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
element.ownerGlobal.gBrowser.selectedBrowser.focus();
}
resetForTesting() {
this.engines = new Map();
}
}
export var ActionsProviderContextualSearch = new ProviderContextualSearch();

View file

@ -0,0 +1,152 @@
/* 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/. */
import {
ActionsProvider,
ActionsResult,
} from "resource:///modules/ActionsProvider.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
QuickActionsLoaderDefault:
"resource:///modules/QuickActionsLoaderDefault.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
});
// These prefs are relative to the `browser.urlbar` branch.
const ENABLED_PREF = "quickactions.enabled";
const MATCH_IN_PHRASE_PREF = "quickactions.matchInPhrase";
const MIN_SEARCH_PREF = "quickactions.minimumSearchString";
/**
* A provider that matches the urlbar input to built in actions.
*/
class ProviderQuickActions extends ActionsProvider {
get name() {
return "ActionsProviderQuickActions";
}
isActive(queryContext) {
return (
lazy.UrlbarPrefs.get(ENABLED_PREF) &&
!queryContext.searchMode &&
queryContext.trimmedSearchString.length < 50 &&
queryContext.trimmedSearchString.length >
lazy.UrlbarPrefs.get(MIN_SEARCH_PREF)
);
}
async queryAction(queryContext) {
await lazy.QuickActionsLoaderDefault.ensureLoaded();
let input = queryContext.trimmedLowerCaseSearchString;
let results = [...(this.#prefixes.get(input) ?? [])];
if (lazy.UrlbarPrefs.get(MATCH_IN_PHRASE_PREF)) {
for (let [keyword, key] of this.#keywords) {
if (input.includes(keyword)) {
results.push(key);
}
}
}
// Remove invisible actions.
results = results.filter(key => {
const action = this.#actions.get(key);
return action.isVisible?.() ?? true;
});
if (!results.length) {
return null;
}
let action = this.#actions.get(results[0]);
return new ActionsResult({
key: results[0],
l10nId: action.label,
icon: action.icon,
dataset: {
action: results[0],
inputLength: queryContext.trimmedSearchString.length,
},
});
}
pickAction(_queryContext, _controller, element) {
let action = element.dataset.action;
let inputLength = Math.min(element.dataset.inputLength, 10);
Services.telemetry.keyedScalarAdd(
`quickaction.picked`,
`${action}-${inputLength}`,
1
);
let options = this.#actions.get(action).onPick();
if (options?.focusContent) {
element.ownerGlobal.gBrowser.selectedBrowser.focus();
}
}
/**
* Adds a new QuickAction.
*
* @param {string} key A key to identify this action.
* @param {string} definition An object that describes the action.
*/
addAction(key, definition) {
this.#actions.set(key, definition);
definition.commands.forEach(cmd => this.#keywords.set(cmd, key));
this.#loopOverPrefixes(definition.commands, prefix => {
let result = this.#prefixes.get(prefix);
if (result) {
if (!result.includes(key)) {
result.push(key);
}
} else {
result = [key];
}
this.#prefixes.set(prefix, result);
});
}
/**
* Removes an action.
*
* @param {string} key A key to identify this action.
*/
removeAction(key) {
let definition = this.#actions.get(key);
this.#actions.delete(key);
definition.commands.forEach(cmd => this.#keywords.delete(cmd));
this.#loopOverPrefixes(definition.commands, prefix => {
let result = this.#prefixes.get(prefix);
if (result) {
result = result.filter(val => val != key);
}
this.#prefixes.set(prefix, result);
});
}
// A map from keywords to an action.
#keywords = new Map();
// A map of all prefixes to an array of actions.
#prefixes = new Map();
// The actions that have been added.
#actions = new Map();
#loopOverPrefixes(commands, fun) {
for (const command of commands) {
// Loop over all the prefixes of the word, ie
// "", "w", "wo", "wor", stopping just before the full
// word itself which will be matched by the whole
// phrase matching.
for (let i = 1; i <= command.length; i++) {
let prefix = command.substring(0, command.length - i);
fun(prefix);
}
}
}
}
export var ActionsProviderQuickActions = new ProviderQuickActions();

View file

@ -8,12 +8,10 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
ClientEnvironment: "resource://normandy/lib/ClientEnvironment.sys.mjs",
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
ResetProfile: "resource://gre/modules/ResetProfile.sys.mjs",
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarProviderQuickActions:
"resource:///modules/UrlbarProviderQuickActions.sys.mjs",
ActionsProviderQuickActions:
"resource:///modules/ActionsProviderQuickActions.sys.mjs",
});
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
@ -26,7 +24,6 @@ if (AppConstants.MOZ_UPDATER) {
"nsIApplicationUpdateService"
);
}
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"SCREENSHOT_BROWSER_COMPONENT",
@ -97,24 +94,22 @@ const DEFAULT_ACTIONS = {
},
},
downloads: {
l10nCommands: ["quickactions-cmd-downloads", "quickactions-downloads2"],
l10nCommands: ["quickactions-cmd-downloads"],
icon: "chrome://browser/skin/downloads/downloads.svg",
label: "quickactions-downloads2",
onPick: openUrlFun("about:downloads"),
},
extensions: {
l10nCommands: ["quickactions-cmd-extensions", "quickactions-extensions"],
l10nCommands: ["quickactions-cmd-extensions"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-extensions",
onPick: openAddonsUrl("addons://list/extension"),
},
inspect: {
l10nCommands: ["quickactions-cmd-inspector", "quickactions-inspector2"],
l10nCommands: ["quickactions-cmd-inspector"],
icon: "chrome://devtools/skin/images/open-inspector.svg",
label: "quickactions-inspector2",
isVisible: () =>
lazy.DevToolsShim.isEnabled() || lazy.DevToolsShim.isDevToolsUser(),
isActive: () => {
isVisible: () => {
// The inspect action is available if:
// 1. DevTools is enabled.
// 2. The user can be considered as a DevTools user.
@ -132,18 +127,18 @@ const DEFAULT_ACTIONS = {
onPick: openInspector,
},
logins: {
l10nCommands: ["quickactions-cmd-logins", "quickactions-logins2"],
l10nCommands: ["quickactions-cmd-logins"],
label: "quickactions-logins2",
onPick: openUrlFun("about:logins"),
},
plugins: {
l10nCommands: ["quickactions-cmd-plugins", "quickactions-plugins"],
l10nCommands: ["quickactions-cmd-plugins"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-plugins",
onPick: openAddonsUrl("addons://list/plugin"),
},
print: {
l10nCommands: ["quickactions-cmd-print", "quickactions-print2"],
l10nCommands: ["quickactions-cmd-print"],
label: "quickactions-print2",
icon: "chrome://global/skin/icons/print.svg",
onPick: () => {
@ -153,7 +148,7 @@ const DEFAULT_ACTIONS = {
},
},
private: {
l10nCommands: ["quickactions-cmd-private", "quickactions-private2"],
l10nCommands: ["quickactions-cmd-private"],
label: "quickactions-private2",
icon: "chrome://global/skin/icons/indicator-private-browsing.svg",
onPick: () => {
@ -163,7 +158,7 @@ const DEFAULT_ACTIONS = {
},
},
refresh: {
l10nCommands: ["quickactions-cmd-refresh", "quickactions-refresh"],
l10nCommands: ["quickactions-cmd-refresh"],
label: "quickactions-refresh",
onPick: () => {
lazy.ResetProfile.openConfirmationDialog(
@ -172,7 +167,7 @@ const DEFAULT_ACTIONS = {
},
},
restart: {
l10nCommands: ["quickactions-cmd-restart", "quickactions-restart"],
l10nCommands: ["quickactions-cmd-restart"],
label: "quickactions-restart",
onPick: restartBrowser,
},
@ -197,10 +192,10 @@ const DEFAULT_ACTIONS = {
},
},
screenshot: {
l10nCommands: ["quickactions-cmd-screenshot", "quickactions-screenshot3"],
l10nCommands: ["quickactions-cmd-screenshot"],
label: "quickactions-screenshot3",
icon: "chrome://browser/skin/screenshot.svg",
isActive: () => {
isVisible: () => {
return !lazy.BrowserWindowTracker.getTopWindow().gScreenshots.shouldScreenshotsButtonBeDisabled();
},
onPick: () => {
@ -221,21 +216,21 @@ const DEFAULT_ACTIONS = {
},
},
settings: {
l10nCommands: ["quickactions-cmd-settings", "quickactions-settings2"],
l10nCommands: ["quickactions-cmd-settings"],
icon: "chrome://global/skin/icons/settings.svg",
label: "quickactions-settings2",
onPick: openUrlFun("about:preferences"),
},
themes: {
l10nCommands: ["quickactions-cmd-themes", "quickactions-themes"],
l10nCommands: ["quickactions-cmd-themes"],
icon: "chrome://mozapps/skin/extensions/category-extensions.svg",
label: "quickactions-themes",
onPick: openAddonsUrl("addons://list/theme"),
},
update: {
l10nCommands: ["quickactions-cmd-update", "quickactions-update"],
l10nCommands: ["quickactions-cmd-update"],
label: "quickactions-update",
isActive: () => {
isVisible: () => {
if (!AppConstants.MOZ_UPDATER) {
return false;
}
@ -246,10 +241,10 @@ const DEFAULT_ACTIONS = {
onPick: restartBrowser,
},
viewsource: {
l10nCommands: ["quickactions-cmd-viewsource", "quickactions-viewsource2"],
l10nCommands: ["quickactions-cmd-viewsource"],
icon: "chrome://global/skin/icons/settings.svg",
label: "quickactions-viewsource2",
isActive: () => currentBrowser()?.currentURI.scheme !== "view-source",
isVisible: () => currentBrowser()?.currentURI.scheme !== "view-source",
onPick: () => openUrl("view-source:" + currentBrowser().currentURI.spec),
},
};
@ -287,18 +282,6 @@ function restartBrowser() {
}
}
function random(seed) {
let x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
function shuffle(array, seed) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(random(seed) * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
/**
* Loads the default QuickActions.
*/
@ -308,18 +291,6 @@ export class QuickActionsLoaderDefault {
static async load() {
let keys = Object.keys(DEFAULT_ACTIONS);
if (lazy.UrlbarPrefs.get("quickactions.randomOrderActions")) {
// We insert the actions in a random order which means they will be returned
// in a random but consistent order (the order of results for "view" and "views"
// should be the same).
// We use the Nimbus randomizationId as the seed as the order should not change
// for the user between restarts, it should be random between users but a user should
// see actions the same order.
let seed = [...lazy.ClientEnvironment.randomizationId]
.map(x => x.charCodeAt(0))
.reduce((sum, a) => sum + a, 0);
shuffle(keys, seed);
}
for (const key of keys) {
let actionData = DEFAULT_ACTIONS[key];
let messages = await lazy.gFluentStrings.formatMessages(
@ -328,7 +299,7 @@ export class QuickActionsLoaderDefault {
actionData.commands = messages
.map(({ value }) => value.split(",").map(x => x.trim().toLowerCase()))
.flat();
lazy.UrlbarProviderQuickActions.addAction(key, actionData);
lazy.ActionsProviderQuickActions.addAction(key, actionData);
}
}
static async ensureLoaded() {

View file

@ -1001,7 +1001,6 @@ class TelemetryEvent {
searchWords,
searchSource,
searchMode,
selectedElement,
selIndex,
selType,
}
@ -1045,11 +1044,7 @@ class TelemetryEvent {
currentResults[selIndex],
selType
);
const selected_result_subtype =
lazy.UrlbarUtils.searchEngagementTelemetrySubtype(
currentResults[selIndex],
selectedElement
);
const selected_result_subtype = "";
if (selected_result === "input_field" && !this._controller.view?.isOpen) {
numResults = 0;

View file

@ -24,6 +24,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs",
UrlbarQueryContext: "resource:///modules/UrlbarUtils.sys.mjs",
UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.sys.mjs",
UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.sys.mjs",
UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.sys.mjs",
UrlbarTokenizer: "resource:///modules/UrlbarTokenizer.sys.mjs",
UrlbarUtils: "resource:///modules/UrlbarUtils.sys.mjs",
@ -900,6 +901,15 @@ export class UrlbarInput {
if (!result) {
return;
}
if (element?.dataset.action && element?.dataset.action != "tabswitch") {
this.view.close();
let provider = lazy.UrlbarProvidersManager.getActionProvider(
element.dataset.providerName
);
let { queryContext } = this.controller._lastQueryContextWrapper || {};
provider.pickAction(queryContext, this.controller, element);
return;
}
this.pickResult(result, event, element);
}
@ -1053,7 +1063,13 @@ export class UrlbarInput {
break;
}
case lazy.UrlbarUtils.RESULT_TYPE.TAB_SWITCH: {
if (this.hasAttribute("action-override")) {
// Behaviour is reversed with SecondaryActions, default behaviour is to navigate
// and button is provided to switch to tab.
if (
this.hasAttribute("action-override") ||
(lazy.UrlbarPrefs.get("secondaryActions.featureGate") &&
element?.dataset.action !== "tabswitch")
) {
where = "current";
break;
}
@ -2771,20 +2787,18 @@ export class UrlbarInput {
return;
}
let url =
element.dataset.command == "help"
? result.payload.helpUrl
: element.dataset.url;
let url;
if (element.dataset.command == "help") {
url = result.payload.helpUrl;
}
url ||= element.dataset.url;
if (!url) {
return;
}
let where = this._whereToOpen(event);
if (
url &&
result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP &&
where == "current"
) {
if (result.type != lazy.UrlbarUtils.RESULT_TYPE.TIP && where == "current") {
// Open non-tip help links in a new tab unless the user held a modifier.
// TODO (bug 1696232): Do this for tip help links, too.
where = "tab";

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