Update On Sun Apr 13 20:22:56 CEST 2025
This commit is contained in:
parent
745952e3b8
commit
9c69112ad3
2090 changed files with 102264 additions and 32761 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -2524,6 +2524,7 @@ dependencies = [
|
|||
name = "gkrust-uniffi-components"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.2",
|
||||
"relevancy",
|
||||
"search",
|
||||
"suggest",
|
||||
|
@ -2794,7 +2795,6 @@ version = "2.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"num-traits",
|
||||
|
@ -4498,7 +4498,7 @@ checksum = "a2983372caf4480544083767bf2d27defafe32af49ab4df3a0b7fc90793a3664"
|
|||
[[package]]
|
||||
name = "naga"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
|
@ -4506,7 +4506,7 @@ dependencies = [
|
|||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"half 2.5.0",
|
||||
"hashbrown 0.14.999",
|
||||
"hashbrown 0.15.2",
|
||||
"hexf-parse",
|
||||
"indexmap",
|
||||
"log",
|
||||
|
@ -5931,19 +5931,20 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.0.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
|
||||
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_with_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.0.0"
|
||||
version = "3.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
|
||||
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
|
@ -7266,7 +7267,7 @@ version = "0.3.100"
|
|||
|
||||
[[package]]
|
||||
name = "webdriver"
|
||||
version = "0.52.1"
|
||||
version = "0.53.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
|
@ -7427,7 +7428,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
|
@ -7436,7 +7437,7 @@ dependencies = [
|
|||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.14.999",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"log",
|
||||
"naga",
|
||||
|
@ -7457,7 +7458,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core-deps-apple"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
@ -7465,7 +7466,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-core-deps-windows-linux-android"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"wgpu-hal",
|
||||
]
|
||||
|
@ -7473,7 +7474,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-hal"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"arrayvec",
|
||||
|
@ -7487,7 +7488,7 @@ dependencies = [
|
|||
"gpu-alloc",
|
||||
"gpu-allocator",
|
||||
"gpu-descriptor",
|
||||
"hashbrown 0.14.999",
|
||||
"hashbrown 0.15.2",
|
||||
"libc",
|
||||
"libloading",
|
||||
"log",
|
||||
|
@ -7509,7 +7510,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "wgpu-types"
|
||||
version = "24.0.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=c7c79a0dc9356081a884b5518d1c08ce7a09c7c5#c7c79a0dc9356081a884b5518d1c08ce7a09c7c5"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109#a0dbe5ebc6fa24422fb84b2e0fea1cc94dee5109"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"bytemuck",
|
||||
|
@ -7723,7 +7724,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wr_malloc_size_of"
|
||||
version = "0.0.2"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"app_units",
|
||||
"euclid",
|
||||
|
|
|
@ -554,6 +554,7 @@ void DocManager::ClearDocCache() {
|
|||
}
|
||||
|
||||
void DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) {
|
||||
MOZ_ASSERT(aDoc->IsTopLevel());
|
||||
if (!sRemoteDocuments) {
|
||||
sRemoteDocuments = new nsTArray<DocAccessibleParent*>;
|
||||
ClearOnShutdown(&sRemoteDocuments);
|
||||
|
@ -563,6 +564,12 @@ void DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) {
|
|||
"How did we already have the doc!");
|
||||
sRemoteDocuments->AppendElement(aDoc);
|
||||
ProxyCreated(aDoc);
|
||||
// Fire a reorder event on the OuterDocAccessible.
|
||||
if (LocalAccessible* outerDoc = aDoc->OuterDocOfRemoteBrowser()) {
|
||||
MOZ_ASSERT(outerDoc->Document());
|
||||
RefPtr<AccReorderEvent> reorder = new AccReorderEvent(outerDoc);
|
||||
outerDoc->Document()->FireDelayedEvent(reorder);
|
||||
}
|
||||
}
|
||||
|
||||
DocAccessible* mozilla::a11y::GetExistingDocAccessible(
|
||||
|
|
|
@ -15,7 +15,6 @@ XULMAP_TYPE(menu, XULMenuitemAccessible)
|
|||
XULMAP_TYPE(menubar, XULMenubarAccessible)
|
||||
XULMAP_TYPE(menucaption, XULMenuitemAccessible)
|
||||
XULMAP_TYPE(menuitem, XULMenuitemAccessible)
|
||||
XULMAP_TYPE(menulist, XULComboboxAccessible)
|
||||
XULMAP_TYPE(menuseparator, XULMenuSeparatorAccessible)
|
||||
XULMAP_TYPE(notification, XULAlertAccessible)
|
||||
XULMAP_TYPE(radio, XULRadioButtonAccessible)
|
||||
|
@ -67,6 +66,19 @@ XULMAP(image,
|
|||
return new ImageAccessible(aElement, aContext->Document());
|
||||
})
|
||||
|
||||
XULMAP(menulist,
|
||||
[](Element* aElement, LocalAccessible* aContext) -> LocalAccessible* {
|
||||
nsAutoString domID;
|
||||
if (nsCoreUtils::GetID(aElement, domID)) {
|
||||
if (domID.Equals(u"ContentSelectDropdown"_ns)) {
|
||||
return new XULContentSelectDropdownAccessible(
|
||||
aElement, aContext->Document());
|
||||
}
|
||||
}
|
||||
|
||||
return new XULComboboxAccessible(aElement, aContext->Document());
|
||||
})
|
||||
|
||||
XULMAP(menupopup, [](Element* aElement, LocalAccessible* aContext) {
|
||||
return CreateMenupopupAccessible(aElement, aContext);
|
||||
})
|
||||
|
|
|
@ -1806,11 +1806,8 @@ void DocAccessible::ProcessLoad() {
|
|||
#endif
|
||||
|
||||
// Do not fire document complete/stop events for root chrome document
|
||||
// accessibles and for frame/iframe documents because
|
||||
// a) screen readers start working on focus event in the case of root chrome
|
||||
// documents
|
||||
// b) document load event on sub documents causes screen readers to act is if
|
||||
// entire page is reloaded.
|
||||
// accessibles because screen readers start working on focus event in the case
|
||||
// of root chrome documents.
|
||||
if (!IsLoadEventTarget()) return;
|
||||
|
||||
// Fire complete/load stopped if the load event type is given.
|
||||
|
@ -2984,31 +2981,7 @@ void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) {
|
|||
}
|
||||
|
||||
bool DocAccessible::IsLoadEventTarget() const {
|
||||
nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
|
||||
if (!treeItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
|
||||
treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
|
||||
|
||||
// Not a root document.
|
||||
if (parentTreeItem) {
|
||||
// Return true if it's either:
|
||||
// a) tab document;
|
||||
nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
|
||||
treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
|
||||
if (parentTreeItem == rootTreeItem) return true;
|
||||
|
||||
// b) frame/iframe document and its parent document is not in loading state
|
||||
// Note: we can get notifications while document is loading (and thus
|
||||
// while there's no parent document yet).
|
||||
DocAccessible* parentDoc = ParentDocument();
|
||||
return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
|
||||
}
|
||||
|
||||
// It's content (not chrome) root document.
|
||||
return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
|
||||
return mDocumentNode->GetBrowsingContext()->IsContent();
|
||||
}
|
||||
|
||||
void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) {
|
||||
|
|
|
@ -624,10 +624,8 @@ class DocAccessible : public HyperTextAccessible,
|
|||
* Return true if the document is a target of document loading events
|
||||
* (for example, state busy change or document reload events).
|
||||
*
|
||||
* Rules: The root chrome document accessible is never an event target
|
||||
* (for example, Firefox UI window). If the sub document is loaded within its
|
||||
* parent document then the parent document is a target only (aka events
|
||||
* coalescence).
|
||||
* Rule: The root chrome document accessible is never an event target
|
||||
* (for example, Firefox UI window).
|
||||
*/
|
||||
bool IsLoadEventTarget() const;
|
||||
|
||||
|
|
|
@ -126,36 +126,18 @@ export const CommonUtils = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Extract DOMNode id from an accessible. If the accessible is in the remote
|
||||
* process, DOMNode is not present in parent process. However, if specified by
|
||||
* the author, DOMNode id will be attached to an accessible object.
|
||||
*
|
||||
* Obtain DOMNode id from an accessible. This simply queries the .id property
|
||||
* on the accessible, but it catches exceptions which might occur if the
|
||||
* accessible has died.
|
||||
* @param {nsIAccessible} accessible accessible
|
||||
* @return {String?} DOMNode id if available
|
||||
*/
|
||||
getAccessibleDOMNodeID(accessible) {
|
||||
if (accessible instanceof Ci.nsIAccessibleDocument) {
|
||||
// If accessible is a document, trying to find its document body id.
|
||||
try {
|
||||
return accessible.DOMNode.body.id;
|
||||
} catch (e) {
|
||||
/* This only works if accessible is not a proxy. */
|
||||
}
|
||||
}
|
||||
try {
|
||||
return accessible.DOMNode.id;
|
||||
} catch (e) {
|
||||
/* This will fail if DOMNode is in different process. */
|
||||
}
|
||||
try {
|
||||
// When e10s is enabled, accessible will have an "id" property if its
|
||||
// corresponding DOMNode has an id. If accessible is a document, its "id"
|
||||
// property corresponds to the "id" of its body element.
|
||||
return accessible.id;
|
||||
} catch (e) {
|
||||
/* This will fail if accessible is not a proxy. */
|
||||
// This will fail if the accessible has died.
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ support-files = [
|
|||
["browser_test_caret_move_granularity.js"]
|
||||
|
||||
["browser_test_docload.js"]
|
||||
skip-if = ["true"]
|
||||
|
||||
["browser_test_focus_browserui.js"]
|
||||
|
||||
|
|
|
@ -31,17 +31,13 @@ function urlChecker(url) {
|
|||
}
|
||||
|
||||
async function runTests(browser) {
|
||||
let onLoadEvents = waitForEvents({
|
||||
expected: [
|
||||
[EVENT_REORDER, getAccessible(browser)],
|
||||
[EVENT_DOCUMENT_LOAD_COMPLETE, "body2"],
|
||||
[EVENT_STATE_CHANGE, busyChecker(false)],
|
||||
],
|
||||
unexpected: [
|
||||
[EVENT_DOCUMENT_LOAD_COMPLETE, inIframeChecker("iframe1")],
|
||||
[EVENT_STATE_CHANGE, inIframeChecker("iframe1")],
|
||||
],
|
||||
});
|
||||
let onLoadEvents = waitForEvents([
|
||||
[EVENT_REORDER, getAccessible(browser)],
|
||||
[EVENT_DOCUMENT_LOAD_COMPLETE, "body2"],
|
||||
[EVENT_STATE_CHANGE, busyChecker(false)],
|
||||
[EVENT_DOCUMENT_LOAD_COMPLETE, inIframeChecker("iframe1")],
|
||||
[EVENT_STATE_CHANGE, inIframeChecker("iframe1")],
|
||||
]);
|
||||
|
||||
BrowserTestUtils.startLoadingURIString(
|
||||
browser,
|
||||
|
@ -123,6 +119,6 @@ async function runTests(browser) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Test caching of accessible object states
|
||||
* Test events when a document loads.
|
||||
*/
|
||||
addAccessibleTask("", runTests);
|
||||
addAccessibleTask("", runTests, { chrome: true, topLevel: true });
|
||||
|
|
|
@ -595,6 +595,9 @@ function accessibleTask(doc, task, options = {}) {
|
|||
} else {
|
||||
({ accessible: docAccessible } = await onContentDocLoad);
|
||||
}
|
||||
// The test may want to access document methods/attributes such as URL
|
||||
// and browsingContext.
|
||||
docAccessible.QueryInterface(nsIAccessibleDocument);
|
||||
let iframeDocAccessible;
|
||||
if (gIsIframe) {
|
||||
if (!options.skipFissionDocLoad) {
|
||||
|
@ -603,6 +606,7 @@ function accessibleTask(doc, task, options = {}) {
|
|||
? (await onIframeDocLoad).accessible
|
||||
: findAccessibleChildByID(docAccessible, DEFAULT_IFRAME_ID)
|
||||
.firstChild;
|
||||
iframeDocAccessible.QueryInterface(nsIAccessibleDocument);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,8 @@ skip-if = [
|
|||
|
||||
["browser_searchbar.js"]
|
||||
|
||||
["browser_select.js"]
|
||||
|
||||
["browser_shadowdom.js"]
|
||||
|
||||
["browser_test_nsIAccessibleDocument_URL.js"]
|
||||
|
|
168
accessible/tests/browser/tree/browser_select.js
Normal file
168
accessible/tests/browser/tree/browser_select.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/* import-globals-from ../../mochitest/states.js */
|
||||
loadScripts({ name: "states.js", dir: MOCHITESTS_DIR });
|
||||
|
||||
/* import-globals-from ../../mochitest/role.js */
|
||||
loadScripts({ name: "role.js", dir: MOCHITESTS_DIR });
|
||||
|
||||
/**
|
||||
* Verify that the local accessible created in the parent process for the
|
||||
* id=ContentSelectDropdown node has its parent spoofed to the relevant
|
||||
* expanded select.
|
||||
* Note: id=select2 is unused and serves only to ensure we don't return
|
||||
* _just any_ select inside the document :)
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<select id="select">
|
||||
<option id="a">optiona</option>
|
||||
</select>
|
||||
<select id="select2">
|
||||
<option id="b">optionb</option>
|
||||
</select>
|
||||
`,
|
||||
async function testSelectAncestorChain(browser, accDoc) {
|
||||
const LOCAL_DROPDOWN_ID = "ContentSelectDropdown";
|
||||
|
||||
const rootAcc = getRootAccessible(document);
|
||||
ok(rootAcc, "Root Accessible exists");
|
||||
|
||||
const optA = findAccessibleChildByID(accDoc, "a");
|
||||
const select = findAccessibleChildByID(accDoc, "select");
|
||||
let remoteAccDropdown = select.firstChild;
|
||||
ok(remoteAccDropdown, "Remote acc dropdown exists");
|
||||
let isRemote = true;
|
||||
try {
|
||||
remoteAccDropdown.id;
|
||||
} catch (e) {
|
||||
isRemote = false;
|
||||
}
|
||||
|
||||
// Verify the remote dropdown:
|
||||
// - is the role we expect
|
||||
// - is not identical to the local dropdown via ID
|
||||
// - is the parent of the remote accessible for the
|
||||
// option the select contains
|
||||
if (isRemote) {
|
||||
is(
|
||||
remoteAccDropdown.role,
|
||||
ROLE_COMBOBOX_LIST,
|
||||
"Select's first child is the combobox list"
|
||||
);
|
||||
isnot(
|
||||
remoteAccDropdown.id,
|
||||
LOCAL_DROPDOWN_ID,
|
||||
"Remote dropdown does not match local dropdown's ID."
|
||||
);
|
||||
is(
|
||||
remoteAccDropdown.firstChild,
|
||||
optA,
|
||||
"Remote dropdown contains remote acc of option A."
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to fetch the local dropdown
|
||||
let localAccDropdown = findAccessibleChildByID(rootAcc, LOCAL_DROPDOWN_ID);
|
||||
is(
|
||||
localAccDropdown,
|
||||
null,
|
||||
"Local dropdown cannot be reached while select is collapsed"
|
||||
);
|
||||
|
||||
// Focus the combo box.
|
||||
await invokeFocus(browser, "select");
|
||||
|
||||
// Expand the combobox dropdown.
|
||||
let p = waitForEvent(EVENT_STATE_CHANGE, LOCAL_DROPDOWN_ID);
|
||||
EventUtils.synthesizeKey("VK_SPACE");
|
||||
await p;
|
||||
|
||||
// Attempt to fetch the local dropdown
|
||||
localAccDropdown = findAccessibleChildByID(rootAcc, LOCAL_DROPDOWN_ID);
|
||||
ok(localAccDropdown, "Local dropdown exists when select is expanded.");
|
||||
|
||||
// Verify the dropdown rendered in the parent
|
||||
// process is a child of the select
|
||||
is(localAccDropdown.parent, select, "Dropdown is a child of the select");
|
||||
|
||||
// Verify walking from the select produces the
|
||||
// appropriate option
|
||||
remoteAccDropdown = select.firstChild;
|
||||
|
||||
// Verify the remote dropdown:
|
||||
// - is the role we expect
|
||||
// - is not identical to the local dropdown via ID
|
||||
// - is the parent of the remote accessible for the
|
||||
// option the select contains
|
||||
if (isRemote) {
|
||||
is(
|
||||
remoteAccDropdown.role,
|
||||
ROLE_COMBOBOX_LIST,
|
||||
"Select's first child is the combobox list"
|
||||
);
|
||||
isnot(
|
||||
remoteAccDropdown.id,
|
||||
LOCAL_DROPDOWN_ID,
|
||||
"Remote dropdown does not match local dropdown's ID."
|
||||
);
|
||||
is(
|
||||
remoteAccDropdown.firstChild,
|
||||
optA,
|
||||
"Remote dropdown contains remote acc of option A."
|
||||
);
|
||||
}
|
||||
|
||||
// Close the dropdown.
|
||||
p = waitForEvents({
|
||||
expected: [[EVENT_HIDE, LOCAL_DROPDOWN_ID]],
|
||||
});
|
||||
|
||||
EventUtils.synthesizeKey("VK_ESCAPE");
|
||||
await p;
|
||||
|
||||
// Verify walking down from the select produces the
|
||||
// appropriate option
|
||||
remoteAccDropdown = select.firstChild;
|
||||
|
||||
// Verify the remote dropdown:
|
||||
// - is the role we expect
|
||||
// - is not identical to the local dropdown via ID
|
||||
// - is the parent of the remote accessible for the
|
||||
// option the select contains
|
||||
if (isRemote) {
|
||||
is(
|
||||
remoteAccDropdown.role,
|
||||
ROLE_COMBOBOX_LIST,
|
||||
"Select's first child is the combobox list"
|
||||
);
|
||||
isnot(
|
||||
remoteAccDropdown.id,
|
||||
LOCAL_DROPDOWN_ID,
|
||||
"Remote dropdown does not match local dropdown's ID."
|
||||
);
|
||||
is(
|
||||
remoteAccDropdown.firstChild,
|
||||
optA,
|
||||
"Remote dropdown contains remote acc of option A."
|
||||
);
|
||||
}
|
||||
|
||||
// Attempt to fetch the local dropdown
|
||||
localAccDropdown = findAccessibleChildByID(rootAcc, LOCAL_DROPDOWN_ID);
|
||||
is(
|
||||
localAccDropdown,
|
||||
null,
|
||||
"Local dropdown cannot be reached while select is collapsed"
|
||||
);
|
||||
},
|
||||
{
|
||||
chrome: true,
|
||||
topLevel: true,
|
||||
iframe: true,
|
||||
remoteIframe: true,
|
||||
}
|
||||
);
|
|
@ -868,33 +868,17 @@ function getTextFromClipboard() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Extract DOMNode id from an accessible. If e10s is enabled, DOMNode is not
|
||||
* present in parent process but, if available, DOMNode id is attached to an
|
||||
* accessible object.
|
||||
* Obtain DOMNode id from an accessible. This simply queries the .id property
|
||||
* on the accessible, but it catches exceptions which might occur if the
|
||||
* accessible has died.
|
||||
* @param {nsIAccessible} accessible accessible
|
||||
* @return {String?} DOMNode id if available
|
||||
*/
|
||||
function getAccessibleDOMNodeID(accessible) {
|
||||
if (accessible instanceof nsIAccessibleDocument) {
|
||||
// If accessible is a document, trying to find its document body id.
|
||||
try {
|
||||
return accessible.DOMNode.body.id;
|
||||
} catch (e) {
|
||||
/* This only works if accessible is not a proxy. */
|
||||
}
|
||||
}
|
||||
try {
|
||||
return accessible.DOMNode.id;
|
||||
} catch (e) {
|
||||
/* This will fail if DOMNode is in different process. */
|
||||
}
|
||||
try {
|
||||
// When e10s is enabled, accessible will have an "id" property if its
|
||||
// corresponding DOMNode has an id. If accessible is a document, its "id"
|
||||
// property corresponds to the "id" of its body element.
|
||||
return accessible.id;
|
||||
} catch (e) {
|
||||
/* This will fail if accessible is not a proxy. */
|
||||
// This will fail if the accessible has died.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -173,13 +173,8 @@ xpcAccessible::GetId(nsAString& aID) {
|
|||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
RemoteAccessible* proxy = IntlGeneric()->AsRemote();
|
||||
if (!proxy) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
nsString id;
|
||||
proxy->DOMNodeID(id);
|
||||
IntlGeneric()->DOMNodeID(id);
|
||||
aID.Assign(id);
|
||||
|
||||
return NS_OK;
|
||||
|
|
|
@ -9,6 +9,9 @@
|
|||
#include "nsAccessibilityService.h"
|
||||
#include "DocAccessible.h"
|
||||
#include "nsCoreUtils.h"
|
||||
#include "nsFocusManager.h"
|
||||
|
||||
#include "mozilla/a11y/DocAccessibleParent.h"
|
||||
#include "mozilla/a11y/Role.h"
|
||||
#include "States.h"
|
||||
|
||||
|
@ -140,3 +143,46 @@ bool XULComboboxAccessible::AreItemsOperable() const {
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// XULContentSelectDropdownAccessible
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Accessible* XULContentSelectDropdownAccessible::Parent() const {
|
||||
// We render the expanded dropdown for <select>s in the parent process
|
||||
// as a child of the application accessible. This confuses some
|
||||
// ATs which expect the select to _always_ parent the dropdown (in
|
||||
// both expanded and collapsed states).
|
||||
// To rectify this, we spoof the <select> as the parent of the
|
||||
// expanded dropdown here. Note that we do not spoof the child relationship.
|
||||
|
||||
// First, try to find the select that spawned this dropdown.
|
||||
// The select that was activated does not get states::EXPANDED, but
|
||||
// it should still have focus.
|
||||
Accessible* focusedAcc = nullptr;
|
||||
if (auto* focusedNode = FocusMgr()->FocusedDOMNode()) {
|
||||
// If we get a node here, we're in a non-remote browser.
|
||||
DocAccessible* doc =
|
||||
GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
|
||||
focusedAcc = doc->GetAccessible(focusedNode);
|
||||
} else {
|
||||
nsFocusManager* focusManagerDOM = nsFocusManager::GetFocusManager();
|
||||
dom::BrowsingContext* focusedContext =
|
||||
focusManagerDOM->GetFocusedBrowsingContextInChrome();
|
||||
|
||||
DocAccessibleParent* focusedDoc =
|
||||
DocAccessibleParent::GetFrom(focusedContext);
|
||||
MOZ_ASSERT(focusedDoc && focusedDoc->IsDoc(), "No focused document found");
|
||||
focusedAcc = focusedDoc->AsDoc()->GetFocusedAcc();
|
||||
}
|
||||
|
||||
if (!NS_WARN_IF(focusedAcc && focusedAcc->IsHTMLCombobox())) {
|
||||
// We can sometimes get a document here if the select that
|
||||
// this dropdown should anchor to loses focus. This can happen when
|
||||
// calling AXPressed on macOS. Call into the regular parent
|
||||
// function instead.
|
||||
return LocalParent();
|
||||
}
|
||||
|
||||
return focusedAcc;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,21 @@ class XULComboboxAccessible : public AccessibleWrap {
|
|||
MOZ_CAN_RUN_SCRIPT_BOUNDARY bool AreItemsOperable() const override;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for the singular, global instance of a XULCombobox which is rendered
|
||||
* in the parent process and contains the options of the focused and expanded
|
||||
* HTML select in a content document. This combobox should have
|
||||
* id=ContentSelectDropdown
|
||||
*/
|
||||
class XULContentSelectDropdownAccessible : public XULComboboxAccessible {
|
||||
public:
|
||||
XULContentSelectDropdownAccessible(nsIContent* aContent, DocAccessible* aDoc)
|
||||
: XULComboboxAccessible(aContent, aDoc) {}
|
||||
// Accessible
|
||||
|
||||
virtual Accessible* Parent() const override;
|
||||
};
|
||||
|
||||
} // namespace a11y
|
||||
} // namespace mozilla
|
||||
|
||||
|
|
|
@ -212,7 +212,7 @@ pref("app.update.langpack.enabled", true);
|
|||
// This feature is also affected by
|
||||
// `app.update.multiSessionInstallLockout.timeoutMs`, which is in the branding
|
||||
// section.
|
||||
pref("app.update.multiSessionInstallLockout.enabled", true);
|
||||
pref("app.update.multiSessionInstallLockout.enabled", false);
|
||||
|
||||
#if defined(MOZ_BACKGROUNDTASKS)
|
||||
// The amount of time, in seconds, before background tasks time out and exit.
|
||||
|
@ -1811,8 +1811,9 @@ pref("browser.partnerlink.campaign.topsites", "amzn_2020_a1");
|
|||
pref("browser.newtab.preload", true);
|
||||
|
||||
// If an on-train limited rollout of the preonboarding modal is enabled, the
|
||||
// percentage of the Mac, Linux, and MSIX population to enroll
|
||||
pref("browser.preonboarding.onTrainRolloutPopulation", 0);
|
||||
// percentage of the Mac, Linux, and MSIX population to enroll. Default to 25% of
|
||||
// population (2500 / 10000).
|
||||
pref("browser.preonboarding.onTrainRolloutPopulation", 2500);
|
||||
|
||||
// Mozilla Ad Routing Service (MARS) unified ads service
|
||||
pref("browser.newtabpage.activity-stream.unifiedAds.tiles.enabled", true);
|
||||
|
@ -3055,9 +3056,6 @@ pref("devtools.webconsole.filter.netxhr", false);
|
|||
// Webconsole autocomplete preference
|
||||
pref("devtools.webconsole.input.autocomplete",true);
|
||||
|
||||
// Show context selector in console input
|
||||
pref("devtools.webconsole.input.context", true);
|
||||
|
||||
// Set to true to eagerly show the results of webconsole terminal evaluations
|
||||
// when they don't have side effects.
|
||||
pref("devtools.webconsole.input.eagerEvaluation", true);
|
||||
|
|
|
@ -146,9 +146,18 @@ customElements.define(
|
|||
}
|
||||
|
||||
get hasNoPermissions() {
|
||||
const { strings, showIncognitoCheckbox } =
|
||||
this.notification.options.customElementOptions;
|
||||
return !(showIncognitoCheckbox || strings.msgs.length);
|
||||
const {
|
||||
strings,
|
||||
showIncognitoCheckbox,
|
||||
showTechnicalAndInteractionCheckbox,
|
||||
} = this.notification.options.customElementOptions;
|
||||
|
||||
return !(
|
||||
strings.msgs.length ||
|
||||
this.#dataCollectionPermissions?.msg ||
|
||||
showIncognitoCheckbox ||
|
||||
showTechnicalAndInteractionCheckbox
|
||||
);
|
||||
}
|
||||
|
||||
get domainsSet() {
|
||||
|
@ -171,9 +180,25 @@ customElements.define(
|
|||
return strings.fullDomainsList.msgIdIndex === idx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{idx: number, collectsTechnicalAndInteractionData: boolean}}
|
||||
* An object with information about data collection permissions for the UI.
|
||||
*/
|
||||
get #dataCollectionPermissions() {
|
||||
if (!this.notification?.options?.customElementOptions) {
|
||||
return undefined;
|
||||
}
|
||||
const { strings } = this.notification.options.customElementOptions;
|
||||
return strings.dataCollectionPermissions;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { strings, showIncognitoCheckbox, isUserScriptsRequest } =
|
||||
this.notification.options.customElementOptions;
|
||||
const {
|
||||
strings,
|
||||
showIncognitoCheckbox,
|
||||
showTechnicalAndInteractionCheckbox,
|
||||
isUserScriptsRequest,
|
||||
} = this.notification.options.customElementOptions;
|
||||
|
||||
const { textEl, introEl, permsListEl } = this;
|
||||
|
||||
|
@ -240,6 +265,28 @@ customElements.define(
|
|||
permsListEl.appendChild(item);
|
||||
}
|
||||
|
||||
if (this.#dataCollectionPermissions?.msg) {
|
||||
let item = doc.createElementNS(HTML_NS, "li");
|
||||
item.classList.add(
|
||||
"webext-perm-granted",
|
||||
"webext-data-collection-perm-granted"
|
||||
);
|
||||
item.textContent = this.#dataCollectionPermissions.msg;
|
||||
permsListEl.appendChild(item);
|
||||
}
|
||||
|
||||
// Add a checkbox for the "technicalAndInteraction" optional data
|
||||
// collection permission.
|
||||
if (showTechnicalAndInteractionCheckbox) {
|
||||
let item = doc.createElementNS(HTML_NS, "li");
|
||||
item.classList.add(
|
||||
"webext-perm-optional",
|
||||
"webext-data-collection-perm-optional"
|
||||
);
|
||||
item.appendChild(this.#createTechnicalAndInteractionDataCheckbox());
|
||||
permsListEl.appendChild(item);
|
||||
}
|
||||
|
||||
if (showIncognitoCheckbox) {
|
||||
let item = doc.createElementNS(HTML_NS, "li");
|
||||
item.classList.add(
|
||||
|
@ -367,6 +414,28 @@ customElements.define(
|
|||
);
|
||||
return checkboxEl;
|
||||
}
|
||||
|
||||
#createTechnicalAndInteractionDataCheckbox() {
|
||||
const { grantTechnicalAndInteractionDataCollection } =
|
||||
this.notification.options.customElementOptions;
|
||||
|
||||
const checkboxEl = this.ownerDocument.createXULElement("checkbox");
|
||||
checkboxEl.label = lazy.PERMISSION_L10N.formatValueSync(
|
||||
"webext-perms-description-data-long-technicalAndInteraction"
|
||||
);
|
||||
checkboxEl.checked = grantTechnicalAndInteractionDataCollection;
|
||||
checkboxEl.addEventListener("CheckboxStateChange", () => {
|
||||
// NOTE: the popupnotification instances will be reused
|
||||
// and so the callback function is destructured here to
|
||||
// avoid this custom element to prevent it from being
|
||||
// garbage collected.
|
||||
const { onTechnicalAndInteractionDataChanged } =
|
||||
this.notification.options.customElementOptions;
|
||||
onTechnicalAndInteractionDataChanged?.(checkboxEl.checked);
|
||||
});
|
||||
|
||||
return checkboxEl;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -1185,8 +1185,12 @@ var gIdentityHandler = {
|
|||
},
|
||||
|
||||
setURI(uri) {
|
||||
if (uri instanceof Ci.nsINestedURI) {
|
||||
uri = uri.QueryInterface(Ci.nsINestedURI).innermostURI;
|
||||
// Unnest the URI, turning "view-source:https://example.com" into
|
||||
// "https://example.com" for example. "about:" URIs are a special exception
|
||||
// here, as some of them have a hidden moz-safe-about inner URI we do not
|
||||
// want to unnest.
|
||||
while (uri instanceof Ci.nsINestedURI && !uri.schemeIs("about")) {
|
||||
uri = uri.QueryInterface(Ci.nsINestedURI).innerURI;
|
||||
}
|
||||
this._uri = uri;
|
||||
|
||||
|
@ -1197,7 +1201,7 @@ var gIdentityHandler = {
|
|||
this._uriHasHost = false;
|
||||
}
|
||||
|
||||
if (uri.schemeIs("about") || uri.schemeIs("moz-safe-about")) {
|
||||
if (uri.schemeIs("about")) {
|
||||
let module = E10SUtils.getAboutModule(uri);
|
||||
if (module) {
|
||||
let flags = module.getURIFlags(uri);
|
||||
|
|
|
@ -4541,7 +4541,10 @@ function undoCloseTab(aIndex, sourceWindowSSId) {
|
|||
if (SessionStore.getSavedTabGroup(lastClosedTabGroupId)) {
|
||||
group = SessionStore.openSavedTabGroup(
|
||||
lastClosedTabGroupId,
|
||||
targetWindow
|
||||
targetWindow,
|
||||
{
|
||||
source: "recent",
|
||||
}
|
||||
);
|
||||
} else {
|
||||
group = SessionStore.undoCloseTabGroup(
|
||||
|
@ -4917,17 +4920,26 @@ var RestoreLastSessionObserver = {
|
|||
!PrivateBrowsingUtils.isWindowPrivate(window)
|
||||
) {
|
||||
Services.obs.addObserver(this, "sessionstore-last-session-cleared", true);
|
||||
Services.obs.addObserver(
|
||||
this,
|
||||
"sessionstore-last-session-re-enable",
|
||||
true
|
||||
);
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", true);
|
||||
} else if (SessionStore.willAutoRestore) {
|
||||
document.getElementById("Browser:RestoreLastSession").hidden = true;
|
||||
}
|
||||
},
|
||||
|
||||
observe() {
|
||||
// The last session can only be restored once so there's
|
||||
// no way we need to re-enable our menu item.
|
||||
Services.obs.removeObserver(this, "sessionstore-last-session-cleared");
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", false);
|
||||
observe(aSubject, aTopic) {
|
||||
switch (aTopic) {
|
||||
case "sessionstore-last-session-cleared":
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", false);
|
||||
break;
|
||||
case "sessionstore-last-session-re-enable":
|
||||
goSetCommandEnabled("Browser:RestoreLastSession", true);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
|
||||
<!-- These localization links are not automatically applied to any XHR
|
||||
response body and must be applied manually as well. They are included
|
||||
so that viewing the file directly shows the results. -->
|
||||
|
|
|
@ -319,12 +319,20 @@
|
|||
</menupopup>
|
||||
|
||||
<menupopup id="sidebar-history-menu">
|
||||
<menuitem data-l10n-id="sidebar-history-sort-by-date"
|
||||
<html:h1 data-l10n-id="sidebar-history-sort-by-heading"
|
||||
id="sidebar-history-sort-by-heading"/>
|
||||
<menuitem data-l10n-id="sidebar-history-sort-option-date"
|
||||
id="sidebar-history-sort-by-date"
|
||||
type="checkbox"/>
|
||||
<menuitem data-l10n-id="sidebar-history-sort-by-site"
|
||||
<menuitem data-l10n-id="sidebar-history-sort-option-site"
|
||||
id="sidebar-history-sort-by-site"
|
||||
type="checkbox"/>
|
||||
<menuitem data-l10n-id="sidebar-history-sort-option-date-and-site"
|
||||
id="sidebar-history-sort-by-date-and-site"
|
||||
type="checkbox"/>
|
||||
<menuitem data-l10n-id="sidebar-history-sort-option-last-visited"
|
||||
id="sidebar-history-sort-by-last-visited"
|
||||
type="checkbox"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-l10n-id="sidebar-history-clear"
|
||||
id="sidebar-history-clear"/>
|
||||
|
|
|
@ -10,8 +10,7 @@ document.addEventListener(
|
|||
() => {
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
TabGroupMetrics:
|
||||
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
|
||||
TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
|
||||
});
|
||||
let mainPopupSet = document.getElementById("mainPopupSet");
|
||||
// eslint-disable-next-line complexity
|
||||
|
@ -135,11 +134,12 @@ document.addEventListener(
|
|||
let tabGroup = gBrowser.getTabGroupById(tabGroupId);
|
||||
// Tabs need to be removed by their owning `Tabbrowser` or else
|
||||
// there are errors.
|
||||
tabGroup.ownerGlobal.gBrowser.removeTabGroup(tabGroup, {
|
||||
isUserTriggered: true,
|
||||
telemetrySource:
|
||||
lazy.TabGroupMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
|
||||
});
|
||||
tabGroup.ownerGlobal.gBrowser.removeTabGroup(
|
||||
tabGroup,
|
||||
lazy.TabMetrics.userTriggeredContext(
|
||||
lazy.TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -147,14 +147,18 @@ document.addEventListener(
|
|||
case "saved-tab-group-context-menu_openInThisWindow":
|
||||
{
|
||||
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
|
||||
SessionStore.openSavedTabGroup(tabGroupId, window);
|
||||
SessionStore.openSavedTabGroup(tabGroupId, window, {
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "saved-tab-group-context-menu_openInNewWindow":
|
||||
{
|
||||
// TODO Bug 1940112: "Open Group in New Window" should directly restore saved tab groups into a new window
|
||||
let { tabGroupId } = event.target.parentElement.triggerNode.dataset;
|
||||
let tabGroup = SessionStore.openSavedTabGroup(tabGroupId, window);
|
||||
let tabGroup = SessionStore.openSavedTabGroup(tabGroupId, window, {
|
||||
source: lazy.TabMetrics.METRIC_SOURCE.RECENT_TABS,
|
||||
});
|
||||
gBrowser.replaceGroupWithWindow(tabGroup);
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -151,13 +151,14 @@ security.ui.protectionspopup:
|
|||
type: boolean
|
||||
bugs:
|
||||
- https://bugzil.la/1920735
|
||||
- https://bugzil.la/1958162
|
||||
data_reviews:
|
||||
- https://bugzil.la/1920735
|
||||
notification_emails:
|
||||
- wwen@mozilla.com
|
||||
- emz@mozilla.com
|
||||
expires:
|
||||
140
|
||||
146
|
||||
|
||||
open_protectionspopup_cfr:
|
||||
type: event
|
||||
|
@ -389,13 +390,14 @@ security.ui.protectionspopup:
|
|||
type: string
|
||||
bugs:
|
||||
- https://bugzil.la/1920735
|
||||
- https://bugzil.la/1958162
|
||||
data_reviews:
|
||||
- https://bugzil.la/1920735
|
||||
notification_emails:
|
||||
- wwen@mozilla.com
|
||||
- emz@mozilla.com
|
||||
expires:
|
||||
140
|
||||
146
|
||||
|
||||
smartblockembeds_shown:
|
||||
type: counter
|
||||
|
@ -403,13 +405,14 @@ security.ui.protectionspopup:
|
|||
How many times the SmartBlock placeholders are shown on the page
|
||||
bugs:
|
||||
- https://bugzil.la/1920735
|
||||
- https://bugzil.la/1958162
|
||||
data_reviews:
|
||||
- https://bugzil.la/1920735
|
||||
notification_emails:
|
||||
- wwen@mozilla.com
|
||||
- emz@mozilla.com
|
||||
expires:
|
||||
140
|
||||
146
|
||||
|
||||
browser.engagement:
|
||||
bookmarks_toolbar_bookmark_added:
|
||||
|
|
|
@ -164,7 +164,6 @@
|
|||
<toolbartabstop/>
|
||||
<html:div id="urlbar"
|
||||
popover="manual"
|
||||
context=""
|
||||
focused="true"
|
||||
pageproxystate="invalid"
|
||||
unifiedsearchbutton-available=""
|
||||
|
|
|
@ -60,6 +60,14 @@ const knownUnshownImages = [
|
|||
file: "chrome://global/skin/icons/highlights.svg",
|
||||
platforms: ["win", "linux", "macosx"],
|
||||
intermittentShown: ["win", "linux"],
|
||||
// this file is not loaded in beta since the pref is only
|
||||
// turned on in nightly
|
||||
intermittentNotLoaded: Services.prefs.getBoolPref(
|
||||
"browser.tabs.groups.smart.enabled",
|
||||
true
|
||||
)
|
||||
? []
|
||||
: ["win", "linux", "macosx"],
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -79,6 +79,12 @@ var tests = [
|
|||
hostForDisplay: "chrome://global/content/mozilla.html",
|
||||
hasSubview: false,
|
||||
},
|
||||
{
|
||||
name: "about:logo with nested moz-safe-about:logo",
|
||||
location: "about:logo",
|
||||
hostForDisplay: "about:logo",
|
||||
hasSubview: false,
|
||||
},
|
||||
];
|
||||
|
||||
add_task(async function test() {
|
||||
|
|
|
@ -28,6 +28,7 @@ export const AdditionalCTA = ({ content, handleAction }) => {
|
|||
<div className={className}>
|
||||
<Localized text={content.additional_button?.label}>
|
||||
<button
|
||||
id="additional_button"
|
||||
className={`${buttonStyle} additional-cta`}
|
||||
onClick={handleAction}
|
||||
value="additional_button"
|
||||
|
|
|
@ -401,6 +401,7 @@ export const SecondaryCTA = props => {
|
|||
</Localized>
|
||||
<Localized text={props.content[targetElement].label}>
|
||||
<button
|
||||
id="secondary_button"
|
||||
className={buttonStyling}
|
||||
value={targetElement}
|
||||
disabled={isDisabled(props.content.secondary_button?.disabled)}
|
||||
|
|
|
@ -142,12 +142,14 @@ const SubmenuButtonInner = ({ content, handleAction }) => {
|
|||
return (
|
||||
<Localized text={content.submenu_button.label ?? {}}>
|
||||
<button
|
||||
id="submenu_button"
|
||||
className={`submenu-button ${isPrimary ? "primary" : "secondary"}`}
|
||||
value="submenu_button"
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isSubmenuExpanded}
|
||||
aria-labelledby={`${content.submenu_button.attached_to} submenu_button`}
|
||||
/>
|
||||
</Localized>
|
||||
);
|
||||
|
|
|
@ -468,6 +468,7 @@ const SecondaryCTA = props => {
|
|||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null)), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: props.content[targetElement].label
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
|
||||
id: "secondary_button",
|
||||
className: buttonStyling,
|
||||
value: targetElement,
|
||||
disabled: isDisabled(props.content.secondary_button?.disabled),
|
||||
|
@ -1828,6 +1829,7 @@ const AdditionalCTA = ({
|
|||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: content.additional_button?.label
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
|
||||
id: "additional_button",
|
||||
className: `${buttonStyle} additional-cta`,
|
||||
onClick: handleAction,
|
||||
value: "additional_button",
|
||||
|
@ -1991,12 +1993,14 @@ const SubmenuButtonInner = ({
|
|||
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: content.submenu_button.label ?? {}
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
|
||||
id: "submenu_button",
|
||||
className: `submenu-button ${isPrimary ? "primary" : "secondary"}`,
|
||||
value: "submenu_button",
|
||||
onClick: onClick,
|
||||
ref: ref,
|
||||
"aria-haspopup": "menu",
|
||||
"aria-expanded": isSubmenuExpanded
|
||||
"aria-expanded": isSubmenuExpanded,
|
||||
"aria-labelledby": `${content.submenu_button.attached_to} submenu_button`
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
|
@ -564,6 +564,10 @@
|
|||
"minumum": 0,
|
||||
"exclusiveMaximum": 10
|
||||
},
|
||||
"dismissable": {
|
||||
"description": "Should the infobar include an X dismiss button, defaults to true",
|
||||
"type": "boolean"
|
||||
},
|
||||
"buttons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
"minumum": 0,
|
||||
"exclusiveMaximum": 10
|
||||
},
|
||||
"dismissable": {
|
||||
"description": "Should the infobar include an X dismiss button, defaults to true",
|
||||
"type": "boolean"
|
||||
},
|
||||
"buttons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
|
|
@ -1692,6 +1692,7 @@ const MESSAGES = () => {
|
|||
style: "secondary",
|
||||
label: {
|
||||
marginBlock: "0 -8px",
|
||||
string_id: "split-dismiss-button-default-label",
|
||||
},
|
||||
},
|
||||
tiles: {
|
||||
|
@ -1960,6 +1961,7 @@ const MESSAGES = () => {
|
|||
style: "secondary",
|
||||
label: {
|
||||
marginBlock: "0 -8px",
|
||||
string_id: "split-dismiss-button-default-label",
|
||||
},
|
||||
},
|
||||
tiles: {
|
||||
|
|
|
@ -45,7 +45,9 @@ class InfoBarNotification {
|
|||
priority,
|
||||
eventCallback: this.infobarCallback,
|
||||
},
|
||||
content.buttons.map(b => this.formatButtonConfig(b))
|
||||
content.buttons.map(b => this.formatButtonConfig(b)),
|
||||
false,
|
||||
content.dismissable
|
||||
);
|
||||
|
||||
this.addImpression();
|
||||
|
|
|
@ -177,3 +177,78 @@ add_task(async function prevent_multiple_messages() {
|
|||
infobar.notification.closeButton.click();
|
||||
Assert.equal(InfoBar._activeInfobar, null, "Cleared the active notification");
|
||||
});
|
||||
|
||||
add_task(async function default_dismissable_button_shows() {
|
||||
let message = (await CFRMessageProvider.getMessages()).find(
|
||||
m => m.id === "INFOBAR_ACTION_86"
|
||||
);
|
||||
Assert.ok(message, "Found the message");
|
||||
|
||||
// Use the base message which has no dismissable property by default.
|
||||
let dispatchStub = sinon.stub();
|
||||
let infobar = await InfoBar.showInfoBarMessage(
|
||||
BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser,
|
||||
message,
|
||||
dispatchStub
|
||||
);
|
||||
|
||||
Assert.ok(
|
||||
infobar.notification.closeButton,
|
||||
"Default message should display a close button"
|
||||
);
|
||||
|
||||
infobar.notification.closeButton.click();
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => infobar.notification === null,
|
||||
"Wait for default message notification to be dismissed."
|
||||
);
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function non_dismissable_notification_does_not_show_close_button() {
|
||||
let baseMessage = (await CFRMessageProvider.getMessages()).find(
|
||||
m => m.id === "INFOBAR_ACTION_86"
|
||||
);
|
||||
Assert.ok(baseMessage, "Found the base message");
|
||||
|
||||
let message = {
|
||||
...baseMessage,
|
||||
content: {
|
||||
...baseMessage.content,
|
||||
dismissable: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Add a footer button we can close the infobar with
|
||||
message.content.buttons.push({
|
||||
label: "Cancel",
|
||||
action: {
|
||||
type: "CANCEL",
|
||||
},
|
||||
});
|
||||
|
||||
let dispatchStub = sinon.stub();
|
||||
let infobar = await InfoBar.showInfoBarMessage(
|
||||
BrowserWindowTracker.getTopWindow().gBrowser.selectedBrowser,
|
||||
message,
|
||||
dispatchStub
|
||||
);
|
||||
|
||||
Assert.ok(
|
||||
!infobar.notification.closeButton,
|
||||
"Non-dismissable message should not display a close button"
|
||||
);
|
||||
|
||||
let cancelButton = infobar.notification.querySelector(
|
||||
".footer-button:not(.primary)"
|
||||
);
|
||||
|
||||
Assert.ok(cancelButton, "Non-primary footer button exists");
|
||||
|
||||
cancelButton.click();
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => infobar.notification === null,
|
||||
"Wait for default message notification to close."
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -67,18 +67,57 @@ export const ContentAnalysis = {
|
|||
|
||||
isInitialized: false,
|
||||
|
||||
// Maps string UserActionId to { userActionId, requestTokenSet, timer } or
|
||||
// { userActionId, requestTokenSet, notification }
|
||||
/**
|
||||
* @typedef {object} NotificationInfo - information about the busy dialog itself that is showing
|
||||
* @property {*} [close] - Method to close the native notification
|
||||
* @property {BrowsingContext} [dialogBrowsingContext] - browsing context where the
|
||||
* confirm() dialog is shown
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BusyDialogInfo - information about a busy dialog that is either showing or will
|
||||
* will be shown after a delay.
|
||||
* @property {string} userActionId - The userActionId of the request
|
||||
* @property {Set<string>} requestTokenSet - The set of requestTokens associated with the userActionId
|
||||
* @property {*} [timer] - Result of a setTimeout() call that can be used to cancel the showing of the busy
|
||||
* dialog if it has not been displayed yet.
|
||||
* @property {NotificationInfo} [notification] - Information about the busy dialog that is being shown.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<string, BusyDialogInfo>}
|
||||
*
|
||||
* Maps string UserActionId to info about the busy dialog.
|
||||
*/
|
||||
userActionToBusyDialogMap: new Map(),
|
||||
|
||||
/**
|
||||
* @type {Map<string, {browsingContext: BrowsingContext, resourceNameOrOperationType: object}>}
|
||||
* @typedef {object} ResourceNameOrOperationType
|
||||
* @property {string} [name] - the name of the resource
|
||||
* @property {number} [operationType] - the type of operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} RequestInfo
|
||||
* @property {BrowsingContext} browsingContext - browsing context where the request was sent from
|
||||
* @property {ResourceNameOrOperationType} resourceNameOrOperationType - name of the operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Map<string, RequestInfo>}
|
||||
*/
|
||||
requestTokenToRequestInfo: new Map(),
|
||||
|
||||
/**
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
warnDialogRequestTokens: new Set(),
|
||||
|
||||
/**
|
||||
* Registers for various messages/events that will indicate the
|
||||
* need for communicating something to the user.
|
||||
*
|
||||
* @param {Window} window - The window to monitor
|
||||
*/
|
||||
initialize(window) {
|
||||
if (!lazy.gContentAnalysis.isActive) {
|
||||
|
@ -180,6 +219,17 @@ export const ContentAnalysis = {
|
|||
break;
|
||||
}
|
||||
case "quit-application": {
|
||||
// We're quitting, so respond false to all WARN dialogs.
|
||||
let requestTokensToCancel = this.warnDialogRequestTokens;
|
||||
// Clear this first so the handler showing the dialog will know not
|
||||
// to call respondToWarnDialog() again.
|
||||
this.warnDialogRequestTokens = new Set();
|
||||
for (let warnDialogRequestToken of requestTokensToCancel) {
|
||||
lazy.gContentAnalysis.respondToWarnDialog(
|
||||
warnDialogRequestToken,
|
||||
false
|
||||
);
|
||||
}
|
||||
this.uninitialize();
|
||||
break;
|
||||
}
|
||||
|
@ -256,6 +306,12 @@ export const ContentAnalysis = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the panel that indicates that DLP is active.
|
||||
*
|
||||
* @param {Element} element The toolbarbutton the user has clicked on
|
||||
* @param {panelUI} panelUI Maintains state for the main menu panel
|
||||
*/
|
||||
async showPanel(element, panelUI) {
|
||||
element.ownerDocument.l10n.setAttributes(
|
||||
lazy.PanelMultiView.getViewNode(
|
||||
|
@ -268,6 +324,11 @@ export const ContentAnalysis = {
|
|||
panelUI.showSubView("content-analysis-panel", element);
|
||||
},
|
||||
|
||||
/**
|
||||
* Closes a busy dialog
|
||||
*
|
||||
* @param {BusyDialogInfo?} caView - the busy dialog to close
|
||||
*/
|
||||
_disconnectFromView(caView) {
|
||||
if (!caView) {
|
||||
return;
|
||||
|
@ -301,6 +362,16 @@ export const ContentAnalysis = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows either a dialog or native notification or both, depending on the values of
|
||||
* _SHOW_DIALOGS and _SHOW_NOTIFICATIONS.
|
||||
*
|
||||
* @param {string} aMessage - Message to show
|
||||
* @param {BrowsingContext} aBrowsingContext - BrowsingContext to show the dialog in.
|
||||
* @param {number} aTimeout - timeout for closing the native notification. 0 indicates it is
|
||||
* not automatically closed.
|
||||
* @returns {NotificationInfo?} - information about the native notification, if it has been shown.
|
||||
*/
|
||||
_showMessage(aMessage, aBrowsingContext, aTimeout = 0) {
|
||||
if (this._SHOW_DIALOGS) {
|
||||
Services.prompt.asyncAlert(
|
||||
|
@ -331,6 +402,12 @@ export const ContentAnalysis = {
|
|||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the notification should block browser interaction.
|
||||
*
|
||||
* @param {number} aAnalysisType The type of DLP analysis being done.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_shouldShowBlockingNotification(aAnalysisType) {
|
||||
return !(
|
||||
aAnalysisType == Ci.nsIContentAnalysisRequest.eFileDownloaded ||
|
||||
|
@ -338,8 +415,13 @@ export const ContentAnalysis = {
|
|||
);
|
||||
},
|
||||
|
||||
// This function also transforms the nameOrOperationType so we won't have to
|
||||
// look it up again.
|
||||
/**
|
||||
* This function also transforms the nameOrOperationType so we won't have to
|
||||
* look it up again.
|
||||
*
|
||||
* @param {ResourceNameOrOperationType} nameOrOperationType
|
||||
* @returns {string}
|
||||
*/
|
||||
_getResourceNameFromNameOrOperationType(nameOrOperationType) {
|
||||
if (!nameOrOperationType.name) {
|
||||
let l10nId = undefined;
|
||||
|
@ -374,8 +456,7 @@ export const ContentAnalysis = {
|
|||
* line. This is used to add more context to the message
|
||||
* if a file is being uploaded rather than just the name
|
||||
* of the file.
|
||||
* @returns {object} An object with either a name property that can be used as-is, or
|
||||
* an operationType property.
|
||||
* @returns {ResourceNameOrOperationType}
|
||||
*/
|
||||
_getResourceNameOrOperationTypeFromRequest(aRequest, aStandalone) {
|
||||
if (
|
||||
|
@ -395,6 +476,14 @@ export const ContentAnalysis = {
|
|||
return { operationType: aRequest.operationTypeForDisplay };
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up an "operation is in progress" dialog to be shown after a delay,
|
||||
* unless one is already showing for this userActionId.
|
||||
*
|
||||
* @param {nsIContentAnalysisRequest} aRequest
|
||||
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
|
||||
* @param {BrowsingContext} aBrowsingContext
|
||||
*/
|
||||
_queueSlowCAMessage(
|
||||
aRequest,
|
||||
aResourceNameOrOperationType,
|
||||
|
@ -433,6 +522,12 @@ export const ContentAnalysis = {
|
|||
}, slowTimeoutMs);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the Slow CA message, if it is showing
|
||||
*
|
||||
* @param {string} aUserActionId The user action ID to remove
|
||||
* @param {string} aRequestToken The request token to remove
|
||||
*/
|
||||
_removeSlowCAMessage(aUserActionId, aRequestToken) {
|
||||
let entry = this.userActionToBusyDialogMap.get(aUserActionId);
|
||||
if (!entry) {
|
||||
|
@ -456,8 +551,9 @@ export const ContentAnalysis = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Gets all the requests that are still in progress.
|
||||
*
|
||||
* @returns {Iterable<{browsingContext: BrowsingContext, resourceNameOrOperationType: object}>} Information about the requests that are still in progress
|
||||
* @returns {Iterable<RequestInfo>} Information about the requests that are still in progress
|
||||
*/
|
||||
_getAllSlowCARequestInfos() {
|
||||
return this.userActionToBusyDialogMap
|
||||
|
@ -469,6 +565,11 @@ export const ContentAnalysis = {
|
|||
/**
|
||||
* Show a message to the user to indicate that a CA request is taking
|
||||
* a long time.
|
||||
*
|
||||
* @param {string} aOperation Name of the operation
|
||||
* @param {nsIContentAnalysisRequest} aRequest The request that is taking a long time
|
||||
* @param {string} aBodyMessage Message to show in the body of the alert
|
||||
* @param {BrowsingContext} aBrowsingContext BrowsingContext to show the alert in
|
||||
*/
|
||||
_showSlowCAMessage(aOperation, aRequest, aBodyMessage, aBrowsingContext) {
|
||||
if (!this._shouldShowBlockingNotification(aOperation)) {
|
||||
|
@ -489,6 +590,13 @@ export const ContentAnalysis = {
|
|||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the dialog message to show for the Slow CA dialog.
|
||||
*
|
||||
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
|
||||
* @param {number} aNumRequests
|
||||
* @returns {string}
|
||||
*/
|
||||
_getSlowDialogMessage(aResourceNameOrOperationType, aNumRequests) {
|
||||
if (aResourceNameOrOperationType.name) {
|
||||
let label =
|
||||
|
@ -524,6 +632,12 @@ export const ContentAnalysis = {
|
|||
return this.l10n.formatValueSync(l10nId, { agent: lazy.agentName });
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the dialog message to show when the request has an error.
|
||||
*
|
||||
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
|
||||
* @returns {string}
|
||||
*/
|
||||
_getErrorDialogMessage(aResourceNameOrOperationType) {
|
||||
if (aResourceNameOrOperationType.name) {
|
||||
return this.l10n.formatValueSync(
|
||||
|
@ -552,6 +666,16 @@ export const ContentAnalysis = {
|
|||
}
|
||||
return this.l10n.formatValueSync(l10nId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the Slow CA blocking dialog.
|
||||
*
|
||||
* @param {BrowsingContext} aBrowsingContext
|
||||
* @param {string} aUserActionId
|
||||
* @param {string} aRequestToken
|
||||
* @param {string} aBodyMessage
|
||||
* @returns {NotificationInfo}
|
||||
*/
|
||||
_showSlowCABlockingMessage(
|
||||
aBrowsingContext,
|
||||
aUserActionId,
|
||||
|
@ -589,12 +713,12 @@ export const ContentAnalysis = {
|
|||
// in which case we need to cancel the request.
|
||||
if (this.requestTokenToRequestInfo.delete(aRequestToken)) {
|
||||
// TODO: Is this useful? I think no.
|
||||
this._removeSlowCAMessage({}, aRequestToken);
|
||||
this._removeSlowCAMessage(aUserActionId, aRequestToken);
|
||||
lazy.gContentAnalysis.cancelRequestsByRequestToken(aRequestToken);
|
||||
}
|
||||
});
|
||||
return {
|
||||
requestToken: aRequestToken,
|
||||
dialogBrowsingContext: aBrowsingContext,
|
||||
};
|
||||
},
|
||||
|
@ -602,7 +726,14 @@ export const ContentAnalysis = {
|
|||
/**
|
||||
* Show a message to the user to indicate the result of a CA request.
|
||||
*
|
||||
* @returns {object} a notification object (if shown)
|
||||
* @param {object} aResourceNameOrOperationType
|
||||
* @param {BrowsingContext} aBrowsingContext
|
||||
* @param {string} aRequestToken
|
||||
* @param {string} aUserActionId
|
||||
* @param {number} aCAResult
|
||||
* @param {bool} aIsAgentResponse
|
||||
* @param {number} aRequestCancelError
|
||||
* @returns {NotificationInfo?} a notification object (if shown)
|
||||
*/
|
||||
async _showCAResult(
|
||||
aResourceNameOrOperationType,
|
||||
|
@ -635,6 +766,7 @@ export const ContentAnalysis = {
|
|||
case Ci.nsIContentAnalysisResponse.eWarn: {
|
||||
let allow = false;
|
||||
try {
|
||||
this.warnDialogRequestTokens.add(aRequestToken);
|
||||
const result = await Services.prompt.asyncConfirmEx(
|
||||
aBrowsingContext,
|
||||
Ci.nsIPromptService.MODAL_TYPE_TAB,
|
||||
|
@ -664,7 +796,13 @@ export const ContentAnalysis = {
|
|||
// the request is still active.
|
||||
allow = false;
|
||||
}
|
||||
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
|
||||
// Note that the shutdown code in the "quit-application" handler
|
||||
// may have cleared out warnDialogRequestTokens and responded
|
||||
// to the request already, so don't call respondToWarnDialog()
|
||||
// if aRequestToken is not in warnDialogRequestTokens.
|
||||
if (this.warnDialogRequestTokens.delete(aRequestToken)) {
|
||||
lazy.gContentAnalysis.respondToWarnDialog(aRequestToken, allow);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case Ci.nsIContentAnalysisResponse.eBlock: {
|
||||
|
@ -832,6 +970,8 @@ export const ContentAnalysis = {
|
|||
|
||||
/**
|
||||
* Returns the correct text for warn dialog contents.
|
||||
*
|
||||
* @param {ResourceNameOrOperationType} aResourceNameOrOperationType
|
||||
*/
|
||||
async _warnDialogText(aResourceNameOrOperationType) {
|
||||
const caInfo = await lazy.gContentAnalysis.getDiagnosticInfo();
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Toolkit", "General")
|
||||
BUG_COMPONENT = ("Firefox", "Data Loss Prevention")
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
"content/ContentAnalysis.sys.mjs",
|
||||
|
|
|
@ -10,6 +10,8 @@ class TestNoToolbarChanges(MarionetteTestCase):
|
|||
Test that toolbar widgets remain in the same order over several restarts of the browser
|
||||
"""
|
||||
|
||||
have_seen_import_button = False
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.marionette.set_context("chrome")
|
||||
|
@ -37,13 +39,30 @@ class TestNoToolbarChanges(MarionetteTestCase):
|
|||
navbarPlacements,
|
||||
msg="AREA_NAVBAR placements are as expected",
|
||||
)
|
||||
actualBookmarkPlacements = self.get_area_widgets("AREA_BOOKMARKS")
|
||||
bookmarkPlacements = self.get_area_default_placements("AREA_BOOKMARKS")
|
||||
bookmarkPlacements.insert(0, "import-button")
|
||||
self.assertEqual(
|
||||
self.get_area_widgets("AREA_BOOKMARKS"),
|
||||
bookmarkPlacements,
|
||||
msg="AREA_BOOKMARKS placements are as expected",
|
||||
# The import button is added lazily on startup, so we can't predict
|
||||
# whether it'll be here. Turning it off via prefs=[] annotations on the
|
||||
# test also doesn't work
|
||||
# (https://bugzilla.mozilla.org/show_bug.cgi?id=1959688).
|
||||
# So we simply accept placements either with or without the button - but
|
||||
# if we ever see the button we should keep seeing it.
|
||||
self.have_seen_import_button = (
|
||||
self.have_seen_import_button or "import-button" in actualBookmarkPlacements
|
||||
)
|
||||
if self.have_seen_import_button:
|
||||
self.assertEqual(
|
||||
actualBookmarkPlacements,
|
||||
["import-button"] + bookmarkPlacements,
|
||||
msg="AREA_BOOKMARKS placements are as expected",
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
actualBookmarkPlacements,
|
||||
bookmarkPlacements,
|
||||
msg="AREA_BOOKMARKS placements are as expected",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.get_area_widgets("AREA_ADDONS"),
|
||||
self.get_area_default_placements("AREA_ADDONS"),
|
||||
|
|
|
@ -13,6 +13,8 @@ module.exports = {
|
|||
TabContext: true,
|
||||
Window: true,
|
||||
clickModifiersFromEvent: true,
|
||||
getExtTabGroupIdForInternalTabGroupId: true,
|
||||
getInternalTabGroupIdForExtTabGroupId: true,
|
||||
makeWidgetId: true,
|
||||
openOptionsPage: true,
|
||||
replaceUrlInTab: true,
|
||||
|
|
|
@ -135,6 +135,49 @@ global.replaceUrlInTab = (gBrowser, tab, uri) => {
|
|||
return loaded;
|
||||
};
|
||||
|
||||
// The tabs.Tab.groupId type in the public extension API is an integer,
|
||||
// but tabbrowser's tab group ID are strings. This handles the conversion.
|
||||
//
|
||||
// tabbrowser.addTabGroup() generates the internal tab group ID as follows:
|
||||
// internal group id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
|
||||
// After dropping the hyphen ("-"), the result can be coerced into a safe
|
||||
// integer.
|
||||
//
|
||||
// As a safeguard, in case the format changes, we fall back to maintaining
|
||||
// an internal mapping (that never gets cleaned up).
|
||||
// This may change in https://bugzilla.mozilla.org/show_bug.cgi?id=1960104
|
||||
const fallbackTabGroupIdMap = new Map();
|
||||
let nextFallbackTabGroupId = 1;
|
||||
global.getExtTabGroupIdForInternalTabGroupId = groupIdStr => {
|
||||
const parsedTabId = /^(\d{13})-(\d{1,3})$/.exec(groupIdStr);
|
||||
if (parsedTabId) {
|
||||
const groupId = parsedTabId[1] * 1000 + parseInt(parsedTabId[2], 10);
|
||||
if (Number.isSafeInteger(groupId)) {
|
||||
return groupId;
|
||||
}
|
||||
}
|
||||
// Fall back.
|
||||
let fallbackGroupId = fallbackTabGroupIdMap.get(groupIdStr);
|
||||
if (!fallbackGroupId) {
|
||||
fallbackGroupId = nextFallbackTabGroupId++;
|
||||
fallbackTabGroupIdMap.set(groupIdStr, fallbackGroupId);
|
||||
}
|
||||
return fallbackGroupId;
|
||||
};
|
||||
global.getInternalTabGroupIdForExtTabGroupId = groupId => {
|
||||
if (Number.isSafeInteger(groupId) && groupId >= 1e15) {
|
||||
// 16 digits - this inverts getExtTabGroupIdForInternalTabGroupId.
|
||||
const groupIdStr = `${Math.floor(groupId / 1000)}-${groupId % 1000}`;
|
||||
return groupIdStr;
|
||||
}
|
||||
for (let [groupIdStr, fallbackGroupId] of fallbackTabGroupIdMap) {
|
||||
if (fallbackGroupId === groupId) {
|
||||
return groupIdStr;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages tab-specific and window-specific context data, and dispatches
|
||||
* tab select events across all windows.
|
||||
|
@ -878,6 +921,11 @@ class Tab extends TabBase {
|
|||
return successor ? tabTracker.getId(successor) : -1;
|
||||
}
|
||||
|
||||
get groupId() {
|
||||
const { group } = this.nativeTab;
|
||||
return group ? getExtTabGroupIdForInternalTabGroupId(group.id) : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts session store data to an object compatible with the return value
|
||||
* of the convert() method, representing that data.
|
||||
|
|
|
@ -160,6 +160,7 @@ const allProperties = new Set([
|
|||
"autoDiscardable",
|
||||
"discarded",
|
||||
"favIconUrl",
|
||||
"groupId",
|
||||
"hidden",
|
||||
"isArticle",
|
||||
"mutedInfo",
|
||||
|
@ -260,13 +261,17 @@ this.tabs = class extends ExtensionAPIPersistent {
|
|||
}),
|
||||
onMoved({ fire }) {
|
||||
let { tabManager } = this.extension;
|
||||
/**
|
||||
* @param {CustomEvent} event
|
||||
*/
|
||||
let moveListener = event => {
|
||||
let nativeTab = event.originalTarget;
|
||||
let { previousTabState, currentTabState } = event.detail;
|
||||
if (tabManager.canAccessTab(nativeTab)) {
|
||||
fire.async(tabTracker.getId(nativeTab), {
|
||||
windowId: windowTracker.getId(nativeTab.ownerGlobal),
|
||||
fromIndex: event.detail,
|
||||
toIndex: nativeTab._tPos,
|
||||
fromIndex: previousTabState.tabIndex,
|
||||
toIndex: currentTabState.tabIndex,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -461,6 +466,15 @@ this.tabs = class extends ExtensionAPIPersistent {
|
|||
needed.push("discarded");
|
||||
} else if (event.type == "TabBrowserDiscarded") {
|
||||
needed.push("discarded");
|
||||
} else if (event.type === "TabGrouped") {
|
||||
needed.push("groupId");
|
||||
} else if (event.type === "TabUngrouped") {
|
||||
if (event.originalTarget.group) {
|
||||
// If there is still a group, that means that the group changed,
|
||||
// so TabGrouped will also fire. Ignore to avoid duplicate events.
|
||||
return;
|
||||
}
|
||||
needed.push("groupId");
|
||||
} else if (event.type == "TabShow") {
|
||||
needed.push("hidden");
|
||||
} else if (event.type == "TabHide") {
|
||||
|
@ -527,6 +541,10 @@ this.tabs = class extends ExtensionAPIPersistent {
|
|||
listeners.set("TabBrowserInserted", listener);
|
||||
listeners.set("TabBrowserDiscarded", listener);
|
||||
}
|
||||
if (filter.properties.has("groupId")) {
|
||||
listeners.set("TabGrouped", listener);
|
||||
listeners.set("TabUngrouped", listener);
|
||||
}
|
||||
if (filter.properties.has("hidden")) {
|
||||
listeners.set("TabShow", listener);
|
||||
listeners.set("TabHide", listener);
|
||||
|
@ -1661,6 +1679,113 @@ this.tabs = class extends ExtensionAPIPersistent {
|
|||
let nativeTab = getTabOrActive(tabId);
|
||||
nativeTab.linkedBrowser.goBack(false);
|
||||
},
|
||||
|
||||
group(options) {
|
||||
let nativeTabs = getNativeTabsFromIDArray(options.tabIds);
|
||||
let window = windowTracker.getWindow(
|
||||
options.createProperties?.windowId ?? Window.WINDOW_ID_CURRENT,
|
||||
context
|
||||
);
|
||||
const windowIsPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
|
||||
for (const nativeTab of nativeTabs) {
|
||||
if (
|
||||
PrivateBrowsingUtils.isWindowPrivate(nativeTab.ownerGlobal) !==
|
||||
windowIsPrivate
|
||||
) {
|
||||
if (windowIsPrivate) {
|
||||
throw new ExtensionError(
|
||||
"Cannot move non-private tabs to private window"
|
||||
);
|
||||
}
|
||||
throw new ExtensionError(
|
||||
"Cannot move private tabs to non-private window"
|
||||
);
|
||||
}
|
||||
}
|
||||
function unpinTabsBeforeGrouping() {
|
||||
for (const nativeTab of nativeTabs) {
|
||||
nativeTab.ownerGlobal.gBrowser.unpinTab(nativeTab);
|
||||
}
|
||||
}
|
||||
let group;
|
||||
if (options.groupId == null) {
|
||||
// By default, tabs are appended after all other tabs in the
|
||||
// window. But if we are grouping tabs within a window, ideally the
|
||||
// tabs should just be grouped without moving positions.
|
||||
// TODO bug 1939214: when addTabGroup inserts tabs at the front as
|
||||
// needed (instead of always appending), simplify this logic.
|
||||
const tabInWin = nativeTabs.find(t => t.ownerGlobal === window);
|
||||
let insertBefore = tabInWin;
|
||||
if (tabInWin?.group) {
|
||||
if (tabInWin.group.tabs[0] === tabInWin) {
|
||||
// When tabInWin is at the front of a tab group, insert before
|
||||
// the tab group (instead of after it).
|
||||
insertBefore = tabInWin.group;
|
||||
} else {
|
||||
insertBefore = insertBefore.group.nextElementSibling;
|
||||
}
|
||||
}
|
||||
unpinTabsBeforeGrouping();
|
||||
group = window.gBrowser.addTabGroup(nativeTabs, { insertBefore });
|
||||
// Note: group is never null, because the only condition for which
|
||||
// it could be null is when all tabs are pinned, and we are already
|
||||
// explicitly unpinning them before moving.
|
||||
} else {
|
||||
group = window.gBrowser.getTabGroupById(
|
||||
getInternalTabGroupIdForExtTabGroupId(options.groupId)
|
||||
);
|
||||
if (!group) {
|
||||
throw new ExtensionError(`No group with id: ${options.groupId}`);
|
||||
}
|
||||
unpinTabsBeforeGrouping();
|
||||
// When moving tabs within the same window, try to maintain their
|
||||
// relative positions.
|
||||
const tabsBefore = [];
|
||||
const tabsAfter = [];
|
||||
const firstTabInGroup = group.tabs[0];
|
||||
for (const nativeTab of nativeTabs) {
|
||||
if (
|
||||
nativeTab.ownerGlobal === window &&
|
||||
nativeTab._tPos < firstTabInGroup._tPos
|
||||
) {
|
||||
tabsBefore.push(nativeTab);
|
||||
} else {
|
||||
tabsAfter.push(nativeTab);
|
||||
}
|
||||
}
|
||||
if (tabsBefore.length) {
|
||||
window.gBrowser.moveTabsBefore(tabsBefore, firstTabInGroup);
|
||||
}
|
||||
if (tabsAfter.length) {
|
||||
group.addTabs(tabsAfter);
|
||||
}
|
||||
}
|
||||
return getExtTabGroupIdForInternalTabGroupId(group.id);
|
||||
},
|
||||
|
||||
ungroup(tabIds) {
|
||||
const nativeTabs = getNativeTabsFromIDArray(tabIds);
|
||||
// Ungroup tabs while trying to preserve the relative order of tabs
|
||||
// within the tab strip as much as possible. This is not always
|
||||
// possible, e.g. when a tab group is only partially ungrouped.
|
||||
const ungroupOrder = new DefaultMap(() => []);
|
||||
for (const nativeTab of nativeTabs) {
|
||||
if (nativeTab.group) {
|
||||
ungroupOrder.get(nativeTab.group).push(nativeTab);
|
||||
}
|
||||
}
|
||||
for (const [group, tabs] of ungroupOrder) {
|
||||
// Preserve original order of ungrouped tabs.
|
||||
tabs.sort((a, b) => a._tPos - b._tPos);
|
||||
if (tabs[0] === tabs[0].group.tabs[0]) {
|
||||
// The tab is the front of the tab group, so insert before
|
||||
// current tab group to preserve order.
|
||||
tabs[0].ownerGlobal.gBrowser.moveTabsBefore(tabs, group);
|
||||
} else {
|
||||
tabs[0].ownerGlobal.gBrowser.moveTabsAfter(tabs, group);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
return tabsApi;
|
||||
|
|
|
@ -227,6 +227,12 @@
|
|||
"optional": true,
|
||||
"minimum": -1,
|
||||
"description": "The ID of this tab's successor, if any; $(ref:tabs.TAB_ID_NONE) otherwise."
|
||||
},
|
||||
"groupId": {
|
||||
"type": "integer",
|
||||
"optional": true,
|
||||
"minimum": -1,
|
||||
"description": "The ID of the group that the tab belongs to. $(ref:tabGroups.TAB_GROUP_ID_NONE) (-1) if the tab does not belong to a tab group."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -429,6 +435,7 @@
|
|||
"autoDiscardable",
|
||||
"discarded",
|
||||
"favIconUrl",
|
||||
"groupId",
|
||||
"hidden",
|
||||
"isArticle",
|
||||
"mutedInfo",
|
||||
|
@ -832,6 +839,12 @@
|
|||
"optional": true,
|
||||
"description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab."
|
||||
},
|
||||
"groupId": {
|
||||
"type": "integer",
|
||||
"minimum": -1,
|
||||
"optional": true,
|
||||
"description": "The ID of the group that the tabs are in, or $(ref:tabGroups.TAB_GROUP_ID_NONE) (-1) for ungrouped tabs."
|
||||
},
|
||||
"screen": {
|
||||
"choices": [
|
||||
{
|
||||
|
@ -1591,6 +1604,83 @@
|
|||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "group",
|
||||
"type": "function",
|
||||
"description": "Adds one or more tabs to a specified group, or if no group is specified, adds the given tabs to a newly created group.",
|
||||
"async": "callback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "options",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tabIds": {
|
||||
"description": "The tab ID or list of tab IDs to add to the specified group.",
|
||||
"choices": [
|
||||
{ "type": "integer", "minimum": 0 },
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "integer", "minimum": 0 },
|
||||
"minItems": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"groupId": {
|
||||
"type": "integer",
|
||||
"description": "The ID of the group to add the tabs to. If not specified, a new group will be created.",
|
||||
"minimum": 0,
|
||||
"optional": true
|
||||
},
|
||||
"createProperties": {
|
||||
"type": "object",
|
||||
"optional": true,
|
||||
"description": "Configurations for creating a group. Cannot be used if groupId is already specified.",
|
||||
"properties": {
|
||||
"windowId": {
|
||||
"type": "integer",
|
||||
"minimum": -2,
|
||||
"optional": true,
|
||||
"description": "The window of the new group. Defaults to the current window."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"name": "callback",
|
||||
"optional": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "groupId",
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "The ID of the group that the tabs were added to."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ungroup",
|
||||
"type": "function",
|
||||
"description": "Removes one or more tabs from their respective groups. If any groups become empty, they are deleted.",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tabIds",
|
||||
"description": "The tab ID or list of tab IDs to remove from their respective groups.",
|
||||
"choices": [
|
||||
{ "type": "integer", "minimum": 0 },
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "integer", "minimum": 0 },
|
||||
"minItems": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
|
|
|
@ -538,6 +538,10 @@ https_first_disabled = true
|
|||
|
||||
["browser_ext_tabs_goBack_goForward.js"]
|
||||
|
||||
["browser_ext_tabs_groupId.js"]
|
||||
|
||||
["browser_ext_tabs_group_ungroup.js"]
|
||||
|
||||
["browser_ext_tabs_hide.js"]
|
||||
https_first_disabled = true
|
||||
|
||||
|
@ -579,6 +583,8 @@ skip-if = [
|
|||
|
||||
["browser_ext_tabs_onUpdated_filter.js"]
|
||||
|
||||
["browser_ext_tabs_onUpdated_groupId.js"]
|
||||
|
||||
["browser_ext_tabs_opener.js"]
|
||||
|
||||
["browser_ext_tabs_printPreview.js"]
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
function resetInternalExtensionFallbackTabGroupIdMap() {
|
||||
const { ExtensionParent } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionParent.sys.mjs"
|
||||
);
|
||||
// ExtensionParent.apiManager.global may be unset if there has not been any
|
||||
// attempt to load any extension yet.
|
||||
// We need to use eval() because the variables are declared with const/let,
|
||||
// which cannot simply be read as properties from the global.
|
||||
ExtensionParent.apiManager.global?.eval(`
|
||||
"use strict";
|
||||
// Reset to initial values as set by ext-browser.js
|
||||
fallbackTabGroupIdMap.clear();
|
||||
nextFallbackTabGroupId = 1;
|
||||
`);
|
||||
}
|
||||
function getInternalExtensionFallbackTabGroupIdMapEntries() {
|
||||
const { ExtensionParent } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/ExtensionParent.sys.mjs"
|
||||
);
|
||||
return Array.from(
|
||||
ExtensionParent.apiManager.global.eval("fallbackTabGroupIdMap").entries()
|
||||
);
|
||||
}
|
||||
|
||||
async function getCurrentTabGroupId() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
browser.test.assertTrue(
|
||||
Number.isSafeInteger(tab.groupId),
|
||||
`groupId is safe integer: ${tab.groupId}`
|
||||
);
|
||||
browser.test.assertTrue(
|
||||
tab.groupId >= 0,
|
||||
`groupId is non-negative integer: ${tab.groupId}`
|
||||
);
|
||||
const tabRepeated = await browser.tabs.get(tab.id);
|
||||
browser.test.assertEq(
|
||||
tab.groupId,
|
||||
tabRepeated.groupId,
|
||||
"groupId should consistently return the same value"
|
||||
);
|
||||
const tabs = await browser.tabs.query({ groupId: tab.groupId });
|
||||
browser.test.assertEq(
|
||||
tabs.length,
|
||||
1,
|
||||
"tabs.query({ groupId }) found the one and only tab in that group"
|
||||
);
|
||||
browser.test.assertEq(tab.id, tabs[0].id, "Got expected tab");
|
||||
browser.test.sendMessage("ext_groupId", tab.groupId);
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
const groupId = await extension.awaitMessage("ext_groupId");
|
||||
await extension.unload();
|
||||
return groupId;
|
||||
}
|
||||
|
||||
// This test checks whether the tabs.Tab.groupId field has a meaningful value
|
||||
// derived from a tabbrowser tab group.
|
||||
add_task(async function tabs_Tab_groupId() {
|
||||
resetInternalExtensionFallbackTabGroupIdMap();
|
||||
|
||||
const tab1 = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"https://example.com/?1",
|
||||
true
|
||||
);
|
||||
const tab2 = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"https://example.com/?2",
|
||||
true
|
||||
);
|
||||
|
||||
gBrowser.selectedTab = tab2;
|
||||
|
||||
// Without a given ID, tabbrowser generates a new ID.
|
||||
const group1 = gBrowser.addTabGroup([tab1]);
|
||||
// The following ID differs from the tabbrowser format:
|
||||
const group2 = gBrowser.addTabGroup([tab2], { id: "non-numericid" });
|
||||
is(group2.id, "non-numericid", "Created group with non-numeric ID");
|
||||
|
||||
info("Testing tab group with internally generated ID");
|
||||
gBrowser.selectedTab = tab1;
|
||||
const generatedId1 = await getCurrentTabGroupId();
|
||||
const generatedId2 = await getCurrentTabGroupId();
|
||||
is(generatedId1, generatedId2, "Same groupId across extensions (generated)");
|
||||
|
||||
info("Testing tab group with ID, not matching internally generated IDs");
|
||||
gBrowser.selectedTab = tab2;
|
||||
const strangeId1 = await getCurrentTabGroupId();
|
||||
const strangeId2 = await getCurrentTabGroupId();
|
||||
is(strangeId1, strangeId2, "Same groupId across extensions (non-internal)");
|
||||
|
||||
isnot(generatedId1, strangeId1, "Independent tab groups have different ID");
|
||||
|
||||
// Move tab to another tab group.
|
||||
const group3 = gBrowser.addTabGroup([tab2], { id: "another-non-numericid" });
|
||||
is(group3.id, "another-non-numericid", "Updated group with non-numeric ID");
|
||||
const strangeId3 = await getCurrentTabGroupId();
|
||||
isnot(strangeId2, strangeId3, "New tab group has different tab group ID");
|
||||
|
||||
// Verify assumptions behind the above tests, to make sure that the test
|
||||
// passes for the expected reasons.
|
||||
is(
|
||||
`${Math.floor(generatedId1 / 1000)}-${generatedId1 % 1000}`,
|
||||
group1.id,
|
||||
"groupId in extension API derived from internally-generated group ID"
|
||||
);
|
||||
is(
|
||||
strangeId1,
|
||||
1,
|
||||
"groupId in extension API is next available ID (non-numericid)"
|
||||
);
|
||||
is(
|
||||
strangeId3,
|
||||
2,
|
||||
"groupId in extension API is next available ID (another-non-numericid)"
|
||||
);
|
||||
|
||||
BrowserTestUtils.removeTab(tab1);
|
||||
BrowserTestUtils.removeTab(tab2);
|
||||
|
||||
// The main purpose of this check is to verify that we do not store mappings
|
||||
// for internal groupIds that are in the expected format. Additionally, this
|
||||
// also shows that the map is not cleaned up even after the group disappears.
|
||||
Assert.deepEqual(
|
||||
getInternalExtensionFallbackTabGroupIdMapEntries(),
|
||||
[
|
||||
["non-numericid", strangeId1],
|
||||
["another-non-numericid", strangeId3],
|
||||
],
|
||||
"The internal fallback map contains only non-numeric IDs"
|
||||
);
|
||||
|
||||
resetInternalExtensionFallbackTabGroupIdMap();
|
||||
});
|
|
@ -0,0 +1,438 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
// TODO bug 1938594: group_across_private_browsing_windows sometimes triggers
|
||||
// this error. See https://bugzilla.mozilla.org/show_bug.cgi?id=1938594#c1
|
||||
PromiseTestUtils.allowMatchingRejectionsGlobally(
|
||||
/Unexpected undefined tabState for onMoveToNewWindow/
|
||||
);
|
||||
|
||||
add_task(async function group_ungroup_and_index() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
files: {
|
||||
"tab1.htm": "<title>tab1.html</title>",
|
||||
"tab2.htm": "<title>tab2.html</title>",
|
||||
"tab3.htm": "<title>tab3.html</title>",
|
||||
},
|
||||
async background() {
|
||||
const { id: tabId1 } = await browser.tabs.create({ url: "tab1.htm" });
|
||||
const { id: tabId2 } = await browser.tabs.create({ url: "tab2.htm " });
|
||||
const { id: tabId3 } = await browser.tabs.create({ url: "tab3.htm" });
|
||||
|
||||
async function assertAllTabExpectations(expectations, desc) {
|
||||
const tabs = await Promise.all([
|
||||
browser.tabs.get(tabId1),
|
||||
browser.tabs.get(tabId2),
|
||||
browser.tabs.get(tabId3),
|
||||
]);
|
||||
const { indexes, groupIds, ...rest } = expectations;
|
||||
if (Object.keys(rest).length) {
|
||||
// Sanity check, so we don't miss expectations due to typos.
|
||||
throw new Error(
|
||||
`Unexpected keys in expectations: ${Object.keys(rest)} for ${desc}`
|
||||
);
|
||||
}
|
||||
browser.test.assertDeepEq(
|
||||
indexes,
|
||||
tabs.map(t => t.index),
|
||||
`${desc} : Tabs should be at expected indexes`
|
||||
);
|
||||
browser.test.assertDeepEq(
|
||||
groupIds,
|
||||
tabs.toSorted((a, b) => a.index - b.index).map(t => t.groupId),
|
||||
`${desc} : Tab groupIds in order of tabs`
|
||||
);
|
||||
}
|
||||
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [-1, -1, -1] },
|
||||
"Initial tab indexes and group IDs before group()"
|
||||
);
|
||||
|
||||
const groupId1 = await browser.tabs.group({ tabIds: tabId1 });
|
||||
const groupId2 = await browser.tabs.group({ tabIds: [tabId2, tabId3] });
|
||||
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId1, groupId2, groupId2] },
|
||||
"Got two groups after group(tab1) + group([tab2, tab3])"
|
||||
);
|
||||
|
||||
// It should be possible to ungroup tabs from different groups.
|
||||
// And their position should not move, since these tabs are not in the
|
||||
// middle of a tab group.
|
||||
await browser.tabs.ungroup([tabId3, tabId1]);
|
||||
await browser.tabs.ungroup(tabId2);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: tabId3, groupId: groupId1 }),
|
||||
`No group with id: ${groupId1}`,
|
||||
"After ungrouping, the groupId should no longer be valid"
|
||||
);
|
||||
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [-1, -1, -1] },
|
||||
"Tab groups should still be at their original position in the tab"
|
||||
);
|
||||
|
||||
// Now grouping two tabs that are apart - they should be together.
|
||||
const groupId3 = await browser.tabs.group({ tabIds: [tabId1, tabId3] });
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 3, 2], groupIds: [groupId3, groupId3, -1] },
|
||||
"Tabs in same tab group must be next to each other"
|
||||
);
|
||||
|
||||
// Join existing tab group - now we should have three in the tab group.
|
||||
const groupId4 = await browser.tabs.group({
|
||||
tabIds: [tabId2],
|
||||
groupId: groupId3,
|
||||
});
|
||||
browser.test.assertEq(
|
||||
groupId3,
|
||||
groupId4,
|
||||
"group() with a groupId parameter returns given groupId"
|
||||
);
|
||||
|
||||
// Joining an existing group should not have changed positions.
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 3, 2], groupIds: [groupId3, groupId3, groupId3] },
|
||||
"Tab order did not change when joining adjacent group"
|
||||
);
|
||||
|
||||
await browser.tabs.ungroup([tabId1, tabId2, tabId3]);
|
||||
|
||||
// Ungrouping of the group should not have changed positions either,
|
||||
// despite the list of tabIds passed to ungroup() being out of order.
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 3, 2], groupIds: [-1, -1, -1] },
|
||||
"Tab order did not change when ungrouping with out-of-order tabIds"
|
||||
);
|
||||
|
||||
// Group tabs together. Tab positions should match given order.
|
||||
const groupId5 = await browser.tabs.group({
|
||||
tabIds: [tabId1, tabId2, tabId3],
|
||||
});
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId5, groupId5, groupId5] },
|
||||
"Tab order matches order of tabIds passed to tabs.group()"
|
||||
);
|
||||
|
||||
// Move the leftmost tab to a new group. That tab should still be
|
||||
// positioned at the left of the original tab group.
|
||||
const groupId6 = await browser.tabs.group({ tabIds: [tabId1] });
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId6, groupId5, groupId5] },
|
||||
"Leftmost tab should still be ordered before the original tab group"
|
||||
);
|
||||
|
||||
// Join an existing group (from the left). Position should not change.
|
||||
await browser.tabs.group({ tabIds: [tabId1], groupId: groupId5 });
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId5, groupId5, groupId5] },
|
||||
"Tab order did not change when joining a tab group from the left"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: tabId3, groupId: groupId6 }),
|
||||
`No group with id: ${groupId6}`,
|
||||
"Old groupId should be invalid after last tab was moved from group"
|
||||
);
|
||||
|
||||
// Move the middle tab to a new group. That tab should be at the right.
|
||||
const groupId7 = await browser.tabs.group({ tabIds: [tabId2] });
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 3, 2], groupIds: [groupId5, groupId5, groupId7] },
|
||||
"group() on middle tab in existing group appears on the right"
|
||||
);
|
||||
|
||||
// Prepare: tabId1 and tabId2 together at the left, followed by tabId3.
|
||||
const groupId8 = await browser.tabs.group({ tabIds: [tabId1, tabId2] });
|
||||
await browser.tabs.ungroup(tabId3);
|
||||
|
||||
// When tabId2 is moved to a new group, it should stay in the middle,
|
||||
// meaning that the tab was inserted after its original tab group.
|
||||
// In particular, it should not move to the end of the tab strip.
|
||||
const groupId9 = await browser.tabs.group({ tabIds: [tabId2] });
|
||||
await assertAllTabExpectations(
|
||||
{ indexes: [1, 2, 3], groupIds: [groupId8, groupId9, -1] },
|
||||
"group() on rightmost tab should appear after original tab group"
|
||||
);
|
||||
|
||||
await browser.tabs.remove(tabId1);
|
||||
await browser.tabs.remove(tabId2);
|
||||
await browser.tabs.remove(tabId3);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_with_windowId() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const { id: windowId, tabs: initialTabs } = await browser.windows.create(
|
||||
{}
|
||||
);
|
||||
browser.test.assertEq(1, initialTabs.length, "Got window with 1 tab");
|
||||
|
||||
const { id: tabId1 } = await browser.tabs.create({});
|
||||
const { id: tabId2 } = await browser.tabs.create({});
|
||||
|
||||
const groupId1 = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId1],
|
||||
createProperties: { windowId },
|
||||
});
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ groupId: groupId1 }), t => t.id),
|
||||
[tabId2, tabId1],
|
||||
"Moved tabs to group"
|
||||
);
|
||||
browser.test.assertDeepEq(
|
||||
Array.from(await browser.tabs.query({ windowId }), t => t.id),
|
||||
[initialTabs[0].id, tabId2, tabId1],
|
||||
"Moved tabs to group in new window (next to initial tab)"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_adopt_from_multiple_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const {
|
||||
id: windowId1,
|
||||
tabs: [{ id: tabId1 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId2,
|
||||
tabs: [{ id: tabId2 }],
|
||||
} = await browser.windows.create({});
|
||||
const {
|
||||
id: windowId3,
|
||||
tabs: [{ id: tabId3 }],
|
||||
} = await browser.windows.create({});
|
||||
|
||||
// This confirms that group() can adapt tabs from different windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: [tabId2, tabId3],
|
||||
createProperties: { windowId: windowId1 },
|
||||
});
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.windows.get(windowId2),
|
||||
`Invalid window ID: ${windowId2}`,
|
||||
"Window closes when group() adopts the last tab of the window"
|
||||
);
|
||||
// We just confirmed that window2 is closed, do the same for window3.
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: tabId1,
|
||||
createProperties: { windowId: windowId3 },
|
||||
}),
|
||||
`Invalid window ID: ${windowId3}`,
|
||||
"group() cannot adapt groups from a closed window"
|
||||
);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
// Note: tabId1 is missing because the above group() rejected.
|
||||
[tabId2, tabId3],
|
||||
Array.from(await browser.tabs.query({ groupId }), tab => tab.id),
|
||||
"All specified tabIds should now belong to the given group"
|
||||
);
|
||||
|
||||
await browser.windows.remove(windowId1);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_across_private_browsing_windows() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
incognitoOverride: "spanning",
|
||||
async background() {
|
||||
const privateWin = await browser.windows.create({ incognito: true });
|
||||
const normalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
const otherPrivateWin = await browser.windows.create({ incognito: true });
|
||||
const otherNormalWin = await browser.windows.create({ incognito: false });
|
||||
|
||||
// Mixture of private and non-private tabIDs, so we can easily verify
|
||||
// whether we inadvertently move some of the tabs.
|
||||
const privateTab = await browser.tabs.create({ windowId: privateWin.id });
|
||||
const privateAndNonPrivateTabs = [
|
||||
privateTab,
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
await browser.tabs.create({ windowId: privateWin.id }),
|
||||
await browser.tabs.create({ windowId: normalWin.id }),
|
||||
];
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private+non-private tabs to current window"
|
||||
);
|
||||
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
}),
|
||||
"Cannot move non-private tabs to private window",
|
||||
"Should not be able to move non-private tab to private window"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: privateAndNonPrivateTabs.map(t => t.id),
|
||||
createProperties: { windowId: otherNormalWin.id },
|
||||
}),
|
||||
"Cannot move private tabs to non-private window",
|
||||
"Should not be able to move private tab to non-private window"
|
||||
);
|
||||
|
||||
const reply = await browser.runtime.sendMessage("@no_private", {
|
||||
privateWindowId: privateWin.id,
|
||||
privateTabId: privateTab.id,
|
||||
});
|
||||
browser.test.assertEq("no_private:done", reply, "Reply from other ext");
|
||||
|
||||
for (const tab of privateAndNonPrivateTabs) {
|
||||
const actualTab = await browser.tabs.get(tab.id);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved to a different window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.windowId,
|
||||
actualTab.windowId,
|
||||
"Tab should not have moved within its window"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
tab.groupId,
|
||||
-1,
|
||||
"Tab should not have joined a group"
|
||||
);
|
||||
}
|
||||
|
||||
// Now check that we can actually group tabs in private windows.
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: privateTab.id,
|
||||
createProperties: { windowId: otherPrivateWin.id },
|
||||
});
|
||||
const updatedPrivateTab = await browser.tabs.get(privateTab.id);
|
||||
browser.test.assertEq(
|
||||
groupId,
|
||||
updatedPrivateTab.groupId,
|
||||
"group() succeeded with private tab"
|
||||
);
|
||||
browser.test.assertEq(
|
||||
otherPrivateWin.id,
|
||||
updatedPrivateTab.windowId,
|
||||
"Private tab is now part of the destination private window"
|
||||
);
|
||||
|
||||
await browser.windows.remove(privateWin.id);
|
||||
await browser.windows.remove(normalWin.id);
|
||||
await browser.windows.remove(otherPrivateWin.id);
|
||||
await browser.windows.remove(otherNormalWin.id);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
const extensionWithoutPrivateAccess = ExtensionTestUtils.loadExtension({
|
||||
manifest: { browser_specific_settings: { gecko: { id: "@no_private" } } },
|
||||
background() {
|
||||
browser.runtime.onMessageExternal.addListener(async data => {
|
||||
const { privateWindowId, privateTabId } = data;
|
||||
const { id: normalTabId } = await browser.tabs.create({});
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({ tabIds: [privateTabId] }),
|
||||
`Invalid tab ID: ${privateTabId}`,
|
||||
"@no_private should not be able to group private tabs"
|
||||
);
|
||||
await browser.test.assertRejects(
|
||||
browser.tabs.group({
|
||||
tabIds: [normalTabId],
|
||||
createProperties: { windowId: privateWindowId },
|
||||
}),
|
||||
`Invalid window ID: ${privateWindowId}`,
|
||||
"@no_private should not see private windows"
|
||||
);
|
||||
await browser.tabs.remove(normalTabId);
|
||||
return "no_private:done"; // Checked by sender.
|
||||
});
|
||||
},
|
||||
});
|
||||
await extensionWithoutPrivateAccess.startup();
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extensionWithoutPrivateAccess.unload();
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_pinned_tab() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const { id: tabId1 } = await browser.tabs.create({ pinned: true });
|
||||
const { id: tabId2 } = await browser.tabs.create({ pinned: true });
|
||||
const groupId = await browser.tabs.group({ tabIds: [tabId1] });
|
||||
browser.test.assertTrue(groupId > 0, `group() created group: ${groupId}`);
|
||||
|
||||
const tab1 = await browser.tabs.get(tabId1);
|
||||
browser.test.assertFalse(tab1.pinned, "group() unpins tab 1");
|
||||
browser.test.assertEq(groupId, tab1.groupId, "group() grouped tab 1");
|
||||
|
||||
const groupId2 = await browser.tabs.group({ tabIds: [tabId2], groupId });
|
||||
browser.test.assertEq(groupId, groupId2, "group() existing group");
|
||||
|
||||
const tab2 = await browser.tabs.get(tabId2);
|
||||
browser.test.assertFalse(tab2.pinned, "group() unpins tab 2");
|
||||
browser.test.assertEq(groupId, tab2.groupId, "Tab joined existing group");
|
||||
|
||||
await browser.tabs.remove(tabId1);
|
||||
await browser.tabs.remove(tabId2);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function group_pinned_tab_from_different_window() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const { id: tabId } = await browser.tabs.create({ pinned: true });
|
||||
const { id: windowId } = await browser.windows.create({});
|
||||
const groupId = await browser.tabs.group({
|
||||
tabIds: [tabId],
|
||||
createProperties: { windowId },
|
||||
});
|
||||
browser.test.assertTrue(groupId > 0, `group() created group: ${groupId}`);
|
||||
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
browser.test.assertFalse(tab.pinned, "group() unpins tab");
|
||||
browser.test.assertEq(groupId, tab.groupId, "group() grouped tab");
|
||||
browser.test.assertEq(windowId, tab.windowId, "Moved tab to new window");
|
||||
|
||||
await browser.windows.remove(windowId);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
add_task(async function onUpdated_when_grouping_and_ungrouping() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const changes = [];
|
||||
browser.tabs.onUpdated.addListener(
|
||||
(tabId, changeInfo, tab) => {
|
||||
browser.test.assertEq(
|
||||
changeInfo.groupId,
|
||||
tab.groupId,
|
||||
"changeInfo.groupId matches tab.groupId"
|
||||
);
|
||||
changes.push(changeInfo);
|
||||
},
|
||||
{ properties: ["groupId"] }
|
||||
);
|
||||
|
||||
const { id: tabId } = await browser.tabs.create({});
|
||||
const groupId1 = await browser.tabs.group({ tabIds: [tabId] });
|
||||
await browser.tabs.ungroup(tabId);
|
||||
const groupId2 = await browser.tabs.group({ tabIds: [tabId] });
|
||||
await browser.tabs.remove(tabId);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
[{ groupId: groupId1 }, { groupId: -1 }, { groupId: groupId2 }],
|
||||
changes,
|
||||
"Observed tabs.onUpdated events after group(), ungroup() and group()"
|
||||
);
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function onUpdated_when_grouping_and_regrouping() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const changes = [];
|
||||
browser.tabs.onUpdated.addListener(
|
||||
(tabId, changeInfo, tab) => {
|
||||
browser.test.assertEq(
|
||||
changeInfo.groupId,
|
||||
tab.groupId,
|
||||
"changeInfo.groupId matches tab.groupId"
|
||||
);
|
||||
changes.push(changeInfo);
|
||||
},
|
||||
{ properties: ["groupId"] }
|
||||
);
|
||||
|
||||
const { id: tabId } = await browser.tabs.create({});
|
||||
const groupId1 = await browser.tabs.group({ tabIds: [tabId] });
|
||||
const groupId2 = await browser.tabs.group({ tabIds: [tabId] });
|
||||
const groupId3 = await browser.tabs.group({ tabIds: [tabId] });
|
||||
await browser.tabs.remove(tabId);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
[{ groupId: groupId1 }, { groupId: groupId2 }, { groupId: groupId3 }],
|
||||
changes,
|
||||
"Observed tabs.onUpdated events after group() and regrouping repeatedly"
|
||||
);
|
||||
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
||||
|
||||
add_task(async function onUpdated_when_grouping_pinned_tab() {
|
||||
const extension = ExtensionTestUtils.loadExtension({
|
||||
async background() {
|
||||
const changes = [];
|
||||
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
||||
if (changeInfo.groupId) {
|
||||
browser.test.assertEq(
|
||||
changeInfo.groupId,
|
||||
tab.groupId,
|
||||
"changeInfo.groupId matches tab.groupId"
|
||||
);
|
||||
changes.push(changeInfo);
|
||||
} else if (changeInfo.pinned != null) {
|
||||
changes.push(changeInfo);
|
||||
}
|
||||
});
|
||||
|
||||
const { id: tabId } = await browser.tabs.create({ pinned: true });
|
||||
const groupId = await browser.tabs.group({ tabIds: [tabId] });
|
||||
await browser.tabs.remove(tabId);
|
||||
|
||||
browser.test.assertDeepEq(
|
||||
[{ pinned: true }, { pinned: false }, { groupId: groupId }],
|
||||
changes,
|
||||
"Observed tabs.onUpdated events after group() of pinned tab"
|
||||
);
|
||||
browser.test.sendMessage("done");
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
await extension.awaitMessage("done");
|
||||
await extension.unload();
|
||||
});
|
|
@ -33,6 +33,7 @@ let expectedBackgroundApisTargetSpecific = [
|
|||
"tabs.getZoomSettings",
|
||||
"tabs.goBack",
|
||||
"tabs.goForward",
|
||||
"tabs.group",
|
||||
"tabs.highlight",
|
||||
"tabs.insertCSS",
|
||||
"tabs.move",
|
||||
|
@ -58,6 +59,7 @@ let expectedBackgroundApisTargetSpecific = [
|
|||
"tabs.setZoom",
|
||||
"tabs.setZoomSettings",
|
||||
"tabs.toggleReaderMode",
|
||||
"tabs.ungroup",
|
||||
"tabs.update",
|
||||
"tabs.warmup",
|
||||
"windows.CreateType",
|
||||
|
|
|
@ -37,13 +37,22 @@ const HISTORY_MAP_L10N_IDS = {
|
|||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* When sorting by date or site, each card "item" is a single visit.
|
||||
*
|
||||
* When sorting by date *and* site, each card "item" is a mapping of site
|
||||
* domains to their respective list of visits.
|
||||
*
|
||||
* @typedef {HistoryVisit | [string, HistoryVisit[]]} CardItem
|
||||
*/
|
||||
|
||||
/**
|
||||
* A list of visits displayed on a card.
|
||||
*
|
||||
* @typedef {object} CardEntry
|
||||
*
|
||||
* @property {string} domain
|
||||
* @property {HistoryVisit[]} items
|
||||
* @property {CardItem[]} items
|
||||
* @property {string} l10nId
|
||||
*/
|
||||
|
||||
|
@ -127,7 +136,7 @@ export class HistoryController {
|
|||
/**
|
||||
* Update cached history.
|
||||
*
|
||||
* @param {Map<CacheKey, HistoryVisit[]>} [historyMap]
|
||||
* @param {CachedHistory} [historyMap]
|
||||
* If provided, performs an update using the given data (instead of fetching
|
||||
* it from the db).
|
||||
*/
|
||||
|
@ -146,7 +155,19 @@ export class HistoryController {
|
|||
}
|
||||
for (const { items } of entries) {
|
||||
for (const item of items) {
|
||||
this.#normalizeVisit(item);
|
||||
switch (sortOption) {
|
||||
case "datesite": {
|
||||
// item is a [ domain, visit[] ] entry.
|
||||
const [, visits] = item;
|
||||
for (const visit of visits) {
|
||||
this.#normalizeVisit(visit);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// item is a single visit.
|
||||
this.#normalizeVisit(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.historyCache = { entries, searchQuery, sortOption };
|
||||
|
@ -203,6 +224,11 @@ export class HistoryController {
|
|||
return this.#getVisitsForDate(historyMap);
|
||||
case "site":
|
||||
return this.#getVisitsForSite(historyMap);
|
||||
case "datesite":
|
||||
this.#setTodaysDate();
|
||||
return this.#getVisitsForDateSite(historyMap);
|
||||
case "lastvisited":
|
||||
return this.#getVisitsForLastVisited(historyMap);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
@ -285,9 +311,9 @@ export class HistoryController {
|
|||
* Get a list of visits per day for each day on this month, excluding today
|
||||
* and yesterday.
|
||||
*
|
||||
* @param {Map<number, HistoryVisit[]>} cachedHistory
|
||||
* @param {CachedHistory} cachedHistory
|
||||
* The history cache to process.
|
||||
* @returns {HistoryVisit[][]}
|
||||
* @returns {CardItem[]}
|
||||
* A list of visits for each day.
|
||||
*/
|
||||
#getVisitsByDay(cachedHistory) {
|
||||
|
@ -313,9 +339,9 @@ export class HistoryController {
|
|||
* excluding yesterday's visits if yesterday happens to fall on the previous
|
||||
* month.
|
||||
*
|
||||
* @param {Map<number, HistoryVisit[]>} cachedHistory
|
||||
* @param {CachedHistory} cachedHistory
|
||||
* The history cache to process.
|
||||
* @returns {HistoryVisit[][]}
|
||||
* @returns {CardItem[]}
|
||||
* A list of visits for each month.
|
||||
*/
|
||||
#getVisitsByMonth(cachedHistory) {
|
||||
|
@ -332,6 +358,12 @@ export class HistoryController {
|
|||
const month = this.placesQuery.getStartOfMonthTimestamp(date);
|
||||
if (month !== previousMonth) {
|
||||
visitsPerMonth.push(visits);
|
||||
} else if (this.sortOption === "datesite") {
|
||||
// CardItem type is currently Map<string, HistoryVisit[]>.
|
||||
visitsPerMonth[visitsPerMonth.length - 1] = this.#mergeMaps(
|
||||
visitsPerMonth.at(-1),
|
||||
visits
|
||||
);
|
||||
} else {
|
||||
visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth
|
||||
.at(-1)
|
||||
|
@ -372,6 +404,22 @@ export class HistoryController {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two maps of (domain: string) => HistoryVisit[] into a single map.
|
||||
*
|
||||
* @param {Map<string, HistoryVisit[]>} oldMap
|
||||
* @param {Map<string, HistoryVisit[]>} newMap
|
||||
* @returns {Map<string, HistoryVisit[]>}
|
||||
*/
|
||||
#mergeMaps(oldMap, newMap) {
|
||||
const map = new Map(oldMap);
|
||||
for (const [domain, newVisits] of newMap) {
|
||||
const oldVisits = map.get(domain);
|
||||
map.set(domain, oldVisits?.concat(newVisits) ?? newVisits);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of visits, sorted by site, in alphabetical order.
|
||||
*
|
||||
|
@ -386,6 +434,75 @@ export class HistoryController {
|
|||
})).sort((a, b) => a.domain.localeCompare(b.domain));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of visits, sorted by date and site, in reverse chronological
|
||||
* order.
|
||||
*
|
||||
* @param {Map<number, Map<string, HistoryVisit[]>>} historyMap
|
||||
* @returns {CardEntry[]}
|
||||
*/
|
||||
#getVisitsForDateSite(historyMap) {
|
||||
const entries = [];
|
||||
const visitsFromToday = this.#getVisitsFromToday(historyMap);
|
||||
const visitsFromYesterday = this.#getVisitsFromYesterday(historyMap);
|
||||
const visitsByDay = this.#getVisitsByDay(historyMap);
|
||||
const visitsByMonth = this.#getVisitsByMonth(historyMap);
|
||||
|
||||
/**
|
||||
* Sorts items alphabetically by domain name.
|
||||
*
|
||||
* @param {[string, HistoryVisit[]][]} items
|
||||
* @returns {[string, HistoryVisit[]][]} The items in sorted order.
|
||||
*/
|
||||
function sortItems(items) {
|
||||
return items.sort(([aDomain], [bDomain]) =>
|
||||
aDomain.localeCompare(bDomain)
|
||||
);
|
||||
}
|
||||
|
||||
// Add visits from today and yesterday.
|
||||
if (visitsFromToday.length) {
|
||||
entries.push({
|
||||
l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"],
|
||||
items: sortItems(visitsFromToday),
|
||||
});
|
||||
}
|
||||
if (visitsFromYesterday.length) {
|
||||
entries.push({
|
||||
l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"],
|
||||
items: sortItems(visitsFromYesterday),
|
||||
});
|
||||
}
|
||||
|
||||
// Add visits from this month, grouped by day.
|
||||
visitsByDay.forEach(visits => {
|
||||
entries.push({
|
||||
l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"],
|
||||
items: sortItems([...visits]),
|
||||
});
|
||||
});
|
||||
|
||||
// Add visits from previous months, grouped by month.
|
||||
visitsByMonth.forEach(visits => {
|
||||
entries.push({
|
||||
l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"],
|
||||
items: sortItems([...visits]),
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of visits sorted by recency.
|
||||
*
|
||||
* @param {HistoryVisit[]} items
|
||||
* @returns {CardEntry[]}
|
||||
*/
|
||||
#getVisitsForLastVisited(items) {
|
||||
return [{ items }];
|
||||
}
|
||||
|
||||
async #fetchHistory() {
|
||||
return this.placesQuery.getHistory({
|
||||
daysOld: 60,
|
||||
|
|
|
@ -10,6 +10,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ASRouterTargeting: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
|
||||
ContentAnalysisUtils: "resource://gre/modules/ContentAnalysisUtils.sys.mjs",
|
||||
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
|
||||
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
|
||||
|
@ -552,6 +553,16 @@ export const GenAI = {
|
|||
}
|
||||
});
|
||||
|
||||
// For Content Analysis, we need to specify the URL that the data is being sent to.
|
||||
// In this case it's not the URL in the browsingContext (like it is in other cases),
|
||||
// but the URL of the chatProvider is close enough to where the content will eventually
|
||||
// be sent.
|
||||
lazy.ContentAnalysisUtils.setupContentAnalysisEventsForTextElement(
|
||||
textAreaEl,
|
||||
browser.browsingContext,
|
||||
Services.io.newURI(lazy.chatProvider)
|
||||
);
|
||||
|
||||
const resetHeight = () => {
|
||||
textAreaEl.style.height = "auto";
|
||||
textAreaEl.style.height = textAreaEl.scrollHeight + "px";
|
||||
|
|
|
@ -57,20 +57,81 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
> ul {
|
||||
font-size: var(--og-main-font-size);
|
||||
padding-inline-start: var(--space-large);
|
||||
}
|
||||
> ul {
|
||||
font-size: var(--og-main-font-size);
|
||||
line-height: 1.15; /* Design requires 18px line-height */
|
||||
list-style-type: square;
|
||||
padding-inline-start: var(--space-large);
|
||||
}
|
||||
|
||||
li {
|
||||
margin-block: var(--space-medium);
|
||||
li {
|
||||
margin-block: var(--space-medium);
|
||||
padding-inline-start: 5px;
|
||||
&::marker {
|
||||
color: var(--border-color-deemphasized);
|
||||
}
|
||||
}
|
||||
|
||||
> hr {
|
||||
border-color: var(--border-color-card);
|
||||
}
|
||||
> hr {
|
||||
border-color: var(--border-color-card);
|
||||
}
|
||||
|
||||
> p {
|
||||
margin-block: var(--space-medium) 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the animation for the loading state of link preview keypoints
|
||||
* Creates a smooth gradient animation that moves from right to left
|
||||
* to indicate content is being loaded
|
||||
*/
|
||||
@keyframes link-preview-keypoints-loading {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.keypoints-list {
|
||||
.content-item {
|
||||
margin-bottom: var(--space-xlarge);
|
||||
width: 100%;
|
||||
|
||||
&.loading {
|
||||
div {
|
||||
--skeleton-loader-background-color: var(--tab-group-suggestions-loading-animation-color-1);
|
||||
--skeleton-loader-motion-element-color: var(--tab-group-suggestions-loading-animation-color-2);
|
||||
animation: link-preview-keypoints-loading 3s infinite;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
var(--skeleton-loader-background-color) 30%,
|
||||
var(--skeleton-loader-motion-element-color) 50%,
|
||||
var(--skeleton-loader-background-color) 70%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
border-radius: 5px;
|
||||
height: var(--og-main-font-size);
|
||||
margin-bottom: 4px;
|
||||
width: 100%;
|
||||
/* Add non-impactful references to the CSS variables to satisfy the test browser_parsable_css */
|
||||
outline-color: var(--skeleton-loader-background-color);
|
||||
border-color: var(--skeleton-loader-motion-element-color);
|
||||
}
|
||||
|
||||
div:nth-of-type(1) {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
div:nth-of-type(2) {
|
||||
max-width: 98%;
|
||||
}
|
||||
|
||||
div:nth-of-type(3) {
|
||||
max-width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ class LinkPreviewCard extends MozLitElement {
|
|||
// Text for the link to visit the original URL when in error state
|
||||
static VISIT_LINK_TEXT = "Visit link";
|
||||
|
||||
// Number of placeholder rows to show when loading
|
||||
static PLACEHOLDER_COUNT = 3;
|
||||
|
||||
static properties = {
|
||||
generating: { type: Number }, // 0 = off, 1-4 = generating & dots state
|
||||
keyPoints: { type: Array },
|
||||
|
@ -160,13 +163,35 @@ class LinkPreviewCard extends MozLitElement {
|
|||
${this.generating || this.keyPoints.length
|
||||
? html`
|
||||
<div class="ai-content">
|
||||
<h3>
|
||||
${this.generating
|
||||
? "Generating key points" + ".".repeat(this.generating - 1)
|
||||
: "Key points"}
|
||||
</h3>
|
||||
<ul>
|
||||
${this.keyPoints.map(item => html`<li>${item}</li>`)}
|
||||
<h3>Key points</h3>
|
||||
<ul class="keypoints-list">
|
||||
${
|
||||
/* All populated content items */
|
||||
this.keyPoints.map(
|
||||
item => html`<li class="content-item">${item}</li>`
|
||||
)
|
||||
}
|
||||
${
|
||||
/* Loading placeholders with three divs each */
|
||||
this.generating
|
||||
? Array(
|
||||
Math.max(
|
||||
0,
|
||||
LinkPreviewCard.PLACEHOLDER_COUNT -
|
||||
this.keyPoints.length
|
||||
)
|
||||
)
|
||||
.fill()
|
||||
.map(
|
||||
() =>
|
||||
html` <li class="content-item loading">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</li>`
|
||||
)
|
||||
: []
|
||||
}
|
||||
</ul>
|
||||
${this.progress >= 0
|
||||
? html`
|
||||
|
|
|
@ -112,6 +112,7 @@ if CONFIG["MOZ_DEBUG"] or CONFIG["MOZ_DEV_EDITION"] or CONFIG["NIGHTLY_BUILD"]:
|
|||
BROWSER_CHROME_MANIFESTS += [
|
||||
"safebrowsing/content/test/browser.toml",
|
||||
"tests/browser/browser.toml",
|
||||
"tests/browser/eval/browser.toml",
|
||||
]
|
||||
|
||||
if CONFIG["MOZ_UPDATER"]:
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
# 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/.
|
||||
-->
|
||||
<?csp default-src chrome:; img-src chrome: moz-icon:; object-src 'none'; ?>
|
||||
|
||||
<!doctype html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src chrome:; img-src chrome: moz-icon:; style-src chrome: 'unsafe-inline'; object-src 'none';"
|
||||
/>
|
||||
<title>Interactions Debug Viewer</title>
|
||||
<script
|
||||
type="module"
|
||||
|
|
|
@ -1314,6 +1314,12 @@ var gMainPane = {
|
|||
"command",
|
||||
this.handleDeleteAll
|
||||
);
|
||||
|
||||
Services.obs.addObserver(this, "intl:app-locales-changed");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
Services.obs.removeObserver(this, "intl:app-locales-changed");
|
||||
}
|
||||
|
||||
handleInstallAll = async () => {
|
||||
|
@ -1397,6 +1403,7 @@ var gMainPane = {
|
|||
for (const { langTag, displayName } of this.state.languageList) {
|
||||
const hboxRow = document.createXULElement("hbox");
|
||||
hboxRow.classList.add("translations-manage-language");
|
||||
hboxRow.setAttribute("data-lang-tag", langTag);
|
||||
|
||||
const languageLabel = document.createXULElement("label");
|
||||
languageLabel.textContent = displayName; // The display name is already localized.
|
||||
|
@ -1558,11 +1565,41 @@ var gMainPane = {
|
|||
hideError() {
|
||||
this.elements.error.hidden = true;
|
||||
}
|
||||
|
||||
observe(_subject, topic, _data) {
|
||||
if (topic === "intl:app-locales-changed") {
|
||||
this.refreshLanguageListDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
refreshLanguageListDisplay() {
|
||||
try {
|
||||
const languageDisplayNames =
|
||||
TranslationsParent.createLanguageDisplayNames();
|
||||
|
||||
for (const row of this.elements.installList.children) {
|
||||
const rowLangTag = row.getAttribute("data-lang-tag");
|
||||
if (!rowLangTag) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = row.querySelector("label");
|
||||
if (label) {
|
||||
const newDisplayName = languageDisplayNames.of(rowLangTag);
|
||||
if (label.textContent !== newDisplayName) {
|
||||
label.textContent = newDisplayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TranslationsState.create().then(
|
||||
state => {
|
||||
new TranslationsView(state);
|
||||
this._translationsView = new TranslationsView(state);
|
||||
},
|
||||
error => {
|
||||
// This error can happen when a user is not connected to the internet, or
|
||||
|
@ -2736,6 +2773,13 @@ var gMainPane = {
|
|||
Services.prefs.removeObserver(PREF_CONTAINERS_EXTENSION, this);
|
||||
Services.obs.removeObserver(this, AUTO_UPDATE_CHANGED_TOPIC);
|
||||
Services.obs.removeObserver(this, BACKGROUND_UPDATE_CHANGED_TOPIC);
|
||||
|
||||
// Clean up the TranslationsView instance if it exists
|
||||
if (this._translationsView) {
|
||||
this._translationsView.destroy();
|
||||
this._translationsView = null;
|
||||
}
|
||||
|
||||
AppearanceChooser.destroy();
|
||||
},
|
||||
|
||||
|
|
|
@ -54,11 +54,12 @@ ChromeUtils.defineLazyGetter(lazy, "AboutLoginsL10n", () => {
|
|||
return new Localization(["branding/brand.ftl", "browser/aboutLogins.ftl"]);
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
lazy,
|
||||
"gParentalControlsService",
|
||||
"@mozilla.org/parental-controls-service;1",
|
||||
"nsIParentalControlsService"
|
||||
ChromeUtils.defineLazyGetter(lazy, "gParentalControlsService", () =>
|
||||
"@mozilla.org/parental-controls-service;1" in Cc
|
||||
? Cc["@mozilla.org/parental-controls-service;1"].getService(
|
||||
Ci.nsIParentalControlsService
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
|
@ -154,8 +155,6 @@ Preferences.addAll([
|
|||
{ id: "browser.urlbar.suggest.quicksuggest.nonsponsored", type: "bool" },
|
||||
{ id: "browser.urlbar.suggest.quicksuggest.sponsored", type: "bool" },
|
||||
{ id: "browser.urlbar.quicksuggest.dataCollection.enabled", type: "bool" },
|
||||
{ id: PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, type: "string" },
|
||||
{ id: PREF_URLBAR_WEATHER_USER_ENABLED, type: "bool" },
|
||||
|
||||
// History
|
||||
{ id: "places.history.enabled", type: "bool" },
|
||||
|
@ -709,7 +708,7 @@ var gPrivacyPane = {
|
|||
mode == Ci.nsIDNSService.MODE_TRRFIRST ||
|
||||
mode == Ci.nsIDNSService.MODE_TRRONLY
|
||||
) {
|
||||
if (lazy.gParentalControlsService.parentalControlsEnabled) {
|
||||
if (lazy.gParentalControlsService?.parentalControlsEnabled) {
|
||||
return "preferences-doh-status-not-active";
|
||||
}
|
||||
let confirmationState = Services.dns.currentTrrConfirmationState;
|
||||
|
@ -732,7 +731,7 @@ var gPrivacyPane = {
|
|||
if (
|
||||
(mode == Ci.nsIDNSService.MODE_TRRFIRST ||
|
||||
mode == Ci.nsIDNSService.MODE_TRRONLY) &&
|
||||
lazy.gParentalControlsService.parentalControlsEnabled
|
||||
lazy.gParentalControlsService?.parentalControlsEnabled
|
||||
) {
|
||||
errReason = Services.dns.getTRRSkipReasonName(
|
||||
Ci.nsITRRSkipReason.TRR_PARENTAL_CONTROL
|
||||
|
|
|
@ -14,10 +14,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
UserSearchEngine: "resource://gre/modules/UserSearchEngine.sys.mjs",
|
||||
});
|
||||
|
||||
const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST =
|
||||
"browser.urlbar.quicksuggest.blockedDigests";
|
||||
const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather";
|
||||
|
||||
Preferences.addAll([
|
||||
{ id: "browser.search.suggest.enabled", type: "bool" },
|
||||
{ id: "browser.urlbar.suggest.searches", type: "bool" },
|
||||
|
@ -74,9 +70,11 @@ var gSearchPane = {
|
|||
|
||||
Services.obs.addObserver(this, "browser-search-engine-modified");
|
||||
Services.obs.addObserver(this, "intl:app-locales-changed");
|
||||
Services.obs.addObserver(this, "quicksuggest-dismissals-changed");
|
||||
window.addEventListener("unload", () => {
|
||||
Services.obs.removeObserver(this, "browser-search-engine-modified");
|
||||
Services.obs.removeObserver(this, "intl:app-locales-changed");
|
||||
Services.obs.removeObserver(this, "quicksuggest-dismissals-changed");
|
||||
});
|
||||
|
||||
let suggestsPref = Preferences.get("browser.search.suggest.enabled");
|
||||
|
@ -365,14 +363,8 @@ var gSearchPane = {
|
|||
QuickSuggest.SETTINGS_UI.FULL;
|
||||
|
||||
this._updateDismissedSuggestionsStatus();
|
||||
Preferences.get(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST).on("change", () =>
|
||||
this._updateDismissedSuggestionsStatus()
|
||||
);
|
||||
Preferences.get(PREF_URLBAR_WEATHER_USER_ENABLED).on("change", () =>
|
||||
this._updateDismissedSuggestionsStatus()
|
||||
);
|
||||
setEventListener("restoreDismissedSuggestions", "command", () =>
|
||||
this.restoreDismissedSuggestions()
|
||||
QuickSuggest.clearDismissedSuggestions()
|
||||
);
|
||||
|
||||
container.hidden = false;
|
||||
|
@ -414,21 +406,9 @@ var gSearchPane = {
|
|||
* Enables/disables the "Restore" button for dismissed Firefox Suggest
|
||||
* suggestions.
|
||||
*/
|
||||
_updateDismissedSuggestionsStatus() {
|
||||
async _updateDismissedSuggestionsStatus() {
|
||||
document.getElementById("restoreDismissedSuggestions").disabled =
|
||||
!Services.prefs.prefHasUserValue(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST) &&
|
||||
!(
|
||||
Services.prefs.prefHasUserValue(PREF_URLBAR_WEATHER_USER_ENABLED) &&
|
||||
!Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores Firefox Suggest suggestions dismissed by the user.
|
||||
*/
|
||||
restoreDismissedSuggestions() {
|
||||
Services.prefs.clearUserPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST);
|
||||
Services.prefs.clearUserPref(PREF_URLBAR_WEATHER_USER_ENABLED);
|
||||
!(await QuickSuggest.canClearDismissedSuggestions());
|
||||
},
|
||||
|
||||
handleEvent(aEvent) {
|
||||
|
@ -481,7 +461,11 @@ var gSearchPane = {
|
|||
default:
|
||||
this._engineStore.browserSearchEngineModified(engine, data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "quicksuggest-dismissals-changed":
|
||||
this._updateDismissedSuggestionsStatus();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -13,9 +13,6 @@ const CONTAINER_ID = "firefoxSuggestContainer";
|
|||
const DATA_COLLECTION_TOGGLE_ID = "firefoxSuggestDataCollectionSearchToggle";
|
||||
const LEARN_MORE_ID = "firefoxSuggestLearnMore";
|
||||
const BUTTON_RESTORE_DISMISSED_ID = "restoreDismissedSuggestions";
|
||||
const PREF_URLBAR_QUICKSUGGEST_BLOCKLIST =
|
||||
"browser.urlbar.quicksuggest.blockedDigests";
|
||||
const PREF_URLBAR_WEATHER_USER_ENABLED = "browser.urlbar.suggest.weather";
|
||||
|
||||
// Maps `SETTINGS_UI` values to expected visibility state objects. See
|
||||
// `assertSuggestVisibility()` in `head.js` for info on the state objects.
|
||||
|
@ -202,6 +199,11 @@ add_task(async function initiallyEnabled_settingsUiOfflineOnly() {
|
|||
|
||||
// Tests the "Restore" button for dismissed suggestions.
|
||||
add_task(async function restoreDismissedSuggestions() {
|
||||
Assert.ok(
|
||||
!(await QuickSuggest.canClearDismissedSuggestions()),
|
||||
"Sanity check: This test expects canClearDismissedSuggestions to return false initially"
|
||||
);
|
||||
|
||||
await openPreferencesViaOpenPreferencesAPI("search", { leaveOpen: true });
|
||||
|
||||
let doc = gBrowser.selectedBrowser.contentDocument;
|
||||
|
@ -209,47 +211,29 @@ add_task(async function restoreDismissedSuggestions() {
|
|||
addressBarSection.scrollIntoView();
|
||||
|
||||
let button = doc.getElementById(BUTTON_RESTORE_DISMISSED_ID);
|
||||
Assert.equal(
|
||||
Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
|
||||
"",
|
||||
"Block list is empty initially"
|
||||
);
|
||||
Assert.ok(
|
||||
Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED),
|
||||
"Weather suggestions are enabled initially"
|
||||
);
|
||||
Assert.ok(button.disabled, "Restore button is disabled initially.");
|
||||
|
||||
await QuickSuggest.blockedSuggestions.add("https://example.com/");
|
||||
Assert.notEqual(
|
||||
Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
|
||||
"",
|
||||
"Block list is non-empty after adding URL"
|
||||
|
||||
Assert.ok(
|
||||
await QuickSuggest.canClearDismissedSuggestions(),
|
||||
"canClearDismissedSuggestions should return true after dismissing a suggestion"
|
||||
);
|
||||
Assert.ok(!button.disabled, "Restore button is enabled after blocking URL.");
|
||||
|
||||
let clearPromise = TestUtils.topicObserved("quicksuggest-dismissals-cleared");
|
||||
button.click();
|
||||
Assert.equal(
|
||||
Services.prefs.getStringPref(PREF_URLBAR_QUICKSUGGEST_BLOCKLIST, ""),
|
||||
"",
|
||||
"Block list is empty clicking Restore button"
|
||||
await clearPromise;
|
||||
|
||||
Assert.ok(
|
||||
await QuickSuggest.blockedSuggestions.isEmpty(),
|
||||
"blockedSuggestions.isEmpty() should return true after restoring dismissals"
|
||||
);
|
||||
Assert.ok(
|
||||
!(await QuickSuggest.canClearDismissedSuggestions()),
|
||||
"canClearDismissedSuggestions should return false after restoring dismissals"
|
||||
);
|
||||
Assert.ok(button.disabled, "Restore button is disabled after clicking it.");
|
||||
|
||||
Services.prefs.setBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED, false);
|
||||
Assert.ok(
|
||||
!button.disabled,
|
||||
"Restore button is enabled after disabling weather suggestions."
|
||||
);
|
||||
button.click();
|
||||
Assert.ok(
|
||||
Services.prefs.getBoolPref(PREF_URLBAR_WEATHER_USER_ENABLED),
|
||||
"Weather suggestions are enabled after clicking Restore button"
|
||||
);
|
||||
Assert.ok(
|
||||
button.disabled,
|
||||
"Restore button is disabled after clicking it again."
|
||||
);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ClientID: "resource://gre/modules/ClientID.sys.mjs",
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
|
||||
});
|
||||
|
||||
|
@ -231,12 +230,10 @@ add_task(async function testReactivateProfileGroupID() {
|
|||
"upload should be disabled after unchecking checkbox"
|
||||
);
|
||||
|
||||
// TODO: what could we explicitly await, rather than resorting to a timeout?
|
||||
await new Promise(resolve => lazy.setTimeout(resolve, 1000));
|
||||
|
||||
Assert.equal(
|
||||
Services.prefs.getStringPref("toolkit.telemetry.cachedProfileGroupID"),
|
||||
lazy.TelemetryUtils.knownProfileGroupID,
|
||||
await TestUtils.waitForCondition(
|
||||
() =>
|
||||
Services.prefs.getStringPref("toolkit.telemetry.cachedProfileGroupID") ===
|
||||
lazy.TelemetryUtils.knownProfileGroupID,
|
||||
"after disabling data collection, the profile group ID pref should have the canary value"
|
||||
);
|
||||
|
||||
|
|
|
@ -62,6 +62,9 @@ export var SearchUIUtils = {
|
|||
case "search-engine-removal":
|
||||
this.removalOfSearchEngineNotificationBox(...args);
|
||||
break;
|
||||
case "search-settings-reset":
|
||||
this.searchSettingsResetNotificationBox(...args);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -123,6 +126,45 @@ export var SearchUIUtils = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Infobar informing the user that the search settings had to be reset
|
||||
* and what their new default engine is.
|
||||
*
|
||||
* @param {string} newEngine
|
||||
* Name of the new default engine.
|
||||
*/
|
||||
async searchSettingsResetNotificationBox(newEngine) {
|
||||
let win = lazy.BrowserWindowTracker.getTopWindow();
|
||||
|
||||
let buttons = [
|
||||
{
|
||||
"l10n-id": "reset-search-settings-button",
|
||||
primary: true,
|
||||
callback() {
|
||||
const notificationBox = win.gNotificationBox.getNotificationWithValue(
|
||||
"search-settings-reset"
|
||||
);
|
||||
win.gNotificationBox.removeNotification(notificationBox);
|
||||
},
|
||||
},
|
||||
{
|
||||
supportPage: "prefs-search",
|
||||
},
|
||||
];
|
||||
|
||||
await win.gNotificationBox.appendNotification(
|
||||
"search-settings-reset",
|
||||
{
|
||||
label: {
|
||||
"l10n-id": "reset-search-settings-message",
|
||||
"l10n-args": { newEngine },
|
||||
},
|
||||
priority: win.gNotificationBox.PRIORITY_SYSTEM,
|
||||
},
|
||||
buttons
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an open search engine and handles error UI.
|
||||
*
|
||||
|
|
|
@ -36,7 +36,8 @@ serp-categorization:
|
|||
send_if_empty: false
|
||||
metadata:
|
||||
include_info_sections: false
|
||||
use_ohttp: true
|
||||
uploader_capabilities:
|
||||
- ohttp
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1868476
|
||||
data_reviews:
|
||||
|
|
|
@ -27,3 +27,27 @@ add_task(async function test_removalMessage() {
|
|||
|
||||
notificationBox.close();
|
||||
});
|
||||
|
||||
add_task(async function test_resetMessage() {
|
||||
Assert.ok(
|
||||
!gNotificationBox.getNotificationWithValue("search-settings-reset"),
|
||||
"Message is not displayed initially."
|
||||
);
|
||||
|
||||
BrowserUtils.callModulesFromCategory(
|
||||
{ categoryName: "search-service-notification" },
|
||||
"search-settings-reset",
|
||||
"Engine 1"
|
||||
);
|
||||
|
||||
await TestUtils.waitForCondition(
|
||||
() => gNotificationBox.getNotificationWithValue("search-settings-reset"),
|
||||
"Waiting for message to be displayed"
|
||||
);
|
||||
let notificationBox = gNotificationBox.getNotificationWithValue(
|
||||
"search-settings-reset"
|
||||
);
|
||||
Assert.ok(notificationBox, "Message is displayed.");
|
||||
|
||||
notificationBox.close();
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ const NOTIFY_SINGLE_WINDOW_RESTORED = "sessionstore-single-window-restored";
|
|||
const NOTIFY_WINDOWS_RESTORED = "sessionstore-windows-restored";
|
||||
const NOTIFY_BROWSER_STATE_RESTORED = "sessionstore-browser-state-restored";
|
||||
const NOTIFY_LAST_SESSION_CLEARED = "sessionstore-last-session-cleared";
|
||||
const NOTIFY_LAST_SESSION_RE_ENABLED = "sessionstore-last-session-re-enable";
|
||||
const NOTIFY_RESTORING_ON_STARTUP = "sessionstore-restoring-on-startup";
|
||||
const NOTIFY_INITIATING_MANUAL_RESTORE =
|
||||
"sessionstore-initiating-manual-restore";
|
||||
|
@ -157,6 +158,7 @@ const kLastIndex = Number.MAX_SAFE_INTEGER - 1;
|
|||
|
||||
import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
|
||||
|
||||
import { TabMetrics } from "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs";
|
||||
import { TelemetryTimestamps } from "resource://gre/modules/TelemetryTimestamps.sys.mjs";
|
||||
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
|
@ -174,6 +176,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||
DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
|
||||
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
||||
HomePage: "resource:///modules/HomePage.sys.mjs",
|
||||
PrivacyFilter: "resource://gre/modules/sessionstore/PrivacyFilter.sys.mjs",
|
||||
sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
|
||||
RunState: "resource:///modules/sessionstore/RunState.sys.mjs",
|
||||
SessionCookies: "resource:///modules/sessionstore/SessionCookies.sys.mjs",
|
||||
|
@ -249,6 +252,10 @@ export var SessionStore = {
|
|||
return SessionStoreInternal.willAutoRestore;
|
||||
},
|
||||
|
||||
get shouldRestoreLastSession() {
|
||||
return SessionStoreInternal._shouldRestoreLastSession;
|
||||
},
|
||||
|
||||
init: function ss_init() {
|
||||
SessionStoreInternal.init();
|
||||
},
|
||||
|
@ -889,7 +896,21 @@ export var SessionStore = {
|
|||
* @returns {MozTabbrowserTabGroup}
|
||||
* a reference to the restored tab group in a browser window.
|
||||
*/
|
||||
openSavedTabGroup(tabGroupId, targetWindow) {
|
||||
openSavedTabGroup(
|
||||
tabGroupId,
|
||||
targetWindow,
|
||||
{ source = TabMetrics.METRIC_SOURCE.UNKNOWN } = {}
|
||||
) {
|
||||
let isVerticalMode = targetWindow.gBrowser.tabContainer.verticalMode;
|
||||
Glean.tabgroup.reopen.record({
|
||||
id: tabGroupId,
|
||||
source,
|
||||
layout: isVerticalMode
|
||||
? TabMetrics.METRIC_TABS_LAYOUT.VERTICAL
|
||||
: TabMetrics.METRIC_TABS_LAYOUT.HORIZONTAL,
|
||||
type: TabMetrics.METRIC_REOPEN_TYPE.SAVED,
|
||||
});
|
||||
|
||||
return SessionStoreInternal.openSavedTabGroup(tabGroupId, targetWindow);
|
||||
},
|
||||
|
||||
|
@ -1008,6 +1029,15 @@ var SessionStoreInternal = {
|
|||
// whether the last window was closed and should be restored
|
||||
_restoreLastWindow: false,
|
||||
|
||||
// whether we should restore last session on the next launch
|
||||
// of a regular Firefox window. This scenario is triggered
|
||||
// when a user closes all regular Firefox windows but the session is not over
|
||||
_shouldRestoreLastSession: false,
|
||||
|
||||
// whether we will potentially be restoring the session
|
||||
// more than once without Firefox restarting in between
|
||||
_restoreWithoutRestart: false,
|
||||
|
||||
// number of tabs currently restoring
|
||||
_tabsRestoringCount: 0,
|
||||
|
||||
|
@ -1937,6 +1967,10 @@ var SessionStoreInternal = {
|
|||
this._windows[aWindow.__SSi].isPopup = true;
|
||||
}
|
||||
|
||||
if (aWindow.document.documentElement.hasAttribute("taskbartab")) {
|
||||
this._windows[aWindow.__SSi].isTaskbarTab = true;
|
||||
}
|
||||
|
||||
let tabbrowser = aWindow.gBrowser;
|
||||
|
||||
// add tab change listeners to all already existing tabs
|
||||
|
@ -1966,6 +2000,10 @@ var SessionStoreInternal = {
|
|||
*/
|
||||
initializeWindow(aWindow, aInitialState = null) {
|
||||
let isPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(aWindow);
|
||||
let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
|
||||
// A regular window is not a private window, taskbar tab window, or popup window
|
||||
let isRegularWindow =
|
||||
!isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible;
|
||||
|
||||
// perform additional initialization when the first window is loading
|
||||
if (lazy.RunState.isStopped) {
|
||||
|
@ -2111,6 +2149,24 @@ var SessionStoreInternal = {
|
|||
// we actually restored the session just now.
|
||||
this._prefBranch.setBoolPref("sessionstore.resume_session_once", false);
|
||||
}
|
||||
// This is a taskbar-tab specific scenario. If an user closes
|
||||
// all regular Firefox windows except for taskbar tabs and has
|
||||
// auto restore on startup enabled, _shouldRestoreLastSession
|
||||
// will be set to true. We should then restore when a
|
||||
// regular Firefox window is opened.
|
||||
else if (
|
||||
Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) &&
|
||||
this._shouldRestoreLastSession &&
|
||||
isRegularWindow
|
||||
) {
|
||||
let lastSessionState = LastSession.getState();
|
||||
this._globalState.setFromState(lastSessionState);
|
||||
lazy.SessionCookies.restore(lastSessionState.cookies || []);
|
||||
this.restoreWindows(aWindow, lastSessionState, {
|
||||
firstWindow: true,
|
||||
});
|
||||
this._shouldRestoreLastSession = false;
|
||||
}
|
||||
|
||||
if (this._restoreLastWindow && aWindow.toolbar.visible) {
|
||||
// always reset (if not a popup window)
|
||||
|
@ -2302,6 +2358,43 @@ var SessionStoreInternal = {
|
|||
// we explicitly allow saving an "empty" window state.
|
||||
let isLastWindow = this.isLastRestorableWindow();
|
||||
|
||||
let isLastRegularWindow =
|
||||
Object.values(this._windows).filter(
|
||||
wData => !wData.isPrivate && !wData.isTaskbarTab
|
||||
).length == 1;
|
||||
|
||||
let taskbarTabsRemains = Object.values(this._windows).some(
|
||||
wData => wData.isTaskbarTab
|
||||
);
|
||||
|
||||
// Closing the last regular Firefox window with
|
||||
// at least one taskbar tab window still active.
|
||||
// The session is considered over and we need to restore
|
||||
// the next time a non-private, non-taskbar-tab window
|
||||
// is opened.
|
||||
if (
|
||||
Services.prefs.getBoolPref("browser.taskbarTabs.enabled", false) &&
|
||||
isLastRegularWindow &&
|
||||
!winData.isTaskbarTab &&
|
||||
!winData.isPrivate &&
|
||||
taskbarTabsRemains
|
||||
) {
|
||||
// If the setting is enabled, Firefox should auto-restore
|
||||
// the next time a regular window is opened
|
||||
if (this.willAutoRestore) {
|
||||
this._shouldRestoreLastSession = true;
|
||||
// Otherwise, we want "restore last session" button
|
||||
// to be avaliable in the hamburger menu
|
||||
} else {
|
||||
Services.obs.notifyObservers(null, NOTIFY_LAST_SESSION_RE_ENABLED);
|
||||
}
|
||||
|
||||
let savedState = this.getCurrentState(true);
|
||||
lazy.PrivacyFilter.filterPrivateWindowsAndTabs(savedState);
|
||||
LastSession.setState(savedState);
|
||||
this._restoreWithoutRestart = true;
|
||||
}
|
||||
|
||||
// clear this window from the list, since it has definitely been closed.
|
||||
delete this._windows[aWindow.__SSi];
|
||||
|
||||
|
@ -2321,7 +2414,7 @@ var SessionStoreInternal = {
|
|||
// 2) Flush the window.
|
||||
// 3) When the flush is complete, revisit our decision to store the window
|
||||
// in _closedWindows, and add/remove as necessary.
|
||||
if (!winData.isPrivate) {
|
||||
if (!winData.isPrivate && !winData.isTaskbarTab) {
|
||||
this.maybeSaveClosedWindow(winData, isLastWindow);
|
||||
}
|
||||
|
||||
|
@ -2342,7 +2435,7 @@ var SessionStoreInternal = {
|
|||
|
||||
// Save non-private windows if they have at
|
||||
// least one saveable tab or are the last window.
|
||||
if (!winData.isPrivate) {
|
||||
if (!winData.isPrivate && !winData.isTaskbarTab) {
|
||||
this.maybeSaveClosedWindow(winData, isLastWindow);
|
||||
|
||||
if (!isLastWindow && winData.closedId > -1) {
|
||||
|
@ -4941,6 +5034,19 @@ var SessionStoreInternal = {
|
|||
// Restore into windows or open new ones as needed.
|
||||
for (let i = 0; i < lastSessionState.windows.length; i++) {
|
||||
let winState = lastSessionState.windows[i];
|
||||
|
||||
// If we're restoring multiple times without
|
||||
// Firefox restarting, we need to remove
|
||||
// the window being restored from "previously closed windows"
|
||||
if (this._restoreWithoutRestart) {
|
||||
let restoreIndex = this._closedWindows.findIndex(win => {
|
||||
return win.closedId == winState.closedId;
|
||||
});
|
||||
if (restoreIndex > -1) {
|
||||
this._closedWindows.splice(restoreIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
let lastSessionWindowID = winState.__lastSessionWindowID;
|
||||
// delete lastSessionWindowID so we don't add that to the window again
|
||||
delete winState.__lastSessionWindowID;
|
||||
|
@ -4990,6 +5096,10 @@ var SessionStoreInternal = {
|
|||
this._restoreWindowsInReversedZOrder(openWindows.concat(openedWindows))
|
||||
);
|
||||
|
||||
if (this._restoreWithoutRestart) {
|
||||
this.removeDuplicateClosedWindows(lastSessionState);
|
||||
}
|
||||
|
||||
// Merge closed windows from this session with ones from last session
|
||||
if (lastSessionState._closedWindows) {
|
||||
// reset window closedIds and any references to them from closed tabs
|
||||
|
@ -5033,6 +5143,26 @@ var SessionStoreInternal = {
|
|||
this._notifyOfClosedObjectsChange();
|
||||
},
|
||||
|
||||
/**
|
||||
* There might be duplicates in these two arrays if we
|
||||
* restore multiple times without restarting in between.
|
||||
* We will keep the contents of the more recent _closedWindows array
|
||||
*
|
||||
* @param lastSessionState
|
||||
* An object containing information about the previous browsing session
|
||||
*/
|
||||
removeDuplicateClosedWindows(lastSessionState) {
|
||||
// A set of closedIDs for the most recent list of closed windows
|
||||
let currentClosedIds = new Set(
|
||||
this._closedWindows.map(window => window.closedId)
|
||||
);
|
||||
|
||||
// Remove closed windows that are present in both current and last session
|
||||
lastSessionState._closedWindows = lastSessionState._closedWindows.filter(
|
||||
win => !currentClosedIds.has(win.closedId)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Revive a crashed tab and restore its state from before it crashed.
|
||||
*
|
||||
|
@ -5264,7 +5394,7 @@ var SessionStoreInternal = {
|
|||
|
||||
// collect the data for all windows
|
||||
for (ix in this._windows) {
|
||||
if (this._windows[ix]._restoring) {
|
||||
if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
|
||||
// window data is still in _statesToRestore
|
||||
continue;
|
||||
}
|
||||
|
@ -6946,6 +7076,9 @@ var SessionStoreInternal = {
|
|||
* @returns {boolean} true if the group is saveable.
|
||||
*/
|
||||
shouldSaveTabGroup: function ssi_shouldSaveTabGroup(group) {
|
||||
if (!group) {
|
||||
return false;
|
||||
}
|
||||
for (let tab of group.tabs) {
|
||||
let tabState = lazy.TabState.collect(tab);
|
||||
if (this._shouldSaveTabState(tabState)) {
|
||||
|
|
|
@ -109,11 +109,9 @@ export var StartupPerformance = {
|
|||
delta
|
||||
);
|
||||
} else {
|
||||
Services.telemetry
|
||||
.getHistogramById(
|
||||
"FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS"
|
||||
)
|
||||
.add(delta);
|
||||
Glean.sessionRestore.manualRestoreDurationUntilEagerTabsRestored.accumulateSingleSample(
|
||||
delta
|
||||
);
|
||||
}
|
||||
Glean.sessionRestore.numberOfEagerTabsRestored.accumulateSingleSample(
|
||||
this._totalNumberOfEagerTabs
|
||||
|
|
|
@ -27,3 +27,9 @@ skip-if = [
|
|||
"os == 'win' && os_version == '11.26100' && processor == 'x86'", # Bug 1727691
|
||||
"os == 'win' && os_version == '11.26100' && processor == 'x86_64'", # Bug 1727691
|
||||
]
|
||||
|
||||
["test_taskbartab_restore.py"]
|
||||
run-if = ["os == 'win'"]
|
||||
|
||||
["test_taskbartab_sessionstate.py"]
|
||||
run-if = ["os == 'win'"]
|
||||
|
|
|
@ -43,6 +43,7 @@ class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase):
|
|||
no_auto_updates=True,
|
||||
win_register_restart=False,
|
||||
test_windows=DEFAULT_WINDOWS,
|
||||
taskbartabs_enable=False,
|
||||
):
|
||||
super(SessionStoreTestCase, self).setUp()
|
||||
self.marionette.set_context("chrome")
|
||||
|
@ -79,6 +80,8 @@ class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase):
|
|||
"browser.sessionstore.debug.no_auto_updates": no_auto_updates,
|
||||
# Whether to enable the register application restart mechanism.
|
||||
"toolkit.winRegisterApplicationRestart": win_register_restart,
|
||||
# Whether to enable taskbar tabs for this test
|
||||
"browser.taskbarTabs.enabled": taskbartabs_enable,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -138,6 +141,47 @@ class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase):
|
|||
self.marionette.switch_to_window(win)
|
||||
self.open_tabs(win, urls)
|
||||
|
||||
# Open a Firefox web app (taskbar tab) window
|
||||
def open_taskbartab_window(self):
|
||||
self.marionette.execute_async_script(
|
||||
"""
|
||||
let [resolve] = arguments;
|
||||
(async () => {
|
||||
let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
|
||||
Ci.nsIWritablePropertyBag2
|
||||
);
|
||||
extraOptions.setPropertyAsBool("taskbartab", true);
|
||||
|
||||
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
||||
args.appendElement(null);
|
||||
args.appendElement(extraOptions);
|
||||
args.appendElement(null);
|
||||
|
||||
// Simulate opening a taskbar tab window
|
||||
let win = Services.ww.openWindow(
|
||||
null,
|
||||
AppConstants.BROWSER_CHROME_URL,
|
||||
"_blank",
|
||||
"chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars",
|
||||
args
|
||||
);
|
||||
await new Promise(resolve => {
|
||||
win.addEventListener("load", resolve, { once: true });
|
||||
});
|
||||
await win.delayedStartupPromise;
|
||||
})().then(resolve);
|
||||
"""
|
||||
)
|
||||
|
||||
# Helper function for taskbar tabs tests, opens a taskbar tab window,
|
||||
# closes the regular window, and reopens another regular window.
|
||||
# Firefox will then be in a "ready to restore" state
|
||||
def setup_taskbartab_restore_scenario(self):
|
||||
self.open_taskbartab_window()
|
||||
taskbar_tab_window_handle = self.marionette.close_chrome_window()[0]
|
||||
self.marionette.switch_to_window(taskbar_tab_window_handle)
|
||||
self.marionette.open(type="window")
|
||||
|
||||
def open_tabs(self, win, urls):
|
||||
"""Open a set of URLs inside a window in new tabs.
|
||||
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
# 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 os
|
||||
import sys
|
||||
|
||||
from marionette_driver import Wait
|
||||
|
||||
# add this directory to the path
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
from session_store_test_case import SessionStoreTestCase
|
||||
|
||||
|
||||
def inline(title):
|
||||
return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
|
||||
title
|
||||
)
|
||||
|
||||
|
||||
class TestManualRestoreWithTaskbarTabs(SessionStoreTestCase):
|
||||
def setUp(self):
|
||||
super(TestManualRestoreWithTaskbarTabs, self).setUp(
|
||||
startup_page=1,
|
||||
include_private=False,
|
||||
restore_on_demand=False,
|
||||
taskbartabs_enable=True,
|
||||
test_windows=set(
|
||||
[
|
||||
# Window 1
|
||||
(
|
||||
inline("lorem ipsom"),
|
||||
inline("dolor"),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
"""
|
||||
Close all regular windows except for a taskbar tab window. The
|
||||
session should be over at this point. Opening another regular Firefox
|
||||
window will open "restore previous session" in the hamburger menu.
|
||||
And clicking it will restore the correct session.
|
||||
"""
|
||||
|
||||
def test_restore_without_closing_taskbartab(self):
|
||||
self.wait_for_windows(
|
||||
self.all_windows, "Not all requested windows have been opened"
|
||||
)
|
||||
|
||||
# See session_store_test_case.py
|
||||
self.setup_taskbartab_restore_scenario()
|
||||
|
||||
# Verify that "restore previous session" button
|
||||
# is visible in the hamburger menu
|
||||
self.assertEqual(
|
||||
self.marionette.execute_script(
|
||||
"""
|
||||
let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false });
|
||||
return PanelMultiView.getViewNode(
|
||||
newWindow.document,
|
||||
"appMenu-restoreSession"
|
||||
).hasAttribute("disabled");
|
||||
"""
|
||||
),
|
||||
False,
|
||||
"The restore last session button should be visible",
|
||||
)
|
||||
|
||||
# Simulate clicking "restore previous session"
|
||||
self.marionette.execute_script(
|
||||
"""
|
||||
SessionStore.restoreLastSession();
|
||||
"""
|
||||
)
|
||||
|
||||
# Wait for the restore to be completed,
|
||||
# meaning the window we opened should have
|
||||
# two tabs again.
|
||||
Wait(self.marionette).until(
|
||||
lambda mn: mn.execute_script(
|
||||
"""
|
||||
let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false });
|
||||
return newWindow.gBrowser.tabs.length;
|
||||
"""
|
||||
)
|
||||
== 2
|
||||
)
|
||||
|
||||
|
||||
class TestAutoRestoreWithTaskbarTabs(SessionStoreTestCase):
|
||||
def setUp(self):
|
||||
super(TestAutoRestoreWithTaskbarTabs, self).setUp(
|
||||
startup_page=3,
|
||||
include_private=False,
|
||||
restore_on_demand=False,
|
||||
taskbartabs_enable=True,
|
||||
test_windows=set(
|
||||
[
|
||||
# Window 1
|
||||
(
|
||||
inline("lorem ipsom"),
|
||||
inline("dolor"),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
"""
|
||||
Close all regular windows except for a taskbar tab window. The
|
||||
session should be over at this point. Opening another regular Firefox
|
||||
window will open automatically restore the correct session
|
||||
"""
|
||||
|
||||
def test_restore_without_closing_taskbartab(self):
|
||||
self.wait_for_windows(
|
||||
self.all_windows, "Not all requested windows have been opened"
|
||||
)
|
||||
|
||||
self.setup_taskbartab_restore_scenario()
|
||||
|
||||
# Wait for the auto restore to be completed,
|
||||
# meaning the window we opened should have
|
||||
# the original two tabs plus the home page tab.
|
||||
Wait(self.marionette).until(
|
||||
lambda mn: mn.execute_script(
|
||||
"""
|
||||
let newWindow = BrowserWindowTracker.getTopWindow({ allowTaskbarTabs: false });
|
||||
return newWindow.gBrowser.tabs.length;
|
||||
"""
|
||||
)
|
||||
== 3
|
||||
)
|
|
@ -0,0 +1,91 @@
|
|||
# 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 os
|
||||
import sys
|
||||
|
||||
# add this directory to the path
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
from session_store_test_case import SessionStoreTestCase
|
||||
|
||||
|
||||
def inline(title):
|
||||
return "data:text/html;charset=utf-8,<html><head><title>{}</title></head><body></body></html>".format(
|
||||
title
|
||||
)
|
||||
|
||||
|
||||
class TestTaskbarTabSessionState(SessionStoreTestCase):
|
||||
def setUp(self):
|
||||
super(TestTaskbarTabSessionState, self).setUp(
|
||||
startup_page=1,
|
||||
include_private=False,
|
||||
restore_on_demand=False,
|
||||
taskbartabs_enable=True,
|
||||
test_windows=set(
|
||||
[
|
||||
# Window 1
|
||||
(
|
||||
inline("lorem ipsom"),
|
||||
inline("dolor"),
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
"""
|
||||
Close all Firefox windows with the web app being closed last,
|
||||
the session store state should include the last regular window
|
||||
that's closed, but not the web app
|
||||
"""
|
||||
|
||||
def test_taskbartab_session_state(self):
|
||||
self.wait_for_windows(
|
||||
self.all_windows, "Not all requested windows have been opened"
|
||||
)
|
||||
|
||||
self.marionette.enforce_gecko_prefs({"browser.taskbarTabs.enabled": True})
|
||||
|
||||
self.open_taskbartab_window()
|
||||
|
||||
# Close the original regular Firefox window
|
||||
taskbar_tab_window_handle = self.marionette.close_chrome_window()[0]
|
||||
self.marionette.switch_to_window(taskbar_tab_window_handle)
|
||||
|
||||
self.marionette.set_context("content")
|
||||
dummy_html = self.marionette.absolute_url("empty.html")
|
||||
self.marionette.navigate(dummy_html)
|
||||
self.marionette.set_context("chrome")
|
||||
|
||||
self.marionette.quit()
|
||||
self.marionette.start_session()
|
||||
self.marionette.set_context("chrome")
|
||||
|
||||
# check for the session store state, it should have only two tabs and one window
|
||||
self.assertEqual(
|
||||
self.marionette.execute_script(
|
||||
"""
|
||||
const { _LastSession } = ChromeUtils.importESModule(
|
||||
"resource:///modules/sessionstore/SessionStore.sys.mjs"
|
||||
);
|
||||
return _LastSession.getState().windows.length
|
||||
"""
|
||||
),
|
||||
1,
|
||||
"One window should be in the session state",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.marionette.execute_script(
|
||||
"""
|
||||
const { _LastSession } = ChromeUtils.importESModule(
|
||||
"resource:///modules/sessionstore/SessionStore.sys.mjs"
|
||||
);
|
||||
return _LastSession.getState().windows[0].tabs.length
|
||||
"""
|
||||
),
|
||||
2,
|
||||
"Two tabs should be in the session state",
|
||||
)
|
|
@ -1753,7 +1753,7 @@ var SidebarController = {
|
|||
* hiding of the sidebar.
|
||||
* @param {boolean} options.dismissPanel -Only close the panel or close the whole sidebar (the default.)
|
||||
*/
|
||||
hide({ triggerNode, dismissPanel = true } = {}) {
|
||||
hide({ triggerNode, dismissPanel = this.sidebarRevampEnabled } = {}) {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
@ -1910,6 +1910,14 @@ var SidebarController = {
|
|||
// Re-render sidebar-main so that templating is updated
|
||||
// for proper keyboard navigation for Tools
|
||||
this.sidebarMain.requestUpdate();
|
||||
if (
|
||||
!this.verticalTabsEnabled &&
|
||||
this.sidebarRevampVisibility == "hide-sidebar"
|
||||
) {
|
||||
// the sidebar.visibility pref didn't change so updateVisbility hasn't
|
||||
// been called; we need to call it here to un-expand the launcher
|
||||
this._state.updateVisibility(undefined, false);
|
||||
}
|
||||
},
|
||||
|
||||
debouncedMouseEnter() {
|
||||
|
|
|
@ -15,3 +15,8 @@ fxview-search-textbox {
|
|||
.menu-button::part(button) {
|
||||
margin-inline-start: var(--space-small);
|
||||
}
|
||||
|
||||
.nested-card {
|
||||
--card-accordion-closed-icon: url("chrome://global/skin/icons/arrow-right.svg");
|
||||
--card-accordion-open-icon: url("chrome://global/skin/icons/arrow-down.svg");
|
||||
}
|
||||
|
|
|
@ -45,6 +45,12 @@ export class SidebarHistory extends SidebarPage {
|
|||
this._menu = doc.getElementById("sidebar-history-menu");
|
||||
this._menuSortByDate = doc.getElementById("sidebar-history-sort-by-date");
|
||||
this._menuSortBySite = doc.getElementById("sidebar-history-sort-by-site");
|
||||
this._menuSortByDateSite = doc.getElementById(
|
||||
"sidebar-history-sort-by-date-and-site"
|
||||
);
|
||||
this._menuSortByLastVisited = doc.getElementById(
|
||||
"sidebar-history-sort-by-last-visited"
|
||||
);
|
||||
this._menu.addEventListener("command", this);
|
||||
this._menu.addEventListener("popuphidden", this.handlePopupEvent);
|
||||
this.addContextMenuListeners();
|
||||
|
@ -75,6 +81,12 @@ export class SidebarHistory extends SidebarPage {
|
|||
case "sidebar-history-sort-by-site":
|
||||
this.controller.onChangeSortOption(e, "site");
|
||||
break;
|
||||
case "sidebar-history-sort-by-date-and-site":
|
||||
this.controller.onChangeSortOption(e, "datesite");
|
||||
break;
|
||||
case "sidebar-history-sort-by-last-visited":
|
||||
this.controller.onChangeSortOption(e, "lastvisited");
|
||||
break;
|
||||
case "sidebar-history-clear":
|
||||
lazy.Sanitizer.showUI(this.topWindow);
|
||||
break;
|
||||
|
@ -161,39 +173,63 @@ export class SidebarHistory extends SidebarPage {
|
|||
const { historyVisits } = this.controller;
|
||||
switch (this.controller.sortOption) {
|
||||
case "date":
|
||||
return historyVisits.map(({ l10nId, items }, i) => {
|
||||
let tabIndex = i > 0 ? "-1" : undefined;
|
||||
return html` <moz-card
|
||||
type="accordion"
|
||||
?expanded=${i < DAYS_EXPANDED_INITIALLY}
|
||||
data-l10n-id=${l10nId}
|
||||
data-l10n-args=${JSON.stringify({
|
||||
date: items[0].time,
|
||||
})}
|
||||
@keydown=${this.handleCardKeydown}
|
||||
tabindex=${ifDefined(tabIndex)}
|
||||
>
|
||||
${this.#tabListTemplate(this.getTabItems(items))}
|
||||
</moz-card>`;
|
||||
});
|
||||
return historyVisits.map(({ l10nId, items }, i) =>
|
||||
this.#dateCardTemplate(l10nId, i, items)
|
||||
);
|
||||
case "site":
|
||||
return historyVisits.map(({ domain, items }, i) => {
|
||||
let tabIndex = i > 0 ? "-1" : undefined;
|
||||
return html` <moz-card
|
||||
type="accordion"
|
||||
expanded
|
||||
heading=${domain}
|
||||
@keydown=${this.handleCardKeydown}
|
||||
tabindex=${ifDefined(tabIndex)}
|
||||
>
|
||||
${this.#tabListTemplate(this.getTabItems(items))}
|
||||
</moz-card>`;
|
||||
});
|
||||
return historyVisits.map(({ domain, items }, i) =>
|
||||
this.#siteCardTemplate(domain, i, items)
|
||||
);
|
||||
case "datesite":
|
||||
return historyVisits.map(({ l10nId, items }, i) =>
|
||||
this.#dateCardTemplate(l10nId, i, items, true)
|
||||
);
|
||||
case "lastvisited":
|
||||
return historyVisits.map(
|
||||
({ items }) =>
|
||||
html`<moz-card>
|
||||
${this.#tabListTemplate(this.getTabItems(items))}
|
||||
</moz-card>`
|
||||
);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
#dateCardTemplate(l10nId, index, items, isDateSite = false) {
|
||||
const tabIndex = index > 0 ? "-1" : undefined;
|
||||
return html` <moz-card
|
||||
type="accordion"
|
||||
?expanded=${index < DAYS_EXPANDED_INITIALLY}
|
||||
data-l10n-id=${l10nId}
|
||||
data-l10n-args=${JSON.stringify({
|
||||
date: isDateSite ? items[0][1][0].time : items[0].time,
|
||||
})}
|
||||
@keydown=${this.handleCardKeydown}
|
||||
tabindex=${ifDefined(tabIndex)}
|
||||
>
|
||||
${isDateSite
|
||||
? items.map(([domain, visits], i) =>
|
||||
this.#siteCardTemplate(domain, i, visits, true)
|
||||
)
|
||||
: this.#tabListTemplate(this.getTabItems(items))}
|
||||
</moz-card>`;
|
||||
}
|
||||
|
||||
#siteCardTemplate(domain, index, items, isDateSite = false) {
|
||||
let tabIndex = index > 0 ? "-1" : undefined;
|
||||
return html` <moz-card
|
||||
class=${isDateSite ? "nested-card" : ""}
|
||||
type="accordion"
|
||||
?expanded=${!isDateSite}
|
||||
heading=${domain}
|
||||
@keydown=${this.handleCardKeydown}
|
||||
tabindex=${ifDefined(tabIndex)}
|
||||
>
|
||||
${this.#tabListTemplate(this.getTabItems(items))}
|
||||
</moz-card>`;
|
||||
}
|
||||
|
||||
#emptyMessageTemplate() {
|
||||
let descriptionHeader;
|
||||
let descriptionLabels;
|
||||
|
@ -302,6 +338,14 @@ export class SidebarHistory extends SidebarPage {
|
|||
"checked",
|
||||
this.controller.sortOption == "site"
|
||||
);
|
||||
this._menuSortByDateSite.setAttribute(
|
||||
"checked",
|
||||
this.controller.sortOption == "datesite"
|
||||
);
|
||||
this._menuSortByLastVisited.setAttribute(
|
||||
"checked",
|
||||
this.controller.sortOption == "lastvisited"
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -36,7 +36,7 @@ export class SidebarPanelHeader extends MozLitElement {
|
|||
view=${this.view}
|
||||
size="default"
|
||||
type="icon ghost"
|
||||
tabindex="-1"
|
||||
tabindex="1"
|
||||
>
|
||||
</moz-button>
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@ function isActiveElement(el) {
|
|||
}
|
||||
|
||||
add_task(async function test_keyboard_navigation() {
|
||||
const { document } = win;
|
||||
const { document, SidebarController } = win;
|
||||
const sidebar = document.querySelector("sidebar-main");
|
||||
info("Waiting for tool buttons to be present");
|
||||
await BrowserTestUtils.waitForMutationCondition(
|
||||
|
@ -87,7 +87,38 @@ add_task(async function test_keyboard_navigation() {
|
|||
info("Press Tab key.");
|
||||
EventUtils.synthesizeKey("KEY_Tab", {}, win);
|
||||
ok(isActiveElement(customizeButton), "Customize button is focused.");
|
||||
}).skip(); // Bug 1950504
|
||||
info("Press Enter key again.");
|
||||
const promiseFocused = BrowserTestUtils.waitForEvent(win, "SidebarFocused");
|
||||
EventUtils.synthesizeKey("KEY_Enter", {}, win);
|
||||
await promiseFocused;
|
||||
await sidebar.updateComplete;
|
||||
ok(sidebar.open, "Sidebar is open.");
|
||||
|
||||
let customizeDocument = SidebarController.browser.contentDocument;
|
||||
const customizeComponent =
|
||||
customizeDocument.querySelector("sidebar-customize");
|
||||
const sidebarPanelHeader = customizeComponent.shadowRoot.querySelector(
|
||||
"sidebar-panel-header"
|
||||
);
|
||||
let closeButton = sidebarPanelHeader.closeButton;
|
||||
info("Press Tab key.");
|
||||
EventUtils.synthesizeKey("KEY_Tab", {}, win);
|
||||
ok(isActiveElement(closeButton), "Close button is focused.");
|
||||
|
||||
info("Press Tab key.");
|
||||
EventUtils.synthesizeKey("KEY_Tab", {}, win);
|
||||
ok(
|
||||
isActiveElement(customizeComponent.verticalTabsInput),
|
||||
"First customize component is focused"
|
||||
);
|
||||
|
||||
info("Press Tab and Shift key.");
|
||||
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true }, win);
|
||||
ok(isActiveElement(closeButton), "Close button is focused.");
|
||||
EventUtils.synthesizeKey("KEY_Enter", {}, win);
|
||||
await sidebar.updateComplete;
|
||||
ok(!sidebar.open, "Sidebar is closed.");
|
||||
});
|
||||
|
||||
add_task(async function test_menu_items_labeled() {
|
||||
const { document, SidebarController } = win;
|
||||
|
|
|
@ -188,6 +188,8 @@ add_task(async function test_history_sort() {
|
|||
const menu = component._menu;
|
||||
const sortByDateButton = component._menuSortByDate;
|
||||
const sortBySiteButton = component._menuSortBySite;
|
||||
const sortByDateSiteButton = component._menuSortByDateSite;
|
||||
const sortByLastVisitedButton = component._menuSortByLastVisited;
|
||||
|
||||
info("Sort history by site.");
|
||||
let promiseMenuShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
|
||||
|
@ -233,6 +235,58 @@ add_task(async function test_history_sort() {
|
|||
"The cards for Today and Yesterday are expanded."
|
||||
);
|
||||
}
|
||||
|
||||
info("Sort history by date and site.");
|
||||
promiseMenuShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
|
||||
EventUtils.synthesizeMouseAtCenter(menuButton, {}, contentWindow);
|
||||
await promiseMenuShown;
|
||||
menu.activateItem(sortByDateSiteButton);
|
||||
await BrowserTestUtils.waitForMutationCondition(
|
||||
component.shadowRoot,
|
||||
{ childList: true, subtree: true },
|
||||
() => component.lists.length === dates.length * URLs.length
|
||||
);
|
||||
Assert.ok(
|
||||
true,
|
||||
"There is a card for each date, and a nested card for each site."
|
||||
);
|
||||
Assert.equal(
|
||||
sortByDateSiteButton.getAttribute("checked"),
|
||||
"true",
|
||||
"Sort by date and site is checked."
|
||||
);
|
||||
const outerCards = [...component.cards].filter(
|
||||
el => !el.classList.contains("nested-card")
|
||||
);
|
||||
for (const [i, card] of outerCards.entries()) {
|
||||
Assert.equal(
|
||||
card.expanded,
|
||||
i === 0 || i === 1,
|
||||
"The cards for Today and Yesterday are expanded."
|
||||
);
|
||||
}
|
||||
|
||||
info("Sort history by last visited.");
|
||||
promiseMenuShown = BrowserTestUtils.waitForEvent(menu, "popupshown");
|
||||
EventUtils.synthesizeMouseAtCenter(menuButton, {}, contentWindow);
|
||||
await promiseMenuShown;
|
||||
menu.activateItem(sortByLastVisitedButton);
|
||||
await BrowserTestUtils.waitForMutationCondition(
|
||||
component.shadowRoot,
|
||||
{ childList: true, subtree: true },
|
||||
() => component.lists.length === 1
|
||||
);
|
||||
Assert.equal(
|
||||
component.lists[0].tabItems.length,
|
||||
URLs.length,
|
||||
"There is a single card with a row for each site."
|
||||
);
|
||||
Assert.equal(
|
||||
sortByLastVisitedButton.getAttribute("checked"),
|
||||
"true",
|
||||
"Sort by last visited is checked."
|
||||
);
|
||||
|
||||
win.SidebarController.hide();
|
||||
});
|
||||
|
||||
|
|
|
@ -585,3 +585,57 @@ add_task(async function test_vertical_tabs_min_width() {
|
|||
}
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_launcher_collapsed_entering_horiz_tabs_with_hide_sidebar() {
|
||||
const { sidebarMain } = SidebarController;
|
||||
await SpecialPowers.pushPrefEnv({ set: [["sidebar.verticalTabs", true]] });
|
||||
await waitForTabstripOrientation("vertical");
|
||||
ok(
|
||||
BrowserTestUtils.isVisible(sidebarMain),
|
||||
"Revamped sidebar main is shown initially."
|
||||
);
|
||||
ok(
|
||||
sidebarMain.expanded,
|
||||
"Launcher is expanded with vertical tabs and always-show"
|
||||
);
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["sidebar.visibility", "hide-sidebar"]],
|
||||
});
|
||||
await sidebarMain.updateComplete;
|
||||
ok(
|
||||
BrowserTestUtils.isHidden(sidebarMain),
|
||||
"Revamped sidebar main hidden when we switch to hide-sidebar."
|
||||
);
|
||||
|
||||
// toggle the launcher back open.
|
||||
document.getElementById("sidebar-button").doCommand();
|
||||
await sidebarMain.updateComplete;
|
||||
ok(
|
||||
BrowserTestUtils.isVisible(sidebarMain),
|
||||
"Revamped sidebar main visible again."
|
||||
);
|
||||
ok(
|
||||
sidebarMain.expanded,
|
||||
"Launcher is still expanded as vertical tabs are still enabled"
|
||||
);
|
||||
|
||||
// switch back to horizontal tabs and confirm the launcher get un-expanded
|
||||
await SpecialPowers.pushPrefEnv({ set: [["sidebar.verticalTabs", false]] });
|
||||
await waitForTabstripOrientation("horizontal");
|
||||
|
||||
ok(
|
||||
BrowserTestUtils.isVisible(sidebarMain),
|
||||
"Revamped sidebar main is still visible when we switch to horizontal tabs."
|
||||
);
|
||||
ok(
|
||||
!sidebarMain.expanded,
|
||||
"Launcher is collapsed when we switch to horizontal tabs with hide-sidebar"
|
||||
);
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,16 +17,16 @@
|
|||
}
|
||||
},
|
||||
"duty-start-dates": {
|
||||
"2025-24-03": "Nikki Sharpley",
|
||||
"2025-01-06": "Kelly Cochrane",
|
||||
"2025-01-08": "Jonathan Sudiaman",
|
||||
"2025-01-10": "Sam Foster",
|
||||
"2025-01-12": "Sarah Clements",
|
||||
"2026-01-02": "Nikki Sharpley",
|
||||
"2026-01-04": "Kelly Cochrane",
|
||||
"2026-01-06": "Jonathan Sudiaman",
|
||||
"2026-01-08": "Sam Foster",
|
||||
"2026-01-10": "Sarah Clements",
|
||||
"2026-01-12": "Nikki Sharpley"
|
||||
"2025-04-10": "Nikki Sharpley",
|
||||
"2025-06-01": "Kelly Cochrane",
|
||||
"2025-08-01": "Jonathan Sudiaman",
|
||||
"2025-10-01": "Sam Foster",
|
||||
"2025-12-01": "Sarah Clements",
|
||||
"2026-02-01": "Nikki Sharpley",
|
||||
"2026-04-01": "Kelly Cochrane",
|
||||
"2026-06-01": "Jonathan Sudiaman",
|
||||
"2026-08-01": "Sam Foster",
|
||||
"2026-10-01": "Sarah Clements",
|
||||
"2026-12-01": "Nikki Sharpley"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
# 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 os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import mozpack.path as mozpath
|
||||
from mach.decorators import Command, CommandArgument, SubCommand
|
||||
|
@ -40,6 +42,19 @@ def storybook_build(command_context):
|
|||
return run_npm(command_context, args=["run", "build-storybook"])
|
||||
|
||||
|
||||
@SubCommand(
|
||||
"storybook",
|
||||
"upgrade",
|
||||
description="Upgrade all storybook dependencies to latest of ranges in package.json",
|
||||
)
|
||||
def storybook_upgrade(command_context):
|
||||
delete_storybook_node_modules()
|
||||
package_lock_path = "browser/components/storybook/package-lock.json"
|
||||
if os.path.exists(package_lock_path):
|
||||
os.unlink(package_lock_path)
|
||||
return run_npm(command_context, args=["install"])
|
||||
|
||||
|
||||
@SubCommand(
|
||||
"storybook", "launch", description="Launch the Storybook site in your local build."
|
||||
)
|
||||
|
@ -55,6 +70,21 @@ def storybook_launch(command_context):
|
|||
)
|
||||
|
||||
|
||||
def delete_path(path):
|
||||
if path.is_file() or path.is_symlink():
|
||||
path.unlink()
|
||||
return
|
||||
for p in path.iterdir():
|
||||
delete_path(p)
|
||||
path.rmdir()
|
||||
|
||||
|
||||
def delete_storybook_node_modules():
|
||||
node_modules_path = Path("browser/components/storybook/node_modules")
|
||||
if node_modules_path.exists():
|
||||
delete_path(node_modules_path)
|
||||
|
||||
|
||||
def start_browser(command_context):
|
||||
# This delay is used to avoid launching the browser before the Storybook server has started.
|
||||
time.sleep(5)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs";
|
||||
import { TabMetrics } from "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs";
|
||||
|
||||
const MAX_INITIAL_ITEMS = 5;
|
||||
|
||||
|
@ -77,7 +78,9 @@ export class GroupsPanel {
|
|||
}
|
||||
|
||||
case "allTabsGroupView_restoreGroup":
|
||||
this.win.SessionStore.openSavedTabGroup(tabGroupId, this.win);
|
||||
this.win.SessionStore.openSavedTabGroup(tabGroupId, this.win, {
|
||||
source: TabMetrics.METRIC_SOURCE.TAB_OVERFLOW_MENU,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -143,23 +143,15 @@ export class SmartTabGroupingManager {
|
|||
async smartTabGroupingForGroup(group, tabs) {
|
||||
// Add tabs to suggested group
|
||||
const groupTabs = group.tabs;
|
||||
const uniqueSpecs = new Set();
|
||||
const allTabs = tabs.filter(tab => {
|
||||
// Don't include tabs already pinned
|
||||
if (tab.pinned) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const spec = tab?.linkedBrowser?.currentURI?.spec;
|
||||
if (!spec) {
|
||||
if (!tab?.linkedBrowser?.currentURI?.spec) {
|
||||
return false;
|
||||
}
|
||||
if (!uniqueSpecs.has(spec)) {
|
||||
uniqueSpecs.add(spec);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// find tabs that are part of the group
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* A common list of systems, surfaces, controls, etc. from which user
|
||||
* interactions with tab groups could originate. These "source" values
|
||||
* should be sent as extra data with tab group-related metrics events.
|
||||
*/
|
||||
const METRIC_SOURCE = Object.freeze({
|
||||
TAB_OVERFLOW_MENU: "tab_overflow",
|
||||
TAB_GROUP_MENU: "tab_group",
|
||||
UNKNOWN: "unknown",
|
||||
});
|
||||
|
||||
export const TabGroupMetrics = {
|
||||
METRIC_SOURCE,
|
||||
};
|
62
browser/components/tabbrowser/TabMetrics.sys.mjs
Normal file
62
browser/components/tabbrowser/TabMetrics.sys.mjs
Normal file
|
@ -0,0 +1,62 @@
|
|||
/* 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 common list of systems, surfaces, controls, etc. from which user
|
||||
* interactions with tabs could originate. These "source" values
|
||||
* should be sent as extra data with tab-related metrics events.
|
||||
*/
|
||||
const METRIC_SOURCE = Object.freeze({
|
||||
TAB_OVERFLOW_MENU: "tab_overflow",
|
||||
TAB_GROUP_MENU: "tab_group",
|
||||
TAB_MENU: "tab_menu",
|
||||
DRAG_AND_DROP: "drag",
|
||||
SUGGEST: "suggest",
|
||||
RECENT_TABS: "recent",
|
||||
UNKNOWN: "unknown",
|
||||
});
|
||||
|
||||
const METRIC_TABS_LAYOUT = Object.freeze({
|
||||
HORIZONTAL: "horizontal",
|
||||
VERTICAL: "vertical",
|
||||
});
|
||||
|
||||
const METRIC_REOPEN_TYPE = Object.freeze({
|
||||
SAVED: "saved",
|
||||
DELETED: "deleted",
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {object} TabMetricsContext
|
||||
* @property {boolean} [isUserTriggered=false]
|
||||
* Should be true if there was an explicit action/request from the user
|
||||
* (as opposed to some action being taken internally or for technical
|
||||
* bookkeeping reasons alone). This causes telemetry events to fire.
|
||||
* @property {string} [telemetrySource="unknown"]
|
||||
* The system, surface, or control the user used to take this action.
|
||||
* @see TabMetrics.METRIC_SOURCE for possible values.
|
||||
* Defaults to "unknown".
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a `TabMetricsContext` object for a user event originating from
|
||||
* the specified source.
|
||||
*
|
||||
* @param {string} telemetrySource
|
||||
* @see TabMetrics.METRIC_SOURCE
|
||||
* @returns {TabMetricsContext}
|
||||
*/
|
||||
function userTriggeredContext(telemetrySource) {
|
||||
return {
|
||||
isUserTriggered: true,
|
||||
telemetrySource,
|
||||
};
|
||||
}
|
||||
|
||||
export const TabMetrics = {
|
||||
METRIC_SOURCE,
|
||||
METRIC_TABS_LAYOUT,
|
||||
METRIC_REOPEN_TYPE,
|
||||
userTriggeredContext,
|
||||
};
|
|
@ -235,6 +235,9 @@ class TabsListBase {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab} tab
|
||||
*/
|
||||
_moveTab(tab) {
|
||||
let item = this.tabToElement.get(tab);
|
||||
if (item) {
|
||||
|
|
|
@ -110,8 +110,8 @@
|
|||
PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs",
|
||||
SmartTabGroupingManager:
|
||||
"moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs",
|
||||
TabGroupMetrics:
|
||||
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs",
|
||||
TabMetrics:
|
||||
"moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
|
||||
TabStateFlusher:
|
||||
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
|
||||
UrlbarProviderOpenTabs:
|
||||
|
@ -2958,7 +2958,7 @@
|
|||
* Causes the group create UI to be displayed and telemetry events to be fired.
|
||||
* @param {string} [options.telemetryUserCreateSource]
|
||||
* The means by which the tab group was created.
|
||||
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
|
||||
* @see TabMetrics.METRIC_SOURCE for possible values.
|
||||
* Defaults to "unknown".
|
||||
*/
|
||||
addTabGroup(
|
||||
|
@ -2981,6 +2981,10 @@
|
|||
}
|
||||
|
||||
if (!id) {
|
||||
// Note: If this changes, make sure to also update the
|
||||
// getExtTabGroupIdForInternalTabGroupId implementation in
|
||||
// browser/components/extensions/parent/ext-browser.js.
|
||||
// See: Bug 1960104 - Improve tab group ID generation in addTabGroup
|
||||
id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
|
||||
}
|
||||
let group = this._createTabGroup(id, color, false, label);
|
||||
|
@ -3035,14 +3039,14 @@
|
|||
* switches windows). This causes telemetry events to fire.
|
||||
* @param {string} [options.telemetrySource="unknown"]
|
||||
* The means by which the tab group was removed.
|
||||
* @see TabGroupMetrics.METRIC_SOURCE for possible values.
|
||||
* @see TabMetrics.METRIC_SOURCE for possible values.
|
||||
* Defaults to "unknown".
|
||||
*/
|
||||
async removeTabGroup(
|
||||
group,
|
||||
options = {
|
||||
isUserTriggered: false,
|
||||
telemetrySource: this.TabGroupMetrics.METRIC_SOURCE.UNKNOWN,
|
||||
telemetrySource: this.TabMetrics.METRIC_SOURCE.UNKNOWN,
|
||||
}
|
||||
) {
|
||||
if (this.tabGroupMenu.panel.state != "closed") {
|
||||
|
@ -3943,11 +3947,11 @@
|
|||
// Place tab at the end of the contextual tab group because one of:
|
||||
// 1) no `itemAfter` so `tab` should be the last tab in the tab strip
|
||||
// 2) `itemAfter` is in a different tab group
|
||||
this.moveTabToGroup(tab, tabGroup);
|
||||
tabGroup.appendChild(tab);
|
||||
}
|
||||
} else if (
|
||||
this.isTab(itemAfter) &&
|
||||
itemAfter?.group?.tabs[0] == itemAfter
|
||||
(this.isTab(itemAfter) && itemAfter.group?.tabs[0] == itemAfter) ||
|
||||
this.isTabGroupLabel(itemAfter)
|
||||
) {
|
||||
// If there is ambiguity around whether or not a tab should be inserted
|
||||
// into a group (i.e. because the new tab is being inserted on the
|
||||
|
@ -5869,7 +5873,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* @param {Element} element
|
||||
* @returns {boolean}
|
||||
* `true` if element is a `<tab>`
|
||||
*/
|
||||
|
@ -5878,7 +5882,16 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTextLabel} element
|
||||
* @param {Element} element
|
||||
* @returns {boolean}
|
||||
* `true` if element is a `<tab-group>`
|
||||
*/
|
||||
isTabGroup(element) {
|
||||
return !!(element?.tagName == "tab-group");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} element
|
||||
* @returns {boolean}
|
||||
* `true` if element is the `<label>` in a `<tab-group>`
|
||||
*/
|
||||
|
@ -5911,7 +5924,7 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTab
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* The tab or tab group to move. Also accepts a tab group label as a
|
||||
* stand-in for its group.
|
||||
* @param {object} [options]
|
||||
|
@ -5921,18 +5934,36 @@
|
|||
* The desired position, expressed as the index within the
|
||||
* `MozTabbrowserTabs::ariaFocusableItems` array.
|
||||
* @param {boolean} [options.forceUngrouped=false]
|
||||
* Force `aTab` to move into position as a standalone tab, overriding
|
||||
* Force `element` to move into position as a standalone tab, overriding
|
||||
* any possibility of entering a tab group. For example, setting `true`
|
||||
* ensures that a pinned tab will not accidentally be placed inside of
|
||||
* a tab group, since pinned tabs are presently not allowed in tab groups.
|
||||
* @property {boolean} [options.isUserTriggered=false]
|
||||
* Should be true if there was an explicit action/request from the user
|
||||
* (as opposed to some action being taken internally or for technical
|
||||
* bookkeeping reasons alone) to move the tab. This causes telemetry
|
||||
* events to fire.
|
||||
* @property {string} [options.telemetrySource="unknown"]
|
||||
* The system, surface, or control the user used to move the tab.
|
||||
* @see TabMetrics.METRIC_SOURCE for possible values.
|
||||
* Defaults to "unknown".
|
||||
*/
|
||||
moveTabTo(aTab, { elementIndex, tabIndex, forceUngrouped = false } = {}) {
|
||||
moveTabTo(
|
||||
element,
|
||||
{
|
||||
elementIndex,
|
||||
tabIndex,
|
||||
forceUngrouped = false,
|
||||
isUserTriggered = false,
|
||||
telemetrySource = this.TabMetrics.METRIC_SOURCE.UNKNOWN,
|
||||
} = {}
|
||||
) {
|
||||
if (typeof elementIndex == "number") {
|
||||
tabIndex = this.#elementIndexToTabIndex(elementIndex);
|
||||
}
|
||||
|
||||
// Don't allow mixing pinned and unpinned tabs.
|
||||
if (this.isTab(aTab) && aTab.pinned) {
|
||||
if (this.isTab(element) && element.pinned) {
|
||||
tabIndex = Math.min(tabIndex, this.pinnedTabCount - 1);
|
||||
} else {
|
||||
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
|
||||
|
@ -5940,73 +5971,84 @@
|
|||
|
||||
// Return early if the tab is already in the right spot.
|
||||
if (
|
||||
this.isTab(aTab) &&
|
||||
aTab._tPos == tabIndex &&
|
||||
!(aTab.group && forceUngrouped)
|
||||
this.isTab(element) &&
|
||||
element._tPos == tabIndex &&
|
||||
!(element.group && forceUngrouped)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When asked to move a tab group label, we need to move the whole group
|
||||
// instead.
|
||||
if (this.isTabGroupLabel(aTab)) {
|
||||
if (this.isTabGroupLabel(element)) {
|
||||
element = element.group;
|
||||
}
|
||||
if (this.isTabGroup(element)) {
|
||||
forceUngrouped = true;
|
||||
aTab = aTab.group;
|
||||
}
|
||||
|
||||
this.#handleTabMove(aTab, () => {
|
||||
let neighbor = this.tabs[tabIndex];
|
||||
if (forceUngrouped && neighbor.group) {
|
||||
neighbor = neighbor.group;
|
||||
}
|
||||
if (neighbor && tabIndex > aTab._tPos) {
|
||||
neighbor.after(aTab);
|
||||
} else {
|
||||
this.tabContainer.insertBefore(aTab, neighbor);
|
||||
}
|
||||
});
|
||||
this.#handleTabMove(
|
||||
element,
|
||||
() => {
|
||||
let neighbor = this.tabs[tabIndex];
|
||||
if (forceUngrouped && neighbor.group) {
|
||||
neighbor = neighbor.group;
|
||||
}
|
||||
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
|
||||
neighbor.after(element);
|
||||
} else {
|
||||
this.tabContainer.insertBefore(element, neighbor);
|
||||
}
|
||||
},
|
||||
{ isUserTriggered, telemetrySource }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
moveTabBefore(tab, targetElement) {
|
||||
this.#moveTabNextTo(tab, targetElement, true);
|
||||
moveTabBefore(element, targetElement, metricsContext) {
|
||||
this.#moveTabNextTo(element, targetElement, true, metricsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} tabs
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
moveTabsBefore(tabs, targetElement) {
|
||||
this.#moveTabsNextTo(tabs, targetElement, true);
|
||||
moveTabsBefore(elements, targetElement, metricsContext) {
|
||||
this.#moveTabsNextTo(elements, targetElement, true, metricsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
moveTabAfter(tab, targetElement) {
|
||||
this.#moveTabNextTo(tab, targetElement, false);
|
||||
moveTabAfter(element, targetElement, metricsContext) {
|
||||
this.#moveTabNextTo(element, targetElement, false, metricsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} tabs
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup[]} elements
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
moveTabsAfter(tabs, targetElement) {
|
||||
this.#moveTabsNextTo(tabs, targetElement, false);
|
||||
moveTabsAfter(elements, targetElement, metricsContext) {
|
||||
this.#moveTabsNextTo(elements, targetElement, false, metricsContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} tab
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* The tab or tab group to move. Also accepts a tab group label as a
|
||||
* stand-in for its group.
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {boolean} moveBefore
|
||||
* @param {boolean} [moveBefore=false]
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
#moveTabNextTo(tab, targetElement, moveBefore = false) {
|
||||
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
|
||||
if (this.isTabGroupLabel(targetElement)) {
|
||||
targetElement = targetElement.group;
|
||||
if (!moveBefore) {
|
||||
|
@ -6014,53 +6056,82 @@
|
|||
moveBefore = true;
|
||||
}
|
||||
}
|
||||
if (this.isTabGroupLabel(tab)) {
|
||||
tab = tab.group;
|
||||
if (this.isTabGroupLabel(element)) {
|
||||
element = element.group;
|
||||
if (targetElement?.group) {
|
||||
targetElement = targetElement.group;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't allow mixing pinned and unpinned tabs.
|
||||
if (tab.pinned && !targetElement?.pinned) {
|
||||
if (element.pinned && !targetElement?.pinned) {
|
||||
targetElement = this.tabs[this.pinnedTabCount - 1];
|
||||
moveBefore = false;
|
||||
} else if (!tab.pinned && targetElement && targetElement.pinned) {
|
||||
} else if (!element.pinned && targetElement && targetElement.pinned) {
|
||||
targetElement = this.tabs[this.pinnedTabCount];
|
||||
moveBefore = true;
|
||||
}
|
||||
|
||||
let getContainer = () => {
|
||||
if (tab.pinned && this.tabContainer.verticalMode) {
|
||||
if (element.pinned && this.tabContainer.verticalMode) {
|
||||
return this.tabContainer.verticalPinnedTabsContainer;
|
||||
}
|
||||
return this.tabContainer;
|
||||
};
|
||||
|
||||
this.#handleTabMove(tab, () => {
|
||||
if (moveBefore) {
|
||||
getContainer().insertBefore(tab, targetElement);
|
||||
} else if (targetElement) {
|
||||
targetElement.after(tab);
|
||||
} else {
|
||||
getContainer().appendChild(tab);
|
||||
}
|
||||
});
|
||||
this.#handleTabMove(
|
||||
element,
|
||||
() => {
|
||||
if (moveBefore) {
|
||||
getContainer().insertBefore(element, targetElement);
|
||||
} else if (targetElement) {
|
||||
targetElement.after(element);
|
||||
} else {
|
||||
getContainer().appendChild(element);
|
||||
}
|
||||
},
|
||||
metricsContext
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab[]} tabs
|
||||
* @param {MozTabbrowserTab[]} elements
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} targetElement
|
||||
* @param {boolean} moveBefore
|
||||
* @param {boolean} [moveBefore=false]
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
#moveTabsNextTo(tabs, targetElement, moveBefore = false) {
|
||||
this.#moveTabNextTo(tabs[0], targetElement, moveBefore);
|
||||
for (let i = 1; i < tabs.length; i++) {
|
||||
this.#moveTabNextTo(tabs[i], tabs[i - 1]);
|
||||
#moveTabsNextTo(
|
||||
elements,
|
||||
targetElement,
|
||||
moveBefore = false,
|
||||
metricsContext
|
||||
) {
|
||||
this.#moveTabNextTo(
|
||||
elements[0],
|
||||
targetElement,
|
||||
moveBefore,
|
||||
metricsContext
|
||||
);
|
||||
for (let i = 1; i < elements.length; i++) {
|
||||
this.#moveTabNextTo(
|
||||
elements[i],
|
||||
elements[i - 1],
|
||||
false,
|
||||
metricsContext
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
moveTabToGroup(aTab, aGroup) {
|
||||
/**
|
||||
*
|
||||
* @param {MozTabbrowserTab} aTab
|
||||
* @param {MozTabbrowserTabGroup} aGroup
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
moveTabToGroup(aTab, aGroup, metricsContext) {
|
||||
if (!this.isTab(aTab)) {
|
||||
throw new Error("Can only move a tab into a tab group");
|
||||
}
|
||||
if (aTab.pinned) {
|
||||
return;
|
||||
}
|
||||
|
@ -6069,47 +6140,118 @@
|
|||
}
|
||||
|
||||
aGroup.collapsed = false;
|
||||
this.#handleTabMove(aTab, () => aGroup.appendChild(aTab));
|
||||
this.#handleTabMove(aTab, () => aGroup.appendChild(aTab), metricsContext);
|
||||
this.removeFromMultiSelectedTabs(aTab);
|
||||
this.tabContainer._notifyBackgroundTab(aTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab} aTab
|
||||
* @param {function():void} moveActionCallback
|
||||
* @returns
|
||||
* @typedef {object} TabMoveState
|
||||
* @property {number} tabIndex
|
||||
* @property {number} [elementIndex]
|
||||
* @property {string} [tabGroupId]
|
||||
*/
|
||||
#handleTabMove(aTab, moveActionCallback) {
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab} tab
|
||||
* @returns {TabMoveState|undefined}
|
||||
*/
|
||||
#getTabMoveState(tab) {
|
||||
if (!this.isTab(tab)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let state = {
|
||||
tabIndex: tab._tPos,
|
||||
};
|
||||
if (tab.visible) {
|
||||
state.elementIndex = tab.elementIndex;
|
||||
}
|
||||
if (tab.group) {
|
||||
state.tabGroupId = tab.group.id;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab} tab
|
||||
* @param {TabMoveState} [previousTabState]
|
||||
* @param {TabMoveState} [currentTabState]
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
#notifyOnTabMove(tab, previousTabState, currentTabState, metricsContext) {
|
||||
if (!this.isTab(tab) || !previousTabState || !currentTabState) {
|
||||
return;
|
||||
}
|
||||
|
||||
let changedPosition =
|
||||
previousTabState.tabIndex != currentTabState.tabIndex;
|
||||
let changedTabGroup =
|
||||
previousTabState.tabGroupId != currentTabState.tabGroupId;
|
||||
|
||||
if (changedPosition || changedTabGroup) {
|
||||
tab.dispatchEvent(
|
||||
new CustomEvent("TabMove", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
previousTabState,
|
||||
currentTabState,
|
||||
isUserTriggered: metricsContext?.isUserTriggered ?? false,
|
||||
telemetrySource:
|
||||
metricsContext?.telemetrySource ??
|
||||
this.TabMetrics.METRIC_SOURCE.UNKNOWN,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* @param {function():void} moveActionCallback
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
*/
|
||||
#handleTabMove(element, moveActionCallback, metricsContext) {
|
||||
let tabs;
|
||||
if (this.isTab(element)) {
|
||||
tabs = [element];
|
||||
} else if (this.isTabGroup(element)) {
|
||||
tabs = element.tabs;
|
||||
} else {
|
||||
throw new Error("Can only move a tab or tab group within the tab bar");
|
||||
}
|
||||
|
||||
let wasFocused = document.activeElement == this.selectedTab;
|
||||
let oldPosition = this.isTab(aTab) && aTab._tPos;
|
||||
let previousTabStates = tabs.map(tab => this.#getTabMoveState(tab));
|
||||
|
||||
moveActionCallback();
|
||||
|
||||
// Clear tabs cache after moving nodes because the order of tabs may have
|
||||
// changed.
|
||||
this.tabContainer._invalidateCachedTabs();
|
||||
|
||||
this._lastRelatedTabMap = new WeakMap();
|
||||
|
||||
this._updateTabsAfterInsert();
|
||||
|
||||
if (wasFocused) {
|
||||
this.selectedTab.focus();
|
||||
}
|
||||
|
||||
if (aTab.selected) {
|
||||
this.tabContainer._handleTabSelect(true);
|
||||
}
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
let tab = tabs[i];
|
||||
if (tab.selected) {
|
||||
this.tabContainer._handleTabSelect(true);
|
||||
}
|
||||
if (tab.pinned) {
|
||||
this.tabContainer._positionPinnedTabs();
|
||||
}
|
||||
|
||||
if (aTab.pinned) {
|
||||
this.tabContainer._positionPinnedTabs();
|
||||
}
|
||||
// Pinning/unpinning vertical tabs, and moving tabs into tab groups, both bypass moveTabTo.
|
||||
// We still want to check whether its worth dispatching an event.
|
||||
if (this.isTab(aTab) && oldPosition != aTab._tPos) {
|
||||
let evt = document.createEvent("UIEvents");
|
||||
evt.initUIEvent("TabMove", true, false, window, oldPosition);
|
||||
aTab.dispatchEvent(evt);
|
||||
let currentTabState = this.#getTabMoveState(tab);
|
||||
this.#notifyOnTabMove(
|
||||
tab,
|
||||
previousTabStates[i],
|
||||
currentTabState,
|
||||
metricsContext
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8696,11 +8838,7 @@ var TabContextMenu = {
|
|||
}
|
||||
|
||||
item.classList.add("menuitem-iconic");
|
||||
if (group.collapsed) {
|
||||
item.classList.add("tab-group-icon-collapsed");
|
||||
} else {
|
||||
item.classList.add("tab-group-icon");
|
||||
}
|
||||
item.classList.add("tab-group-icon");
|
||||
item.style.setProperty(
|
||||
"--tab-group-color",
|
||||
group.style.getPropertyValue("--tab-group-color")
|
||||
|
@ -9056,8 +9194,16 @@ var TabContextMenu = {
|
|||
gTabsPanel.hideAllTabsPanel();
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTabGroup} group
|
||||
*/
|
||||
moveTabsToGroup(group) {
|
||||
group.addTabs(this.contextTabs);
|
||||
group.addTabs(
|
||||
this.contextTabs,
|
||||
gBrowser.TabMetrics.userTriggeredContext(
|
||||
gBrowser.TabMetrics.METRIC_SOURCE.TAB_MENU
|
||||
)
|
||||
);
|
||||
group.ownerGlobal.focus();
|
||||
},
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
// This is loaded into chrome windows with the subscript loader. Wrap in
|
||||
// a block to prevent accidentally leaking globals onto `window`.
|
||||
{
|
||||
const { TabGroupMetrics } = ChromeUtils.importESModule(
|
||||
"moz-src:///browser/components/tabbrowser/TabGroupMetrics.sys.mjs"
|
||||
const { TabMetrics } = ChromeUtils.importESModule(
|
||||
"moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs"
|
||||
);
|
||||
const { TabStateFlusher } = ChromeUtils.importESModule(
|
||||
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
|
||||
|
@ -448,10 +448,12 @@
|
|||
document
|
||||
.getElementById("tabGroupEditor_deleteGroup")
|
||||
.addEventListener("command", () => {
|
||||
gBrowser.removeTabGroup(this.activeGroup, {
|
||||
isUserTriggered: true,
|
||||
telemetrySource: TabGroupMetrics.METRIC_SOURCE.TAB_GROUP_MENU,
|
||||
});
|
||||
gBrowser.removeTabGroup(
|
||||
this.activeGroup,
|
||||
TabMetrics.userTriggeredContext(
|
||||
TabMetrics.METRIC_SOURCE.TAB_GROUP_MENU
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
this.panel.addEventListener("popupshown", this);
|
||||
|
|
|
@ -234,9 +234,11 @@
|
|||
/**
|
||||
* add tabs to the group
|
||||
*
|
||||
* @param tabs array of tabs to add
|
||||
* @param {MozTabbrowserTab[]} tabs
|
||||
* @param {TabMetricsContext} [metricsContext]
|
||||
* Optional context to record for metrics purposes.
|
||||
*/
|
||||
addTabs(tabs) {
|
||||
addTabs(tabs, metricsContext) {
|
||||
for (let tab of tabs) {
|
||||
let tabToMove =
|
||||
this.ownerGlobal === tab.ownerGlobal
|
||||
|
@ -245,7 +247,7 @@
|
|||
tabIndex: gBrowser.tabs.at(-1)._tPos + 1,
|
||||
selectTab: tab.selected,
|
||||
});
|
||||
gBrowser.moveTabToGroup(tabToMove, this);
|
||||
gBrowser.moveTabToGroup(tabToMove, this, metricsContext);
|
||||
}
|
||||
this.#lastAddedTo = Date.now();
|
||||
}
|
||||
|
|
|
@ -9,21 +9,20 @@
|
|||
// This is loaded into all browser windows. Wrap in a block to prevent
|
||||
// leaking to window scope.
|
||||
{
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
TabMetrics: "moz-src:///browser/components/tabbrowser/TabMetrics.sys.mjs",
|
||||
});
|
||||
|
||||
const TAB_PREVIEW_PREF = "browser.tabs.hoverPreview.enabled";
|
||||
|
||||
const DIRECTION_BACKWARD = -1;
|
||||
const DIRECTION_FORWARD = 1;
|
||||
|
||||
const isTab = element => gBrowser.isTab(element);
|
||||
const isTabGroup = element => gBrowser.isTabGroup(element);
|
||||
const isTabGroupLabel = element => gBrowser.isTabGroupLabel(element);
|
||||
|
||||
/**
|
||||
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} element
|
||||
* @returns {boolean}
|
||||
* `true` if element is a `<tab-group>`
|
||||
*/
|
||||
const isTabGroup = element => !!(element?.tagName == "tab-group");
|
||||
|
||||
class MozTabbrowserTabs extends MozElements.TabsBase {
|
||||
static observedAttributes = ["orient"];
|
||||
|
||||
|
@ -1060,6 +1059,10 @@
|
|||
var dropEffect = dt.dropEffect;
|
||||
var draggedTab;
|
||||
let movingTabs;
|
||||
/** @type {TabMetricsContext} */
|
||||
const dropMetricsContext = lazy.TabMetrics.userTriggeredContext(
|
||||
lazy.TabMetrics.METRIC_SOURCE.DRAG_AND_DROP
|
||||
);
|
||||
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
|
||||
// tab copy or move
|
||||
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
|
@ -1084,7 +1087,7 @@
|
|||
duplicatedDraggedTab = duplicatedTab;
|
||||
}
|
||||
}
|
||||
gBrowser.moveTabsBefore(duplicatedTabs, dropTarget);
|
||||
gBrowser.moveTabsBefore(duplicatedTabs, dropTarget, dropMetricsContext);
|
||||
if (draggedTab.container != this || event.shiftKey) {
|
||||
this.selectedItem = duplicatedDraggedTab;
|
||||
}
|
||||
|
@ -1172,15 +1175,23 @@
|
|||
let moveTabs = () => {
|
||||
if (dropIndex !== undefined) {
|
||||
for (let tab of movingTabs) {
|
||||
gBrowser.moveTabTo(tab, { elementIndex: dropIndex });
|
||||
gBrowser.moveTabTo(
|
||||
tab,
|
||||
{ elementIndex: dropIndex },
|
||||
dropMetricsContext
|
||||
);
|
||||
if (!directionForward) {
|
||||
dropIndex++;
|
||||
}
|
||||
}
|
||||
} else if (dropBefore) {
|
||||
gBrowser.moveTabsBefore(movingTabs, dropElement);
|
||||
gBrowser.moveTabsBefore(
|
||||
movingTabs,
|
||||
dropElement,
|
||||
dropMetricsContext
|
||||
);
|
||||
} else {
|
||||
gBrowser.moveTabsAfter(movingTabs, dropElement);
|
||||
gBrowser.moveTabsAfter(movingTabs, dropElement, dropMetricsContext);
|
||||
}
|
||||
this.#expandGroupOnDrop(draggedTab);
|
||||
};
|
||||
|
|
|
@ -168,6 +168,61 @@ tabgroup:
|
|||
type: string
|
||||
expires: never
|
||||
|
||||
reopen:
|
||||
type: event
|
||||
description: >
|
||||
Recorded when a user reopens a saved tab group
|
||||
notification_emails:
|
||||
- dao@mozilla.com
|
||||
- dwalker@mozilla.com
|
||||
- jswinarton@mozilla.com
|
||||
- dwalker@mozilla.com
|
||||
bugs:
|
||||
- https://bugzil.la/1938425
|
||||
data_reviews:
|
||||
- https://bugzil.la/1938425
|
||||
extra_keys:
|
||||
source:
|
||||
description: The surface used to find and recall the saved group
|
||||
type: string
|
||||
layout:
|
||||
description: The tabs layout (horizontal or vertical)
|
||||
type: string
|
||||
id:
|
||||
description: The ID of the tab group. Tab group IDs are derived from their creation timestamps and have no other relationship to any tab group metadata.
|
||||
type: string
|
||||
type:
|
||||
description: Whether the user reopened a saved group or a deleted group.
|
||||
type: string
|
||||
expires: never
|
||||
|
||||
add_tab:
|
||||
type: event
|
||||
disabled: true # To be controlled by server knobs during Firefox 138 launch due to expected high volume
|
||||
description: >
|
||||
Recorded when the user adds one or more ungrouped tabs to an existing tab group
|
||||
notification_emails:
|
||||
- dao@mozilla.com
|
||||
- jswinarton@mozilla.com
|
||||
- sthompson@mozilla.com
|
||||
bugs:
|
||||
- https://bugzil.la/1938424
|
||||
data_reviews:
|
||||
- https://bugzil.la/1938424
|
||||
data_sensitivity:
|
||||
- interaction
|
||||
extra_keys:
|
||||
source:
|
||||
description: The system, surface, or control the user used to add the tab(s) to the tab group
|
||||
type: string
|
||||
tabs:
|
||||
description: The number of tabs added to the tab group
|
||||
type: quantity
|
||||
layout:
|
||||
description: The layout of the tab strip when the tabs were added (either "horizontal" or "vertical")
|
||||
type: string
|
||||
expires: never
|
||||
|
||||
active_groups:
|
||||
type: labeled_quantity
|
||||
description: >
|
||||
|
|
|
@ -13,7 +13,7 @@ MOZ_SRC_FILES += [
|
|||
"NewTabPagePreloading.sys.mjs",
|
||||
"OpenInTabsUtils.sys.mjs",
|
||||
"SmartTabGrouping.sys.mjs",
|
||||
"TabGroupMetrics.sys.mjs",
|
||||
"TabMetrics.sys.mjs",
|
||||
"TabsList.sys.mjs",
|
||||
"TabUnloader.sys.mjs",
|
||||
]
|
||||
|
|
|
@ -580,6 +580,22 @@ add_task(async function test_TabGroupEvents() {
|
|||
tabGroupCollapsedTrigger.uninit();
|
||||
});
|
||||
|
||||
add_task(async function test_moveTabGroup() {
|
||||
let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
let group = gBrowser.addTabGroup([tab1, tab2]);
|
||||
|
||||
let tabMoveEvents = Promise.all([
|
||||
BrowserTestUtils.waitForEvent(tab1, "TabMove"),
|
||||
BrowserTestUtils.waitForEvent(tab2, "TabMove"),
|
||||
]);
|
||||
info("moving tab group and awaiting TabMove events");
|
||||
gBrowser.moveTabToStart(group);
|
||||
await tabMoveEvents;
|
||||
|
||||
await removeTabGroup(group);
|
||||
});
|
||||
|
||||
add_task(async function test_moveTabBetweenGroups() {
|
||||
let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
let tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
|
@ -1686,19 +1702,19 @@ add_task(async function test_tabGroupCreatePanel() {
|
|||
let tabgroupPanel = tabgroupEditor.panel;
|
||||
let nameField = tabgroupPanel.querySelector("#tab-group-name");
|
||||
let tab = BrowserTestUtils.addTab(gBrowser, "about:blank");
|
||||
let group;
|
||||
|
||||
let openCreatePanel = async () => {
|
||||
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
|
||||
group = gBrowser.addTabGroup([tab], {
|
||||
let group = gBrowser.addTabGroup([tab], {
|
||||
color: "cyan",
|
||||
label: "Food",
|
||||
isUserTriggered: true,
|
||||
});
|
||||
await panelShown;
|
||||
return group;
|
||||
};
|
||||
|
||||
await openCreatePanel();
|
||||
let group = await openCreatePanel();
|
||||
Assert.equal(tabgroupPanel.state, "open", "Create panel is visible");
|
||||
Assert.ok(tabgroupEditor.createMode, "Group editor is in create mode");
|
||||
// Edit panel should be populated with correct group details
|
||||
|
@ -1725,14 +1741,14 @@ add_task(async function test_tabGroupCreatePanel() {
|
|||
Assert.ok(!tab.group, "Tab is ungrouped after hitting Cancel");
|
||||
|
||||
info("New group should be removed after hitting Esc");
|
||||
await openCreatePanel();
|
||||
group = await openCreatePanel();
|
||||
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
|
||||
EventUtils.synthesizeKey("KEY_Escape");
|
||||
await panelHidden;
|
||||
Assert.ok(!tab.group, "Tab is ungrouped after hitting Esc");
|
||||
|
||||
info("New group should remain when dismissing panel");
|
||||
await openCreatePanel();
|
||||
group = await openCreatePanel();
|
||||
panelHidden = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "hidden");
|
||||
tabgroupPanel.hidePopup();
|
||||
await panelHidden;
|
||||
|
@ -1742,7 +1758,7 @@ add_task(async function test_tabGroupCreatePanel() {
|
|||
group.ungroupTabs();
|
||||
|
||||
info("Panel inputs should work correctly");
|
||||
await openCreatePanel();
|
||||
group = await openCreatePanel();
|
||||
nameField.focus();
|
||||
nameField.value = "";
|
||||
EventUtils.sendString("Shopping");
|
||||
|
@ -1819,7 +1835,7 @@ add_task(async function test_tabGroupCreatePanel() {
|
|||
tabGroupCreatedTrigger.uninit();
|
||||
});
|
||||
|
||||
async function createTabGroupAndOpenEditPanel(tabs = []) {
|
||||
async function createTabGroupAndOpenEditPanel(tabs = [], label = "") {
|
||||
let tabgroupEditor = document.getElementById("tab-group-editor");
|
||||
let tabgroupPanel = tabgroupEditor.panel;
|
||||
if (!tabs.length) {
|
||||
|
@ -1828,7 +1844,7 @@ async function createTabGroupAndOpenEditPanel(tabs = []) {
|
|||
});
|
||||
tabs = [tab];
|
||||
}
|
||||
let group = gBrowser.addTabGroup(tabs, { color: "cyan", label: "Food" });
|
||||
let group = gBrowser.addTabGroup(tabs, { color: "cyan", label });
|
||||
|
||||
let panelShown = BrowserTestUtils.waitForPopupEvent(tabgroupPanel, "shown");
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
|
@ -1844,7 +1860,10 @@ async function createTabGroupAndOpenEditPanel(tabs = []) {
|
|||
}
|
||||
|
||||
add_task(async function test_tabGroupPanelAddTab() {
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel();
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel(
|
||||
[],
|
||||
"test_tabGroupPanelAddTab"
|
||||
);
|
||||
let tabgroupPanel = tabgroupEditor.panel;
|
||||
|
||||
let addNewTabButton = tabgroupPanel.querySelector(
|
||||
|
@ -1858,13 +1877,14 @@ add_task(async function test_tabGroupPanelAddTab() {
|
|||
Assert.ok(tabgroupPanel.state === "closed", "Group editor is closed");
|
||||
Assert.equal(group.tabs.length, 2, "Group has 2 tabs");
|
||||
|
||||
for (let tab of group.tabs) {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
await removeTabGroup(group);
|
||||
});
|
||||
|
||||
add_task(async function test_tabGroupPanelUngroupTabs() {
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel();
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel(
|
||||
[],
|
||||
"test_tabGroupPanelAddTab"
|
||||
);
|
||||
let tabgroupPanel = tabgroupEditor.panel;
|
||||
let tab = group.tabs[0];
|
||||
let ungroupTabsButton = tabgroupPanel.querySelector(
|
||||
|
@ -1914,7 +1934,10 @@ add_task(async function test_moveGroupToNewWindow() {
|
|||
"about:mozilla is third"
|
||||
);
|
||||
};
|
||||
let { group } = await createTabGroupAndOpenEditPanel(tabs);
|
||||
let { group } = await createTabGroupAndOpenEditPanel(
|
||||
tabs,
|
||||
"test_moveGroupToNewWindow"
|
||||
);
|
||||
|
||||
let newWindowOpened = BrowserTestUtils.waitForNewWindow();
|
||||
document.getElementById("tabGroupEditor_moveGroupToNewWindow").click();
|
||||
|
@ -1964,7 +1987,7 @@ add_task(async function test_moveGroupToNewWindow() {
|
|||
!moveGroupButton.disabled,
|
||||
"Button is enabled again when additional tab present"
|
||||
);
|
||||
|
||||
await removeTabGroup(movedGroup);
|
||||
await BrowserTestUtils.closeWindow(newWin, { animate: false });
|
||||
});
|
||||
|
||||
|
@ -1973,7 +1996,10 @@ add_task(async function test_moveGroupToNewWindow() {
|
|||
* group is not saveable.
|
||||
*/
|
||||
add_task(async function test_saveDisabledForUnimportantGroup() {
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel();
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel(
|
||||
[],
|
||||
"test_saveDisabledForUnimportantGroups"
|
||||
);
|
||||
let saveAndCloseGroupButton = tabgroupEditor.panel.querySelector(
|
||||
"#tabGroupEditor_saveAndCloseGroup"
|
||||
);
|
||||
|
@ -1987,7 +2013,7 @@ add_task(async function test_saveDisabledForUnimportantGroup() {
|
|||
);
|
||||
tabgroupEditor.panel.hidePopup();
|
||||
await panelHidden;
|
||||
await gBrowser.removeTabGroup(group);
|
||||
await removeTabGroup(group);
|
||||
});
|
||||
|
||||
add_task(async function test_saveAndCloseGroup() {
|
||||
|
@ -1997,7 +2023,10 @@ add_task(async function test_saveAndCloseGroup() {
|
|||
tabGroupSavedTrigger.init(triggerHandler);
|
||||
|
||||
let tab = await addTab("about:mozilla");
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel([tab]);
|
||||
let { tabgroupEditor, group } = await createTabGroupAndOpenEditPanel(
|
||||
[tab],
|
||||
"test_saveAndCloseGroup"
|
||||
);
|
||||
let tabgroupPanel = tabgroupEditor.panel;
|
||||
await TabStateFlusher.flush(tab.linkedBrowser);
|
||||
let saveAndCloseGroupButton = tabgroupPanel.querySelector(
|
||||
|
@ -2093,6 +2122,7 @@ add_task(async function test_pinningInteractionsWithTabGroups() {
|
|||
);
|
||||
|
||||
moreTabs.concat(tabs).forEach(tab => BrowserTestUtils.removeTab(tab));
|
||||
await removeTabGroup(group);
|
||||
});
|
||||
|
||||
add_task(async function test_pinFirstGroupedTab() {
|
||||
|
@ -2120,6 +2150,7 @@ add_task(async function test_adoptTab() {
|
|||
Assert.equal(adoptedTab._tPos, 1, "tab adopted into expected position");
|
||||
Assert.equal(adoptedTab.group, group, "tab adopted into tab group");
|
||||
|
||||
await removeTabGroup(group);
|
||||
await BrowserTestUtils.closeWindow(newWin, { animate: false });
|
||||
});
|
||||
|
||||
|
@ -2229,3 +2260,33 @@ add_task(async function test_bug1957723_addTabsByIndex() {
|
|||
|
||||
gBrowser.removeAllTabsBut(initialTab);
|
||||
});
|
||||
|
||||
add_task(async function test_bug1959438_duplicateTabJustBeforeGroup() {
|
||||
let initialTab = gBrowser.tabs[0];
|
||||
let triggeringPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
|
||||
const tabs = createManyTabs(3);
|
||||
|
||||
gBrowser.addTabGroup(tabs);
|
||||
|
||||
Assert.equal(gBrowser.tabs.length, 4, "Tab strip starts with four tabs");
|
||||
|
||||
gBrowser.selectTabAtIndex(0);
|
||||
|
||||
// Simulate an addTab call similar to what would be called when a tab is
|
||||
// duplicated. This produces a situation where addTab has no index, but knows
|
||||
// it needs to create one next to the currently selected tab, and guesses for
|
||||
// itself.
|
||||
// If this happens next to a tab group, the resulting element index will
|
||||
// point to the tab group label.
|
||||
gBrowser.addTab("https://example.com", {
|
||||
index: undefined,
|
||||
relatedToCurrent: true,
|
||||
ownerTab: gBrowser.selectedTab,
|
||||
triggeringPrincipal,
|
||||
});
|
||||
|
||||
// This will fail if the tab ends up merged with the tab label.
|
||||
Assert.equal(gBrowser.tabs.length, 5, "A new tab was added to the tab strip");
|
||||
|
||||
gBrowser.removeAllTabsBut(initialTab);
|
||||
});
|
||||
|
|
|
@ -6,6 +6,10 @@ const { TabStateFlusher } = ChromeUtils.importESModule(
|
|||
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
|
||||
);
|
||||
|
||||
const { UrlbarTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/UrlbarTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
let resetTelemetry = async () => {
|
||||
await Services.fog.testFlushAllChildren();
|
||||
Services.fog.testResetFOG();
|
||||
|
@ -15,10 +19,17 @@ let resetTelemetry = async () => {
|
|||
let win;
|
||||
|
||||
add_setup(async () => {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["browser.tabs.groups.enabled", true],
|
||||
["browser.urlbar.scotchBonnet.enableOverride", true],
|
||||
],
|
||||
});
|
||||
win = await BrowserTestUtils.openNewBrowserWindow();
|
||||
win.gTabsPanel.init();
|
||||
registerCleanupFunction(async () => {
|
||||
await BrowserTestUtils.closeWindow(win);
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -349,56 +360,38 @@ async function closeTabsMenu() {
|
|||
await hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {XULToolbarButton} triggerNode
|
||||
* @param {string} contextMenuId
|
||||
* @returns {Promise<XULMenuElement|XULPopupElement>}
|
||||
*/
|
||||
async function getContextMenu(triggerNode, contextMenuId) {
|
||||
let nodeWindow = triggerNode.ownerGlobal;
|
||||
triggerNode.scrollIntoView();
|
||||
const contextMenu = nodeWindow.document.getElementById(contextMenuId);
|
||||
const contextMenuShown = BrowserTestUtils.waitForPopupEvent(
|
||||
contextMenu,
|
||||
"shown"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
triggerNode,
|
||||
{ type: "contextmenu", button: 2 },
|
||||
nodeWindow
|
||||
);
|
||||
await contextMenuShown;
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {XULMenuElement|XULPopupElement} contextMenu
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function closeContextMenu(contextMenu) {
|
||||
let menuHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
|
||||
contextMenu.hidePopup();
|
||||
await menuHidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new basic, unnamed tab group that is fully loaded in the browser
|
||||
* and in session state.
|
||||
*
|
||||
* @returns {Promise<MozTabbrowserTabGroup>}
|
||||
*/
|
||||
async function makeTabGroup() {
|
||||
async function makeTabGroup(name = "") {
|
||||
let tab = BrowserTestUtils.addTab(win.gBrowser, "https://example.com");
|
||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
await TabStateFlusher.flush(tab.linkedBrowser);
|
||||
|
||||
let group = win.gBrowser.addTabGroup([tab]);
|
||||
let group = win.gBrowser.addTabGroup([tab], { label: name });
|
||||
// Close the automatically-opened "create tab group" menu.
|
||||
win.gBrowser.tabGroupMenu.close();
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a basic tab group from makeTabGroup and saves it.
|
||||
*
|
||||
* @returns {string} the ID of the saved group
|
||||
*/
|
||||
async function saveAndCloseGroup(group) {
|
||||
let closedObjectsChanged = TestUtils.topicObserved(
|
||||
"sessionstore-closed-objects-changed"
|
||||
);
|
||||
group.ownerGlobal.SessionStore.addSavedTabGroup(group);
|
||||
await removeTabGroup(group);
|
||||
await closedObjectsChanged;
|
||||
return group.id;
|
||||
}
|
||||
|
||||
add_task(async function test_tabOverflowContextMenu_deleteOpenTabGroup() {
|
||||
await resetTelemetry();
|
||||
|
||||
|
@ -446,3 +439,194 @@ add_task(async function test_tabOverflowContextMenu_deleteOpenTabGroup() {
|
|||
|
||||
await resetTelemetry();
|
||||
});
|
||||
|
||||
async function waitForReopenRecord() {
|
||||
return BrowserTestUtils.waitForCondition(() => {
|
||||
let tabGroupReopenTelemetry = Glean.tabgroup.reopen.testGetValue();
|
||||
return tabGroupReopenTelemetry?.length > 0;
|
||||
}, "Waiting for reopen telemetry to populate");
|
||||
}
|
||||
function assertReopenEvent({ id, source, layout, type }) {
|
||||
let tabGroupReopenEvents = Glean.tabgroup.reopen.testGetValue();
|
||||
Assert.equal(
|
||||
tabGroupReopenEvents.length,
|
||||
1,
|
||||
"should have recorded one tabgroup.reopen event"
|
||||
);
|
||||
|
||||
let [reopenEvent] = tabGroupReopenEvents;
|
||||
|
||||
Assert.deepEqual(
|
||||
reopenEvent.extra,
|
||||
{
|
||||
id,
|
||||
source,
|
||||
layout,
|
||||
type,
|
||||
},
|
||||
"should have recorded correct id, source, and layout for reopen event"
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForNoActiveGroups() {
|
||||
return BrowserTestUtils.waitForCondition(
|
||||
() => !win.gBrowser.getAllTabGroups().length,
|
||||
"waiting for an empty group list"
|
||||
);
|
||||
}
|
||||
|
||||
async function doReopenTests(useVerticalTabs) {
|
||||
await waitForNoActiveGroups();
|
||||
Assert.ok(!win.gBrowser.getAllTabGroups().length, "there are no tab groups");
|
||||
Assert.ok(!win.SessionStore.savedGroups.length, "no saved groups");
|
||||
let expectedLayout = useVerticalTabs ? "vertical" : "horizontal";
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["sidebar.revamp", true],
|
||||
["sidebar.verticalTabs", useVerticalTabs],
|
||||
],
|
||||
});
|
||||
let group = await makeTabGroup("reopen-test");
|
||||
let groupId = await saveAndCloseGroup(group);
|
||||
|
||||
info("Restoring from overflow menu");
|
||||
await waitForNoActiveGroups();
|
||||
let menu = await openTabsMenu();
|
||||
let groupItems = menu.querySelectorAll(
|
||||
"#allTabsMenu-groupsView .all-tabs-group-action-button"
|
||||
);
|
||||
Assert.equal(groupItems.length, 1, "1 group in menu");
|
||||
let groupButton = groupItems[0];
|
||||
Assert.equal(
|
||||
groupButton.getAttribute("data-tab-group-id"),
|
||||
groupId,
|
||||
"Correct group appears in menu"
|
||||
);
|
||||
groupButton.click();
|
||||
await waitForReopenRecord();
|
||||
assertReopenEvent({
|
||||
id: groupId,
|
||||
source: "tab_overflow",
|
||||
layout: expectedLayout,
|
||||
type: "saved",
|
||||
});
|
||||
await resetTelemetry();
|
||||
await saveAndCloseGroup(win.gBrowser.getTabGroupById(groupId));
|
||||
|
||||
info("restoring saved group via undoClosetab");
|
||||
await waitForNoActiveGroups();
|
||||
undoCloseTab(undefined, win.__SSi);
|
||||
await waitForReopenRecord();
|
||||
assertReopenEvent({
|
||||
id: groupId,
|
||||
source: "recent",
|
||||
layout: expectedLayout,
|
||||
type: "saved",
|
||||
});
|
||||
await addTab("about:blank"); // removed by undoCloseTab
|
||||
await saveAndCloseGroup(win.gBrowser.getTabGroupById(groupId));
|
||||
await resetTelemetry();
|
||||
|
||||
info("restoring saved group from URLbar suggestion");
|
||||
await waitForNoActiveGroups();
|
||||
await UrlbarTestUtils.promiseAutocompleteResultPopup({
|
||||
window: win,
|
||||
waitForFocus,
|
||||
value: "reopen-test",
|
||||
fireInputEvent: true,
|
||||
reopenOnBlur: true,
|
||||
});
|
||||
let reopenGroupButton = win.gURLBar.panel.querySelector(
|
||||
`[data-action^="tabgroup"]`
|
||||
);
|
||||
Assert.ok(!!reopenGroupButton, "Reopen group action is present in results");
|
||||
let closedObjectsChanged = TestUtils.topicObserved(
|
||||
"sessionstore-closed-objects-changed"
|
||||
);
|
||||
await UrlbarTestUtils.promisePopupClose(win, () => {
|
||||
EventUtils.synthesizeKey("KEY_Tab", {}, win);
|
||||
EventUtils.synthesizeKey("KEY_Enter", {}, win);
|
||||
});
|
||||
await closedObjectsChanged;
|
||||
await waitForReopenRecord();
|
||||
assertReopenEvent({
|
||||
id: groupId,
|
||||
source: "suggest",
|
||||
layout: expectedLayout,
|
||||
type: "saved",
|
||||
});
|
||||
|
||||
await win.gBrowser.removeTabGroup(win.gBrowser.getTabGroupById(groupId));
|
||||
await resetTelemetry();
|
||||
await SpecialPowers.popPrefEnv();
|
||||
}
|
||||
|
||||
add_task(async function test_reopenSavedGroupTelemetry() {
|
||||
info("Perform reopen tests in horizontal tabs mode");
|
||||
await doReopenTests(false);
|
||||
info("Perform reopen tests in vertical tabs mode");
|
||||
await doReopenTests(true);
|
||||
});
|
||||
|
||||
add_task(async function test_tabContextMenu_addTabsToGroup() {
|
||||
await resetTelemetry();
|
||||
|
||||
// `tabgroup.add_tab` is disabled by default and enabled by server knobs,
|
||||
// so this test needs to enable it manually in order to test it.
|
||||
Services.fog.applyServerKnobsConfig(
|
||||
JSON.stringify({
|
||||
metrics_enabled: {
|
||||
"tabgroup.add_tab": true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
info("set up a tab group to test with");
|
||||
let group = await makeTabGroup();
|
||||
let groupId = group.id;
|
||||
|
||||
info("create 8 ungrouped tabs to test with");
|
||||
let moreTabs = Array.from({ length: 8 }).map(() =>
|
||||
BrowserTestUtils.addTab(win.gBrowser, "https://example.com")
|
||||
);
|
||||
|
||||
info("select first ungrouped tab and multi-select three more tabs");
|
||||
win.gBrowser.selectedTab = moreTabs[0];
|
||||
moreTabs.slice(1, 4).forEach(tab => win.gBrowser.addToMultiSelectedTabs(tab));
|
||||
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return win.gBrowser.multiSelectedTabsCount == 4;
|
||||
}, "Wait for Tabbrowser to update the multiselected tab state");
|
||||
|
||||
let menu = await getContextMenu(win.gBrowser.selectedTab, "tabContextMenu");
|
||||
let moveTabToGroupItem = win.document.getElementById(
|
||||
"context_moveTabToGroup"
|
||||
);
|
||||
let tabGroupButton = moveTabToGroupItem.querySelector(
|
||||
`[tab-group-id="${groupId}"]`
|
||||
);
|
||||
tabGroupButton.click();
|
||||
await closeContextMenu(menu);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(() => {
|
||||
return Glean.tabgroup.addTab.testGetValue() !== null;
|
||||
}, "Wait for a Glean event to be recorded");
|
||||
|
||||
let [addTabEvent] = Glean.tabgroup.addTab.testGetValue();
|
||||
Assert.deepEqual(
|
||||
addTabEvent.extra,
|
||||
{
|
||||
source: "tab_menu",
|
||||
tabs: "4",
|
||||
layout: "horizontal",
|
||||
},
|
||||
"should have recorded the correct event metadata"
|
||||
);
|
||||
|
||||
for (let tab of moreTabs) {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
await removeTabGroup(group);
|
||||
|
||||
await resetTelemetry();
|
||||
});
|
||||
|
|
|
@ -601,3 +601,36 @@ async function removeTabGroup(group) {
|
|||
await group.ownerGlobal.gBrowser.removeTabGroup(group, { animate: false });
|
||||
await removePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Node} triggerNode
|
||||
* @param {string} contextMenuId
|
||||
* @returns {Promise<XULMenuElement|XULPopupElement>}
|
||||
*/
|
||||
async function getContextMenu(triggerNode, contextMenuId) {
|
||||
let win = triggerNode.ownerGlobal;
|
||||
triggerNode.scrollIntoView({ behavior: "instant" });
|
||||
const contextMenu = win.document.getElementById(contextMenuId);
|
||||
const contextMenuShown = BrowserTestUtils.waitForPopupEvent(
|
||||
contextMenu,
|
||||
"shown"
|
||||
);
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(
|
||||
triggerNode,
|
||||
{ type: "contextmenu", button: 2 },
|
||||
win
|
||||
);
|
||||
await contextMenuShown;
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {XULMenuElement|XULPopupElement} contextMenu
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function closeContextMenu(contextMenu) {
|
||||
let menuHidden = BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
|
||||
contextMenu.hidePopup();
|
||||
await menuHidden;
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|||
const kWebAppWindowFeatures =
|
||||
"chrome,dialog=no,titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars";
|
||||
|
||||
let lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
|
||||
});
|
||||
|
||||
export let TaskbarTabs = {
|
||||
async init(window) {
|
||||
if (
|
||||
AppConstants.platform != "win" ||
|
||||
!Services.prefs.getBoolPref(kEnabledPref, false) ||
|
||||
window.document.documentElement.hasAttribute("taskbartab")
|
||||
window.document.documentElement.hasAttribute("taskbartab") ||
|
||||
!window.toolbar.visible ||
|
||||
lazy.PrivateBrowsingUtils.isWindowPrivate(window)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
|
5
browser/components/tests/browser/eval/browser.toml
Normal file
5
browser/components/tests/browser/eval/browser.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[DEFAULT]
|
||||
# Startup pref, needs to be set in [DEFAULT]
|
||||
prefs = ["security.allow_unsafe_dangerous_privileged_evil_eval=true"]
|
||||
|
||||
["browser_csp_eval.js"]
|
18
browser/components/tests/browser/eval/browser_csp_eval.js
Normal file
18
browser/components/tests/browser/eval/browser_csp_eval.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(function eval_allowed_with_special_pref() {
|
||||
is(
|
||||
window.location.pathname,
|
||||
"/content/browser.xhtml",
|
||||
"Running in browser.xhtml"
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
eval("window.test_code_ran = 1;");
|
||||
is(window.test_code_ran, 1, "eval() executed successfully");
|
||||
|
||||
delete window.test_code_ran;
|
||||
});
|
|
@ -655,19 +655,10 @@ var FullPageTranslationsPanel = new (class {
|
|||
"full-page-translations-panel-view-default"
|
||||
);
|
||||
|
||||
if (!this._hasShownPanel) {
|
||||
actor.firstShowUriSpec = gBrowser.currentURI.spec;
|
||||
}
|
||||
|
||||
if (
|
||||
this._hasShownPanel &&
|
||||
gBrowser.currentURI.spec !== actor.firstShowUriSpec
|
||||
) {
|
||||
document.l10n.setAttributes(header, "translations-panel-header");
|
||||
actor.firstShowUriSpec = null;
|
||||
if (TranslationsParent.hasUserEverTranslated()) {
|
||||
intro.hidden = true;
|
||||
document.l10n.setAttributes(header, "translations-panel-header");
|
||||
} else {
|
||||
Services.prefs.setBoolPref("browser.translations.panelShown", true);
|
||||
intro.hidden = false;
|
||||
document.l10n.setAttributes(header, "translations-panel-intro-header");
|
||||
}
|
||||
|
@ -1450,11 +1441,9 @@ var FullPageTranslationsPanel = new (class {
|
|||
async #showEngineError(actor) {
|
||||
const { button } = this.buttonElements;
|
||||
await this.#ensureLangListsBuilt();
|
||||
if (!this.#isShowingDefaultView()) {
|
||||
await this.#showDefaultView(actor).catch(e => {
|
||||
this.console?.error(e);
|
||||
});
|
||||
}
|
||||
await this.#showDefaultView(actor).catch(e => {
|
||||
this.console?.error(e);
|
||||
});
|
||||
this.elements.error.hidden = false;
|
||||
this.#showError({
|
||||
message: "translations-panel-error-translating",
|
||||
|
@ -1647,10 +1636,7 @@ var FullPageTranslationsPanel = new (class {
|
|||
|
||||
// Follow the same rules for displaying the first-run intro text for the
|
||||
// button's accessible tooltip label.
|
||||
if (
|
||||
this._hasShownPanel &&
|
||||
gBrowser.currentURI.spec !== actor.firstShowUriSpec
|
||||
) {
|
||||
if (TranslationsParent.hasUserEverTranslated()) {
|
||||
document.l10n.setAttributes(
|
||||
button,
|
||||
"urlbar-translations-button2"
|
||||
|
@ -1688,10 +1674,3 @@ var FullPageTranslationsPanel = new (class {
|
|||
}
|
||||
};
|
||||
})();
|
||||
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
FullPageTranslationsPanel,
|
||||
"_hasShownPanel",
|
||||
"browser.translations.panelShown",
|
||||
false
|
||||
);
|
||||
|
|
|
@ -27,7 +27,7 @@ add_task(
|
|||
await FullPageTranslationsTestUtils.openPanel({
|
||||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
|
||||
await FullPageTranslationsTestUtils.clickTranslateButton();
|
||||
|
|
|
@ -27,7 +27,7 @@ add_task(
|
|||
await FullPageTranslationsTestUtils.openPanel({
|
||||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
|
||||
await FullPageTranslationsTestUtils.clickTranslateButton();
|
||||
|
|
|
@ -26,7 +26,7 @@ add_task(async function test_browser_translations_full_page_multiple_windows() {
|
|||
await FullPageTranslationsTestUtils.openPanel({
|
||||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
|
||||
await FullPageTranslationsTestUtils.clickCancelButton();
|
||||
|
|
|
@ -36,14 +36,14 @@ add_task(async function test_translations_moz_extension() {
|
|||
"The button is available."
|
||||
);
|
||||
|
||||
is(button.getAttribute("data-l10n-id"), "urlbar-translations-button2");
|
||||
is(button.getAttribute("data-l10n-id"), "urlbar-translations-button-intro");
|
||||
|
||||
await FullPageTranslationsTestUtils.assertPageIsUntranslated(runInPage);
|
||||
|
||||
await FullPageTranslationsTestUtils.openPanel({
|
||||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
|
||||
await FullPageTranslationsTestUtils.clickTranslateButton({
|
||||
|
|
|
@ -29,7 +29,7 @@ add_task(async function test_browser_translations_full_page_multiple_windows() {
|
|||
await FullPageTranslationsTestUtils.openPanel({
|
||||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
await FullPageTranslationsTestUtils.clickTranslateButton({
|
||||
downloadHandler: testPage1.resolveDownloads,
|
||||
|
|
|
@ -21,7 +21,7 @@ add_task(async function test_translations_panel_a11y_focus() {
|
|||
expectedFromLanguage: "es",
|
||||
expectedToLanguage: "en",
|
||||
openWithKeyboard: true,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewDefault,
|
||||
onOpenPanel: FullPageTranslationsTestUtils.assertPanelViewIntro,
|
||||
});
|
||||
|
||||
is(
|
||||
|
|
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