Update On Thu Mar 14 19:42:58 CET 2024

This commit is contained in:
github-action[bot] 2024-03-14 19:42:59 +01:00
parent 8150b844db
commit c6d5964c58
745 changed files with 34957 additions and 43147 deletions

31
Cargo.lock generated
View file

@ -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"

View file

@ -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",

View file

@ -844,7 +844,7 @@ ROLE(LISTBOX,
ROLE_SYSTEM_LIST,
java::SessionAccessibility::CLASSNAME_LISTVIEW,
IsAccessibilityElementRule::IfChildlessWithNameAndFocusable,
eNoNameRule)
eNameFromValueRule)
ROLE(FLAT_EQUATION,
"flat equation",

View file

@ -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;

View file

@ -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

View file

@ -6,4 +6,6 @@ skip-if = [
]
support-files = ["head.js"]
["browser_osPicker.js"]
["browser_role.js"]

View 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");
}
);

View file

@ -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;
}

View file

@ -16,6 +16,7 @@
#if defined(MOZ_CRASHREPORTER)
/minidump-analyzer
#endif
/nmhproxy
/pingsender
/pk12util
/ssltunnel

View file

@ -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.

View 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"

View 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"]

View 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()));
}
}

View 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(())
}

View file

@ -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);

View file

@ -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
);
},

View file

@ -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",

View file

@ -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");
});

View file

@ -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();

View file

@ -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);

View file

@ -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();
});

View file

@ -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();
});
},

View file

@ -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.

View file

@ -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]],
});
});

View file

@ -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);
});

View file

@ -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");

View file

@ -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.
*/

View file

@ -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({

View file

@ -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) {

View file

@ -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;
}
}
}
}
}
}
);
});

View file

@ -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=""

View file

@ -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).

View file

@ -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

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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"

View 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 {};

View file

@ -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);
}
}
}

View 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

View file

@ -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",
]

View 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.");
}
}

View file

@ -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" }
}
}

View file

@ -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 });
});

View file

@ -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"
);
});

View 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"]

View file

@ -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"
);

View file

@ -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.

View file

@ -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) {

View file

@ -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() {

View file

@ -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"
// );

View file

@ -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)

View file

@ -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(() => {

View file

@ -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>

View file

@ -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" &&

View file

@ -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"))

View file

@ -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);

View file

@ -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);

View file

@ -232,7 +232,7 @@
}
}
let uri = engine.getIconURL();
let uri = await engine.getIconURL();
if (uri) {
this.setAttribute("src", uri);
} else {

View file

@ -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);
}

View file

@ -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");

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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(

View file

@ -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&",

View file

@ -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"

View file

@ -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"
);

View file

@ -0,0 +1,5 @@
{
"sourceType": "unambiguous",
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": []
}

View file

@ -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

View file

@ -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, {

View file

@ -0,0 +1,110 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// 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();
};

View file

@ -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>
);
}
}

View file

@ -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,

View file

@ -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%;
}

View file

@ -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 }),
});
});

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
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();
};

View file

@ -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",
},
};

View file

@ -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 };
});
};

View file

@ -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;
};

View 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,
};

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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,

View file

@ -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

View file

@ -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;

View file

@ -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,
});

View file

@ -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,
})

View file

@ -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,
})

View file

@ -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) {

View file

@ -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,
}
);

View file

@ -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,
}
)

View file

@ -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) {

View file

@ -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.

View file

@ -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

View file

@ -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 => {

View file

@ -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) {

View file

@ -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"
);

View file

@ -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();

View file

@ -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