Update On Tue Aug 6 20:49:55 CEST 2024

This commit is contained in:
github-action[bot] 2024-08-06 20:49:55 +02:00
parent d64d696764
commit 18b2188e4e
566 changed files with 17199 additions and 10292 deletions

View file

@ -958,11 +958,7 @@ pref("browser.tabs.tooltipsShowPidAndActiveness", true);
pref("browser.tabs.tooltipsShowPidAndActiveness", false);
#endif
#ifdef NIGHTLY_BUILD
pref("browser.tabs.hoverPreview.enabled", true);
#else
pref("browser.tabs.hoverPreview.enabled", false);
#endif
pref("browser.tabs.hoverPreview.showThumbnails", true);
pref("browser.tabs.firefox-view.logLevel", "Warn");

View file

@ -109,14 +109,21 @@
noautofocus="true"
hidden="true" />
<panel id="searchmode-switcher-popup" noautofocus="true" orient="vertical" type="arrow">
<panel id="searchmode-switcher-popup"
class="panel-no-padding"
orient="vertical"
type="autocomplete-richlistbox"
role="menu"
aria-labelledby="searchmode-switcher-popup-description">
<label data-l10n-id="urlbar-searchmode-popup-description" id="searchmode-switcher-popup-description" />
<toolbarseparator />
<vbox class="panel-subview-body"></vbox>
<toolbarseparator />
<toolbarbutton oncommand="window.openPreferences('paneSearch');"
class="subviewbutton subviewbutton-iconic"
id="searchmode-switcher-popup-search-settings-button">
<toolbarbutton class="subviewbutton subviewbutton-iconic panel-subview-footer-button"
data-action="openpreferences"
id="searchmode-switcher-popup-search-settings-button"
role="button"
tabindex="0">
<image id="searchmode-switcher-popup-search-settings-icon" />
<label data-l10n-id="urlbar-searchmode-popup-search-settings" />
</toolbarbutton>

View file

@ -313,15 +313,26 @@
</box>
</box>
<toolbarbutton id="urlbar-searchmode-switcher"
data-l10n-id="urlbar-searchmode-button"
data-action="openpopup"
align="center" noautofocus="true" tooltip="dynamic-shortcut-tooltip" class="chromeclass-toolbar-additional">
align="center"
role="button"
aria-haspopup="menu"
aria-expanded="false"
tooltip="dynamic-shortcut-tooltip"
class="chromeclass-toolbar-additional">
<image id="searchmode-switcher-icon" />
<label id="searchmode-switcher-title" />
<image id="searchmode-switcher-dropmarker" data-l10n-id="urlbar-searchmode-dropmarker"
<image id="searchmode-switcher-dropmarker"
data-l10n-id="urlbar-searchmode-dropmarker"
class="toolbarbutton-icon toolbarbutton-combined-buttons-dropmarker" />
<toolbarbutton id="searchmode-switcher-close" data-action="exitsearchmode" noautofocus="true" class="toolbarbutton-icon close-button" />
</toolbarbutton>
<box id="searchmode-switcher-chicklet">
<label id="searchmode-switcher-title" />
<toolbarbutton id="searchmode-switcher-close"
data-action="exitsearchmode"
role="button"
tooltip="dynamic-shortcut-tooltip"
class="toolbarbutton-icon close-button" />
</box>
<box id="urlbar-label-box" align="center">
<label id="urlbar-label-switchtab" class="urlbar-label" data-l10n-id="urlbar-switch-to-tab"/>
<label id="urlbar-label-extension" class="urlbar-label" data-l10n-id="urlbar-extension"/>

View file

@ -429,17 +429,29 @@ class TabTracker extends TabTrackerBase {
}
/**
* Sets the opener of `tab` to the ID `openerTab`. Both tabs must be in the
* same window, or this function will throw a type error.
* Sets the opener of `tab` to the ID `openerTabId`. Both tabs must be in the
* same window, or this function will throw an error. if `openerTabId` is `-1`
* the opener tab is cleared.
*
* @param {Element} tab The tab for which to set the owner.
* @param {Element} openerTab The opener of <tab>.
* @param {Element} nativeTab The tab for which to set the owner.
* @param {number} openerTabId The openerTabId of <tab>.
*/
setOpener(tab, openerTab) {
if (tab.ownerDocument !== openerTab.ownerDocument) {
throw new Error("Tab must be in the same window as its opener");
setOpener(nativeTab, openerTabId) {
let nativeOpenerTab = null;
if (openerTabId > -1) {
nativeOpenerTab = tabTracker.getTab(openerTabId);
if (nativeTab.ownerDocument !== nativeOpenerTab.ownerDocument) {
throw new ExtensionError(
"Opener tab must be in the same window as the tab being updated"
);
}
}
if (nativeTab.openerTab !== nativeOpenerTab) {
nativeTab.openerTab = nativeOpenerTab;
this.emit("tab-openerTabId", { nativeTab, openerTabId });
}
tab.openerTab = openerTab;
}
deferredForTabOpen(nativeTab) {

View file

@ -160,6 +160,7 @@ const allProperties = new Set([
"hidden",
"isArticle",
"mutedInfo",
"openerTabId",
"pinned",
"sharingState",
"status",
@ -503,6 +504,11 @@ this.tabs = class extends ExtensionAPIPersistent {
}
};
let openerTabIdChangeListener = (_, { nativeTab, openerTabId }) => {
let tab = tabManager.getWrapper(nativeTab);
fireForTab(tab, { openerTabId }, nativeTab);
};
let listeners = new Map();
if (filter.properties.has("status") || filter.properties.has("url")) {
listeners.set("status", statusListener);
@ -531,6 +537,10 @@ this.tabs = class extends ExtensionAPIPersistent {
tabTracker.on("tab-isarticle", isArticleChangeListener);
}
if (filter.properties.has("openerTabId")) {
tabTracker.on("tab-openerTabId", openerTabIdChangeListener);
}
return {
unregister() {
for (let [name, listener] of listeners) {
@ -540,6 +550,10 @@ this.tabs = class extends ExtensionAPIPersistent {
if (filter.properties.has("isArticle")) {
tabTracker.off("tab-isarticle", isArticleChangeListener);
}
if (filter.properties.has("openerTabId")) {
tabTracker.off("tab-openerTabId", openerTabIdChangeListener);
}
},
convert(_fire, _context) {
fire = _fire;
@ -939,14 +953,7 @@ this.tabs = class extends ExtensionAPIPersistent {
}
}
if (updateProperties.openerTabId !== null) {
let opener = tabTracker.getTab(updateProperties.openerTabId);
if (opener.ownerDocument !== nativeTab.ownerDocument) {
return Promise.reject({
message:
"Opener tab must be in the same window as the tab being updated",
});
}
tabTracker.setOpener(nativeTab, opener);
tabTracker.setOpener(nativeTab, updateProperties.openerTabId);
}
if (updateProperties.successorTabId !== null) {
let successor = null;

View file

@ -109,7 +109,7 @@
},
"openerTabId": {
"type": "integer",
"minimum": 0,
"minimum": -1,
"optional": true,
"description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."
},
@ -623,7 +623,7 @@
},
"openerTabId": {
"type": "integer",
"minimum": 0,
"minimum": -1,
"optional": true,
"description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as the newly created tab."
},
@ -828,7 +828,7 @@
},
"openerTabId": {
"type": "integer",
"minimum": 0,
"minimum": -1,
"optional": true,
"description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab."
},
@ -967,7 +967,7 @@
},
"openerTabId": {
"type": "integer",
"minimum": 0,
"minimum": -1,
"optional": true,
"description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab."
},

View file

@ -128,3 +128,114 @@ add_task(async function () {
BrowserTestUtils.removeTab(tab1);
BrowserTestUtils.removeTab(tab2);
});
add_task(async function test_tabs_onUpdated_fired_on_openerTabId_change() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
},
async background() {
let changedOpenerTabIds = [];
let promise = new Promise(resolve => {
browser.tabs.onUpdated.addListener((tabId, changeInfo, _tab) => {
const { openerTabId } = changeInfo;
if (openerTabId) {
browser.test.assertDeepEq(
{ openerTabId },
changeInfo,
`"openerTabId" is the only key in changeInfo for tab ${tabId}`
);
changedOpenerTabIds.push(openerTabId);
if (openerTabId === -1) {
// The last part of the test changes openerTabId back to -1.
resolve();
}
}
});
});
try {
let tab1 = await browser.tabs.create({});
let tab2 = await browser.tabs.create({});
browser.test.assertDeepEq(
[],
changedOpenerTabIds,
"No tabs.onUpdated fired with openerTabId at tab creation"
);
// Not changed, should not emit event:
await browser.tabs.update(tab1.id, { openerTabId: -1 });
// Should emit event:
await browser.tabs.update(tab1.id, { openerTabId: tab2.id });
// Not changed, should not emit event:
await browser.tabs.update(tab1.id, { openerTabId: tab2.id });
// Should emit event:
await browser.tabs.update(tab1.id, { openerTabId: -1 });
await promise;
browser.test.assertDeepEq(
[tab2.id, -1],
changedOpenerTabIds,
"Got expected tabs.onUpdated for openerTabId changes"
);
await browser.tabs.remove(tab1.id);
await browser.tabs.remove(tab2.id);
browser.test.notifyPass("tab-onUpdated-opener");
} catch (e) {
browser.test.fail(`${e} :: ${e.stack}`);
browser.test.notifyFail("tab-onUpdated-opener");
}
},
});
await extension.startup();
await extension.awaitFinish("tab-onUpdated-opener");
await extension.unload();
});
add_task(async function test_errors_on_openerTabId_change() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["tabs"],
},
async background() {
let tab = await browser.tabs.create({});
await browser.test.assertRejects(
browser.tabs.update(tab.id, { openerTabId: 123456789 }),
"Invalid tab ID: 123456789",
"Got error when openerTabId is invalid"
);
let win = await browser.windows.create({ url: "about:blank" });
await browser.test.assertRejects(
browser.tabs.update(tab.id, { openerTabId: win.tabs[0].id }),
"Opener tab must be in the same window as the tab being updated",
"Got error when openerTabId belongs to a different window"
);
tab = await browser.tabs.get(tab.id);
browser.test.assertEq(
undefined,
tab.openerTabId,
"Got initial tab.openerTabId after failing updates"
);
await browser.windows.remove(win.id);
await browser.tabs.remove(tab.id);
browser.test.notifyPass("tab-opener-with-wrong-window");
},
});
await extension.startup();
await extension.awaitFinish("tab-opener-with-wrong-window");
await extension.unload();
});

View file

@ -13,8 +13,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
ChromeUtils.defineLazyGetter(
lazy,
@ -89,12 +87,6 @@ XPCOMUtils.defineLazyPreferenceGetter(
"chatShortcutsCustom",
"browser.ml.chat.shortcuts.custom"
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"chatShortcutsDebounce",
"browser.ml.chat.shortcutsDebounce",
200
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"chatSidebar",
@ -329,7 +321,7 @@ export const GenAI = {
case "GenAI:HideShortcuts":
hide();
break;
case "GenAI:SelectionChange":
case "GenAI:ShowShortcuts": {
// Add shortcuts to the current tab's brower stack if it doesn't exist
if (!shortcuts) {
shortcuts = stack.appendChild(document.createElement("div"));
@ -392,36 +384,29 @@ export const GenAI = {
});
}
// Immediately hide shortcuts and debounce multiple selection changes
hide();
if (shortcuts.timeout) {
lazy.clearTimeout(shortcuts.timeout);
}
// Save the latest selection so it can be used by timeout and popup
// Save the latest selection so it can be used by popup
shortcuts.selection = data.selection;
shortcuts.timeout = lazy.setTimeout(() => {
// Pref might have changed since the timeout started
if (!lazy.chatShortcuts || shortcuts.hasAttribute("shown")) {
return;
}
if (shortcuts.hasAttribute("shown")) {
return;
}
shortcuts.toggleAttribute("shown");
Glean.genaiChatbot.shortcutsDisplayed.record({
selection: shortcuts.selection.length,
});
shortcuts.toggleAttribute("shown");
Glean.genaiChatbot.shortcutsDisplayed.record({
selection: shortcuts.selection.length,
});
// Position the shortcuts relative to the browser's top-left corner
const rect = browser.getBoundingClientRect();
shortcuts.style.setProperty(
"--shortcuts-x",
data.x - window.screenX - rect.x + "px"
);
shortcuts.style.setProperty(
"--shortcuts-y",
data.y - window.screenY - rect.y + "px"
);
}, lazy.chatShortcutsDebounce);
// Position the shortcuts relative to the browser's top-left corner
const rect = browser.getBoundingClientRect();
shortcuts.style.setProperty(
"--shortcuts-x",
data.x - window.screenX - rect.x + "px"
);
shortcuts.style.setProperty(
"--shortcuts-y",
data.y - window.screenY - rect.y + "px"
);
break;
}
}
},

View file

@ -2,44 +2,49 @@
* 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/. */
// Additional events to listen with others to create the actor in BrowserGlue
const EVENTS = ["mousedown", "mouseup"];
/**
* JSWindowActor to detect content page events to send GenAI related data.
*/
export class GenAIChild extends JSWindowActorChild {
actorCreated() {
this.document.addEventListener("selectionchange", this);
EVENTS.forEach(ev => this.contentWindow.addEventListener(ev, this));
}
didDestroy() {
this.document.removeEventListener("selectionchange", this);
EVENTS.forEach(ev => this.contentWindow?.removeEventListener(ev, this));
}
handleEvent(event) {
const sendHide = () => this.sendQuery("GenAI:HideShortcuts", event.type);
switch (event.type) {
case "mousemove":
// Track the pointer's screen position to avoid container positioning
this.lastX = event.screenX;
this.lastY = event.screenY;
case "mousedown":
this.downTime = Date.now();
sendHide();
break;
case "mouseup": {
// Show immediately on selection or allow long press with no selection
const selection =
this.contentWindow.getSelection()?.toString().trim() ?? "";
if (selection || Date.now() - (this.downTime ?? 0) > 200) {
this.sendQuery("GenAI:ShowShortcuts", {
x: event.screenX,
y: event.screenY,
selection,
});
}
break;
}
case "resize":
case "scroll":
case "selectionchange":
// Hide if selection might have shifted away from shortcuts
sendHide();
break;
case "selectionchange": {
const selection = this.contentWindow.getSelection().toString().trim();
if (!selection) {
sendHide();
break;
}
this.sendQuery("GenAI:SelectionChange", {
x: this.lastX ?? 0,
y: this.lastY ?? 0,
selection,
});
break;
}
}
}
}

View file

@ -24,20 +24,26 @@ add_task(async function test_no_shortcuts() {
add_task(async function test_show_shortcuts() {
Services.fog.testResetFOG();
await SpecialPowers.pushPrefEnv({
set: [
["browser.ml.chat.shortcuts", true],
["browser.ml.chat.shortcutsDebounce", 0],
],
set: [["browser.ml.chat.shortcuts", true]],
});
await BrowserTestUtils.withNewTab("data:text/plain,hi", async browser => {
await SimpleTest.promiseFocus(browser);
const selectPromise = SpecialPowers.spawn(browser, [], () => {
ContentTaskUtils.waitForCondition(() => content.getSelection());
});
goDoCommand("cmd_selectAll");
await selectPromise;
BrowserTestUtils.synthesizeMouseAtCenter(
browser,
{ type: "mouseup" },
browser
);
const shortcuts = await TestUtils.waitForCondition(() =>
document.querySelector(".content-shortcuts[shown]")
);
Assert.ok(shortcuts, "Shortcuts added on select");
let events = Glean.genaiChatbot.shortcutsDisplayed.testGetValue();
Assert.ok(events.length, "Shortcuts shown");
Assert.equal(events.length, 1, "Shortcuts shown once");
Assert.equal(events[0].extra.selection, 2, "Selected hi");
const popup = document.getElementById("ask-chat-shortcuts");

View file

@ -64,6 +64,22 @@
justify-content: space-between;
align-items: center;
> a {
color: var(--link-color);
&:hover {
color: var(--link-color-hover);
}
&:hover:active {
color: var(--link-color-active);
}
&:visited {
color: var(--link-color-visited);
}
}
.button-group {
gap: var(--space-medium);
display: flex;

View file

@ -5579,6 +5579,18 @@ main section {
justify-content: space-between;
align-items: center;
}
.topic-selection-container .modal-footer > a {
color: var(--link-color);
}
.topic-selection-container .modal-footer > a:hover {
color: var(--link-color-hover);
}
.topic-selection-container .modal-footer > a:hover:active {
color: var(--link-color-active);
}
.topic-selection-container .modal-footer > a:visited {
color: var(--link-color-visited);
}
.topic-selection-container .modal-footer .button-group {
gap: var(--space-medium);
display: flex;

View file

@ -5583,6 +5583,18 @@ main section {
justify-content: space-between;
align-items: center;
}
.topic-selection-container .modal-footer > a {
color: var(--link-color);
}
.topic-selection-container .modal-footer > a:hover {
color: var(--link-color-hover);
}
.topic-selection-container .modal-footer > a:hover:active {
color: var(--link-color-active);
}
.topic-selection-container .modal-footer > a:visited {
color: var(--link-color-visited);
}
.topic-selection-container .modal-footer .button-group {
gap: var(--space-medium);
display: flex;

View file

@ -5579,6 +5579,18 @@ main section {
justify-content: space-between;
align-items: center;
}
.topic-selection-container .modal-footer > a {
color: var(--link-color);
}
.topic-selection-container .modal-footer > a:hover {
color: var(--link-color-hover);
}
.topic-selection-container .modal-footer > a:hover:active {
color: var(--link-color-active);
}
.topic-selection-container .modal-footer > a:visited {
color: var(--link-color-visited);
}
.topic-selection-container .modal-footer .button-group {
gap: var(--space-medium);
display: flex;

View file

@ -320,6 +320,13 @@
return this.querySelector(".tab-close-button");
}
get group() {
if (this.parentElement.tagName == "tab-group") {
return this.parentElement;
}
return null;
}
updateLastAccessed(aDate) {
this._lastAccessed = this.selected ? Infinity : aDate || Date.now();
}

View file

@ -850,7 +850,6 @@
if (this.tabContainer.verticalMode) {
let wasFocused = document.activeElement == this.selectedTab;
let oldPosition = aTab._tPos;
this.tabContainer._invalidateCachedTabs();
this.verticalPinnedTabsContainer.appendChild(aTab);
this._updateAfterMoveTabTo(aTab, oldPosition, wasFocused);
} else {
@ -873,7 +872,6 @@
// the moving of a tab from the vertical pinned tabs container
// and back into arrowscrollbox.
aTab.removeAttribute("pinned");
this.tabContainer._invalidateCachedTabs();
this.tabContainer.arrowScrollbox.prepend(aTab);
this._updateAfterMoveTabTo(aTab, oldPosition, wasFocused);
} else {
@ -2741,7 +2739,8 @@
}
let openerTab =
(openerBrowser && this.getTabForBrowser(openerBrowser)) ||
(relatedToCurrent && this.selectedTab);
(relatedToCurrent && this.selectedTab) ||
null;
// When overflowing, new tabs are scrolled into view smoothly, which
// doesn't go well together with the width transition. So we skip the
@ -2930,6 +2929,7 @@
group.color = color;
group.label = label;
this.tabContainer.appendChild(group);
return group;
},
_determineURIToLoad(uriString, createLazyBrowser) {
@ -5331,13 +5331,29 @@
neighbor.after(aTab);
}
// We want to clear _allTabs after moving nodes because the order of
// vertical tabs may have changed.
this.tabContainer._invalidateCachedTabs();
this._updateAfterMoveTabTo(aTab, oldPosition, wasFocused);
},
moveTabToGroup(aTab, aGroup) {
if (aTab.pinned) {
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
return;
}
let wasFocused = document.activeElement == this.selectedTab;
aGroup.appendChild(aTab);
// pass -1 to oldPosition because a move occurred even if position
// hasn't changed
this._updateAfterMoveTabTo(aTab, -1, wasFocused);
},
_updateAfterMoveTabTo(aTab, oldPosition, wasFocused = null) {
// We want to clear _allTabs after moving nodes because the order of
// vertical tabs may have changed.
this.tabContainer._invalidateCachedTabs();
this._updateTabsAfterInsert();
if (wasFocused) {

View file

@ -57,6 +57,21 @@
set label(val) {
this.setAttribute("label", val);
}
get tabs() {
return Array.from(this.children).filter(node => node.matches("tab"));
}
/**
* add tabs to the group
*
* @param tabs array of tabs to add
*/
addTabs(tabs) {
for (let tab of tabs) {
gBrowser.moveTabToGroup(tab, this);
}
}
}
customElements.define("tab-group", MozTabbrowserTabGroup);

View file

@ -1139,6 +1139,13 @@
// remove arrowScrollbox periphery element
children.pop();
// explode tab groups
Array.from(children).forEach((node, index) => {
if (node.tagName == "tab-group") {
children.splice(index, 1, ...node.tabs);
}
});
let allChildren = [...verticalPinnedTabsContainer.children, ...children];
this._allTabs = allChildren;
return allChildren;

View file

@ -317,6 +317,8 @@ skip-if = ["true"] # Bug 1616418 Bug 1549985
["browser_tab_a11y_description.js"]
["browser_tab_groups.js"]
["browser_tab_label_during_reload.js"]
["browser_tab_label_picture_in_picture.js"]

View file

@ -0,0 +1,18 @@
/* 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/. */
add_task(async function test_tabGroups() {
let group = gBrowser.addTabGroup("blue", "test");
Assert.ok(group.id, "group has id");
let tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank");
group.addTabs([tab1]);
Assert.ok(group.tabs.includes(tab1), "tab1 is in group");
// TODO add API to remove group
BrowserTestUtils.removeTab(tab1);
group.remove();
});

View file

@ -20,7 +20,9 @@ ChromeUtils.defineLazyGetter(lazy, "SearchModeSwitcherL10n", () => {
*/
export class SearchModeSwitcher {
#engineListNeedsRebuild = true;
#input = null;
#popup;
#input;
#toolbarbutton;
constructor(input) {
this.#input = input;
@ -31,11 +33,13 @@ export class SearchModeSwitcher {
]);
Services.obs.addObserver(this, "browser-search-engine-modified", true);
let toolbarbutton = input.document.querySelector(
this.#popup = input.document.getElementById("searchmode-switcher-popup");
this.#toolbarbutton = input.document.querySelector(
"#urlbar-searchmode-switcher"
);
toolbarbutton.addEventListener("mousedown", this);
toolbarbutton.addEventListener("keypress", this);
this.#toolbarbutton.addEventListener("mousedown", this);
this.#toolbarbutton.addEventListener("keypress", this);
let closebutton = input.document.querySelector(
"#searchmode-switcher-close"
@ -43,6 +47,12 @@ export class SearchModeSwitcher {
closebutton.addEventListener("mousedown", this);
closebutton.addEventListener("keypress", this);
let prefsbutton = input.document.querySelector(
"#searchmode-switcher-popup-search-settings-button"
);
prefsbutton.addEventListener("mousedown", this);
prefsbutton.addEventListener("keypress", this);
input.window.addEventListener(
"MozAfterPaint",
() => this.#updateSearchIcon(),
@ -57,8 +67,18 @@ export class SearchModeSwitcher {
* The event that triggered the opening of the popup.
*/
async openPanel(event) {
if (
(event.type == "click" && event.button != 0) ||
(event.type == "keypress" &&
event.charCode != KeyEvent.DOM_VK_SPACE &&
event.keyCode != KeyEvent.DOM_VK_RETURN &&
event.keyCode != KeyEvent.DOM_VK_DOWN)
) {
return; // Left click, down arrow, space or enter only
}
let anchor = event.target;
event.stopPropagation();
event.preventDefault();
if (this.#input.document.documentElement.hasAttribute("customizing")) {
return;
@ -71,23 +91,47 @@ export class SearchModeSwitcher {
if (anchor.getAttribute("open") != "true") {
this.#input.view.hideTemporarily();
this.#getPopup().addEventListener(
this.#popup.addEventListener(
"popuphidden",
() => {
anchor.removeAttribute("open");
anchor.setAttribute("aria-expanded", false);
this.#input.view.restoreVisibility();
},
{ once: true }
);
anchor.setAttribute("open", true);
anchor.setAttribute("aria-expanded", true);
lazy.PanelMultiView.openPopup(this.#getPopup(), anchor, {
this.#popup.addEventListener(
"popupshown",
() => {
this.#popup.querySelector("toolbarbutton").focus();
},
{ once: true }
);
lazy.PanelMultiView.openPopup(this.#popup, anchor, {
position: "bottomleft topleft",
triggerEvent: event,
}).catch(console.error);
}
}
#openPreferences(event) {
if (
(event.type == "click" && event.button != 0) ||
(event.type == "keypress" &&
event.charCode != KeyEvent.DOM_VK_SPACE &&
event.keyCode != KeyEvent.DOM_VK_RETURN)
) {
return; // Left click, space or enter only
}
this.#input.window.openPreferences("paneSearch");
this.#popup.hidePopup();
}
/**
* Exit the engine specific searchMode.
*
@ -95,7 +139,6 @@ export class SearchModeSwitcher {
* The event that triggered the searchMode exit.
*/
exitSearchMode(event) {
event.stopPropagation();
event.preventDefault();
this.#input.searchMode = null;
}
@ -119,6 +162,10 @@ export class SearchModeSwitcher {
this.exitSearchMode(event);
break;
}
case "openpreferences": {
this.#openPreferences(event);
break;
}
}
}
@ -131,70 +178,63 @@ export class SearchModeSwitcher {
}
}
#popup = null;
#getPopup() {
if (!this.#popup) {
this.#popup = this.#input.document.getElementById(
"searchmode-switcher-popup"
);
}
return this.#popup;
}
async #updateSearchIcon() {
try {
await lazy.UrlbarSearchUtils.init();
} catch {
// We should still work if the SearchService is not working.
}
let button = this.#input.document.getElementById(
"searchmode-switcher-icon"
let { label, icon } = await this.#getDisplayedEngineDetails(
this.#input.searchMode
);
let iconURL = await this.#getIconForSearchMode(this.#input.searchMode);
if (iconURL) {
button.style.listStyleImage = `url(${iconURL})`;
}
let label = this.#input.document.getElementById(
let iconUrl = icon ? `url(${icon})` : "";
this.#input.document.getElementById(
"searchmode-switcher-icon"
).style.listStyleImage = iconUrl;
this.#input.document.l10n.setAttributes(
this.#toolbarbutton,
"urlbar-searchmode-button",
{ engine: label }
);
let labelEl = this.#input.document.getElementById(
"searchmode-switcher-title"
);
if (!this.#input.searchMode) {
label.replaceChildren();
} else if (this.#input.searchMode?.engineName) {
label.textContent = this.#input.searchMode.engineName;
} else if (this.#input.searchMode?.source) {
// Certainly the wrong way to do this, the same string is used
// as a <label and as a <toolbarbutton label= and not sure
// how to define an l10n ID to be used both ways.
let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
m => m.source == this.#input.searchMode.source
);
let [str] = await lazy.SearchModeSwitcherL10n.formatMessages([
{
id: mode.uiLabel,
args: { restrict: mode.restrict },
},
]);
label.textContent = str.attributes[0].value;
labelEl.replaceChildren();
} else if (this.#input.searchMode) {
labelEl.textContent = label;
}
}
async #getIconForSearchMode(searchMode = null) {
async #getSearchModeLabel(source) {
let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
m => m.source == source
);
let [str] = await lazy.SearchModeSwitcherL10n.formatMessages([
{ id: mode.uiLabel },
]);
return str.attributes[0].value;
}
async #getDisplayedEngineDetails(searchMode = null) {
if (!searchMode || searchMode.engineName) {
let engine = searchMode
? lazy.UrlbarSearchUtils.getEngineByName(searchMode.engineName)
: lazy.UrlbarSearchUtils.getDefaultEngine();
return engine.getIconURL();
return { label: engine.name, icon: await engine.getIconURL() };
}
let mode = lazy.UrlbarUtils.LOCAL_SEARCH_MODES.find(
m => m.source == searchMode.source
);
return mode.icon;
return {
label: await this.#getSearchModeLabel(searchMode.source),
icon: mode.icon,
};
}
async #rebuildSearchModeList() {
let container = this.#getPopup().querySelector(".panel-subview-body");
let container = this.#popup.querySelector(".panel-subview-body");
container.replaceChildren();
let engines = await Services.search.getVisibleEngines();
let frag = this.#input.document.createDocumentFragment();
@ -202,6 +242,12 @@ export class SearchModeSwitcher {
remoteContainer.className = "remote-options";
frag.appendChild(remoteContainer);
let fireCommand = e => {
if (e.keyCode == KeyEvent.DOM_VK_RETURN) {
e.target.doCommand();
}
};
for (let engine of engines) {
if (engine.hideOneOffButton) {
continue;
@ -211,11 +257,13 @@ export class SearchModeSwitcher {
menuitem.setAttribute("class", "subviewbutton subviewbutton-iconic");
menuitem.setAttribute("label", engine.name);
menuitem.setAttribute("tabindex", "0");
menuitem.setAttribute("role", "menuitem");
menuitem.engine = engine;
menuitem.setAttribute(
"oncommand",
"gURLBar.searchModeSwitcher.search(event, { engine: this.engine })"
);
menuitem.addEventListener("keypress", fireCommand);
menuitem.addEventListener("command", () => {
this.search({ engine });
});
menuitem.setAttribute("image", await engine.getIconURL());
remoteContainer.appendChild(menuitem);
}
@ -232,18 +280,21 @@ export class SearchModeSwitcher {
let button = this.#input.document.createXULElement("toolbarbutton");
button.id = `search-button-${name}`;
button.setAttribute("class", "subviewbutton subviewbutton-iconic");
let iconUrl = await this.#getIconForSearchMode({
button.setAttribute("tabindex", "0");
button.setAttribute("role", "menuitem");
let { icon } = await this.#getDisplayedEngineDetails({
source,
pref,
restrict,
});
if (iconUrl) {
button.setAttribute("image", iconUrl);
if (icon) {
button.setAttribute("image", icon);
}
button.setAttribute(
"oncommand",
"gURLBar.searchModeSwitcher.search(event, { restrict: this.restrict })"
);
button.addEventListener("keypress", fireCommand);
button.addEventListener("command", () => {
this.search({ restrict });
});
this.#input.document.l10n.setAttributes(
button,
`urlbar-searchmode-${name}`,
@ -258,7 +309,7 @@ export class SearchModeSwitcher {
container.appendChild(frag);
}
search(event, { engine = null, restrict = null } = {}) {
search({ engine = null, restrict = null } = {}) {
let gBrowser = this.#input.window.gBrowser;
let search = "";
let opts = null;
@ -270,6 +321,6 @@ export class SearchModeSwitcher {
opts = { searchModeEntry: "searchbutton" };
}
this.#input.search(search, opts);
this.#getPopup().hidePopup();
this.#popup.hidePopup();
}
}

View file

@ -1238,19 +1238,13 @@ export class UrlbarInput {
return;
}
case lazy.UrlbarUtils.RESULT_TYPE.DYNAMIC: {
if (url) {
break;
}
url = result.payload.url;
// Keep the searchMode for telemetry since handleRevert sets it to null.
const searchMode = this.searchMode;
// Do not revert the Urlbar if we're going to navigate. We want the URL
// populated so we can navigate to it.
if (!url || !result.payload.shouldNavigate) {
if (!url) {
// If we're not loading a URL, the engagement is done. First revert
// and then record the engagement since providers expect the urlbar to
// be reverted when they're notified of the engagement, but before
// reverting, copy the search mode since it's nulled on revert.
const { searchMode } = this;
this.handleRevert();
}
// If we won't be navigating, this is the end of the engagement.
if (!url || !result.payload.shouldNavigate) {
this.controller.engagementEvent.record(event, {
result,
element,

View file

@ -816,10 +816,10 @@ class MuxerUnifiedComplete extends UrlbarMuxer {
return false;
}
// For tab-to-search results, result.payload.url is the engine's domain
// with the public suffix already stripped, for example "www.mozilla.".
// `searchUrlDomainWithoutSuffix` is the engine's domain with the public
// suffix already stripped, for example "www.mozilla.".
let [engineDomain] = UrlbarUtils.stripPrefixAndTrim(
result.payload.url,
result.payload.searchUrlDomainWithoutSuffix,
{
stripWww: true,
}

View file

@ -228,52 +228,46 @@ class ProviderQuickSuggest extends UrlbarProvider {
}
}
onLegacyEngagement(state, queryContext, details, controller) {
// Ignore engagements on other results that didn't end the session.
if (details.result?.providerName != this.name && details.isSessionOngoing) {
return;
onImpression(state, queryContext, controller, providerVisibleResults) {
// Legacy Suggest telemetry should be recorded when a Suggest result is
// visible at the end of an engagement on any result.
this.#sessionResult =
state == "engagement" ? providerVisibleResults[0].result : null;
}
onEngagement(queryContext, controller, details) {
if (details.isSessionOngoing) {
// When the session remains ongoing -- e.g., a result is dismissed --
// tests expect legacy telemetry to be recorded immediately on engagement,
// not deferred until the session ends, so record it now.
this.#recordEngagement(queryContext, details.result, details);
}
let feature = this.#getFeatureByResult(details.result);
if (feature?.handleCommand) {
feature.handleCommand(
controller.view,
details.result,
details.selType,
this._trimmedSearchString
);
} else if (details.selType == "dismiss") {
// Handle dismissals.
this.#dismissResult(controller, details.result);
}
feature?.onEngagement?.(queryContext, controller, details);
}
onSearchSessionEnd(queryContext, controller, details) {
// Reset the Merino session ID when a session ends. By design for the user's
// privacy, we don't keep it around between engagements.
if (!details.isSessionOngoing) {
this.#merino?.resetSession();
}
this.#merino?.resetSession();
// Impression and clicked telemetry are both recorded on engagement. We
// define "impression" to mean a quick suggest result was present in the
// view when any result was picked.
if (state == "engagement" && queryContext) {
// Get the result that's visible in the view. `details.result` is the
// engaged result, if any; if it's from this provider, then that's the
// visible result. Otherwise fall back to #getVisibleResultFromLastQuery.
let { result } = details;
if (result?.providerName != this.name) {
result = this.#getVisibleResultFromLastQuery(controller.view);
}
this.#recordEngagement(queryContext, result, details);
}
if (details.result?.providerName == this.name) {
let feature = this.#getFeatureByResult(details.result);
if (feature?.handleCommand) {
feature.handleCommand(
controller.view,
details.result,
details.selType,
this._trimmedSearchString
);
} else if (details.selType == "dismiss") {
// Handle dismissals.
this.#dismissResult(controller, details.result);
}
if (state == "engagement" && feature?.onEngagement) {
feature.onEngagement(queryContext, controller, details);
}
}
// Record legacy Suggest telemetry.
this.#recordEngagement(queryContext, this.#sessionResult, details);
this.#sessionResult = null;
this.#resultFromLastQuery = null;
}
@ -451,22 +445,6 @@ class ProviderQuickSuggest extends UrlbarProvider {
);
}
#getVisibleResultFromLastQuery(view) {
let result = this.#resultFromLastQuery;
if (
result?.rowIndex >= 0 &&
view?.visibleResults?.[result.rowIndex] == result
) {
// The result was visible.
return result;
}
// Find a visible result. Quick suggest results typically appear last in the
// view, so do a reverse search.
return view?.visibleResults?.findLast(r => r.providerName == this.name);
}
#dismissResult(controller, result) {
if (!result.payload.isBlockable) {
this.logger.info("Dismissals disabled, ignoring dismissal");
@ -493,8 +471,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
* end of the engagement or that was dismissed. Null if no quick suggest
* result was present.
* @param {object} details
* The `details` object that was passed to `onLegacyEngagement()`. It must
* look like this: `{ selType, selIndex }`
* The `details` object that was passed to `onEngagement()` or
* `onSearchSessionEnd()`.
*/
#recordEngagement(queryContext, result, details) {
let resultSelType = "";
@ -785,8 +763,8 @@ class ProviderQuickSuggest extends UrlbarProvider {
* True if the main part of the result's row was clicked; false if a button
* like help or dismiss was clicked or if no part of the row was clicked.
* @param {object} options.details
* The `details` object that was passed to `onLegacyEngagement()`. It must
* look like this: `{ selType, selIndex }`
* The `details` object that was passed to `onEngagement()` or
* `onSearchSessionEnd()`.
*/
#recordNavSuggestionTelemetry({
queryContext,
@ -945,9 +923,13 @@ class ProviderQuickSuggest extends UrlbarProvider {
return this.#merino;
}
// The result we added during the most recent query.
// The result we added during the most recent query. It may not be visible.
#resultFromLastQuery = null;
// The result from this provider that was visible at the end of the current
// search session, if the session ended in an engagement.
#sessionResult;
// The Merino client.
#merino = null;
}

View file

@ -404,16 +404,12 @@ class ProviderTabToSearch extends UrlbarProvider {
}
function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) {
let [url] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
stripWww: true,
});
url = url.substr(0, url.length - engine.searchUrlPublicSuffix.length);
let result = new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.DYNAMIC,
UrlbarUtils.RESULT_SOURCE.SEARCH,
{
engine: engine.name,
url,
searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
providesSearchMode: true,
icon: UrlbarUtils.ICON.SEARCH_GLASS,
dynamicType: DYNAMIC_RESULT_TYPE,
@ -426,17 +422,13 @@ function makeOnboardingResult(engine, satisfiesAutofillThreshold = false) {
}
function makeResult(context, engine, satisfiesAutofillThreshold = false) {
let [url] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
stripWww: true,
});
url = url.substr(0, url.length - engine.searchUrlPublicSuffix.length);
let result = new lazy.UrlbarResult(
UrlbarUtils.RESULT_TYPE.SEARCH,
UrlbarUtils.RESULT_SOURCE.SEARCH,
...lazy.UrlbarResult.payloadAndSimpleHighlights(context.tokens, {
engine: engine.name,
isGeneralPurposeEngine: engine.isGeneralPurposeEngine,
url,
searchUrlDomainWithoutSuffix: searchUrlDomainWithoutSuffix(engine),
providesSearchMode: true,
icon: UrlbarUtils.ICON.SEARCH_GLASS,
query: "",
@ -447,5 +439,12 @@ function makeResult(context, engine, satisfiesAutofillThreshold = false) {
return result;
}
function searchUrlDomainWithoutSuffix(engine) {
let [value] = UrlbarUtils.stripPrefixAndTrim(engine.searchUrlDomain, {
stripWww: true,
});
return value.substr(0, value.length - engine.searchUrlPublicSuffix.length);
}
export var UrlbarProviderTabToSearch = new ProviderTabToSearch();
initializeDynamicResult();

View file

@ -212,7 +212,7 @@ class ProviderWeather extends UrlbarProvider {
* A non-empty string means the user picked the weather row or some part of
* it, and both impression and click telemetry will be recorded. The
* non-empty-string values come from the `details.selType` passed in to
* `onLegacyEngagement()`; see `TelemetryEvent.typeFromElement()`.
* `onEngagement()`; see `TelemetryEvent.typeFromElement()`.
*/
#recordEngagementTelemetry(result, isPrivate, selType) {
// Indexes recorded in quick suggest telemetry are 1-based, so add 1 to the

View file

@ -442,7 +442,8 @@ class ProvidersManager {
this.#notifySearchSessionEnd(
this.providersByNotificationType.onSearchSessionEnd,
queryContext,
controller
controller,
details
);
}
@ -500,9 +501,19 @@ class ProvidersManager {
}
}
#notifySearchSessionEnd(searchSessionEndProviders, queryContext, controller) {
#notifySearchSessionEnd(
searchSessionEndProviders,
queryContext,
controller,
details
) {
for (const provider of searchSessionEndProviders) {
provider.tryMethod("onSearchSessionEnd", queryContext, controller);
provider.tryMethod(
"onSearchSessionEnd",
queryContext,
controller,
details
);
}
}

View file

@ -570,38 +570,34 @@ export var UrlbarUtils = {
},
/**
* Extracts an url from a result, if possible.
* Extracts the URL from a result.
*
* @param {UrlbarResult} result The result to extract from.
* @returns {object} a {url, postData} object, or null if a url can't be built
* from this result.
* @param {UrlbarResult} result
* The result to extract from.
* @returns {object}
* An object: `{ url, postData }`
* `url` will be null if the result doesn't have a URL. `postData` will be
* null if the result doesn't have post data.
*/
getUrlFromResult(result) {
switch (result.type) {
case UrlbarUtils.RESULT_TYPE.URL:
case UrlbarUtils.RESULT_TYPE.REMOTE_TAB:
case UrlbarUtils.RESULT_TYPE.TAB_SWITCH:
return { url: result.payload.url, postData: null };
case UrlbarUtils.RESULT_TYPE.KEYWORD:
return {
url: result.payload.url,
postData: result.payload.postData
? this.getPostDataStream(result.payload.postData)
: null,
};
case UrlbarUtils.RESULT_TYPE.SEARCH: {
if (result.payload.engine) {
const engine = Services.search.getEngineByName(result.payload.engine);
let [url, postData] = this.getSearchQueryUrl(
engine,
result.payload.suggestion || result.payload.query
);
return { url, postData };
}
break;
}
if (
result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
result.payload.engine
) {
const engine = Services.search.getEngineByName(result.payload.engine);
let [url, postData] = this.getSearchQueryUrl(
engine,
result.payload.suggestion || result.payload.query
);
return { url, postData };
}
return { url: null, postData: null };
return {
url: result.payload.url ?? null,
postData: result.payload.postData
? this.getPostDataStream(result.payload.postData)
: null,
};
},
/**
@ -1722,6 +1718,9 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
satisfiesAutofillThreshold: {
type: "boolean",
},
searchUrlDomainWithoutSuffix: {
type: "string",
},
suggestion: {
type: "string",
},
@ -2090,12 +2089,6 @@ UrlbarUtils.RESULT_PAYLOAD_SCHEMA = {
dynamicType: {
type: "string",
},
// If `shouldNavigate` is `true` and the payload contains a `url`
// property, when the result is selected the browser will navigate to the
// `url`.
shouldNavigate: {
type: "boolean",
},
},
},
[UrlbarUtils.RESULT_TYPE.RESTRICT]: {

View file

@ -398,23 +398,19 @@ urlbar-firefox-suggest-contextual-opt-in-dismiss = Not now
## Searchmode Switcher button
# Variables:
# $engine (String): the current default search engine.
urlbar-searchmode-button =
.label = Pick a Search Engine
.tooltiptext = Pick a Search Engine
.label = { $engine }, Pick a Search Engine
.tooltiptext = { $engine }, Pick a Search Engine
urlbar-searchmode-dropmarker =
.tooltiptext = Pick a Search Engine
# Variables:
# $restrict (Character): the character used to filter results of type.
urlbar-searchmode-bookmarks =
.label = Bookmarks ({ $restrict })
# Variables:
# $restrict (Character): the character used to filter results of type.
.label = Bookmarks
urlbar-searchmode-tabs =
.label = Tabs ({ $restrict })
# Variables:
# $restrict (Character): the character used to filter results of type.
.label = Tabs
urlbar-searchmode-history =
.label = History ({ $restrict })
.label = History
urlbar-searchmode-popup-description = This time search with:
urlbar-searchmode-popup-search-settings = Search Settings

View file

@ -614,10 +614,9 @@ useful in this case.
URL Navigation
~~~~~~~~~~~~~~
If a result's payload includes a string ``url`` property and a boolean
``shouldNavigate: true`` property, then picking the result will navigate to the
URL. The ``onLegacyEngagement`` method of the result's provider will still be called
before navigation.
If a result's payload includes a string ``url`` property, picking the result
will navigate to the URL. The ``onEngagement`` method of the result's provider
will be called before navigation.
Text Highlighting
~~~~~~~~~~~~~~~~~

View file

@ -130,7 +130,6 @@ export class FakespotSuggestions extends BaseFeature {
totalReviews: Number(suggestion.totalReviews),
fakespotGrade: suggestion.fakespotGrade,
fakespotProvider: this.#parseProvider(suggestion),
shouldNavigate: true,
dynamicType: "fakespot",
};

View file

@ -361,7 +361,6 @@ export class Weather extends BaseFeature {
forecast: suggestion.forecast.summary,
high: suggestion.forecast.high[unit],
low: suggestion.forecast.low[unit],
shouldNavigate: true,
}
),
{

View file

@ -460,7 +460,7 @@ add_task(async function pick() {
// Tests picking elements in a dynamic result.
add_task(async function shouldNavigate() {
/**
* A dummy provider that providers results with a `shouldNavigate` property.
* A dummy provider that providers results with a `url` property.
*/
class TestShouldNavigateProvider extends TestProvider {
/**
@ -470,7 +470,6 @@ add_task(async function shouldNavigate() {
async startQuery(context, addCallback) {
for (let result of this.results) {
result.payload.searchString = context.searchString;
result.payload.shouldNavigate = true;
result.payload.url = DUMMY_PAGE;
addCallback(this, result);
}

View file

@ -1,6 +1,10 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
setTimeout: "resource://gre/modules/Timer.sys.mjs",
});
add_setup(async function setup() {
await SpecialPowers.pushPrefEnv({
set: [["browser.urlbar.scotchBonnet.enableOverride", true]],
@ -104,3 +108,130 @@ add_task(async function detect_searchmode_changes() {
);
}, "The searchMode name has been removed when we exit search mode");
});
function focusSwitcher() {
EventUtils.synthesizeKey("l", { accelKey: true });
EventUtils.synthesizeKey("KEY_Escape");
EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
}
/**
* Test we can open the SearchModeSwitcher with various keys
*
* @param {string} openKey - The keyboard character used to open the popup.
*/
async function test_open_switcher(openKey) {
let popup = UrlbarTestUtils.searchModeSwitcherPopup(window);
let promiseMenuOpen = BrowserTestUtils.waitForEvent(popup, "popupshown");
info("Open the urlbar and open the switcher via keyboard");
focusSwitcher();
EventUtils.synthesizeKey(openKey);
await promiseMenuOpen;
EventUtils.synthesizeKey("KEY_Escape");
}
/**
* Test that not all characters will open the SearchModeSwitcher
*
* @param {string} dontOpenKey - The keyboard character we will ignore.
*/
async function test_dont_open_switcher(dontOpenKey) {
let popup = UrlbarTestUtils.searchModeSwitcherPopup(window);
let popupOpened = false;
let opened = () => {
popupOpened = true;
};
info("Open the urlbar and open the switcher via keyboard");
popup.addEventListener("popupshown", opened);
focusSwitcher();
EventUtils.synthesizeKey(dontOpenKey);
/* eslint-disable mozilla/no-arbitrary-setTimeout */
await new Promise(r => setTimeout(r, 50));
Assert.ok(!popupOpened, "The popup was not opened");
popup.removeEventListener("popupshown", opened);
}
/**
* Test we can navigate the SearchModeSwitcher with various keys
*
* @param {string} navKey - The keyboard character used to navigate.
* @param {Int} navTimes - The number of times we press that key.
* @param {object} searchMode - The searchMode that we expect to select.
*/
async function test_navigate_switcher(navKey, navTimes, searchMode) {
let popup = UrlbarTestUtils.searchModeSwitcherPopup(window);
let promiseMenuOpen = BrowserTestUtils.waitForEvent(popup, "popupshown");
info("Open the urlbar and open the switcher via keyboard");
focusSwitcher();
EventUtils.synthesizeKey("KEY_Enter");
await promiseMenuOpen;
info("Select first result and enter search mode");
for (let i = 0; i < navTimes; i++) {
EventUtils.synthesizeKey(navKey);
}
EventUtils.synthesizeKey("KEY_Enter");
await UrlbarTestUtils.assertSearchMode(window, searchMode);
info("Escape Search mode");
EventUtils.synthesizeKey("KEY_Escape");
EventUtils.synthesizeKey("KEY_Escape");
await UrlbarTestUtils.assertSearchMode(window, null);
}
let amazonSearchMode = {
engineName: "Amazon.com",
entry: "other",
isPreview: false,
isGeneralPurposeEngine: true,
};
let bingSearchMode = {
engineName: "Bing",
isGeneralPurposeEngine: true,
source: 3,
isPreview: false,
entry: "other",
};
add_task(async function test_keyboard_nav() {
await test_open_switcher("KEY_Enter");
await test_open_switcher("KEY_ArrowDown");
await test_open_switcher(" ");
await test_dont_open_switcher("a");
await test_dont_open_switcher("KEY_ArrowUp");
await test_dont_open_switcher("x");
await test_navigate_switcher("KEY_Tab", 1, amazonSearchMode);
await test_navigate_switcher("KEY_ArrowDown", 1, amazonSearchMode);
await test_navigate_switcher("KEY_Tab", 2, bingSearchMode);
await test_navigate_switcher("KEY_ArrowDown", 2, bingSearchMode);
});
add_task(async function open_settings() {
let popup = UrlbarTestUtils.searchModeSwitcherPopup(window);
let promiseMenuOpen = BrowserTestUtils.waitForEvent(popup, "popupshown");
info("Open the urlbar and open the switcher via keyboard");
focusSwitcher();
EventUtils.synthesizeKey("KEY_Enter");
await promiseMenuOpen;
let pageLoaded = BrowserTestUtils.browserLoaded(window);
EventUtils.synthesizeKey("KEY_ArrowUp");
EventUtils.synthesizeKey("KEY_Enter");
await pageLoaded;
Assert.equal(
window.gBrowser.selectedBrowser.currentURI.spec,
"about:preferences#search",
"Opened settings page"
);
});

View file

@ -4,6 +4,9 @@
// Tests the `fakespot_engagement` event.
const FAKESPOT_URL = "https://example.com/fakespot";
const AMP_URL = "https://example.com/amp";
add_setup(async function test_setup() {
Services.fog.testResetFOG();
@ -17,8 +20,8 @@ add_setup(async function test_setup() {
type: "fakespot-suggestions",
attachment: [
{
url: "https://example.com/maybe-good-item",
title: "Maybe Good Item",
url: FAKESPOT_URL,
title: "Fakespot suggestion",
rating: 4.8,
total_reviews: 1234567,
fakespot_grade: "A",
@ -29,19 +32,8 @@ add_setup(async function test_setup() {
},
{
type: "data",
attachment: [
{
id: 1,
url: "https://example.com/sponsored",
title: "Sponsored suggestion",
keywords: ["sponsored"],
click_url: "https://example.com/click",
impression_url: "https://example.com/impression",
advertiser: "TestAdvertiser",
iab_category: "22 - Shopping",
icon: "1234",
},
],
// eslint-disable-next-line mozilla/valid-lazy
attachment: [lazy.QuickSuggestTestUtils.ampRemoteSettings()],
},
],
prefs: [["fakespot.featureGate", true]],
@ -53,14 +45,20 @@ add_setup(async function test_setup() {
});
});
// Clicks a Fakespot suggestion.
add_task(async function engagement_fakespot() {
await BrowserTestUtils.withNewTab("about:blank", async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "maybe",
value: "fakespot",
});
await selectRowByProvider("UrlbarProviderQuickSuggest");
Assert.ok(
await getRowByURL(FAKESPOT_URL),
"Fakespot row should be present"
);
await selectRowByURL(FAKESPOT_URL);
await doEnter();
assertGleanTelemetry("fakespot_engagement", [
@ -71,14 +69,21 @@ add_task(async function engagement_fakespot() {
Services.fog.testResetFOG();
});
add_task(async function engagement_notFakespot() {
// Clicks a non-Fakespot suggestion without matching Fakespot.
add_task(async function engagement_other_fakespotAbsent() {
await BrowserTestUtils.withNewTab("about:blank", async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "sponsored",
value: "amp",
});
await selectRowByURL("https://example.com/sponsored");
Assert.ok(
!(await getRowByURL(FAKESPOT_URL)),
"Fakespot row should be absent"
);
Assert.ok(await getRowByURL(AMP_URL), "AMP row should be present");
await selectRowByURL(AMP_URL);
await doEnter();
assertGleanTelemetry("fakespot_engagement", []);
@ -87,14 +92,48 @@ add_task(async function engagement_notFakespot() {
Services.fog.testResetFOG();
});
// Clicks a non-Fakespot suggestion after also matching Fakespot.
add_task(async function engagement_other_fakespotPresent() {
let visitUrl = "https://example.com/some-other-suggestion";
await PlacesTestUtils.addVisits([visitUrl]);
await BrowserTestUtils.withNewTab("about:blank", async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "suggestion",
});
Assert.ok(
await getRowByURL(FAKESPOT_URL),
"Fakespot row should be present"
);
Assert.ok(
await getRowByURL(visitUrl),
"Visit/history row should be present"
);
await selectRowByURL(visitUrl);
await doEnter();
assertGleanTelemetry("fakespot_engagement", []);
});
Services.fog.testResetFOG();
});
// Abandons the search session after after matching Fakespot.
add_task(async function abandonment() {
await BrowserTestUtils.withNewTab("about:blank", async () => {
await UrlbarTestUtils.promiseAutocompleteResultPopup({
window,
value: "maybe",
value: "fakespot",
});
await selectRowByProvider("UrlbarProviderQuickSuggest");
Assert.ok(
await getRowByURL(FAKESPOT_URL),
"Fakespot row should be present"
);
await doBlur();
assertGleanTelemetry("fakespot_engagement", []);
@ -102,3 +141,13 @@ add_task(async function abandonment() {
Services.fog.testResetFOG();
});
async function getRowByURL(url) {
for (let i = 0; i < UrlbarTestUtils.getResultCount(window); i++) {
const detail = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
if (detail.url === url) {
return detail;
}
}
return null;
}

View file

@ -551,10 +551,10 @@ class MockMerinoServer {
provider: "adm",
full_keyword: "amp",
title: "Amp Suggestion",
url: "http://example.com/amp",
url: "https://example.com/amp",
icon: null,
impression_url: "http://example.com/amp-impression",
click_url: "http://example.com/amp-click",
impression_url: "https://example.com/amp-impression",
click_url: "https://example.com/amp-click",
block_id: 1,
advertiser: "Amp",
iab_category: "22 - Shopping",

View file

@ -393,7 +393,7 @@ class _QuickSuggestTestUtils {
*/
ampRemoteSettings({
keywords = ["amp"],
url = "http://example.com/amp",
url = "https://example.com/amp",
title = "Amp Suggestion",
score = 0.3,
} = {}) {
@ -403,8 +403,8 @@ class _QuickSuggestTestUtils {
title,
score,
id: 1,
click_url: "http://example.com/amp-click",
impression_url: "http://example.com/amp-impression",
click_url: "https://example.com/amp-click",
impression_url: "https://example.com/amp-impression",
advertiser: "Amp",
iab_category: "22 - Shopping",
icon: "1234",
@ -420,7 +420,7 @@ class _QuickSuggestTestUtils {
*/
wikipediaRemoteSettings({
keywords = ["wikipedia"],
url = "http://example.com/wikipedia",
url = "https://example.com/wikipedia",
title = "Wikipedia Suggestion",
score = 0.2,
} = {}) {
@ -430,8 +430,8 @@ class _QuickSuggestTestUtils {
title,
score,
id: 2,
click_url: "http://example.com/wikipedia-click",
impression_url: "http://example.com/wikipedia-impression",
click_url: "https://example.com/wikipedia-click",
impression_url: "https://example.com/wikipedia-impression",
advertiser: "Wikipedia",
iab_category: "5 - Education",
icon: "1234",
@ -447,7 +447,7 @@ class _QuickSuggestTestUtils {
*/
amoRemoteSettings({
keywords = ["amo"],
url = "http://example.com/amo",
url = "https://example.com/amo",
title = "Amo Suggestion",
score = 0.2,
} = {}) {
@ -725,8 +725,8 @@ class _QuickSuggestTestUtils {
* substrings. For example:
* ```js
* {
* url: "http://example.com/foo-%YYYYMMDDHH%",
* sponsoredClickUrl: "http://example.com/bar-%YYYYMMDDHH%",
* url: "https://example.com/foo-%YYYYMMDDHH%",
* sponsoredClickUrl: "https://example.com/bar-%YYYYMMDDHH%",
* }
* ```
*/

View file

@ -361,10 +361,10 @@ async function doEngagementWithoutAddingResultToView(
let getPriorityStub = sandbox.stub(UrlbarProviderQuickSuggest, "getPriority");
getPriorityStub.returns(Infinity);
// Spy on `UrlbarProviderQuickSuggest.onLegacyEngagement()`.
let onLegacyEngagementSpy = sandbox.spy(
// Spy on `UrlbarProviderQuickSuggest.onSearchSessionEnd()`.
let onSearchSessionEndSpy = sandbox.spy(
UrlbarProviderQuickSuggest,
"onLegacyEngagement"
"onSearchSessionEnd"
);
let sandboxCleanup = () => {
@ -442,11 +442,8 @@ async function doEngagementWithoutAddingResultToView(
});
await loadPromise;
let engagementCalls = onLegacyEngagementSpy.getCalls().filter(call => {
let state = call.args[0];
return state == "engagement";
});
Assert.equal(engagementCalls.length, 1, "One engagement occurred");
let spyCalls = onSearchSessionEndSpy.getCalls();
Assert.equal(spyCalls.length, 1, "One session occurred");
// Clean up.
resolveQuery();

View file

@ -155,12 +155,12 @@ function makeWikipediaResult({
provider,
keyword = "wikipedia",
title = "Wikipedia Suggestion",
url = "http://example.com/wikipedia",
originalUrl = "http://example.com/wikipedia",
url = "https://example.com/wikipedia",
originalUrl = "https://example.com/wikipedia",
icon = null,
iconBlob = new Blob([new Uint8Array([])]),
impressionUrl = "http://example.com/wikipedia-impression",
clickUrl = "http://example.com/wikipedia-click",
impressionUrl = "https://example.com/wikipedia-impression",
clickUrl = "https://example.com/wikipedia-click",
blockId = 2,
advertiser = "Wikipedia",
iabCategory = "5 - Education",
@ -221,12 +221,12 @@ function makeAmpResult({
provider,
keyword = "amp",
title = "Amp Suggestion",
url = "http://example.com/amp",
originalUrl = "http://example.com/amp",
url = "https://example.com/amp",
originalUrl = "https://example.com/amp",
icon = null,
iconBlob = new Blob([new Uint8Array([])]),
impressionUrl = "http://example.com/amp-impression",
clickUrl = "http://example.com/amp-click",
impressionUrl = "https://example.com/amp-impression",
clickUrl = "https://example.com/amp-click",
blockId = 1,
advertiser = "Amp",
iabCategory = "22 - Shopping",
@ -339,8 +339,8 @@ function makeAmoResult({
provider,
title = "Amo Suggestion",
description = "Amo description",
url = "http://example.com/amo",
originalUrl = "http://example.com/amo",
url = "https://example.com/amo",
originalUrl = "https://example.com/amo",
icon = null,
setUtmParams = true,
}) {
@ -425,7 +425,6 @@ function makeWeatherResult({
forecast: MerinoTestUtils.WEATHER_SUGGESTION.forecast.summary,
high: MerinoTestUtils.WEATHER_SUGGESTION.forecast.high[temperatureUnit],
low: MerinoTestUtils.WEATHER_SUGGESTION.forecast.low[temperatureUnit],
shouldNavigate: true,
},
};

View file

@ -1526,7 +1526,9 @@ add_tasks_with_rust(async function tabToSearch() {
makeSearchResult(context, {
engineName: engine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
engine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -1641,8 +1643,8 @@ add_task(async function rustProviders() {
"suggest.quicksuggest.sponsored": true,
},
expectedUrls: [
"http://example.com/amp",
"http://example.com/wikipedia",
"https://example.com/amp",
"https://example.com/wikipedia",
],
},
{
@ -1650,14 +1652,14 @@ add_task(async function rustProviders() {
"suggest.quicksuggest.nonsponsored": true,
"suggest.quicksuggest.sponsored": false,
},
expectedUrls: ["http://example.com/wikipedia"],
expectedUrls: ["https://example.com/wikipedia"],
},
{
prefs: {
"suggest.quicksuggest.nonsponsored": false,
"suggest.quicksuggest.sponsored": true,
},
expectedUrls: ["http://example.com/amp"],
expectedUrls: ["https://example.com/amp"],
},
{
prefs: {

View file

@ -856,7 +856,6 @@ function makeExpectedResult({
totalReviews,
fakespotGrade,
fakespotProvider,
shouldNavigate: true,
dynamicType: "fakespot",
icon: null,
},

View file

@ -149,16 +149,31 @@ add_task(async function canceledQueries() {
});
function endEngagement({ controller, context = null, state = "engagement" }) {
UrlbarProviderQuickSuggest.onLegacyEngagement(
state,
context ||
createContext("endEngagement", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
}),
{ selIndex: -1 },
controller
);
context ||= createContext("endEngagement", {
providers: [UrlbarProviderQuickSuggest.name],
isPrivate: false,
});
let details = { selIndex: -1, result: { payload: {} } };
switch (state) {
case "engagement":
UrlbarProviderQuickSuggest.onEngagement(context, controller, details);
UrlbarProviderQuickSuggest.onSearchSessionEnd(
context,
controller,
details
);
break;
case "abandonment":
UrlbarProviderQuickSuggest.onSearchSessionEnd(
context,
controller,
details
);
break;
default:
throw new Error("Unrecognized engagement state: " + state);
}
Assert.strictEqual(
gClient.sessionID,

View file

@ -724,24 +724,11 @@ add_tasks_with_rust(async function block() {
let provider = UrlbarProvidersManager.getProvider(result.providerName);
Assert.ok(provider, "Sanity check: Result provider found");
if (result.providerName === "UrlbarProviderQuickSuggest") {
provider.onLegacyEngagement(
"engagement",
context,
{
result,
selType: "dismiss",
selIndex: context.results[0].rowIndex,
},
controller
);
} else {
provider.onEngagement(context, controller, {
result,
selType: "dismiss",
selIndex: context.results[0].rowIndex,
});
}
provider.onEngagement(context, controller, {
result,
selType: "dismiss",
selIndex: context.results[0].rowIndex,
});
Assert.ok(
!UrlbarPrefs.get("suggest.weather"),
"suggest.weather is false after blocking the result"

View file

@ -711,6 +711,9 @@ function makeRemoteTabResult(
* If the window to test is a private window.
* @param {boolean} [options.isPrivateEngine]
* If the engine is a private engine.
* @param {string} [options.searchUrlDomainWithoutSuffix]
* For tab-to-search results, the search engine domain without the public
* suffix.
* @param {number} [options.type]
* The type of the search result. Defaults to UrlbarUtils.RESULT_TYPE.SEARCH.
* @param {number} [options.source]
@ -739,6 +742,7 @@ function makeSearchResult(
providerName,
inPrivateWindow,
isPrivateEngine,
searchUrlDomainWithoutSuffix,
heuristic = false,
trending = false,
isRichSuggestion = false,
@ -786,10 +790,11 @@ function makeSearchResult(
payload.url = uri;
}
if (providerName == "TabToSearch") {
payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
if (payload.url.startsWith("www.")) {
payload.url = payload.url.substring(4);
if (searchUrlDomainWithoutSuffix.startsWith("www.")) {
searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix.substring(4);
}
payload.searchUrlDomainWithoutSuffix = searchUrlDomainWithoutSuffix;
payload.satisfiesAutofillThreshold = satisfiesAutofillThreshold;
payload.isGeneralPurposeEngine = false;
}

View file

@ -106,7 +106,9 @@ add_task(
makeSearchResult(context, {
engineName: engine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
engine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",

View file

@ -144,12 +144,12 @@ function makeAmpResult({
provider,
keyword = "amp",
title = "Amp Suggestion",
url = "http://example.com/amp",
originalUrl = "http://example.com/amp",
url = "https://example.com/amp",
originalUrl = "https://example.com/amp",
icon = null,
iconBlob = new Blob([new Uint8Array([])]),
impressionUrl = "http://example.com/amp-impression",
clickUrl = "http://example.com/amp-click",
impressionUrl = "https://example.com/amp-impression",
clickUrl = "https://example.com/amp-click",
blockId = 1,
advertiser = "Amp",
iabCategory = "22 - Shopping",
@ -209,12 +209,12 @@ function makeWikipediaResult({
provider,
keyword = "wikipedia",
title = "Wikipedia Suggestion",
url = "http://example.com/wikipedia",
originalUrl = "http://example.com/wikipedia",
url = "https://example.com/wikipedia",
originalUrl = "https://example.com/wikipedia",
icon = null,
iconBlob = new Blob([new Uint8Array([])]),
impressionUrl = "http://example.com/wikipedia-impression",
clickUrl = "http://example.com/wikipedia-click",
impressionUrl = "https://example.com/wikipedia-impression",
clickUrl = "https://example.com/wikipedia-click",
blockId = 1,
advertiser = "Wikipedia",
iabCategory = "5 - Education",

View file

@ -49,7 +49,9 @@ add_task(async function basic() {
makeSearchResult(context, {
engineName: testEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
testEngine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -94,7 +96,9 @@ add_task(async function noAutofill() {
makeSearchResult(context, {
engineName: testEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
testEngine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -145,7 +149,9 @@ add_task(async function ignoreWww() {
makeSearchResult(context, {
engineName: testEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
testEngine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -179,7 +185,7 @@ add_task(async function ignoreWww() {
makeSearchResult(context, {
engineName: wwwTestEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
wwwTestEngine.searchUrlDomain
),
providesSearchMode: true,
@ -207,7 +213,7 @@ add_task(async function ignoreWww() {
makeSearchResult(context, {
engineName: wwwTestEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
wwwTestEngine.searchUrlDomain
),
providesSearchMode: true,
@ -266,7 +272,7 @@ add_task(async function conflictingEngines() {
makeSearchResult(context, {
engineName: fooTestEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
fooTestEngine.searchUrlDomain
),
providesSearchMode: true,
@ -298,7 +304,7 @@ add_task(async function conflictingEngines() {
makeSearchResult(context, {
engineName: fooBarTestEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
fooBarTestEngine.searchUrlDomain
),
providesSearchMode: true,
@ -361,7 +367,9 @@ add_task(async function multipleEnginesForHostname() {
makeSearchResult(context, {
engineName: testEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
testEngine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -396,7 +404,9 @@ add_task(async function test_casing() {
makeSearchResult(context, {
engineName: testEngine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(testEngine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
testEngine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -430,7 +440,9 @@ add_task(async function test_publicSuffix() {
makeSearchResult(context, {
engineName: engine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
engine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -505,7 +517,9 @@ add_task(async function test_disabledEngine() {
makeSearchResult(context, {
engineName: engine.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: UrlbarUtils.stripPublicSuffixFromHost(engine.searchUrlDomain),
searchUrlDomainWithoutSuffix: UrlbarUtils.stripPublicSuffixFromHost(
engine.searchUrlDomain
),
providesSearchMode: true,
query: "",
providerName: "TabToSearch",

View file

@ -71,7 +71,7 @@ add_task(async function test() {
makeSearchResult(context, {
engineName: "TestEngine",
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: "en.example.",
searchUrlDomainWithoutSuffix: "en.example.",
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -114,7 +114,7 @@ add_task(async function test() {
makeSearchResult(context, {
engineName: engine2.name,
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: "www.it.mochi.",
searchUrlDomainWithoutSuffix: "www.it.mochi.",
providesSearchMode: true,
query: "",
providerName: "TabToSearch",
@ -162,7 +162,7 @@ add_task(async function test() {
makeSearchResult(context, {
engineName: "TestEngine3",
engineIconUri: UrlbarUtils.ICON.SEARCH_GLASS,
uri: "search.foo.",
searchUrlDomainWithoutSuffix: "search.foo.",
providesSearchMode: true,
query: "",
providerName: "TabToSearch",

View file

@ -414,6 +414,7 @@ newtab-topic-label-society = Life Hacks
newtab-topic-label-sports = Sports
newtab-topic-label-tech = Tech
newtab-topic-label-travel = Travel
newtab-topic-label-home = Home & Garden
## Topic Selection Modal

View file

@ -16,7 +16,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"af": {
"pin": false,
@ -35,7 +35,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"an": {
"pin": false,
@ -54,7 +54,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ar": {
"pin": false,
@ -73,7 +73,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ast": {
"pin": false,
@ -92,7 +92,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"az": {
"pin": false,
@ -111,7 +111,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"be": {
"pin": false,
@ -130,7 +130,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"bg": {
"pin": false,
@ -149,7 +149,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"bn": {
"pin": false,
@ -168,7 +168,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"bo": {
"pin": false,
@ -187,7 +187,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"br": {
"pin": false,
@ -206,7 +206,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"brx": {
"pin": false,
@ -225,7 +225,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"bs": {
"pin": false,
@ -244,7 +244,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ca": {
"pin": false,
@ -263,7 +263,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ca-valencia": {
"pin": false,
@ -282,7 +282,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"cak": {
"pin": false,
@ -301,7 +301,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ckb": {
"pin": false,
@ -320,7 +320,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"cs": {
"pin": false,
@ -339,7 +339,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"cy": {
"pin": false,
@ -358,7 +358,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"da": {
"pin": false,
@ -377,7 +377,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"de": {
"pin": false,
@ -396,7 +396,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"dsb": {
"pin": false,
@ -415,7 +415,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"el": {
"pin": false,
@ -434,7 +434,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"en-CA": {
"pin": false,
@ -453,7 +453,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"en-GB": {
"pin": false,
@ -472,7 +472,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"eo": {
"pin": false,
@ -491,7 +491,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"es-AR": {
"pin": false,
@ -510,7 +510,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"es-CL": {
"pin": false,
@ -529,7 +529,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"es-ES": {
"pin": false,
@ -548,7 +548,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"es-MX": {
"pin": false,
@ -567,7 +567,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"et": {
"pin": false,
@ -586,7 +586,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"eu": {
"pin": false,
@ -605,7 +605,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"fa": {
"pin": false,
@ -624,7 +624,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ff": {
"pin": false,
@ -643,7 +643,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"fi": {
"pin": false,
@ -662,7 +662,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"fr": {
"pin": false,
@ -681,7 +681,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"fur": {
"pin": false,
@ -700,7 +700,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"fy-NL": {
"pin": false,
@ -719,7 +719,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ga-IE": {
"pin": false,
@ -738,7 +738,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"gd": {
"pin": false,
@ -757,7 +757,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"gl": {
"pin": false,
@ -776,7 +776,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"gn": {
"pin": false,
@ -795,7 +795,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"gu-IN": {
"pin": false,
@ -814,7 +814,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"he": {
"pin": false,
@ -833,7 +833,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hi-IN": {
"pin": false,
@ -852,7 +852,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hr": {
"pin": false,
@ -871,7 +871,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hsb": {
"pin": false,
@ -890,7 +890,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hu": {
"pin": false,
@ -909,7 +909,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hy-AM": {
"pin": false,
@ -928,7 +928,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"hye": {
"pin": false,
@ -947,7 +947,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ia": {
"pin": false,
@ -966,7 +966,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"id": {
"pin": false,
@ -985,7 +985,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"is": {
"pin": false,
@ -1004,7 +1004,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"it": {
"pin": false,
@ -1023,7 +1023,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ja": {
"pin": false,
@ -1040,7 +1040,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ja-JP-mac": {
"pin": false,
@ -1048,7 +1048,7 @@
"macosx64",
"macosx64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ka": {
"pin": false,
@ -1067,7 +1067,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"kab": {
"pin": false,
@ -1086,7 +1086,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"kk": {
"pin": false,
@ -1105,7 +1105,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"km": {
"pin": false,
@ -1124,7 +1124,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"kn": {
"pin": false,
@ -1143,7 +1143,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ko": {
"pin": false,
@ -1162,7 +1162,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"lij": {
"pin": false,
@ -1181,7 +1181,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"lo": {
"pin": false,
@ -1200,7 +1200,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"lt": {
"pin": false,
@ -1219,7 +1219,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ltg": {
"pin": false,
@ -1238,7 +1238,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"lv": {
"pin": false,
@ -1257,7 +1257,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"meh": {
"pin": false,
@ -1276,7 +1276,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"mk": {
"pin": false,
@ -1295,7 +1295,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"mr": {
"pin": false,
@ -1314,7 +1314,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ms": {
"pin": false,
@ -1333,7 +1333,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"my": {
"pin": false,
@ -1352,7 +1352,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"nb-NO": {
"pin": false,
@ -1371,7 +1371,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ne-NP": {
"pin": false,
@ -1390,7 +1390,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"nl": {
"pin": false,
@ -1409,7 +1409,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"nn-NO": {
"pin": false,
@ -1428,7 +1428,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"oc": {
"pin": false,
@ -1447,7 +1447,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"pa-IN": {
"pin": false,
@ -1466,7 +1466,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"pl": {
"pin": false,
@ -1485,7 +1485,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"pt-BR": {
"pin": false,
@ -1504,7 +1504,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"pt-PT": {
"pin": false,
@ -1523,7 +1523,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"rm": {
"pin": false,
@ -1542,7 +1542,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ro": {
"pin": false,
@ -1561,7 +1561,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ru": {
"pin": false,
@ -1580,7 +1580,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sat": {
"pin": false,
@ -1599,7 +1599,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sc": {
"pin": false,
@ -1618,7 +1618,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"scn": {
"pin": false,
@ -1637,7 +1637,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sco": {
"pin": false,
@ -1656,7 +1656,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"si": {
"pin": false,
@ -1675,7 +1675,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sk": {
"pin": false,
@ -1694,7 +1694,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"skr": {
"pin": false,
@ -1713,7 +1713,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sl": {
"pin": false,
@ -1732,7 +1732,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"son": {
"pin": false,
@ -1751,7 +1751,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sq": {
"pin": false,
@ -1770,7 +1770,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sr": {
"pin": false,
@ -1789,7 +1789,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"sv-SE": {
"pin": false,
@ -1808,7 +1808,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"szl": {
"pin": false,
@ -1827,7 +1827,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ta": {
"pin": false,
@ -1846,7 +1846,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"te": {
"pin": false,
@ -1865,7 +1865,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"tg": {
"pin": false,
@ -1884,7 +1884,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"th": {
"pin": false,
@ -1903,7 +1903,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"tl": {
"pin": false,
@ -1922,7 +1922,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"tr": {
"pin": false,
@ -1941,7 +1941,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"trs": {
"pin": false,
@ -1960,7 +1960,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"uk": {
"pin": false,
@ -1979,7 +1979,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"ur": {
"pin": false,
@ -1998,7 +1998,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"uz": {
"pin": false,
@ -2017,7 +2017,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"vi": {
"pin": false,
@ -2036,7 +2036,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"wo": {
"pin": false,
@ -2055,7 +2055,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"xh": {
"pin": false,
@ -2074,7 +2074,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"zh-CN": {
"pin": false,
@ -2093,7 +2093,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
},
"zh-TW": {
"pin": false,
@ -2112,6 +2112,6 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "163c5ee9ce09176092a5e1290a8f5134bc7590b8"
"revision": "c923bb54ed924cf5d23635b623e6f43c2951ccce"
}
}

View file

@ -45,9 +45,6 @@ DEFINES["APP_VERSION"] = CONFIG["MOZ_APP_VERSION"]
for cdm in CONFIG["MOZ_EME_MODULES"]:
DEFINES["MOZ_%s_EME" % cdm.upper()] = True
if CONFIG["MOZ_GPSD"]:
DEFINES["MOZ_GPSD"] = True
if CONFIG["MOZ_UPDATE_AGENT"]:
DEFINES["MOZ_UPDATE_AGENT"] = True

View file

@ -29,6 +29,14 @@
}
}
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "browser.urlbar.searchModeSwitcher.featureGate") or (-moz-bool-pref: "browser.urlbar.scotchBonnet.enableOverride") {
#identity-box[pageproxystate="invalid"],
#identity-box.chromeUI {
display: none;
}
}
.identity-box-button {
padding-inline: var(--urlbar-icon-padding);
border-radius: var(--urlbar-icon-border-radius);

View file

@ -1079,18 +1079,26 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
}
#urlbar-searchmode-switcher {
/* Hidden until urlbar opens */
display: none;
margin-inline-end: var(--urlbar-icon-padding);
background-color: var(--toolbar-field-background-color);
border-radius: var(--toolbarbutton-border-radius);
margin-inline-end: var(--urlbar-icon-padding);
padding-block: 0;
&:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-inset);
}
#urlbar[searchmode] & {
margin-inline-end: 0;
border-start-end-radius: 0;
border-end-end-radius: 0;
}
/* stylelint-disable-next-line media-query-no-invalid */
@media (-moz-bool-pref: "browser.urlbar.searchModeSwitcher.featureGate") or (-moz-bool-pref: "browser.urlbar.scotchBonnet.enableOverride") {
/* Don't show on mousedown to avoid shifting input field text */
#urlbar[focused]:not([focusing-via-mousedown]) & {
display: flex;
}
display: flex;
}
}
@ -1103,8 +1111,22 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
fill: var(--text-color-deemphasized);
}
#searchmode-switcher-chicklet {
background-color: var(--toolbar-field-background-color);
border-start-end-radius: var(--toolbarbutton-border-radius);
border-end-end-radius: var(--toolbarbutton-border-radius);
align-items: center;
margin-inline-end: var(--urlbar-icon-padding);
display: none;
#urlbar[searchmode] & {
display: inline-flex;
}
}
#searchmode-switcher-title {
margin-inline-start: 0px;
margin-inline-start: 0;
align-items: center;
&:empty {
display: none;
@ -1114,7 +1136,6 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
#searchmode-switcher-close {
list-style-image: url("chrome://global/skin/icons/close-12.svg");
pointer-events: all;
display: none;
-moz-context-properties: fill;
fill: var(--text-color-deemphasized);
@ -1132,13 +1153,9 @@ moz-input-box > menupopup .context-menu-add-engine > .menu-iconic-left::after {
}
}
#searchmode-switcher-popup {
--panel-padding: var(--space-small);
--arrowpanel-menuitem-margin: 0;
}
#searchmode-switcher-popup-description {
margin: var(--space-small);
margin-bottom: var(--space-xsmall);
}
#searchmode-switcher-popup-search-settings-icon {

View file

@ -316,7 +316,6 @@ system_headers = [
"gmodule.h",
"gnome.h",
"gnu/libc-version.h",
"gps.h",
"grp.h",
"gssapi_generic.h",
"gssapi/gssapi_generic.h",

View file

@ -49,6 +49,7 @@ if (isNode()) {
pref("devtools.debugger.javascript-tracing-on-next-interaction", false);
pref("devtools.debugger.javascript-tracing-on-next-load", false);
pref("devtools.debugger.javascript-tracing-function-return", false);
pref("devtools.debugger.javascript-tracing-native", false);
pref("devtools.debugger.hide-ignored-sources", false);
pref("devtools.debugger.source-map-ignore-list-enabled", true);
pref("devtools.debugger.features.wasm", true);

View file

@ -4,6 +4,8 @@
"use strict";
const FLASH_TARGETS_SELECTOR = "[class*=theme-fg-color], .force-color-on-flash";
/**
* Apply a 'flashed' background and foreground color to elements. Intended
* to be used with flashElementOff as a way of drawing attention to an element.
@ -32,10 +34,9 @@ function flashElementOn(
backgroundElt.classList.add(backgroundClass);
foregroundElt.classList.add("theme-fg-contrast");
[].forEach.call(
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.add("theme-fg-contrast")
);
foregroundElt
.querySelectorAll(FLASH_TARGETS_SELECTOR)
.forEach(span => span.classList.add("theme-fg-contrast"));
}
/**
@ -68,10 +69,9 @@ function flashElementOff(
foregroundElt.classList.remove("theme-fg-contrast");
// Make sure the foreground animation class is removed
foregroundElt.classList.remove("flash-out");
[].forEach.call(
foregroundElt.querySelectorAll("[class*=theme-fg-color]"),
span => span.classList.remove("theme-fg-contrast")
);
foregroundElt
.querySelectorAll(FLASH_TARGETS_SELECTOR)
.forEach(span => span.classList.remove("theme-fg-contrast"));
}
/**

View file

@ -161,7 +161,7 @@ ElementEditor.prototype = {
this.elt.appendChild(open);
this.tag = this.doc.createElement("span");
this.tag.classList.add("tag", "theme-fg-color3");
this.tag.classList.add("tag", "force-color-on-flash");
this.tag.setAttribute("tabindex", "-1");
this.tag.textContent = this.node.displayName;
open.appendChild(this.tag);
@ -194,6 +194,7 @@ ElementEditor.prototype = {
this.newAttr.editMode = editableField({
element: this.newAttr,
multiline: true,
inputClass: "newattr-input",
maxWidth: () => getAutocompleteMaxWidth(this.newAttr, this.container.elt),
trigger: "dblclick",
stopOnReturn: true,
@ -234,7 +235,7 @@ ElementEditor.prototype = {
this.elt.appendChild(close);
this.closeTag = this.doc.createElement("span");
this.closeTag.classList.add("tag", "theme-fg-color3");
this.closeTag.classList.add("tag");
this.closeTag.textContent = this.node.displayName;
close.appendChild(this.closeTag);
@ -699,9 +700,9 @@ ElementEditor.prototype = {
* " ",
* dom.span(
* { className: "editable", tabIndex: 0 },
* dom.span({ className: "attr-name theme-fg-color1" }, attribute.name),
* dom.span({ className: "attr-name" }, attribute.name),
* '="',
* dom.span({ className: "attr-value theme-fg-color2" }, attribute.value),
* dom.span({ className: "attr-value" }, attribute.value),
* '"'
* )
*/
@ -720,16 +721,14 @@ ElementEditor.prototype = {
attr.appendChild(inner);
const name = this.doc.createElement("span");
name.classList.add("attr-name");
name.classList.add("theme-fg-color1");
name.classList.add("attr-name", "force-color-on-flash");
name.textContent = attribute.name;
inner.appendChild(name);
inner.appendChild(this.doc.createTextNode('="'));
const val = this.doc.createElement("span");
val.classList.add("attr-value");
val.classList.add("theme-fg-color2");
val.classList.add("attr-value", "force-color-on-flash");
inner.appendChild(val);
inner.appendChild(this.doc.createTextNode('"'));

View file

@ -15,7 +15,7 @@ function ReadOnlyEditor(container, node) {
this.buildMarkup();
if (node.isPseudoElement) {
this.tag.classList.add("theme-fg-color3");
this.tag.classList.add("pseudo", "force-color-on-flash");
if (node.isMarkerPseudoElement) {
this.tag.textContent = "::marker";
} else if (node.isBeforePseudoElement) {

View file

@ -56,6 +56,7 @@ class Rule {
this.pseudoElement = options.pseudoElement || "";
this.isSystem = options.isSystem;
this.isUnmatched = options.isUnmatched || false;
this.darkColorScheme = options.darkColorScheme;
this.inherited = options.inherited || null;
this.keyframes = options.keyframes || null;
this.userAdded = options.rule.userAdded;
@ -637,6 +638,8 @@ class Rule {
*/
refresh(options) {
this.matchedSelectorIndexes = options.matchedSelectorIndexes || [];
const colorSchemeChanged = this.darkColorScheme !== options.darkColorScheme;
this.darkColorScheme = options.darkColorScheme;
const newTextProps = this._getTextProperties();
@ -679,6 +682,17 @@ class Rule {
} else {
delete prop._visited;
}
// Valid properties that aren't disabled might need to get updated in some condition
if (
prop.enabled &&
prop.isValid() &&
// Update if it's using light-dark and the color scheme changed
colorSchemeChanged &&
prop.value.includes("light-dark")
) {
prop.updateEditor();
}
}
// Add brand new properties.

View file

@ -193,6 +193,8 @@ skip-if = [
["browser_rules_layer.js"]
["browser_rules_light_dark.js"]
["browser_rules_lineNumbers.js"]
["browser_rules_linear-easing-swatch.js"]

View file

@ -0,0 +1,157 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test for light-dark() in rule view.
const TEST_URI = `data:text/html,<meta charset=utf8>
<style>
@media not (prefers-color-scheme: dark) {
html {
color-scheme: light;
}
}
@media (prefers-color-scheme: dark) {
html {
color-scheme: dark;
}
}
h1 {
background-color: light-dark(gold, tomato);
}
.light {
color-scheme: light;
color: light-dark(red, blue);
border-color: light-dark(pink, cyan);
}
.dark {
color-scheme: dark;
background: linear-gradient(
light-dark(blue, darkblue),
light-dark(red, darkred)
);
}
</style>
<h1>Hello</h1>
<div class="light">
<pre>color-scheme: light</pre>
<div class="dark">
<pre>color-scheme: dark</pre>
</div>
</div>`;
add_task(async function () {
await addTab(TEST_URI);
const { inspector, view } = await openRuleView();
info("Select node with color-scheme: light");
await selectNode(".light", inspector);
// `color: light-dark(red, blue)`
assertColorSpans(view, ".light", "color", [
{ color: "red", matched: true },
{ color: "blue", matched: false },
]);
// `border-color: light-dark(pink, cyan)`
assertColorSpans(view, ".light", "border-color", [
{ color: "pink", matched: true },
{ color: "cyan", matched: false },
]);
info("Select node with color-scheme: dark");
await selectNode(".dark", inspector);
// `background: linear-gradient(light-dark(blue, darkblue),light-dark(red, darkred))`
assertColorSpans(view, ".dark", "background", [
{ color: "blue", matched: false },
{ color: "darkblue", matched: true },
{ color: "red", matched: false },
{ color: "darkred", matched: true },
]);
// We show the inherited rule for color, which is matched by `.light`, so here, even if
// the node has a dark color scheme, light-dark should mark second parameters as unmatched
// `color: light-dark(red, blue)`
assertColorSpans(view, ".light", "color", [
{ color: "red", matched: true },
{ color: "blue", matched: false },
]);
info("Select node without explicit color-scheme property");
// h1 has no color-scheme so it inherits from the html one, which depends on the actual
// media query value. light-dark in the associated rule should be updated when toggling
// simulation.
await selectNode("h1", inspector);
// Read the color scheme to know if we should click on the light or dark button
// to trigger a change.
info("Enable light mode simulation if needed");
const isDarkScheme = await SpecialPowers.spawn(
gBrowser.selectedBrowser,
[],
() => {
return content.matchMedia("(prefers-color-scheme: dark)").matches;
}
);
if (isDarkScheme) {
const onRuleViewRefreshed = view.once("ruleview-refreshed");
inspector.panelDoc
.querySelector("#color-scheme-simulation-light-toggle")
.click();
await onRuleViewRefreshed;
}
// `background-color: light-dark(gold, tomato)`
assertColorSpans(view, "h1", "background-color", [
{ color: "gold", matched: true },
{ color: "tomato", matched: false },
]);
info("Trigger dark mode simulation");
const onRuleViewRefreshed = view.once("ruleview-refreshed");
inspector.panelDoc
.querySelector("#color-scheme-simulation-dark-toggle")
.click();
await onRuleViewRefreshed;
// `background-color: light-dark(gold, tomato)`
assertColorSpans(view, "h1", "background-color", [
{ color: "gold", matched: false },
{ color: "tomato", matched: true },
]);
});
function assertColorSpans(view, ruleSelector, propertyName, expectedColors) {
const colorSpans = getRuleViewProperty(
view,
ruleSelector,
propertyName
).valueSpan.querySelectorAll("span:has(> .ruleview-color)");
is(
colorSpans.length,
expectedColors.length,
"Got expected number of color spans"
);
for (let i = 0; i < expectedColors.length; i++) {
const colorSpan = colorSpans[i];
const expectedColorData = expectedColors[i];
is(
colorSpan.textContent,
expectedColorData.color,
`Expected color ${i} for ${ruleSelector} ${propertyName}`
);
is(
!colorSpan.classList.contains("ruleview-unmatched"),
expectedColorData.matched,
`Color ${i} for ${ruleSelector} ${propertyName} is ${
expectedColorData.matched ? "matched" : "unmatched"
}`
);
}
}

View file

@ -614,6 +614,10 @@ TextPropertyEditor.prototype = {
),
inStartingStyleRule: this.rule.isInStartingStyle(),
};
if (this.rule.darkColorScheme !== undefined) {
parserOptions.isDarkColorScheme = this.rule.darkColorScheme;
}
const frag = outputParser.parseCssProperty(name, val, parserOptions);
// Save the initial value as the last committed value,

View file

@ -96,6 +96,8 @@ function isKeyIn(key, ...keys) {
* @param {Object} options: Options for the editable field
* @param {Element} options.element:
* (required) The span to be edited on focus.
* @param {String} options.inputClass:
* An optional class to be added to the input.
* @param {Function} options.canEdit:
* Will be called before creating the inplace editor. Editor
* won't be created if canEdit returns false.
@ -246,6 +248,8 @@ function editableItem(options, callback) {
// Mark the element editable field for tab
// navigation while editing.
element._editable = true;
// And an attribute that can be used to target
element.setAttribute("editable", "");
// Save the trigger type so we can dispatch this later
element._trigger = trigger;
@ -423,6 +427,8 @@ class InplaceEditor extends EventEmitter {
* Optional aria-label attribute value that will be added to the input.
* @param {String} options.inputAriaLabelledBy
* Optional aria-labelledby attribute value that will be added to the input.
* @param {String} options.inputClass:
* Optional class to be added to the input.
*/
#createInput(options = {}) {
this.input = this.doc.createElementNS(
@ -440,6 +446,9 @@ class InplaceEditor extends EventEmitter {
}
this.input.classList.add("styleinspector-propertyeditor");
if (options.inputClass) {
this.input.classList.add(options.inputClass);
}
this.input.value = this.initial;
if (options.inputAriaLabel) {
this.input.setAttribute("aria-label", options.inputAriaLabel);

View file

@ -124,6 +124,7 @@ class OutputParser {
#cssProperties;
#doc;
#parsed = [];
#stack = [];
/**
* Parse a CSS property value given a property name.
@ -221,6 +222,7 @@ class OutputParser {
} else if (token.tokenType === "ParenthesisBlock") {
++depth;
} else if (token.tokenType === "CloseParenthesis") {
this.#onCloseParenthesis(options);
--depth;
if (depth === 0) {
break;
@ -376,9 +378,12 @@ class OutputParser {
const subOptions = Object.assign({}, options);
subOptions.expectFilter = false;
const saveParsed = this.#parsed;
const savedStack = this.#stack;
this.#parsed = [];
this.#stack = [];
const rest = this.#doParse(text, subOptions, tokenStream, true);
this.#parsed = saveParsed;
this.#stack = savedStack;
const span = this.#createNode("span", secondOpts);
span.appendChild(rest);
@ -412,18 +417,15 @@ class OutputParser {
*/
// eslint-disable-next-line complexity
#doParse(text, options, tokenStream, stopAtCloseParen) {
let parenDepth = stopAtCloseParen ? 1 : 0;
let outerMostFunctionTakesColor = false;
const colorFunctions = [];
let fontFamilyNameParts = [];
let previousWasBang = false;
const colorOK = function () {
const colorOK = () => {
return (
options.supportsColor ||
(options.expectFilter &&
parenDepth === 1 &&
outerMostFunctionTakesColor)
this.#stack.length !== 0 &&
this.#stack.at(-1).isColorTakingFunction)
);
};
@ -455,6 +457,18 @@ class OutputParser {
const isColorTakingFunction = COLOR_TAKING_FUNCTIONS.has(
lowerCaseFunctionName
);
this.#stack.push({
lowerCaseFunctionName,
functionName,
isColorTakingFunction,
// The position of the function separators ("," or "/") in the `parts` property
separatorIndexes: [],
// The parsed parts of the function that will be rendered on screen.
// This can hold both simple strings and DOMNodes.
parts: [],
});
if (
isColorTakingFunction ||
ANGLE_TAKING_FUNCTIONS.has(lowerCaseFunctionName)
@ -466,13 +480,6 @@ class OutputParser {
this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
);
if (parenDepth === 0) {
outerMostFunctionTakesColor = isColorTakingFunction;
}
if (isColorTakingFunction) {
colorFunctions.push({ parenDepth, functionName });
}
++parenDepth;
} else if (
lowerCaseFunctionName === "var" &&
options.getVariableData
@ -483,14 +490,18 @@ class OutputParser {
tokenStream,
options
);
if (value && colorOK() && InspectorUtils.isValidCSSColor(value)) {
const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
);
this.#appendColor(value, {
...options,
variableContainer: variableNode,
colorFunction: colorFunctions.at(-1)?.functionName,
colorFunction: colorFunctionEntry?.functionName,
});
} else {
this.#parsed.push(variableNode);
this.#append(variableNode);
}
} else {
const {
@ -517,9 +528,13 @@ class OutputParser {
colorOK() &&
InspectorUtils.isValidCSSColor(computedFunctionText)
) {
const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
);
this.#appendColor(computedFunctionText, {
...options,
colorFunction: colorFunctions.at(-1)?.functionName,
colorFunction: colorFunctionEntry?.functionName,
valueParts: [
functionName,
"(",
@ -535,7 +550,7 @@ class OutputParser {
if (typeof data === "string") {
this.#appendTextNode(data);
} else if (data) {
this.#parsed.push(data.node);
this.#append(data.node);
}
}
this.#appendTextNode(")");
@ -583,9 +598,12 @@ class OutputParser {
colorOK() &&
InspectorUtils.isValidCSSColor(functionText)
) {
const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
);
this.#appendColor(functionText, {
...options,
colorFunction: colorFunctions.at(-1)?.functionName,
colorFunction: colorFunctionEntry?.functionName,
});
} else if (
options.expectShape &&
@ -622,9 +640,12 @@ class OutputParser {
options.gridClass
);
} else if (colorOK() && InspectorUtils.isValidCSSColor(token.text)) {
const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
);
this.#appendColor(token.text, {
...options,
colorFunction: colorFunctions.at(-1)?.functionName,
colorFunction: colorFunctionEntry?.functionName,
});
} else if (angleOK(token.text)) {
this.#appendAngle(token.text, options);
@ -649,9 +670,12 @@ class OutputParser {
// color is changed to something like rgb(...).
this.#appendTextNode(" ");
}
const colorFunctionEntry = this.#stack.findLast(
entry => entry.isColorTakingFunction
);
this.#appendColor(original, {
...options,
colorFunction: colorFunctions.at(-1)?.functionName,
colorFunction: colorFunctionEntry?.functionName,
});
} else {
this.#appendTextNode(original);
@ -698,27 +722,23 @@ class OutputParser {
break;
case "ParenthesisBlock":
++parenDepth;
this.#stack.push({
isParenthesis: true,
separatorIndexes: [],
});
this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
);
break;
case "CloseParenthesis":
--parenDepth;
this.#onCloseParenthesis(options);
if (colorFunctions.at(-1)?.parenDepth == parenDepth) {
colorFunctions.pop();
}
if (stopAtCloseParen && parenDepth === 0) {
if (stopAtCloseParen && this.#stack.length === 0) {
done = true;
break;
}
if (parenDepth === 0) {
outerMostFunctionTakesColor = false;
}
this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
);
@ -735,6 +755,14 @@ class OutputParser {
fontFamilyNameParts = [];
}
// Add separator for the current function
if (this.#stack.length) {
this.#appendTextNode(token.text);
const entry = this.#stack.at(-1);
entry.separatorIndexes.push(entry.parts.length - 1);
break;
}
// falls through
default:
this.#appendTextNode(
@ -761,6 +789,15 @@ class OutputParser {
this.#appendFontFamily(fontFamilyNameParts.join(""), options);
}
// We might never encounter a matching closing parenthesis for a function and still
// have a "valid" value (e.g. `background: linear-gradient(90deg, red, blue"`)
// In such case, go through the stack and handle each items until we have nothing left.
if (this.#stack.length) {
while (this.#stack.length !== 0) {
this.#onCloseParenthesis(options);
}
}
let result = this.#toDOM();
if (options.expectFilter && !options.filterSwatch) {
@ -770,6 +807,111 @@ class OutputParser {
return result;
}
#onCloseParenthesis(options) {
if (!this.#stack.length) {
return;
}
const stackEntry = this.#stack.at(-1);
if (
stackEntry.lowerCaseFunctionName === "light-dark" &&
typeof options.isDarkColorScheme === "boolean" &&
// light-dark takes exactly two parameters, so if we don't get exactly 1 separator
// at this point, that means that the value is valid at parse time, but is invalid
// at computed value time.
// TODO: We might want to add a class to indicate that this is invalid at computed
// value time (See Bug 1910845)
stackEntry.separatorIndexes.length === 1
) {
const stackEntryParts = this.#getCurrentStackParts();
const separatorIndex = stackEntry.separatorIndexes[0];
let startIndex;
let endIndex;
if (options.isDarkColorScheme) {
// If we're using a dark color scheme, we want to mark the first param as
// not used.
// The first "part" is `light-dark(`, so we can start after that.
// We want to filter out white space character before the first parameter
for (startIndex = 1; startIndex < separatorIndex; startIndex++) {
const part = stackEntryParts[startIndex];
if (typeof part !== "string" || part.trim() !== "") {
break;
}
}
// same for the end of the parameter, we want to filter out whitespaces
// after the parameter and before the comma
for (
endIndex = separatorIndex - 1;
endIndex >= startIndex;
endIndex--
) {
const part = stackEntryParts[endIndex];
if (typeof part !== "string" || part.trim() !== "") {
// We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex++;
break;
}
}
} else {
// If we're not using a dark color scheme, we want to mark the second param as
// not used.
// We want to filter out white space character after the comma and before the
// second parameter
for (
startIndex = separatorIndex + 1;
startIndex < stackEntryParts.length;
startIndex++
) {
const part = stackEntryParts[startIndex];
if (typeof part !== "string" || part.trim() !== "") {
break;
}
}
// same for the end of the parameter, we want to filter out whitespaces
// after the parameter and before the closing parenthesis (which is not yet
// included in stackEntryParts)
for (
endIndex = stackEntryParts.length - 1;
endIndex > separatorIndex;
endIndex--
) {
const part = stackEntryParts[endIndex];
if (typeof part !== "string" || part.trim() !== "") {
// We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex++;
break;
}
}
}
const parts = stackEntryParts.slice(startIndex, endIndex);
// If the item we need to mark is already an element (e.g. a parsed color),
// just add a class to it.
if (parts.length === 1 && Element.isInstance(parts[0])) {
parts[0].classList.add(options.unmatchedClass);
} else {
// Otherwise, we need to wrap our parts into a specific element so we can
// style them
const node = this.#createNode("span", {
class: options.unmatchedClass,
});
node.append(...parts);
stackEntryParts.splice(startIndex, parts.length, node);
}
}
// Our job is done here, pop last stack entry
const { parts } = this.#stack.pop();
// Put all the parts in the "new" last stack, or the main parsed array if there
// is no more entry in the stack
this.#getCurrentStackParts().push(...parts);
}
/**
* Parse a string.
*
@ -784,6 +926,7 @@ class OutputParser {
#parse(text, options = {}) {
text = text.trim();
this.#parsed.length = 0;
this.#stack.length = 0;
const tokenStream = new InspectorCSSParserWrapper(text);
return this.#doParse(text, options, tokenStream, false);
@ -855,7 +998,7 @@ class OutputParser {
);
container.appendChild(value);
this.#parsed.push(container);
this.#append(container);
}
#appendLinear(text, options) {
@ -882,7 +1025,7 @@ class OutputParser {
);
container.appendChild(value);
this.#parsed.push(container);
this.#append(container);
}
/**
@ -907,7 +1050,7 @@ class OutputParser {
const value = this.#createNode("span", {}, text);
container.append(value);
this.#parsed.push(container);
this.#append(container);
}
/**
@ -967,7 +1110,7 @@ class OutputParser {
}
}
this.#parsed.push(container);
this.#append(container);
}
/**
@ -1642,7 +1785,7 @@ class OutputParser {
);
container.appendChild(value);
this.#parsed.push(container);
this.#append(container);
}
/**
@ -1745,7 +1888,7 @@ class OutputParser {
container.appendChild(value);
}
this.#parsed.push(container);
this.#append(container);
} else {
this.#appendTextNode(color);
}
@ -1980,7 +2123,7 @@ class OutputParser {
}
/**
* Append a node to the output.
* Create and append a node to the output.
*
* @param {String} tagName
* Tag type e.g. "div"
@ -1996,7 +2139,16 @@ class OutputParser {
node.classList.add(TRUNCATE_NODE_CLASSNAME);
}
this.#parsed.push(node);
this.#append(node);
}
/**
* Append an element or a text node to the output.
*
* @param {DOMNode|String} item
*/
#append(item) {
this.#getCurrentStackParts().push(item);
}
/**
@ -2007,18 +2159,19 @@ class OutputParser {
* Text to append
*/
#appendTextNode(text) {
const lastItem = this.#parsed[this.#parsed.length - 1];
if (text.length > TRUNCATE_LENGTH_THRESHOLD) {
// If the text is too long, force creating a node, which will add the
// necessary classname to truncate the property correctly.
this.#appendNode("span", {}, text);
} else if (typeof lastItem === "string") {
this.#parsed[this.#parsed.length - 1] = lastItem + text;
} else {
this.#parsed.push(text);
this.#append(text);
}
}
#getCurrentStackParts() {
return this.#stack.at(-1)?.parts || this.#parsed;
}
/**
* Take all output and append it into a single DocumentFragment.
*
@ -2037,6 +2190,7 @@ class OutputParser {
}
this.#parsed.length = 0;
this.#stack.length = 0;
return frag;
}
@ -2077,6 +2231,7 @@ class OutputParser {
* - {RegisteredPropertyResource|undefined} registeredProperty: The registered
* property data (syntax, initial value, inherits). Undefined if the variable
* is not a registered property.
* @param {Boolean} overrides.isDarkColorScheme: Is the currently applied color scheme dark.
* @return {Object} Overridden options object
*/
#mergeOptions(overrides) {
@ -2101,6 +2256,7 @@ class OutputParser {
getVariableData: null,
unmatchedClass: null,
inStartingStyleRule: false,
isDarkColorScheme: null,
};
for (const item in overrides) {

View file

@ -18,7 +18,7 @@ add_task(async function () {
await testAdvanceCharCommit(doc);
await testAdvanceCharsFunction(doc);
await testEscapeCancel(doc);
await testInputAriaLabel(doc);
await testInputAttributes(doc);
host.destroy();
gBrowser.removeCurrentTab();
@ -155,7 +155,7 @@ function testEscapeCancel(doc) {
});
}
function testInputAriaLabel(doc) {
function testInputAttributes(doc) {
info("Testing that inputAriaLabel works as expected");
doc.body.innerHTML = "";
@ -190,6 +190,22 @@ function testInputAriaLabel(doc) {
"TEST_ARIA_LABELLED_BY",
"Input has expected aria-labelledby"
);
info("Testing that inputClass works as expected");
doc.body.innerHTML = "";
element = createSpan(doc);
editableField({
element,
inputClass: "TEST_INPUT_CLASS",
});
info("Clicking on the inplace-editor field to turn to edit mode");
element.click();
input = doc.querySelector("input");
ok(
input.classList.contains("TEST_INPUT_CLASS"),
"Input has expected TEST_INPUT_CLASS class"
);
}
function onDone(value, isCommit, resolve) {

View file

@ -36,6 +36,7 @@ async function performTest() {
testParseVariable(doc, parser);
testParseColorVariable(doc, parser);
testParseFontFamily(doc, parser);
testParseLightDark(doc, parser);
host.destroy();
}
@ -1051,3 +1052,367 @@ function testParseFontFamily(doc, parser) {
"Got expected output for font-family with custom properties"
);
}
function testParseLightDark(doc, parser) {
const TESTS = [
{
message:
"Not passing isDarkColorScheme doesn't add unmatched classes to parameters",
propertyName: "color",
propertyValue: "light-dark(red, blue)",
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>red</span>` +
`</span>, ` +
`<span data-color="blue">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`)`,
},
{
message: "in light mode, the second parameter gets the unmatched class",
propertyName: "color",
propertyValue: "light-dark(red, blue)",
isDarkColorScheme: false,
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>red</span>` +
`</span>, ` +
`<span data-color="blue" class="unmatched-class">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`)`,
},
{
message: "in dark mode, the first parameter gets the unmatched class",
propertyName: "color",
propertyValue: "light-dark(red, blue)",
isDarkColorScheme: true,
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red" class="unmatched-class">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>red</span>` +
`</span>, ` +
`<span data-color="blue">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`)`,
},
{
message: "light-dark gets parsed as expected in shorthands in light mode",
propertyName: "border",
propertyValue: "1px solid light-dark(red, blue)",
isDarkColorScheme: false,
expected:
// prettier-ignore
`1px solid light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>red</span>` +
`</span>, ` +
`<span data-color="blue" class="unmatched-class">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`)`,
},
{
message: "light-dark gets parsed as expected in shorthands in dark mode",
propertyName: "border",
propertyValue: "1px solid light-dark(red, blue)",
isDarkColorScheme: true,
expected:
// prettier-ignore
`1px solid light-dark(` +
`<span data-color="red" class="unmatched-class">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>red</span>` +
`</span>, ` +
`<span data-color="blue">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`)`,
},
{
message: "Nested light-dark gets parsed as expected in light mode",
propertyName: "background",
propertyValue:
"linear-gradient(45deg, light-dark(red, blue), light-dark(pink, cyan))",
isDarkColorScheme: false,
expected:
// prettier-ignore
`linear-gradient(` +
`<span data-angle="45deg"><span>45deg</span></span>, ` +
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>`+
`<span>red</span>`+
`</span>, `+
`<span data-color="blue" class="unmatched-class">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`), ` +
`light-dark(` +
`<span data-color="pink">` +
`<span class="test-class" style="background-color:pink" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>pink</span>` +
`</span>, ` +
`<span data-color="cyan" class="unmatched-class">` +
`<span class="test-class" style="background-color:cyan" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>cyan</span>` +
`</span>` +
`)` +
`)`,
},
{
message: "Nested light-dark gets parsed as expected in dark mode",
propertyName: "background",
propertyValue:
"linear-gradient(33deg, light-dark(red, blue), light-dark(pink, cyan))",
isDarkColorScheme: true,
expected:
// prettier-ignore
`linear-gradient(` +
`<span data-angle="33deg"><span>33deg</span></span>, ` +
`light-dark(` +
`<span data-color="red" class="unmatched-class">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>`+
`<span>red</span>`+
`</span>, `+
`<span data-color="blue">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>blue</span>` +
`</span>` +
`), ` +
`light-dark(` +
`<span data-color="pink" class="unmatched-class">` +
`<span class="test-class" style="background-color:pink" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>pink</span>` +
`</span>, ` +
`<span data-color="cyan">` +
`<span class="test-class" style="background-color:cyan" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>cyan</span>` +
`</span>` +
`)` +
`)`,
},
{
message:
"in light mode, the second parameter gets the unmatched class when it's a variable",
propertyName: "color",
propertyValue: "light-dark(var(--x), var(--y))",
isDarkColorScheme: false,
variables: { "--x": "red", "--y": "blue" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>var(` +
`<span data-variable="--x = red">--x</span>` +
`)</span>` +
`</span>, ` +
`<span data-color="blue" class="unmatched-class">` +
`<span class="test-class" style="background-color:blue" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>var(` +
`<span data-variable="--y = blue">--y</span>` +
`)</span>` +
`</span>` +
`)`,
},
{
message:
"in light mode, the second parameter gets the unmatched class when some param are not parsed",
propertyName: "color",
// Using `notacolor` so we don't get a wrapping Node for it (contrary to colors).
// The value is still valid at parse time since we're using a variable,
// so the OutputParser will actually parse the different parts
propertyValue: "light-dark(var(--x),notacolor)",
isDarkColorScheme: false,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>,` +
`<span class="unmatched-class">notacolor</span>` +
`)`,
},
{
message:
"in dark mode, the first parameter gets the unmatched class when some param are not parsed",
propertyName: "color",
// Using `notacolor` so we don't get a wrapping Node for it (contrary to colors).
// The value is still valid at parse time since we're using a variable,
// so the OutputParser will actually parse the different parts
propertyValue: "light-dark(notacolor,var(--x))",
isDarkColorScheme: true,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span class="unmatched-class">notacolor</span>,` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>` +
`)`,
},
{
message:
"in light mode, the second parameter gets the unmatched class, comments are stripped out and whitespace are preserved",
propertyName: "color",
propertyValue:
"light-dark( /* 1st param */ var(--x) /* delim */ , /* 2nd param */ notacolor /* delim */ )",
isDarkColorScheme: false,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark( ` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span> , ` +
`<span class="unmatched-class">notacolor</span> ` +
`)`,
},
{
message:
"in dark mode, the first parameter gets the unmatched class, comments are stripped out and whitespace are preserved",
propertyName: "color",
propertyValue:
"light-dark( /* 1st param */ notacolor /* delim */ , /* 2nd param */ var(--x) /* delim */ )",
isDarkColorScheme: true,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark( ` +
`<span class="unmatched-class">notacolor</span> , ` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span> ` +
`)`,
},
{
message:
"in light mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
propertyName: "color",
propertyValue: "light-dark(var(--x))",
isDarkColorScheme: false,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>` +
`)`,
},
{
message:
"in dark mode with a single parameter, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
propertyName: "color",
propertyValue: "light-dark(var(--x))",
isDarkColorScheme: true,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>` +
`)`,
},
{
message:
"in light mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
propertyName: "color",
propertyValue: "light-dark(var(--x),a,b)",
isDarkColorScheme: false,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>,a,b` +
`)`,
},
{
message:
"in dark mode with 3 parameters, we don't strike through any parameter (TODO wrap with IACVT - Bug 1910845)",
propertyName: "color",
propertyValue: "light-dark(var(--x),a,b)",
isDarkColorScheme: true,
variables: { "--x": "red" },
expected:
// prettier-ignore
`light-dark(` +
`<span data-color="red">` +
`<span class="test-class" style="background-color:red" tabindex="0" role="button" data-color-function="light-dark"></span>` +
`<span>` +
`var(<span data-variable="--x = red">--x</span>)` +
`</span>` +
`</span>,a,b` +
`)`,
},
];
for (const test of TESTS) {
const frag = parser.parseCssProperty(
test.propertyName,
test.propertyValue,
{
isDarkColorScheme: test.isDarkColorScheme,
unmatchedClass: "unmatched-class",
colorSwatchClass: COLOR_TEST_CLASS,
getVariableData: varName => {
if (typeof test.variables[varName] === "string") {
return { value: test.variables[varName] };
}
return test.variables[varName] || {};
},
}
);
const target = doc.querySelector("div");
target.appendChild(frag);
is(target.innerHTML, test.expected, test.message);
target.innerHTML = "";
}
}

View file

@ -12,6 +12,8 @@
--markup-drag-line: var(--grey-40);
--markup-drop-line: var(--blue-55);
--markup-overflow-causing-background-color: rgba(128, 0, 215, 0.15);
--markup-new-attr-size: 2ch;
--markup-new-attr-focused-margin-inline: 1ch 2px;
}
.theme-dark:root {
@ -169,6 +171,17 @@ ul.children + .tag-line::before {
cursor: default;
}
/* Change selected line style when editing an attribute */
.tag-line[selected]:has(.styleinspector-propertyeditor:focus) .theme-selected {
background-color: var(--theme-body-alternate-emphasized-background);
color: unset;
/* revert the icon to the color it has when the line doesn't have the "selection" background */
& ~ .theme-twisty {
fill: var(--theme-icon-dimmed-color);
}
}
.tag-line[selected] + .children,
.tag-line[selected] ~ .tag-line {
background-image: linear-gradient(to top, var(--markup-outline), var(--markup-outline));
@ -334,11 +347,22 @@ ul.children + .tag-line::before {
}
.newattr {
margin-right: -13px;
}
/* offset the element so it doesn't take any space when not focused */
margin-inline-start: calc(-1 * var(--markup-new-attr-size));
.newattr:before {
content: "\00a0\00a0";
&::before {
content: "\00a0\00a0" / "";
}
&:focus-visible {
margin-inline: var(--markup-new-attr-focused-margin-inline);
background-image: url("chrome://devtools/skin/images/add.svg");
background-repeat: no-repeat;
background-size: 90%;
background-position: center;
-moz-context-properties: fill;
fill: var(--theme-icon-color);
}
}
.attr-value .link {
@ -371,10 +395,6 @@ ul.children + .tag-line::before {
}
}
.newattr:focus {
margin-right: 0;
}
.flash-out {
transition: background,color .5s;
}
@ -408,8 +428,13 @@ ul.children + .tag-line::before {
.styleinspector-propertyeditor {
vertical-align: top;
margin: -1px 0;
border: 1px solid #CCC;
border: none;
&.newattr-input {
margin-inline: var(--markup-new-attr-focused-margin-inline);
/* Apply the same size as the new attr button so we don't shift the layout when dislaying the input */
min-width: var(--markup-new-attr-size);
}
}
.reveal-link {
@ -453,26 +478,89 @@ ul.children + .tag-line::before {
color: var(--markup-hidden-tag-color);
}
/* Selected nodes in the tree should have light selected text.
@layer markup-base {
.tag {
color: var(--theme-highlight-blue);
}
.attr-name {
color: var(--theme-highlight-red);
}
.attr-value {
color: var(--theme-highlight-purple);
}
.pseudo {
color: var(--theme-highlight-blue);
}
}
/* Make focusable elements stand out a bit more when they're focused */
.tag-line :is(
[editable],
/* tag is not always editable (e.g. for `html`, `DOCTYPE`, …), but is always focusable */
.tag
):focus-visible {
border-radius: 2px;
outline: var(--theme-focus-outline);
outline-offset: var(--theme-outline-offset);
}
/* Selected nodes in the tree should have light selected text (unless there's an active
inplace editor input).
theme-selected doesn't work in this case since the text is a
sibling of the class, not a child. */
.theme-selected ~ .editor,
.theme-selected ~ .editor.comment,
.theme-selected ~ .editor .tag,
.theme-selected ~ .editor .theme-fg-color1,
.theme-selected ~ .editor .theme-fg-color2,
.theme-selected ~ .editor .theme-fg-color3 {
.tag-line[selected]:not(:has(.styleinspector-propertyeditor:focus)) :is(
.editor,
.editor.comment,
.editor :is(.attr-name, .attr-value, .tag, .pseudo)
) {
color: var(--theme-selection-color);
& :is([editable], .tag):focus-visible {
/* When an editable item in the markup view is focused, we want to make it stand out
Since the node is selected, we have a blue background on the whole node, so we set
the default body background, and set the original color we have when the line is
not selected.
This is done by using revert-layer, so we'll get colors set in <@layer markup-base>,
and not the one set in this rule's parent rule.
*/
color: revert-layer;
background-color: var(--theme-body-background);
outline-offset: 2px;
/* Better style for attributes spanning multiple lines */
box-decoration-break: clone;
mix-blend-mode: lighten;
.theme-dark & {
mix-blend-mode: darken;
}
/* We also need to take care of items that don't have their color set in <@layer markup-base> */
&:not(.attr-name, .attr-value, .tag, .pseudo) {
color: var(--theme-body-color);
@media (prefers-color-scheme: dark) {
color: var(--theme-text-color-strong);
}
}
& * {
color: revert-layer;
}
}
}
/* Selected nodes being flashed in the tree should have dark selected text. Here we target
nodes with theme-selected text colors and apply a dark, accessible text color for when
the yellow flashing background is applied. */
.theme-selected.theme-bg-contrast ~ .editor,
.theme-selected ~ .editor .theme-fg-contrast {
color: var(--theme-contrast-color);
.tag-line[selected] :is(
.theme-selected.theme-bg-contrast ~ .editor,
.theme-selected ~ .editor .theme-fg-contrast
) {
color: var(--theme-contrast-color) !important;
}
/* Applicable to the DOCTYPE */
.doctype {
font-style: italic;

View file

@ -29,14 +29,7 @@ rawPackets.set(`GET request`, {
"cause": {
"loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
"type": "img",
"stacktraceAvailable": true,
"lastFrame": {
"filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
"lineNumber": 3,
"columnNumber": 1,
"functionName": "triggerPacket",
"asyncCause": null
}
"stacktraceAvailable": false
},
"httpVersion": "HTTP/1.1",
"status": "404",
@ -68,14 +61,7 @@ rawPackets.set(`GET request update`, {
"cause": {
"loadingDocumentUri": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
"type": "img",
"stacktraceAvailable": true,
"lastFrame": {
"filename": "https://example.com/browser/devtools/client/webconsole/test/browser/stub-generators/test-network-event.html",
"lineNumber": 3,
"columnNumber": 1,
"functionName": "triggerPacket",
"asyncCause": null
}
"stacktraceAvailable": false
},
"httpVersion": "HTTP/1.1",
"status": "404",

View file

@ -619,6 +619,10 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
return fragment.rows.lines[fragment.rows.lines.length - 1].start;
}
getNode(id) {
return this.markup.content.root.getElementById(this.ID_CLASS_PREFIX + id);
}
/**
* The AutoRefreshHighlighter's _hasMoved method returns true only if the
* element's quads have changed. Override it so it also returns true if the
@ -726,6 +730,18 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
// Hide the canvas, grid element highlights and infobar.
this._hide();
// Set z-index.
this.markup.content.root.firstElementChild.style.setProperty(
"z-index",
this.options.zIndex
);
// Update the grid color
this.markup.content.root.firstElementChild.style.setProperty(
"--grid-color",
this.color
);
return this._update();
}
@ -1800,24 +1816,7 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
_update() {
setIgnoreLayoutChanges(true);
// Set z-index.
this.markup.content.root.firstElementChild.style.setProperty(
"z-index",
this.options.zIndex
);
const root = this.getElement("root");
const cells = this.getElement("cells");
const areas = this.getElement("areas");
// Set the grid cells and areas fill to the current grid colour.
cells.setAttribute("style", `fill: ${this.color}`);
areas.setAttribute("style", `fill: ${this.color}`);
// Hide the root element and force the reflow in order to get the proper window's
// dimensions without increasing them.
root.setAttribute("style", "display: none");
this.win.document.documentElement.offsetWidth;
const root = this.getNode("root");
this._winDimensions = getWindowDimensions(this.win);
const { width, height } = this._winDimensions;
@ -1866,10 +1865,8 @@ class CssGridHighlighter extends AutoRefreshHighlighter {
this._showGrid();
this._showGridElements();
root.setAttribute(
"style",
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`
);
root.style.setProperty("width", `${width}px`);
root.style.setProperty("height", `${height}px`);
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
return true;

View file

@ -250,8 +250,20 @@
color: var(--grey-40);
}
/* CSS Flexbox Highlighter */
.flexbox-root {
position: absolute;
overflow: hidden;
}
/* CSS Grid Highlighter */
.css-grid-root {
position: absolute;
overflow: hidden;
}
.css-grid-canvas {
position: absolute;
pointer-events: none;
@ -268,6 +280,8 @@
.css-grid-cells {
opacity: 0.5;
stroke: none;
/* Set in css-grid.js */
fill: var(--grid-color);
}
.css-grid-area-infobar-name,

View file

@ -302,6 +302,10 @@ class FlexboxHighlighter extends AutoRefreshHighlighter {
return pattern;
}
getNode(id) {
return this.markup.content.root.getElementById(this.ID_CLASS_PREFIX + id);
}
/**
* The AutoRefreshHighlighter's _hasMoved method returns true only if the
* element's quads have changed. Override it so it also returns true if the
@ -829,12 +833,6 @@ class FlexboxHighlighter extends AutoRefreshHighlighter {
_update() {
setIgnoreLayoutChanges(true);
const root = this.getElement("root");
// Hide the root element and force the reflow in order to get the proper window's
// dimensions without increasing them.
root.setAttribute("style", "display: none");
this.win.document.documentElement.offsetWidth;
this._winDimensions = getWindowDimensions(this.win);
const { width, height } = this._winDimensions;
@ -870,10 +868,9 @@ class FlexboxHighlighter extends AutoRefreshHighlighter {
this._showFlexbox();
this.prevColor = this.color;
root.setAttribute(
"style",
`position: absolute; width: ${width}px; height: ${height}px; overflow: hidden`
);
const root = this.getNode("root");
root.style.setProperty("width", `${width}px`);
root.style.setProperty("height", `${height}px`);
setIgnoreLayoutChanges(false, this.highlighterEnv.document.documentElement);
return true;

View file

@ -634,6 +634,7 @@ class PageStyleActor extends Actor {
* - isSystem Boolean
* - inherited Boolean
* - pseudoElement String
* - darkColorScheme Boolean
*/
_getAllElementRules(node, inherited, options) {
const { bindingElement, pseudo } = CssLogic.getBindingElementAndPseudo(
@ -650,12 +651,11 @@ class PageStyleActor extends Actor {
const showInheritedStyles =
inherited && this._hasInheritedProps(bindingElement.style);
const rule = {
rule: elementStyle,
const rule = this._getRuleItem(elementStyle, node.rawNode, {
pseudoElement: null,
isSystem: false,
inherited: false,
};
});
// First any inline styles
if (showElementStyles) {
@ -717,6 +717,27 @@ class PageStyleActor extends Actor {
return rules;
}
/**
* @param {DOMNode} rawNode
* @param {StyleRuleActor} styleRuleActor
* @param {Object} params
* @param {Boolean} params.inherited
* @param {Boolean} params.isSystem
* @param {String|null} params.pseudoElement
* @returns Object
*/
_getRuleItem(rule, rawNode, { inherited, isSystem, pseudoElement }) {
return {
rule,
pseudoElement,
isSystem,
inherited,
// We can't compute the value for the whole document as the color scheme
// can be set at the node level (e.g. with `color-scheme`)
darkColorScheme: InspectorUtils.isUsedColorSchemeDark(rawNode),
};
}
_nodeIsTextfieldLike(node) {
if (node.nodeName == "TEXTAREA") {
return true;
@ -841,12 +862,13 @@ class PageStyleActor extends Actor {
const ruleActor = this._styleRef(domRule);
rules.push({
rule: ruleActor,
inherited,
isSystem,
pseudoElement: pseudo,
});
rules.push(
this._getRuleItem(ruleActor, node, {
inherited,
isSystem,
pseudoElement: pseudo,
})
);
}
return rules;
}

View file

@ -158,12 +158,15 @@ class TracerActor extends Actor {
switch (this.logMethod) {
case TRACER_LOG_METHODS.STDOUT:
ListenerClass = StdoutTracingListener;
// Currently only the profiler output is supported with the native tracer.
options.useNativeTracing = false;
break;
case TRACER_LOG_METHODS.CONSOLE:
case TRACER_LOG_METHODS.DEBUGGER_SIDEBAR:
// Console and debugger sidebar are both using JSTRACE_STATE/JSTRACE_TRACE resources
// to receive tracing data.
ListenerClass = ResourcesTracingListener;
options.useNativeTracing = false;
break;
case TRACER_LOG_METHODS.PROFILER:
ListenerClass = ProfilerTracingListener;
@ -194,6 +197,8 @@ class TracerActor extends Actor {
traceOnNextInteraction: !!options.traceOnNextInteraction,
// Notify about frame exit / function call returning
traceFunctionReturn: !!options.traceFunctionReturn,
// Use the native tracing implementation
useNativeTracing: !!options.useNativeTracing,
// Ignore frames beyond the given depth
maxDepth: options.maxDepth,
// Stop the tracing after a number of top level frames
@ -214,7 +219,9 @@ class TracerActor extends Actor {
// Remove before stopping to prevent receiving the stop notification
lazy.JSTracer.removeTracingListener(this.tracingListener);
// Save the result of the stop request for the profiler and the getProfile RDP method
this.#stopResult = this.tracingListener.stop();
this.#stopResult = this.tracingListener.stop(
lazy.JSTracer.maybeGetNativeTrace()
);
this.tracingListener = null;
lazy.JSTracer.stopTracing();

View file

@ -70,10 +70,61 @@ class ProfilerTracingListener {
/**
* Stop the record and return the gecko profiler data.
*
* @param {Object} nativeTrace
* If we're using native tracing, this contains a table of what the
* native tracer has collected.
* @return {Object}
* The Gecko profile object.
*/
stop() {
stop(nativeTrace) {
if (nativeTrace) {
const KIND_INDEX = 0;
const LINENO_INDEX = 1;
const COLUMN_INDEX = 2;
const SCRIPT_ID_INDEX = 3;
const FUNCTION_NAME_ID_INDEX = 4;
const IMPLEMENTATION_INDEX = 5;
const TIME_INDEX = 6;
const LABEL_INDEX = 1;
const LABEL_TIME_INDEX = 2;
const IMPLEMENTATION_STRINGS = ["interpreter", "baseline", "ion", "wasm"];
for (const entry of nativeTrace.events) {
const kind = entry[KIND_INDEX];
switch (kind) {
case Debugger.TRACING_EVENT_KIND_FUNCTION_ENTER: {
this.#onFramePush(
{
name: nativeTrace.atoms[entry[FUNCTION_NAME_ID_INDEX]],
url: nativeTrace.scriptURLs[entry[SCRIPT_ID_INDEX]],
lineNumber: entry[LINENO_INDEX],
columnNumber: entry[COLUMN_INDEX],
category: IMPLEMENTATION_STRINGS[entry[IMPLEMENTATION_INDEX]],
sourceId: entry[SCRIPT_ID_INDEX],
},
entry[TIME_INDEX]
);
break;
}
case Debugger.TRACING_EVENT_KIND_FUNCTION_LEAVE: {
this.#onFramePop(entry[TIME_INDEX], false);
break;
}
case Debugger.TRACING_EVENT_KIND_LABEL_ENTER: {
this.#logDOMEvent(entry[LABEL_INDEX], entry[LABEL_TIME_INDEX]);
break;
}
case Debugger.TRACING_EVENT_KIND_LABEL_LEAVE: {
this.#onFramePop(entry[LABEL_TIME_INDEX], false);
break;
}
}
}
}
// Create the profile to return.
const profile = this.#getEmptyProfile();
profile.meta.categories = this.#categories;
@ -413,7 +464,7 @@ class ProfilerTracingListener {
this.#logDOMEvent(currentDOMEvent);
}
const frameIndex = this.#getOrCreateFrame({
this.#onFramePush({
// formatedDisplayName has a lambda at the beginning, remove it.
name: formatedDisplayName.replace("λ ", ""),
url,
@ -422,16 +473,6 @@ class ProfilerTracingListener {
category: frame.implementation,
sourceId: script.source.id,
});
this.#currentStackIndex = this.#getOrCreateStack(
frameIndex,
this.#currentStackIndex
);
this.#thread.samples.data.push([
this.#currentStackIndex,
ChromeUtils.dateNow() - this.#startTime,
0, // eventDelay
]);
return false;
}
@ -440,8 +481,14 @@ class ProfilerTracingListener {
* Called when a DOM Event just fired (and some listener in JS is about to run).
*
* @param {String} domEventName
* @param {Number|undefined} [time=undefined]
* The time at which this event occurred
*/
#logDOMEvent(domEventName) {
#logDOMEvent(domEventName, time = undefined) {
if (time === undefined) {
time = ChromeUtils.dateNow();
}
const frameIndex = this.#getOrCreateLabelFrame(domEventName);
this.#currentStackIndex = this.#getOrCreateStack(
frameIndex,
@ -450,7 +497,7 @@ class ProfilerTracingListener {
this.#thread.samples.data.push([
this.#currentStackIndex,
ChromeUtils.dateNow() - this.#startTime,
time - this.#startTime,
0, // eventDelay
]);
}
@ -510,13 +557,46 @@ class ProfilerTracingListener {
}
/**
* Called when a function call ends and returns.
* Called when a new function is called.
*
* @param {Object} frameInfo
* @param {Number|undefined} [time=undefined]
* The time at which this event occurred
*/
#onFramePop() {
#onFramePush(frameInfo, time) {
if (time === undefined) {
time = ChromeUtils.dateNow();
}
const frameIndex = this.#getOrCreateFrame(frameInfo);
this.#currentStackIndex = this.#getOrCreateStack(
frameIndex,
this.#currentStackIndex
);
this.#thread.samples.data.push([
this.#currentStackIndex,
time - this.#startTime,
0, // eventDelay
]);
}
/**
* Called when a function call ends and returns.
* @param {Number|undefined} [time=undefined]
* The time at which this event occurred
* @param {Boolean} [autoPopLabels=false]
* Whether we should automatically pop label frames if we're popping a root
*/
#onFramePop(time = undefined, autoPopLabels = true) {
if (this.#currentStackIndex === null) {
return;
}
if (time === undefined) {
time = ChromeUtils.dateNow();
}
this.#currentStackIndex =
this.#thread.stackTable.data[this.#currentStackIndex][
INDEXES.stacks.prefix
@ -526,13 +606,13 @@ class ProfilerTracingListener {
// so that the frontend considers that the last executed frame stops its execution.
this.#thread.samples.data.push([
this.#currentStackIndex,
ChromeUtils.dateNow() - this.#startTime,
time - this.#startTime,
0, // eventDelay
]);
// If we popped and now are on a label frame, with a null line,
// automatically also pop that label frame.
if (this.#currentStackIndex !== null) {
if (autoPopLabels && this.#currentStackIndex !== null) {
const currentFrameIndex =
this.#thread.stackTable.data[this.#currentStackIndex][
INDEXES.stacks.frame
@ -540,7 +620,7 @@ class ProfilerTracingListener {
const currentFrameLine =
this.#thread.frameTable.data[currentFrameIndex][INDEXES.frames.line];
if (currentFrameLine == null) {
this.#onFramePop();
this.#onFramePop(time);
}
}
}

View file

@ -907,6 +907,7 @@ WebConsoleCommandsManager.register({
traceFunctionReturn: !!args.returns,
traceValues: !!args.values,
traceOnNextInteraction: args["on-next-interaction"] || null,
useNativeTracing: args["use-native-tracing"] || null,
traceDOMMutations,
maxDepth: args["max-depth"] || null,
maxRecords: args["max-records"] || null,

View file

@ -139,6 +139,8 @@ const customLazy = {
* @param {Boolean} options.traceFunctionReturn
* Optional setting to enable when the tracing should notify about frame exit.
* i.e. when a function call returns or throws.
* @param {Boolean} options.useNativeTracing
* Optional setting to enable the native tracing implementation.
* @param {String} options.filterFrameSourceUrl
* Optional setting to restrict all traces to only a given source URL.
* This is a loose check, so any source whose URL includes the passed string will be traced.
@ -218,6 +220,7 @@ class JavaScriptTracer {
this.traceSteps = !!options.traceSteps;
this.traceValues = !!options.traceValues;
this.traceFunctionReturn = !!options.traceFunctionReturn;
this.useNativeTracing = !!options.useNativeTracing;
this.maxDepth = options.maxDepth;
this.infiniteLoopDepthLimit = isWorker
? 200
@ -299,7 +302,11 @@ class JavaScriptTracer {
#startTracing() {
this.isTracing = true;
this.dbg.onEnterFrame = this.onEnterFrame;
if (this.useNativeTracing) {
this.dbg.nativeTracing = true;
} else {
this.dbg.onEnterFrame = this.onEnterFrame;
}
if (this.traceDOMEvents) {
this.startTracingDOMEvents();
@ -334,6 +341,7 @@ class JavaScriptTracer {
win.browsingContext.browserId == browserId
) {
this.dbg.addDebuggee(g);
this.dbg.nativeTracing = this.useNativeTracing;
this.debuggerNotificationObserver.connect(win);
}
} catch (e) {}
@ -498,6 +506,17 @@ class JavaScriptTracer {
}
}
/**
* If native tracing is enabled, get the trace from the native tracer
*/
maybeGetNativeTrace() {
if (this.useNativeTracing) {
return this.dbg.collectNativeTrace();
}
return null;
}
/**
* Stop observing execution.
*
@ -510,7 +529,12 @@ class JavaScriptTracer {
return;
}
this.dbg.onEnterFrame = undefined;
if (!this.useNativeTracing) {
this.dbg.nativeTracing = false;
} else {
this.dbg.onEnterFrame = undefined;
}
this.dbg.removeAllDebuggees();
this.dbg.onNewGlobalObject = undefined;
this.dbg = null;
@ -1028,6 +1052,17 @@ function stopTracing() {
}
}
/**
* If native tracing is enabled, get the trace from the native tracer
*/
function maybeGetNativeTrace() {
if (activeTracer) {
return activeTracer.maybeGetNativeTrace();
}
console.warn("Can't get a native trace as we were not tracing.");
return null;
}
/**
* Listen for tracing updates.
*
@ -1128,6 +1163,7 @@ function syncPause(duration) {
export const JSTracer = {
startTracing,
stopTracing,
maybeGetNativeTrace,
addTracingListener,
removeTracingListener,
NEXT_INTERACTION_MESSAGE,

View file

@ -21,8 +21,8 @@ const createParentProcessRequests = async () => {
const EXPECTED_METHOD_NAME = "createParentProcessRequests";
const EXPECTED_REQUEST_LINE_1 = 12;
const EXPECTED_REQUEST_COL_1 = 9;
const EXPECTED_REQUEST_LINE_2 = 17;
const EXPECTED_REQUEST_COL_2 = 3;
// const EXPECTED_REQUEST_LINE_2 = 17;
// const EXPECTED_REQUEST_COL_2 = 3;
// Test the ResourceCommand API around NETWORK_EVENT for the parent process
@ -133,13 +133,20 @@ add_task(async function testParentProcessRequests() {
ok(!firstImageRequest.fromCache, "The first image request isn't cached");
ok(firstImageRequest.chromeContext, "The first image request is privileged");
const firstImageStacktrace = receivedStacktraces[1].lastFrame;
is(receivedStacktraces[1].resourceId, firstImageRequest.stacktraceResourceId);
const firstImageStacktrace = receivedStacktraces[1].lastFrame;
// TODO(bug 1911435).
todo(
!!firstImageStacktrace,
"After bug 1076583, image load is async and we can't get a stack trace"
);
/*
is(firstImageStacktrace.filename, gTestPath);
is(firstImageStacktrace.lineNumber, EXPECTED_REQUEST_LINE_2);
is(firstImageStacktrace.columnNumber, EXPECTED_REQUEST_COL_2);
is(firstImageStacktrace.functionName, EXPECTED_METHOD_NAME);
is(firstImageStacktrace.asyncCause, null);
*/
info("Assert the second image request");
const secondImageRequest = receivedNetworkEvents[2];

View file

@ -106,6 +106,10 @@ class TracerCommand extends EventEmitter {
"devtools.debugger.javascript-tracing-function-return",
false
),
useNativeTracing: Services.prefs.getBoolPref(
"devtools.debugger.javascript-tracing-native",
false
),
};
}

View file

@ -16,6 +16,7 @@ types.addDictType("tracer.start.options", {
traceOnNextInteraction: "boolean",
traceOnNextLoad: "boolean",
traceDOMMutations: "nullable:array:string",
useNativeTracing: "boolean",
});
const tracerSpec = generateActorSpec({

View file

@ -0,0 +1,70 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_CacheExpirationTime_h___
#define mozilla_dom_CacheExpirationTime_h___
#include <stdint.h> // uint32_t
#include "mozilla/Assertions.h" // MOZ_ASSERT
#include "prtime.h" // PRTime, PR_USEC_PER_SEC
#include "nsICacheEntry.h" // nsICacheEntry
/*
* The expiration time for sub resource cache.
*/
struct CacheExpirationTime {
private:
uint32_t mTime;
static constexpr uint32_t kAlreadyExpired = 0;
static constexpr uint32_t kNever = nsICacheEntry::NO_EXPIRATION_TIME;
constexpr CacheExpirationTime() : mTime(kNever) {}
explicit constexpr CacheExpirationTime(uint32_t aTime) : mTime(aTime) {}
static uint32_t SecondsFromPRTime(PRTime aTime) {
return uint32_t(int64_t(aTime) / int64_t(PR_USEC_PER_SEC));
}
public:
static constexpr CacheExpirationTime AlreadyExpired() {
return CacheExpirationTime(kAlreadyExpired);
}
static constexpr CacheExpirationTime Never() {
return CacheExpirationTime(kNever);
}
static constexpr CacheExpirationTime ExpireAt(uint32_t aTime) {
return CacheExpirationTime(aTime);
}
bool IsExpired() const {
if (IsNever()) {
return false;
}
return mTime <= SecondsFromPRTime(PR_Now());
}
bool IsNever() const { return mTime == kNever; }
bool IsShorterThan(const CacheExpirationTime& aOther) const {
return mTime < aOther.mTime;
}
void SetMinimum(const CacheExpirationTime& aOther) {
if (aOther.IsNever()) {
return;
}
if (IsNever() || aOther.IsShorterThan(*this)) {
mTime = aOther.mTime;
}
}
};
#endif /* mozilla_dom_CacheExpirationTime_h___ */

View file

@ -149,7 +149,7 @@ static void LazyLoadCallback(
Element* target = entry->Target();
if (entry->IsIntersecting()) {
if (auto* image = HTMLImageElement::FromNode(target)) {
image->StopLazyLoading(HTMLImageElement::StartLoading::Yes);
image->StopLazyLoading();
} else if (auto* iframe = HTMLIFrameElement::FromNode(target)) {
iframe->StopLazyLoading();
} else {

View file

@ -194,6 +194,7 @@
#include "mozilla/dom/HTMLTextAreaElement.h"
#include "mozilla/dom/ImageTracker.h"
#include "mozilla/dom/InspectorUtils.h"
#include "mozilla/dom/InteractiveWidget.h"
#include "mozilla/dom/Link.h"
#include "mozilla/dom/MediaQueryList.h"
#include "mozilla/dom/MediaSource.h"
@ -1432,6 +1433,7 @@ Document::Document(const char* aContentType)
mHttpsOnlyStatus(nsILoadInfo::HTTPS_ONLY_UNINITIALIZED),
mViewportType(Unknown),
mViewportFit(ViewportFitType::Auto),
mInteractiveWidgetMode(InteractiveWidget::ResizesContent),
mHeaderData(nullptr),
mServoRestyleRootDirtyBits(0),
mThrowOnDynamicMarkupInsertionCounter(0),
@ -8376,9 +8378,6 @@ void Document::UnblockDOMContentLoaded() {
("DOCUMENT %p UnblockDOMContentLoaded", this));
mDidFireDOMContentLoaded = true;
if (PresShell* presShell = GetPresShell()) {
presShell->GetRefreshDriver()->NotifyDOMContentLoaded();
}
MOZ_ASSERT(mReadyState == READYSTATE_INTERACTIVE);
if (!mSynchronousDOMContentLoaded) {
@ -10956,11 +10955,47 @@ ViewportMetaData Document::GetViewportMetaData() const {
: ViewportMetaData();
}
static InteractiveWidget ParseInteractiveWidget(
const ViewportMetaData& aViewportMetaData) {
if (aViewportMetaData.mInteractiveWidgetMode.IsEmpty()) {
// The spec defines "use `resizes-visual` if no value specified", but here
// we use `resizes-content` for the backward compatibility now.
// We will change it in bug 1884807.
return InteractiveWidget::ResizesContent;
}
if (aViewportMetaData.mInteractiveWidgetMode.EqualsIgnoreCase(
"resizes-visual")) {
return InteractiveWidget::ResizesVisual;
}
if (aViewportMetaData.mInteractiveWidgetMode.EqualsIgnoreCase(
"resizes-content")) {
return InteractiveWidget::ResizesContent;
}
if (aViewportMetaData.mInteractiveWidgetMode.EqualsIgnoreCase(
"overlays-content")) {
return InteractiveWidget::OverlaysContent;
}
// For the same reason above empty case, we use `resizes-content` here.
return InteractiveWidget::ResizesContent;
}
void Document::SetMetaViewportData(UniquePtr<ViewportMetaData> aData) {
mLastModifiedViewportMetaData = std::move(aData);
// Trigger recomputation of the nsViewportInfo the next time it's queried.
mViewportType = Unknown;
// Parse interactive-widget here anyway. Normally we parse any data in the
// meta viewport tag in GetViewportInfo(), but GetViewportInfo() depends on
// the document state (e.g. display size, fullscreen, desktop-mode etc.)
// whereas interactive-widget is independent from the document state, it's
// necessary whatever the document state is.
dom::InteractiveWidget interactiveWidget =
ParseInteractiveWidget(*mLastModifiedViewportMetaData);
if (mInteractiveWidgetMode != interactiveWidget) {
mInteractiveWidgetMode = interactiveWidget;
}
AsyncEventDispatcher::RunDOMEventWhenSafe(
*this, u"DOMMetaViewportFitChanged"_ns, CanBubble::eYes,
ChromeOnlyDispatch::eYes);

View file

@ -246,7 +246,6 @@ class FeaturePolicy;
class FontFaceSet;
class FragmentDirective;
class FrameRequestCallback;
class ImageTracker;
class HighlightRegistry;
class HTMLAllCollection;
class HTMLBodyElement;
@ -256,6 +255,8 @@ class HTMLDialogElement;
class HTMLSharedElement;
class HTMLVideoElement;
class HTMLImageElement;
class ImageTracker;
enum class InteractiveWidget : uint8_t;
struct LifecycleCallbackArgs;
class Link;
class Location;
@ -3958,6 +3959,10 @@ class Document : public nsINode,
public:
const OriginTrials& Trials() const { return mTrials; }
dom::InteractiveWidget InteractiveWidget() const {
return mInteractiveWidgetMode;
}
private:
void DoCacheAllKnownLangPrefs();
void RecomputeLanguageFromCharset();
@ -5173,6 +5178,9 @@ class Document : public nsINode,
// https://drafts.csswg.org/css-round-display/#viewport-fit-descriptor
ViewportFitType mViewportFit;
// https://drafts.csswg.org/css-viewport/#interactive-widget-section
dom::InteractiveWidget mInteractiveWidgetMode;
// XXXdholbert This should really be modernized to a nsTHashMap or similar,
// though note that the modernization will need to take care to also convert
// the special hash_table_ops logic (e.g. how SubDocClearEntry clears the

View file

@ -0,0 +1,19 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef DOM_INTERACTIVE_WIDGET_H_
#define DOM_INTERACTIVE_WIDGET_H_
namespace mozilla::dom {
// https://drafts.csswg.org/css-viewport/#interactive-widget-section
enum class InteractiveWidget : uint8_t {
OverlaysContent,
ResizesContent,
ResizesVisual,
};
} // namespace mozilla::dom
#endif // DOM_INTERACTIVE_WIDGET_H_

View file

@ -303,6 +303,58 @@ nsCString SelectionChangeReasonsToCString(int16_t aReasons) {
} // namespace mozilla
SelectionNodeCache::SelectionNodeCache(PresShell& aOwningPresShell)
: mOwningPresShell(aOwningPresShell) {
MOZ_ASSERT(!mOwningPresShell.mSelectionNodeCache);
mOwningPresShell.mSelectionNodeCache = this;
}
SelectionNodeCache::~SelectionNodeCache() {
mOwningPresShell.mSelectionNodeCache = nullptr;
}
bool SelectionNodeCache::MaybeCollectNodesAndCheckIfFullySelectedInAnyOf(
const nsINode* aNode, const nsTArray<Selection*>& aSelections) {
for (const auto* sel : aSelections) {
if (MaybeCollectNodesAndCheckIfFullySelected(aNode, sel)) {
return true;
}
}
return false;
}
const nsTHashSet<const nsINode*>& SelectionNodeCache::MaybeCollect(
const Selection* aSelection) {
MOZ_ASSERT(aSelection);
return mSelectedNodes.LookupOrInsertWith(aSelection, [sel = RefPtr(
aSelection)] {
nsTHashSet<const nsINode*> fullySelectedNodes;
for (size_t rangeIndex = 0; rangeIndex < sel->RangeCount(); ++rangeIndex) {
AbstractRange* range = sel->GetAbstractRangeAt(rangeIndex);
const RangeBoundary& startRef = range->MayCrossShadowBoundaryStartRef();
const RangeBoundary& endRef = range->MayCrossShadowBoundaryEndRef();
const nsINode* startContainer =
startRef.IsStartOfContainer() ? nullptr : startRef.Container();
const nsINode* endContainer =
endRef.IsEndOfContainer() ? nullptr : endRef.Container();
UnsafePreContentIterator iter;
iter.Init(range);
for (; !iter.IsDone(); iter.Next()) {
if (const nsINode* node = iter.GetCurrentNode()) {
// Only collect start and end container if they are fully
// selected (they are null in that case).
if (node == startContainer || node == endContainer) {
continue;
}
fullySelectedNodes.Insert(node);
}
}
}
return fullySelectedNodes;
});
}
// #define DEBUG_SELECTION // uncomment for printf describing every collapse and
// extend. #define DEBUG_NAVIGATION
@ -1954,6 +2006,29 @@ UniquePtr<SelectionDetails> Selection::LookUpSelection(
}
nsTArray<AbstractRange*> overlappingRanges;
SelectionNodeCache* cache =
GetPresShell() ? GetPresShell()->GetSelectionNodeCache() : nullptr;
if (cache && RangeCount() == 1) {
const bool isFullySelected =
cache->MaybeCollectNodesAndCheckIfFullySelected(aContent, this);
if (isFullySelected) {
auto newHead = MakeUnique<SelectionDetails>();
newHead->mNext = std::move(aDetailsHead);
newHead->mStart = AssertedCast<int32_t>(0);
newHead->mEnd = AssertedCast<int32_t>(aContentLength);
newHead->mSelectionType = aSelectionType;
newHead->mHighlightData = mHighlightData;
StyledRange* rd = mStyledRanges.FindRangeData(GetAbstractRangeAt(0));
if (rd) {
newHead->mTextRangeStyle = rd->mTextRangeStyle;
}
auto detailsHead = std::move(newHead);
return detailsHead;
}
}
nsresult rv = GetAbstractRangesForIntervalArray(
aContent, aContentOffset, aContent, aContentOffset + aContentLength,
false, &overlappingRanges);

View file

@ -56,6 +56,71 @@ namespace mozilla {
namespace dom {
/**
* This cache allows to store all selected nodes during a reflow operation.
*
* All fully selected nodes are stored in a hash set per-selection instance.
* This allows fast paths in `nsINode::IsSelected()` and
* `Selection::LookupSelection()`. For partially selected nodes, the old
* mechanisms are used. This is okay, because for partially selected nodes
* no expensive node traversal is necessary.
*
* This cache is designed to be used in a context where no script is allowed
* to run. It assumes that the selection itself, or any range therein, does not
* change during its lifetime.
*
* By design, this class can only be instantiated in the `PresShell`.
*/
class MOZ_RAII SelectionNodeCache final {
public:
~SelectionNodeCache();
/**
* Returns true if `aNode` is fully selected by any of the given selections.
*
* This method will collect all fully selected nodes of `aSelections` and
* store them internally (therefore this method isn't const).
*/
bool MaybeCollectNodesAndCheckIfFullySelectedInAnyOf(
const nsINode* aNode, const nsTArray<Selection*>& aSelections);
/**
* Returns true if `aNode` is fully selected by any range in `aSelection`.
*
* This method collects all fully selected nodes from `aSelection` and store
* them internally.
*/
bool MaybeCollectNodesAndCheckIfFullySelected(const nsINode* aNode,
const Selection* aSelection) {
return MaybeCollect(aSelection).Contains(aNode);
}
private:
/**
* This class is supposed to be only created by the PresShell.
*/
friend PresShell;
explicit SelectionNodeCache(PresShell& aOwningPresShell);
/**
* Collects all nodes from a given list of selections.
*
* This method assumes that the selections itself won't change during this
* object's lifetime. It's not possible to 'update' the cached selected ranges
* by calling this method again.
*/
void MaybeCollect(const nsTArray<Selection*>& aSelections);
/**
* Iterates all ranges in `aSelection` and collects its fully selected nodes
* into a hash set, which is also returned.
*
* If `aSelection` is already cached, the hash set is returned directly.
*/
const nsTHashSet<const nsINode*>& MaybeCollect(const Selection* aSelection);
nsTHashMap<const Selection*, nsTHashSet<const nsINode*>> mSelectedNodes;
PresShell& mOwningPresShell;
};
// Note, the ownership of mozilla::dom::Selection depends on which way the
// object is created. When nsFrameSelection has created Selection,
// addreffing/releasing the Selection object is aggregated to nsFrameSelection.

View file

@ -5,6 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/SourceLocation.h"
#include "mozilla/ThreadLocal.h"
#include "nsContentUtils.h"
#include "jsapi.h"
@ -21,12 +22,30 @@ SourceLocation::SourceLocation(nsCOMPtr<nsIURI>&& aResource, uint32_t aLine,
uint32_t aCol)
: mResource(std::move(aResource)), mLine(aLine), mColumn(aCol) {}
static MOZ_THREAD_LOCAL(const JSCallingLocation*) tlsFallback;
const JSCallingLocation* JSCallingLocation::GetFallback() {
if (!tlsFallback.initialized()) {
return nullptr;
}
return tlsFallback.get();
}
void JSCallingLocation::SetFallback(const JSCallingLocation* aFallback) {
if (!tlsFallback.init()) {
return;
}
tlsFallback.set(aFallback);
}
JSCallingLocation JSCallingLocation::Get() {
return Get(nsContentUtils::GetCurrentJSContext());
}
JSCallingLocation JSCallingLocation::Get(JSContext* aCx) {
JSCallingLocation result;
if (const JSCallingLocation* loc = GetFallback()) {
result = *loc;
}
if (!aCx) {
return result;
}

View file

@ -40,6 +40,22 @@ struct JSCallingLocation : SourceLocation {
static JSCallingLocation Get();
static JSCallingLocation Get(JSContext*);
class MOZ_STACK_CLASS AutoFallback {
public:
explicit AutoFallback(const JSCallingLocation* aFallback)
: mOldFallback(GetFallback()) {
SetFallback(aFallback);
}
~AutoFallback() { SetFallback(mOldFallback); }
private:
const JSCallingLocation* mOldFallback;
};
private:
static const JSCallingLocation* GetFallback();
static void SetFallback(const JSCallingLocation*);
};
} // namespace mozilla

View file

@ -59,6 +59,8 @@ static void ProcessViewportToken(ViewportMetaData& aData,
aData.mUserScalable.Assign(value);
} else if (key_atom == nsGkAtoms::viewport_fit) {
aData.mViewportFit.Assign(value);
} else if (key_atom == nsGkAtoms::interactive_widget) {
aData.mInteractiveWidgetMode.Assign(value);
}
}

View file

@ -20,6 +20,7 @@ struct ViewportMetaData {
nsString mMaximumScale;
nsString mUserScalable;
nsString mViewportFit;
nsString mInteractiveWidgetMode;
bool operator==(const ViewportMetaData& aOther) const {
return mWidth == aOther.mWidth && mHeight == aOther.mHeight &&
@ -27,7 +28,8 @@ struct ViewportMetaData {
mMinimumScale == aOther.mMinimumScale &&
mMaximumScale == aOther.mMaximumScale &&
mUserScalable == aOther.mUserScalable &&
mViewportFit == aOther.mViewportFit;
mViewportFit == aOther.mViewportFit &&
mInteractiveWidgetMode == aOther.mInteractiveWidgetMode;
}
bool operator!=(const ViewportMetaData& aOther) const {
return !(*this == aOther);

View file

@ -157,6 +157,7 @@ EXPORTS.mozilla.dom += [
"BodyConsumer.h",
"BodyUtil.h",
"BorrowedAttrInfo.h",
"CacheExpirationTime.h",
"CCGCScheduler.h",
"CharacterData.h",
"ChildIterator.h",
@ -209,6 +210,7 @@ EXPORTS.mozilla.dom += [
"IDTracker.h",
"ImageEncoder.h",
"ImageTracker.h",
"InteractiveWidget.h",
"IntlUtils.h",
"JSExecutionContext.h",
"Link.h",

View file

@ -145,6 +145,7 @@
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/BrowsingContext.h"
#include "mozilla/dom/BrowsingContextGroup.h"
#include "mozilla/dom/CacheExpirationTime.h"
#include "mozilla/dom/CallbackFunction.h"
#include "mozilla/dom/CallbackObject.h"
#include "mozilla/dom/ChromeMessageBroadcaster.h"
@ -11402,7 +11403,9 @@ nsContentUtils::GetSubresourceCacheValidationInfo(nsIRequest* aRequest,
if (nsCOMPtr<nsICacheInfoChannel> cache = do_QueryInterface(aRequest)) {
uint32_t value = 0;
if (NS_SUCCEEDED(cache->GetCacheTokenExpirationTime(&value))) {
info.mExpirationTime.emplace(value);
// NOTE: If the cache doesn't expire, the value should be
// nsICacheEntry::NO_EXPIRATION_TIME.
info.mExpirationTime.emplace(CacheExpirationTime::ExpireAt(value));
}
}
@ -11439,12 +11442,24 @@ nsContentUtils::GetSubresourceCacheValidationInfo(nsIRequest* aRequest,
if (knownCacheable) {
MOZ_ASSERT(!info.mExpirationTime);
MOZ_ASSERT(!info.mMustRevalidate);
info.mExpirationTime = Some(0); // 0 means "doesn't expire".
info.mExpirationTime = Some(CacheExpirationTime::Never());
}
return info;
}
CacheExpirationTime nsContentUtils::GetSubresourceCacheExpirationTime(
nsIRequest* aRequest, nsIURI* aURI) {
auto info = GetSubresourceCacheValidationInfo(aRequest, aURI);
// For now, we never cache entries that we have to revalidate, or whose
// channel don't support caching.
if (info.mMustRevalidate || !info.mExpirationTime) {
return CacheExpirationTime::AlreadyExpired();
}
return *info.mExpirationTime;
}
/* static */
bool nsContentUtils::ShouldBypassSubResourceCache(Document* aDoc) {
RefPtr<nsILoadGroup> lg = aDoc->GetDocumentLoadGroup();

View file

@ -39,6 +39,7 @@
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/BindingDeclarations.h"
#include "mozilla/dom/CacheExpirationTime.h"
#include "mozilla/dom/FromParser.h"
#include "mozilla/dom/FetchPriority.h"
#include "mozilla/fallible.h"
@ -3440,17 +3441,24 @@ class nsContentUtils {
struct SubresourceCacheValidationInfo {
// The expiration time, in seconds, if known.
mozilla::Maybe<uint32_t> mExpirationTime;
mozilla::Maybe<CacheExpirationTime> mExpirationTime;
bool mMustRevalidate = false;
};
/**
* Gets cache validation info for subresources such as images or CSS
* stylesheets.
* Gets cache validation info for subresources such as images, CSS
* stylesheets, or JS.
*/
static SubresourceCacheValidationInfo GetSubresourceCacheValidationInfo(
nsIRequest*, nsIURI*);
/**
* Gets cache expiration time for subresources such as images, CSS
* stylesheets, or JS.
*/
static CacheExpirationTime GetSubresourceCacheExpirationTime(nsIRequest*,
nsIURI*);
/**
* Returns true if the request associated with the document should bypass the
* shared sub resource cache.

View file

@ -488,37 +488,6 @@ void nsDOMNavigationTiming::NotifyLargestContentfulRenderForRootContentDocument(
mNavigationStart + TimeDuration::FromMilliseconds(aRenderTime);
}
void nsDOMNavigationTiming::NotifyDOMContentFlushedForRootContentDocument() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mNavigationStart.IsNull());
if (!mDOMContentFlushed.IsNull()) {
return;
}
mDOMContentFlushed = TimeStamp::Now();
if (profiler_thread_is_being_profiled_for_markers() ||
PAGELOAD_LOG_ENABLED()) {
TimeDuration elapsed = mDOMContentFlushed - mNavigationStart;
nsPrintfCString marker(
"DOMContentFlushed after %dms for URL %s, %s",
int(elapsed.ToMilliseconds()),
nsContentUtils::TruncatedURLForDisplay(mLoadedURI).get(),
mDocShellHasBeenActiveSinceNavigationStart
? "foreground tab"
: "this tab was inactive some of the time between navigation start "
"and DOMContentFlushed");
PAGELOAD_LOG(("%s", marker.get()));
PROFILER_MARKER_TEXT(
"DOMContentFlushed", DOM,
MarkerOptions(
MarkerTiming::Interval(mNavigationStart, mDOMContentFlushed),
MarkerInnerWindowIdFromDocShell(mDocShell)),
marker);
}
}
void nsDOMNavigationTiming::NotifyDocShellStateChanged(
DocShellState aDocShellState) {
mDocShellHasBeenActiveSinceNavigationStart &=
@ -595,7 +564,6 @@ nsDOMNavigationTiming::nsDOMNavigationTiming(nsDocShell* aDocShell,
mNavigationStart(aOther->mNavigationStart),
mNonBlankPaint(aOther->mNonBlankPaint),
mContentfulComposite(aOther->mContentfulComposite),
mDOMContentFlushed(aOther->mDOMContentFlushed),
mBeforeUnloadStart(aOther->mBeforeUnloadStart),
mUnloadStart(aOther->mUnloadStart),
mUnloadEnd(aOther->mUnloadEnd),
@ -629,7 +597,6 @@ void mozilla::ipc::IPDLParamTraits<nsDOMNavigationTiming*>::Write(
WriteIPDLParam(aWriter, aActor, aParam->mNavigationStart);
WriteIPDLParam(aWriter, aActor, aParam->mNonBlankPaint);
WriteIPDLParam(aWriter, aActor, aParam->mContentfulComposite);
WriteIPDLParam(aWriter, aActor, aParam->mDOMContentFlushed);
WriteIPDLParam(aWriter, aActor, aParam->mBeforeUnloadStart);
WriteIPDLParam(aWriter, aActor, aParam->mUnloadStart);
WriteIPDLParam(aWriter, aActor, aParam->mUnloadEnd);
@ -669,7 +636,6 @@ bool mozilla::ipc::IPDLParamTraits<nsDOMNavigationTiming*>::Read(
!ReadIPDLParam(aReader, aActor, &timing->mNavigationStart) ||
!ReadIPDLParam(aReader, aActor, &timing->mNonBlankPaint) ||
!ReadIPDLParam(aReader, aActor, &timing->mContentfulComposite) ||
!ReadIPDLParam(aReader, aActor, &timing->mDOMContentFlushed) ||
!ReadIPDLParam(aReader, aActor, &timing->mBeforeUnloadStart) ||
!ReadIPDLParam(aReader, aActor, &timing->mUnloadStart) ||
!ReadIPDLParam(aReader, aActor, &timing->mUnloadEnd) ||

View file

@ -113,9 +113,6 @@ class nsDOMNavigationTiming final : public mozilla::RelativeTimeline {
return TimeStampToDOM(mLargestContentfulRender);
}
DOMTimeMilliSec GetTimeToTTFI() const { return TimeStampToDOM(mTTFI); }
DOMTimeMilliSec GetTimeToDOMContentFlushed() const {
return TimeStampToDOM(mDOMContentFlushed);
}
DOMHighResTimeStamp GetUnloadEventStartHighRes() {
mozilla::TimeStamp stamp = GetUnloadEventStartTimeStamp();
@ -182,7 +179,6 @@ class nsDOMNavigationTiming final : public mozilla::RelativeTimeline {
const mozilla::TimeStamp& aCompositeEndTime);
void NotifyLargestContentfulRenderForRootContentDocument(
const DOMHighResTimeStamp& aRenderTime);
void NotifyDOMContentFlushedForRootContentDocument();
void NotifyDocShellStateChanged(DocShellState aDocShellState);
void MaybeAddLCPProfilerMarker(mozilla::MarkerInnerWindowId aInnerWindowID);
@ -243,7 +239,6 @@ class nsDOMNavigationTiming final : public mozilla::RelativeTimeline {
mozilla::TimeStamp mNonBlankPaint;
mozilla::TimeStamp mContentfulComposite;
mozilla::TimeStamp mLargestContentfulRender;
mozilla::TimeStamp mDOMContentFlushed;
mozilla::TimeStamp mBeforeUnloadStart;
mozilla::TimeStamp mUnloadStart;

View file

@ -356,17 +356,16 @@ class IsItemInRangeComparator {
nsContentUtils::NodeIndexCache* mCache;
};
bool nsINode::IsSelected(const uint32_t aStartOffset,
const uint32_t aEndOffset) const {
bool nsINode::IsSelected(const uint32_t aStartOffset, const uint32_t aEndOffset,
SelectionNodeCache* aCache) const {
MOZ_ASSERT(aStartOffset <= aEndOffset);
const nsINode* n = GetClosestCommonInclusiveAncestorForRangeInSelection(this);
NS_ASSERTION(n || !IsMaybeSelected(),
"A node without a common inclusive ancestor for a range in "
"Selection is for sure not selected.");
// Collect the selection objects for potential ranges.
nsTHashSet<Selection*> ancestorSelections;
AutoTArray<Selection*, 1> ancestorSelections;
for (; n; n = GetClosestCommonInclusiveAncestorForRangeInSelection(
n->GetParentNode())) {
const LinkedList<AbstractRange>* ranges =
@ -380,13 +379,17 @@ bool nsINode::IsSelected(const uint32_t aStartOffset,
// Looks like that IsInSelection() assert fails sometimes...
if (range->IsInAnySelection()) {
for (const WeakPtr<Selection>& selection : range->GetSelections()) {
if (selection) {
ancestorSelections.Insert(selection);
if (selection && !ancestorSelections.Contains(selection)) {
ancestorSelections.AppendElement(selection);
}
}
}
}
}
if (aCache && aCache->MaybeCollectNodesAndCheckIfFullySelectedInAnyOf(
this, ancestorSelections)) {
return true;
}
nsContentUtils::NodeIndexCache cache;
IsItemInRangeComparator comparator{*this, aStartOffset, aEndOffset, &cache};

View file

@ -103,6 +103,7 @@ class MutationObservers;
template <typename T>
class Optional;
class OwningNodeOrString;
class SelectionNodeCache;
template <typename>
class Sequence;
class ShadowRoot;
@ -1654,8 +1655,12 @@ class nsINode : public mozilla::dom::EventTarget {
* for that nsRange. Collapsed ranges always counts as non-overlapping.
*
* @param aStartOffset has to be less or equal to aEndOffset.
* @param aCache A cache which contains all fully selected nodes for each
* selection. If present, this provides a fast path to check if
* a node is fully selected.
*/
bool IsSelected(uint32_t aStartOffset, uint32_t aEndOffset) const;
bool IsSelected(uint32_t aStartOffset, uint32_t aEndOffset,
mozilla::dom::SelectionNodeCache* aCache = nullptr) const;
/**
* Get the root element of the text editor associated with this node or the

View file

@ -105,7 +105,6 @@ nsImageLoadingContent::nsImageLoadingContent()
mRequestGeneration(0),
mLoadingEnabled(true),
mLoading(false),
mNewRequestsWillNeedAnimationReset(false),
mUseUrgentStartForChannel(false),
mLazyLoading(false),
mStateChangerDepth(0),
@ -971,7 +970,6 @@ nsImageLoadingContent::LoadImageWithChannel(nsIChannel* aChannel,
if (NS_SUCCEEDED(rv)) {
CloneScriptedRequests(req);
TrackImage(req);
ResetAnimationIfNeeded();
return NS_OK;
}
@ -1167,7 +1165,6 @@ nsresult nsImageLoadingContent::LoadImage(nsIURI* aNewURI, bool aForce,
CloneScriptedRequests(req);
TrackImage(req);
ResetAnimationIfNeeded();
// Handle cases when we just ended up with a request but it's already done.
// In that situation we have to synchronously switch that request to being
@ -1483,10 +1480,6 @@ RefPtr<imgRequestProxy>& nsImageLoadingContent::PrepareCurrentRequest(
// Get rid of anything that was there previously.
ClearCurrentRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DiscardImages));
if (mNewRequestsWillNeedAnimationReset) {
mCurrentRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET;
}
if (aImageLoadType == eImageLoadType_Imageset) {
mCurrentRequestFlags |= REQUEST_IS_IMAGESET;
}
@ -1500,10 +1493,6 @@ RefPtr<imgRequestProxy>& nsImageLoadingContent::PreparePendingRequest(
// Get rid of anything that was there previously.
ClearPendingRequest(NS_BINDING_ABORTED, Some(OnNonvisible::DiscardImages));
if (mNewRequestsWillNeedAnimationReset) {
mPendingRequestFlags |= REQUEST_NEEDS_ANIMATION_RESET;
}
if (aImageLoadType == eImageLoadType_Imageset) {
mPendingRequestFlags |= REQUEST_IS_IMAGESET;
}
@ -1562,7 +1551,6 @@ void nsImageLoadingContent::MakePendingRequestCurrent() {
mPendingRequestFlags = 0;
mCurrentRequestRegistered = mPendingRequestRegistered;
mPendingRequestRegistered = false;
ResetAnimationIfNeeded();
}
void nsImageLoadingContent::ClearCurrentRequest(
@ -1606,16 +1594,6 @@ void nsImageLoadingContent::ClearPendingRequest(
mPendingRequestFlags = 0;
}
void nsImageLoadingContent::ResetAnimationIfNeeded() {
if (mCurrentRequest &&
(mCurrentRequestFlags & REQUEST_NEEDS_ANIMATION_RESET)) {
nsCOMPtr<imgIContainer> container;
mCurrentRequest->GetImage(getter_AddRefs(container));
if (container) container->ResetAnimation();
mCurrentRequestFlags &= ~REQUEST_NEEDS_ANIMATION_RESET;
}
}
bool nsImageLoadingContent::HaveSize(imgIRequest* aImage) {
// Handle the null case
if (!aImage) return false;

View file

@ -424,13 +424,6 @@ class nsImageLoadingContent : public nsIImageLoadingContent {
nsresult aReason,
const Maybe<OnNonvisible>& aNonvisibleAction = Nothing());
/**
* Reset animation of the current request if
* |mNewRequestsWillNeedAnimationReset| was true when the request was
* prepared.
*/
void ResetAnimationIfNeeded();
/**
* Static helper method to tell us if we have the size of a request. The
* image may be null.
@ -467,13 +460,11 @@ class nsImageLoadingContent : public nsIImageLoadingContent {
uint8_t mPendingRequestFlags = 0;
enum {
// Set if the request needs ResetAnimation called on it.
REQUEST_NEEDS_ANIMATION_RESET = 1 << 0,
// Set if the request is currently tracked with the document.
REQUEST_IS_TRACKED = 1 << 1,
REQUEST_IS_TRACKED = 1 << 0,
// Set if this is an imageset request, such as from <img srcset> or
// <picture>
REQUEST_IS_IMAGESET = 1 << 2,
REQUEST_IS_IMAGESET = 1 << 1,
};
// If the image was blocked or if there was an error loading, it's nice to
@ -565,16 +556,6 @@ class nsImageLoadingContent : public nsIImageLoadingContent {
*/
bool mLoading : 1;
/**
* A hack to get animations to reset, see bug 594771. On requests
* that originate from setting .src, we mark them for needing their animation
* reset when they are ready. mNewRequestsWillNeedAnimationReset is set to
* true while preparing such requests (as a hack around needing to change an
* interface), and the other two booleans store which of the current
* and pending requests are of the sort that need their animation restarted.
*/
bool mNewRequestsWillNeedAnimationReset : 1;
/**
* Flag to indicate whether the channel should be mark as urgent-start.
* It should be set in *Element and passed to nsContentUtils::LoadImage.

View file

@ -78,63 +78,71 @@ nsLineBreaker::~nsLineBreaker() {
"Should have Reset() before destruction!");
}
/* static */
bool nsLineBreaker::ShouldCapitalize(uint32_t aChar, bool& aCapitalizeNext) {
using mozilla::intl::GeneralCategory;
auto category = UnicodeProperties::CharType(aChar);
switch (category) {
case GeneralCategory::Uppercase_Letter:
case GeneralCategory::Lowercase_Letter:
case GeneralCategory::Titlecase_Letter:
case GeneralCategory::Modifier_Letter:
case GeneralCategory::Other_Letter:
case GeneralCategory::Decimal_Number:
case GeneralCategory::Letter_Number:
case GeneralCategory::Other_Number:
if (aCapitalizeNext) {
aCapitalizeNext = false;
return true;
}
break;
case GeneralCategory::Space_Separator:
case GeneralCategory::Line_Separator:
case GeneralCategory::Paragraph_Separator:
case GeneralCategory::Dash_Punctuation:
case GeneralCategory::Initial_Punctuation:
/* These punctuation categories are excluded, for examples like
* "what colo[u]r" -> "What Colo[u]r?" (rather than "What Colo[U]R?")
* and
* "snake_case" -> "Snake_case" (to match word selection behavior)
case GeneralCategory::Open_Punctuation:
case GeneralCategory::Close_Punctuation:
case GeneralCategory::Connector_Punctuation:
*/
aCapitalizeNext = true;
break;
case GeneralCategory::Final_Punctuation:
/* Special-case: exclude Unicode single-close-quote/apostrophe,
for examples like "Lowes" etc. */
if (aChar != 0x2019) {
aCapitalizeNext = true;
}
break;
case GeneralCategory::Other_Punctuation:
/* Special-case: exclude ASCII apostrophe, for "Lowe's" etc.,
and MIDDLE DOT, for Catalan "l·l". */
if (aChar != '\'' && aChar != 0x00B7) {
aCapitalizeNext = true;
}
break;
default:
break;
}
return false;
}
static void SetupCapitalization(const char16_t* aWord, uint32_t aLength,
bool* aCapitalization) {
// Capitalize the first alphanumeric character after a space or punctuation.
using mozilla::intl::GeneralCategory;
bool capitalizeNextChar = true;
for (uint32_t i = 0; i < aLength; ++i) {
uint32_t ch = aWord[i];
if (i + 1 < aLength && NS_IS_SURROGATE_PAIR(ch, aWord[i + 1])) {
ch = SURROGATE_TO_UCS4(ch, aWord[i + 1]);
}
auto category = UnicodeProperties::CharType(ch);
switch (category) {
case GeneralCategory::Uppercase_Letter:
case GeneralCategory::Lowercase_Letter:
case GeneralCategory::Titlecase_Letter:
case GeneralCategory::Modifier_Letter:
case GeneralCategory::Other_Letter:
case GeneralCategory::Decimal_Number:
case GeneralCategory::Letter_Number:
case GeneralCategory::Other_Number:
if (capitalizeNextChar) {
aCapitalization[i] = true;
capitalizeNextChar = false;
}
break;
case GeneralCategory::Space_Separator:
case GeneralCategory::Line_Separator:
case GeneralCategory::Paragraph_Separator:
case GeneralCategory::Dash_Punctuation:
case GeneralCategory::Initial_Punctuation:
/* These punctuation categories are excluded, for examples like
* "what colo[u]r" -> "What Colo[u]r?" (rather than "What Colo[U]R?")
* and
* "snake_case" -> "Snake_case" (to match word selection behavior)
case GeneralCategory::Open_Punctuation:
case GeneralCategory::Close_Punctuation:
case GeneralCategory::Connector_Punctuation:
*/
capitalizeNextChar = true;
break;
case GeneralCategory::Final_Punctuation:
/* Special-case: exclude Unicode single-close-quote/apostrophe,
for examples like "Lowes" etc. */
if (ch != 0x2019) {
capitalizeNextChar = true;
}
break;
case GeneralCategory::Other_Punctuation:
/* Special-case: exclude ASCII apostrophe, for "Lowe's" etc.,
and MIDDLE DOT, for Catalan "l·l". */
if (ch != '\'' && ch != 0x00B7) {
capitalizeNextChar = true;
}
break;
default:
break;
}
aCapitalization[i] =
nsLineBreaker::ShouldCapitalize(ch, capitalizeNextChar);
if (!IS_IN_BMP(ch)) {
++i;
}

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