Update On Thu Mar 14 19:42:58 CET 2024
This commit is contained in:
parent
8150b844db
commit
c6d5964c58
745 changed files with 34957 additions and 43147 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -3880,8 +3880,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "neqo-common"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.1#721a1eff430f644e23a3e06ef4c5a248a0cbf592"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.2#ce5cbe4dfc2e38b238abb022c39eee4215058221"
|
||||
dependencies = [
|
||||
"enum-map",
|
||||
"env_logger",
|
||||
|
@ -3893,8 +3893,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "neqo-crypto"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.1#721a1eff430f644e23a3e06ef4c5a248a0cbf592"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.2#ce5cbe4dfc2e38b238abb022c39eee4215058221"
|
||||
dependencies = [
|
||||
"bindgen 0.69.4",
|
||||
"log",
|
||||
|
@ -3907,8 +3907,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "neqo-http3"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.1#721a1eff430f644e23a3e06ef4c5a248a0cbf592"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.2#ce5cbe4dfc2e38b238abb022c39eee4215058221"
|
||||
dependencies = [
|
||||
"enumset",
|
||||
"log",
|
||||
|
@ -3924,8 +3924,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "neqo-qpack"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.1#721a1eff430f644e23a3e06ef4c5a248a0cbf592"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.2#ce5cbe4dfc2e38b238abb022c39eee4215058221"
|
||||
dependencies = [
|
||||
"log",
|
||||
"neqo-common",
|
||||
|
@ -3937,8 +3937,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "neqo-transport"
|
||||
version = "0.7.1"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.1#721a1eff430f644e23a3e06ef4c5a248a0cbf592"
|
||||
version = "0.7.2"
|
||||
source = "git+https://github.com/mozilla/neqo?tag=v0.7.2#ce5cbe4dfc2e38b238abb022c39eee4215058221"
|
||||
dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"log",
|
||||
|
@ -4013,6 +4013,17 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nmhproxy"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mozbuild",
|
||||
"mozilla-central-workspace-hack",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
# and do not need to be listed here. Their external dependencies are vendored
|
||||
# into `third_party/rust` by `mach vendor rust`.
|
||||
members = [
|
||||
"browser/app/nmhproxy/",
|
||||
"js/src/frontend/smoosh",
|
||||
"js/src/rust",
|
||||
"netwerk/test/http3server",
|
||||
|
|
|
@ -844,7 +844,7 @@ ROLE(LISTBOX,
|
|||
ROLE_SYSTEM_LIST,
|
||||
java::SessionAccessibility::CLASSNAME_LISTVIEW,
|
||||
IsAccessibilityElementRule::IfChildlessWithNameAndFocusable,
|
||||
eNoNameRule)
|
||||
eNameFromValueRule)
|
||||
|
||||
ROLE(FLAT_EQUATION,
|
||||
"flat equation",
|
||||
|
|
|
@ -177,6 +177,16 @@ nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
|
|||
|
||||
bool isEmptyTextEquiv = true;
|
||||
|
||||
// Attempt to find the value. If it's non-empty, append and return it. See the
|
||||
// "embedded control" section of the name spec.
|
||||
nsAutoString val;
|
||||
nsresult rv = AppendFromValue(aAccessible, &val);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
if (rv == NS_OK) {
|
||||
AppendString(aString, val);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If the name is from tooltip then append it to result string in the end
|
||||
// (see h. step of name computation guide).
|
||||
nsAutoString text;
|
||||
|
@ -184,12 +194,6 @@ nsresult nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible,
|
|||
isEmptyTextEquiv = !AppendString(aString, text);
|
||||
}
|
||||
|
||||
// Implementation of f. step.
|
||||
nsresult rv = AppendFromValue(aAccessible, aString);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) isEmptyTextEquiv = false;
|
||||
|
||||
// Implementation of g) step of text equivalent computation guide. Go down
|
||||
// into subtree if accessible allows "text equivalent from subtree rule" or
|
||||
// it's not root and not control.
|
||||
|
@ -230,6 +234,19 @@ nsresult nsTextEquivUtils::AppendFromValue(Accessible* aAccessible,
|
|||
|
||||
nsAutoString text;
|
||||
if (aAccessible != sInitiatorAcc) {
|
||||
// For listboxes in non-initiator computations, we need to get the selected
|
||||
// item and append its text alternative.
|
||||
if (aAccessible->IsListControl()) {
|
||||
Accessible* selected = aAccessible->GetSelectedItem(0);
|
||||
if (selected) {
|
||||
nsresult rv = AppendFromAccessible(selected, &text);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
return AppendString(aString, text) ? NS_OK
|
||||
: NS_OK_NO_NAME_CLAUSE_HANDLED;
|
||||
}
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
aAccessible->Value(text);
|
||||
|
||||
return AppendString(aString, text) ? NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED;
|
||||
|
|
|
@ -9,19 +9,28 @@ import ctypes
|
|||
import os
|
||||
from ctypes import POINTER, byref
|
||||
from ctypes.wintypes import BOOL, HWND, LPARAM, POINT # noqa: F401
|
||||
from dataclasses import dataclass
|
||||
|
||||
import comtypes.automation
|
||||
import comtypes.client
|
||||
import psutil
|
||||
from comtypes import COMError, IServiceProvider
|
||||
|
||||
CHILDID_SELF = 0
|
||||
COWAIT_DEFAULT = 0
|
||||
EVENT_OBJECT_FOCUS = 0x8005
|
||||
GA_ROOT = 2
|
||||
NAVRELATION_EMBEDS = 0x1009
|
||||
OBJID_CLIENT = -4
|
||||
RPC_S_CALLPENDING = -2147417835
|
||||
WINEVENT_OUTOFCONTEXT = 0
|
||||
WM_CLOSE = 0x0010
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
oleacc = ctypes.oledll.oleacc
|
||||
oleaccMod = comtypes.client.GetModule("oleacc.dll")
|
||||
IAccessible = oleaccMod.IAccessible
|
||||
del oleaccMod
|
||||
OBJID_CLIENT = -4
|
||||
CHILDID_SELF = 0
|
||||
NAVRELATION_EMBEDS = 0x1009
|
||||
# This is the path if running locally.
|
||||
ia2Tlb = os.path.join(
|
||||
os.getcwd(),
|
||||
|
@ -65,6 +74,13 @@ def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT):
|
|||
return p
|
||||
|
||||
|
||||
def getWindowClass(hwnd):
|
||||
MAX_CHARS = 257
|
||||
buffer = ctypes.create_unicode_buffer(MAX_CHARS)
|
||||
user32.GetClassNameW(hwnd, buffer, MAX_CHARS)
|
||||
return buffer.value
|
||||
|
||||
|
||||
def getFirefoxHwnd():
|
||||
"""Search all top level windows for the Firefox instance being
|
||||
tested.
|
||||
|
@ -78,9 +94,7 @@ def getFirefoxHwnd():
|
|||
|
||||
@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
|
||||
def callback(hwnd, lParam):
|
||||
name = ctypes.create_unicode_buffer(100)
|
||||
user32.GetClassNameW(hwnd, name, 100)
|
||||
if name.value != "MozillaWindowClass":
|
||||
if getWindowClass(hwnd) != "MozillaWindowClass":
|
||||
return True
|
||||
pid = ctypes.wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, byref(pid))
|
||||
|
@ -127,6 +141,106 @@ def findIa2ByDomId(root, id):
|
|||
return descendant
|
||||
|
||||
|
||||
@dataclass
|
||||
class WinEvent:
|
||||
event: int
|
||||
hwnd: int
|
||||
objectId: int
|
||||
childId: int
|
||||
|
||||
def getIa2(self):
|
||||
acc = ctypes.POINTER(IAccessible)()
|
||||
child = comtypes.automation.VARIANT()
|
||||
ctypes.oledll.oleacc.AccessibleObjectFromEvent(
|
||||
self.hwnd,
|
||||
self.objectId,
|
||||
self.childId,
|
||||
ctypes.byref(acc),
|
||||
ctypes.byref(child),
|
||||
)
|
||||
if child.value != CHILDID_SELF:
|
||||
# This isn't an IAccessible2 object.
|
||||
return None
|
||||
return toIa2(acc)
|
||||
|
||||
|
||||
class WaitForWinEvent:
|
||||
"""Wait for a win event, usually for IAccessible2.
|
||||
This should be used as follows:
|
||||
1. Create an instance to wait for the desired event.
|
||||
2. Perform the action that should fire the event.
|
||||
3. Call wait() on the instance you created in 1) to wait for the event.
|
||||
"""
|
||||
|
||||
def __init__(self, eventId, match):
|
||||
"""event is the event id to wait for.
|
||||
match is either None to match any object, an str containing the DOM id
|
||||
of the desired object, or a function taking a WinEvent which should
|
||||
return True if this is the requested event.
|
||||
"""
|
||||
self._matched = None
|
||||
# A kernel event used to signal when we get the desired event.
|
||||
self._signal = ctypes.windll.kernel32.CreateEventW(None, True, False, None)
|
||||
|
||||
# We define this as a nested function because it has to be a static
|
||||
# function, but we need a reference to self.
|
||||
@ctypes.WINFUNCTYPE(
|
||||
None,
|
||||
ctypes.wintypes.HANDLE,
|
||||
ctypes.wintypes.DWORD,
|
||||
ctypes.wintypes.HWND,
|
||||
ctypes.wintypes.LONG,
|
||||
ctypes.wintypes.LONG,
|
||||
ctypes.wintypes.DWORD,
|
||||
ctypes.wintypes.DWORD,
|
||||
)
|
||||
def winEventProc(hook, eventId, hwnd, objectId, childId, thread, time):
|
||||
event = WinEvent(eventId, hwnd, objectId, childId)
|
||||
if isinstance(match, str):
|
||||
try:
|
||||
ia2 = event.getIa2()
|
||||
if f"id:{match};" in ia2.attributes:
|
||||
self._matched = event
|
||||
except (comtypes.COMError, TypeError):
|
||||
pass
|
||||
elif callable(match):
|
||||
try:
|
||||
if match(event):
|
||||
self._matched = event
|
||||
except Exception as e:
|
||||
self._matched = e
|
||||
if self._matched:
|
||||
ctypes.windll.kernel32.SetEvent(self._signal)
|
||||
|
||||
self._hook = user32.SetWinEventHook(
|
||||
eventId, eventId, None, winEventProc, 0, 0, WINEVENT_OUTOFCONTEXT
|
||||
)
|
||||
# Hold a reference to winEventProc so it doesn't get destroyed.
|
||||
self._proc = winEventProc
|
||||
|
||||
def wait(self):
|
||||
"""Wait for and return the desired WinEvent."""
|
||||
# Pump Windows messages until we get the desired event, which will be
|
||||
# signalled using a kernel event.
|
||||
handles = (ctypes.c_void_p * 1)(self._signal)
|
||||
index = ctypes.wintypes.DWORD()
|
||||
TIMEOUT = 10000
|
||||
try:
|
||||
ctypes.oledll.ole32.CoWaitForMultipleHandles(
|
||||
COWAIT_DEFAULT, TIMEOUT, 1, handles, ctypes.byref(index)
|
||||
)
|
||||
except WindowsError as e:
|
||||
if e.winerror == RPC_S_CALLPENDING:
|
||||
raise TimeoutError("Timeout before desired event received")
|
||||
raise
|
||||
finally:
|
||||
user32.UnhookWinEvent(self._hook)
|
||||
self._proc = None
|
||||
if isinstance(self._matched, Exception):
|
||||
raise self._matched from self._matched
|
||||
return self._matched
|
||||
|
||||
|
||||
def getDocUia():
|
||||
"""Get the IUIAutomationElement for the document being tested."""
|
||||
# We start with IAccessible2 because there's no efficient way to
|
||||
|
|
|
@ -6,4 +6,6 @@ skip-if = [
|
|||
]
|
||||
support-files = ["head.js"]
|
||||
|
||||
["browser_osPicker.js"]
|
||||
|
||||
["browser_role.js"]
|
||||
|
|
51
accessible/tests/browser/windows/ia2/browser_osPicker.js
Normal file
51
accessible/tests/browser/windows/ia2/browser_osPicker.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
addAccessibleTask(
|
||||
`<input id="file" type="file">`,
|
||||
async function (browser, docAcc) {
|
||||
info("Focusing file input");
|
||||
await runPython(`
|
||||
global focused
|
||||
focused = WaitForWinEvent(EVENT_OBJECT_FOCUS, "file")
|
||||
`);
|
||||
const file = findAccessibleChildByID(docAcc, "file");
|
||||
file.takeFocus();
|
||||
await runPython(`
|
||||
focused.wait()
|
||||
`);
|
||||
ok(true, "file input got focus");
|
||||
info("Opening file picker");
|
||||
await runPython(`
|
||||
global focused
|
||||
focused = WaitForWinEvent(
|
||||
EVENT_OBJECT_FOCUS,
|
||||
lambda evt: getWindowClass(evt.hwnd) == "Edit"
|
||||
)
|
||||
`);
|
||||
file.doAction(0);
|
||||
await runPython(`
|
||||
global event
|
||||
event = focused.wait()
|
||||
`);
|
||||
ok(true, "Picker got focus");
|
||||
info("Dismissing picker");
|
||||
await runPython(`
|
||||
# If the picker is dismissed too quickly, it seems to re-enable the root
|
||||
# window before we do. This sleep isn't ideal, but it's more likely to
|
||||
# reproduce the case that our root window gets focus before it is enabled.
|
||||
# See bug 1883568 for further details.
|
||||
import time
|
||||
time.sleep(1)
|
||||
focused = WaitForWinEvent(EVENT_OBJECT_FOCUS, "file")
|
||||
# Sending key presses to the picker is unreliable, so use WM_CLOSE.
|
||||
pickerRoot = user32.GetAncestor(event.hwnd, GA_ROOT)
|
||||
user32.SendMessageW(pickerRoot, WM_CLOSE, 0, 0)
|
||||
focused.wait()
|
||||
`);
|
||||
ok(true, "file input got focus");
|
||||
}
|
||||
);
|
|
@ -553,22 +553,34 @@ export let ContentSearch = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Converts the engine's icon into an appropriate URL for display at
|
||||
* Converts the engine's icon into a URL or an ArrayBuffer for passing to the
|
||||
* content process.
|
||||
*
|
||||
* @param {nsISearchEngine} engine
|
||||
* The engine to get the icon for.
|
||||
* @returns {string|ArrayBuffer}
|
||||
* The icon's URL or an ArrayBuffer containing the icon data.
|
||||
*/
|
||||
async _getEngineIconURL(engine) {
|
||||
let url = engine.getIconURL();
|
||||
let url = await engine.getIconURL();
|
||||
if (!url) {
|
||||
return SEARCH_ENGINE_PLACEHOLDER_ICON;
|
||||
}
|
||||
|
||||
// The uri received here can be of two types
|
||||
// The uri received here can be one of several types:
|
||||
// 1 - moz-extension://[uuid]/path/to/icon.ico
|
||||
// 2 - data:image/x-icon;base64,VERY-LONG-STRING
|
||||
// 3 - blob:
|
||||
//
|
||||
// If the URI is not a data: URI, there's no point in converting
|
||||
// it to an arraybuffer (which is used to optimize passing the data
|
||||
// accross processes): we can just pass the original URI, which is cheaper.
|
||||
if (!url.startsWith("data:")) {
|
||||
// For moz-extension URIs we can pass the URI to the content process and
|
||||
// use it directly as they can be accessed from there and it is cheaper.
|
||||
//
|
||||
// For blob URIs the content process is a different scope and we can't share
|
||||
// the blob with that scope. Hence we have to create a copy of the data.
|
||||
//
|
||||
// For data: URIs we convert to an ArrayBuffer as that is more optimal for
|
||||
// passing the data across to the content process.
|
||||
if (!url.startsWith("data:") && !url.startsWith("blob:")) {
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#if defined(MOZ_CRASHREPORTER)
|
||||
/minidump-analyzer
|
||||
#endif
|
||||
/nmhproxy
|
||||
/pingsender
|
||||
/pk12util
|
||||
/ssltunnel
|
||||
|
|
|
@ -135,6 +135,9 @@ if CONFIG["MOZ_SANDBOX"] and CONFIG["OS_ARCH"] == "WINNT":
|
|||
"usp10.dll",
|
||||
]
|
||||
|
||||
if CONFIG["TARGET_OS"] in ("WINNT", "OSX"):
|
||||
DIRS += ["nmhproxy"]
|
||||
|
||||
# Control the default heap size.
|
||||
# This is the heap returned by GetProcessHeap().
|
||||
# As we use the CRT heap, the default size is too large and wastes VM.
|
||||
|
|
17
browser/app/nmhproxy/Cargo.toml
Normal file
17
browser/app/nmhproxy/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "nmhproxy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "A lightweight native messaging listener executable for the Firefox Bridge extension which launches Firefox in regular or private modes, avoiding the need to convert Firefox itself into a listener."
|
||||
|
||||
[[bin]]
|
||||
name = "nmhproxy"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
mozbuild = "0.1"
|
||||
mozilla-central-workspace-hack = { version = "0.1", features = ["nmhproxy"], optional = true }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1.0"
|
||||
url = "2.4"
|
15
browser/app/nmhproxy/moz.build
Normal file
15
browser/app/nmhproxy/moz.build
Normal file
|
@ -0,0 +1,15 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
RUST_PROGRAMS += [
|
||||
"nmhproxy",
|
||||
]
|
||||
|
||||
# Ideally, the build system would set @rpath to be @executable_path as
|
||||
# a default for this executable so that this addition to LDFLAGS would not be
|
||||
# needed here. Bug 1772575 is filed to implement that.
|
||||
if CONFIG["OS_ARCH"] == "Darwin":
|
||||
LDFLAGS += ["-Wl,-rpath,@executable_path"]
|
350
browser/app/nmhproxy/src/commands.rs
Normal file
350
browser/app/nmhproxy/src/commands.rs
Normal file
|
@ -0,0 +1,350 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::Command;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const OS_NAME: &str = "windows";
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const OS_NAME: &str = "macos";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "command", content = "data")]
|
||||
// {
|
||||
// "command": "LaunchFirefox",
|
||||
// "data": {"url": "https://example.com"},
|
||||
// }
|
||||
pub enum FirefoxCommand {
|
||||
LaunchFirefox { url: String },
|
||||
LaunchFirefoxPrivate { url: String },
|
||||
GetVersion {},
|
||||
}
|
||||
#[derive(Serialize, Deserialize)]
|
||||
// {
|
||||
// "message": "Successful launch",
|
||||
// "result_code": 1,
|
||||
// }
|
||||
pub struct Response {
|
||||
pub message: String,
|
||||
pub result_code: u32,
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
pub enum ResultCode {
|
||||
Success = 0,
|
||||
Error = 1,
|
||||
}
|
||||
impl From<ResultCode> for u32 {
|
||||
fn from(m: ResultCode) -> u32 {
|
||||
m as u32
|
||||
}
|
||||
}
|
||||
|
||||
trait CommandRunner {
|
||||
fn new() -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
fn arg(&mut self, arg: &str) -> &mut Self;
|
||||
fn args(&mut self, args: &[&str]) -> &mut Self;
|
||||
fn spawn(&mut self) -> std::io::Result<()>;
|
||||
fn to_string(&mut self) -> std::io::Result<String>;
|
||||
}
|
||||
|
||||
impl CommandRunner for Command {
|
||||
fn new() -> Self {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use mozbuild::config::MOZ_APP_NAME;
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
// Get the current executable's path, we know Firefox is in the
|
||||
// same folder is nmhproxy.exe so we can use that.
|
||||
let nmh_exe_path = env::current_exe().unwrap();
|
||||
let nmh_exe_folder = nmh_exe_path.parent().unwrap_or_else(|| Path::new(""));
|
||||
let moz_exe_path = nmh_exe_folder.join(format!("{}.exe", MOZ_APP_NAME));
|
||||
Command::new(moz_exe_path)
|
||||
}
|
||||
}
|
||||
fn arg(&mut self, arg: &str) -> &mut Self {
|
||||
self.arg(arg)
|
||||
}
|
||||
fn args(&mut self, args: &[&str]) -> &mut Self {
|
||||
self.args(args)
|
||||
}
|
||||
fn spawn(&mut self) -> std::io::Result<()> {
|
||||
self.spawn().map(|_| ())
|
||||
}
|
||||
fn to_string(&mut self) -> std::io::Result<String> {
|
||||
Ok("".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
struct MockCommand {
|
||||
command_line: String,
|
||||
}
|
||||
|
||||
impl CommandRunner for MockCommand {
|
||||
fn new() -> Self {
|
||||
MockCommand {
|
||||
command_line: String::new(),
|
||||
}
|
||||
}
|
||||
fn arg(&mut self, arg: &str) -> &mut Self {
|
||||
self.command_line.push_str(arg);
|
||||
self.command_line.push(' ');
|
||||
self
|
||||
}
|
||||
fn args(&mut self, args: &[&str]) -> &mut Self {
|
||||
for arg in args {
|
||||
self.command_line.push_str(arg);
|
||||
self.command_line.push(' ');
|
||||
}
|
||||
self
|
||||
}
|
||||
fn spawn(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn to_string(&mut self) -> std::io::Result<String> {
|
||||
Ok(self.command_line.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// The message length is a 32-bit integer in native byte order
|
||||
pub fn read_message_length<R: Read>(mut reader: R) -> std::io::Result<u32> {
|
||||
let mut buffer = [0u8; 4];
|
||||
reader.read_exact(&mut buffer)?;
|
||||
let length: u32 = u32::from_ne_bytes(buffer);
|
||||
if (length > 0) && (length < 100 * 1024) {
|
||||
Ok(length)
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid message length",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_message_string<R: Read>(mut reader: R, length: u32) -> io::Result<String> {
|
||||
let mut buffer = vec![0u8; length.try_into().unwrap()];
|
||||
reader.read_exact(&mut buffer)?;
|
||||
let message =
|
||||
String::from_utf8(buffer).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub fn process_command(command: &FirefoxCommand) -> std::io::Result<bool> {
|
||||
match &command {
|
||||
FirefoxCommand::LaunchFirefox { url } => {
|
||||
launch_firefox::<Command>(url.to_owned(), false, OS_NAME)?;
|
||||
Ok(true)
|
||||
}
|
||||
FirefoxCommand::LaunchFirefoxPrivate { url } => {
|
||||
launch_firefox::<Command>(url.to_owned(), true, OS_NAME)?;
|
||||
Ok(true)
|
||||
}
|
||||
FirefoxCommand::GetVersion {} => generate_response("1", ResultCode::Success.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_response(message: &str, result_code: u32) -> std::io::Result<bool> {
|
||||
let response_struct = Response {
|
||||
message: message.to_string(),
|
||||
result_code,
|
||||
};
|
||||
let response_str = serde_json::to_string(&response_struct)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let response_len_bytes: [u8; 4] = (response_str.len() as u32).to_ne_bytes();
|
||||
std::io::stdout().write_all(&response_len_bytes)?;
|
||||
std::io::stdout().write_all(response_str.as_bytes())?;
|
||||
std::io::stdout().flush()?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn validate_url(url: String) -> std::io::Result<String> {
|
||||
let parsed_url = Url::parse(url.as_str())
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
match parsed_url.scheme() {
|
||||
"http" | "https" | "file" => Ok(parsed_url.to_string()),
|
||||
_ => Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Invalid URL scheme",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_firefox<C: CommandRunner>(
|
||||
url: String,
|
||||
private: bool,
|
||||
os: &str,
|
||||
) -> std::io::Result<String> {
|
||||
let validated_url: String = validate_url(url)?;
|
||||
let mut command = C::new();
|
||||
if os == "macos" {
|
||||
use mozbuild::config::MOZ_MACBUNDLE_ID;
|
||||
let mut args: [&str; 2] = ["--args", "-url"];
|
||||
if private {
|
||||
args[1] = "-private-window";
|
||||
}
|
||||
command
|
||||
.arg("-n")
|
||||
.arg("-b")
|
||||
.arg(MOZ_MACBUNDLE_ID)
|
||||
.args(&args)
|
||||
.arg(validated_url.as_str());
|
||||
} else if os == "windows" {
|
||||
let mut args: [&str; 2] = ["-osint", "-url"];
|
||||
if private {
|
||||
args[1] = "-private-window";
|
||||
}
|
||||
command.args(&args).arg(validated_url.as_str());
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => generate_response(
|
||||
if private {
|
||||
"Successful private launch"
|
||||
} else {
|
||||
"Sucessful launch"
|
||||
},
|
||||
ResultCode::Success.into(),
|
||||
)?,
|
||||
Err(_) => generate_response(
|
||||
if private {
|
||||
"Failed private launch"
|
||||
} else {
|
||||
"Failed launch"
|
||||
},
|
||||
ResultCode::Error.into(),
|
||||
)?,
|
||||
};
|
||||
command.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
#[test]
|
||||
fn test_validate_url() {
|
||||
let valid_test_cases = vec![
|
||||
"https://example.com/".to_string(),
|
||||
"http://example.com/".to_string(),
|
||||
"file:///path/to/file".to_string(),
|
||||
"https://test.example.com/".to_string(),
|
||||
];
|
||||
|
||||
for input in valid_test_cases {
|
||||
let result = validate_url(input.clone());
|
||||
assert!(result.is_ok(), "Expected Ok, got Err");
|
||||
// Safe to unwrap because we know the result is Ok
|
||||
let ok_value = result.unwrap();
|
||||
assert_eq!(ok_value, input);
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
validate_url("fakeprotocol://test.example.com/".to_string()).map_err(|e| e.kind()),
|
||||
Err(std::io::ErrorKind::InvalidInput)
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
validate_url("invalidURL".to_string()).map_err(|e| e.kind()),
|
||||
Err(std::io::ErrorKind::InvalidData)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_message_length_valid() {
|
||||
let input: [u8; 4] = 256u32.to_ne_bytes();
|
||||
let mut cursor = Cursor::new(input);
|
||||
let length = read_message_length(&mut cursor);
|
||||
assert!(length.is_ok(), "Expected Ok, got Err");
|
||||
assert_eq!(length.unwrap(), 256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_message_length_invalid_too_large() {
|
||||
let input: [u8; 4] = 1_000_000u32.to_ne_bytes();
|
||||
let mut cursor = Cursor::new(input);
|
||||
let result = read_message_length(&mut cursor);
|
||||
assert!(result.is_err());
|
||||
let error = result.err().unwrap();
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_message_length_invalid_zero() {
|
||||
let input: [u8; 4] = 0u32.to_ne_bytes();
|
||||
let mut cursor = Cursor::new(input);
|
||||
let result = read_message_length(&mut cursor);
|
||||
assert!(result.is_err());
|
||||
let error = result.err().unwrap();
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_message_string_valid() {
|
||||
let input_data = b"Valid UTF8 string!";
|
||||
let input_length = input_data.len() as u32;
|
||||
let message = read_message_string(&input_data[..], input_length);
|
||||
assert!(message.is_ok(), "Expected Ok, got Err");
|
||||
assert_eq!(message.unwrap(), "Valid UTF8 string!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_message_string_invalid() {
|
||||
let input_data: [u8; 3] = [0xff, 0xfe, 0xfd];
|
||||
let input_length = input_data.len() as u32;
|
||||
let result = read_message_string(&input_data[..], input_length);
|
||||
assert!(result.is_err());
|
||||
let error = result.err().unwrap();
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_regular_command_macos() {
|
||||
let url = "https://example.com";
|
||||
let result = launch_firefox::<MockCommand>(url.to_string(), false, "macos");
|
||||
assert!(result.is_ok());
|
||||
let command_line = result.unwrap();
|
||||
let correct_url_format = format!("-url {}", url);
|
||||
assert!(command_line.contains(correct_url_format.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_regular_command_windows() {
|
||||
let url = "https://example.com";
|
||||
let result = launch_firefox::<MockCommand>(url.to_string(), false, "windows");
|
||||
assert!(result.is_ok());
|
||||
let command_line = result.unwrap();
|
||||
let correct_url_format = format!("-osint -url {}", url);
|
||||
assert!(command_line.contains(correct_url_format.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_private_command_macos() {
|
||||
let url = "https://example.com";
|
||||
let result = launch_firefox::<MockCommand>(url.to_string(), true, "macos");
|
||||
assert!(result.is_ok());
|
||||
let command_line = result.unwrap();
|
||||
let correct_url_format = format!("-private-window {}", url);
|
||||
assert!(command_line.contains(correct_url_format.as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_launch_private_command_windows() {
|
||||
let url = "https://example.com";
|
||||
let result = launch_firefox::<MockCommand>(url.to_string(), true, "windows");
|
||||
assert!(result.is_ok());
|
||||
let command_line = result.unwrap();
|
||||
let correct_url_format = format!("-osint -private-window {}", url);
|
||||
assert!(command_line.contains(correct_url_format.as_str()));
|
||||
}
|
||||
}
|
55
browser/app/nmhproxy/src/main.rs
Normal file
55
browser/app/nmhproxy/src/main.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* 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/. */
|
||||
|
||||
mod commands;
|
||||
use commands::ResultCode;
|
||||
use std::io::Error;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
// The general structure of these functions is to print error cases to
|
||||
// stdout so that the extension can read them and then do error-handling
|
||||
// on that end.
|
||||
let message_length: u32 =
|
||||
commands::read_message_length(std::io::stdin()).or_else(|_| -> Result<u32, _> {
|
||||
commands::generate_response("Failed to read message length", ResultCode::Error.into())
|
||||
.expect("JSON error");
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Failed to read message length",
|
||||
));
|
||||
})?;
|
||||
let message: String = commands::read_message_string(std::io::stdin(), message_length).or_else(
|
||||
|_| -> Result<String, _> {
|
||||
commands::generate_response("Failed to read message", ResultCode::Error.into())
|
||||
.expect("JSON error");
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Failed to read message",
|
||||
));
|
||||
},
|
||||
)?;
|
||||
// Deserialize the message with the following expected format
|
||||
let native_messaging_json: commands::FirefoxCommand =
|
||||
serde_json::from_str(&message).or_else(|_| -> Result<commands::FirefoxCommand, _> {
|
||||
commands::generate_response(
|
||||
"Failed to deserialize message JSON",
|
||||
ResultCode::Error.into(),
|
||||
)
|
||||
.expect("JSON error");
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Failed to deserialize message JSON",
|
||||
));
|
||||
})?;
|
||||
commands::process_command(&native_messaging_json).or_else(|_| -> Result<bool, _> {
|
||||
commands::generate_response("Failed to process command", ResultCode::Error.into())
|
||||
.expect("JSON error");
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"Failed to process command",
|
||||
));
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
|
@ -718,6 +718,13 @@ pref("browser.download.clearHistoryOnDelete", 0);
|
|||
pref("browser.helperApps.showOpenOptionForPdfJS", true);
|
||||
pref("browser.helperApps.showOpenOptionForViewableInternally", true);
|
||||
|
||||
// Whether search-config-v2 is enabled.
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("browser.search.newSearchConfig.enabled", true);
|
||||
#else
|
||||
pref("browser.search.newSearchConfig.enabled", false);
|
||||
#endif
|
||||
|
||||
// search engines URL
|
||||
pref("browser.search.searchEnginesURL", "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/");
|
||||
|
||||
|
@ -886,6 +893,8 @@ pref("browser.tabs.warnOnClose", false);
|
|||
pref("browser.tabs.warnOnCloseOtherTabs", true);
|
||||
pref("browser.tabs.warnOnOpen", true);
|
||||
pref("browser.tabs.maxOpenBeforeWarn", 15);
|
||||
pref("browser.tabs.loadInBackground", true);
|
||||
pref("browser.tabs.opentabfor.middleclick", true);
|
||||
pref("browser.tabs.loadDivertedInBackground", false);
|
||||
pref("browser.tabs.loadBookmarksInBackground", false);
|
||||
pref("browser.tabs.loadBookmarksInTabs", false);
|
||||
|
@ -1298,8 +1307,6 @@ pref("browser.sessionstore.upgradeBackup.maxUpgradeBackups", 3);
|
|||
pref("browser.sessionstore.debug", false);
|
||||
// Forget closed windows/tabs after two weeks
|
||||
pref("browser.sessionstore.cleanup.forget_closed_after", 1209600000);
|
||||
// Platform collects session storage data for session store
|
||||
pref("browser.sessionstore.collect_session_storage", true);
|
||||
|
||||
// temporary pref that will be removed in a future release, see bug 1836952
|
||||
pref("browser.sessionstore.persist_closed_tabs_between_sessions", true);
|
||||
|
@ -2915,6 +2922,13 @@ pref("svg.context-properties.content.allowed-domains", "profile.accounts.firefox
|
|||
pref("extensions.translations.disabled", true);
|
||||
#endif
|
||||
|
||||
#if defined(XP_MACOSX) || defined(XP_WIN)
|
||||
pref("browser.firefoxbridge.enabled", false);
|
||||
pref("browser.firefoxbridge.extensionOrigins",
|
||||
"chrome-extension://gkcbmfjnnjoambnfmihmnkneakghogca/"
|
||||
);
|
||||
#endif
|
||||
|
||||
// Turn on interaction measurements
|
||||
pref("browser.places.interactions.enabled", true);
|
||||
|
||||
|
|
|
@ -6053,7 +6053,7 @@ nsBrowserAccess.prototype = {
|
|||
aName = "",
|
||||
aCsp = null,
|
||||
aSkipLoad = false,
|
||||
aWhere = undefined
|
||||
aForceLoadInBackground = false
|
||||
) {
|
||||
let win, needToFocusWin;
|
||||
|
||||
|
@ -6076,20 +6076,11 @@ nsBrowserAccess.prototype = {
|
|||
return win.gBrowser.selectedBrowser;
|
||||
}
|
||||
|
||||
// OPEN_NEWTAB_BACKGROUND and OPEN_NEWTAB_FOREGROUND are used by
|
||||
// `window.open` with modifiers.
|
||||
// The last case is OPEN_NEWTAB, which is used by:
|
||||
// * a link with `target="_blank"`, without modifiers
|
||||
// * `window.open` without features, without modifiers
|
||||
let loadInBackground;
|
||||
if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND) {
|
||||
let loadInBackground = Services.prefs.getBoolPref(
|
||||
"browser.tabs.loadDivertedInBackground"
|
||||
);
|
||||
if (aForceLoadInBackground) {
|
||||
loadInBackground = true;
|
||||
} else if (aWhere === Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_FOREGROUND) {
|
||||
loadInBackground = false;
|
||||
} else {
|
||||
loadInBackground = Services.prefs.getBoolPref(
|
||||
"browser.tabs.loadDivertedInBackground"
|
||||
);
|
||||
}
|
||||
|
||||
let tab = win.gBrowser.addTab(aURI ? aURI.spec : "about:blank", {
|
||||
|
@ -6266,8 +6257,7 @@ nsBrowserAccess.prototype = {
|
|||
}
|
||||
break;
|
||||
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB:
|
||||
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND:
|
||||
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_FOREGROUND: {
|
||||
case Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND: {
|
||||
// If we have an opener, that means that the caller is expecting access
|
||||
// to the nsIDOMWindow of the opened tab right away. For e10s windows,
|
||||
// this means forcing the newly opened browser to be non-remote so that
|
||||
|
@ -6278,6 +6268,8 @@ nsBrowserAccess.prototype = {
|
|||
let userContextId = aOpenWindowInfo
|
||||
? aOpenWindowInfo.originAttributes.userContextId
|
||||
: openingUserContextId;
|
||||
let forceLoadInBackground =
|
||||
aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND;
|
||||
let browser = this._openURIInNewTab(
|
||||
aURI,
|
||||
referrerInfo,
|
||||
|
@ -6291,7 +6283,7 @@ nsBrowserAccess.prototype = {
|
|||
"",
|
||||
aCsp,
|
||||
aSkipLoad,
|
||||
aWhere
|
||||
forceLoadInBackground
|
||||
);
|
||||
if (browser) {
|
||||
browsingContext = browser.browsingContext;
|
||||
|
@ -6392,8 +6384,7 @@ nsBrowserAccess.prototype = {
|
|||
|
||||
if (
|
||||
aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB &&
|
||||
aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND &&
|
||||
aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_FOREGROUND
|
||||
aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND
|
||||
) {
|
||||
dump("Error: openURIInFrame can only open in new tabs or print");
|
||||
return null;
|
||||
|
@ -6407,6 +6398,9 @@ nsBrowserAccess.prototype = {
|
|||
? aParams.openerOriginAttributes.userContextId
|
||||
: Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;
|
||||
|
||||
var forceLoadInBackground =
|
||||
aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB_BACKGROUND;
|
||||
|
||||
return this._openURIInNewTab(
|
||||
aURI,
|
||||
aParams.referrerInfo,
|
||||
|
@ -6420,7 +6414,7 @@ nsBrowserAccess.prototype = {
|
|||
aName,
|
||||
aParams.csp,
|
||||
aSkipLoad,
|
||||
aWhere
|
||||
forceLoadInBackground
|
||||
);
|
||||
},
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ var gSanitizePromptDialog = {
|
|||
if (!lazy.USE_OLD_DIALOG) {
|
||||
// Begin collecting how long it takes to load from here
|
||||
let timerId = Glean.privacySanitize.loadTime.start();
|
||||
|
||||
this._dataSizesUpdated = false;
|
||||
this.dataSizesFinishedUpdatingPromise = this.getAndUpdateDataSizes()
|
||||
.then(() => {
|
||||
// We're done loading, stop telemetry here
|
||||
|
@ -208,8 +208,6 @@ var gSanitizePromptDialog = {
|
|||
if (!lazy.USE_OLD_DIALOG) {
|
||||
this.reportTelemetry("open");
|
||||
}
|
||||
|
||||
await this.dataSizesFinishedUpdatingPromise;
|
||||
},
|
||||
|
||||
updateAcceptButtonState() {
|
||||
|
@ -399,6 +397,8 @@ var gSanitizePromptDialog = {
|
|||
);
|
||||
}
|
||||
this.cacheSize = lazy.DownloadUtils.convertByteUnits(cacheSize);
|
||||
|
||||
this._dataSizesUpdated = true;
|
||||
this.updateDataSizesInUI();
|
||||
},
|
||||
|
||||
|
@ -474,6 +474,10 @@ var gSanitizePromptDialog = {
|
|||
* Updates data sizes displayed based on new selected timespan
|
||||
*/
|
||||
updateDataSizesInUI() {
|
||||
if (!this._dataSizesUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const TIMESPAN_SELECTION_MAP = {
|
||||
0: "TIMESPAN_EVERYTHING",
|
||||
1: "TIMESPAN_HOUR",
|
||||
|
|
|
@ -1,36 +1,11 @@
|
|||
const TEST_PAGE =
|
||||
"http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
|
||||
|
||||
const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
|
||||
"prompts.contentPromptSubDialog",
|
||||
false
|
||||
);
|
||||
|
||||
var expectingDialog = false;
|
||||
var wantToClose = true;
|
||||
var resolveDialogPromise;
|
||||
|
||||
function onTabModalDialogLoaded(node) {
|
||||
ok(
|
||||
!CONTENT_PROMPT_SUBDIALOG,
|
||||
"Should not be using content prompt subdialogs."
|
||||
);
|
||||
ok(expectingDialog, "Should be expecting this dialog.");
|
||||
expectingDialog = false;
|
||||
if (wantToClose) {
|
||||
// This accepts the dialog, closing it
|
||||
node.querySelector(".tabmodalprompt-button0").click();
|
||||
} else {
|
||||
// This keeps the page open
|
||||
node.querySelector(".tabmodalprompt-button1").click();
|
||||
}
|
||||
if (resolveDialogPromise) {
|
||||
resolveDialogPromise();
|
||||
}
|
||||
}
|
||||
|
||||
function onCommonDialogLoaded(promptWindow) {
|
||||
ok(CONTENT_PROMPT_SUBDIALOG, "Should be using content prompt subdialogs.");
|
||||
ok(expectingDialog, "Should be expecting this dialog.");
|
||||
expectingDialog = false;
|
||||
let dialog = promptWindow.Dialog;
|
||||
|
@ -51,11 +26,9 @@ SpecialPowers.pushPrefEnv({
|
|||
});
|
||||
|
||||
// Listen for the dialog being created
|
||||
Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
|
||||
Services.obs.addObserver(onCommonDialogLoaded, "common-dialog-loaded");
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref("browser.tabs.warnOnClose");
|
||||
Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
|
||||
Services.obs.removeObserver(onCommonDialogLoaded, "common-dialog-loaded");
|
||||
});
|
||||
|
||||
|
|
|
@ -4,24 +4,15 @@ const TEST_PAGE =
|
|||
"http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
|
||||
var testTab;
|
||||
|
||||
const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
|
||||
"prompts.contentPromptSubDialog",
|
||||
false
|
||||
);
|
||||
|
||||
function waitForDialog(callback) {
|
||||
function onDialogLoaded(nodeOrDialogWindow) {
|
||||
let node = CONTENT_PROMPT_SUBDIALOG
|
||||
? nodeOrDialogWindow.document.querySelector("dialog")
|
||||
: nodeOrDialogWindow;
|
||||
Services.obs.removeObserver(onDialogLoaded, "tabmodal-dialog-loaded");
|
||||
let node = nodeOrDialogWindow.document.querySelector("dialog");
|
||||
Services.obs.removeObserver(onDialogLoaded, "common-dialog-loaded");
|
||||
// Allow dialog's onLoad call to run to completion
|
||||
Promise.resolve().then(() => callback(node));
|
||||
}
|
||||
|
||||
// Listen for the dialog being created
|
||||
Services.obs.addObserver(onDialogLoaded, "tabmodal-dialog-loaded");
|
||||
Services.obs.addObserver(onDialogLoaded, "common-dialog-loaded");
|
||||
}
|
||||
|
||||
|
@ -35,9 +26,7 @@ function waitForDialogDestroyed(node, callback) {
|
|||
});
|
||||
observer.observe(node.parentNode, { childList: true });
|
||||
|
||||
if (CONTENT_PROMPT_SUBDIALOG) {
|
||||
node.ownerGlobal.addEventListener("unload", done);
|
||||
}
|
||||
node.ownerGlobal.addEventListener("unload", done);
|
||||
|
||||
let failureTimeout = setTimeout(function () {
|
||||
ok(false, "Dialog should have been destroyed");
|
||||
|
@ -49,12 +38,8 @@ function waitForDialogDestroyed(node, callback) {
|
|||
observer.disconnect();
|
||||
observer = null;
|
||||
|
||||
if (CONTENT_PROMPT_SUBDIALOG) {
|
||||
node.ownerGlobal.removeEventListener("unload", done);
|
||||
SimpleTest.executeSoon(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
node.ownerGlobal.removeEventListener("unload", done);
|
||||
SimpleTest.executeSoon(callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,23 +61,12 @@ add_task(async function () {
|
|||
let doCompletion = () => setTimeout(resolveOuter, 0);
|
||||
info("Now checking if dialog is destroyed");
|
||||
|
||||
if (CONTENT_PROMPT_SUBDIALOG) {
|
||||
ok(
|
||||
!dialogNode.ownerGlobal || dialogNode.ownerGlobal.closed,
|
||||
"onbeforeunload dialog should be gone."
|
||||
);
|
||||
if (dialogNode.ownerGlobal && !dialogNode.ownerGlobal.closed) {
|
||||
dialogNode.acceptDialog();
|
||||
}
|
||||
} else {
|
||||
ok(!dialogNode.parentNode, "onbeforeunload dialog should be gone.");
|
||||
if (dialogNode.parentNode) {
|
||||
// Failed to remove onbeforeunload dialog, so do it ourselves:
|
||||
let leaveBtn = dialogNode.querySelector(".tabmodalprompt-button0");
|
||||
waitForDialogDestroyed(dialogNode, doCompletion);
|
||||
EventUtils.synthesizeMouseAtCenter(leaveBtn, {});
|
||||
return;
|
||||
}
|
||||
ok(
|
||||
!dialogNode.ownerGlobal || dialogNode.ownerGlobal.closed,
|
||||
"onbeforeunload dialog should be gone."
|
||||
);
|
||||
if (dialogNode.ownerGlobal && !dialogNode.ownerGlobal.closed) {
|
||||
dialogNode.acceptDialog();
|
||||
}
|
||||
|
||||
doCompletion();
|
||||
|
|
|
@ -200,7 +200,7 @@ async function performActionsOnDialog({
|
|||
cache = false,
|
||||
siteSettings = false,
|
||||
}) {
|
||||
let dh = new ClearHistoryDialogHelper(context);
|
||||
let dh = new ClearHistoryDialogHelper({ mode: context });
|
||||
dh.onload = function () {
|
||||
this.selectDuration(timespan);
|
||||
this.checkPrefCheckbox(
|
||||
|
@ -262,7 +262,7 @@ add_task(async function test_cancel() {
|
|||
|
||||
// test remembering user options for various entry points
|
||||
add_task(async function test_pref_remembering() {
|
||||
let dh = new ClearHistoryDialogHelper("clearSiteData");
|
||||
let dh = new ClearHistoryDialogHelper({ mode: "clearSiteData" });
|
||||
dh.onload = function () {
|
||||
this.checkPrefCheckbox("cookiesAndStorage", false);
|
||||
this.checkPrefCheckbox("siteSettings", true);
|
||||
|
@ -273,7 +273,7 @@ add_task(async function test_pref_remembering() {
|
|||
await dh.promiseClosed;
|
||||
|
||||
// validate if prefs are remembered
|
||||
dh = new ClearHistoryDialogHelper("clearSiteData");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearSiteData" });
|
||||
dh.onload = function () {
|
||||
this.validateCheckbox("cookiesAndStorage", false);
|
||||
this.validateCheckbox("siteSettings", true);
|
||||
|
@ -289,7 +289,7 @@ add_task(async function test_pref_remembering() {
|
|||
await dh.promiseClosed;
|
||||
|
||||
// validate if prefs did not change since we cancelled the dialog
|
||||
dh = new ClearHistoryDialogHelper("clearSiteData");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearSiteData" });
|
||||
dh.onload = function () {
|
||||
this.validateCheckbox("cookiesAndStorage", false);
|
||||
this.validateCheckbox("siteSettings", true);
|
||||
|
@ -302,7 +302,7 @@ add_task(async function test_pref_remembering() {
|
|||
// test rememebering prefs from the clear history context
|
||||
// since clear history and clear site data have seperate remembering
|
||||
// of prefs
|
||||
dh = new ClearHistoryDialogHelper("clearHistory");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearHistory" });
|
||||
dh.onload = function () {
|
||||
this.checkPrefCheckbox("cookiesAndStorage", true);
|
||||
this.checkPrefCheckbox("siteSettings", false);
|
||||
|
@ -314,7 +314,7 @@ add_task(async function test_pref_remembering() {
|
|||
await dh.promiseClosed;
|
||||
|
||||
// validate if prefs are remembered across both clear history and browser
|
||||
dh = new ClearHistoryDialogHelper("browser");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "browser" });
|
||||
dh.onload = function () {
|
||||
this.validateCheckbox("cookiesAndStorage", true);
|
||||
this.validateCheckbox("siteSettings", false);
|
||||
|
@ -448,7 +448,7 @@ add_task(async function testAcceptButtonDisabled() {
|
|||
* Tests to see if the warning box is hidden when opened in the clear on shutdown context
|
||||
*/
|
||||
add_task(async function testWarningBoxInClearOnShutdown() {
|
||||
let dh = new ClearHistoryDialogHelper("clearSiteData");
|
||||
let dh = new ClearHistoryDialogHelper({ mode: "clearSiteData" });
|
||||
dh.onload = function () {
|
||||
this.selectDuration(Sanitizer.TIMESPAN_EVERYTHING);
|
||||
is(
|
||||
|
@ -461,7 +461,7 @@ add_task(async function testWarningBoxInClearOnShutdown() {
|
|||
dh.open();
|
||||
await dh.promiseClosed;
|
||||
|
||||
dh = new ClearHistoryDialogHelper("clearOnShutdown");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearOnShutdown" });
|
||||
dh.onload = function () {
|
||||
is(
|
||||
BrowserTestUtils.isVisible(this.getWarningPanel()),
|
||||
|
@ -639,7 +639,7 @@ add_task(async function test_clear_on_shutdown() {
|
|||
set: [["privacy.sanitize.sanitizeOnShutdown", true]],
|
||||
});
|
||||
|
||||
let dh = new ClearHistoryDialogHelper("clearOnShutdown");
|
||||
let dh = new ClearHistoryDialogHelper({ mode: "clearOnShutdown" });
|
||||
dh.onload = async function () {
|
||||
this.uncheckAllCheckboxes();
|
||||
this.checkPrefCheckbox("historyFormDataAndDownloads", false);
|
||||
|
@ -708,7 +708,7 @@ add_task(async function test_clear_on_shutdown() {
|
|||
await ensureDownloadsClearedState(downloadIDs, false);
|
||||
await ensureDownloadsClearedState(olderDownloadIDs, false);
|
||||
|
||||
dh = new ClearHistoryDialogHelper("clearOnShutdown");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearOnShutdown" });
|
||||
dh.onload = async function () {
|
||||
this.uncheckAllCheckboxes();
|
||||
this.checkPrefCheckbox("historyFormDataAndDownloads", true);
|
||||
|
@ -908,7 +908,7 @@ add_task(async function testClearHistoryCheckboxStatesAfterMigration() {
|
|||
],
|
||||
});
|
||||
|
||||
let dh = new ClearHistoryDialogHelper("clearHistory");
|
||||
let dh = new ClearHistoryDialogHelper({ mode: "clearHistory" });
|
||||
dh.onload = function () {
|
||||
this.validateCheckbox("cookiesAndStorage", true);
|
||||
this.validateCheckbox("historyFormDataAndDownloads", false);
|
||||
|
@ -929,7 +929,7 @@ add_task(async function testClearHistoryCheckboxStatesAfterMigration() {
|
|||
);
|
||||
|
||||
// make sure the migration doesn't run again
|
||||
dh = new ClearHistoryDialogHelper("clearHistory");
|
||||
dh = new ClearHistoryDialogHelper({ mode: "clearHistory" });
|
||||
dh.onload = function () {
|
||||
this.validateCheckbox("siteSettings", true);
|
||||
this.validateCheckbox("cookiesAndStorage", false);
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
/**
|
||||
* This tests the new clear history dialog's data size display functionality
|
||||
*/
|
||||
ChromeUtils.defineESModuleGetters(this, {
|
||||
sinon: "resource://testing-common/Sinon.sys.mjs",
|
||||
Sanitizer: "resource:///modules/Sanitizer.sys.mjs",
|
||||
});
|
||||
|
||||
add_setup(async function () {
|
||||
await blankSlate();
|
||||
|
@ -84,13 +88,12 @@ async function clearAndValidateDataSizes({
|
|||
}) {
|
||||
await blankSlate();
|
||||
|
||||
await addToDownloadList();
|
||||
await addToSiteUsage();
|
||||
let promiseSanitized = promiseSanitizationComplete();
|
||||
|
||||
await openPreferencesViaOpenPreferencesAPI("privacy", { leaveOpen: true });
|
||||
|
||||
let dh = new ClearHistoryDialogHelper();
|
||||
let dh = new ClearHistoryDialogHelper({ checkingDataSizes: true });
|
||||
dh.onload = async function () {
|
||||
await validateDataSizes(this);
|
||||
this.checkPrefCheckbox("cache", clearCache);
|
||||
|
@ -105,7 +108,7 @@ async function clearAndValidateDataSizes({
|
|||
dh.open();
|
||||
await dh.promiseClosed;
|
||||
|
||||
let dh2 = new ClearHistoryDialogHelper();
|
||||
let dh2 = new ClearHistoryDialogHelper({ checkingDataSizes: true });
|
||||
// Check if the newly cleared values are reflected
|
||||
dh2.onload = async function () {
|
||||
await validateDataSizes(this);
|
||||
|
@ -180,3 +183,128 @@ add_task(async function test_all_data_sizes() {
|
|||
timespan: Sanitizer.TIMESPAN_EVERYTHING,
|
||||
});
|
||||
});
|
||||
|
||||
// This test makes sure that the user can change their timerange option
|
||||
// even if the data sizes are not loaded yet.
|
||||
add_task(async function testUIWithDataSizesLoading() {
|
||||
await blankSlate();
|
||||
await addToSiteUsage();
|
||||
|
||||
let origGetQuotaUsageForTimeRanges =
|
||||
SiteDataManager.getQuotaUsageForTimeRanges.bind(SiteDataManager);
|
||||
let resolveStubFn;
|
||||
let resolverAssigned = false;
|
||||
|
||||
let dh = new ClearHistoryDialogHelper();
|
||||
// Create a sandbox for isolated stubbing within the test
|
||||
let sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(SiteDataManager, "getQuotaUsageForTimeRanges")
|
||||
.callsFake(async (...args) => {
|
||||
info("stub called");
|
||||
|
||||
let dataSizesReadyToLoadPromise = new Promise(resolve => {
|
||||
resolveStubFn = resolve;
|
||||
info("Sending message to notify dialog that the resolver is assigned");
|
||||
window.postMessage("resolver-assigned", "*");
|
||||
resolverAssigned = true;
|
||||
});
|
||||
await dataSizesReadyToLoadPromise;
|
||||
return origGetQuotaUsageForTimeRanges(...args);
|
||||
});
|
||||
dh.onload = async function () {
|
||||
// we add this event listener in the case where init finishes before the resolver is assigned
|
||||
if (!resolverAssigned) {
|
||||
await new Promise(resolve => {
|
||||
let listener = event => {
|
||||
if (event.data === "resolver-assigned") {
|
||||
window.removeEventListener("message", listener);
|
||||
// we are ready to test the dialog without any data sizes loaded
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", listener);
|
||||
});
|
||||
}
|
||||
|
||||
ok(
|
||||
!this.win.gSanitizePromptDialog._dataSizesUpdated,
|
||||
"Data sizes should not have loaded yet"
|
||||
);
|
||||
this.selectDuration(Sanitizer.TIMESPAN_2HOURS);
|
||||
|
||||
info("triggering loading state end");
|
||||
resolveStubFn();
|
||||
|
||||
await this.win.gSanitizePromptDialog.dataSizesFinishedUpdatingPromise;
|
||||
|
||||
validateDataSizes(this);
|
||||
this.cancelDialog();
|
||||
};
|
||||
dh.open();
|
||||
await dh.promiseClosed;
|
||||
|
||||
// Restore the sandbox after the test is complete
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
add_task(async function testClearingBeforeDataSizesLoad() {
|
||||
await blankSlate();
|
||||
await addToSiteUsage();
|
||||
|
||||
// add site data that we can verify if it gets cleared
|
||||
await createDummyDataForHost("example.org");
|
||||
await createDummyDataForHost("example.com");
|
||||
|
||||
ok(
|
||||
await SiteDataTestUtils.hasIndexedDB("https://example.org"),
|
||||
"We have indexedDB data for example.org"
|
||||
);
|
||||
ok(
|
||||
await SiteDataTestUtils.hasIndexedDB("https://example.com"),
|
||||
"We have indexedDB data for example.com"
|
||||
);
|
||||
|
||||
let dh = new ClearHistoryDialogHelper();
|
||||
let promiseSanitized = promiseSanitizationComplete();
|
||||
// Create a sandbox for isolated stubbing within the test
|
||||
let sandbox = sinon.createSandbox();
|
||||
sandbox
|
||||
.stub(SiteDataManager, "getQuotaUsageForTimeRanges")
|
||||
.callsFake(async () => {
|
||||
info("stub called");
|
||||
|
||||
info("This promise should never resolve");
|
||||
await new Promise(resolve => {});
|
||||
});
|
||||
dh.onload = async function () {
|
||||
// we don't need to initiate a event listener to wait for the resolver to be assigned for this
|
||||
// test since we do not want the data sizes to load
|
||||
ok(
|
||||
!this.win.gSanitizePromptDialog._dataSizesUpdated,
|
||||
"Data sizes should not be loaded yet"
|
||||
);
|
||||
this.selectDuration(Sanitizer.TIMESPAN_2HOURS);
|
||||
this.checkPrefCheckbox("cookiesAndStorage", true);
|
||||
this.acceptDialog();
|
||||
};
|
||||
dh.onunload = async () => {
|
||||
await promiseSanitized;
|
||||
};
|
||||
dh.open();
|
||||
await dh.promiseClosed;
|
||||
|
||||
// Data for example.org should be cleared
|
||||
ok(
|
||||
!(await SiteDataTestUtils.hasIndexedDB("https://example.org")),
|
||||
"We don't have indexedDB data for example.org"
|
||||
);
|
||||
// Data for example.com should be cleared
|
||||
ok(
|
||||
!(await SiteDataTestUtils.hasIndexedDB("https://example.com")),
|
||||
"We don't have indexedDB data for example.com"
|
||||
);
|
||||
|
||||
// Restore the sandbox after the test is complete
|
||||
sandbox.restore();
|
||||
});
|
||||
|
|
|
@ -501,21 +501,29 @@ function promiseSanitizationComplete() {
|
|||
* This wraps the dialog and provides some convenience methods for interacting
|
||||
* with it.
|
||||
*
|
||||
* @param browserWin (optional)
|
||||
* @param {Window} browserWin (optional)
|
||||
* The browser window that the dialog is expected to open in. If not
|
||||
* supplied, the initial browser window of the test run is used.
|
||||
* @param mode (optional)
|
||||
* One of
|
||||
* clear on shutdown settings context ("clearOnShutdown"),
|
||||
* clear site data settings context ("clearSiteData"),
|
||||
* clear history context ("clearHistory"),
|
||||
* browser context ("browser")
|
||||
* "browser" by default
|
||||
* @param {Object} {mode, checkingDataSizes}
|
||||
* mode: context to open the dialog in
|
||||
* One of
|
||||
* clear on shutdown settings context ("clearOnShutdown"),
|
||||
* clear site data settings context ("clearSiteData"),
|
||||
* clear history context ("clearHistory"),
|
||||
* browser context ("browser")
|
||||
* "browser" by default
|
||||
* checkingDataSizes: boolean check if we should wait for the data sizes
|
||||
* to load
|
||||
*
|
||||
*/
|
||||
function ClearHistoryDialogHelper(openContext = "browser") {
|
||||
function ClearHistoryDialogHelper({
|
||||
mode = "browser",
|
||||
checkingDataSizes = false,
|
||||
} = {}) {
|
||||
this._browserWin = window;
|
||||
this.win = null;
|
||||
this._mode = openContext;
|
||||
this._mode = mode;
|
||||
this._checkingDataSizes = checkingDataSizes;
|
||||
this.promiseClosed = new Promise(resolve => {
|
||||
this._resolveClosed = resolve;
|
||||
});
|
||||
|
@ -673,7 +681,11 @@ ClearHistoryDialogHelper.prototype = {
|
|||
() => {
|
||||
// Run onload on next tick so that gSanitizePromptDialog.init can run first.
|
||||
executeSoon(async () => {
|
||||
await this.win.gSanitizePromptDialog.dataSizesFinishedUpdatingPromise;
|
||||
if (this._checkingDataSizes) {
|
||||
// we wait for the data sizes to load to avoid async errors when validating sizes
|
||||
await this.win.gSanitizePromptDialog
|
||||
.dataSizesFinishedUpdatingPromise;
|
||||
}
|
||||
this.onload();
|
||||
});
|
||||
},
|
||||
|
|
|
@ -9,11 +9,6 @@ const TEST_ROOT = getRootDirectory(gTestPath).replace(
|
|||
"http://example.com"
|
||||
);
|
||||
|
||||
const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
|
||||
"prompts.contentPromptSubDialog",
|
||||
false
|
||||
);
|
||||
|
||||
add_task(async function test_beforeunload_stay_clears_urlbar() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.require_user_interaction_for_beforeunload", false]],
|
||||
|
@ -27,27 +22,10 @@ add_task(async function test_beforeunload_stay_clears_urlbar() {
|
|||
gURLBar.value = inputValue.slice(0, -1);
|
||||
EventUtils.sendString(inputValue.slice(-1));
|
||||
|
||||
if (CONTENT_PROMPT_SUBDIALOG) {
|
||||
let promptOpenedPromise =
|
||||
BrowserTestUtils.promiseAlertDialogOpen("cancel");
|
||||
EventUtils.synthesizeKey("VK_RETURN");
|
||||
await promptOpenedPromise;
|
||||
await TestUtils.waitForTick();
|
||||
} else {
|
||||
let promptOpenedPromise = TestUtils.topicObserved(
|
||||
"tabmodal-dialog-loaded"
|
||||
);
|
||||
EventUtils.synthesizeKey("VK_RETURN");
|
||||
await promptOpenedPromise;
|
||||
let promptElement = browser.parentNode.querySelector("tabmodalprompt");
|
||||
|
||||
// Click the cancel button
|
||||
promptElement.querySelector(".tabmodalprompt-button1").click();
|
||||
await TestUtils.waitForCondition(
|
||||
() => promptElement.parentNode == null,
|
||||
"tabprompt should be removed"
|
||||
);
|
||||
}
|
||||
let promptOpenedPromise = BrowserTestUtils.promiseAlertDialogOpen("cancel");
|
||||
EventUtils.synthesizeKey("VK_RETURN");
|
||||
await promptOpenedPromise;
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
// Can't just compare directly with TEST_URL because the URL may be trimmed.
|
||||
// Just need it to not be the example.org thing we typed in.
|
||||
|
|
|
@ -129,11 +129,7 @@ async function checkDialog(
|
|||
|
||||
add_setup(async function () {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["prompts.contentPromptSubDialog", true],
|
||||
["prompts.modalType.httpAuth", Ci.nsIPrompt.MODAL_TYPE_TAB],
|
||||
["prompts.tabChromePromptSubDialog", true],
|
||||
],
|
||||
set: [["prompts.modalType.httpAuth", Ci.nsIPrompt.MODAL_TYPE_TAB]],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
|
||||
"prompts.contentPromptSubDialog",
|
||||
false
|
||||
);
|
||||
|
||||
/**
|
||||
* Goes through a stacked series of dialogs opened with
|
||||
* CONTENT_PROMPT_SUBDIALOG set to true, and ensures that
|
||||
* Goes through a stacked series of dialogs and ensures that
|
||||
* the oldest one is front-most and has the right type. It
|
||||
* then closes the oldest to newest dialog.
|
||||
*
|
||||
|
@ -58,64 +52,6 @@ async function closeDialogs(tab, dialogCount) {
|
|||
is(dialogs.length, 0, "Dialogs should all be dismissed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through a stacked series of tabprompt modals opened with
|
||||
* CONTENT_PROMPT_SUBDIALOG set to false, and ensures that
|
||||
* the oldest one is front-most and has the right type. It also
|
||||
* ensures that the other tabprompt modals are hidden. It
|
||||
* then closes the oldest to newest dialog.
|
||||
*
|
||||
* @param {Element} tab The <tab> that has had tabprompt modals opened
|
||||
* for it.
|
||||
* @param {Number} promptCount How many modals we expected to have been
|
||||
* opened.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves {undefined} Once the modals have all been closed.
|
||||
*/
|
||||
async function closeTabModals(tab, promptCount) {
|
||||
let promptElementsCount = promptCount;
|
||||
while (promptElementsCount--) {
|
||||
let promptElements =
|
||||
tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
|
||||
is(
|
||||
promptElements.length,
|
||||
promptElementsCount + 1,
|
||||
"There should be " + (promptElementsCount + 1) + " prompt(s)."
|
||||
);
|
||||
// The oldest should be the first.
|
||||
let i = 0;
|
||||
|
||||
for (let promptElement of promptElements) {
|
||||
let prompt = tab.linkedBrowser.tabModalPromptBox.getPrompt(promptElement);
|
||||
let expectedType = ["alert", "prompt", "confirm"][i % 3];
|
||||
is(
|
||||
prompt.Dialog.args.text,
|
||||
expectedType + " countdown #" + i,
|
||||
"The #" + i + " alert should be labelled as such."
|
||||
);
|
||||
if (i !== promptElementsCount) {
|
||||
is(prompt.element.hidden, true, "This prompt should be hidden.");
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
is(prompt.element.hidden, false, "The last prompt should not be hidden.");
|
||||
prompt.onButtonClick(0);
|
||||
|
||||
// The click is handled async; wait for an event loop turn for that to
|
||||
// happen.
|
||||
await new Promise(function (resolve) {
|
||||
Services.tm.dispatchToMainThread(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let promptElements =
|
||||
tab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
|
||||
is(promptElements.length, 0, "Prompts should all be dismissed.");
|
||||
}
|
||||
|
||||
/*
|
||||
* This test triggers multiple alerts on one single tab, because it"s possible
|
||||
* for web content to do so. The behavior is described in bug 1266353.
|
||||
|
@ -161,11 +97,7 @@ add_task(async function () {
|
|||
|
||||
await promptsOpenedPromise;
|
||||
|
||||
if (CONTENT_PROMPT_SUBDIALOG) {
|
||||
await closeDialogs(tab, PROMPTCOUNT);
|
||||
} else {
|
||||
await closeTabModals(tab, PROMPTCOUNT);
|
||||
}
|
||||
await closeDialogs(tab, PROMPTCOUNT);
|
||||
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
});
|
||||
|
|
|
@ -22,118 +22,7 @@ registerCleanupFunction(function () {
|
|||
* the user to enable this automatically re-selecting. We then check that
|
||||
* checking the checkbox does actually enable that behaviour.
|
||||
*/
|
||||
add_task(async function test_old_modal_ui() {
|
||||
// We're intentionally testing the old modal mechanism, so disable the new one.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["prompts.contentPromptSubDialog", false]],
|
||||
});
|
||||
|
||||
let firstTab = gBrowser.selectedTab;
|
||||
// load page that opens prompt when page is hidden
|
||||
let openedTab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
pageWithAlert,
|
||||
true
|
||||
);
|
||||
let openedTabGotAttentionPromise = BrowserTestUtils.waitForAttribute(
|
||||
"attention",
|
||||
openedTab
|
||||
);
|
||||
// switch away from that tab again - this triggers the alert.
|
||||
await BrowserTestUtils.switchTab(gBrowser, firstTab);
|
||||
// ... but that's async on e10s...
|
||||
await openedTabGotAttentionPromise;
|
||||
// check for attention attribute
|
||||
is(
|
||||
openedTab.hasAttribute("attention"),
|
||||
true,
|
||||
"Tab with alert should have 'attention' attribute."
|
||||
);
|
||||
ok(!openedTab.selected, "Tab with alert should not be selected");
|
||||
|
||||
// switch tab back, and check the checkbox is displayed:
|
||||
await BrowserTestUtils.switchTab(gBrowser, openedTab);
|
||||
// check the prompt is there, and the extra row is present
|
||||
let promptElements =
|
||||
openedTab.linkedBrowser.parentNode.querySelectorAll("tabmodalprompt");
|
||||
is(promptElements.length, 1, "There should be 1 prompt");
|
||||
let ourPromptElement = promptElements[0];
|
||||
let checkbox = ourPromptElement.querySelector(
|
||||
"checkbox[label*='example.com']"
|
||||
);
|
||||
ok(checkbox, "The checkbox should be there");
|
||||
ok(!checkbox.checked, "Checkbox shouldn't be checked");
|
||||
// tick box and accept dialog
|
||||
checkbox.checked = true;
|
||||
let ourPrompt =
|
||||
openedTab.linkedBrowser.tabModalPromptBox.getPrompt(ourPromptElement);
|
||||
ourPrompt.onButtonClick(0);
|
||||
// Wait for that click to actually be handled completely.
|
||||
await new Promise(function (resolve) {
|
||||
Services.tm.dispatchToMainThread(resolve);
|
||||
});
|
||||
// check permission is set
|
||||
is(
|
||||
Services.perms.ALLOW_ACTION,
|
||||
PermissionTestUtils.testPermission(pageWithAlert, "focus-tab-by-prompt"),
|
||||
"Tab switching should now be allowed"
|
||||
);
|
||||
|
||||
// Check if the control center shows the correct permission.
|
||||
let shown = BrowserTestUtils.waitForEvent(
|
||||
window,
|
||||
"popupshown",
|
||||
true,
|
||||
event => event.target == gPermissionPanel._permissionPopup
|
||||
);
|
||||
gPermissionPanel._identityPermissionBox.click();
|
||||
await shown;
|
||||
let labelText = SitePermissions.getPermissionLabel("focus-tab-by-prompt");
|
||||
let permissionsList = document.getElementById(
|
||||
"permission-popup-permission-list"
|
||||
);
|
||||
let label = permissionsList.querySelector(
|
||||
".permission-popup-permission-label"
|
||||
);
|
||||
is(label.textContent, labelText);
|
||||
gPermissionPanel._permissionPopup.hidePopup();
|
||||
|
||||
// Check if the identity icon signals granted permission.
|
||||
ok(
|
||||
gPermissionPanel._identityPermissionBox.hasAttribute("hasPermissions"),
|
||||
"identity-box signals granted permissions"
|
||||
);
|
||||
|
||||
let openedTabSelectedPromise = BrowserTestUtils.waitForAttribute(
|
||||
"selected",
|
||||
openedTab,
|
||||
"true"
|
||||
);
|
||||
// switch to other tab again
|
||||
await BrowserTestUtils.switchTab(gBrowser, firstTab);
|
||||
|
||||
// This is sync in non-e10s, but in e10s we need to wait for this, so yield anyway.
|
||||
// Note that the switchTab promise doesn't actually guarantee anything about *which*
|
||||
// tab ends up as selected when its event fires, so using that here wouldn't work.
|
||||
await openedTabSelectedPromise;
|
||||
// should be switched back
|
||||
ok(openedTab.selected, "Ta-dah, the other tab should now be selected again!");
|
||||
|
||||
// In e10s, with the conformant promise scheduling, we have to wait for next tick
|
||||
// to ensure that the prompt is open before removing the opened tab, because the
|
||||
// promise callback of 'openedTabSelectedPromise' could be done at the middle of
|
||||
// RemotePrompt.openTabPrompt() while 'DOMModalDialogClosed' event is fired.
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
BrowserTestUtils.removeTab(openedTab);
|
||||
});
|
||||
|
||||
add_task(async function test_new_modal_ui() {
|
||||
// We're intentionally testing the new modal mechanism, so make sure it's enabled.
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["prompts.contentPromptSubDialog", true]],
|
||||
});
|
||||
|
||||
add_task(async function test_modal_ui() {
|
||||
// Make sure we clear the focus tab permission set in the previous test
|
||||
PermissionTestUtils.remove(pageWithAlert, "focus-tab-by-prompt");
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const CONTENT_PROMPT_PREF = "prompts.contentPromptSubDialog";
|
||||
const TEST_ROOT_CHROME = getRootDirectory(gTestPath);
|
||||
const TEST_DIALOG_PATH = TEST_ROOT_CHROME + "subdialog.xhtml";
|
||||
|
||||
|
@ -41,13 +40,6 @@ var commonDialogsBundle = Services.strings.createBundle(
|
|||
"chrome://global/locale/commonDialogs.properties"
|
||||
);
|
||||
|
||||
// Setup.
|
||||
add_setup(async function () {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [[CONTENT_PROMPT_PREF, true]],
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that a manager for content prompts is added to tab dialog box.
|
||||
*/
|
||||
|
|
|
@ -4,14 +4,7 @@
|
|||
// beforeunload confirmation ignores the beforeunload listener and
|
||||
// unblocks the original close call.
|
||||
|
||||
const CONTENT_PROMPT_SUBDIALOG = Services.prefs.getBoolPref(
|
||||
"prompts.contentPromptSubDialog",
|
||||
false
|
||||
);
|
||||
|
||||
const DIALOG_TOPIC = CONTENT_PROMPT_SUBDIALOG
|
||||
? "common-dialog-loaded"
|
||||
: "tabmodal-dialog-loaded";
|
||||
const DIALOG_TOPIC = "common-dialog-loaded";
|
||||
|
||||
add_task(async function () {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
|
|
|
@ -11,7 +11,6 @@ add_task(async function () {
|
|||
const tests = [
|
||||
["OPEN_NEWTAB", false],
|
||||
["OPEN_NEWTAB_BACKGROUND", true],
|
||||
["OPEN_NEWTAB_FOREGROUND", false],
|
||||
];
|
||||
|
||||
for (const [flag, isBackground] of tests) {
|
||||
|
|
|
@ -5,274 +5,171 @@
|
|||
"use strict";
|
||||
|
||||
// Opening many windows take long time on some configuration.
|
||||
requestLongerTimeout(6);
|
||||
requestLongerTimeout(4);
|
||||
|
||||
const TEST_URL =
|
||||
"https://example.com/browser/browser/base/content/test/tabs/file_window_open.html";
|
||||
add_task(async function () {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
"https://example.com/browser/browser/base/content/test/tabs/file_window_open.html",
|
||||
async function (browser) {
|
||||
const metaKey = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
|
||||
const normalEvent = {};
|
||||
const shiftEvent = { shiftKey: true };
|
||||
const metaEvent = { [metaKey]: true };
|
||||
const metaShiftEvent = { [metaKey]: true, shiftKey: true };
|
||||
|
||||
const metaKey = AppConstants.platform == "macosx" ? "metaKey" : "ctrlKey";
|
||||
const tests = [
|
||||
// type, id, options, result
|
||||
["mouse", "#instant", normalEvent, "tab"],
|
||||
["mouse", "#instant", shiftEvent, "window"],
|
||||
["mouse", "#instant", metaEvent, "tab-bg"],
|
||||
["mouse", "#instant", metaShiftEvent, "tab"],
|
||||
|
||||
const normalEvent = {};
|
||||
const shiftEvent = { shiftKey: true };
|
||||
const metaEvent = { [metaKey]: true };
|
||||
const metaShiftEvent = { [metaKey]: true, shiftKey: true };
|
||||
["mouse", "#instant-popup", normalEvent, "popup"],
|
||||
["mouse", "#instant-popup", shiftEvent, "window"],
|
||||
["mouse", "#instant-popup", metaEvent, "tab-bg"],
|
||||
["mouse", "#instant-popup", metaShiftEvent, "tab"],
|
||||
|
||||
const altEvent = { altKey: true };
|
||||
const altShiftEvent = { altKey: true, shiftKey: true };
|
||||
const altMetaEvent = { altKey: true, [metaKey]: true };
|
||||
const altMetaShiftEvent = { altKey: true, [metaKey]: true, shiftKey: true };
|
||||
["mouse", "#delayed", normalEvent, "tab"],
|
||||
["mouse", "#delayed", shiftEvent, "window"],
|
||||
["mouse", "#delayed", metaEvent, "tab-bg"],
|
||||
["mouse", "#delayed", metaShiftEvent, "tab"],
|
||||
|
||||
const middleEvent = { button: 1 };
|
||||
const middleShiftEvent = { button: 1, shiftKey: true };
|
||||
const middleMetaEvent = { button: 1, [metaKey]: true };
|
||||
const middleMetaShiftEvent = { button: 1, [metaKey]: true, shiftKey: true };
|
||||
["mouse", "#delayed-popup", normalEvent, "popup"],
|
||||
["mouse", "#delayed-popup", shiftEvent, "window"],
|
||||
["mouse", "#delayed-popup", metaEvent, "tab-bg"],
|
||||
["mouse", "#delayed-popup", metaShiftEvent, "tab"],
|
||||
|
||||
add_task(async function testMouse() {
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
// type, id, options, result
|
||||
["mouse", "#instant", normalEvent, "tab"],
|
||||
["mouse", "#instant", shiftEvent, "window"],
|
||||
["mouse", "#instant", metaEvent, "tab-bg"],
|
||||
["mouse", "#instant", metaShiftEvent, "tab"],
|
||||
// NOTE: meta+keyboard doesn't activate.
|
||||
|
||||
["mouse", "#instant-popup", normalEvent, "popup"],
|
||||
["mouse", "#instant-popup", shiftEvent, "window"],
|
||||
["mouse", "#instant-popup", metaEvent, "tab-bg"],
|
||||
["mouse", "#instant-popup", metaShiftEvent, "tab"],
|
||||
["VK_SPACE", "#instant", normalEvent, "tab"],
|
||||
["VK_SPACE", "#instant", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#delayed", normalEvent, "tab"],
|
||||
["mouse", "#delayed", shiftEvent, "window"],
|
||||
["mouse", "#delayed", metaEvent, "tab-bg"],
|
||||
["mouse", "#delayed", metaShiftEvent, "tab"],
|
||||
["VK_SPACE", "#instant-popup", normalEvent, "popup"],
|
||||
["VK_SPACE", "#instant-popup", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#delayed-popup", normalEvent, "popup"],
|
||||
["mouse", "#delayed-popup", shiftEvent, "window"],
|
||||
["mouse", "#delayed-popup", metaEvent, "tab-bg"],
|
||||
["mouse", "#delayed-popup", metaShiftEvent, "tab"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
["VK_SPACE", "#delayed", normalEvent, "tab"],
|
||||
["VK_SPACE", "#delayed", shiftEvent, "window"],
|
||||
|
||||
add_task(async function testAlt() {
|
||||
// Alt key shouldn't affect the behavior.
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
["mouse", "#instant", altEvent, "tab"],
|
||||
["mouse", "#instant", altShiftEvent, "window"],
|
||||
["mouse", "#instant", altMetaEvent, "tab-bg"],
|
||||
["mouse", "#instant", altMetaShiftEvent, "tab"],
|
||||
["VK_SPACE", "#delayed-popup", normalEvent, "popup"],
|
||||
["VK_SPACE", "#delayed-popup", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#instant-popup", altEvent, "popup"],
|
||||
["mouse", "#instant-popup", altShiftEvent, "window"],
|
||||
["mouse", "#instant-popup", altMetaEvent, "tab-bg"],
|
||||
["mouse", "#instant-popup", altMetaShiftEvent, "tab"],
|
||||
["KEY_Enter", "#link-instant", normalEvent, "tab"],
|
||||
["KEY_Enter", "#link-instant", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#delayed", altEvent, "tab"],
|
||||
["mouse", "#delayed", altShiftEvent, "window"],
|
||||
["mouse", "#delayed", altMetaEvent, "tab-bg"],
|
||||
["mouse", "#delayed", altMetaShiftEvent, "tab"],
|
||||
["KEY_Enter", "#link-instant-popup", normalEvent, "popup"],
|
||||
["KEY_Enter", "#link-instant-popup", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#delayed-popup", altEvent, "popup"],
|
||||
["mouse", "#delayed-popup", altShiftEvent, "window"],
|
||||
["mouse", "#delayed-popup", altMetaEvent, "tab-bg"],
|
||||
["mouse", "#delayed-popup", altMetaShiftEvent, "tab"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
["KEY_Enter", "#link-delayed", normalEvent, "tab"],
|
||||
["KEY_Enter", "#link-delayed", shiftEvent, "window"],
|
||||
|
||||
add_task(async function testMiddleMouse() {
|
||||
// Middle click is equivalent to meta key.
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
["mouse", "#instant", middleEvent, "tab-bg"],
|
||||
["mouse", "#instant", middleShiftEvent, "tab"],
|
||||
["mouse", "#instant", middleMetaEvent, "tab-bg"],
|
||||
["mouse", "#instant", middleMetaShiftEvent, "tab"],
|
||||
["KEY_Enter", "#link-delayed-popup", normalEvent, "popup"],
|
||||
["KEY_Enter", "#link-delayed-popup", shiftEvent, "window"],
|
||||
|
||||
["mouse", "#instant-popup", middleEvent, "tab-bg"],
|
||||
["mouse", "#instant-popup", middleShiftEvent, "tab"],
|
||||
["mouse", "#instant-popup", middleMetaEvent, "tab-bg"],
|
||||
["mouse", "#instant-popup", middleMetaShiftEvent, "tab"],
|
||||
// Trigger user-defined shortcut key, where modifiers shouldn't affect.
|
||||
|
||||
["mouse", "#delayed", middleEvent, "tab-bg"],
|
||||
["mouse", "#delayed", middleShiftEvent, "tab"],
|
||||
["mouse", "#delayed", middleMetaEvent, "tab-bg"],
|
||||
["mouse", "#delayed", middleMetaShiftEvent, "tab"],
|
||||
["x", "#instant", normalEvent, "tab"],
|
||||
["x", "#instant", shiftEvent, "tab"],
|
||||
["x", "#instant", metaEvent, "tab"],
|
||||
["x", "#instant", metaShiftEvent, "tab"],
|
||||
|
||||
["mouse", "#delayed-popup", middleEvent, "tab-bg"],
|
||||
["mouse", "#delayed-popup", middleShiftEvent, "tab"],
|
||||
["mouse", "#delayed-popup", middleMetaEvent, "tab-bg"],
|
||||
["mouse", "#delayed-popup", middleMetaShiftEvent, "tab"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
["y", "#instant", normalEvent, "popup"],
|
||||
["y", "#instant", shiftEvent, "popup"],
|
||||
["y", "#instant", metaEvent, "popup"],
|
||||
["y", "#instant", metaShiftEvent, "popup"],
|
||||
];
|
||||
for (const [type, id, event, result] of tests) {
|
||||
const eventStr = JSON.stringify(event);
|
||||
|
||||
add_task(async function testBackgroundPrefTests() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.tabs.loadInBackground", false]],
|
||||
});
|
||||
let openPromise;
|
||||
if (result == "tab" || result == "tab-bg") {
|
||||
openPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
"about:blank",
|
||||
true
|
||||
);
|
||||
} else {
|
||||
openPromise = BrowserTestUtils.waitForNewWindow({
|
||||
url: "about:blank",
|
||||
});
|
||||
}
|
||||
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
["mouse", "#instant", metaEvent, "tab"],
|
||||
["mouse", "#instant", metaShiftEvent, "tab-bg"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
if (type == "mouse") {
|
||||
BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser);
|
||||
} else {
|
||||
// Make sure the keyboard activates a simple button on the page.
|
||||
await ContentTask.spawn(browser, id, elementId => {
|
||||
content.document.querySelector("#focus-result").value = "";
|
||||
content.document.querySelector("#focus-check").focus();
|
||||
});
|
||||
BrowserTestUtils.synthesizeKey("VK_SPACE", {}, browser);
|
||||
await ContentTask.spawn(browser, {}, async () => {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() =>
|
||||
content.document.querySelector("#focus-result").value === "ok"
|
||||
);
|
||||
});
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
});
|
||||
// Once confirmed the keyboard event works, send the actual event
|
||||
// that calls window.open.
|
||||
await ContentTask.spawn(browser, id, elementId => {
|
||||
content.document.querySelector(elementId).focus();
|
||||
});
|
||||
BrowserTestUtils.synthesizeKey(type, { ...event }, browser);
|
||||
}
|
||||
|
||||
add_task(async function testSpaceKey() {
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
// NOTE: meta+keyboard doesn't activate.
|
||||
const openedThing = await openPromise;
|
||||
|
||||
["VK_SPACE", "#instant", normalEvent, "tab"],
|
||||
["VK_SPACE", "#instant", shiftEvent, "window"],
|
||||
if (result == "tab" || result == "tab-bg") {
|
||||
const newTab = openedThing;
|
||||
|
||||
["VK_SPACE", "#instant-popup", normalEvent, "popup"],
|
||||
["VK_SPACE", "#instant-popup", shiftEvent, "window"],
|
||||
if (result == "tab") {
|
||||
Assert.equal(
|
||||
gBrowser.selectedTab,
|
||||
newTab,
|
||||
`${id} with ${type} and ${eventStr} opened a foreground tab`
|
||||
);
|
||||
} else {
|
||||
Assert.notEqual(
|
||||
gBrowser.selectedTab,
|
||||
newTab,
|
||||
`${id} with ${type} and ${eventStr} opened a background tab`
|
||||
);
|
||||
}
|
||||
|
||||
["VK_SPACE", "#delayed", normalEvent, "tab"],
|
||||
["VK_SPACE", "#delayed", shiftEvent, "window"],
|
||||
gBrowser.removeTab(newTab);
|
||||
} else {
|
||||
const newWindow = openedThing;
|
||||
|
||||
["VK_SPACE", "#delayed-popup", normalEvent, "popup"],
|
||||
["VK_SPACE", "#delayed-popup", shiftEvent, "window"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
const tabs = newWindow.document.getElementById("TabsToolbar");
|
||||
if (result == "window") {
|
||||
ok(
|
||||
!tabs.collapsed,
|
||||
`${id} with ${type} and ${eventStr} opened a regular window`
|
||||
);
|
||||
} else {
|
||||
ok(
|
||||
tabs.collapsed,
|
||||
`${id} with ${type} and ${eventStr} opened a popup window`
|
||||
);
|
||||
}
|
||||
|
||||
add_task(async function testEnterKey() {
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
["KEY_Enter", "#link-instant", normalEvent, "tab"],
|
||||
["KEY_Enter", "#link-instant", shiftEvent, "window"],
|
||||
const closedPopupPromise = BrowserTestUtils.windowClosed(newWindow);
|
||||
newWindow.close();
|
||||
await closedPopupPromise;
|
||||
|
||||
["KEY_Enter", "#link-instant-popup", normalEvent, "popup"],
|
||||
["KEY_Enter", "#link-instant-popup", shiftEvent, "window"],
|
||||
|
||||
["KEY_Enter", "#link-delayed", normalEvent, "tab"],
|
||||
["KEY_Enter", "#link-delayed", shiftEvent, "window"],
|
||||
|
||||
["KEY_Enter", "#link-delayed-popup", normalEvent, "popup"],
|
||||
["KEY_Enter", "#link-delayed-popup", shiftEvent, "window"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function testUserDefinedShortcut() {
|
||||
await BrowserTestUtils.withNewTab(TEST_URL, async function (browser) {
|
||||
const tests = [
|
||||
// Trigger user-defined shortcut key, where modifiers shouldn't affect.
|
||||
|
||||
["x", "#instant", normalEvent, "tab"],
|
||||
["x", "#instant", shiftEvent, "tab"],
|
||||
["x", "#instant", metaEvent, "tab"],
|
||||
["x", "#instant", metaShiftEvent, "tab"],
|
||||
|
||||
["y", "#instant", normalEvent, "popup"],
|
||||
["y", "#instant", shiftEvent, "popup"],
|
||||
["y", "#instant", metaEvent, "popup"],
|
||||
["y", "#instant", metaShiftEvent, "popup"],
|
||||
];
|
||||
await runWindowOpenTests(browser, tests);
|
||||
});
|
||||
});
|
||||
|
||||
async function runWindowOpenTests(browser, tests) {
|
||||
for (const [type, id, event, result] of tests) {
|
||||
let eventStr = JSON.stringify(event);
|
||||
|
||||
let openPromise;
|
||||
if (result == "tab" || result == "tab-bg") {
|
||||
openPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
"about:blank",
|
||||
true
|
||||
);
|
||||
} else {
|
||||
openPromise = BrowserTestUtils.waitForNewWindow({
|
||||
url: "about:blank",
|
||||
});
|
||||
}
|
||||
|
||||
if (type == "mouse") {
|
||||
BrowserTestUtils.synthesizeMouseAtCenter(id, { ...event }, browser);
|
||||
} else {
|
||||
// Make sure the keyboard activates a simple button on the page.
|
||||
await ContentTask.spawn(browser, id, elementId => {
|
||||
content.document.querySelector("#focus-result").value = "";
|
||||
content.document.querySelector("#focus-check").focus();
|
||||
});
|
||||
BrowserTestUtils.synthesizeKey("VK_SPACE", {}, browser);
|
||||
await ContentTask.spawn(browser, {}, async () => {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => content.document.querySelector("#focus-result").value === "ok"
|
||||
);
|
||||
});
|
||||
|
||||
// Once confirmed the keyboard event works, send the actual event
|
||||
// that calls window.open.
|
||||
await ContentTask.spawn(browser, id, elementId => {
|
||||
content.document.querySelector(elementId).focus();
|
||||
});
|
||||
BrowserTestUtils.synthesizeKey(type, { ...event }, browser);
|
||||
}
|
||||
|
||||
const openedThing = await openPromise;
|
||||
|
||||
if (result == "tab" || result == "tab-bg") {
|
||||
const newTab = openedThing;
|
||||
|
||||
if (result == "tab") {
|
||||
Assert.equal(
|
||||
gBrowser.selectedTab,
|
||||
newTab,
|
||||
`${id} with ${type} and ${eventStr} opened a foreground tab`
|
||||
);
|
||||
} else {
|
||||
Assert.notEqual(
|
||||
gBrowser.selectedTab,
|
||||
newTab,
|
||||
`${id} with ${type} and ${eventStr} opened a background tab`
|
||||
);
|
||||
}
|
||||
|
||||
gBrowser.removeTab(newTab);
|
||||
} else {
|
||||
const newWindow = openedThing;
|
||||
|
||||
const tabs = newWindow.document.getElementById("TabsToolbar");
|
||||
if (result == "window") {
|
||||
ok(
|
||||
!tabs.collapsed,
|
||||
`${id} with ${type} and ${eventStr} opened a regular window`
|
||||
);
|
||||
} else {
|
||||
ok(
|
||||
tabs.collapsed,
|
||||
`${id} with ${type} and ${eventStr} opened a popup window`
|
||||
);
|
||||
}
|
||||
|
||||
const closedPopupPromise = BrowserTestUtils.windowClosed(newWindow);
|
||||
newWindow.close();
|
||||
await closedPopupPromise;
|
||||
|
||||
// Make sure the focus comes back to this window before proceeding
|
||||
// to the next test.
|
||||
if (Services.focus.focusedWindow != window) {
|
||||
const focusBack = BrowserTestUtils.waitForEvent(window, "focus", true);
|
||||
window.focus();
|
||||
await focusBack;
|
||||
// Make sure the focus comes back to this window before proceeding
|
||||
// to the next test.
|
||||
if (Services.focus.focusedWindow != window) {
|
||||
const focusBack = BrowserTestUtils.waitForEvent(
|
||||
window,
|
||||
"focus",
|
||||
true
|
||||
);
|
||||
window.focus();
|
||||
await focusBack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -11,23 +11,19 @@ div {
|
|||
<body>
|
||||
<div>
|
||||
<input id="instant" type="button" value="instant no features"
|
||||
onclick="window.open('about:blank', '_blank');"
|
||||
onmousedown="if (event.button == 1) { window.open('about:blank', '_blank'); event.preventDefault(); }">
|
||||
onclick="window.open('about:blank', '_blank');">
|
||||
</div>
|
||||
<div>
|
||||
<input id="instant-popup" type="button" value="instant popup"
|
||||
onclick="window.open('about:blank', '_blank', 'popup=true');"
|
||||
onmousedown="if (event.button == 1) { window.open('about:blank', '_blank', 'popup=true'); }">
|
||||
onclick="window.open('about:blank', '_blank', 'popup=true');">
|
||||
</div>
|
||||
<div>
|
||||
<input id="delayed" type="button" value="delayed no features"
|
||||
onclick="setTimeout(() => window.open('about:blank', '_blank'), 100);"
|
||||
onmousedown="if (event.button == 1) { setTimeout(() => window.open('about:blank', '_blank'), 100); }">
|
||||
onclick="setTimeout(() => window.open('about:blank', '_blank'), 100);">
|
||||
</div>
|
||||
<div>
|
||||
<input id="delayed-popup" type="button" value="delayed popup"
|
||||
onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100);"
|
||||
onmousedown="if (event.button == 1) { setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100); }">
|
||||
onclick="setTimeout(() => window.open('about:blank', '_blank', 'popup=true'), 100);">
|
||||
<div>
|
||||
<div>
|
||||
<a id="link-instant" href=""
|
||||
|
|
|
@ -2654,6 +2654,17 @@ BrowserGlue.prototype = {
|
|||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "firefoxBridgeNativeMessaging",
|
||||
condition:
|
||||
(AppConstants.platform == "macosx" ||
|
||||
AppConstants.platform == "win") &&
|
||||
Services.prefs.getBoolPref("browser.firefoxbridge.enabled", false),
|
||||
task: async () => {
|
||||
await lazy.FirefoxBridgeExtensionUtils.ensureRegistered();
|
||||
},
|
||||
},
|
||||
|
||||
// Ensure a Private Browsing Shortcut exists. This is needed in case
|
||||
// a user tries to use Windows functionality to pin our Private Browsing
|
||||
// mode icon to the Taskbar (eg: the "Pin to Taskbar" context menu item).
|
||||
|
|
|
@ -56,4 +56,5 @@ skip-if = [
|
|||
"os == 'linux' && debug", # Bug 1804804 - disabled on win && linux for extremely high failure rate
|
||||
]
|
||||
|
||||
["browser_aboutwelcome_multistage_urlbar_focus.js"]
|
||||
["browser_aboutwelcome_multistage_transitions.js"]
|
||||
skip-if = ["debug"] # Bug 1875203
|
||||
|
|
|
@ -10,101 +10,6 @@ const { TelemetryTestUtils } = ChromeUtils.importESModule(
|
|||
"resource://testing-common/TelemetryTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const TEST_PROTON_CONTENT = [
|
||||
{
|
||||
id: "AW_STEP1",
|
||||
content: {
|
||||
title: "Step 1",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
secondary_button_top: {
|
||||
label: "link top",
|
||||
action: {
|
||||
type: "SHOW_FIREFOX_ACCOUNTS",
|
||||
data: { entrypoint: "test" },
|
||||
},
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP2",
|
||||
content: {
|
||||
title: "Step 2",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP3",
|
||||
content: {
|
||||
title: "Step 3",
|
||||
tiles: {
|
||||
type: "theme",
|
||||
action: {
|
||||
theme: "<event>",
|
||||
},
|
||||
data: [
|
||||
{
|
||||
theme: "automatic",
|
||||
label: "theme-1",
|
||||
tooltip: "test-tooltip",
|
||||
},
|
||||
{
|
||||
theme: "dark",
|
||||
label: "theme-2",
|
||||
},
|
||||
],
|
||||
},
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "Import",
|
||||
action: {
|
||||
type: "SHOW_MIGRATION_WIZARD",
|
||||
data: { source: "chrome" },
|
||||
},
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP4",
|
||||
content: {
|
||||
title: "Step 4",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Test the zero onboarding using ExperimentAPI
|
||||
*/
|
||||
|
@ -354,119 +259,6 @@ add_task(async function test_multistage_aboutwelcome_experimentAPI() {
|
|||
await doExperimentCleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the multistage proton welcome UI using ExperimentAPI with transitions
|
||||
*/
|
||||
add_task(async function test_multistage_aboutwelcome_transitions() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
|
||||
let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
enabled: true,
|
||||
screens: TEST_PROTON_CONTENT,
|
||||
transitions: true,
|
||||
},
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
const browser = tab.linkedBrowser;
|
||||
|
||||
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
|
||||
// Stub AboutWelcomeParent Content Message Handler
|
||||
sandbox.spy(aboutWelcomeActor, "onContentMessage");
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1",
|
||||
// Expected selectors:
|
||||
["div.proton.transition- .screen"],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out"]
|
||||
);
|
||||
|
||||
// Double click should still only transition once.
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await onButtonClick(browser, "button.primary");
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1 transition to 2",
|
||||
// Expected selectors:
|
||||
["div.proton.transition-out .screen", "div.proton.transition- .screen-1"]
|
||||
);
|
||||
|
||||
await doExperimentCleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the multistage proton welcome UI using ExperimentAPI without transitions
|
||||
*/
|
||||
add_task(async function test_multistage_aboutwelcome_transitions_off() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
|
||||
let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
enabled: true,
|
||||
screens: TEST_PROTON_CONTENT,
|
||||
transitions: false,
|
||||
},
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
const browser = tab.linkedBrowser;
|
||||
|
||||
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
|
||||
// Stub AboutWelcomeParent Content Message Handler
|
||||
sandbox.spy(aboutWelcomeActor, "onContentMessage");
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1",
|
||||
// Expected selectors:
|
||||
["div.proton.transition- .screen"],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out"]
|
||||
);
|
||||
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1 no transition to 2",
|
||||
// Expected selectors:
|
||||
[],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out .screen-0"]
|
||||
);
|
||||
|
||||
await doExperimentCleanup();
|
||||
});
|
||||
|
||||
/* Test multistage custom backdrop
|
||||
*/
|
||||
add_task(async function test_multistage_aboutwelcome_backdrop() {
|
||||
|
@ -590,3 +382,52 @@ add_task(async function test_multistage_aboutwelcome_utm_term() {
|
|||
|
||||
await doExperimentCleanup();
|
||||
});
|
||||
|
||||
add_task(async function test_multistage_aboutwelcome_newtab_urlbar_focus() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
const TEST_CONTENT = [
|
||||
{
|
||||
id: "TEST_SCREEN",
|
||||
content: {
|
||||
position: "split",
|
||||
logo: {},
|
||||
title: "Test newtab url focus",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
await pushPrefs(["browser.aboutwelcome.newtabUrlBarFocus", true]);
|
||||
|
||||
const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
screens: TEST_CONTENT,
|
||||
},
|
||||
});
|
||||
|
||||
const tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
const browser = tab.linkedBrowser;
|
||||
let focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus");
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await focused;
|
||||
Assert.ok(gURLBar.focused, "focus should be on url bar");
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
await doExperimentCleanup();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
"use strict";
|
||||
|
||||
const { ExperimentAPI } = ChromeUtils.importESModule(
|
||||
"resource://nimbus/ExperimentAPI.sys.mjs"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/NimbusTestUtils.sys.mjs"
|
||||
);
|
||||
const { TelemetryTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/TelemetryTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
const TEST_PROTON_CONTENT = [
|
||||
{
|
||||
id: "AW_STEP1",
|
||||
content: {
|
||||
title: "Step 1",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
secondary_button_top: {
|
||||
label: "link top",
|
||||
action: {
|
||||
type: "SHOW_FIREFOX_ACCOUNTS",
|
||||
data: { entrypoint: "test" },
|
||||
},
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP2",
|
||||
content: {
|
||||
title: "Step 2",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP3",
|
||||
content: {
|
||||
title: "Step 3",
|
||||
tiles: {
|
||||
type: "theme",
|
||||
action: {
|
||||
theme: "<event>",
|
||||
},
|
||||
data: [
|
||||
{
|
||||
theme: "automatic",
|
||||
label: "theme-1",
|
||||
tooltip: "test-tooltip",
|
||||
},
|
||||
{
|
||||
theme: "dark",
|
||||
label: "theme-2",
|
||||
},
|
||||
],
|
||||
},
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "Import",
|
||||
action: {
|
||||
type: "SHOW_MIGRATION_WIZARD",
|
||||
data: { source: "chrome" },
|
||||
},
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "AW_STEP4",
|
||||
content: {
|
||||
title: "Step 4",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: "link",
|
||||
},
|
||||
has_noodles: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Test the multistage proton welcome UI using ExperimentAPI with transitions
|
||||
*/
|
||||
add_task(async function test_multistage_aboutwelcome_transitions() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
|
||||
let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
enabled: true,
|
||||
screens: TEST_PROTON_CONTENT,
|
||||
transitions: true,
|
||||
},
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
const browser = tab.linkedBrowser;
|
||||
|
||||
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
|
||||
// Stub AboutWelcomeParent Content Message Handler
|
||||
sandbox.spy(aboutWelcomeActor, "onContentMessage");
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1",
|
||||
// Expected selectors:
|
||||
["div.proton.transition- .screen"],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out"]
|
||||
);
|
||||
|
||||
// Double click should still only transition once.
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await onButtonClick(browser, "button.primary");
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1 transition to 2",
|
||||
// Expected selectors:
|
||||
["div.proton.transition-out .screen", "div.proton.transition- .screen-1"]
|
||||
);
|
||||
|
||||
await doExperimentCleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test the multistage proton welcome UI using ExperimentAPI without transitions
|
||||
*/
|
||||
add_task(async function test_multistage_aboutwelcome_transitions_off() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
|
||||
let doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
enabled: true,
|
||||
screens: TEST_PROTON_CONTENT,
|
||||
transitions: false,
|
||||
},
|
||||
});
|
||||
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
|
||||
const browser = tab.linkedBrowser;
|
||||
|
||||
let aboutWelcomeActor = await getAboutWelcomeParent(browser);
|
||||
// Stub AboutWelcomeParent Content Message Handler
|
||||
sandbox.spy(aboutWelcomeActor, "onContentMessage");
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1",
|
||||
// Expected selectors:
|
||||
["div.proton.transition- .screen"],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out"]
|
||||
);
|
||||
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await test_screen_content(
|
||||
browser,
|
||||
"multistage proton step 1 no transition to 2",
|
||||
// Expected selectors:
|
||||
[],
|
||||
// Unexpected selectors:
|
||||
["div.proton.transition-out .screen-0"]
|
||||
);
|
||||
|
||||
await doExperimentCleanup();
|
||||
});
|
|
@ -1,60 +0,0 @@
|
|||
"use strict";
|
||||
|
||||
const { ExperimentAPI } = ChromeUtils.importESModule(
|
||||
"resource://nimbus/ExperimentAPI.sys.mjs"
|
||||
);
|
||||
const { ExperimentFakes } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/NimbusTestUtils.sys.mjs"
|
||||
);
|
||||
const { TelemetryTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/TelemetryTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
add_task(async function test_multistage_aboutwelcome_newtab_urlbar_focus() {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
const TEST_CONTENT = [
|
||||
{
|
||||
id: "TEST_SCREEN",
|
||||
content: {
|
||||
position: "split",
|
||||
logo: {},
|
||||
title: "Test newtab url focus",
|
||||
primary_button: {
|
||||
label: "Next",
|
||||
action: {
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
await setAboutWelcomePref(true);
|
||||
await ExperimentAPI.ready();
|
||||
await pushPrefs(["browser.aboutwelcome.newtabUrlBarFocus", true]);
|
||||
|
||||
const doExperimentCleanup = await ExperimentFakes.enrollWithFeatureConfig({
|
||||
featureId: "aboutwelcome",
|
||||
value: {
|
||||
id: "my-mochitest-experiment",
|
||||
screens: TEST_CONTENT,
|
||||
},
|
||||
});
|
||||
|
||||
const tab = await BrowserTestUtils.openNewForegroundTab(
|
||||
gBrowser,
|
||||
"about:welcome",
|
||||
true
|
||||
);
|
||||
const browser = tab.linkedBrowser;
|
||||
let focused = BrowserTestUtils.waitForEvent(gURLBar.inputField, "focus");
|
||||
await onButtonClick(browser, "button.primary");
|
||||
await focused;
|
||||
Assert.ok(gURLBar.focused, "focus should be on url bar");
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
sandbox.restore();
|
||||
});
|
||||
await doExperimentCleanup();
|
||||
});
|
|
@ -11,9 +11,7 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
"resource:///modules/asrouter/FeatureCalloutMessages.sys.mjs",
|
||||
|
||||
PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
|
||||
});
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
QueryCache: "resource:///modules/asrouter/ASRouterTargeting.jsm",
|
||||
QueryCache: "resource:///modules/asrouter/ASRouterTargeting.sys.mjs",
|
||||
});
|
||||
const { FxAccounts } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/FxAccounts.sys.mjs"
|
||||
|
|
15
browser/components/backup/BackupResources.sys.mjs
Normal file
15
browser/components/backup/BackupResources.sys.mjs
Normal file
|
@ -0,0 +1,15 @@
|
|||
/* 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 https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Remove this import after BackupResource is referenced elsewhere.
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BackupResource } from "resource:///modules/backup/BackupResource.sys.mjs";
|
||||
|
||||
/**
|
||||
* Classes exported here are registered as a resource that can be
|
||||
* backed up and restored in the BackupService.
|
||||
*
|
||||
* They must extend the BackupResource base class.
|
||||
*/
|
||||
export {};
|
|
@ -2,6 +2,8 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import * as BackupResources from "resource:///modules/backup/BackupResources.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
|
||||
|
@ -27,6 +29,13 @@ export class BackupService {
|
|||
*/
|
||||
static #instance = null;
|
||||
|
||||
/**
|
||||
* Map of instantiated BackupResource classes.
|
||||
*
|
||||
* @type {Map<string, BackupResource>}
|
||||
*/
|
||||
#resources = new Map();
|
||||
|
||||
/**
|
||||
* Returns a reference to a BackupService singleton. If this is the first time
|
||||
* that this getter is accessed, this causes the BackupService singleton to be
|
||||
|
@ -39,10 +48,56 @@ export class BackupService {
|
|||
if (this.#instance) {
|
||||
return this.#instance;
|
||||
}
|
||||
return (this.#instance = new BackupService());
|
||||
this.#instance = new BackupService(BackupResources);
|
||||
this.#instance.takeMeasurements();
|
||||
|
||||
return this.#instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Create a BackupService instance.
|
||||
*
|
||||
* @param {object} [backupResources=BackupResources] - Object containing BackupResource classes to associate with this service.
|
||||
*/
|
||||
constructor(backupResources = BackupResources) {
|
||||
lazy.logConsole.debug("Instantiated");
|
||||
|
||||
for (const resourceName in backupResources) {
|
||||
let resource = BackupResources[resourceName];
|
||||
this.#resources.set(resource.key, resource);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take measurements of the current profile state for Telemetry.
|
||||
*
|
||||
* @returns {Promise<undefined>}
|
||||
*/
|
||||
async takeMeasurements() {
|
||||
lazy.logConsole.debug("Taking Telemetry measurements");
|
||||
|
||||
// Note: We're talking about kilobytes here, not kibibytes. That means
|
||||
// 1000 bytes, and not 1024 bytes.
|
||||
const BYTES_IN_KB = 1000;
|
||||
const BYTES_IN_MB = 1000000;
|
||||
|
||||
// We'll start by measuring the available disk space on the storage
|
||||
// device that the profile directory is on.
|
||||
let profileDir = await IOUtils.getFile(PathUtils.profileDir);
|
||||
|
||||
let profDDiskSpaceBytes = profileDir.diskSpaceAvailable;
|
||||
|
||||
// Make the measurement fuzzier by rounding to the nearest 10MB.
|
||||
let profDDiskSpaceMB =
|
||||
Math.round(profDDiskSpaceBytes / BYTES_IN_MB / 100) * 100;
|
||||
|
||||
// And then record the value in kilobytes, since that's what everything
|
||||
// else is going to be measured in.
|
||||
Glean.browserBackup.profDDiskSpace.set(profDDiskSpaceMB * BYTES_IN_KB);
|
||||
|
||||
// Measure the size of each file we are going to backup.
|
||||
for (let resourceClass of this.#resources.values()) {
|
||||
await new resourceClass().measure(PathUtils.profileDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
29
browser/components/backup/metrics.yaml
Normal file
29
browser/components/backup/metrics.yaml
Normal file
|
@ -0,0 +1,29 @@
|
|||
# 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/.
|
||||
|
||||
# Adding a new metric? We have docs for that!
|
||||
# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
|
||||
|
||||
---
|
||||
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
|
||||
$tags:
|
||||
- 'Firefox :: Profiles'
|
||||
|
||||
browser.backup:
|
||||
prof_d_disk_space:
|
||||
type: quantity
|
||||
unit: kilobyte
|
||||
description: >
|
||||
The total disk space available on the storage device that the profile
|
||||
directory is stored on. To reduce fingerprintability, we round to the
|
||||
nearest 10 megabytes and return the result in kilobytes.
|
||||
bugs:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1884407
|
||||
data_reviews:
|
||||
- https://bugzilla.mozilla.org/show_bug.cgi?id=1884407
|
||||
data_sensitivity:
|
||||
- technical
|
||||
notification_emails:
|
||||
- mconley@mozilla.com
|
||||
expires: never
|
|
@ -4,8 +4,15 @@
|
|||
# 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/.
|
||||
|
||||
with Files("**"):
|
||||
BUG_COMPONENT = ("Firefox", "Profiles")
|
||||
|
||||
SPHINX_TREES["docs"] = "docs"
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
|
||||
|
||||
EXTRA_JS_MODULES.backup += [
|
||||
"BackupResources.sys.mjs",
|
||||
"BackupService.sys.mjs",
|
||||
"resources/BackupResource.sys.mjs",
|
||||
]
|
||||
|
|
109
browser/components/backup/resources/BackupResource.sys.mjs
Normal file
109
browser/components/backup/resources/BackupResource.sys.mjs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/* 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 https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Convert from bytes to kilobytes (not kibibytes).
|
||||
const BYTES_IN_KB = 1000;
|
||||
|
||||
/**
|
||||
* An abstract class representing a set of data within a user profile
|
||||
* that can be persisted to a separate backup archive file, and restored
|
||||
* to a new user profile from that backup archive file.
|
||||
*/
|
||||
export class BackupResource {
|
||||
/**
|
||||
* This must be overridden to return a simple string identifier for the
|
||||
* resource, for example "places" or "extensions". This key is used as
|
||||
* a unique identifier for the resource.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
static get key() {
|
||||
throw new Error("BackupResource::key needs to be overridden.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of a file.
|
||||
*
|
||||
* @param {string} filePath - path to a file.
|
||||
* @returns {Promise<number|null>} - the size of the file in kilobytes, or null if the
|
||||
* file does not exist, the path is a directory or the size is unknown.
|
||||
*/
|
||||
static async getFileSize(filePath) {
|
||||
if (!(await IOUtils.exists(filePath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { size } = await IOUtils.stat(filePath);
|
||||
|
||||
if (size < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let sizeInKb = Math.ceil(size / BYTES_IN_KB);
|
||||
// Make the measurement fuzzier by rounding to the nearest 10kb.
|
||||
let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
|
||||
|
||||
return Math.max(nearestTenthKb, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total size of a directory.
|
||||
*
|
||||
* @param {string} directoryPath - path to a directory.
|
||||
* @returns {Promise<number|null>} - the size of all descendants of the directory in kilobytes, or null if the
|
||||
* directory does not exist, the path is not a directory or the size is unknown.
|
||||
*/
|
||||
static async getDirectorySize(directoryPath) {
|
||||
if (!(await IOUtils.exists(directoryPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { type } = await IOUtils.stat(directoryPath);
|
||||
|
||||
if (type != "directory") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let children = await IOUtils.getChildren(directoryPath, {
|
||||
ignoreAbsent: true,
|
||||
});
|
||||
|
||||
let size = 0;
|
||||
for (const childFilePath of children) {
|
||||
let { size: childSize, type: childType } = await IOUtils.stat(
|
||||
childFilePath
|
||||
);
|
||||
|
||||
if (childSize >= 0) {
|
||||
let sizeInKb = Math.ceil(childSize / BYTES_IN_KB);
|
||||
// Make the measurement fuzzier by rounding to the nearest 10kb.
|
||||
let nearestTenthKb = Math.round(sizeInKb / 10) * 10;
|
||||
size += Math.max(nearestTenthKb, 1);
|
||||
}
|
||||
|
||||
if (childType == "directory") {
|
||||
let childDirectorySize = await this.getDirectorySize(childFilePath);
|
||||
if (Number.isInteger(childDirectorySize)) {
|
||||
size += childDirectorySize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* This must be overridden to record telemetry on the size of any
|
||||
* data associated with this BackupResource.
|
||||
*
|
||||
* @param {string} profilePath - path to a profile directory.
|
||||
* @returns {Promise<undefined>}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async measure(profilePath) {
|
||||
throw new Error("BackupResource::measure needs to be overridden.");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"chrome://browser/content/browser.xhtml": {
|
||||
"PersonalToolbar": { "collapsed": "false" },
|
||||
"main-window": {
|
||||
"screenX": "852",
|
||||
"screenY": "125",
|
||||
"width": "1484",
|
||||
"height": "1256",
|
||||
"sizemode": "normal"
|
||||
},
|
||||
"sidebar-box": {
|
||||
"sidebarcommand": "viewBookmarksSidebar",
|
||||
"width": "323",
|
||||
"style": "width: 323px;"
|
||||
},
|
||||
"sidebar-title": { "value": "Bookmarks" }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { BackupResource } = ChromeUtils.importESModule(
|
||||
"resource:///modules/backup/BackupResource.sys.mjs"
|
||||
);
|
||||
|
||||
const EXPECTED_KILOBYTES_FOR_XULSTORE = 1;
|
||||
|
||||
add_setup(() => {
|
||||
do_get_profile();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that BackupService.getFileSize will get the size of a file in kilobytes.
|
||||
*/
|
||||
add_task(async function test_getFileSize() {
|
||||
let file = do_get_file("data/test_xulstore.json");
|
||||
|
||||
let testFilePath = PathUtils.join(PathUtils.profileDir, "test_xulstore.json");
|
||||
|
||||
await IOUtils.copy(file.path, PathUtils.profileDir);
|
||||
|
||||
let size = await BackupResource.getFileSize(testFilePath);
|
||||
|
||||
Assert.equal(
|
||||
size,
|
||||
EXPECTED_KILOBYTES_FOR_XULSTORE,
|
||||
"Size of the test_xulstore.json is rounded up to the nearest kilobyte."
|
||||
);
|
||||
|
||||
await IOUtils.remove(testFilePath);
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that BackupService.getFileSize will get the total size of all the files in a directory and it's children in kilobytes.
|
||||
*/
|
||||
add_task(async function test_getDirectorySize() {
|
||||
let file = do_get_file("data/test_xulstore.json");
|
||||
|
||||
// Create a test directory with the test json file in it.
|
||||
let testDir = PathUtils.join(PathUtils.profileDir, "testDir");
|
||||
await IOUtils.makeDirectory(testDir);
|
||||
await IOUtils.copy(file.path, testDir);
|
||||
|
||||
// Create another test directory inside of that one.
|
||||
let nestedTestDir = PathUtils.join(testDir, "testDir");
|
||||
await IOUtils.makeDirectory(nestedTestDir);
|
||||
await IOUtils.copy(file.path, nestedTestDir);
|
||||
|
||||
let size = await BackupResource.getDirectorySize(testDir);
|
||||
|
||||
Assert.equal(
|
||||
size,
|
||||
EXPECTED_KILOBYTES_FOR_XULSTORE * 2,
|
||||
`Total size of the directory is rounded up to the nearest kilobyte
|
||||
and is equal to twice the size of the test_xulstore.json file`
|
||||
);
|
||||
|
||||
await IOUtils.remove(testDir, { recursive: true });
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const { BackupService } = ChromeUtils.importESModule(
|
||||
"resource:///modules/backup/BackupService.sys.mjs"
|
||||
);
|
||||
|
||||
add_setup(() => {
|
||||
do_get_profile();
|
||||
// FOG needs to be initialized in order for data to flow.
|
||||
Services.fog.initializeFOG();
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests that we can measure the disk space available in the profile directory.
|
||||
*/
|
||||
add_task(async function test_profDDiskSpace() {
|
||||
let bs = new BackupService();
|
||||
await bs.takeMeasurements();
|
||||
Assert.greater(
|
||||
Glean.browserBackup.profDDiskSpace.testGetValue(),
|
||||
0,
|
||||
"Should have collected a measurement for the profile directory storage " +
|
||||
"device"
|
||||
);
|
||||
});
|
8
browser/components/backup/tests/xpcshell/xpcshell.toml
Normal file
8
browser/components/backup/tests/xpcshell/xpcshell.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[DEFAULT]
|
||||
firefox-appdir = "browser"
|
||||
skip-if = ["os == 'android'"]
|
||||
|
||||
["test_BrowserResource.js"]
|
||||
support-files = ["data/test_xulstore.json"]
|
||||
|
||||
["test_measurements.js"]
|
|
@ -189,7 +189,7 @@ add_task(async function test_install_and_remove() {
|
|||
Assert.notEqual(engine, null, "Specified search engine should be installed");
|
||||
|
||||
Assert.equal(
|
||||
engine.wrappedJSObject.getIconURL(),
|
||||
await engine.wrappedJSObject.getIconURL(),
|
||||
iconURL,
|
||||
"Icon should be present"
|
||||
);
|
||||
|
|
|
@ -40,7 +40,7 @@ this.search = class extends ExtensionAPI {
|
|||
let defaultEngine = await Services.search.getDefault();
|
||||
return Promise.all(
|
||||
visibleEngines.map(async engine => {
|
||||
let favIconUrl = engine.getIconURL();
|
||||
let favIconUrl = await engine.getIconURL();
|
||||
// Convert moz-extension:-URLs to data:-URLs to make sure that
|
||||
// extensions can see icons from other extensions, even if they
|
||||
// are not web-accessible.
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
/* vim: set sts=2 sw=2 et tw=80: */
|
||||
"use strict";
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PromptTestUtils",
|
||||
"resource://testing-common/PromptTestUtils.jsm"
|
||||
const { PromptTestUtils } = ChromeUtils.importESModule(
|
||||
"resource://testing-common/PromptTestUtils.sys.mjs"
|
||||
);
|
||||
|
||||
async function waitForExtensionModalPrompt(extension) {
|
||||
|
|
|
@ -25,18 +25,18 @@ server.registerPathHandler("/ico.png", (request, response) => {
|
|||
response.write(atob(HTTP_ICON_DATA));
|
||||
});
|
||||
|
||||
function promiseEngineIconLoaded(engineName) {
|
||||
return TestUtils.topicObserved(
|
||||
async function promiseEngineIconLoaded(engineName) {
|
||||
await TestUtils.topicObserved(
|
||||
"browser-search-engine-modified",
|
||||
(engine, verb) => {
|
||||
engine.QueryInterface(Ci.nsISearchEngine);
|
||||
return (
|
||||
verb == "engine-changed" &&
|
||||
engine.name == engineName &&
|
||||
engine.getIconURL()
|
||||
);
|
||||
return verb == "engine-changed" && engine.name == engineName;
|
||||
}
|
||||
);
|
||||
Assert.ok(
|
||||
await Services.search.getEngineByName(engineName).getIconURL(),
|
||||
"Should have a valid icon URL"
|
||||
);
|
||||
}
|
||||
|
||||
add_task(async function test_search_favicon() {
|
||||
|
|
|
@ -60,19 +60,19 @@ add_task(async function test_extension_adding_engine() {
|
|||
|
||||
let { baseURI } = ext1.extension;
|
||||
equal(
|
||||
engine.getIconURL(),
|
||||
await engine.getIconURL(),
|
||||
baseURI.resolve("foo.ico"),
|
||||
"16x16 icon path matches"
|
||||
);
|
||||
equal(
|
||||
engine.getIconURL(16),
|
||||
await engine.getIconURL(16),
|
||||
baseURI.resolve("foo.ico"),
|
||||
"16x16 icon path matches"
|
||||
);
|
||||
// TODO: Bug 1871036 - Differently sized icons are currently incorrectly
|
||||
// handled for add-ons.
|
||||
// equal(
|
||||
// engine.getIconURL(32),
|
||||
// await engine.getIconURL(32),
|
||||
// baseURI.resolve("foo32.ico"),
|
||||
// "32x32 icon path matches"
|
||||
// );
|
||||
|
|
|
@ -23,7 +23,9 @@ const {
|
|||
BOOKMARKS_RESTORE_SUCCESS_EVENT,
|
||||
BOOKMARKS_RESTORE_FAILED_EVENT,
|
||||
SECTION_ID,
|
||||
} = ChromeUtils.import("resource://activity-stream/lib/HighlightsFeed.jsm");
|
||||
} = ChromeUtils.importESModule(
|
||||
"resource://activity-stream/lib/HighlightsFeed.sys.mjs"
|
||||
);
|
||||
|
||||
const FAKE_LINKS = new Array(20)
|
||||
.fill(null)
|
||||
|
|
|
@ -6,11 +6,6 @@ let authPromptModalType = Services.prefs.getIntPref(
|
|||
"prompts.modalType.httpAuth"
|
||||
);
|
||||
|
||||
let commonDialogEnabled =
|
||||
authPromptModalType === Services.prompt.MODAL_TYPE_WINDOW ||
|
||||
(authPromptModalType === Services.prompt.MODAL_TYPE_TAB &&
|
||||
Services.prefs.getBoolPref("prompts.tabChromePromptSubDialog"));
|
||||
|
||||
let server = new HttpServer();
|
||||
server.registerPathHandler("/file.html", fileHandler);
|
||||
server.start(-1);
|
||||
|
@ -38,23 +33,15 @@ function fileHandler(metadata, response) {
|
|||
}
|
||||
|
||||
function onCommonDialogLoaded(subject) {
|
||||
let dialog;
|
||||
if (commonDialogEnabled) {
|
||||
dialog = subject.Dialog;
|
||||
} else {
|
||||
let promptBox =
|
||||
subject.ownerGlobal.gBrowser.selectedBrowser.tabModalPromptBox;
|
||||
dialog = promptBox.getPrompt(subject).Dialog;
|
||||
}
|
||||
let dialog = subject.Dialog;
|
||||
|
||||
// Submit random account and password
|
||||
dialog.ui.loginTextbox.setAttribute("value", Math.random());
|
||||
dialog.ui.password1Textbox.setAttribute("value", Math.random());
|
||||
dialog.ui.button0.click();
|
||||
}
|
||||
|
||||
let authPromptTopic = commonDialogEnabled
|
||||
? "common-dialog-loaded"
|
||||
: "tabmodal-dialog-loaded";
|
||||
let authPromptTopic = "common-dialog-loaded";
|
||||
Services.obs.addObserver(onCommonDialogLoaded, authPromptTopic);
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
<head>
|
||||
<!-- @CSP: We should remove 'unsafe-inline' from style-src, see Bug 1579160 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src chrome:; img-src chrome: moz-icon: https: blob: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
|
||||
|
||||
<title data-l10n-id="settings-page-title"></title>
|
||||
|
||||
|
|
|
@ -628,6 +628,16 @@ class EngineStore {
|
|||
}
|
||||
}
|
||||
|
||||
notifyEngineIconUpdated(engine) {
|
||||
// Check the engine is still in the list.
|
||||
let index = this._getIndexForEngine(engine);
|
||||
if (index != -1) {
|
||||
for (let listener of this.#listeners) {
|
||||
listener.engineIconUpdated(index, this.engines);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getIndexForEngine(aEngine) {
|
||||
return this.engines.indexOf(aEngine);
|
||||
}
|
||||
|
@ -638,12 +648,28 @@ class EngineStore {
|
|||
|
||||
_cloneEngine(aEngine) {
|
||||
var clonedObj = {
|
||||
iconURL: aEngine.getIconURL(),
|
||||
iconURL: null,
|
||||
};
|
||||
for (let i of ["id", "name", "alias", "hidden"]) {
|
||||
clonedObj[i] = aEngine[i];
|
||||
}
|
||||
clonedObj.originalEngine = aEngine;
|
||||
|
||||
// Trigger getting the iconURL for this engine.
|
||||
aEngine.getIconURL().then(iconURL => {
|
||||
if (iconURL) {
|
||||
clonedObj.iconURL = iconURL;
|
||||
} else if (window.devicePixelRatio > 1) {
|
||||
clonedObj.iconURL =
|
||||
"chrome://browser/skin/search-engine-placeholder@2x.png";
|
||||
} else {
|
||||
clonedObj.iconURL =
|
||||
"chrome://browser/skin/search-engine-placeholder.png";
|
||||
}
|
||||
|
||||
this.notifyEngineIconUpdated(clonedObj);
|
||||
});
|
||||
|
||||
return clonedObj;
|
||||
}
|
||||
|
||||
|
@ -895,6 +921,13 @@ class EngineView {
|
|||
}
|
||||
}
|
||||
|
||||
engineIconUpdated(index) {
|
||||
this.tree?.invalidateCell(
|
||||
index,
|
||||
this.tree.columns.getNamedColumn("engineName")
|
||||
);
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
this.tree?.invalidate();
|
||||
}
|
||||
|
@ -1128,14 +1161,7 @@ class EngineView {
|
|||
return shortcut.icon;
|
||||
}
|
||||
|
||||
if (this._engineStore.engines[index].iconURL) {
|
||||
return this._engineStore.engines[index].iconURL;
|
||||
}
|
||||
|
||||
if (window.devicePixelRatio > 1) {
|
||||
return "chrome://browser/skin/search-engine-placeholder@2x.png";
|
||||
}
|
||||
return "chrome://browser/skin/search-engine-placeholder.png";
|
||||
return this._engineStore.engines[index].iconURL;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
@ -1381,6 +1407,14 @@ class DefaultEngineDropDown {
|
|||
}
|
||||
}
|
||||
|
||||
engineIconUpdated(index, enginesList) {
|
||||
let item = this.#element.getItemAtIndex(index);
|
||||
// Check this is the right item.
|
||||
if (item?.label == enginesList[index].name) {
|
||||
item.setAttribute("image", enginesList[index].iconURL);
|
||||
}
|
||||
}
|
||||
|
||||
async rebuild(enginesList) {
|
||||
if (
|
||||
this.#type == "private" &&
|
||||
|
|
|
@ -46,6 +46,10 @@ add_task(async function test_change_engine() {
|
|||
let row = findRow(tree, "Example");
|
||||
|
||||
Assert.notEqual(row, -1, "Should have found the entry");
|
||||
await TestUtils.waitForCondition(
|
||||
() => tree.view.getImageSrc(row, tree.columns.getNamedColumn("engineName")),
|
||||
"Should have go an image URL"
|
||||
);
|
||||
Assert.ok(
|
||||
tree.view
|
||||
.getImageSrc(row, tree.columns.getNamedColumn("engineName"))
|
||||
|
@ -74,6 +78,13 @@ add_task(async function test_change_engine() {
|
|||
row = findRow(tree, "Example 2");
|
||||
|
||||
Assert.notEqual(row, -1, "Should have found the updated entry");
|
||||
await TestUtils.waitForCondition(
|
||||
() =>
|
||||
tree.view
|
||||
.getImageSrc(row, tree.columns.getNamedColumn("engineName"))
|
||||
?.includes("img456.png"),
|
||||
"Should have updated the image URL"
|
||||
);
|
||||
Assert.ok(
|
||||
tree.view
|
||||
.getImageSrc(row, tree.columns.getNamedColumn("engineName"))
|
||||
|
|
|
@ -55,7 +55,7 @@ add_setup(async function () {
|
|||
Ci.nsISearchService.CHANGE_REASON_UNKNOWN
|
||||
);
|
||||
expectedEngineAlias = privateEngine.aliases[0];
|
||||
expectedIconURL = privateEngine.getIconURL();
|
||||
expectedIconURL = await privateEngine.getIconURL();
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await Services.search.setDefaultPrivate(
|
||||
|
@ -101,11 +101,28 @@ add_task(async function test_search_icon() {
|
|||
let { win, tab } = await openAboutPrivateBrowsing();
|
||||
|
||||
await SpecialPowers.spawn(tab, [expectedIconURL], async function (iconURL) {
|
||||
is(
|
||||
content.document.body.getAttribute("style"),
|
||||
`--newtab-search-icon: url(${iconURL});`,
|
||||
"Should have the correct icon URL for the logo"
|
||||
let computedStyle = content.window.getComputedStyle(content.document.body);
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => computedStyle.getPropertyValue("--newtab-search-icon") != "null",
|
||||
"Search Icon should get set."
|
||||
);
|
||||
|
||||
if (iconURL.startsWith("blob:")) {
|
||||
// We don't check the data here as `browser_contentSearch.js` performs
|
||||
// those checks.
|
||||
Assert.ok(
|
||||
computedStyle
|
||||
.getPropertyValue("--newtab-search-icon")
|
||||
.startsWith("url(blob:"),
|
||||
"Should have a blob URL for the logo"
|
||||
);
|
||||
} else {
|
||||
Assert.equal(
|
||||
computedStyle.getPropertyValue("--newtab-search-icon"),
|
||||
`url(${iconURL})`,
|
||||
"Should have the correct icon URL for the logo"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await BrowserTestUtils.closeWindow(win);
|
||||
|
|
|
@ -466,7 +466,7 @@ export class SearchOneOffs {
|
|||
this.settingsButton.id = origin + "-anon-search-settings";
|
||||
|
||||
let engines = (await this.getEngineInfo()).engines;
|
||||
this._rebuildEngineList(engines, addEngines);
|
||||
await this._rebuildEngineList(engines, addEngines);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -477,14 +477,14 @@ export class SearchOneOffs {
|
|||
* @param {Array} addEngines
|
||||
* The engines that can be added.
|
||||
*/
|
||||
_rebuildEngineList(engines, addEngines) {
|
||||
async _rebuildEngineList(engines, addEngines) {
|
||||
for (let i = 0; i < engines.length; ++i) {
|
||||
let engine = engines[i];
|
||||
let button = this.document.createXULElement("button");
|
||||
button.engine = engine;
|
||||
button.id = this._buttonIDForEngine(engine);
|
||||
let iconURL =
|
||||
engine.getIconURL() ||
|
||||
(await engine.getIconURL()) ||
|
||||
"chrome://browser/skin/search-engine-placeholder.png";
|
||||
button.setAttribute("image", iconURL);
|
||||
button.setAttribute("class", "searchbar-engine-one-off-item");
|
||||
|
@ -981,7 +981,7 @@ export class SearchOneOffs {
|
|||
this.handleSearchCommand(event, engine);
|
||||
}
|
||||
|
||||
_on_command(event) {
|
||||
async _on_command(event) {
|
||||
let target = event.target;
|
||||
|
||||
if (target == this.settingsButton) {
|
||||
|
@ -1043,7 +1043,7 @@ export class SearchOneOffs {
|
|||
// search engine first. Doing this as opposed to rebuilding all the
|
||||
// one-off buttons avoids flicker.
|
||||
let iconURL =
|
||||
currentEngine.getIconURL() ||
|
||||
(await currentEngine.getIconURL()) ||
|
||||
"chrome://browser/skin/search-engine-placeholder.png";
|
||||
button.setAttribute("image", iconURL);
|
||||
button.setAttribute("tooltiptext", currentEngine.name);
|
||||
|
|
|
@ -232,7 +232,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
let uri = engine.getIconURL();
|
||||
let uri = await engine.getIconURL();
|
||||
if (uri) {
|
||||
this.setAttribute("src", uri);
|
||||
} else {
|
||||
|
|
|
@ -50,6 +50,14 @@ add_setup(async function () {
|
|||
await SearchTestUtils.promiseNewSearchEngine({
|
||||
url: getRootDirectory(gTestPath) + "testEngine_chromeicon.xml",
|
||||
});
|
||||
|
||||
// Install a WebExtension based engine to allow testing passing of plain
|
||||
// URIs (moz-extension://) to the content process.
|
||||
await SearchTestUtils.installSearchExtension({
|
||||
icons: {
|
||||
16: "favicon.ico",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function GetState() {
|
||||
|
@ -460,7 +468,7 @@ var currentStateObj = async function (isPrivateWindowValue, hiddenEngine = "") {
|
|||
),
|
||||
};
|
||||
for (let engine of await Services.search.getVisibleEngines()) {
|
||||
let uri = engine.getIconURL(16);
|
||||
let uri = await engine.getIconURL(16);
|
||||
state.engines.push({
|
||||
name: engine.name,
|
||||
iconData: await iconDataFromURI(uri),
|
||||
|
@ -476,7 +484,7 @@ var currentStateObj = async function (isPrivateWindowValue, hiddenEngine = "") {
|
|||
};
|
||||
|
||||
async function constructEngineObj(engine) {
|
||||
let uriFavicon = engine.getIconURL(16);
|
||||
let uriFavicon = await engine.getIconURL(16);
|
||||
return {
|
||||
name: engine.name,
|
||||
iconData: await iconDataFromURI(uriFavicon),
|
||||
|
@ -491,7 +499,7 @@ function iconDataFromURI(uri) {
|
|||
);
|
||||
}
|
||||
|
||||
if (!uri.startsWith("data:")) {
|
||||
if (!uri.startsWith("data:") && !uri.startsWith("blob:")) {
|
||||
plainURIIconTested = true;
|
||||
return Promise.resolve(uri);
|
||||
}
|
||||
|
|
|
@ -24,17 +24,6 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
"resource://gre/modules/SearchSuggestionController.sys.mjs",
|
||||
});
|
||||
|
||||
const pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
|
||||
BrowserTestUtils.registerAboutPage(
|
||||
registerCleanupFunction,
|
||||
"test-about-content-search-ui",
|
||||
pageURL,
|
||||
Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
|
||||
Ci.nsIAboutModule.ALLOW_SCRIPT |
|
||||
Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS
|
||||
);
|
||||
|
||||
requestLongerTimeout(2);
|
||||
|
||||
function waitForSuggestions() {
|
||||
|
@ -261,6 +250,19 @@ let extension1;
|
|||
let extension2;
|
||||
|
||||
add_setup(async function () {
|
||||
const pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
|
||||
|
||||
let cleanupAboutPage;
|
||||
await BrowserTestUtils.registerAboutPage(
|
||||
callback => (cleanupAboutPage = callback),
|
||||
"test-about-content-search-ui",
|
||||
pageURL,
|
||||
Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
|
||||
Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
|
||||
Ci.nsIAboutModule.ALLOW_SCRIPT |
|
||||
Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS
|
||||
);
|
||||
|
||||
let originalOnMessageSearch = ContentSearch._onMessageSearch;
|
||||
let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
|
||||
|
||||
|
@ -290,8 +292,20 @@ add_setup(async function () {
|
|||
}
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
// Ensure tabs are closed before we continue on with the cleanup.
|
||||
for (let tab of tabs) {
|
||||
BrowserTestUtils.removeTab(tab);
|
||||
}
|
||||
Services.search.restoreDefaultEngines();
|
||||
|
||||
await TestUtils.waitForTick();
|
||||
|
||||
ContentSearch._onMessageSearch = originalOnMessageSearch;
|
||||
ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
|
||||
|
||||
if (cleanupAboutPage) {
|
||||
await cleanupAboutPage();
|
||||
}
|
||||
});
|
||||
|
||||
await promiseTab();
|
||||
|
@ -1096,10 +1110,6 @@ add_task(async function settings() {
|
|||
await msg("reset");
|
||||
});
|
||||
|
||||
add_task(async function cleanup() {
|
||||
Services.search.restoreDefaultEngines();
|
||||
});
|
||||
|
||||
function checkState(
|
||||
actualState,
|
||||
expectedInputVal,
|
||||
|
@ -1147,10 +1157,10 @@ function checkState(
|
|||
}
|
||||
|
||||
var gMsgMan;
|
||||
|
||||
var tabs = [];
|
||||
async function promiseTab() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
|
||||
registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));
|
||||
tabs.push(tab);
|
||||
|
||||
let loadedPromise = BrowserTestUtils.firstBrowserLoaded(window);
|
||||
openTrustedLinkIn("about:test-about-content-search-ui", "current");
|
||||
|
|
|
@ -58,11 +58,22 @@ async function ensureIcon(tab, expectedIcon) {
|
|||
"Search Icon not set."
|
||||
);
|
||||
|
||||
Assert.equal(
|
||||
computedStyle.getPropertyValue("--newtab-search-icon"),
|
||||
`url(${icon})`,
|
||||
"Should have the expected icon"
|
||||
);
|
||||
if (icon.startsWith("blob:")) {
|
||||
// We don't check the data here as `browser_contentSearch.js` performs
|
||||
// those checks.
|
||||
Assert.ok(
|
||||
computedStyle
|
||||
.getPropertyValue("--newtab-search-icon")
|
||||
.startsWith("url(blob:"),
|
||||
"Should have a blob URL"
|
||||
);
|
||||
} else {
|
||||
Assert.equal(
|
||||
computedStyle.getPropertyValue("--newtab-search-icon"),
|
||||
`url(${icon})`,
|
||||
"Should have the expected icon"
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -96,7 +107,7 @@ async function runNewTabTest(isHandoff) {
|
|||
waitForLoad: false,
|
||||
});
|
||||
|
||||
let engineIcon = defaultEngine.getIconURL(16);
|
||||
let engineIcon = await defaultEngine.getIconURL(16);
|
||||
|
||||
await ensureIcon(tab, engineIcon);
|
||||
if (isHandoff) {
|
||||
|
@ -162,7 +173,7 @@ add_task(async function test_content_search_attributes_in_private_window() {
|
|||
});
|
||||
let tab = win.gBrowser.selectedTab;
|
||||
|
||||
let engineIcon = defaultEngine.getIconURL(16);
|
||||
let engineIcon = await defaultEngine.getIconURL(16);
|
||||
|
||||
await ensureIcon(tab, engineIcon);
|
||||
await ensurePlaceholder(
|
||||
|
|
|
@ -31,6 +31,47 @@ const CONFIG_DEFAULT = [
|
|||
},
|
||||
];
|
||||
|
||||
const CONFIG_V2 = [
|
||||
{
|
||||
recordType: "engine",
|
||||
identifier: "basic",
|
||||
base: {
|
||||
name: "basic",
|
||||
urls: {
|
||||
search: {
|
||||
base: "https://example.com",
|
||||
searchTermParamName: "q",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: [{ environment: { allRegionsAndLocales: true } }],
|
||||
},
|
||||
{
|
||||
recordType: "engine",
|
||||
identifier: "private",
|
||||
base: {
|
||||
name: "private",
|
||||
urls: {
|
||||
search: {
|
||||
base: "https://example.com",
|
||||
searchTermParamName: "q",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: [{ environment: { allRegionsAndLocales: true } }],
|
||||
},
|
||||
{
|
||||
recordType: "defaultEngines",
|
||||
globalDefault: "basic",
|
||||
globalDefaultPrivate: "private",
|
||||
specificDefaults: [],
|
||||
},
|
||||
{
|
||||
recordType: "engineOrders",
|
||||
orders: [],
|
||||
},
|
||||
];
|
||||
|
||||
SearchTestUtils.init(this);
|
||||
|
||||
add_setup(async () => {
|
||||
|
@ -50,7 +91,9 @@ add_setup(async () => {
|
|||
});
|
||||
|
||||
SearchTestUtils.useMockIdleService();
|
||||
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
|
||||
await SearchTestUtils.updateRemoteSettingsConfig(
|
||||
SearchUtils.newSearchConfigEnabled ? CONFIG_V2 : CONFIG_DEFAULT
|
||||
);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
let settingsWritten = SearchTestUtils.promiseSearchNotification(
|
||||
|
|
|
@ -71,10 +71,10 @@ async function testSearchBarChangeEngine(win, testPrivate, isPrivateWindow) {
|
|||
|
||||
if (testPrivate == isPrivateWindow) {
|
||||
let expectedName = originalEngine.name;
|
||||
let expectedImage = originalEngine.getIconURL();
|
||||
let expectedImage = await originalEngine.getIconURL();
|
||||
if (isPrivateWindow) {
|
||||
expectedName = originalPrivateEngine.name;
|
||||
expectedImage = originalPrivateEngine.getIconURL();
|
||||
expectedImage = await originalPrivateEngine.getIconURL();
|
||||
}
|
||||
|
||||
Assert.equal(
|
||||
|
|
|
@ -17,6 +17,58 @@ const CONFIG_DEFAULT = [
|
|||
},
|
||||
];
|
||||
|
||||
const CONFIG_V2 = [
|
||||
{
|
||||
recordType: "engine",
|
||||
identifier: "basic",
|
||||
base: {
|
||||
name: "basic",
|
||||
urls: {
|
||||
search: {
|
||||
base: "https://example.com",
|
||||
searchTermParamName: "q",
|
||||
},
|
||||
trending: {
|
||||
base: "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs",
|
||||
method: "GET",
|
||||
params: [
|
||||
{
|
||||
name: "richsuggestions",
|
||||
value: "true",
|
||||
},
|
||||
],
|
||||
},
|
||||
suggestions: {
|
||||
base: "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs",
|
||||
method: "GET",
|
||||
params: [
|
||||
{
|
||||
name: "richsuggestions",
|
||||
value: "true",
|
||||
},
|
||||
],
|
||||
searchTermParamName: "query",
|
||||
},
|
||||
},
|
||||
aliases: ["basic"],
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
environment: { allRegionsAndLocales: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
recordType: "defaultEngines",
|
||||
globalDefault: "basic",
|
||||
specificDefaults: [],
|
||||
},
|
||||
{
|
||||
recordType: "engineOrders",
|
||||
orders: [],
|
||||
},
|
||||
];
|
||||
|
||||
SearchTestUtils.init(this);
|
||||
|
||||
add_setup(async () => {
|
||||
|
@ -37,7 +89,9 @@ add_setup(async () => {
|
|||
});
|
||||
|
||||
SearchTestUtils.useMockIdleService();
|
||||
await SearchTestUtils.updateRemoteSettingsConfig(CONFIG_DEFAULT);
|
||||
await SearchTestUtils.updateRemoteSettingsConfig(
|
||||
SearchUtils.newSearchConfigEnabled ? CONFIG_V2 : CONFIG_DEFAULT
|
||||
);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
let settingsWritten = SearchTestUtils.promiseSearchNotification(
|
||||
|
|
|
@ -22,9 +22,13 @@ const SEARCH_ENGINE_DETAILS = [
|
|||
},
|
||||
{
|
||||
alias: "b",
|
||||
baseURL: `https://www.bing.com/search?{code}pc=${
|
||||
SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "MOZR" : "MOZI"
|
||||
}&q=foo`,
|
||||
baseURL: SearchUtils.newSearchConfigEnabled
|
||||
? `https://www.bing.com/search?pc=${
|
||||
SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "MOZR" : "MOZI"
|
||||
}&{code}q=foo`
|
||||
: `https://www.bing.com/search?{code}pc=${
|
||||
SearchUtils.MODIFIED_APP_CHANNEL == "esr" ? "MOZR" : "MOZI"
|
||||
}&q=foo`,
|
||||
codes: {
|
||||
context: "form=MOZCON&",
|
||||
keyword: "form=MOZLBR&",
|
||||
|
|
|
@ -29,18 +29,10 @@ async function checkHeader(engine) {
|
|||
// The header can be updated after getting the engine, so we may have to
|
||||
// wait for it.
|
||||
let header = searchPopup.searchbarEngineName;
|
||||
if (!header.getAttribute("value").includes(engine.name)) {
|
||||
await new Promise(resolve => {
|
||||
let observer = new MutationObserver(() => {
|
||||
observer.disconnect();
|
||||
resolve();
|
||||
});
|
||||
observer.observe(searchPopup.searchbarEngineName, {
|
||||
attributes: true,
|
||||
attributeFilter: ["value"],
|
||||
});
|
||||
});
|
||||
}
|
||||
await TestUtils.waitForCondition(
|
||||
() => header.getAttribute("value").includes(engine.name),
|
||||
"Should have the correct engine name displayed in the header"
|
||||
);
|
||||
Assert.ok(
|
||||
header.getAttribute("value").includes(engine.name),
|
||||
"Should have the correct engine name displayed in the header"
|
||||
|
|
|
@ -126,7 +126,7 @@ add_task(async function open_empty() {
|
|||
let image = searchPopup.querySelector(".searchbar-engine-image");
|
||||
Assert.equal(
|
||||
image.src,
|
||||
engine.getIconURL(16),
|
||||
await engine.getIconURL(16),
|
||||
"Should have the correct icon"
|
||||
);
|
||||
|
||||
|
|
5
browser/components/storybook/.babelrc.json
Normal file
5
browser/components/storybook/.babelrc.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"sourceType": "unambiguous",
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": []
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React from "react";
|
||||
import { useParameter } from "@storybook/api";
|
||||
import { useParameter } from "@storybook/manager-api";
|
||||
import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Badge,
|
||||
|
@ -85,7 +85,7 @@ export const StatusIndicator = () => {
|
|||
style={{
|
||||
display: "flex",
|
||||
}}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onVisibleChange={onVisibilityChange}
|
||||
tooltip={() => (
|
||||
<div id="statusMessage">
|
||||
<TooltipMessage
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
/** This file handles registering the Storybook addon */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React from "react";
|
||||
import { addons, types } from "@storybook/addons";
|
||||
import { addons, types } from "@storybook/manager-api";
|
||||
import { ADDON_ID, TOOL_ID } from "../constants.mjs";
|
||||
import { StatusIndicator } from "../StatusIndicator.mjs";
|
||||
import { StatusIndicator } from "../StatusIndicator.jsx";
|
||||
|
||||
addons.register(ADDON_ID, () => {
|
||||
addons.add(TOOL_ID, {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { addons, useGlobals, useStorybookApi } from "@storybook/manager-api";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { AddonPanel, Table, Form } from "@storybook/components";
|
||||
import { FLUENT_CHANGED, FLUENT_SET_STRINGS } from "./constants.mjs";
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import "./fluent-panel.css";
|
||||
|
||||
export const FluentPanel = ({ active }) => {
|
||||
const [fileName, setFileName] = useState(null);
|
||||
const [strings, setStrings] = useState([]);
|
||||
const [{ fluentStrings }, updateGlobals] = useGlobals();
|
||||
const channel = addons.getChannel();
|
||||
const api = useStorybookApi();
|
||||
|
||||
useEffect(() => {
|
||||
channel.on(FLUENT_CHANGED, handleFluentChanged);
|
||||
return () => {
|
||||
channel.off(FLUENT_CHANGED, handleFluentChanged);
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
const handleFluentChanged = (nextStrings, fluentFile) => {
|
||||
setFileName(fluentFile);
|
||||
setStrings(nextStrings);
|
||||
};
|
||||
|
||||
const onInput = e => {
|
||||
let nextStrings = [];
|
||||
for (let [key, value] of strings) {
|
||||
if (key == e.target.name) {
|
||||
let stringValue = e.target.value;
|
||||
if (stringValue.startsWith(".")) {
|
||||
stringValue = "\n" + stringValue;
|
||||
}
|
||||
nextStrings.push([key, stringValue]);
|
||||
} else {
|
||||
nextStrings.push([key, value]);
|
||||
}
|
||||
}
|
||||
let stringified = nextStrings
|
||||
.map(([key, value]) => `${key} = ${value}`)
|
||||
.join("\n");
|
||||
channel.emit(FLUENT_SET_STRINGS, stringified);
|
||||
updateGlobals({
|
||||
fluentStrings: { ...fluentStrings, [fileName]: nextStrings },
|
||||
});
|
||||
return { fileName, strings };
|
||||
};
|
||||
|
||||
const addonTemplate = () => {
|
||||
if (strings.length === 0) {
|
||||
return (
|
||||
<AddonPanel active={!!active} api={api}>
|
||||
<div className="addon-panel-body">
|
||||
<div className="addon-panel-message">
|
||||
This story is not configured to use Fluent.
|
||||
</div>
|
||||
</div>
|
||||
</AddonPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddonPanel active={!!active} api={api}>
|
||||
<div className="addon-panel-body">
|
||||
<Table aria-hidden="false" className="addon-panel-table">
|
||||
<thead className="addon-panel-table-head">
|
||||
<tr>
|
||||
<th>
|
||||
<span>Identifier</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>String</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="addon-panel-table-body">
|
||||
{strings.map(([identifier, value]) => (
|
||||
<tr key={identifier}>
|
||||
<td>
|
||||
<span>{identifier}</span>
|
||||
</td>
|
||||
<td>
|
||||
<Form.Textarea
|
||||
name={identifier}
|
||||
onInput={onInput}
|
||||
defaultValue={value
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.join("\n")}
|
||||
></Form.Textarea>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</AddonPanel>
|
||||
);
|
||||
};
|
||||
|
||||
return addonTemplate();
|
||||
};
|
|
@ -1,121 +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/. */
|
||||
|
||||
import React from "react";
|
||||
import { addons } from "@storybook/addons";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { AddonPanel } from "@storybook/components";
|
||||
import { FLUENT_CHANGED, FLUENT_SET_STRINGS } from "./constants.mjs";
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import "./fluent-panel.css";
|
||||
|
||||
export class FluentPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.channel = addons.getChannel();
|
||||
this.state = {
|
||||
name: null,
|
||||
strings: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { api } = this.props;
|
||||
api.on(FLUENT_CHANGED, this.handleFluentChanged);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { api } = this.props;
|
||||
api.off(FLUENT_CHANGED, this.handleFluentChanged);
|
||||
}
|
||||
|
||||
handleFluentChanged = strings => {
|
||||
let storyData = this.props.api.getCurrentStoryData();
|
||||
let fileName = `${storyData.component}.ftl`;
|
||||
this.setState(state => ({ ...state, strings, fileName }));
|
||||
};
|
||||
|
||||
onInput = e => {
|
||||
this.setState(state => {
|
||||
let strings = [];
|
||||
for (let [key, value] of state.strings) {
|
||||
if (key == e.target.name) {
|
||||
let stringValue = e.target.value;
|
||||
if (stringValue.startsWith(".")) {
|
||||
stringValue = "\n" + stringValue;
|
||||
}
|
||||
strings.push([key, stringValue]);
|
||||
} else {
|
||||
strings.push([key, value]);
|
||||
}
|
||||
}
|
||||
let stringified = strings
|
||||
.map(([key, value]) => `${key} = ${value}`)
|
||||
.join("\n");
|
||||
this.channel.emit(FLUENT_SET_STRINGS, stringified);
|
||||
const { fluentStrings } = this.props.api.getGlobals();
|
||||
this.props.api.updateGlobals({
|
||||
fluentStrings: { ...fluentStrings, [state.fileName]: strings },
|
||||
});
|
||||
return { ...state, strings };
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { api, active } = this.props;
|
||||
const { strings } = this.state;
|
||||
if (strings.length === 0) {
|
||||
return (
|
||||
<AddonPanel active={!!active} api={api}>
|
||||
<div className="addon-panel-body">
|
||||
<div className="addon-panel-message">
|
||||
This story is not configured to use Fluent.
|
||||
</div>
|
||||
</div>
|
||||
</AddonPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AddonPanel active={!!active} api={api}>
|
||||
<div className="addon-panel-body">
|
||||
<table aria-hidden="false" className="addon-panel-table">
|
||||
<thead className="addon-panel-table-head">
|
||||
<tr>
|
||||
<th>
|
||||
<span>Identifier</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>String</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="addon-panel-table-body">
|
||||
{strings.map(([identifier, value]) => (
|
||||
<tr key={identifier}>
|
||||
<td>
|
||||
<span>{identifier}</span>
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<textarea
|
||||
name={identifier}
|
||||
onInput={this.onInput}
|
||||
defaultValue={value
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.join("\n")}
|
||||
></textarea>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</AddonPanel>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React from "react";
|
||||
import { useGlobals } from "@storybook/api";
|
||||
import { useGlobals } from "@storybook/manager-api";
|
||||
import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
Icons,
|
|
@ -15,69 +15,59 @@
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
.addon-panel-table {
|
||||
border-collapse: collapse;
|
||||
table.addon-panel-table {
|
||||
border-spacing: 0;
|
||||
color: #333333;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.addon-panel-table-head {
|
||||
color: rgba(51,51,51,0.75);
|
||||
}
|
||||
|
||||
.addon-panel-table-head th {
|
||||
padding: 10px 15px;
|
||||
table.addon-panel-table thead.addon-panel-table-head th {
|
||||
border: none;
|
||||
vertical-align: top;
|
||||
color: rgba(46, 52, 56, 0.75);
|
||||
}
|
||||
|
||||
.addon-panel-table-head th:first-of-type, .addon-panel-table-body td:first-of-type {
|
||||
@media (prefers-color-scheme: dark) {
|
||||
table.addon-panel-table thead.addon-panel-table-head th {
|
||||
color: rgba(201, 205, 207, 0.55)
|
||||
}
|
||||
}
|
||||
|
||||
table.addon-panel-table thead.addon-panel-table-head tr {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.addon-panel-table-head th:first-of-type,
|
||||
.addon-panel-table-body td:first-of-type {
|
||||
width: 25%;
|
||||
padding-left: 20px;
|
||||
border-inline: none;
|
||||
}
|
||||
|
||||
.addon-panel-table-head th:last-of-type, .addon-panel-table-body td:last-of-type {
|
||||
.addon-panel-table-head th:last-of-type,
|
||||
.addon-panel-table-body td:last-of-type {
|
||||
padding-right: 20px;
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
.addon-panel-table-body {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.addon-panel-table-body tr {
|
||||
.addon-panel-body {
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.addon-panel-table-body td {
|
||||
padding: 10px 15px;
|
||||
font-weight: bold;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.addon-panel-table-body label {
|
||||
display: flex;
|
||||
table.addon-panel-table .addon-panel-table-body tr:nth-of-type(2n) {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.addon-panel-table-body tr:last-of-type td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.addon-panel-table-body textarea {
|
||||
height: fit-content;
|
||||
appearance: none;
|
||||
border: none;
|
||||
box-sizing: inherit;
|
||||
display: block;
|
||||
margin: 0;
|
||||
background-color: rgb(255, 255, 255);
|
||||
padding: 6px 10px;
|
||||
color: #333333;
|
||||
box-shadow: rgba(0,0,0,.1) 0 0 0 1px inset;
|
||||
border-radius: 4px;
|
||||
line-height: 20px;
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
overflow: visible;
|
||||
max-height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
@ -4,13 +4,10 @@
|
|||
|
||||
/** This file handles registering the Storybook addon */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import React from "react";
|
||||
import { addons, types } from "@storybook/addons";
|
||||
import { addons, types } from "@storybook/manager-api";
|
||||
import { ADDON_ID, PANEL_ID, TOOL_ID } from "../constants.mjs";
|
||||
import { PseudoLocalizationButton } from "../PseudoLocalizationButton.mjs";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FluentPanel } from "../FluentPanel.mjs";
|
||||
import { PseudoLocalizationButton } from "../PseudoLocalizationButton.jsx";
|
||||
import { FluentPanel } from "../FluentPanel.jsx";
|
||||
|
||||
// Register the addon.
|
||||
addons.register(ADDON_ID, api => {
|
||||
|
@ -27,8 +24,6 @@ addons.register(ADDON_ID, api => {
|
|||
title: "Fluent",
|
||||
//👇 Sets the type of UI element in Storybook
|
||||
type: types.PANEL,
|
||||
render: ({ active, key }) => (
|
||||
<FluentPanel active={active} api={api} key={key}></FluentPanel>
|
||||
),
|
||||
render: ({ active }) => FluentPanel({ active }),
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { useEffect, useGlobals, addons } from "@storybook/addons";
|
||||
import { useEffect, useGlobals, useChannel } from "@storybook/preview-api";
|
||||
import {
|
||||
DIRECTIONS,
|
||||
DIRECTION_BY_STRATEGY,
|
||||
|
@ -25,11 +25,11 @@ export const withPseudoLocalization = (StoryFn, context) => {
|
|||
const [{ pseudoStrategy }] = useGlobals();
|
||||
const direction = DIRECTION_BY_STRATEGY[pseudoStrategy] || DIRECTIONS.ltr;
|
||||
const isInDocs = context.viewMode === "docs";
|
||||
const channel = addons.getChannel();
|
||||
const emit = useChannel({});
|
||||
|
||||
useEffect(() => {
|
||||
if (pseudoStrategy) {
|
||||
channel.emit(UPDATE_STRATEGY_EVENT, pseudoStrategy);
|
||||
emit(UPDATE_STRATEGY_EVENT, pseudoStrategy);
|
||||
}
|
||||
}, [pseudoStrategy]);
|
||||
|
||||
|
@ -56,8 +56,7 @@ export const withPseudoLocalization = (StoryFn, context) => {
|
|||
*/
|
||||
export const withFluentStrings = (StoryFn, context) => {
|
||||
const [{ fluentStrings }, updateGlobals] = useGlobals();
|
||||
const channel = addons.getChannel();
|
||||
|
||||
const emit = useChannel({});
|
||||
const fileName = context.component + ".ftl";
|
||||
let strings = [];
|
||||
|
||||
|
@ -83,7 +82,7 @@ export const withFluentStrings = (StoryFn, context) => {
|
|||
}
|
||||
}
|
||||
|
||||
channel.emit(FLUENT_CHANGED, strings);
|
||||
emit(FLUENT_CHANGED, strings, fileName);
|
||||
|
||||
return StoryFn();
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
const path = require("path");
|
||||
const webpack = require("webpack");
|
||||
const rewriteChromeUri = require("./chrome-uri-utils.js");
|
||||
const mdIndexer = require("./markdown-story-indexer.js");
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "../../../../");
|
||||
|
||||
|
@ -37,34 +38,35 @@ module.exports = {
|
|||
path.resolve(__dirname, "addon-fluent"),
|
||||
path.resolve(__dirname, "addon-component-status"),
|
||||
],
|
||||
framework: "@storybook/web-components",
|
||||
framework: {
|
||||
name: "@storybook/web-components-webpack5",
|
||||
options: {},
|
||||
},
|
||||
|
||||
experimental_indexers: async existingIndexers => {
|
||||
const customIndexer = {
|
||||
test: /(stories|story)\.md$/,
|
||||
createIndex: mdIndexer,
|
||||
};
|
||||
return [...existingIndexers, customIndexer];
|
||||
},
|
||||
webpackFinal: async (config, { configType }) => {
|
||||
// `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
|
||||
// You can change the configuration based on that.
|
||||
// 'PRODUCTION' is used when building the static version of storybook.
|
||||
|
||||
// Make whatever fine-grained changes you need
|
||||
config.resolve.alias.browser = `${projectRoot}/browser`;
|
||||
config.resolve.alias.toolkit = `${projectRoot}/toolkit`;
|
||||
config.resolve.alias[
|
||||
"toolkit-widgets"
|
||||
] = `${projectRoot}/toolkit/content/widgets/`;
|
||||
config.resolve.alias[
|
||||
"lit.all.mjs"
|
||||
] = `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`;
|
||||
// @mdx-js/react@1.x.x versions don't get hoisted to the root node_modules
|
||||
// folder due to the versions of React it accepts as a peer dependency. That
|
||||
// means we have to go one level deeper and look in the node_modules of
|
||||
// @storybook/addon-docs, which depends on @mdx-js/react.
|
||||
config.resolve.alias["@storybook/addon-docs"] =
|
||||
"browser/components/storybook/node_modules/@storybook/addon-docs";
|
||||
config.resolve.alias["@mdx-js/react"] =
|
||||
"@storybook/addon-docs/node_modules/@mdx-js/react";
|
||||
|
||||
// The @storybook/web-components project uses lit-html. Redirect it to our
|
||||
// bundled version.
|
||||
config.resolve.alias["lit-html/directive-helpers.js"] = "lit.all.mjs";
|
||||
config.resolve.alias["lit-html"] = "lit.all.mjs";
|
||||
config.resolve.alias = {
|
||||
browser: `${projectRoot}/browser`,
|
||||
toolkit: `${projectRoot}/toolkit`,
|
||||
"toolkit-widgets": `${projectRoot}/toolkit/content/widgets/`,
|
||||
"lit.all.mjs": `${projectRoot}/toolkit/content/widgets/vendor/lit.all.mjs`,
|
||||
react: "browser/components/storybook/node_modules/react",
|
||||
"react/jsx-runtime":
|
||||
"browser/components/storybook/node_modules/react/jsx-runtime",
|
||||
"@storybook/addon-docs":
|
||||
"browser/components/storybook/node_modules/@storybook/addon-docs",
|
||||
};
|
||||
|
||||
config.plugins.push(
|
||||
// Rewrite chrome:// URI imports to file system paths.
|
||||
|
@ -147,7 +149,4 @@ module.exports = {
|
|||
// Return the altered config
|
||||
return config;
|
||||
},
|
||||
core: {
|
||||
builder: "webpack5",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/* 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/. */
|
||||
/* eslint-env node */
|
||||
|
||||
const { loadCsf } = require("@storybook/csf-tools");
|
||||
const { compile } = require("@storybook/mdx2-csf");
|
||||
const { getStoryTitle, getMDXSource } = require("./markdown-story-utils.js");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* Function that tells Storybook how to index markdown based stories. This is
|
||||
* responsible for Storybook knowing how to populate the sidebar in the
|
||||
* Storybook UI, then retrieve the relevant file when a story is selected. In
|
||||
* order to get the data Storybook needs, we have to convert the markdown to
|
||||
* MDX, the convert that to CSF.
|
||||
* More info on indexers can be found here: storybook.js.org/docs/api/main-config-indexers
|
||||
* @param {string} fileName - Path to the file being processed.
|
||||
* @param {Object} opts - Options to configure the indexer.
|
||||
* @returns Array of IndexInput objects.
|
||||
*/
|
||||
module.exports = async (fileName, opts) => {
|
||||
// eslint-disable-next-line no-unsanitized/method
|
||||
const content = fs.readFileSync(fileName, "utf8");
|
||||
const title = getStoryTitle(fileName);
|
||||
const code = getMDXSource(content, title);
|
||||
|
||||
// Compile MDX into CSF
|
||||
const csfCode = await compile(code, opts);
|
||||
|
||||
// Parse CSF component
|
||||
let csf = loadCsf(csfCode, { fileName, makeTitle: () => title }).parse();
|
||||
|
||||
// Return an array of story indexes.
|
||||
// Cribbed from https://github.com/storybookjs/storybook/blob/4169cd5b4ec9111de69f64a5e06edab9a6d2b0b8/code/addons/docs/src/preset.ts#L189
|
||||
const { indexInputs, stories } = csf;
|
||||
return indexInputs.map((input, index) => {
|
||||
const docsOnly = stories[index].parameters?.docsOnly;
|
||||
const tags = input.tags ? input.tags : [];
|
||||
if (docsOnly) {
|
||||
tags.push("stories-mdx-docsOnly");
|
||||
}
|
||||
// the mdx-csf compiler automatically adds the 'stories-mdx' tag to meta,
|
||||
// here' we're just making sure it is always there
|
||||
if (!tags.includes("stories-mdx")) {
|
||||
tags.push("stories-mdx");
|
||||
}
|
||||
return { ...input, tags };
|
||||
});
|
||||
};
|
|
@ -15,71 +15,7 @@
|
|||
* Storybook usually uses to transform MDX files.
|
||||
*/
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "../../../../");
|
||||
|
||||
/**
|
||||
* Takes a file path and returns a string to use as the story title, capitalized
|
||||
* and split into multiple words. The file name gets transformed into the story
|
||||
* name, which will be visible in the Storybook sidebar. For example, either:
|
||||
*
|
||||
* /stories/hello-world.stories.md or /stories/helloWorld.md
|
||||
*
|
||||
* will result in a story named "Hello World".
|
||||
*
|
||||
* @param {string} filePath - path of the file being processed.
|
||||
* @returns {string} The title of the story.
|
||||
*/
|
||||
function getStoryTitle(filePath) {
|
||||
let fileName = path.basename(filePath, ".stories.md");
|
||||
if (fileName != "README") {
|
||||
try {
|
||||
let relatedFilePath = path.resolve(
|
||||
"../../../",
|
||||
filePath.replace(".md", ".mjs")
|
||||
);
|
||||
let relatedFile = fs.readFileSync(relatedFilePath).toString();
|
||||
let relatedTitle = relatedFile.match(/title: "(.*)"/)[1];
|
||||
if (relatedTitle) {
|
||||
return relatedTitle + "/README";
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return separateWords(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into multiple capitalized words e.g. hello-world, helloWorld,
|
||||
* and hello.world all become "Hello World."
|
||||
* @param {string} str - String in any case.
|
||||
* @returns {string} The string split into multiple words.
|
||||
*/
|
||||
function separateWords(str) {
|
||||
return (
|
||||
str
|
||||
.match(/[A-Z]?[a-z0-9]+/g)
|
||||
?.map(text => text[0].toUpperCase() + text.substring(1))
|
||||
.join(" ") || str
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables rendering code in our markdown docs by parsing the source for
|
||||
* annotated code blocks and replacing them with Storybook's Canvas component.
|
||||
* @param {string} source - Stringified markdown source code.
|
||||
* @returns {string} Source with code blocks replaced by Canvas components.
|
||||
*/
|
||||
function parseStoriesFromMarkdown(source) {
|
||||
let storiesRegex = /```(?:js|html) story\n(?<code>[\s\S]*?)```/g;
|
||||
// $code comes from the <code> capture group in the regex above. It consists
|
||||
// of any code in between backticks and gets run when used in a Canvas component.
|
||||
return source.replace(
|
||||
storiesRegex,
|
||||
"<Canvas withSource='none'><with-common-styles>\n$<code></with-common-styles></Canvas>"
|
||||
);
|
||||
}
|
||||
const { getStoryTitle, getMDXSource } = require("./markdown-story-utils.js");
|
||||
|
||||
/**
|
||||
* The WebpackLoader export. Takes markdown as its source and returns a docs
|
||||
|
@ -89,61 +25,8 @@ function parseStoriesFromMarkdown(source) {
|
|||
* @param {string} source - The markdown source to rewrite to MDX.
|
||||
*/
|
||||
module.exports = function markdownStoryLoader(source) {
|
||||
// Currently we sort docs only stories under "Docs" by default.
|
||||
let storyPath = "Docs";
|
||||
|
||||
// `this.resourcePath` is the path of the file being processed.
|
||||
let relativePath = path
|
||||
.relative(projectRoot, this.resourcePath)
|
||||
.replaceAll(path.sep, "/");
|
||||
let componentName;
|
||||
|
||||
if (relativePath.includes("toolkit/content/widgets")) {
|
||||
let storyNameRegex = /(?<=\/widgets\/)(?<name>.*?)(?=\/)/g;
|
||||
componentName = storyNameRegex.exec(relativePath)?.groups?.name;
|
||||
if (componentName) {
|
||||
// Get the common name for a component e.g. Toggle for moz-toggle
|
||||
storyPath =
|
||||
"UI Widgets/" + separateWords(componentName).replace(/^Moz/g, "");
|
||||
}
|
||||
}
|
||||
|
||||
let storyTitle = getStoryTitle(relativePath);
|
||||
let title = storyTitle.includes("/")
|
||||
? storyTitle
|
||||
: `${storyPath}/${storyTitle}`;
|
||||
|
||||
let componentStories;
|
||||
if (componentName) {
|
||||
componentStories = this.resourcePath
|
||||
.replace("README", componentName)
|
||||
.replace(".md", ".mjs");
|
||||
try {
|
||||
fs.statSync(componentStories);
|
||||
componentStories = "./" + path.basename(componentStories);
|
||||
} catch {
|
||||
componentStories = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately the indentation/spacing here seems to be important for the
|
||||
// MDX parser to know what to do in the next step of the Webpack process.
|
||||
let mdxSource = `
|
||||
import { Meta, Description, Canvas, Story } from "@storybook/addon-docs";
|
||||
${componentStories ? `import * as Stories from "${componentStories}";` : ""}
|
||||
|
||||
<Meta
|
||||
title="${title}"
|
||||
${componentStories ? `of={Stories}` : ""}
|
||||
parameters={{
|
||||
previewTabs: {
|
||||
canvas: { hidden: true },
|
||||
},
|
||||
viewMode: "docs",
|
||||
}}
|
||||
/>
|
||||
|
||||
${parseStoriesFromMarkdown(source)}`;
|
||||
|
||||
let title = getStoryTitle(this.resourcePath);
|
||||
let mdxSource = getMDXSource(source, title, this.resourcePath);
|
||||
return mdxSource;
|
||||
};
|
||||
|
|
197
browser/components/storybook/.storybook/markdown-story-utils.js
Normal file
197
browser/components/storybook/.storybook/markdown-story-utils.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
/* 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/. */
|
||||
/* eslint-env node */
|
||||
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const projectRoot = path.resolve(__dirname, "../../../../");
|
||||
|
||||
/**
|
||||
* Takes a file path and returns a string to use as the story title, capitalized
|
||||
* and split into multiple words. The file name gets transformed into the story
|
||||
* name, which will be visible in the Storybook sidebar. For example, either:
|
||||
*
|
||||
* /stories/hello-world.stories.md or /stories/helloWorld.md
|
||||
*
|
||||
* will result in a story named "Hello World".
|
||||
*
|
||||
* @param {string} filePath - path of the file being processed.
|
||||
* @returns {string} The title of the story.
|
||||
*/
|
||||
function getTitleFromPath(filePath) {
|
||||
let fileName = path.basename(filePath, ".stories.md");
|
||||
if (fileName != "README") {
|
||||
try {
|
||||
let relatedFilePath = path.resolve(
|
||||
"../../../",
|
||||
filePath.replace(".md", ".mjs")
|
||||
);
|
||||
let relatedFile = fs.readFileSync(relatedFilePath).toString();
|
||||
let relatedTitle = relatedFile.match(/title: "(.*)"/)[1];
|
||||
if (relatedTitle) {
|
||||
return relatedTitle + "/README";
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return separateWords(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into multiple capitalized words e.g. hello-world, helloWorld,
|
||||
* and hello.world all become "Hello World."
|
||||
* @param {string} str - String in any case.
|
||||
* @returns {string} The string split into multiple words.
|
||||
*/
|
||||
function separateWords(str) {
|
||||
return (
|
||||
str
|
||||
.match(/[A-Z]?[a-z0-9]+/g)
|
||||
?.map(text => text[0].toUpperCase() + text.substring(1))
|
||||
.join(" ") || str
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables rendering code in our markdown docs by parsing the source for
|
||||
* annotated code blocks and replacing them with Storybook's Canvas component.
|
||||
* @param {string} source - Stringified markdown source code.
|
||||
* @returns {string} Source with code blocks replaced by Canvas components.
|
||||
*/
|
||||
function parseStoriesFromMarkdown(source) {
|
||||
let storiesRegex = /```(?:js|html) story\n(?<code>[\s\S]*?)```/g;
|
||||
// $code comes from the <code> capture group in the regex above. It consists
|
||||
// of any code in between backticks and gets run when used in a Canvas component.
|
||||
return source.replace(
|
||||
storiesRegex,
|
||||
"<Canvas withSource='none'><with-common-styles>\n$<code></with-common-styles></Canvas>"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the name of the component for files in toolkit widgets.
|
||||
* @param {string} resourcePath - Path to the file being processed.
|
||||
* @returns The component name e.g. "moz-toggle"
|
||||
*/
|
||||
function getComponentName(resourcePath) {
|
||||
let componentName = "";
|
||||
if (resourcePath.includes("toolkit/content/widgets")) {
|
||||
let storyNameRegex = /(?<=\/widgets\/)(?<name>.*?)(?=\/)/g;
|
||||
componentName = storyNameRegex.exec(resourcePath)?.groups?.name;
|
||||
}
|
||||
return componentName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out where a markdown based story should live in Storybook i.e.
|
||||
* whether it belongs under "Docs" or "UI Widgets" as well as what name to
|
||||
* display in the sidebar.
|
||||
* @param {string} resourcePath - Path to the file being processed.
|
||||
* @returns {string} The title of the story.
|
||||
*/
|
||||
function getStoryTitle(resourcePath) {
|
||||
// Currently we sort docs only stories under "Docs" by default.
|
||||
let storyPath = "Docs";
|
||||
|
||||
let relativePath = path
|
||||
.relative(projectRoot, resourcePath)
|
||||
.replaceAll(path.sep, "/");
|
||||
|
||||
let componentName = getComponentName(relativePath);
|
||||
if (componentName) {
|
||||
// Get the common name for a component e.g. Toggle for moz-toggle
|
||||
storyPath =
|
||||
"UI Widgets/" + separateWords(componentName).replace(/^Moz/g, "");
|
||||
}
|
||||
|
||||
let storyTitle = getTitleFromPath(relativePath);
|
||||
let title = storyTitle.includes("/")
|
||||
? storyTitle
|
||||
: `${storyPath}/${storyTitle}`;
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Figures out the path to import a component for cases where we have
|
||||
* interactive examples in the docs that require the component to have been
|
||||
* loaded. This wasn't necessary prior to Storybook V7 since everything was
|
||||
* loaded up front; now stories are loaded on demand.
|
||||
* @param {string} resourcePath - Path to the file being processed.
|
||||
* @returns Path used to import a component into a story.
|
||||
*/
|
||||
function getImportPath(resourcePath) {
|
||||
// Limiting this to toolkit widgets for now since we don't have any
|
||||
// interactive examples in other docs stories.
|
||||
if (!resourcePath.includes("toolkit/content/widgets")) {
|
||||
return "";
|
||||
}
|
||||
let componentName = getComponentName(resourcePath);
|
||||
let fileExtension = "";
|
||||
if (componentName) {
|
||||
let mjsPath = resourcePath.replace(
|
||||
"README.stories.md",
|
||||
`${componentName}.mjs`
|
||||
);
|
||||
let jsPath = resourcePath.replace(
|
||||
"README.stories.md",
|
||||
`${componentName}.js`
|
||||
);
|
||||
|
||||
if (fs.existsSync(mjsPath)) {
|
||||
fileExtension = "mjs";
|
||||
} else if (fs.existsSync(jsPath)) {
|
||||
fileExtension = "js";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return `"toolkit-widgets/${componentName}/${componentName}.${fileExtension}"`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes markdown and re-writes it to MDX. Conditionally includes a table of
|
||||
* arguments when we're documenting a component.
|
||||
* @param {string} source - The markdown source to rewrite to MDX.
|
||||
* @param {string} title - The title of the story.
|
||||
* @param {string} resourcePath - Path to the file being processed.
|
||||
* @returns The markdown source converted to MDX.
|
||||
*/
|
||||
function getMDXSource(source, title, resourcePath = "") {
|
||||
let importPath = getImportPath(resourcePath);
|
||||
let componentName = getComponentName(resourcePath);
|
||||
|
||||
// Unfortunately the indentation/spacing here seems to be important for the
|
||||
// MDX parser to know what to do in the next step of the Webpack process.
|
||||
let mdxSource = `
|
||||
import { Meta, Canvas, ArgTypes } from "@storybook/addon-docs";
|
||||
${importPath ? `import ${importPath};` : ""}
|
||||
|
||||
<Meta
|
||||
title="${title}"
|
||||
parameters={{
|
||||
previewTabs: {
|
||||
canvas: { hidden: true },
|
||||
},
|
||||
viewMode: "docs",
|
||||
}}
|
||||
/>
|
||||
|
||||
${parseStoriesFromMarkdown(source)}
|
||||
|
||||
${
|
||||
importPath &&
|
||||
`
|
||||
## Args Table
|
||||
|
||||
<ArgTypes of={"${componentName}"} />
|
||||
`
|
||||
}`;
|
||||
|
||||
return mdxSource;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMDXSource,
|
||||
getStoryTitle,
|
||||
};
|
36325
browser/components/storybook/package-lock.json
generated
36325
browser/components/storybook/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -6,23 +6,27 @@
|
|||
"scripts": {
|
||||
"analyze": "cem analyze",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build-storybook": "npm run analyze && build-storybook",
|
||||
"storybook": "npm run analyze && start-storybook -p 5703 --no-open"
|
||||
"build-storybook": "npm run analyze && storybook build",
|
||||
"storybook": "npm run analyze && storybook dev -p 5703 --no-open"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@custom-elements-manifest/analyzer": "^0.6.6",
|
||||
"@fluent/bundle": "^0.17.1",
|
||||
"@fluent/dom": "^0.8.1",
|
||||
"@storybook/addon-a11y": "^6.5.15",
|
||||
"@storybook/addon-actions": "^6.4.8",
|
||||
"@storybook/addon-essentials": "^6.4.8",
|
||||
"@storybook/addon-links": "^6.4.8",
|
||||
"@storybook/builder-webpack5": "^6.4.8",
|
||||
"@storybook/manager-webpack5": "^6.4.8",
|
||||
"@storybook/web-components": "^6.4.8",
|
||||
"babel-loader": "^8.2.3"
|
||||
"@storybook/addon-a11y": "^7.6.4",
|
||||
"@storybook/addon-actions": "^7.6.4",
|
||||
"@storybook/addon-essentials": "^7.6.4",
|
||||
"@storybook/addon-links": "^7.6.4",
|
||||
"@storybook/web-components": "^7.6.4",
|
||||
"@storybook/web-components-webpack5": "^7.6.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"storybook": "^7.6.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -874,6 +874,8 @@ class TelemetryEvent {
|
|||
startEventInfo.interactionType == "dropped" ? "drop_go" : "paste_go";
|
||||
} else if (event.type == "blur") {
|
||||
action = "blur";
|
||||
} else if (event.type == "tabswitch") {
|
||||
action = "tab_switch";
|
||||
} else if (
|
||||
details.element?.dataset.command &&
|
||||
// The "help" selType is recognized by legacy telemetry, and `action`
|
||||
|
@ -892,7 +894,8 @@ class TelemetryEvent {
|
|||
action = "enter";
|
||||
}
|
||||
|
||||
let method = action == "blur" ? "abandonment" : "engagement";
|
||||
let method =
|
||||
action == "blur" || action == "tab_switch" ? "abandonment" : "engagement";
|
||||
|
||||
if (method == "engagement") {
|
||||
// Not all engagements end the search session. The session remains ongoing
|
||||
|
@ -1060,6 +1063,7 @@ class TelemetryEvent {
|
|||
};
|
||||
} else if (method === "abandonment") {
|
||||
eventInfo = {
|
||||
abandonment_type: action,
|
||||
sap,
|
||||
interaction,
|
||||
search_mode,
|
||||
|
|
|
@ -2204,11 +2204,24 @@ export class UrlbarInput {
|
|||
this.formatValue();
|
||||
this._resetSearchState();
|
||||
|
||||
// Switching tabs doesn't always change urlbar focus, so we must try to
|
||||
// reopen here too, not just on focus.
|
||||
// We don't use the original TabSelect event because caching it causes
|
||||
// leaks on MacOS.
|
||||
if (this.view.autoOpen({ event: new CustomEvent("tabswitch") })) {
|
||||
const event = new CustomEvent("tabswitch");
|
||||
// If the urlbar is focused after a tab switch, record a potential
|
||||
// engagement event. When switching from a focused to a non-focused urlbar,
|
||||
// the blur event would record the abandonment. When switching from an
|
||||
// unfocused to a focused urlbar, there should be no search session ongoing,
|
||||
// so this will be a no-op.
|
||||
if (this.focused) {
|
||||
this.controller.engagementEvent.record(event, {
|
||||
searchString: this._lastSearchString,
|
||||
searchSource: this.getSearchSource(event),
|
||||
});
|
||||
}
|
||||
|
||||
// Switching tabs doesn't always change urlbar focus, so we must try to
|
||||
// reopen here too, not just on focus.
|
||||
if (this.view.autoOpen({ event })) {
|
||||
return;
|
||||
}
|
||||
// The input may retain focus when switching tabs in which case we
|
||||
|
|
|
@ -72,6 +72,7 @@ class ProviderAliasEngines extends UrlbarProvider {
|
|||
alias,
|
||||
queryContext.searchString
|
||||
);
|
||||
let icon = await engine?.getIconURL();
|
||||
if (!engine || instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
@ -83,7 +84,7 @@ class ProviderAliasEngines extends UrlbarProvider {
|
|||
engine: engine.name,
|
||||
keyword: alias,
|
||||
query: query.trimStart(),
|
||||
icon: engine.getIconURL(),
|
||||
icon,
|
||||
})
|
||||
);
|
||||
result.heuristic = true;
|
||||
|
|
|
@ -140,6 +140,12 @@ class ProviderContextualSearch extends UrlbarProvider {
|
|||
}
|
||||
|
||||
if (engine) {
|
||||
let instance = this.queryInstance;
|
||||
let icon = await engine.getIconURL();
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.engines.set(hostname, engine);
|
||||
// Check to see if the engine that was found is the default engine.
|
||||
// The default engine will often be used to populate the heuristic result,
|
||||
|
@ -155,7 +161,7 @@ class ProviderContextualSearch extends UrlbarProvider {
|
|||
let result = this.makeResult({
|
||||
url,
|
||||
engine: engine.name,
|
||||
icon: engine.getIconURL(),
|
||||
icon,
|
||||
input: queryContext.searchString,
|
||||
shouldNavigate: true,
|
||||
});
|
||||
|
|
|
@ -97,7 +97,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
}) ||
|
||||
lazy.UrlbarTokenizer.REGEXP_COMMON_EMAIL.test(str))
|
||||
) {
|
||||
let searchResult = this._engineSearchResult(queryContext);
|
||||
let searchResult = await this._engineSearchResult(queryContext);
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
@ -107,13 +107,16 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
return;
|
||||
}
|
||||
|
||||
result = this._searchModeKeywordResult(queryContext);
|
||||
result = await this._searchModeKeywordResult(queryContext);
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
addCallback(this, result);
|
||||
return;
|
||||
}
|
||||
|
||||
result = this._engineSearchResult(queryContext);
|
||||
result = await this._engineSearchResult(queryContext);
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
@ -228,7 +231,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
return result;
|
||||
}
|
||||
|
||||
_searchModeKeywordResult(queryContext) {
|
||||
async _searchModeKeywordResult(queryContext) {
|
||||
if (!queryContext.tokens.length) {
|
||||
return null;
|
||||
}
|
||||
|
@ -266,7 +269,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
|
||||
let result;
|
||||
if (queryContext.restrictSource == UrlbarUtils.RESULT_SOURCE.SEARCH) {
|
||||
result = this._engineSearchResult(queryContext, firstToken);
|
||||
result = await this._engineSearchResult(queryContext, firstToken);
|
||||
} else {
|
||||
result = new lazy.UrlbarResult(
|
||||
UrlbarUtils.RESULT_TYPE.SEARCH,
|
||||
|
@ -281,7 +284,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
return result;
|
||||
}
|
||||
|
||||
_engineSearchResult(queryContext, keyword = null) {
|
||||
async _engineSearchResult(queryContext, keyword = null) {
|
||||
let engine;
|
||||
if (queryContext.searchMode?.engineName) {
|
||||
engine = lazy.UrlbarSearchUtils.getEngineByName(
|
||||
|
@ -315,7 +318,7 @@ class ProviderHeuristicFallback extends UrlbarProvider {
|
|||
UrlbarUtils.RESULT_SOURCE.SEARCH,
|
||||
...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
||||
engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
icon: engine.getIconURL(),
|
||||
icon: await engine.getIconURL(),
|
||||
query: [query, UrlbarUtils.HIGHLIGHT.NONE],
|
||||
keyword: keyword ? [keyword, UrlbarUtils.HIGHLIGHT.NONE] : undefined,
|
||||
})
|
||||
|
|
|
@ -110,6 +110,7 @@ class ProviderPrivateSearch extends UrlbarProvider {
|
|||
logger: this.logger,
|
||||
}).promise;
|
||||
|
||||
let icon = await engine.getIconURL();
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
@ -120,7 +121,7 @@ class ProviderPrivateSearch extends UrlbarProvider {
|
|||
...lazy.UrlbarResult.payloadAndSimpleHighlights(queryContext.tokens, {
|
||||
engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
query: [searchString, UrlbarUtils.HIGHLIGHT.NONE],
|
||||
icon: engine.getIconURL(),
|
||||
icon,
|
||||
inPrivateWindow: true,
|
||||
isPrivateEngine,
|
||||
})
|
||||
|
|
|
@ -506,7 +506,7 @@ class ProviderSearchSuggestions extends UrlbarProvider {
|
|||
trending: entry.trending,
|
||||
description: entry.description || undefined,
|
||||
query: [searchString.trim(), UrlbarUtils.HIGHLIGHT.NONE],
|
||||
icon: !entry.value ? engine.getIconURL() : entry.icon,
|
||||
icon: !entry.value ? await engine.getIconURL() : entry.icon,
|
||||
};
|
||||
|
||||
if (entry.trending) {
|
||||
|
|
|
@ -192,6 +192,7 @@ class ProviderSearchTips extends UrlbarProvider {
|
|||
this.currentTip = TIPS.NONE;
|
||||
|
||||
let defaultEngine = await Services.search.getDefault();
|
||||
let icon = await defaultEngine.getIconURL();
|
||||
if (instance != this.queryInstance) {
|
||||
return;
|
||||
}
|
||||
|
@ -202,7 +203,7 @@ class ProviderSearchTips extends UrlbarProvider {
|
|||
{
|
||||
type: tip,
|
||||
buttons: [{ l10n: { id: "urlbar-search-tips-confirm" } }],
|
||||
icon: defaultEngine.getIconURL(),
|
||||
icon,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -98,8 +98,8 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
|
|||
|
||||
// If the user is typing a potential engine name, autofill it.
|
||||
if (lazy.UrlbarPrefs.get("autoFill") && queryContext.allowAutofill) {
|
||||
let result = this._getAutofillResult(queryContext);
|
||||
if (result) {
|
||||
let result = await this._getAutofillResult(queryContext);
|
||||
if (result && instance == this.queryInstance) {
|
||||
this._autofillData = { result, instance };
|
||||
return true;
|
||||
}
|
||||
|
@ -127,6 +127,7 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
|
|||
addCallback(this, this._autofillData.result);
|
||||
}
|
||||
|
||||
let instance = this.queryInstance;
|
||||
for (let { engine, tokenAliases } of this._engines) {
|
||||
if (
|
||||
tokenAliases[0].startsWith(queryContext.trimmedSearchString) &&
|
||||
|
@ -139,10 +140,13 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
|
|||
engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
keyword: [tokenAliases[0], UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
query: ["", UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
icon: engine.getIconURL(),
|
||||
icon: await engine.getIconURL(),
|
||||
providesSearchMode: true,
|
||||
})
|
||||
);
|
||||
if (instance != this.queryInstance) {
|
||||
break;
|
||||
}
|
||||
addCallback(this, result);
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +172,7 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
|
|||
}
|
||||
}
|
||||
|
||||
_getAutofillResult(queryContext) {
|
||||
async _getAutofillResult(queryContext) {
|
||||
let lowerCaseSearchString = queryContext.searchString.toLowerCase();
|
||||
|
||||
// The user is typing a specific engine. We should show a heuristic result.
|
||||
|
@ -203,7 +207,7 @@ class ProviderTokenAliasEngines extends UrlbarProvider {
|
|||
engine: [engine.name, UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
keyword: [aliasPreservingUserCase, UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
query: ["", UrlbarUtils.HIGHLIGHT.TYPED],
|
||||
icon: engine.getIconURL(),
|
||||
icon: await engine.getIconURL(),
|
||||
providesSearchMode: true,
|
||||
}
|
||||
)
|
||||
|
|
|
@ -495,8 +495,8 @@ export class UrlbarSearchOneOffs extends SearchOneOffs {
|
|||
* @param {Array} addEngines
|
||||
* The engines that can be added.
|
||||
*/
|
||||
_rebuildEngineList(engines, addEngines) {
|
||||
super._rebuildEngineList(engines, addEngines);
|
||||
async _rebuildEngineList(engines, addEngines) {
|
||||
await super._rebuildEngineList(engines, addEngines);
|
||||
|
||||
for (let { source, pref, restrict } of lazy.UrlbarUtils
|
||||
.LOCAL_SEARCH_MODES) {
|
||||
|
|
|
@ -3136,6 +3136,7 @@ export class UrlbarView {
|
|||
|
||||
let engine = this.oneOffSearchButtons.selectedButton?.engine;
|
||||
let source = this.oneOffSearchButtons.selectedButton?.source;
|
||||
let icon = this.oneOffSearchButtons.selectedButton?.image;
|
||||
|
||||
let localSearchMode;
|
||||
if (source) {
|
||||
|
@ -3259,7 +3260,15 @@ export class UrlbarView {
|
|||
}
|
||||
|
||||
// Update result favicons.
|
||||
let iconOverride = localSearchMode?.icon || engine?.getIconURL();
|
||||
let iconOverride = localSearchMode?.icon;
|
||||
// If the icon is the default one-off search placeholder, assume we
|
||||
// don't have an icon for the engine.
|
||||
if (
|
||||
!iconOverride &&
|
||||
icon != "chrome://browser/skin/search-engine-placeholder.png"
|
||||
) {
|
||||
iconOverride = icon;
|
||||
}
|
||||
if (!iconOverride && (localSearchMode || engine)) {
|
||||
// For one-offs without an icon, do not allow restyled URL results to
|
||||
// use their own icons.
|
||||
|
|
|
@ -15,6 +15,11 @@ urlbar:
|
|||
type: event
|
||||
description: Recorded when the user abandons a search (blurring the urlbar).
|
||||
extra_keys:
|
||||
abandonment_type:
|
||||
description: >
|
||||
Records the reason that resulted in an abandonment. The possible
|
||||
values are: `blur` and `tab_switch`.
|
||||
type: string
|
||||
sap:
|
||||
description: >
|
||||
`sap` is the meaning of `search access point`. It records where the
|
||||
|
|
|
@ -85,9 +85,6 @@ add_task(async function context_one() {
|
|||
|
||||
add_task(async function context_invalid() {
|
||||
info("Checks the context menu with a page that offers an invalid engine.");
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["prompts.contentPromptSubDialog", false]],
|
||||
});
|
||||
|
||||
let url = getRootDirectory(gTestPath) + "add_search_engine_invalid.html";
|
||||
await BrowserTestUtils.withNewTab(url, async tab => {
|
||||
|
|
|
@ -188,7 +188,7 @@ async function heuristicIsRestyled(
|
|||
if (engine) {
|
||||
Assert.equal(
|
||||
resultDetails.image,
|
||||
engine.getIconURL() || UrlbarUtils.ICON.SEARCH_GLASS,
|
||||
(await engine.getIconURL()) || UrlbarUtils.ICON.SEARCH_GLASS,
|
||||
"The restyled result's icon should be the engine's icon."
|
||||
);
|
||||
} else if (source) {
|
||||
|
|
|
@ -94,7 +94,7 @@ add_task(async function localOneOff() {
|
|||
);
|
||||
Assert.equal(
|
||||
result.image,
|
||||
oneOffButtons.selectedButton.engine.getIconURL(),
|
||||
await oneOffButtons.selectedButton.engine.getIconURL(),
|
||||
"Check the heuristic icon"
|
||||
);
|
||||
|
||||
|
|
|
@ -21,6 +21,65 @@ const CONFIG_DEFAULT = [
|
|||
},
|
||||
];
|
||||
|
||||
const CONFIG_V2 = [
|
||||
{
|
||||
recordType: "engine",
|
||||
identifier: "basic",
|
||||
base: {
|
||||
name: "basic",
|
||||
urls: {
|
||||
search: {
|
||||
base: "https://example.com",
|
||||
searchTermParamName: "q",
|
||||
},
|
||||
trending: {
|
||||
base: "https://example.com/browser/browser/components/search/test/browser/trendingSuggestionEngine.sjs",
|
||||
method: "GET",
|
||||
},
|
||||
},
|
||||
aliases: ["basic"],
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
environment: { allRegionsAndLocales: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
recordType: "engine",
|
||||
identifier: "private",
|
||||
base: {
|
||||
name: "private",
|
||||
urls: {
|
||||
search: {
|
||||
base: "https://example.com",
|
||||
searchTermParamName: "q",
|
||||
},
|
||||
suggestions: {
|
||||
base: "https://example.com",
|
||||
method: "GET",
|
||||
searchTermParamName: "search",
|
||||
},
|
||||
},
|
||||
aliases: ["private"],
|
||||
},
|
||||
variants: [
|
||||
{
|
||||
environment: { allRegionsAndLocales: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
recordType: "defaultEngines",
|
||||
globalDefault: "basic",
|
||||
specificDefaults: [],
|
||||
},
|
||||
{
|
||||
recordType: "engineOrders",
|
||||
orders: [],
|
||||
},
|
||||
];
|
||||
|
||||
add_setup(async () => {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
|
@ -39,7 +98,10 @@ add_setup(async () => {
|
|||
});
|
||||
|
||||
await UrlbarTestUtils.formHistory.clear();
|
||||
await SearchTestUtils.setupTestEngines("search-engines", CONFIG_DEFAULT);
|
||||
await SearchTestUtils.setupTestEngines(
|
||||
"search-engines",
|
||||
SearchUtils.newSearchConfigEnabled ? CONFIG_V2 : CONFIG_DEFAULT
|
||||
);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await UrlbarTestUtils.formHistory.clear();
|
||||
|
|
|
@ -26,6 +26,8 @@ prefs = ["browser.bookmarks.testing.skipDefaultBookmarksImport=true"]
|
|||
|
||||
["browser_glean_telemetry_abandonment_n_chars_n_words.js"]
|
||||
|
||||
["browser_glean_telemetry_abandonment_type.js"]
|
||||
|
||||
["browser_glean_telemetry_abandonment_sap.js"]
|
||||
|
||||
["browser_glean_telemetry_abandonment_search_engine_default_id.js"]
|
||||
|
|
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