Update On Tue Apr 23 20:46:52 CEST 2024
This commit is contained in:
parent
8a7be19130
commit
7cdd4eb6fd
1186 changed files with 42039 additions and 15273 deletions
39
Cargo.lock
generated
39
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
|
|
161
accessible/tests/browser/windows/uia/browser_gridPatterns.js
Normal file
161
accessible/tests/browser/windows/uia/browser_gridPatterns.js
Normal 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");
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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
|
||||
|
|
151
accessible/windows/uia/UiaGrid.cpp
Normal file
151
accessible/windows/uia/UiaGrid.cpp
Normal 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;
|
||||
}
|
52
accessible/windows/uia/UiaGrid.h
Normal file
52
accessible/windows/uia/UiaGrid.h
Normal 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
|
130
accessible/windows/uia/UiaGridItem.cpp
Normal file
130
accessible/windows/uia/UiaGridItem.cpp
Normal 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;
|
||||
}
|
51
accessible/windows/uia/UiaGridItem.h
Normal file
51
accessible/windows/uia/UiaGridItem.h
Normal 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
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -2541,7 +2541,7 @@ class nsContextMenu {
|
|||
screenY,
|
||||
this.#getTextToTranslate(),
|
||||
this.#translationsLangPairPromise
|
||||
);
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -122,6 +122,7 @@ add_task(async function test_link_contextmenu() {
|
|||
"context-sendlinktodevice",
|
||||
"context-sep-sendlinktodevice",
|
||||
"context-searchselect",
|
||||
"context-translate-selection",
|
||||
"frame-sep"
|
||||
);
|
||||
|
||||
|
|
|
@ -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",
|
||||
]);
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -63,6 +63,8 @@ export class AddonsBackupResource extends BackupResource {
|
|||
stagingPath,
|
||||
databases
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async measure(profilePath = PathUtils.profileDir) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -20,6 +20,7 @@ export class CookiesBackupResource extends BackupResource {
|
|||
await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
|
||||
"cookies.sqlite",
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
async measure(profilePath = PathUtils.profileDir) {
|
||||
|
|
|
@ -20,6 +20,8 @@ export class FormHistoryBackupResource extends BackupResource {
|
|||
await BackupResource.copySqliteDatabases(profilePath, stagingPath, [
|
||||
"formhistory.sqlite",
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async measure(profilePath = PathUtils.profileDir) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,8 @@ export class SessionStoreBackupResource extends BackupResource {
|
|||
await BackupResource.copyFiles(profilePath, stagingPath, [
|
||||
"sessionstore-backups",
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async measure(profilePath = PathUtils.profileDir) {
|
||||
|
|
5
browser/components/backup/tests/marionette/manifest.toml
Normal file
5
browser/components/backup/tests/marionette/manifest.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[DEFAULT]
|
||||
run-if = ["buildapp == 'browser'"]
|
||||
prefs = ["browser.backup.enabled=true", "browser.backup.log=true"]
|
||||
|
||||
["test_backup.py"]
|
98
browser/components/backup/tests/marionette/test_backup.py
Normal file
98
browser/components/backup/tests/marionette/test_backup.py
Normal 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.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -294,6 +294,7 @@ function generateDocumentation() {
|
|||
SanitizeOnShutdown: "SanitizeOnShutdown2",
|
||||
WindowsSSO: "Windows10SSO",
|
||||
SecurityDevices: "SecurityDevices2",
|
||||
DisableFirefoxAccounts: "DisableFirefoxAccounts1",
|
||||
};
|
||||
|
||||
for (let policyName in schema.properties) {
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
|
|
281
browser/components/shell/Windows11LimitedAccessFeatures.cpp
Normal file
281
browser/components/shell/Windows11LimitedAccessFeatures.cpp
Normal 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
|
53
browser/components/shell/Windows11LimitedAccessFeatures.h
Normal file
53
browser/components/shell/Windows11LimitedAccessFeatures.h
Normal 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__
|
344
browser/components/shell/Windows11TaskbarPinning.cpp
Normal file
344
browser/components/shell/Windows11TaskbarPinning.cpp
Normal 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
|
35
browser/components/shell/Windows11TaskbarPinning.h
Normal file
35
browser/components/shell/Windows11TaskbarPinning.h
Normal 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__
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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%;",
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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();
|
||||
}
|
||||
);
|
|
@ -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"], {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
110
browser/components/urlbar/ActionsProvider.sys.mjs
Normal file
110
browser/components/urlbar/ActionsProvider.sys.mjs
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
152
browser/components/urlbar/ActionsProviderQuickActions.sys.mjs
Normal file
152
browser/components/urlbar/ActionsProviderQuickActions.sys.mjs
Normal 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();
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue