Update On Sat Jan 27 19:40:48 CET 2024

This commit is contained in:
github-action[bot] 2024-01-27 19:40:48 +01:00
parent c893f323ba
commit 06956fe708
651 changed files with 10059 additions and 4887 deletions

View file

@ -1112,13 +1112,6 @@ nsDefaultCommandLineHandler.prototype = {
var urilist = [];
var principalList = [];
if (cmdLine && cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
// Since the purpose of this is to record early in startup,
// only record on launches, not already-running invocations.
Services.telemetry.setEventRecordingEnabled("telemetry", true);
Glean.fogValidation.validateEarlyEvent.record();
}
if (AppConstants.platform == "win") {
// Windows itself does disk I/O when the notification service is
// initialized, so make sure that is lazy.

View file

@ -610,19 +610,19 @@ html {
.additional-cta {
margin: 0;
}
&.cta-link {
@include text-link-styles;
&.cta-link {
@include text-link-styles;
padding: 0;
font-weight: normal;
}
padding: 0;
font-weight: normal;
}
&.secondary {
&:hover,
&[open] {
background-color: var(--in-content-button-background-hover);
&.secondary {
&:hover,
&[open] {
background-color: var(--in-content-button-background-hover);
}
}
}
}

View file

@ -1390,7 +1390,7 @@ html {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta {
margin: 0;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.cta-link {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.cta-link {
background: none;
text-decoration: underline;
cursor: pointer;
@ -1398,18 +1398,18 @@ html {
padding: 0;
font-weight: normal;
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.cta-link:hover {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.cta-link:hover {
color: var(--link-color-hover);
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.cta-link:active {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.cta-link:active {
color: var(--link-color-active);
}
@media (prefers-contrast) {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.cta-link:active {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.cta-link:active {
text-decoration: none;
}
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.secondary:hover, .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box.secondary[open] {
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.secondary:hover, .onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .additional-cta-box .additional-cta.secondary[open] {
background-color: var(--in-content-button-background-hover);
}
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons.additional-cta-container {

View file

@ -0,0 +1,398 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This module provides the means to monitor and query for tab collections against open
* browser windows and allow listeners to be notified of changes to those collections.
*/
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
const TAB_ATTRS_TO_WATCH = Object.freeze(["image", "label"]);
const TAB_CHANGE_EVENTS = Object.freeze([
"TabAttrModified",
"TabClose",
"TabMove",
"TabOpen",
"TabPinned",
"TabUnpinned",
]);
const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([
"activate",
"TabAttrModified",
"TabClose",
"TabOpen",
"TabSelect",
]);
// Debounce tab/tab recency changes and dispatch max once per frame at 60fps
const CHANGES_DEBOUNCE_MS = 1000 / 60;
/**
* A sort function used to order tabs by most-recently seen and active.
*/
export function lastSeenActiveSort(a, b) {
let dt = b.lastSeenActive - a.lastSeenActive;
if (dt) {
return dt;
}
// try to break a deadlock by sorting the selected tab higher
if (!(a.selected || b.selected)) {
return 0;
}
return a.selected ? -1 : 1;
}
/**
* Provides a object capable of monitoring and accessing tab collections for either
* private or non-private browser windows. As the class extends EventTarget, consumers
* should add event listeners for the change events.
*
* @param {boolean} options.usePrivateWindows
Constrain to only windows that match this privateness. Defaults to false.
* @param {Window | null} options.exclusiveWindow
* Constrain to only a specific window.
*/
class OpenTabsTarget extends EventTarget {
#changedWindowsByType = {
TabChange: new Set(),
TabRecencyChange: new Set(),
};
#dispatchChangesTask;
#started = false;
#watchedWindows = new Set();
#exclusiveWindowWeakRef = null;
usePrivateWindows = false;
constructor(options = {}) {
super();
this.usePrivateWindows = !!options.usePrivateWindows;
if (options.exclusiveWindow) {
this.exclusiveWindow = options.exclusiveWindow;
this.everyWindowCallbackId = `opentabs-${this.exclusiveWindow.windowGlobalChild.innerWindowId}`;
} else {
this.everyWindowCallbackId = `opentabs-${
this.usePrivateWindows ? "private" : "non-private"
}`;
}
}
get exclusiveWindow() {
return this.#exclusiveWindowWeakRef?.get();
}
set exclusiveWindow(newValue) {
if (newValue) {
this.#exclusiveWindowWeakRef = Cu.getWeakReference(newValue);
} else {
this.#exclusiveWindowWeakRef = null;
}
}
includeWindowFilter(win) {
if (this.#exclusiveWindowWeakRef) {
return win == this.exclusiveWindow;
}
return (
win.gBrowser &&
!win.closed &&
this.usePrivateWindows == lazy.PrivateBrowsingUtils.isWindowPrivate(win)
);
}
get currentWindows() {
return lazy.EveryWindow.readyWindows.filter(win =>
this.includeWindowFilter(win)
);
}
/**
* A promise that resolves to all matched windows once their delayedStartupPromise resolves
*/
get readyWindowsPromise() {
let windowList = Array.from(
Services.wm.getEnumerator("navigator:browser")
).filter(win => {
// avoid waiting for windows we definitely don't care about
if (this.#exclusiveWindowWeakRef) {
return this.exclusiveWindow == win;
}
return (
this.usePrivateWindows == lazy.PrivateBrowsingUtils.isWindowPrivate(win)
);
});
return Promise.allSettled(
windowList.map(win => win.delayedStartupPromise)
).then(() => {
// re-filter the list as properties might have changed in the interim
return windowList.filter(win => this.includeWindowFilter);
});
}
haveListenersForEvent(eventType) {
switch (eventType) {
case "TabChange":
return Services.els.hasListenersFor(this, "TabChange");
case "TabRecencyChange":
return Services.els.hasListenersFor(this, "TabRecencyChange");
default:
return false;
}
}
get haveAnyListeners() {
return (
this.haveListenersForEvent("TabChange") ||
this.haveListenersForEvent("TabRecencyChange")
);
}
/*
* @param {string} type
* Either "TabChange" or "TabRecencyChange"
* @param {Object|Function} listener
* @param {Object} [options]
*/
addEventListener(type, listener, options) {
let hadListeners = this.haveAnyListeners;
super.addEventListener(type, listener, options);
// if this is the first listener, start up all the window & tab monitoring
if (!hadListeners && this.haveAnyListeners) {
this.start();
}
}
/*
* @param {string} type
* Either "TabChange" or "TabRecencyChange"
* @param {Object|Function} listener
*/
removeEventListener(type, listener) {
let hadListeners = this.haveAnyListeners;
super.removeEventListener(type, listener);
// if this was the last listener, we can stop all the window & tab monitoring
if (hadListeners && !this.haveAnyListeners) {
this.stop();
}
}
/**
* Begin watching for tab-related events from all browser windows matching the instance's private property
*/
start() {
if (this.#started) {
return;
}
// EveryWindow will call #watchWindow for each open window once its delayedStartupPromise resolves.
lazy.EveryWindow.registerCallback(
this.everyWindowCallbackId,
win => this.#watchWindow(win),
win => this.#unwatchWindow(win)
);
this.#started = true;
}
/**
* Stop watching for tab-related events from all browser windows and clean up.
*/
stop() {
if (this.#started) {
lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId);
this.#started = false;
}
for (let changedWindows of Object.values(this.#changedWindowsByType)) {
changedWindows.clear();
}
this.#watchedWindows.clear();
this.#dispatchChangesTask?.disarm();
}
/**
* Add listeners for tab-related events from the given window. The consumer's
* listeners will always be notified at least once for newly-watched window.
*/
#watchWindow(win) {
if (!this.includeWindowFilter(win)) {
return;
}
this.#watchedWindows.add(win);
const { tabContainer } = win.gBrowser;
tabContainer.addEventListener("TabAttrModified", this);
tabContainer.addEventListener("TabClose", this);
tabContainer.addEventListener("TabMove", this);
tabContainer.addEventListener("TabOpen", this);
tabContainer.addEventListener("TabPinned", this);
tabContainer.addEventListener("TabUnpinned", this);
tabContainer.addEventListener("TabSelect", this);
win.addEventListener("activate", this);
this.#scheduleEventDispatch("TabChange", {});
this.#scheduleEventDispatch("TabRecencyChange", {});
}
/**
* Remove all listeners for tab-related events from the given window.
* Consumers will always be notified at least once for unwatched window.
*/
#unwatchWindow(win) {
// We check the window is in our watchedWindows collection rather than currentWindows
// as the unwatched window may not match the criteria we used to watch it anymore,
// and we need to unhook our event listeners regardless.
if (this.#watchedWindows.has(win)) {
this.#watchedWindows.delete(win);
const { tabContainer } = win.gBrowser;
tabContainer.removeEventListener("TabAttrModified", this);
tabContainer.removeEventListener("TabClose", this);
tabContainer.removeEventListener("TabMove", this);
tabContainer.removeEventListener("TabOpen", this);
tabContainer.removeEventListener("TabPinned", this);
tabContainer.removeEventListener("TabSelect", this);
tabContainer.removeEventListener("TabUnpinned", this);
win.removeEventListener("activate", this);
this.#scheduleEventDispatch("TabChange", {});
this.#scheduleEventDispatch("TabRecencyChange", {});
}
}
/**
* Flag the need to notify all our consumers of a change to open tabs.
* Repeated calls within approx 16ms will be consolidated
* into one event dispatch.
*/
#scheduleEventDispatch(eventType, { sourceWindowId } = {}) {
if (!this.haveListenersForEvent(eventType)) {
return;
}
this.#changedWindowsByType[eventType].add(sourceWindowId);
// Queue up an event dispatch - we use a deferred task to make this less noisy by
// consolidating multiple change events into one.
if (!this.#dispatchChangesTask) {
this.#dispatchChangesTask = new lazy.DeferredTask(() => {
this.#dispatchChanges();
}, CHANGES_DEBOUNCE_MS);
}
this.#dispatchChangesTask.arm();
}
#dispatchChanges() {
this.#dispatchChangesTask?.disarm();
for (let [eventType, changedWindowIds] of Object.entries(
this.#changedWindowsByType
)) {
if (this.haveListenersForEvent(eventType) && changedWindowIds.size) {
this.dispatchEvent(
new CustomEvent(eventType, {
detail: {
windowIds: [...changedWindowIds],
},
})
);
changedWindowIds.clear();
}
}
}
/*
* @param {Window} win
* @returns {Array<Tab>}
* The list of visible tabs for the browser window
*/
getTabsForWindow(win) {
if (this.currentWindows.includes(win)) {
return [...win.gBrowser.visibleTabs];
}
return [];
}
/*
* @returns {Array<Tab>}
* A by-recency-sorted, aggregated list of tabs from all the same-privateness browser windows.
*/
getRecentTabs() {
const tabs = [];
for (let win of this.currentWindows) {
tabs.push(...this.getTabsForWindow(win));
}
tabs.sort(lastSeenActiveSort);
return tabs;
}
handleEvent({ detail, target, type }) {
const win = target.ownerGlobal;
// NOTE: we already filtered on privateness by not listening for those events
// from private/not-private windows
if (
type == "TabAttrModified" &&
!detail.changed.some(attr => TAB_ATTRS_TO_WATCH.includes(attr))
) {
return;
}
if (TAB_RECENCY_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabRecencyChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
});
}
if (TAB_CHANGE_EVENTS.includes(type)) {
this.#scheduleEventDispatch("TabChange", {
sourceWindowId: win.windowGlobalChild.innerWindowId,
});
}
}
}
const gExclusiveWindows = new (class {
perWindowInstances = new WeakMap();
constructor() {
Services.obs.addObserver(this, "domwindowclosed");
}
observe(subject, topic, data) {
let win = subject;
let winTarget = this.perWindowInstances.get(win);
if (winTarget) {
winTarget.stop();
this.perWindowInstances.delete(win);
}
}
})();
/**
* Get an OpenTabsTarget instance constrained to a specific window.
*
* @param {Window} exclusiveWindow
* @returns {OpenTabsTarget}
*/
const getTabsTargetForWindow = function (exclusiveWindow) {
let instance = gExclusiveWindows.perWindowInstances.get(exclusiveWindow);
if (instance) {
return instance;
}
instance = new OpenTabsTarget({
exclusiveWindow,
});
gExclusiveWindows.perWindowInstances.set(exclusiveWindow, instance);
return instance;
};
const NonPrivateTabs = new OpenTabsTarget({
usePrivateWindows: false,
});
const PrivateTabs = new OpenTabsTarget({
usePrivateWindows: true,
});
export { NonPrivateTabs, PrivateTabs, getTabsTargetForWindow };

View file

@ -28,12 +28,15 @@ const SEARCH_DEBOUNCE_TIMEOUT_MS = 1000;
* The query that is currently in the search box.
* @property {number} size
* The width (number of characters) of the search box.
* @property {string} pageName
* The hash for the page name that the search input is located on.
*/
export default class FxviewSearchTextbox extends MozLitElement {
static properties = {
placeholder: { type: String },
query: { type: String },
size: { type: Number },
pageName: { type: String },
};
static queries = {
@ -98,6 +101,16 @@ export default class FxviewSearchTextbox extends MozLitElement {
detail: { query: this.query },
})
);
Services.telemetry.recordEvent(
"firefoxview_next",
"search_initiated",
"search",
null,
{
page: this.pageName,
}
);
}
render() {

View file

@ -55,6 +55,7 @@ class HistoryInView extends ViewPage {
this.sortOption = "date";
this.profileAge = 8;
this.fullyUpdated = false;
this.cumulativeSearches = 0;
}
start() {
@ -253,6 +254,14 @@ class HistoryInView extends ViewPage {
{}
);
if (this.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add("history", this.cumulativeSearches);
this.cumulativeSearches = 0;
}
let currentWindow = this.getWindow();
if (currentWindow.openTrustedLinkIn) {
let where = lazy.BrowserUtils.whereToOpenLink(
@ -286,6 +295,7 @@ class HistoryInView extends ViewPage {
null,
{
sort_type: this.sortOption,
search_start: this.searchQuery ? "true" : "false",
}
);
await this.updateHistoryData();
@ -552,6 +562,7 @@ class HistoryInView extends ViewPage {
data-l10n-id="firefoxview-search-text-box-history"
data-l10n-attrs="placeholder"
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing ? "recentbrowsing" : "history"}
@fxview-search-textbox-query=${this.onSearchQuery}
></fxview-search-textbox>
</div>`
@ -641,6 +652,11 @@ class HistoryInView extends ViewPage {
// from the API.
this.createHistoryMaps();
}
if (changedProperties.has("searchQuery")) {
this.cumulativeSearches = this.searchQuery
? this.cumulativeSearches + 1
: 0;
}
}
}
customElements.define("view-history", HistoryInView);

View file

@ -21,7 +21,8 @@ import { ViewPage, ViewPageContent } from "./viewpage.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs",
getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});
@ -31,18 +32,16 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
).getFxAccountsSingleton();
});
const TOPIC_CURRENT_BROWSER_CHANGED = "net:current-browser-id";
/**
* A collection of open tabs grouped by window.
*
* @property {Map<Window, MozTabbrowserTab[]>} windows
* A mapping of windows to their respective list of open tabs.
* @property {Array<Window>} windows
* A list of windows with the same privateness
*/
class OpenTabsInView extends ViewPage {
static properties = {
...ViewPage.properties,
windows: { type: Map },
windows: { type: Array },
searchQuery: { type: String },
};
static queries = {
@ -50,18 +49,20 @@ class OpenTabsInView extends ViewPage {
searchTextbox: "fxview-search-textbox",
};
static TAB_ATTRS_TO_WATCH = Object.freeze(["image", "label"]);
initialWindowsReady = false;
currentWindow = null;
openTabsTarget = null;
constructor() {
super();
this._started = false;
this.everyWindowCallbackId = `firefoxview-${Services.uuid.generateUUID()}`;
this.windows = new Map();
this.windows = [];
this.currentWindow = this.getWindow();
this.isPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(
this.currentWindow
);
this.boundObserve = (...args) => this.observe(...args);
if (lazy.PrivateBrowsingUtils.isWindowPrivate(this.currentWindow)) {
this.openTabsTarget = lazy.getTabsTargetForWindow(this.currentWindow);
} else {
this.openTabsTarget = lazy.NonPrivateTabs;
}
this.searchQuery = "";
}
@ -71,43 +72,19 @@ class OpenTabsInView extends ViewPage {
}
this._started = true;
Services.obs.addObserver(this.boundObserve, TOPIC_CURRENT_BROWSER_CHANGED);
if (this.recentBrowsing) {
this.openTabsTarget.addEventListener("TabRecencyChange", this);
} else {
this.openTabsTarget.addEventListener("TabChange", this);
}
lazy.EveryWindow.registerCallback(
this.everyWindowCallbackId,
win => {
if (win.gBrowser && this._shouldShowOpenTabs(win) && !win.closed) {
const { tabContainer } = win.gBrowser;
tabContainer.addEventListener("TabSelect", this);
tabContainer.addEventListener("TabAttrModified", this);
tabContainer.addEventListener("TabClose", this);
tabContainer.addEventListener("TabMove", this);
tabContainer.addEventListener("TabOpen", this);
tabContainer.addEventListener("TabPinned", this);
tabContainer.addEventListener("TabUnpinned", this);
// BrowserWindowWatcher doesnt always notify "net:current-browser-id" when
// restoring a window, so we need to listen for "activate" events here as well.
win.addEventListener("activate", this);
this._updateOpenTabsList();
}
},
win => {
if (win.gBrowser && this._shouldShowOpenTabs(win)) {
const { tabContainer } = win.gBrowser;
tabContainer.removeEventListener("TabSelect", this);
tabContainer.removeEventListener("TabAttrModified", this);
tabContainer.removeEventListener("TabClose", this);
tabContainer.removeEventListener("TabMove", this);
tabContainer.removeEventListener("TabOpen", this);
tabContainer.removeEventListener("TabPinned", this);
tabContainer.removeEventListener("TabUnpinned", this);
win.removeEventListener("activate", this);
this._updateOpenTabsList();
}
}
);
// EveryWindow will invoke the callback for existing windows - including this one
// So this._updateOpenTabsList will get called for the already-open window
// To resolve the race between this component wanting to render all the windows'
// tabs, while those windows are still potentially opening, flip this property
// once the promise resolves and we'll bail out of rendering until then.
this.openTabsTarget.readyWindowsPromise.finally(() => {
this.initialWindowsReady = true;
this._updateWindowList();
});
for (let card of this.viewCards) {
card.paused = false;
@ -122,6 +99,13 @@ class OpenTabsInView extends ViewPage {
}
}
shouldUpdate(changedProperties) {
if (!this.initialWindowsReady) {
return false;
}
return super.shouldUpdate(changedProperties);
}
disconnectedCallback() {
super.disconnectedCallback();
this.stop();
@ -134,12 +118,8 @@ class OpenTabsInView extends ViewPage {
this._started = false;
this.paused = true;
lazy.EveryWindow.unregisterCallback(this.everyWindowCallbackId);
Services.obs.removeObserver(
this.boundObserve,
TOPIC_CURRENT_BROWSER_CHANGED
);
this.openTabsTarget.removeEventListener("TabChange", this);
this.openTabsTarget.removeEventListener("TabRecencyChange", this);
for (let card of this.viewCards) {
card.paused = true;
@ -162,14 +142,6 @@ class OpenTabsInView extends ViewPage {
this.stop();
}
async observe(subject, topic, data) {
switch (topic) {
case TOPIC_CURRENT_BROWSER_CHANGED:
this.requestUpdate();
break;
}
}
render() {
if (this.recentBrowsing) {
return this.getRecentBrowsingTemplate();
@ -177,7 +149,8 @@ class OpenTabsInView extends ViewPage {
let currentWindowIndex, currentWindowTabs;
let index = 1;
const otherWindows = [];
this.windows.forEach((tabs, win) => {
this.windows.forEach(win => {
const tabs = this.openTabsTarget.getTabsForWindow(win);
if (win === this.currentWindow) {
currentWindowIndex = index++;
currentWindowTabs = tabs;
@ -187,13 +160,13 @@ class OpenTabsInView extends ViewPage {
});
const cardClasses = classMap({
"height-limited": this.windows.size > 3,
"width-limited": this.windows.size > 1,
"height-limited": this.windows.length > 3,
"width-limited": this.windows.length > 1,
});
let cardCount;
if (this.windows.size <= 1) {
if (this.windows.length <= 1) {
cardCount = "one";
} else if (this.windows.size === 2) {
} else if (this.windows.length === 2) {
cardCount = "two";
} else {
cardCount = "three-or-more";
@ -220,6 +193,7 @@ class OpenTabsInView extends ViewPage {
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing ? "recentbrowsing" : "opentabs"}
></fxview-search-textbox>
</div>`
)}
@ -276,19 +250,7 @@ class OpenTabsInView extends ViewPage {
* The recent browsing template.
*/
getRecentBrowsingTemplate() {
const tabs = Array.from(this.windows.values())
.flat()
.sort((a, b) => {
let dt = b.lastSeenActive - a.lastSeenActive;
if (dt) {
return dt;
}
// try to break a deadlock by sorting the selected tab higher
if (!(a.selected || b.selected)) {
return 0;
}
return a.selected ? -1 : 1;
});
const tabs = this.openTabsTarget.getRecentTabs();
return html`<view-opentabs-card
.tabs=${tabs}
.recentBrowsing=${true}
@ -302,76 +264,46 @@ class OpenTabsInView extends ViewPage {
this.onSearchQuery({ detail });
return;
}
const win = target.ownerGlobal;
const tabs = this.windows.get(win);
let windowIds;
switch (type) {
case "TabSelect": {
case "TabRecencyChange":
case "TabChange":
// if we're switching away from our tab, we can halt any updates immediately
if (detail.previousTab == this.getBrowserTab()) {
if (!this.isSelectedBrowserTab) {
this.stop();
}
return;
}
case "TabAttrModified":
if (
!detail.changed.some(attr =>
OpenTabsInView.TAB_ATTRS_TO_WATCH.includes(attr)
)
) {
// We don't care about this attr, bail out to avoid change detection.
return;
}
break;
case "TabClose":
tabs.splice(target._tPos, 1);
break;
case "TabMove":
[tabs[detail], tabs[target._tPos]] = [tabs[target._tPos], tabs[detail]];
break;
case "TabOpen":
tabs.splice(target._tPos, 0, target);
break;
case "TabPinned":
case "TabUnpinned":
this.windows.set(win, [...win.gBrowser.tabs]);
windowIds = detail.windowIds;
this._updateWindowList();
break;
}
this.requestUpdate();
if (!this.recentBrowsing) {
const cardForWin = this.shadowRoot.querySelector(
`view-opentabs-card[data-inner-id="${win.windowGlobalChild.innerWindowId}"]`
if (this.recentBrowsing) {
return;
}
if (windowIds?.length) {
// there were tab changes to one or more windows
for (let winId of windowIds) {
const cardForWin = this.shadowRoot.querySelector(
`view-opentabs-card[data-inner-id="${winId}"]`
);
if (this.searchQuery) {
cardForWin?.updateSearchResults();
}
cardForWin?.requestUpdate();
}
} else {
let winId = window.windowGlobalChild.innerWindowId;
let cardForWin = this.shadowRoot.querySelector(
`view-opentabs-card[data-inner-id="${winId}"]`
);
if (this.searchQuery) {
cardForWin?.updateSearchResults();
}
cardForWin?.requestUpdate();
}
}
_updateOpenTabsList() {
this.windows = this._getOpenTabsPerWindow();
}
/**
* Get a list of open tabs for each window.
*
* @returns {Map<Window, MozTabbrowserTab[]>}
*/
_getOpenTabsPerWindow() {
return new Map(
Array.from(Services.wm.getEnumerator("navigator:browser"))
.filter(
win => win.gBrowser && this._shouldShowOpenTabs(win) && !win.closed
)
.map(win => [win, [...win.gBrowser.tabs]])
);
}
_shouldShowOpenTabs(win) {
return (
win == this.currentWindow ||
(!this.isPrivateWindow && !lazy.PrivateBrowsingUtils.isWindowPrivate(win))
);
async _updateWindowList() {
this.windows = this.openTabsTarget.currentWindows;
}
}
customElements.define("view-opentabs", OpenTabsInView);
@ -395,6 +327,7 @@ class OpenTabsInViewCard extends ViewPageContent {
searchQuery: { type: String },
searchResults: { type: Array },
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
@ -408,6 +341,7 @@ class OpenTabsInViewCard extends ViewPageContent {
this.searchQuery = "";
this.searchResults = null;
this.showAll = false;
this.cumulativeSearches = 0;
}
static queries = {
@ -460,6 +394,15 @@ class OpenTabsInViewCard extends ViewPageContent {
(event.type == "keydown" && event.code == "Space")
) {
event.preventDefault();
Services.telemetry.recordEvent(
"firefoxview_next",
"search_show_all",
"showallbutton",
null,
{
section: "opentabs",
}
);
this.showAll = true;
}
}
@ -480,6 +423,16 @@ class OpenTabsInViewCard extends ViewPageContent {
window: this.title || "Window 1 (Current)",
}
);
if (this.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add(
this.recentBrowsing ? "recentbrowsing" : "opentabs",
this.cumulativeSearches
);
this.cumulativeSearches = 0;
}
}
viewVisibleCallback() {
@ -559,6 +512,9 @@ class OpenTabsInViewCard extends ViewPageContent {
willUpdate(changedProperties) {
if (changedProperties.has("searchQuery")) {
this.showAll = false;
this.cumulativeSearches = this.searchQuery
? this.cumulativeSearches + 1
: 0;
}
if (changedProperties.has("searchQuery") || changedProperties.has("tabs")) {
this.updateSearchResults();

View file

@ -54,6 +54,7 @@ class RecentBrowsingInView extends ViewPage {
data-l10n-id="firefoxview-search-text-box-recentbrowsing"
data-l10n-attrs="placeholder"
.size=${this.searchTextboxSize}
pageName="recentbrowsing"
></fxview-search-textbox>
</div>`
)}

View file

@ -48,12 +48,14 @@ class RecentlyClosedTabsInView extends ViewPage {
this.searchQuery = "";
this.searchResults = null;
this.showAll = false;
this.cumulativeSearches = 0;
}
static properties = {
...ViewPage.properties,
searchResults: { type: Array },
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
static queries = {
@ -232,6 +234,16 @@ class RecentlyClosedTabsInView extends ViewPage {
page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
}
);
if (this.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add(
this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
this.cumulativeSearches
);
this.cumulativeSearches = 0;
}
}
onDismissTab(e) {
@ -267,8 +279,13 @@ class RecentlyClosedTabsInView extends ViewPage {
);
}
willUpdate() {
willUpdate(changedProperties) {
this.fullyUpdated = false;
if (changedProperties.has("searchQuery")) {
this.cumulativeSearches = this.searchQuery
? this.cumulativeSearches + 1
: 0;
}
}
updated() {
@ -350,6 +367,9 @@ class RecentlyClosedTabsInView extends ViewPage {
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing
? "recentbrowsing"
: "recentlyclosed"}
></fxview-search-textbox>
</div>`
)}
@ -441,6 +461,15 @@ class RecentlyClosedTabsInView extends ViewPage {
) {
event.preventDefault();
this.showAll = true;
Services.telemetry.recordEvent(
"firefoxview_next",
"search_show_all",
"showallbutton",
null,
{
section: "recentlyclosed",
}
);
}
}
}

View file

@ -51,6 +51,7 @@ class SyncedTabsInView extends ViewPage {
this.fullyUpdated = false;
this.searchQuery = "";
this.showAll = false;
this.cumulativeSearches = 0;
}
static properties = {
@ -61,6 +62,7 @@ class SyncedTabsInView extends ViewPage {
devices: { type: Array },
searchQuery: { type: String },
showAll: { type: Boolean },
cumulativeSearches: { type: Number },
};
static queries = {
@ -113,6 +115,14 @@ class SyncedTabsInView extends ViewPage {
}
}
willUpdate(changedProperties) {
if (changedProperties.has("searchQuery")) {
this.cumulativeSearches = this.searchQuery
? this.cumulativeSearches + 1
: 0;
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.stop();
@ -318,6 +328,16 @@ class SyncedTabsInView extends ViewPage {
}
);
}
if (this.searchQuery) {
const searchesHistogram = Services.telemetry.getKeyedHistogramById(
"FIREFOX_VIEW_CUMULATIVE_SEARCHES"
);
searchesHistogram.add(
this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
this.cumulativeSearches
);
this.cumulativeSearches = 0;
}
}
onContextMenu(e) {
@ -502,6 +522,15 @@ class SyncedTabsInView extends ViewPage {
) {
event.preventDefault();
this.showAll = true;
Services.telemetry.recordEvent(
"firefoxview_next",
"search_show_all",
"showallbutton",
null,
{
section: "syncedtabs",
}
);
}
}
@ -569,6 +598,9 @@ class SyncedTabsInView extends ViewPage {
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
.size=${this.searchTextboxSize}
pageName=${this.recentBrowsing
? "recentbrowsing"
: "syncedtabs"}
></fxview-search-textbox>
</div>`
)}

View file

@ -32,6 +32,8 @@ skip-if = ["true"] # Bug 1869605 and # Bug 1870296
["browser_notification_dot.js"]
skip-if = ["true"] # Bug 1851453
["browser_opentabs_changes.js"]
["browser_reload_firefoxview.js"]
["browser_tab_close_last_tab.js"]

View file

@ -5,6 +5,9 @@ const tabURL1 = "data:,Tab1";
const tabURL2 = "data:,Tab2";
const tabURL3 = "data:,Tab3";
const { NonPrivateTabs } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
const TestTabs = {};
function getTopLevelViewElements(document) {
@ -38,6 +41,8 @@ async function getElements(document) {
await TestUtils.waitForCondition(() => recentlyClosedView.fullyUpdated);
}
let recentlyClosedList = recentlyClosedView.tabList;
await openTabsView.openTabsTarget.readyWindowsPromise;
await openTabsView.updateComplete;
let openTabsList =
openTabsView.shadowRoot.querySelector("view-opentabs-card")?.tabList;
@ -84,6 +89,17 @@ async function setupOpenAndClosedTabs() {
await SessionStoreTestUtils.closeTab(TestTabs.tab3);
}
function assertSpiesCalled(spiesMap, expectCalled) {
let message = expectCalled ? "to be called" : "to not be called";
for (let [elem, renderSpy] of spiesMap.entries()) {
is(
expectCalled,
renderSpy.called,
`Expected the render method spy on element ${elem.localName} ${message}`
);
}
}
async function checkFxRenderCalls(browser, elements, selectedView) {
const sandbox = sinon.createSandbox();
const topLevelViews = getTopLevelViewElements(browser.contentDocument);
@ -119,7 +135,20 @@ async function checkFxRenderCalls(browser, elements, selectedView) {
}
info("test switches to tab2");
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await BrowserTestUtils.switchTab(gBrowser, TestTabs.tab2);
await tabChangeRaised;
info(
"TabRecencyChange event was raised, check no render() methods were called"
);
assertSpiesCalled(viewSpies, false);
assertSpiesCalled(elementSpies, false);
for (let renderSpy of [...viewSpies.values(), ...elementSpies.values()]) {
renderSpy.resetHistory();
}
// check all the top-level views are paused
ok(
@ -132,21 +161,14 @@ async function checkFxRenderCalls(browser, elements, selectedView) {
);
ok(topLevelViews.openTabsView.paused, "The open tabs view is paused");
function assertSpiesCalled(spiesMap, expectCalled) {
let message = expectCalled ? "to be called" : "to not be called";
for (let [elem, renderSpy] of spiesMap.entries()) {
is(
expectCalled,
renderSpy.called,
`Expected the render method spy on element ${elem.localName} ${message}`
);
}
}
await nextFrame();
info("test removes tab1");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await BrowserTestUtils.removeTab(TestTabs.tab1);
await nextFrame();
await tabChangeRaised;
assertSpiesCalled(viewSpies, false);
assertSpiesCalled(elementSpies, false);

View file

@ -210,7 +210,7 @@ add_task(async function test_list_ordering() {
"sort_history",
"tabs",
undefined,
{ sort_type: "site" },
{ sort_type: "site", search_start: "false" },
],
];
// Select sort by site option
@ -238,7 +238,7 @@ add_task(async function test_list_ordering() {
"sort_history",
"tabs",
undefined,
{ sort_type: "date" },
{ sort_type: "date", search_start: "false" },
],
];
// Select sort by date option

View file

@ -7,6 +7,9 @@ const ROW_DATE_ID = "fxview-tab-row-date";
let gInitialTab;
let gInitialTabURL;
const { NonPrivateTabs } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
add_setup(function () {
// This test opens a lot of windows and tabs and might run long on slower configurations
@ -65,7 +68,8 @@ add_task(async function open_tab_same_window() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 1, "There is one window.");
@ -80,15 +84,23 @@ add_task(async function open_tab_same_window() {
browser.contentDocument,
"visibilitychange"
);
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
await promiseHidden;
await tabChangeRaised;
});
const [originalTab, newTab] = gBrowser.visibleTabs;
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
await TestUtils.waitForTick();
const openTabs = getOpenTabsComponent(browser);
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 1, "There is one window.");
let tabItems = await getRowsForCard(cards[0]);
@ -131,10 +143,15 @@ add_task(async function open_tab_same_window() {
const browser = viewTab.linkedBrowser;
const cards = getCards(browser);
let tabItems;
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
info("Bring the new tab to the front.");
gBrowser.moveTabTo(newTab, 0);
await tabChangeRaised;
await BrowserTestUtils.waitForMutationCondition(
cards[0].shadowRoot,
{ childList: true, subtree: true },
@ -143,7 +160,12 @@ add_task(async function open_tab_same_window() {
return tabItems[0].url === TEST_URL;
}
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await BrowserTestUtils.removeTab(newTab);
await tabChangeRaised;
const [card] = getCards(browser);
await TestUtils.waitForCondition(
@ -174,7 +196,8 @@ add_task(async function open_tab_new_window() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 2, "There are two windows.");
@ -197,8 +220,13 @@ add_task(async function open_tab_new_window() {
"The date is hidden, since we have two windows."
);
info("Select a tab from the original window.");
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
winFocused = BrowserTestUtils.waitForEvent(window, "focus", true);
originalWinRows[0].mainEl.click();
await tabChangeRaised;
});
info("Wait for the original window to be focused");
@ -208,7 +236,8 @@ add_task(async function open_tab_new_window() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 2, "There are two windows.");
@ -216,7 +245,12 @@ add_task(async function open_tab_new_window() {
info("Select a tab from the new window.");
winFocused = BrowserTestUtils.waitForEvent(win, "focus", true);
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
newWinRows[0].mainEl.click();
await tabChangeRaised;
});
info("Wait for the new window to be focused");
await winFocused;
@ -231,7 +265,8 @@ add_task(async function open_tab_new_private_window() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 1, "The private window is not displayed.");
@ -244,33 +279,61 @@ add_task(async function styling_for_multiple_windows() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
ok(
openTabs.shadowRoot.querySelector("[card-count=one]"),
"The container shows one column when one window is open."
);
});
await BrowserTestUtils.openNewBrowserWindow();
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await NonPrivateTabs.readyWindowsPromise;
await tabChangeRaised;
is(
NonPrivateTabs.currentWindows.length,
2,
"NonPrivateTabs now has 2 currentWindows"
);
info("switch to firefox view in the first window");
SimpleTest.promiseFocus(window);
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
is(
openTabs.openTabsTarget.currentWindows.length,
2,
"There should be 2 current windows"
);
ok(
openTabs.shadowRoot.querySelector("[card-count=two]"),
"The container shows two columns when two windows are open."
);
});
await BrowserTestUtils.openNewBrowserWindow();
tabChangeRaised = BrowserTestUtils.waitForEvent(NonPrivateTabs, "TabChange");
await NonPrivateTabs.readyWindowsPromise;
await tabChangeRaised;
is(
NonPrivateTabs.currentWindows.length,
3,
"NonPrivateTabs now has 2 currentWindows"
);
SimpleTest.promiseFocus(window);
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
ok(
openTabs.shadowRoot.querySelector("[card-count=three-or-more]"),
@ -306,7 +369,8 @@ add_task(async function toggle_show_more_link() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, NUMBER_OF_WINDOWS, "There are four windows.");
@ -321,7 +385,8 @@ add_task(async function toggle_show_more_link() {
await openFirefoxViewTab(window).then(async viewTab => {
const browser = viewTab.linkedBrowser;
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
Assert.less(
(await getRowsForCard(lastCard)).length,
NUMBER_OF_TABS,
@ -392,7 +457,8 @@ add_task(async function search_open_tabs() {
const browser = viewTab.linkedBrowser;
await navigateToOpenTabs(browser);
const openTabs = getOpenTabsComponent(browser);
await openTabs.getUpdateComplete();
await openTabs.openTabsTarget.readyWindowsPromise;
await openTabs.updateComplete;
const cards = getCards(browser);
is(cards.length, 2, "There are two windows.");

View file

@ -0,0 +1,541 @@
const { NonPrivateTabs, getTabsTargetForWindow } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
let privateTabsChanges;
const tabURL1 = "data:text/html,<title>Tab1</title>Tab1";
const tabURL2 = "data:text/html,<title>Tab2</title>Tab2";
const tabURL3 = "data:text/html,<title>Tab3</title>Tab3";
const tabURL4 = "data:text/html,<title>Tab4</title>Tab4";
const nonPrivateListener = sinon.stub();
const privateListener = sinon.stub();
function tabUrl(tab) {
return tab.linkedBrowser.currentURI?.spec;
}
function getWindowId(win) {
return win.windowGlobalChild.innerWindowId;
}
async function setup(tabChangeEventName) {
nonPrivateListener.resetHistory();
privateListener.resetHistory();
NonPrivateTabs.addEventListener(tabChangeEventName, nonPrivateListener);
await TestUtils.waitForTick();
is(
NonPrivateTabs.currentWindows.length,
1,
"NonPrivateTabs has 1 window a tick after adding the event listener"
);
info("Opening new windows");
let win0 = window,
win1 = await BrowserTestUtils.openNewBrowserWindow(),
privateWin = await BrowserTestUtils.openNewBrowserWindow({
private: true,
});
BrowserTestUtils.startLoadingURIString(
win1.gBrowser.selectedBrowser,
tabURL1
);
await BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
// load a tab with a title/label we can easily verify
BrowserTestUtils.startLoadingURIString(
privateWin.gBrowser.selectedBrowser,
tabURL2
);
await BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
is(
win1.gBrowser.selectedTab.label,
"Tab1",
"Check the tab label in the new non-private window"
);
is(
privateWin.gBrowser.selectedTab.label,
"Tab2",
"Check the tab label in the new private window"
);
privateTabsChanges = getTabsTargetForWindow(privateWin);
privateTabsChanges.addEventListener(tabChangeEventName, privateListener);
is(
privateTabsChanges,
getTabsTargetForWindow(privateWin),
"getTabsTargetForWindow reuses a single instance per exclusive window"
);
await TestUtils.waitForTick();
is(
NonPrivateTabs.currentWindows.length,
2,
"NonPrivateTabs has 2 windows once openNewBrowserWindow resolves"
);
is(
privateTabsChanges.currentWindows.length,
1,
"privateTabsChanges has 1 window once openNewBrowserWindow resolves"
);
await SimpleTest.promiseFocus(win0);
info("setup, win0 has id: " + getWindowId(win0));
info("setup, win1 has id: " + getWindowId(win1));
info("setup, privateWin has id: " + getWindowId(privateWin));
info("setup,waiting for both private and nonPrivateListener to be called");
await TestUtils.waitForCondition(() => {
return nonPrivateListener.called && privateListener.called;
});
nonPrivateListener.resetHistory();
privateListener.resetHistory();
const cleanup = async eventName => {
NonPrivateTabs.removeEventListener(eventName, nonPrivateListener);
privateTabsChanges.removeEventListener(eventName, privateListener);
await SimpleTest.promiseFocus(window);
await promiseAllButPrimaryWindowClosed();
};
return { windows: [win0, win1, privateWin], cleanup };
}
add_task(async function test_TabChanges() {
const { windows, cleanup } = await setup("TabChange");
const [win0, win1, privateWin] = windows;
let tabChangeRaised;
let changeEvent;
info(
"Verify that manipulating tabs in a non-private window dispatches events on the correct target"
);
for (let win of [win0, win1]) {
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
let newTab = await BrowserTestUtils.openNewForegroundTab(
win.gBrowser,
tabURL1
);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
const navigateUrl = "https://example.org/";
BrowserTestUtils.startLoadingURIString(newTab.linkedBrowser, navigateUrl);
await BrowserTestUtils.browserLoaded(
newTab.linkedBrowser,
null,
navigateUrl
);
// navigation in a tab changes the label which should produce a change event
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
BrowserTestUtils.removeTab(newTab);
// navigation in a tab changes the label which should produce a change event
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win)],
"The event had the correct window id"
);
}
info(
"make sure a change to a private window doesnt dispatch on a nonprivate target"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabChange"
);
BrowserTestUtils.addTab(privateWin.gBrowser, tabURL1);
changeEvent = await tabChangeRaised;
info(
`Check windowIds adding tab to private window: ${getWindowId(
privateWin
)}: ${JSON.stringify(changeEvent.detail.windowIds)}`
);
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
await TestUtils.waitForTick();
Assert.ok(
nonPrivateListener.notCalled,
"A private tab change shouldnt raise a tab change event on the non-private target"
);
info("testTabChanges complete");
await cleanup("TabChange");
});
add_task(async function test_TabRecencyChange() {
const { windows, cleanup } = await setup("TabRecencyChange");
const [win0, win1, privateWin] = windows;
let tabChangeRaised;
let changeEvent;
let sortedTabs;
info("Open some tabs in the non-private windows");
for (let win of [win0, win1]) {
for (let url of [tabURL1, tabURL2]) {
let tab = BrowserTestUtils.addTab(win.gBrowser, url);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
await tabChangeRaised;
}
}
info("Verify switching tabs produces the expected event and result");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
BrowserTestUtils.switchTab(win0.gBrowser, win0.gBrowser.tabs.at(-1));
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win0)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.called,
"Sanity check that the non-private tabs listener was called"
);
Assert.ok(
privateListener.notCalled,
"The private tabs listener was not called"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win0.gBrowser.selectedTab,
"The most-recent tab is the selected tab"
);
info("Verify switching window produces the expected event and result");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(win1)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.called,
"Sanity check that the non-private tabs listener was called"
);
Assert.ok(
privateListener.notCalled,
"The private tabs listener was not called"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the current window"
);
info("Verify behavior with private window changes");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(privateWin);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The recency change event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
Assert.ok(
privateListener.called,
"Sanity check the private tabs listener was called"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the current window"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent non-private tab is still the selected tab in the previous non-private window"
);
info("Verify adding a tab to a private window does the right thing");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
await BrowserTestUtils.openNewForegroundTab(privateWin.gBrowser, tabURL3);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
tabUrl(sortedTabs[0]),
tabURL3,
"The most-recent tab is the tab we just opened in the private window"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
privateTabsChanges,
"TabRecencyChange"
);
BrowserTestUtils.switchTab(privateWin.gBrowser, privateWin.gBrowser.tabs[0]);
changeEvent = await tabChangeRaised;
Assert.deepEqual(
changeEvent.detail.windowIds,
[getWindowId(privateWin)],
"The event had the correct window id"
);
Assert.ok(
nonPrivateListener.notCalled,
"The non-private listener got no recency-change events from the private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent tab is the selected tab in the private window"
);
info("Verify switching back to a non-private does the right thing");
nonPrivateListener.resetHistory();
privateListener.resetHistory();
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
await tabChangeRaised;
if (privateListener.called) {
info(`The private listener was called ${privateListener.callCount} times`);
}
Assert.ok(
privateListener.notCalled,
"The private listener got no recency-change events for the non-private window"
);
Assert.ok(
nonPrivateListener.called,
"Sanity-check the non-private listener got a recency-change event for the non-private window"
);
sortedTabs = privateTabsChanges.getRecentTabs();
is(
sortedTabs[0],
privateWin.gBrowser.selectedTab,
"The most-recent private tab is unchanged"
);
sortedTabs = NonPrivateTabs.getRecentTabs();
is(
sortedTabs[0],
win1.gBrowser.selectedTab,
"The most-recent non-private tab is the selected tab in the current window"
);
await cleanup("TabRecencyChange");
while (win0.gBrowser.tabs.length > 1) {
info(
"Removing last tab:" +
win0.gBrowser.tabs.at(-1).linkedBrowser.currentURI.spec
);
BrowserTestUtils.removeTab(win0.gBrowser.tabs.at(-1));
info("Removed, tabs.length:" + win0.gBrowser.tabs.length);
}
});
add_task(async function test_tabNavigations() {
const { windows, cleanup } = await setup("TabChange");
const [, win1, privateWin] = windows;
// also listen for TabRecencyChange events
const nonPrivateRecencyListener = sinon.stub();
const privateRecencyListener = sinon.stub();
privateTabsChanges.addEventListener(
"TabRecencyChange",
privateRecencyListener
);
NonPrivateTabs.addEventListener(
"TabRecencyChange",
nonPrivateRecencyListener
);
info(
`Verify navigating in tab generates TabChange & TabRecencyChange events`
);
let loaded = BrowserTestUtils.browserLoaded(win1.gBrowser.selectedBrowser);
win1.gBrowser.selectedBrowser.loadURI(Services.io.newURI(tabURL4), {
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
info("waiting for the load into win1 tab to complete");
await loaded;
info("waiting for listeners to be called");
await BrowserTestUtils.waitForCondition(() => {
return nonPrivateListener.called && nonPrivateRecencyListener.called;
});
ok(!privateListener.called, "The private TabChange listener was not called");
ok(
!privateRecencyListener.called,
"The private TabRecencyChange listener was not called"
);
nonPrivateListener.resetHistory();
privateListener.resetHistory();
nonPrivateRecencyListener.resetHistory();
privateRecencyListener.resetHistory();
// Now verify the same with a private window
info(
`Verify navigating in private tab generates TabChange & TabRecencyChange events`
);
ok(
!nonPrivateListener.called,
"The non-private TabChange listener is not yet called"
);
loaded = BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser);
privateWin.gBrowser.selectedBrowser.loadURI(
Services.io.newURI("about:robots"),
{
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
}
);
info("waiting for the load into privateWin tab to complete");
await loaded;
info("waiting for the privateListeners to be called");
await BrowserTestUtils.waitForCondition(() => {
return privateListener.called && privateRecencyListener.called;
});
ok(
!nonPrivateListener.called,
"The non-private TabChange listener was not called"
);
ok(
!nonPrivateRecencyListener.called,
"The non-private TabRecencyChange listener was not called"
);
// cleanup
privateTabsChanges.removeEventListener(
"TabRecencyChange",
privateRecencyListener
);
NonPrivateTabs.removeEventListener(
"TabRecencyChange",
nonPrivateRecencyListener
);
await cleanup();
});
add_task(async function test_tabsFromPrivateWindows() {
const { cleanup } = await setup("TabChange");
const private2Listener = sinon.stub();
const private2Win = await BrowserTestUtils.openNewBrowserWindow({
private: true,
waitForTabURL: "about:privatebrowsing",
});
const private2TabsChanges = getTabsTargetForWindow(private2Win);
private2TabsChanges.addEventListener("TabChange", private2Listener);
ok(
privateTabsChanges !== getTabsTargetForWindow(private2Win),
"getTabsTargetForWindow creates a distinct instance for a different private window"
);
await BrowserTestUtils.waitForCondition(() => private2Listener.called);
ok(
!privateListener.called,
"No TabChange event was raised by opening a different private window"
);
privateListener.resetHistory();
private2Listener.resetHistory();
BrowserTestUtils.addTab(private2Win.gBrowser, tabURL1);
await BrowserTestUtils.waitForCondition(() => private2Listener.called);
ok(
!privateListener.called,
"No TabChange event was raised by adding tab to a different private window"
);
is(
privateTabsChanges.getRecentTabs().length,
1,
"The recent tab count for the first private window tab target only reports the tabs for its associated windodw"
);
is(
private2TabsChanges.getRecentTabs().length,
2,
"The recent tab count for a 2nd private window tab target only reports the tabs for its associated windodw"
);
await cleanup("TabChange");
});

View file

@ -20,6 +20,10 @@ const fxaDevicesWithCommands = [
},
];
const { NonPrivateTabs } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
function getCards(openTabs) {
return openTabs.shadowRoot.querySelectorAll("view-opentabs-card");
}
@ -29,6 +33,64 @@ async function getRowsForCard(card) {
return card.tabList.rowEls;
}
async function add_new_tab(URL) {
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
let tab = BrowserTestUtils.addTab(gBrowser, URL);
// wait so we can reliably compare the tab URL
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
await tabChangeRaised;
return tab;
}
function getVisibleTabURLs(win = window) {
return win.gBrowser.visibleTabs.map(tab => tab.linkedBrowser.currentURI.spec);
}
function getTabRowURLs(rows) {
return Array.from(rows).map(row => row.url);
}
async function waitUntilRowsMatch(openTabs, cardIndex, expectedURLs) {
let card;
info(
"moreMenuSetup: openTabs has openTabsTarget?:" + !!openTabs?.openTabsTarget
);
//await openTabs.openTabsTarget.readyWindowsPromise;
info(
`waitUntilRowsMatch, wait for there to be at least ${cardIndex + 1} cards`
);
await BrowserTestUtils.waitForCondition(() => {
if (!openTabs.initialWindowsReady) {
info("openTabs.initialWindowsReady isn't true");
return false;
}
try {
card = getCards(openTabs)[cardIndex];
} catch (ex) {
info("Calling getCards produced exception: " + ex.message);
}
return !!card;
}, "Waiting for openTabs to be ready and to get the cards");
const expectedURLsAsString = JSON.stringify(expectedURLs);
info(`Waiting for row URLs to match ${expectedURLs.join(", ")}`);
await BrowserTestUtils.waitForMutationCondition(
card.shadowRoot,
{ characterData: true, childList: true, subtree: true },
async () => {
let rows = await getRowsForCard(card);
return (
rows.length == expectedURLs.length &&
JSON.stringify(getTabRowURLs(rows)) == expectedURLsAsString
);
}
);
}
async function getContextMenuPanelListForCard(card) {
let menuContainer = card.shadowRoot.querySelector(
"view-opentabs-contextmenu"
@ -56,25 +118,26 @@ async function openContextMenuForItem(tabItem, card) {
return panelList;
}
async function moreMenuSetup(document) {
await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL2);
await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL3);
async function moreMenuSetup() {
await add_new_tab(TEST_URL2);
await add_new_tab(TEST_URL3);
// once we've opened a few tabs, navigate to the open tabs section in firefox view
await clickFirefoxViewButton(window);
navigateToCategory(document, "opentabs");
const document = window.FirefoxViewHandler.tab.linkedBrowser.contentDocument;
await navigateToCategoryAndWait(document, "opentabs");
let openTabs = document.querySelector("view-opentabs[name=opentabs]");
await openTabs.openTabsTarget.readyWindowsPromise;
let cards;
await TestUtils.waitForCondition(() => {
cards = getCards(openTabs);
return cards.length == 1;
});
info("waiting for openTabs' first card rows");
await waitUntilRowsMatch(openTabs, 0, getVisibleTabURLs());
let cards = getCards(openTabs);
is(cards.length, 1, "There is one open window.");
let rows = await getRowsForCard(cards[0]);
is(rows.length, 3, "There are three tabs in the open tabs list.");
let firstTab = rows[0];
@ -90,7 +153,6 @@ async function moreMenuSetup(document) {
add_task(async function test_more_menus() {
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
let win = browser.ownerGlobal;
let shown, menuHidden;
@ -101,11 +163,22 @@ add_task(async function test_more_menus() {
"Selected tab is about:blank"
);
info(`Loading ${TEST_URL1} into the selected about:blank tab`);
let tabLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
win.gURLBar.focus();
win.gURLBar.value = TEST_URL1;
EventUtils.synthesizeKey("KEY_Enter", {}, win);
await tabLoaded;
info("Waiting for moreMenuSetup to resolve");
let [cards, rows] = await moreMenuSetup();
Assert.deepEqual(
getVisibleTabURLs(),
[TEST_URL1, TEST_URL2, TEST_URL3],
"Prepared 3 open tabs"
);
let [cards, rows] = await moreMenuSetup(document);
let firstTab = rows[0];
// Open the panel list (more menu) from the first list item
let panelList = await openContextMenuForItem(firstTab, cards[0]);
@ -136,30 +209,33 @@ add_task(async function test_more_menus() {
];
// close a tab via the menu
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
menuHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
panelItemButton.click();
info("Waiting for result of closing a tab via the menu");
await tabChangeRaised;
await cards[0].getUpdateComplete();
await menuHidden;
await telemetryEvent(contextMenuEvent);
let visibleTabs = gBrowser.visibleTabs;
is(visibleTabs.length, 2, "Expected to now have 2 open tabs");
Assert.deepEqual(
getVisibleTabURLs(),
[TEST_URL2, TEST_URL3],
"Got the expected 2 open tabs"
);
let openTabs = cards[0].ownerDocument.querySelector(
"view-opentabs[name=opentabs]"
);
await waitUntilRowsMatch(openTabs, 0, [TEST_URL2, TEST_URL3]);
// Move Tab submenu item
firstTab = rows[0];
is(firstTab.url, TEST_URL2, `First tab list item is ${TEST_URL2}`);
is(
visibleTabs[0].linkedBrowser.currentURI.spec,
TEST_URL2,
`First tab in tab strip is ${TEST_URL2}`
);
is(
visibleTabs[visibleTabs.length - 1].linkedBrowser.currentURI.spec,
TEST_URL3,
`Last tab in tab strip is ${TEST_URL3}`
);
panelList = await openContextMenuForItem(firstTab, cards[0]);
let moveTabsPanelItem = panelList.querySelector(
"panel-item[data-l10n-id=fxviewtabrow-move-tab]"
@ -191,35 +267,27 @@ add_task(async function test_more_menus() {
// click on the first option, which should be "Move to the end" since
// this is the first tab
menuHidden = BrowserTestUtils.waitForEvent(panelList, "hidden");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabChange"
);
EventUtils.synthesizeKey("KEY_Enter", {});
info("Waiting for result of moving a tab via the menu");
await telemetryEvent(contextMenuEvent);
await menuHidden;
await tabChangeRaised;
visibleTabs = gBrowser.visibleTabs;
is(
visibleTabs[0].linkedBrowser.currentURI.spec,
TEST_URL3,
`First tab in tab strip is now ${TEST_URL3}`
);
is(
visibleTabs[visibleTabs.length - 1].linkedBrowser.currentURI.spec,
TEST_URL2,
`Last tab in tab strip is now ${TEST_URL2}`
Assert.deepEqual(
getVisibleTabURLs(),
[TEST_URL3, TEST_URL2],
"The last tab became the first tab"
);
// this entire "move tabs" submenu test can be reordered above
// closing a tab since it very clearly reveals the issues
// outlined in bug 1852622 when there are 3 or more tabs open
// and one is moved via the more menus.
await BrowserTestUtils.waitForMutationCondition(
cards[0].shadowRoot,
{ characterData: true, childList: true, subtree: true },
async () => {
rows = await getRowsForCard(cards[0]);
firstTab = rows[0];
return firstTab.url == TEST_URL3;
}
);
await waitUntilRowsMatch(openTabs, 0, [TEST_URL3, TEST_URL2]);
// Copy Link menu item (copyLink function that's called is a member of Viewpage.mjs)
panelList = await openContextMenuForItem(firstTab, cards[0]);
@ -284,7 +352,12 @@ add_task(async function test_send_device_submenu() {
.callsFake(() => fxaDevicesWithCommands);
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
// TEST_URL2 is our only tab, left over from previous test
Assert.deepEqual(
getVisibleTabURLs(),
[TEST_URL2],
`We initially have a single ${TEST_URL2} tab`
);
let shown;
Services.obs.notifyObservers(null, UIState.ON_UPDATE);

View file

@ -13,6 +13,9 @@ const tabURL4 = "data:,Tab4";
let gInitialTab;
let gInitialTabURL;
const { NonPrivateTabs } = ChromeUtils.importESModule(
"resource:///modules/OpenTabs.sys.mjs"
);
add_setup(function () {
gInitialTab = gBrowser.selectedTab;
@ -163,8 +166,14 @@ add_task(async function test_single_window_tabs() {
browser.contentDocument,
"visibilitychange"
);
let tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await BrowserTestUtils.switchTab(gBrowser, gBrowser.visibleTabs[0]);
await promiseHidden;
await tabChangeRaised;
});
// and check the results in the open tabs section of Recent Browsing
@ -178,6 +187,7 @@ add_task(async function test_single_window_tabs() {
add_task(async function test_multiple_window_tabs() {
const fxViewURL = getFirefoxViewURL();
const win1 = window;
let tabChangeRaised;
await prepareOpenTabs([tabURL1, tabURL2]);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3, tabURL4], win2);
@ -196,6 +206,10 @@ add_task(async function test_multiple_window_tabs() {
);
info("Switching to first tab (tab3) in win2");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
let promiseHidden = BrowserTestUtils.waitForEvent(
browser.contentDocument,
"visibilitychange"
@ -209,6 +223,7 @@ add_task(async function test_multiple_window_tabs() {
tabURL3,
`The selected tab in window 2 is ${tabURL3}`
);
await tabChangeRaised;
await promiseHidden;
});
@ -220,7 +235,12 @@ add_task(async function test_multiple_window_tabs() {
});
info("Focusing win1, where tab2 should be selected");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
await tabChangeRaised;
Assert.equal(
tabUrl(win1.gBrowser.selectedTab),
tabURL2,
@ -239,12 +259,17 @@ add_task(async function test_multiple_window_tabs() {
browser.contentDocument,
"visibilitychange"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
info("Switching to first visible tab (tab1) in win1");
await BrowserTestUtils.switchTab(
win1.gBrowser,
win1.gBrowser.visibleTabs[0]
);
await promiseHidden;
await tabChangeRaised;
});
// check result in the fxview in the 1st window
@ -262,27 +287,41 @@ add_task(async function test_windows_activation() {
const win1 = window;
await prepareOpenTabs([tabURL1], win1);
let fxViewTab;
let tabChangeRaised;
info("switch to firefox-view and leave it selected");
await openFirefoxViewTab(win1).then(tab => (fxViewTab = tab));
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL2], win2);
const win3 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3], win3);
await tabChangeRaised;
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win1);
await tabChangeRaised;
const browser = fxViewTab.linkedBrowser;
await checkTabList(browser, [tabURL3, tabURL2, tabURL1]);
info("switch to win2 and confirm its selected tab becomes most recent");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await SimpleTest.promiseFocus(win2);
await tabChangeRaised;
await checkTabList(browser, [tabURL2, tabURL3, tabURL1]);
await cleanup(win2, win3);
});
add_task(async function test_minimize_restore_windows() {
const win1 = window;
let tabChangeRaised;
await prepareOpenTabs([tabURL1, tabURL2]);
const win2 = await BrowserTestUtils.openNewBrowserWindow();
await prepareOpenTabs([tabURL3, tabURL4], win2);
@ -298,19 +337,29 @@ add_task(async function test_minimize_restore_windows() {
browser.contentDocument,
"visibilitychange"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
info("Switching to the first tab (tab3) in 2nd window");
await BrowserTestUtils.switchTab(
win2.gBrowser,
win2.gBrowser.visibleTabs[0]
);
await promiseHidden;
await tabChangeRaised;
});
// then minimize the window, focusing the 1st window
info("Minimizing win2, leaving tab 3 selected");
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await minimizeWindow(win2);
info("Focusing win1, where tab2 is selected - making it most recent");
await SimpleTest.promiseFocus(win1);
await tabChangeRaised;
Assert.equal(
tabUrl(win1.gBrowser.selectedTab),
@ -325,8 +374,13 @@ add_task(async function test_minimize_restore_windows() {
info(
"Restoring win2 and focusing it - which should make its selected tab most recent"
);
tabChangeRaised = BrowserTestUtils.waitForEvent(
NonPrivateTabs,
"TabRecencyChange"
);
await restoreWindow(win2);
await SimpleTest.promiseFocus(win2);
await tabChangeRaised;
info(
"Checking tab order in fxview in win1, to confirm tab3 is most recent"

View file

@ -60,6 +60,7 @@ const URLs = [
"https://example.net/",
"https://example.org/",
"about:robots",
"https://www.mozilla.org/",
];
const syncedTabsData1 = [

View file

@ -70,10 +70,9 @@ export class ViewPageContent extends MozLitElement {
return window.browsingContext.embedderWindowGlobal.browsingContext.window;
}
getBrowserTab() {
return this.getWindow().gBrowser.getTabForBrowser(
window.browsingContext.embedderElement
);
get isSelectedBrowserTab() {
const { gBrowser } = this.getWindow();
return gBrowser.selectedBrowser.browsingContext == window.browsingContext;
}
copyLink(e) {

View file

@ -160,6 +160,8 @@ class AboutPreferences {
document.l10n.setAttributes(homeHeader, "home-prefs-content-header2");
const homeDescription = createAppend("description", contentsGroup);
homeDescription.classList.add("description-deemphasized");
document.l10n.setAttributes(
homeDescription,
"home-prefs-content-description2"
@ -228,10 +230,10 @@ class AboutPreferences {
const detailVbox = createAppend("vbox", sectionVbox);
detailVbox.classList.add("indent");
if (descString) {
const label = createAppend("label", detailVbox);
label.classList.add("indent");
const description = createAppend("description", detailVbox);
description.classList.add("indent", "text-deemphasized");
document.l10n.setAttributes(
label,
description,
getString(descString),
descString.values
);
@ -240,8 +242,8 @@ class AboutPreferences {
if (rowsPref && maxRows) {
const detailHbox = createAppend("hbox", detailVbox);
detailHbox.setAttribute("align", "center");
label.setAttribute("flex", 1);
detailHbox.appendChild(label);
description.setAttribute("flex", 1);
detailHbox.appendChild(description);
// Add box so the search tooltip is positioned correctly
const tooltipBox = createAppend("hbox", detailHbox);

View file

@ -12,6 +12,7 @@
data-category="paneExperimental">
<html:h1 style="flex: 1;" data-l10n-id="pane-experimental-title"/>
<label><html:h2 id="pane-experimental-subtitle" data-l10n-id="pane-experimental-subtitle"/></label>
<html:p data-l10n-id="pane-experimental-description2" class="description-deemphasized"/>
<hbox pack="end">
<button id="experimentalCategory-reset"
class="accessory-button"
@ -25,13 +26,13 @@
<label class="search-header" hidden="true">
<html:h2 id="pane-experimental-search-results-header" data-l10n-id="pane-experimental-search-results-header"/>
</label>
<html:p data-l10n-id="pane-experimental-description2"/>
<html:p data-l10n-id="pane-experimental-description2" class="description-deemphasized"/>
</groupbox>
</html:template>
<html:template id="template-featureGate">
<html:div class="featureGate">
<checkbox class="featureGateCheckbox"/>
<label class="featureGateDescription"/>
<description class="text-deemphasized featureGateDescription"/>
</html:div>
</html:template>

View file

@ -23,7 +23,7 @@
data-category="paneHome"
hidden="true">
<label><html:h2 data-l10n-id="home-new-windows-tabs-header"/></label>
<description data-l10n-id="home-new-windows-tabs-description2" />
<description class="description-deemphasized" data-l10n-id="home-new-windows-tabs-description2" />
<hbox id="homepageAndNewWindowsOption" align="center" data-subcategory="homeOverride">
<label control="homeMode" data-l10n-id="home-homepage-mode-label" flex="1" />

View file

@ -158,7 +158,7 @@
<groupbox id="webAppearanceGroup" data-category="paneGeneral" hidden="true">
<html:h2 data-l10n-id="preferences-web-appearance-header"/>
<html:div id="webAppearanceSettings">
<description data-l10n-id="preferences-web-appearance-description"/>
<description class="description-deemphasized" data-l10n-id="preferences-web-appearance-description"/>
<html:div id="web-appearance-override-warning" class="info-box-container">
<html:div class="info-icon-container">
<html:img class="info-icon"/>
@ -442,7 +442,7 @@
<groupbox id="applicationsGroup" data-category="paneGeneral" hidden="true">
<label><html:h2 data-l10n-id="applications-header"/></label>
<description data-l10n-id="applications-description"/>
<description class="description-deemphasized" data-l10n-id="applications-description"/>
<search-textbox id="filter" flex="1"
data-l10n-id="applications-filter"
data-l10n-attrs="placeholder"
@ -458,8 +458,8 @@
</listheader>
<richlistbox id="handlersView"
preference="pref.downloads.disable_button.edit_actions"/>
<description id="handleNewFileTypesDesc"
data-l10n-id="applications-handle-new-file-types-description"/>
<label id="handleNewFileTypesDesc"
data-l10n-id="applications-handle-new-file-types-description"/>
<radiogroup id="handleNewFileTypes"
preference="browser.download.always_ask_before_handling_new_types">
<radio id="saveForNewTypes"
@ -496,12 +496,12 @@
<groupbox id="updateApp" data-category="paneGeneral" hidden="true">
<label class="search-header" hidden="true"><html:h2 data-l10n-id="update-application-title"/></label>
<label data-l10n-id="update-application-description"/>
<description class="description-deemphasized" data-l10n-id="update-application-description"/>
<hbox align="center">
<vbox flex="1">
<description id="updateAppInfo">
<html:span id="updateAppInfo">
<html:a id="releasenotes" target="_blank" data-l10n-name="learn-more" class="learnMore text-link" hidden="true"/>
</description>
</html:span>
<description id="distribution" class="text-blurb" hidden="true"/>
<description id="distributionId" class="text-blurb" hidden="true"/>
</vbox>
@ -621,7 +621,7 @@
#endif
#ifdef MOZ_UPDATER
<description id="updateAllowDescription" data-l10n-id="update-application-allow-description"></description>
<label id="updateAllowDescription" data-l10n-id="update-application-allow-description"></label>
<vbox id="updateSettingsContainer" class="info-box-container">
<radiogroup id="updateRadioGroup">
<radio id="autoDesktop"

View file

@ -12,7 +12,7 @@
hidden="true"
data-category="paneMoreFromMozilla">
<h1 class="title" data-l10n-id="more-from-moz-title"/>
<p class="subtitle" data-l10n-id="more-from-moz-subtitle"/>
<p class="subtitle description-deemphasized" data-l10n-id="more-from-moz-subtitle"/>
</div>
<div id="moreFromMozillaCategory"
data-category="paneMoreFromMozilla"
@ -27,7 +27,7 @@
<div>
<h2 class="product-title"/>
<div class="product-description-box">
<div class="description"/>
<div class="description description-deemphasized"/>
<a class="text-link" target="_blank" hidden="true"/>
</div>
</div>

View file

@ -402,8 +402,8 @@
<hbox data-subcategory="sitedata" align="baseline">
<vbox flex="1">
<description class="description-with-side-element" flex="1">
<html:span id="totalSiteDataSize" class="tail-with-learn-more section-description"></html:span>
<description class="description-with-side-element description-deemphasized" flex="1">
<html:span id="totalSiteDataSize" class="tail-with-learn-more"></html:span>
<html:a is="moz-support-link"
id="siteDataLearnMoreLink"
data-l10n-id="sitedata-learn-more"
@ -463,8 +463,8 @@
<label><html:h2 data-l10n-id="cookie-banner-blocker-header" /></label>
<vbox flex="1">
<hbox>
<description>
<html:span id="cookieBannerReductionExplanation" class="tail-with-learn-more section-description" data-l10n-id="cookie-banner-blocker-description" ></html:span>
<description class="description-deemphasized">
<html:span id="cookieBannerReductionExplanation" class="tail-with-learn-more" data-l10n-id="cookie-banner-blocker-description" ></html:span>
<html:a is="moz-support-link" id="cookieBannerHandlingLearnMore" class="learnMore" data-l10n-id="cookie-banner-learn-more" support-page="cookie-banner-reduction"/>
</description>
</hbox>
@ -713,10 +713,10 @@
<!-- The hbox around the buttons is to compute the search tooltip position properly -->
<vbox>
<hbox id="locationSettingsRow" align="center" role="group" aria-labelledby="locationPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="geo-icon permission-icon" />
<label id="locationPermissionsLabel" data-l10n-id="permissions-location"/>
</description>
</hbox>
<hbox pack="end">
<button id="locationSettingsButton"
is="highlightable-button"
@ -734,10 +734,10 @@
</hbox>
<hbox id="cameraSettingsRow" align="center" role="group" aria-labelledby="cameraPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="camera-icon permission-icon" />
<label id="cameraPermissionsLabel" data-l10n-id="permissions-camera"/>
</description>
</hbox>
<hbox pack="end">
<button id="cameraSettingsButton"
is="highlightable-button"
@ -755,10 +755,10 @@
</hbox>
<hbox id="microphoneSettingsRow" align="center" role="group" aria-labelledby="microphonePermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="microphone-icon permission-icon" />
<label id="microphonePermissionsLabel" data-l10n-id="permissions-microphone"/>
</description>
</hbox>
<hbox pack="end">
<button id="microphoneSettingsButton"
is="highlightable-button"
@ -776,10 +776,10 @@
</hbox>
<hbox id="speakerSettingsRow" align="center" role="group" aria-labelledby="speakerPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="speaker-icon permission-icon" />
<label id="speakerPermissionsLabel" data-l10n-id="permissions-speaker"/>
</description>
</hbox>
<hbox pack="end">
<button id="speakerSettingsButton"
is="highlightable-button"
@ -795,7 +795,7 @@
</hbox>
<hbox id="notificationSettingsRow" align="center" role="group" aria-labelledby="notificationPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="desktop-notification-icon permission-icon" />
<label id="notificationPermissionsLabel"
class="tail-with-learn-more"
@ -806,7 +806,7 @@
data-l10n-id="permissions-notification-link"
support-page="push"
/>
</description>
</hbox>
<hbox pack="end">
<button id="notificationSettingsButton"
is="highlightable-button"
@ -828,11 +828,11 @@
</vbox>
<hbox id="autoplaySettingsRow" align="center" role="group" aria-labelledby="autoplayPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="autoplay-icon permission-icon" />
<label id="autoplayPermissionsLabel"
data-l10n-id="permissions-autoplay"/>
</description>
</hbox>
<hbox pack="end">
<button id="autoplaySettingsButton"
is="highlightable-button"
@ -848,10 +848,10 @@
</hbox>
<hbox id="xrSettingsRow" align="center" role="group" aria-labelledby="xrPermissionsLabel">
<description flex="1">
<hbox flex="1">
<image class="xr-icon permission-icon" />
<label id="xrPermissionsLabel" data-l10n-id="permissions-xr"/>
</description>
</hbox>
<hbox pack="end">
<button id="xrSettingsButton"
is="highlightable-button"
@ -919,8 +919,8 @@
<groupbox id="dataCollectionGroup" data-category="panePrivacy" hidden="true">
<label class="search-header" hidden="true"><html:h2 data-l10n-id="collection-header2" data-l10n-attrs="searchkeywords"/></label>
<description>
<label class="tail-with-learn-more section-description" data-l10n-id="collection-description"/>
<description class="description-deemphasized">
<html:span class="tail-with-learn-more" data-l10n-id="collection-description"/>
<html:a id="dataCollectionPrivacyNotice"
class="learnMore"
data-l10n-id="collection-privacy-notice"/>
@ -940,7 +940,7 @@
</description>
</hbox>
<vbox data-subcategory="reports">
<description flex="1">
<html:span flex="1">
<checkbox id="submitHealthReportBox"
data-l10n-id="collection-health-report"
class="tail-with-learn-more"/>
@ -960,7 +960,7 @@
/>
</hbox>
</vbox>
</description>
</html:span>
#ifndef MOZ_TELEMETRY_REPORTING
<description id="TelemetryDisabledDesc"
class="indent tip-caption" control="telemetryGroup"
@ -1118,7 +1118,7 @@
<groupbox id="httpsOnlyBox" data-category="panePrivacy" hidden="true">
<label><html:h2 data-l10n-id="httpsonly-header"/></label>
<vbox data-subcategory="httpsonly" flex="1">
<label id="httpsOnlyDescription" class="section-description" data-l10n-id="httpsonly-description"/>
<description id="httpsOnlyDescription" class="description-deemphasized" data-l10n-id="httpsonly-description"/>
<html:a is="moz-support-link"
id="httpsOnlyLearnMore"
class="learnMore"
@ -1162,7 +1162,7 @@
<groupbox id="dohBox" data-category="panePrivacy" data-subcategory="doh" hidden="true" class="highlighting-group">
<label class="search-header" searchkeywords="doh trr" hidden="true"><html:h2 data-l10n-id="preferences-doh-header"/></label>
<vbox flex="1">
<html:span id="dohDescription" class="tail-with-learn-more" data-l10n-id="preferences-doh-description2"></html:span>
<description id="dohDescription" class="tail-with-learn-more description-deemphasized" data-l10n-id="preferences-doh-description2"></description>
<html:a is="moz-support-link"
id="dohLearnMore"
class="learnMore"

View file

@ -14,7 +14,7 @@
<!-- Default Search Engine -->
<groupbox id="defaultEngineGroup" data-category="paneSearch" hidden="true">
<label><html:h2 data-l10n-id="search-engine-default-header" id="search-engine-default-header" /></label>
<description class="section-description" data-l10n-id="search-engine-default-desc-2" id="search-engine-default-desc-2" />
<description class="description-deemphasized" data-l10n-id="search-engine-default-desc-2" id="search-engine-default-desc-2" />
<hbox>
<menulist id="defaultEngine" aria-labelledby="search-engine-default-header" aria-describedby="search-engine-default-desc-2">
<menupopup/>
@ -40,7 +40,7 @@
<groupbox id="searchSuggestionsGroup" data-category="paneSearch" hidden="true">
<label><html:h2 data-l10n-id="search-suggestions-header" /></label>
<description id="searchSuggestionsDesc"
class="section-description"
class="description-deemphasized"
data-l10n-id="search-suggestions-desc" />
<checkbox id="suggestionsInSearchFieldsCheckbox"
@ -71,7 +71,7 @@
hidden="true"
data-subcategory="locationBar">
<label><html:h2 id="locationBarGroupHeader" data-l10n-id="addressbar-header"/></label>
<label id="locationBarSuggestionLabel" class="section-description" data-l10n-id="addressbar-suggest"/>
<label id="locationBarSuggestionLabel" class="description-deemphasized" data-l10n-id="addressbar-suggest"/>
<checkbox id="historySuggestion" data-l10n-id="addressbar-locbar-history-option"
preference="browser.urlbar.suggest.history"/>
<checkbox id="bookmarkSuggestion" data-l10n-id="addressbar-locbar-bookmarks-option"
@ -164,7 +164,7 @@
<groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch" hidden="true">
<label><html:h2 data-l10n-id="search-one-click-header2" /></label>
<description class="section-description" data-l10n-id="search-one-click-desc" />
<description class="description-deemphasized" data-l10n-id="search-one-click-desc" />
<tree id="engineList" flex="1" rows="11" hidecolumnpicker="true" editable="true"
seltype="single" allowunderflowscroll="true">

View file

@ -19,7 +19,7 @@
<hbox>
<vbox flex="1">
<label id="noFxaCaption"><html:h2 data-l10n-id="sync-signedout-caption"/></label>
<description id="noFxaDescription" flex="1" data-l10n-id="sync-signedout-description2"/>
<description id="noFxaDescription" class="description-deemphasized" flex="1" data-l10n-id="sync-signedout-description2"/>
</vbox>
<vbox>
<image class="fxaSyncIllustration"/>

View file

@ -49,6 +49,9 @@ add_task(async function testSendButton() {
});
add_task(async function testSendingMoreInfo() {
ensureReportBrokenSitePreffedOn();
ensureSendMoreInfoEnabled();
const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
const blockedPromise = waitForContentBlockingEvent(4, win);
const tab = await openTab(REPORTABLE_PAGE_URL3, win);

View file

@ -2669,24 +2669,25 @@ export class UrlbarView {
},
});
actionNode.appendChild(textModeLabel);
}
let iconModeLabel = this.#createElement("div");
iconModeLabel.classList.add("urlbarView-userContext-iconMode");
actionNode.appendChild(iconModeLabel);
if (identity.icon) {
let userContextIcon = this.#createElement("img");
userContextIcon.classList.add("urlbarView-userContext-icon");
let iconModeLabel = this.#createElement("div");
iconModeLabel.classList.add("urlbarView-userContext-iconMode");
actionNode.appendChild(iconModeLabel);
if (identity.icon) {
let userContextIcon = this.#createElement("img");
userContextIcon.classList.add("urlbarView-userContext-icon");
userContextIcon.classList.add("identity-icon-" + identity.icon);
userContextIcon.src =
"resource://usercontext-content/" + identity.icon + ".svg";
this.#setElementL10n(iconModeLabel, {
id: "urlbar-result-action-switch-tab",
});
iconModeLabel.appendChild(userContextIcon);
userContextIcon.classList.add("identity-icon-" + identity.icon);
userContextIcon.setAttribute("alt", label);
userContextIcon.src =
"resource://usercontext-content/" + identity.icon + ".svg";
this.#setElementL10n(iconModeLabel, {
id: "urlbar-result-action-switch-tab",
});
iconModeLabel.appendChild(userContextIcon);
}
actionNode.setAttribute("tooltiptext", label);
}
actionNode.setAttribute("tooltiptext", label);
}
} else {
actionNode.classList.remove("urlbarView-userContext");

View file

@ -321,7 +321,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "51810469d2ed19df5cb49082c989f8fcf65fcc4d"
"revision": "10b19832d6f9c6bad12df2d06c270e15122c9e13"
},
"cy": {
"pin": false,
@ -339,7 +339,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "4024c7ecbd1a45d4248a194e16af61c07b0665c5"
"revision": "d0a11c0d32ce900fd4e7fd7acbf7f5fdd9dc3391"
},
"da": {
"pin": false,
@ -429,7 +429,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "a92a63fb79ce056eceaadc5e6009f8615166db24"
"revision": "3e7b7d6fc79e7aa7a522cd4e715d627d36b7da48"
},
"en-GB": {
"pin": false,
@ -483,7 +483,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "144e2857d4416a9f77881758c821b70073641776"
"revision": "e48d80a1eb27d4f2f61cbb5590213aca4bd086bc"
},
"es-CL": {
"pin": false,
@ -501,7 +501,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "8dc0bc0e263866f475aba07aeffac4ac2c33b9fd"
"revision": "99a4be2c74839f57a06ae23092a6ed6a6456be16"
},
"es-ES": {
"pin": false,
@ -663,7 +663,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "acee7581e90605d397358fb1e977d6592b98ee9f"
"revision": "de54777091065d2e2586e14100291443889a87d1"
},
"fy-NL": {
"pin": false,
@ -681,7 +681,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "912eca8abda68ea89fe3828b90be025d12c0e06a"
"revision": "7ecdf7006c660ad24c34b32832fbbc9064f4bedb"
},
"ga-IE": {
"pin": false,
@ -933,7 +933,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "211c31bd4d44345e58d0b60e9fd3f291f00b42ec"
"revision": "5ade332ab27a99ae3cae7b1a3c9a2f8719fb6011"
},
"is": {
"pin": false,
@ -951,7 +951,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "9013b283a10d3221416e41f3286f04362fd101a2"
"revision": "b1fcc013b9532f950b76284fb0d12a3b1ce21d82"
},
"it": {
"pin": false,
@ -969,7 +969,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "435ddfa85768ca5a615effa437ce6ce042b0cfad"
"revision": "274739ede703030e4c0e2b5969fdc8facc6bd829"
},
"ja": {
"pin": false,
@ -1047,7 +1047,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "bb7643734330ea9c8ba8c4114450a15345e786b6"
"revision": "3badac8b94f60804b0a35088b0ea5c14ecfec957"
},
"km": {
"pin": false,
@ -1335,7 +1335,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "91e981553eb44036c937a0444fb8094060e13aa3"
"revision": "eaeaca90356b1cb1befb9b380fa7157a6b159197"
},
"nn-NO": {
"pin": false,
@ -1353,7 +1353,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "e64c7baf0035a40ef2924db1cea53a2408bcc6b0"
"revision": "3d741e78526872af2b3aa9e770f2ae5fce12314c"
},
"oc": {
"pin": false,
@ -1371,7 +1371,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "6cf67388ceb42112f9f0173439b3fa056f9cd1de"
"revision": "cef833d7b2ed57de9d8cd3ff36dde53f82a6fd08"
},
"pa-IN": {
"pin": false,
@ -1425,7 +1425,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "a21f1f89a92766e2e66bb3e22e6c711c4e20cd95"
"revision": "fdc76da512eb2bc09ebd60ab5879884624583d3f"
},
"pt-PT": {
"pin": false,
@ -1641,7 +1641,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "7647b135719899ee039bfa57ed5993011e58809a"
"revision": "190390b8eba1b48fa2e34ea8c077ee155e6b92a2"
},
"son": {
"pin": false,
@ -1875,7 +1875,7 @@
"win64-aarch64-devedition",
"win64-devedition"
],
"revision": "61e1aae8dbe301375d8c12378cd01f7d040eae4d"
"revision": "756a56528941a62738659cad4ab2d8858678528e"
},
"ur": {
"pin": false,

View file

@ -37,6 +37,15 @@ function callForEveryWindow(callback) {
}
export const EveryWindow = {
/**
* The current list of all browser windows whose delayedStartupPromise has resolved
*/
get readyWindows() {
return Array.from(Services.wm.getEnumerator("navigator:browser")).filter(
win => win.gBrowserInit?.delayedStartupFinished
);
},
/**
* Registers init and uninit functions to be called on every window.
*

View file

@ -90,10 +90,6 @@ html|select {
user-select: none;
}
.section-description {
color: var(--text-color-deemphasized);
}
/* Buttons get a default min-width in XUL. When they also have flex, they will
* be large enough anyway, and we don't want them to become smaller than their inner
* text which would then overflow the button. Set min-width back to auto for that:
@ -108,6 +104,10 @@ label {
margin: 0;
}
.description-deemphasized {
color: var(--text-color-deemphasized);
}
.tip-caption {
font-size: .9em;
color: var(--text-color-deemphasized);
@ -1247,7 +1247,6 @@ richlistitem .text-link:hover {
}
#moreFromMozillaCategory-header .subtitle {
color: var(--in-content-text-color);
margin-block-end: 24px;
line-height: 1.4em;
}

View file

@ -28,6 +28,8 @@
#include "mozilla/Components.h"
#include "mozilla/dom/StorageUtils.h"
#include "mozilla/dom/StorageUtils.h"
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/JSONWriter.h"
#include "nsIURL.h"
#include "nsEffectiveTLDService.h"
#include "nsIURIMutator.h"
@ -42,17 +44,16 @@
#include "nsIURIMutator.h"
#include "mozilla/PermissionManager.h"
#include "json/json.h"
#include "nsSerializationHelper.h"
namespace mozilla {
#include "js/JSON.h"
#include "ContentPrincipalJSONHandler.h"
#include "ExpandedPrincipalJSONHandler.h"
#include "NullPrincipalJSONHandler.h"
#include "PrincipalJSONHandler.h"
#include "SubsumedPrincipalJSONHandler.h"
const char* BasePrincipal::JSONEnumKeyStrings[4] = {
"0",
"1",
"2",
"3",
};
namespace mozilla {
BasePrincipal::BasePrincipal(PrincipalKind aKind,
const nsACString& aOriginNoSuffix,
@ -141,113 +142,183 @@ BasePrincipal::GetSiteOriginNoSuffix(nsACString& aSiteOrigin) {
return GetOriginNoSuffix(aSiteOrigin);
}
// Returns the inner Json::value of the serialized principal
// Example input and return values:
// Null principal:
// {"0":{"0":"moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}"}} ->
// {"0":"moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}"}
//
// Content principal:
// {"1":{"0":"https://mozilla.com"}} -> {"0":"https://mozilla.com"}
//
// Expanded principal:
// {"2":{"0":"<base64principal1>,<base64principal2>"}} ->
// {"0":"<base64principal1>,<base64principal2>"}
//
// System principal:
// {"3":{}} -> {}
// The aKey passed in also returns the corresponding PrincipalKind enum
//
// Warning: The Json::Value* pointer is into the aRoot object
static const Json::Value* GetPrincipalObject(const Json::Value& aRoot,
int& aOutPrincipalKind) {
const Json::Value::Members members = aRoot.getMemberNames();
// We only support one top level key in the object
if (members.size() != 1) {
return nullptr;
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::ProcessInnerResult(
bool aResult) {
if (!aResult) {
NS_WARNING("Failed to parse inner object");
mState = State::Error;
return false;
}
// members[0] here is the "0", "1", "2", "3" principalKind
// that is the top level of the serialized JSON principal
const std::string stringPrincipalKind = members[0];
// Next we take the string value from the JSON
// and convert it into the int for the BasePrincipal::PrincipalKind enum
// Verify that the key is within the valid range
int principalKind = std::stoi(stringPrincipalKind);
MOZ_ASSERT(BasePrincipal::eNullPrincipal == 0,
"We need to rely on 0 being a bounds check for the first "
"principal kind.");
if (principalKind < 0 || principalKind > BasePrincipal::eKindMax) {
return nullptr;
}
MOZ_ASSERT(principalKind == BasePrincipal::eNullPrincipal ||
principalKind == BasePrincipal::eContentPrincipal ||
principalKind == BasePrincipal::eExpandedPrincipal ||
principalKind == BasePrincipal::eSystemPrincipal);
aOutPrincipalKind = principalKind;
if (!aRoot[stringPrincipalKind].isObject()) {
return nullptr;
}
// Return the inner value of the principal object
return &aRoot[stringPrincipalKind];
return true;
}
// Accepts the JSON inner object without the wrapping principalKind
// (See GetPrincipalObject for the inner object response examples)
// Creates an array of KeyVal objects that are all defined on the principal
// Each principal type (null, content, expanded) has a KeyVal that stores the
// fields of the JSON
//
// This simplifies deserializing elsewhere as we do the checking for presence
// and string values here for the complete set of serializable keys that the
// corresponding principal supports.
//
// The KeyVal object has the following fields:
// - valueWasSerialized: is true if the deserialized JSON contained a string
// value
// - value: The string that was serialized for this key
// - key: an SerializableKeys enum value specific to the principal.
// For example content principal is an enum of: eURI, eDomain,
// eSuffix, eCSP
//
//
// Given an inner content principal:
// {"0": "https://mozilla.com", "2": "^privateBrowsingId=1"}
// | | | |
// ----------------------------- |
// | | |
// Key ----------------------
// |
// Value
//
// They Key "0" corresponds to ContentPrincipal::eURI
// They Key "1" corresponds to ContentPrincipal::eSuffix
template <typename T>
static nsTArray<typename T::KeyVal> GetJSONKeys(const Json::Value* aInput) {
int size = T::eMax + 1;
nsTArray<typename T::KeyVal> fields;
for (int i = 0; i != size; i++) {
typename T::KeyVal* field = fields.AppendElement();
// field->valueWasSerialized returns if the field was found in the
// deserialized code. This simplifies the consumers from having to check
// length.
field->valueWasSerialized = false;
field->key = static_cast<typename T::SerializableKeys>(i);
const std::string key = std::to_string(field->key);
if (aInput->isMember(key)) {
const Json::Value& val = (*aInput)[key];
if (val.isString()) {
field->value.Append(nsDependentCString(val.asCString()));
field->valueWasSerialized = true;
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::startObject() {
if (mInnerHandler.isSome()) {
return CallOnInner([&](auto& aInner) { return aInner.startObject(); });
}
switch (mState) {
case State::Init:
mState = State::StartObject;
break;
case State::SystemPrincipal_Key:
mState = State::SystemPrincipal_StartObject;
break;
default:
NS_WARNING("Unexpected object value");
mState = State::Error;
return false;
}
return true;
}
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::propertyName(
const JS::Latin1Char* name, size_t length) {
if (mInnerHandler.isSome()) {
return CallOnInner(
[&](auto& aInner) { return aInner.propertyName(name, length); });
}
switch (mState) {
case State::StartObject: {
if (length != 1) {
NS_WARNING(
nsPrintfCString("Unexpected property name length: %zu", length)
.get());
mState = State::Error;
return false;
}
char key = char(name[0]);
switch (key) {
case BasePrincipal::NullPrincipalKey:
mState = State::NullPrincipal_Inner;
mInnerHandler.emplace(VariantType<NullPrincipalJSONHandler>());
break;
case BasePrincipal::ContentPrincipalKey:
mState = State::ContentPrincipal_Inner;
mInnerHandler.emplace(VariantType<ContentPrincipalJSONHandler>());
break;
case BasePrincipal::SystemPrincipalKey:
mState = State::SystemPrincipal_Key;
break;
default:
if constexpr (CanContainExpandedPrincipal) {
if (key == BasePrincipal::ExpandedPrincipalKey) {
mState = State::ExpandedPrincipal_Inner;
mInnerHandler.emplace(
VariantType<ExpandedPrincipalJSONHandler>());
break;
}
}
NS_WARNING(
nsPrintfCString("Unexpected property name: '%c'", key).get());
mState = State::Error;
return false;
}
break;
}
default:
NS_WARNING("Unexpected property name");
mState = State::Error;
return false;
}
return true;
}
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::endObject() {
if (mInnerHandler.isSome()) {
return CallOnInner([&](auto& aInner) {
if (!aInner.endObject()) {
return false;
}
if (aInner.HasAccepted()) {
this->mPrincipal = aInner.mPrincipal.forget();
MOZ_ASSERT(this->mPrincipal);
mInnerHandler.reset();
}
return true;
});
}
switch (mState) {
case State::SystemPrincipal_StartObject:
mState = State::SystemPrincipal_EndObject;
break;
case State::SystemPrincipal_EndObject:
this->mPrincipal =
BasePrincipal::Cast(nsContentUtils::GetSystemPrincipal());
mState = State::EndObject;
break;
case State::NullPrincipal_Inner:
mState = State::EndObject;
break;
case State::ContentPrincipal_Inner:
mState = State::EndObject;
break;
default:
if constexpr (CanContainExpandedPrincipal) {
if (mState == State::ExpandedPrincipal_Inner) {
mState = State::EndObject;
break;
}
}
NS_WARNING("Unexpected end of object");
mState = State::Error;
return false;
}
return true;
}
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::startArray() {
if constexpr (CanContainExpandedPrincipal) {
if (mInnerHandler.isSome()) {
return CallOnInner([&](auto& aInner) { return aInner.startArray(); });
}
}
return fields;
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::endArray() {
if constexpr (CanContainExpandedPrincipal) {
if (mInnerHandler.isSome()) {
return CallOnInner([&](auto& aInner) { return aInner.endArray(); });
}
}
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
template <typename HandlerTypesT>
bool ContainerPrincipalJSONHandler<HandlerTypesT>::stringValue(
const JS::Latin1Char* str, size_t length) {
if (mInnerHandler.isSome()) {
return CallOnInner(
[&](auto& aInner) { return aInner.stringValue(str, length); });
}
NS_WARNING("Unexpected string value");
mState = State::Error;
return false;
}
template class ContainerPrincipalJSONHandler<PrincipalJSONHandlerTypes>;
template class ContainerPrincipalJSONHandler<SubsumedPrincipalJSONHandlerTypes>;
// Takes a JSON string and parses it turning it into a principal of the
// corresponding type
//
@ -266,114 +337,21 @@ static nsTArray<typename T::KeyVal> GetJSONKeys(const Json::Value* aInput) {
// SerializableKeys |
// Value
//
// The string is first deserialized with jsoncpp to get the Json::Value of the
// object. The inner JSON object is parsed with GetPrincipalObject which returns
// a KeyVal array of the inner object's fields. PrincipalKind is returned by
// GetPrincipalObject which is then used to decide which principal
// implementation of FromProperties to call. The corresponding FromProperties
// call takes the KeyVal fields and turns it into a principal.
already_AddRefed<BasePrincipal> BasePrincipal::FromJSON(
const nsACString& aJSON) {
Json::Value root;
Json::CharReaderBuilder builder;
std::unique_ptr<Json::CharReader> const reader(builder.newCharReader());
bool parseSuccess =
reader->parse(aJSON.BeginReading(), aJSON.EndReading(), &root, nullptr);
if (!parseSuccess) {
PrincipalJSONHandler handler;
if (!JS::ParseJSONWithHandler(
reinterpret_cast<const JS::Latin1Char*>(aJSON.BeginReading()),
aJSON.Length(), &handler)) {
NS_WARNING(
nsPrintfCString("Unable to parse: %s", aJSON.BeginReading()).get());
MOZ_ASSERT(false,
"Unable to parse string as JSON to deserialize as a principal");
return nullptr;
}
return FromJSON(root);
}
// Checks if an ExpandedPrincipal is using the legacy format, where
// sub-principals are Base64 encoded.
//
// Given a legacy expanded principal:
//
// *
// {"2": {"0": "eyIxIjp7IjAiOiJodHRwczovL2EuY29tLyJ9fQ=="}}
// | | |
// | ---------- Value
// | |
// PrincipalKind |
// |
// SerializableKeys
//
// The value is a CSV list of Base64 encoded prinipcals. The new format for this
// principal is:
//
// Subsumed principals
// |
// ------------------------------------
// * | |
// {"2": {"0": [{"1": {"0": https://mozilla.com"}}]}}
// | | |
// -------------- Value
// |
// PrincipalKind
//
// It is possible to tell these apart by checking the type of the property noted
// in both diagrams with an asterisk. In the legacy format the type will be a
// string and in the new format it will be an array.
static bool IsLegacyFormat(const Json::Value& aValue) {
const auto& specs = std::to_string(ExpandedPrincipal::eSpecs);
return aValue.isMember(specs) && aValue[specs].isString();
}
/* static */
already_AddRefed<BasePrincipal> BasePrincipal::FromJSON(
const Json::Value& aJSON) {
int principalKind = -1;
const Json::Value* value = GetPrincipalObject(aJSON, principalKind);
if (!value) {
#ifdef DEBUG
fprintf(stderr, "Unexpected JSON principal %s\n",
aJSON.toStyledString().c_str());
#endif
MOZ_ASSERT(false, "Unexpected JSON to deserialize as a principal");
return nullptr;
}
MOZ_ASSERT(principalKind != -1,
"PrincipalKind should always be >=0 by this point");
if (principalKind == eSystemPrincipal) {
RefPtr<BasePrincipal> principal =
BasePrincipal::Cast(nsContentUtils::GetSystemPrincipal());
return principal.forget();
}
if (principalKind == eNullPrincipal) {
nsTArray<NullPrincipal::KeyVal> res = GetJSONKeys<NullPrincipal>(value);
return NullPrincipal::FromProperties(res);
}
if (principalKind == eContentPrincipal) {
nsTArray<ContentPrincipal::KeyVal> res =
GetJSONKeys<ContentPrincipal>(value);
return ContentPrincipal::FromProperties(res);
}
if (principalKind == eExpandedPrincipal) {
// Check if expanded principals is stored in the new or the old format. See
// comment for `IsLegacyFormat`.
if (IsLegacyFormat(*value)) {
nsTArray<ExpandedPrincipal::KeyVal> res =
GetJSONKeys<ExpandedPrincipal>(value);
return ExpandedPrincipal::FromProperties(res);
}
return ExpandedPrincipal::FromProperties(*value);
}
MOZ_RELEASE_ASSERT(false, "Unexpected enum to deserialize as a principal");
}
nsresult BasePrincipal::PopulateJSONObject(Json::Value& aObject) {
return NS_OK;
return handler.Get();
}
// Returns a JSON representation of the principal.
@ -383,34 +361,42 @@ nsresult BasePrincipal::ToJSON(nsACString& aJSON) {
MOZ_ASSERT(aJSON.IsEmpty(), "ToJSON only supports an empty result input");
aJSON.Truncate();
Json::Value root = Json::objectValue;
nsresult rv = ToJSON(root);
NS_ENSURE_SUCCESS(rv, rv);
// NOTE: JSONWriter emits raw UTF-8 code units for non-ASCII range.
JSONStringRefWriteFunc func(aJSON);
JSONWriter writer(func, JSONWriter::CollectionStyle::SingleLineStyle);
static StaticAutoPtr<Json::StreamWriterBuilder> sJSONBuilderForPrincipals;
if (!sJSONBuilderForPrincipals) {
sJSONBuilderForPrincipals = new Json::StreamWriterBuilder();
(*sJSONBuilderForPrincipals)["indentation"] = "";
(*sJSONBuilderForPrincipals)["emitUTF8"] = true;
ClearOnShutdown(&sJSONBuilderForPrincipals);
}
std::string result = Json::writeString(*sJSONBuilderForPrincipals, root);
aJSON.Append(result);
if (aJSON.Length() == 0) {
MOZ_ASSERT(false, "JSON writer failed to output a principal serialization");
return NS_ERROR_UNEXPECTED;
}
nsresult rv = ToJSON(writer);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult BasePrincipal::ToJSON(Json::Value& aObject) {
nsresult BasePrincipal::ToJSON(JSONWriter& aWriter) {
static_assert(eKindMax < ArrayLength(JSONEnumKeyStrings));
nsresult rv = PopulateJSONObject(
(aObject[Json::StaticString(JSONEnumKeyStrings[Kind()])] =
Json::objectValue));
aWriter.Start(JSONWriter::CollectionStyle::SingleLineStyle);
nsresult rv = WriteJSONProperties(aWriter);
NS_ENSURE_SUCCESS(rv, rv);
aWriter.End();
return NS_OK;
}
nsresult BasePrincipal::WriteJSONProperties(JSONWriter& aWriter) {
aWriter.StartObjectProperty(JSONEnumKeyStrings[Kind()],
JSONWriter::CollectionStyle::SingleLineStyle);
nsresult rv = WriteJSONInnerProperties(aWriter);
NS_ENSURE_SUCCESS(rv, rv);
aWriter.EndObject();
return NS_OK;
}
nsresult BasePrincipal::WriteJSONInnerProperties(JSONWriter& aWriter) {
return NS_OK;
}
@ -1618,10 +1604,10 @@ BasePrincipal::Deserializer::Write(nsIObjectOutputStream* aStream) {
}
/* static */
void BasePrincipal::SetJSONValue(Json::Value& aObject, const char* aKey,
const nsCString& aValue) {
aObject[Json::StaticString(aKey)] =
Json::Value(aValue.BeginReading(), aValue.EndReading());
void BasePrincipal::WriteJSONProperty(JSONWriter& aWriter,
const Span<const char>& aKey,
const nsCString& aValue) {
aWriter.StringProperty(aKey, aValue);
}
} // namespace mozilla

View file

@ -27,12 +27,11 @@ class nsIChannel;
class nsIReferrerInfo;
class nsISupports;
class nsIURI;
namespace Json {
class Value;
}
namespace mozilla {
class JSONWriter;
namespace dom {
enum class ReferrerPolicy : uint8_t;
}
@ -91,6 +90,15 @@ class BasePrincipal : public nsJSPrincipals {
eKindMax = eSystemPrincipal
};
static constexpr char NullPrincipalKey = '0';
static_assert(eNullPrincipal == 0);
static constexpr char ContentPrincipalKey = '1';
static_assert(eContentPrincipal == 1);
static constexpr char ExpandedPrincipalKey = '2';
static_assert(eExpandedPrincipal == 2);
static constexpr char SystemPrincipalKey = '3';
static_assert(eSystemPrincipal == 3);
template <typename T>
bool Is() const {
return mKind == T::Kind();
@ -189,13 +197,14 @@ class BasePrincipal : public nsJSPrincipals {
NS_IMETHOD GetPrecursorPrincipal(nsIPrincipal** aPrecursor) override;
nsresult ToJSON(nsACString& aJSON);
nsresult ToJSON(Json::Value& aObject);
nsresult ToJSON(JSONWriter& aWriter);
nsresult WriteJSONProperties(JSONWriter& aWriter);
static already_AddRefed<BasePrincipal> FromJSON(const nsACString& aJSON);
static already_AddRefed<BasePrincipal> FromJSON(const Json::Value& aJSON);
// Method populates a passed Json::Value with serializable fields
// which represent all of the fields to deserialize the principal
virtual nsresult PopulateJSONObject(Json::Value& aObject);
// Method to write serializable fields which represent all of the fields to
// deserialize the principal.
virtual nsresult WriteJSONInnerProperties(JSONWriter& aWriter);
virtual bool AddonHasPermission(const nsAtom* aPerm);
@ -344,20 +353,26 @@ class BasePrincipal : public nsJSPrincipals {
};
private:
static const char* JSONEnumKeyStrings[4];
static constexpr Span<const char> JSONEnumKeyStrings[4] = {
MakeStringSpan("0"),
MakeStringSpan("1"),
MakeStringSpan("2"),
MakeStringSpan("3"),
};
static void SetJSONValue(Json::Value& aObject, const char* aKey,
const nsCString& aValue);
static void WriteJSONProperty(JSONWriter& aWriter,
const Span<const char>& aKey,
const nsCString& aValue);
protected:
template <size_t EnumValue>
static inline constexpr const char* JSONEnumKeyString() {
static inline constexpr const Span<const char>& JSONEnumKeyString() {
static_assert(EnumValue < ArrayLength(JSONEnumKeyStrings));
return JSONEnumKeyStrings[EnumValue];
}
template <size_t EnumValue>
static void SetJSONValue(Json::Value& aObject, const nsCString& aValue) {
SetJSONValue(aObject, JSONEnumKeyString<EnumValue>(), aValue);
static void WriteJSONProperty(JSONWriter& aWriter, const nsCString& aValue) {
WriteJSONProperty(aWriter, JSONEnumKeyString<EnumValue>(), aValue);
}
private:

View file

@ -38,7 +38,9 @@
#include "mozilla/HashFunctions.h"
#include "nsSerializationHelper.h"
#include "json/json.h"
#include "js/JSON.h"
#include "ContentPrincipalJSONHandler.h"
using namespace mozilla;
@ -605,14 +607,14 @@ ContentPrincipal::Deserializer::Read(nsIObjectInputStream* aStream) {
return NS_OK;
}
nsresult ContentPrincipal::PopulateJSONObject(Json::Value& aObject) {
nsresult ContentPrincipal::WriteJSONInnerProperties(JSONWriter& aWriter) {
nsAutoCString principalURI;
nsresult rv = mURI->GetSpec(principalURI);
NS_ENSURE_SUCCESS(rv, rv);
// We turn each int enum field into a JSON string key of the object
// aObject is the inner JSON object that has stringified enum keys
// An example aObject might be:
// We turn each int enum field into a JSON string key of the object, aWriter
// is set up to be inside of the inner object that has stringified enum keys
// An example inner object might be:
//
// eURI eSuffix
// | |
@ -623,7 +625,7 @@ nsresult ContentPrincipal::PopulateJSONObject(Json::Value& aObject) {
// Key ----------------------
// |
// Value
SetJSONValue<eURI>(aObject, principalURI);
WriteJSONProperty<eURI>(aWriter, principalURI);
if (GetHasExplicitDomain()) {
nsAutoCString domainStr;
@ -632,78 +634,159 @@ nsresult ContentPrincipal::PopulateJSONObject(Json::Value& aObject) {
rv = mDomain->GetSpec(domainStr);
NS_ENSURE_SUCCESS(rv, rv);
}
SetJSONValue<eDomain>(aObject, domainStr);
WriteJSONProperty<eDomain>(aWriter, domainStr);
}
nsAutoCString suffix;
OriginAttributesRef().CreateSuffix(suffix);
if (suffix.Length() > 0) {
SetJSONValue<eSuffix>(aObject, suffix);
WriteJSONProperty<eSuffix>(aWriter, suffix);
}
return NS_OK;
}
already_AddRefed<BasePrincipal> ContentPrincipal::FromProperties(
nsTArray<ContentPrincipal::KeyVal>& aFields) {
MOZ_ASSERT(aFields.Length() == eMax + 1, "Must have all the keys");
nsresult rv;
nsCOMPtr<nsIURI> principalURI;
nsCOMPtr<nsIURI> domain;
nsCOMPtr<nsIContentSecurityPolicy> csp;
OriginAttributes attrs;
// The odd structure here is to make the code to not compile
// if all the switch enum cases haven't been codified
for (const auto& field : aFields) {
switch (field.key) {
case ContentPrincipal::eURI:
if (!field.valueWasSerialized) {
MOZ_ASSERT(
false,
"Content principals require a principal URI in serialized JSON");
return nullptr;
}
rv = NS_NewURI(getter_AddRefs(principalURI), field.value.get());
NS_ENSURE_SUCCESS(rv, nullptr);
{
// Enforce re-parsing about: URIs so that if they change, we
// continue to use their new principals correctly.
if (principalURI->SchemeIs("about")) {
nsAutoCString spec;
principalURI->GetSpec(spec);
if (NS_FAILED(NS_NewURI(getter_AddRefs(principalURI), spec))) {
return nullptr;
}
}
}
break;
case ContentPrincipal::eDomain:
if (field.valueWasSerialized) {
rv = NS_NewURI(getter_AddRefs(domain), field.value.get());
NS_ENSURE_SUCCESS(rv, nullptr);
}
break;
case ContentPrincipal::eSuffix:
if (field.valueWasSerialized) {
bool ok = attrs.PopulateFromSuffix(field.value);
if (!ok) {
return nullptr;
}
}
break;
}
}
nsAutoCString originNoSuffix;
rv = ContentPrincipal::GenerateOriginNoSuffixFromURI(principalURI,
originNoSuffix);
if (NS_FAILED(rv)) {
return nullptr;
bool ContentPrincipalJSONHandler::startObject() {
switch (mState) {
case State::Init:
mState = State::StartObject;
break;
default:
NS_WARNING("Unexpected object value");
mState = State::Error;
return false;
}
RefPtr<ContentPrincipal> principal =
new ContentPrincipal(principalURI, attrs, originNoSuffix, domain);
return principal.forget();
return true;
}
bool ContentPrincipalJSONHandler::propertyName(const JS::Latin1Char* name,
size_t length) {
switch (mState) {
case State::StartObject:
case State::AfterPropertyValue: {
if (length != 1) {
NS_WARNING(
nsPrintfCString("Unexpected property name length: %zu", length)
.get());
mState = State::Error;
return false;
}
char key = char(name[0]);
switch (key) {
case ContentPrincipal::URIKey:
mState = State::URIKey;
break;
case ContentPrincipal::DomainKey:
mState = State::DomainKey;
break;
case ContentPrincipal::SuffixKey:
mState = State::SuffixKey;
break;
default:
NS_WARNING(
nsPrintfCString("Unexpected property name: '%c'", key).get());
mState = State::Error;
return false;
}
break;
}
default:
NS_WARNING("Unexpected property name");
mState = State::Error;
return false;
}
return true;
}
bool ContentPrincipalJSONHandler::endObject() {
switch (mState) {
case State::AfterPropertyValue: {
MOZ_ASSERT(mPrincipalURI);
// NOTE: mDomain is optional.
nsAutoCString originNoSuffix;
nsresult rv = ContentPrincipal::GenerateOriginNoSuffixFromURI(
mPrincipalURI, originNoSuffix);
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
mPrincipal =
new ContentPrincipal(mPrincipalURI, mAttrs, originNoSuffix, mDomain);
MOZ_ASSERT(mPrincipal);
mState = State::EndObject;
break;
}
default:
NS_WARNING("Unexpected end of object");
mState = State::Error;
return false;
}
return true;
}
bool ContentPrincipalJSONHandler::stringValue(const JS::Latin1Char* str,
size_t length) {
switch (mState) {
case State::URIKey: {
nsDependentCSubstring spec(reinterpret_cast<const char*>(str), length);
nsresult rv = NS_NewURI(getter_AddRefs(mPrincipalURI), spec);
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
{
// Enforce re-parsing about: URIs so that if they change, we
// continue to use their new principals correctly.
if (mPrincipalURI->SchemeIs("about")) {
nsAutoCString spec;
mPrincipalURI->GetSpec(spec);
rv = NS_NewURI(getter_AddRefs(mPrincipalURI), spec);
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
}
}
mState = State::AfterPropertyValue;
break;
}
case State::DomainKey: {
nsDependentCSubstring spec(reinterpret_cast<const char*>(str), length);
nsresult rv = NS_NewURI(getter_AddRefs(mDomain), spec);
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
mState = State::AfterPropertyValue;
break;
}
case State::SuffixKey: {
nsDependentCSubstring attrs(reinterpret_cast<const char*>(str), length);
if (!mAttrs.PopulateFromSuffix(attrs)) {
mState = State::Error;
return false;
}
mState = State::AfterPropertyValue;
break;
}
default:
NS_WARNING("Unexpected string value");
mState = State::Error;
return false;
}
return true;
}

View file

@ -15,12 +15,10 @@
#include "mozilla/Mutex.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
namespace Json {
class Value;
}
namespace mozilla {
class JSONWriter;
class ContentPrincipal final : public BasePrincipal {
public:
NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
@ -49,7 +47,8 @@ class ContentPrincipal final : public BasePrincipal {
RefPtr<extensions::WebExtensionPolicyCore> AddonPolicyCore();
virtual nsresult PopulateJSONObject(Json::Value& aObject) override;
virtual nsresult WriteJSONInnerProperties(JSONWriter& aWriter) override;
// Serializable keys are the valid enum fields the serialization supports
enum SerializableKeys : uint8_t {
eURI = 0,
@ -57,10 +56,13 @@ class ContentPrincipal final : public BasePrincipal {
eSuffix,
eMax = eSuffix
};
typedef mozilla::BasePrincipal::KeyValT<SerializableKeys> KeyVal;
static already_AddRefed<BasePrincipal> FromProperties(
nsTArray<ContentPrincipal::KeyVal>& aFields);
static constexpr char URIKey = '0';
static_assert(eURI == 0);
static constexpr char DomainKey = '1';
static_assert(eDomain == 1);
static constexpr char SuffixKey = '2';
static_assert(eSuffix == 2);
class Deserializer : public BasePrincipal::Deserializer {
public:

View file

@ -0,0 +1,94 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ContentPrincipalJSONHandler_h
#define mozilla_ContentPrincipalJSONHandler_h
#include <stddef.h> // size_t
#include <stdint.h> // uint32_t
#include "js/TypeDecls.h" // JS::Latin1Char
#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
#include "mozilla/RefPtr.h" // RefPtr
#include "nsCOMPtr.h" // nsCOMPtr
#include "nsDebug.h" // NS_WARNING
#include "nsIURI.h" // nsIURI
#include "ContentPrincipal.h"
#include "OriginAttributes.h"
#include "SharedJSONHandler.h"
namespace mozilla {
// JSON parse handler for an inner object for ContentPrincipal.
// Used by PrincipalJSONHandler or SubsumedPrincipalJSONHandler.
class ContentPrincipalJSONHandler : public PrincipalJSONHandlerShared {
enum class State {
Init,
// After the inner object's '{'.
StartObject,
// After the property key for eURI.
URIKey,
// After the property key for eDomain.
DomainKey,
// After the property key for eSuffix.
SuffixKey,
// After the property value for eURI, eDomain, or eSuffix.
AfterPropertyValue,
// After the inner object's '}'.
EndObject,
Error,
};
public:
ContentPrincipalJSONHandler() = default;
virtual ~ContentPrincipalJSONHandler() = default;
virtual bool startObject() override;
using PrincipalJSONHandlerShared::propertyName;
virtual bool propertyName(const JS::Latin1Char* name, size_t length) override;
virtual bool endObject() override;
virtual bool startArray() override {
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
virtual bool endArray() override {
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
using PrincipalJSONHandlerShared::stringValue;
virtual bool stringValue(const JS::Latin1Char* str, size_t length) override;
bool HasAccepted() const { return mState == State::EndObject; }
protected:
virtual void SetErrorState() override { mState = State::Error; }
private:
State mState = State::Init;
nsCOMPtr<nsIURI> mPrincipalURI;
nsCOMPtr<nsIURI> mDomain;
OriginAttributes mAttrs;
};
} // namespace mozilla
#endif // mozilla_ContentPrincipalJSONHandler_h

View file

@ -10,7 +10,11 @@
#include "nsReadableUtils.h"
#include "mozilla/Base64.h"
#include "mozilla/extensions/WebExtensionPolicy.h"
#include "json/json.h"
#include "mozilla/JSONWriter.h"
#include "js/JSON.h"
#include "ExpandedPrincipalJSONHandler.h"
#include "SubsumedPrincipalJSONHandler.h"
using namespace mozilla;
@ -262,121 +266,211 @@ nsresult ExpandedPrincipal::GetSiteIdentifier(SiteIdentifier& aSite) {
return NS_OK;
}
nsresult ExpandedPrincipal::PopulateJSONObject(Json::Value& aObject) {
Json::Value& principalList =
aObject[Json::StaticString(JSONEnumKeyString<eSpecs>())] =
Json::arrayValue;
nsresult ExpandedPrincipal::WriteJSONInnerProperties(JSONWriter& aWriter) {
aWriter.StartArrayProperty(JSONEnumKeyString<eSpecs>(),
JSONWriter::CollectionStyle::SingleLineStyle);
for (const auto& principal : mPrincipals) {
Json::Value object = Json::objectValue;
nsresult rv = BasePrincipal::Cast(principal)->ToJSON(object);
aWriter.StartObjectElement(JSONWriter::CollectionStyle::SingleLineStyle);
nsresult rv = BasePrincipal::Cast(principal)->WriteJSONProperties(aWriter);
NS_ENSURE_SUCCESS(rv, rv);
principalList.append(std::move(object));
aWriter.EndObject();
}
aWriter.EndArray();
nsAutoCString suffix;
OriginAttributesRef().CreateSuffix(suffix);
if (suffix.Length() > 0) {
SetJSONValue<eSuffix>(aObject, suffix);
WriteJSONProperty<eSuffix>(aWriter, suffix);
}
return NS_OK;
}
already_AddRefed<BasePrincipal> ExpandedPrincipal::FromProperties(
nsTArray<ExpandedPrincipal::KeyVal>& aFields) {
MOZ_ASSERT(aFields.Length() == eMax + 1, "Must have all the keys");
nsTArray<nsCOMPtr<nsIPrincipal>> allowList;
OriginAttributes attrs;
// The odd structure here is to make the code to not compile
// if all the switch enum cases haven't been codified
for (const auto& field : aFields) {
switch (field.key) {
case ExpandedPrincipal::eSpecs:
if (!field.valueWasSerialized) {
MOZ_ASSERT(false,
"Expanded principals require specs in serialized JSON");
return nullptr;
}
for (const nsACString& each : field.value.Split(',')) {
nsAutoCString result;
nsresult rv;
rv = Base64Decode(each, result);
MOZ_ASSERT(NS_SUCCEEDED(rv), "failed to decode");
NS_ENSURE_SUCCESS(rv, nullptr);
nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(result);
allowList.AppendElement(principal);
}
break;
case ExpandedPrincipal::eSuffix:
if (field.valueWasSerialized) {
bool ok = attrs.PopulateFromSuffix(field.value);
if (!ok) {
return nullptr;
}
}
break;
}
bool ExpandedPrincipalJSONHandler::ProcessSubsumedResult(bool aResult) {
if (!aResult) {
NS_WARNING("Failed to parse subsumed principal");
mState = State::Error;
return false;
}
if (allowList.Length() == 0) {
return nullptr;
}
RefPtr<ExpandedPrincipal> expandedPrincipal =
ExpandedPrincipal::Create(allowList, attrs);
return expandedPrincipal.forget();
return true;
}
/* static */
already_AddRefed<BasePrincipal> ExpandedPrincipal::FromProperties(
const Json::Value& aJSON) {
MOZ_ASSERT(aJSON.size() <= eMax + 1, "Must have at most, all the properties");
const std::string specs = std::to_string(eSpecs);
const std::string suffix = std::to_string(eSuffix);
MOZ_ASSERT(aJSON.isMember(specs), "The eSpecs member is required");
MOZ_ASSERT(aJSON.size() == 1 || aJSON.isMember(suffix),
"eSuffix is optional");
const auto* specsValue =
aJSON.find(specs.c_str(), specs.c_str() + specs.length());
if (!specsValue) {
MOZ_ASSERT(false, "Expanded principals require specs in serialized JSON");
return nullptr;
bool ExpandedPrincipalJSONHandler::startObject() {
if (mSubsumedHandler.isSome()) {
return ProcessSubsumedResult(mSubsumedHandler->startObject());
}
nsTArray<nsCOMPtr<nsIPrincipal>> allowList;
for (const auto& principalJSON : *specsValue) {
if (nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::FromJSON(principalJSON)) {
allowList.AppendElement(principal);
switch (mState) {
case State::Init:
mState = State::StartObject;
break;
case State::StartArray:
mState = State::SubsumedPrincipal;
[[fallthrough]];
case State::SubsumedPrincipal:
mSubsumedHandler.emplace();
return ProcessSubsumedResult(mSubsumedHandler->startObject());
default:
NS_WARNING("Unexpected object value");
mState = State::Error;
return false;
}
return true;
}
bool ExpandedPrincipalJSONHandler::propertyName(const JS::Latin1Char* name,
size_t length) {
if (mSubsumedHandler.isSome()) {
return ProcessSubsumedResult(mSubsumedHandler->propertyName(name, length));
}
switch (mState) {
case State::StartObject:
case State::AfterPropertyValue: {
if (length != 1) {
NS_WARNING(
nsPrintfCString("Unexpected property name length: %zu", length)
.get());
mState = State::Error;
return false;
}
char key = char(name[0]);
switch (key) {
case ExpandedPrincipal::SpecsKey:
mState = State::SpecsKey;
break;
case ExpandedPrincipal::SuffixKey:
mState = State::SuffixKey;
break;
default:
NS_WARNING(
nsPrintfCString("Unexpected property name: '%c'", key).get());
mState = State::Error;
return false;
}
break;
}
default:
NS_WARNING("Unexpected property name");
mState = State::Error;
return false;
}
if (allowList.Length() == 0) {
return nullptr;
}
return true;
}
OriginAttributes attrs;
if (aJSON.isMember(suffix)) {
const auto& value = aJSON[suffix];
if (!value.isString()) {
return nullptr;
bool ExpandedPrincipalJSONHandler::endObject() {
if (mSubsumedHandler.isSome()) {
if (!ProcessSubsumedResult(mSubsumedHandler->endObject())) {
return false;
}
bool ok = attrs.PopulateFromSuffix(nsDependentCString(value.asCString()));
if (!ok) {
return nullptr;
if (mSubsumedHandler->HasAccepted()) {
nsCOMPtr<nsIPrincipal> principal = mSubsumedHandler->mPrincipal.forget();
mSubsumedHandler.reset();
mAllowList.AppendElement(principal);
}
return true;
}
RefPtr<ExpandedPrincipal> expandedPrincipal =
ExpandedPrincipal::Create(allowList, attrs);
switch (mState) {
case State::AfterPropertyValue:
mPrincipal = ExpandedPrincipal::Create(mAllowList, mAttrs);
MOZ_ASSERT(mPrincipal);
return expandedPrincipal.forget();
mState = State::EndObject;
break;
default:
NS_WARNING("Unexpected end of object");
mState = State::Error;
return false;
}
return true;
}
bool ExpandedPrincipalJSONHandler::startArray() {
switch (mState) {
case State::SpecsKey:
mState = State::StartArray;
break;
default:
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
return true;
}
bool ExpandedPrincipalJSONHandler::endArray() {
switch (mState) {
case State::SubsumedPrincipal: {
mState = State::AfterPropertyValue;
break;
}
default:
NS_WARNING("Unexpected end of array");
mState = State::Error;
return false;
}
return true;
}
bool ExpandedPrincipalJSONHandler::stringValue(const JS::Latin1Char* str,
size_t length) {
if (mSubsumedHandler.isSome()) {
return ProcessSubsumedResult(mSubsumedHandler->stringValue(str, length));
}
switch (mState) {
case State::SpecsKey: {
nsDependentCSubstring specs(reinterpret_cast<const char*>(str), length);
for (const nsACString& each : specs.Split(',')) {
nsAutoCString result;
nsresult rv = Base64Decode(each, result);
MOZ_ASSERT(NS_SUCCEEDED(rv), "failed to decode");
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
nsCOMPtr<nsIPrincipal> principal = BasePrincipal::FromJSON(result);
if (!principal) {
mState = State::Error;
return false;
}
mAllowList.AppendElement(principal);
}
mState = State::AfterPropertyValue;
break;
}
case State::SuffixKey: {
nsDependentCSubstring attrs(reinterpret_cast<const char*>(str), length);
if (!mAttrs.PopulateFromSuffix(attrs)) {
mState = State::Error;
return false;
}
mState = State::AfterPropertyValue;
break;
}
default:
NS_WARNING("Unexpected string value");
mState = State::Error;
return false;
}
return true;
}
NS_IMETHODIMP

View file

@ -15,9 +15,9 @@
class nsIContentSecurityPolicy;
namespace Json {
class Value;
}
namespace mozilla {
class JSONWriter;
} // namespace mozilla
class ExpandedPrincipal : public nsIExpandedPrincipal,
public mozilla::BasePrincipal {
@ -57,20 +57,16 @@ class ExpandedPrincipal : public nsIExpandedPrincipal,
nsresult GetSiteIdentifier(mozilla::SiteIdentifier& aSite) override;
virtual nsresult PopulateJSONObject(Json::Value& aObject) override;
virtual nsresult WriteJSONInnerProperties(
mozilla::JSONWriter& aWriter) override;
// Serializable keys are the valid enum fields the serialization supports
enum SerializableKeys : uint8_t { eSpecs = 0, eSuffix, eMax = eSuffix };
typedef mozilla::BasePrincipal::KeyValT<SerializableKeys> KeyVal;
// This is the legacy serializer for expanded principals. See note for
// `IsLegacyFormat` in BasePrincipal.cpp.
static already_AddRefed<BasePrincipal> FromProperties(
nsTArray<ExpandedPrincipal::KeyVal>& aFields);
// This is the new serializer for expanded principals. See note for
// `IsLegacyFormat` in BasePrincipal.cpp.
static already_AddRefed<BasePrincipal> FromProperties(
const Json::Value& aJSON);
static constexpr char SpecsKey = '0';
static_assert(eSpecs == 0);
static constexpr char SuffixKey = '1';
static_assert(eSuffix == 1);
class Deserializer : public BasePrincipal::Deserializer {
public:

View file

@ -0,0 +1,130 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_ExpandedPrincipalJSONHandler_h
#define mozilla_ExpandedPrincipalJSONHandler_h
#include <stddef.h> // size_t
#include <stdint.h> // uint32_t
#include "js/TypeDecls.h" // JS::Latin1Char
#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
#include "mozilla/Maybe.h" // Maybe
#include "mozilla/RefPtr.h" // RefPtr
#include "nsCOMPtr.h" // nsCOMPtr
#include "nsDebug.h" // NS_WARNING
#include "nsIPrincipal.h" // nsIPrincipal
#include "nsTArray.h" // nsTArray
#include "OriginAttributes.h"
#include "ExpandedPrincipal.h"
#include "SubsumedPrincipalJSONHandler.h"
#include "SharedJSONHandler.h"
namespace mozilla {
// JSON parse handler for an inner object for ExpandedPrincipal.
//
// # Legacy format
//
// inner object
// |
// -------------------------------------------------
// | |
// {"2": {"0": "eyIxIjp7IjAiOiJodHRwczovL2EuY29tLyJ9fQ=="}}
// | | |
// | ---------- Value
// | |
// PrincipalKind |
// |
// SerializableKeys
//
// The value is a CSV list of Base64 encoded prinipcals.
//
// # New format
//
// inner object
// |
// -------------------------------------------
// | |
// | Subsumed principals |
// | | |
// | ------------------------------------|
// | | ||
// {"2": {"0": [{"1": {"0": https://mozilla.com"}}]}}
// | | |
// -------------- Value
// |
// PrincipalKind
//
// Used by PrincipalJSONHandler.
class ExpandedPrincipalJSONHandler : public PrincipalJSONHandlerShared {
enum class State {
Init,
// After the inner object's '{'.
StartObject,
// After the property key for eSpecs.
SpecsKey,
// After the property key for eSuffix.
SuffixKey,
// After the subsumed principals array's '['.
StartArray,
// Subsumed principals array's item.
// Delegates to mSubsumedHandler until the subsumed object's '}'.
SubsumedPrincipal,
// After the property value for eSpecs or eSuffix,
// including after the subsumed principals array's ']'.
AfterPropertyValue,
// After the inner object's '}'.
EndObject,
Error,
};
public:
ExpandedPrincipalJSONHandler() = default;
virtual ~ExpandedPrincipalJSONHandler() = default;
virtual bool startObject() override;
using PrincipalJSONHandlerShared::propertyName;
virtual bool propertyName(const JS::Latin1Char* name, size_t length) override;
virtual bool endObject() override;
virtual bool startArray() override;
virtual bool endArray() override;
using PrincipalJSONHandlerShared::stringValue;
virtual bool stringValue(const JS::Latin1Char* str, size_t length) override;
bool HasAccepted() const { return mState == State::EndObject; }
protected:
virtual void SetErrorState() override { mState = State::Error; }
private:
bool ProcessSubsumedResult(bool aResult);
private:
State mState = State::Init;
nsTArray<nsCOMPtr<nsIPrincipal>> mAllowList;
OriginAttributes mAttrs;
Maybe<SubsumedPrincipalJSONHandler> mSubsumedHandler;
};
} // namespace mozilla
#endif // mozilla_ExpandedPrincipalJSONHandler_h

View file

@ -27,7 +27,8 @@
#include "pratom.h"
#include "nsIObjectInputStream.h"
#include "json/json.h"
#include "js/JSON.h"
#include "NullPrincipalJSONHandler.h"
using namespace mozilla;
@ -236,58 +237,21 @@ NullPrincipal::Deserializer::Read(nsIObjectInputStream* aStream) {
return NS_OK;
}
nsresult NullPrincipal::PopulateJSONObject(Json::Value& aObject) {
nsresult NullPrincipal::WriteJSONInnerProperties(JSONWriter& aWriter) {
nsAutoCString principalURI;
nsresult rv = mURI->GetSpec(principalURI);
NS_ENSURE_SUCCESS(rv, rv);
SetJSONValue<eSpec>(aObject, principalURI);
WriteJSONProperty<eSpec>(aWriter, principalURI);
nsAutoCString suffix;
OriginAttributesRef().CreateSuffix(suffix);
if (suffix.Length() > 0) {
SetJSONValue<eSuffix>(aObject, suffix);
WriteJSONProperty<eSuffix>(aWriter, suffix);
}
return NS_OK;
}
already_AddRefed<BasePrincipal> NullPrincipal::FromProperties(
nsTArray<NullPrincipal::KeyVal>& aFields) {
MOZ_ASSERT(aFields.Length() == eMax + 1, "Must have all the keys");
nsresult rv;
nsCOMPtr<nsIURI> uri;
OriginAttributes attrs;
// The odd structure here is to make the code to not compile
// if all the switch enum cases haven't been codified
for (const auto& field : aFields) {
switch (field.key) {
case NullPrincipal::eSpec:
if (!field.valueWasSerialized) {
MOZ_ASSERT(false,
"Null principals require a spec URI in serialized JSON");
return nullptr;
}
rv = NS_NewURI(getter_AddRefs(uri), field.value);
NS_ENSURE_SUCCESS(rv, nullptr);
break;
case NullPrincipal::eSuffix:
bool ok = attrs.PopulateFromSuffix(field.value);
if (!ok) {
return nullptr;
}
break;
}
}
if (!uri) {
MOZ_ASSERT(false, "No URI deserialized");
return nullptr;
}
return NullPrincipal::Create(attrs, uri);
}
NS_IMETHODIMP
NullPrincipal::GetPrecursorPrincipal(nsIPrincipal** aPrincipal) {
*aPrincipal = nullptr;
@ -331,3 +295,107 @@ NullPrincipal::GetPrecursorPrincipal(nsIPrincipal** aPrincipal) {
contentPrincipal.forget(aPrincipal);
return NS_OK;
}
bool NullPrincipalJSONHandler::startObject() {
switch (mState) {
case State::Init:
mState = State::StartObject;
break;
default:
NS_WARNING("Unexpected object value");
mState = State::Error;
return false;
}
return true;
}
bool NullPrincipalJSONHandler::propertyName(const JS::Latin1Char* name,
size_t length) {
switch (mState) {
case State::StartObject:
case State::AfterPropertyValue: {
if (length != 1) {
NS_WARNING(
nsPrintfCString("Unexpected property name length: %zu", length)
.get());
mState = State::Error;
return false;
}
char key = char(name[0]);
switch (key) {
case NullPrincipal::SpecKey:
mState = State::SpecKey;
break;
case NullPrincipal::SuffixKey:
mState = State::SuffixKey;
break;
default:
NS_WARNING(
nsPrintfCString("Unexpected property name: '%c'", key).get());
mState = State::Error;
return false;
}
break;
}
default:
NS_WARNING("Unexpected property name");
mState = State::Error;
return false;
}
return true;
}
bool NullPrincipalJSONHandler::endObject() {
switch (mState) {
case State::AfterPropertyValue:
MOZ_ASSERT(mUri);
mPrincipal = NullPrincipal::Create(mAttrs, mUri);
MOZ_ASSERT(mPrincipal);
mState = State::EndObject;
break;
default:
NS_WARNING("Unexpected end of object");
mState = State::Error;
return false;
}
return true;
}
bool NullPrincipalJSONHandler::stringValue(const JS::Latin1Char* str,
size_t length) {
switch (mState) {
case State::SpecKey: {
nsDependentCSubstring spec(reinterpret_cast<const char*>(str), length);
nsresult rv = NS_NewURI(getter_AddRefs(mUri), spec);
if (NS_FAILED(rv)) {
mState = State::Error;
return false;
}
mState = State::AfterPropertyValue;
break;
}
case State::SuffixKey: {
nsDependentCSubstring attrs(reinterpret_cast<const char*>(str), length);
if (!mAttrs.PopulateFromSuffix(attrs)) {
mState = State::Error;
return false;
}
mState = State::AfterPropertyValue;
break;
}
default:
NS_WARNING("Unexpected string value");
mState = State::Error;
return false;
}
return true;
}

View file

@ -21,9 +21,6 @@
class nsIDocShell;
class nsIURI;
namespace Json {
class Value;
}
#define NS_NULLPRINCIPAL_CID \
{ \
@ -36,6 +33,8 @@ class Value;
namespace mozilla {
class JSONWriter;
class NullPrincipal final : public BasePrincipal {
public:
static PrincipalKind Kind() { return eNullPrincipal; }
@ -85,14 +84,15 @@ class NullPrincipal final : public BasePrincipal {
return NS_OK;
}
virtual nsresult PopulateJSONObject(Json::Value& aObject) override;
virtual nsresult WriteJSONInnerProperties(JSONWriter& aWriter) override;
// Serializable keys are the valid enum fields the serialization supports
enum SerializableKeys : uint8_t { eSpec = 0, eSuffix, eMax = eSuffix };
typedef mozilla::BasePrincipal::KeyValT<SerializableKeys> KeyVal;
static already_AddRefed<BasePrincipal> FromProperties(
nsTArray<NullPrincipal::KeyVal>& aFields);
static constexpr char SpecKey = '0';
static_assert(eSpec == 0);
static constexpr char SuffixKey = '1';
static_assert(eSuffix == 1);
class Deserializer : public BasePrincipal::Deserializer {
public:

View file

@ -0,0 +1,91 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_NullPrincipalJSONHandler_h
#define mozilla_NullPrincipalJSONHandler_h
#include <stddef.h> // size_t
#include <stdint.h> // uint32_t
#include "js/TypeDecls.h" // JS::Latin1Char
#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
#include "mozilla/Assertions.h" // MOZ_ASSERT_UNREACHABLE
#include "mozilla/RefPtr.h" // RefPtr
#include "nsCOMPtr.h" // nsCOMPtr
#include "nsDebug.h" // NS_WARNING
#include "nsIURI.h" // nsIURI
#include "NullPrincipal.h"
#include "OriginAttributes.h"
#include "SharedJSONHandler.h"
namespace mozilla {
// JSON parse handler for an inner object for NullPrincipal.
// Used by PrincipalJSONHandler or SubsumedPrincipalJSONHandler.
class NullPrincipalJSONHandler : public PrincipalJSONHandlerShared {
enum class State {
Init,
// After the inner object's '{'.
StartObject,
// After the property key for eSpec.
SpecKey,
// After the property key for eSuffix.
SuffixKey,
// After the property value for eSpec or eSuffix.
AfterPropertyValue,
// After the inner object's '}'.
EndObject,
Error,
};
public:
NullPrincipalJSONHandler() = default;
virtual ~NullPrincipalJSONHandler() = default;
virtual bool startObject() override;
using PrincipalJSONHandlerShared::propertyName;
virtual bool propertyName(const JS::Latin1Char* name, size_t length) override;
virtual bool endObject() override;
virtual bool startArray() override {
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
virtual bool endArray() override {
NS_WARNING("Unexpected array value");
mState = State::Error;
return false;
}
using PrincipalJSONHandlerShared::stringValue;
virtual bool stringValue(const JS::Latin1Char* str, size_t length) override;
bool HasAccepted() const { return mState == State::EndObject; }
protected:
virtual void SetErrorState() override { mState = State::Error; }
private:
State mState = State::Init;
nsCOMPtr<nsIURI> mUri;
OriginAttributes mAttrs;
};
} // namespace mozilla
#endif // mozilla_NullPrincipalJSONHandler_h

116
caps/PrincipalJSONHandler.h Normal file
View file

@ -0,0 +1,116 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_PrincipalJSONHandler_h
#define mozilla_PrincipalJSONHandler_h
#include <stddef.h> // size_t
#include <stdint.h> // uint32_t
#include "js/JSON.h" // JS::JSONParseHandler
#include "js/TypeDecls.h" // JS::Latin1Char
#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
#include "mozilla/RefPtr.h" // RefPtr
#include "mozilla/Variant.h" // Variant
#include "nsDebug.h" // NS_WARNING
#include "nsPrintfCString.h" // nsPrintfCString
#include "BasePrincipal.h"
#include "ContentPrincipalJSONHandler.h"
#include "ExpandedPrincipalJSONHandler.h"
#include "NullPrincipalJSONHandler.h"
#include "SharedJSONHandler.h"
namespace mozilla {
class PrincipalJSONHandlerTypes {
public:
enum class State {
Init,
// After top-level object's '{'.
StartObject,
// After the PrincipalKind property key for SystemPrincipal.
SystemPrincipal_Key,
// After the SystemPrincipal's inner object's '{'.
SystemPrincipal_StartObject,
// After the SystemPrincipal's inner object's '}'.
SystemPrincipal_EndObject,
// After the PrincipalKind property key for NullPrincipal, ContentPrincipal,
// or ExpandedPrincipal, and also the entire inner object.
// Delegates to mInnerHandler until the inner object's '}'.
NullPrincipal_Inner,
ContentPrincipal_Inner,
ExpandedPrincipal_Inner,
// After top-level object's '}'.
EndObject,
Error,
};
using InnerHandlerT =
Maybe<Variant<NullPrincipalJSONHandler, ContentPrincipalJSONHandler,
ExpandedPrincipalJSONHandler>>;
static constexpr bool CanContainExpandedPrincipal = true;
};
// JSON parse handler for the top-level object for principal.
//
// inner object
// |
// ---------------------------------------------------------
// | |
// {"1": {"0": "https://mozilla.com", "2": "^privateBrowsingId=1"}}
// |
// |
// |
// PrincipalKind
//
// For inner object except for the system principal case, this delegates
// to NullPrincipalJSONHandler, ContentPrincipalJSONHandler, or
// ExpandedPrincipalJSONHandler.
//
//// Null principal:
// {"0":{"0":"moz-nullprincipal:{56cac540-864d-47e7-8e25-1614eab5155e}"}}
//
// Content principal:
// {"1":{"0":"https://mozilla.com"}} -> {"0":"https://mozilla.com"}
//
// Expanded principal:
// {"2":{"0":"<base64principal1>,<base64principal2>"}}
//
// System principal:
// {"3":{}}
class PrincipalJSONHandler
: public ContainerPrincipalJSONHandler<PrincipalJSONHandlerTypes> {
using State = PrincipalJSONHandlerTypes::State;
using InnerHandlerT = PrincipalJSONHandlerTypes::InnerHandlerT;
public:
PrincipalJSONHandler() = default;
virtual ~PrincipalJSONHandler() = default;
virtual void error(const char* msg, uint32_t line, uint32_t column) override {
NS_WARNING(
nsPrintfCString("JSON Error: %s at line %u column %u of the JSON data",
msg, line, column)
.get());
}
already_AddRefed<BasePrincipal> Get() { return mPrincipal.forget(); }
protected:
virtual void SetErrorState() override { mState = State::Error; }
};
} // namespace mozilla
#endif // mozilla_PrincipalJSONHandler_h

114
caps/SharedJSONHandler.h Normal file
View file

@ -0,0 +1,114 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_SharedJSONHandler_h
#define mozilla_SharedJSONHandler_h
#include "js/JSON.h" // JS::JSONParseHandler
#include "mozilla/RefPtr.h" // RefPtr
#include "BasePrincipal.h" // BasePrincipal
namespace mozilla {
// Base class of all principal JSON parse handlers.
class PrincipalJSONHandlerShared : public JS::JSONParseHandler {
public:
// Common handlers for inner objects.
// NOTE: propertyName and stringValue are partial overloads.
// Subclasses should put the following:
// * `using PrincipalJSONHandlerShared::propertyName`
// * `using PrincipalJSONHandlerShared::stringValue`
virtual bool propertyName(const char16_t* name, size_t length) override {
NS_WARNING("Principal JSON shouldn't use non-ASCII");
SetErrorState();
return false;
};
virtual bool stringValue(const char16_t* str, size_t length) override {
NS_WARNING("Principal JSON shouldn't use non-ASCII");
SetErrorState();
return true;
}
virtual bool numberValue(double d) override {
NS_WARNING("Unexpected number value");
SetErrorState();
return false;
}
virtual bool booleanValue(bool v) override {
NS_WARNING("Unexpected boolean value");
SetErrorState();
return false;
}
virtual bool nullValue() override {
NS_WARNING("Unexpected null value");
SetErrorState();
return false;
}
virtual void error(const char* msg, uint32_t line, uint32_t column) override {
// Unused.
}
protected:
// Set to the error state for the above handlers.
virtual void SetErrorState() = 0;
public:
RefPtr<BasePrincipal> mPrincipal;
};
// Base class shared between PrincipalJSONHandler and
// SubsumedPrincipalJSONHandler.
// This implements the common code for them, absorbing the difference about
// whether it can contain ExpandedPrincipal or not.
template <typename HandlerTypesT>
class ContainerPrincipalJSONHandler : public PrincipalJSONHandlerShared {
using State = typename HandlerTypesT::State;
using InnerHandlerT = typename HandlerTypesT::InnerHandlerT;
static constexpr bool CanContainExpandedPrincipal =
HandlerTypesT::CanContainExpandedPrincipal;
public:
// Common handlers.
virtual bool startObject() override;
using PrincipalJSONHandlerShared::propertyName;
virtual bool propertyName(const JS::Latin1Char* name, size_t length) override;
virtual bool endObject() override;
virtual bool startArray() override;
virtual bool endArray() override;
using PrincipalJSONHandlerShared::stringValue;
virtual bool stringValue(const JS::Latin1Char* str, size_t length) override;
private:
bool ProcessInnerResult(bool aResult);
template <class Func>
bool CallOnInner(Func&& aFunc) {
return mInnerHandler->match([&](auto& aInner) {
bool result = aFunc(aInner);
return ProcessInnerResult(result);
});
}
protected:
State mState = State::Init;
InnerHandlerT mInnerHandler;
};
} // namespace mozilla
#endif // mozilla_SharedJSONHandler_h

View file

@ -0,0 +1,96 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_SubsumedPrincipalJSONHandler_h
#define mozilla_SubsumedPrincipalJSONHandler_h
#include <stddef.h> // size_t
#include <stdint.h> // uint32_t
#include "js/TypeDecls.h" // JS::Latin1Char
#include "mozilla/AlreadyAddRefed.h" // already_AddRefed
#include "mozilla/Variant.h" // Variant
#include "mozilla/RefPtr.h" // RefPtr
#include "nsDebug.h" // NS_WARNING
#include "BasePrincipal.h"
#include "ContentPrincipalJSONHandler.h"
#include "NullPrincipalJSONHandler.h"
#include "SharedJSONHandler.h"
namespace mozilla {
class SubsumedPrincipalJSONHandlerTypes {
public:
enum class State {
Init,
// After the subsumed principal object's '{'.
StartObject,
// After the PrincipalKind property key for SystemPrincipal.
SystemPrincipal_Key,
// After the SystemPrincipal's inner object's '{'.
SystemPrincipal_StartObject,
// After the SystemPrincipal's inner object's '}'.
SystemPrincipal_EndObject,
// After the PrincipalKind property key for NullPrincipal or
// ContentPrincipal, and also the entire inner object.
// Delegates to mInnerHandler until the inner object's '}'.
//
// Unlike PrincipalJSONHandler, subsumed principal doesn't contain
// ExpandedPrincipal.
NullPrincipal_Inner,
ContentPrincipal_Inner,
// After the subsumed principal object's '}'.
EndObject,
Error,
};
using InnerHandlerT =
Maybe<Variant<NullPrincipalJSONHandler, ContentPrincipalJSONHandler>>;
static constexpr bool CanContainExpandedPrincipal = false;
};
// JSON parse handler for subsumed principal object inside ExpandedPrincipal's
// new format.
//
// Subsumed principal object
// |
// ----------------------------------
// | |
// {"2": {"0": [{"1": {"0": https://mozilla.com"}}]}}
// | | |
// | ---------------------------
// | |
// | inner object
// PrincipalKind
//
// For inner object except for the system principal case, this delegates
// to NullPrincipalJSONHandler or ContentPrincipalJSONHandler.
class SubsumedPrincipalJSONHandler
: public ContainerPrincipalJSONHandler<SubsumedPrincipalJSONHandlerTypes> {
using State = SubsumedPrincipalJSONHandlerTypes::State;
using InnerHandlerT = SubsumedPrincipalJSONHandlerTypes::InnerHandlerT;
public:
SubsumedPrincipalJSONHandler() = default;
virtual ~SubsumedPrincipalJSONHandler() = default;
bool HasAccepted() const { return mState == State::EndObject; }
protected:
virtual void SetErrorState() override { mState = State::Error; }
};
} // namespace mozilla
#endif // mozilla_SubsumedPrincipalJSONHandler_h

View file

@ -24,10 +24,6 @@
class nsScriptSecurityManager;
namespace Json {
class Value;
}
namespace mozilla {
class SystemPrincipal final : public BasePrincipal, public nsISerializable {

View file

@ -17,7 +17,7 @@ using mozilla::SystemPrincipal;
// None of these tests work in debug due to assert guards
#ifndef MOZ_DEBUG
// calling toJson() twice with the same string arg
// calling toJSON() twice with the same string arg
// (ensure that we truncate correctly where needed)
TEST(PrincipalSerialization, ReusedJSONArgument)
{
@ -46,28 +46,6 @@ TEST(PrincipalSerialization, ReusedJSONArgument)
ASSERT_TRUE(JSON.EqualsLiteral("{\"1\":{\"0\":\"https://example.com/\"}}"));
}
// Assure that calling FromProperties() with an empty array list always returns
// a nullptr The exception here is SystemPrincipal which doesn't have fields but
// it also doesn't implement FromProperties These are overly cautious checks
// that we don't try to create a principal in reality FromProperties is only
// called with a populated array.
TEST(PrincipalSerialization, FromPropertiesEmpty)
{
nsTArray<ContentPrincipal::KeyVal> resContent;
nsCOMPtr<nsIPrincipal> contentPrincipal =
ContentPrincipal::FromProperties(resContent);
ASSERT_EQ(nullptr, contentPrincipal);
nsTArray<ExpandedPrincipal::KeyVal> resExpanded;
nsCOMPtr<nsIPrincipal> expandedPrincipal =
ExpandedPrincipal::FromProperties(resExpanded);
ASSERT_EQ(nullptr, expandedPrincipal);
nsTArray<NullPrincipal::KeyVal> resNull;
nsCOMPtr<nsIPrincipal> nullprincipal = NullPrincipal::FromProperties(resNull);
ASSERT_EQ(nullptr, nullprincipal);
}
// Double check that if we have two valid principals in a serialized JSON that
// nullptr is returned
TEST(PrincipalSerialization, TwoKeys)

View file

@ -115,8 +115,9 @@ add_task(async function testRemoteRuntime() {
function assertWarningMessage(doc, expectedMessage) {
const hasMessage = !!doc.querySelector(".qa-service-workers-warning");
ok(
hasMessage === expectedMessage,
Assert.strictEqual(
hasMessage,
expectedMessage,
expectedMessage
? "Warning message is displayed"
: "Warning message is not displayed"

View file

@ -45,8 +45,9 @@ add_task(async function () {
await waitForServiceWorkerRunning(SW_URL, document);
swPane = getDebugTargetPane("Service Workers", document);
ok(
swPane.querySelectorAll(".qa-debug-target-item").length === 1,
Assert.strictEqual(
swPane.querySelectorAll(".qa-debug-target-item").length,
1,
"Service worker list has one element"
);
ok(

View file

@ -47,8 +47,9 @@ function checkTelemetryEvents(expectedEvents, expectedSessionId) {
const sameExtrasEvents = sameMethodEvents.filter(e =>
_eventHasExpectedExtras(e, expectedEvent)
);
ok(
sameExtrasEvents.length === 1,
Assert.strictEqual(
sameExtrasEvents.length,
1,
"Found exactly one event matching the expected extras"
);
if (sameExtrasEvents.length === 0) {

View file

@ -25,8 +25,9 @@ add_task(async function () {
checkManifestMember(doc, "name", "Foo");
checkManifestMember(doc, "background_color", "#ff0000");
ok(
doc.querySelector(".js-manifest-issues") === null,
Assert.strictEqual(
doc.querySelector(".js-manifest-issues"),
null,
"No validation issues are being displayed"
);
@ -56,12 +57,16 @@ add_task(async function () {
checkManifestMember(doc, "background_color", "");
const issuesEl = doc.querySelector(".js-manifest-issues");
ok(issuesEl !== null, "Validation issues are displayed");
Assert.notStrictEqual(issuesEl, null, "Validation issues are displayed");
const warningEl = [...issuesEl.querySelectorAll(".js-manifest-issue")].find(
x => x.textContent.includes("background_color")
);
ok(warningEl !== null, "A warning about background_color is displayed");
Assert.notStrictEqual(
warningEl,
null,
"A warning about background_color is displayed"
);
// close the tab
info("Closing the tab.");
@ -83,12 +88,16 @@ add_task(async function () {
ok(true, "Manifest is being displayed");
const issuesEl = doc.querySelector(".js-manifest-issues");
ok(issuesEl !== null, "Validation issues are displayed");
Assert.notStrictEqual(issuesEl, null, "Validation issues are displayed");
const errorEl = [...issuesEl.querySelectorAll(".js-manifest-issue")].find(x =>
x.textContent.includes("JSON")
);
ok(errorEl !== null, "An error about JSON parsing is displayed");
Assert.notStrictEqual(
errorEl,
null,
"An error about JSON parsing is displayed"
);
// close the tab
info("Closing the tab.");
@ -111,9 +120,13 @@ add_task(async function () {
// assert manifest icon is being displayed
const iconEl = findMemberByLabel(doc, "128x128image/svg");
ok(iconEl !== null, "Icon label is being displayed with size and image type");
Assert.notStrictEqual(
iconEl,
null,
"Icon label is being displayed with size and image type"
);
const imgEl = iconEl.querySelector(".js-manifest-item-content img");
ok(imgEl !== null, "An image is displayed for the icon");
Assert.notStrictEqual(imgEl, null, "An image is displayed for the icon");
is(
imgEl.src,
URL_ROOT + "resources/manifest/icon.svg",

View file

@ -44,7 +44,7 @@ async function enableFirstBreakpoint(dbg) {
await addBreakpoint(dbg, "long.js", 32);
const bpMarkers = await waitForAllElements(dbg, "columnBreakpoints");
ok(bpMarkers.length === 2, "2 column breakpoints");
Assert.strictEqual(bpMarkers.length, 2, "2 column breakpoints");
assertClass(bpMarkers[0], "active");
assertClass(bpMarkers[1], "active", false);
}

View file

@ -123,7 +123,7 @@ async function enableFirstBreakpoint(dbg) {
await addBreakpoint(dbg, "long.js", 32);
const bpMarkers = await waitForAllElements(dbg, "columnBreakpoints");
ok(bpMarkers.length === 2, "2 column breakpoints");
Assert.strictEqual(bpMarkers.length, 2, "2 column breakpoints");
assertClass(bpMarkers[0], "active");
assertClass(bpMarkers[1], "active", false);
}

View file

@ -41,7 +41,7 @@ add_task(async function () {
info("Enable the breakpoint for the second debugger statement on line 12");
let bpElements = await waitForAllElements(dbg, "columnBreakpoints");
ok(bpElements.length === 2, "2 column breakpoints");
Assert.strictEqual(bpElements.length, 2, "2 column breakpoints");
assertClass(bpElements[0], "active");
assertClass(bpElements[1], "active", false);

View file

@ -37,7 +37,11 @@ add_task(async function () {
);
ok(process, "The remote debugger process was created");
ok(process.exitCode == null, "The remote debugger process is running");
Assert.equal(
process.exitCode,
null,
"The remote debugger process is running"
);
is(
typeof process.pid,
"number",

View file

@ -36,7 +36,11 @@ add_task(async function () {
await waitForPaused(dbg);
findElement(dbg, "frame", 1).focus();
await clickElement(dbg, "frame", 1);
ok(cm.getScrollInfo().top != 0, "frame scrolled down to correct location");
Assert.notEqual(
cm.getScrollInfo().top,
0,
"frame scrolled down to correct location"
);
info("Navigating while paused, goes to the correct location");
await selectSource(dbg, "long.js");

View file

@ -44,7 +44,7 @@ add_task(async function () {
);
const matchScrollTop = getScrollTop(dbg);
ok(pauseScrollTop != matchScrollTop, "did not jump to debug line");
Assert.notEqual(pauseScrollTop, matchScrollTop, "did not jump to debug line");
});
function getScrollTop(dbg) {

View file

@ -52,11 +52,12 @@ add_task(async function () {
await waitForSelectedSource(dbg, "bundle.js");
// Assert that reducer do have some data before remove the target.
ok(dbg.selectors.getSourceCount() > 0, "Some sources exists");
Assert.greater(dbg.selectors.getSourceCount(), 0, "Some sources exists");
is(dbg.selectors.getBreakpointCount(), 1, "There is one breakpoint");
is(dbg.selectors.getSourceTabs().length, 2, "Two tabs are opened");
ok(
dbg.selectors.getAllThreads().length > 1,
Assert.greater(
dbg.selectors.getAllThreads().length,
1,
"There is many targets/threads involved by the intergration test"
);
ok(
@ -69,8 +70,9 @@ add_task(async function () {
"The generated source is reporeted as selected"
);
ok(!!dbg.selectors.getFocusedSourceItem(), "Has a focused source tree item");
ok(
dbg.selectors.getExpandedState().size > 0,
Assert.greater(
dbg.selectors.getExpandedState().size,
0,
"Has some expanded source tree items"
);
@ -86,20 +88,24 @@ add_task(async function () {
"Some symbols for generated sources exists"
);
ok(!!Object.keys(state.ast.mutableInScopeLines).length, "Some scopes exists");
ok(
state.sourceActors.mutableSourceActors.size > 0,
Assert.greater(
state.sourceActors.mutableSourceActors.size,
0,
"Some source actor exists"
);
ok(
state.sourceActors.mutableBreakableLines.size > 0,
Assert.greater(
state.sourceActors.mutableBreakableLines.size,
0,
"Some breakable line exists"
);
ok(
state.sources.mutableBreakpointPositions.size > 0,
Assert.greater(
state.sources.mutableBreakpointPositions.size,
0,
"Some breakable positions exists"
);
ok(
state.sources.mutableOriginalBreakableLines.size > 0,
Assert.greater(
state.sources.mutableOriginalBreakableLines.size,
0,
"Some original breakable lines exists"
);

View file

@ -153,10 +153,14 @@ add_task(async function () {
let nodes = getAllLabels(dbg);
const originalNodesCount = nodes.length;
const targetNodeIndex = nodes.indexOf("target");
ok(targetNodeIndex > -1, "Found the target node");
Assert.greater(targetNodeIndex, -1, "Found the target node");
await toggleScopeNode(dbg, targetNodeIndex);
nodes = getAllLabels(dbg);
ok(nodes.length > originalNodesCount, "the target node was expanded");
Assert.greater(
nodes.length,
originalNodesCount,
"the target node was expanded"
);
ok(nodes.includes("classList"), "classList is displayed");
await resume(dbg);

View file

@ -63,5 +63,5 @@ function findNodeValue(dbg, text) {
async function checkObjectNode(dbg, text, value) {
await toggleNode(dbg, text);
ok(findNodeValue(dbg, "a") == value, "object value");
Assert.equal(findNodeValue(dbg, "a"), value, "object value");
}

View file

@ -23,7 +23,7 @@ add_task(async function () {
invokeInTab("webpack3Babel6EsmodulesCjs");
await waitForPaused(dbg);
ok(getOriginalScope(dbg) != null, "Scopes are now mapped");
Assert.notEqual(getOriginalScope(dbg), null, "Scopes are now mapped");
ok(!findFooterNotificationMessage(dbg), "No footer notification message");
await assertPreviewTextValue(dbg, 20, 20, {

View file

@ -23,8 +23,8 @@ add_task(async function test() {
// We step past the 'let x' at the start of the function because it is not
// a breakpoint position.
ok(findNodeValue(dbg, "x") == "undefined", "x undefined");
ok(findNodeValue(dbg, "y") == "(uninitialized)", "y uninitialized");
Assert.equal(findNodeValue(dbg, "x"), "undefined", "x undefined");
Assert.equal(findNodeValue(dbg, "y"), "(uninitialized)", "y uninitialized");
await stepOver(dbg);
@ -34,7 +34,7 @@ add_task(async function test() {
9
);
ok(findNodeValue(dbg, "y") == "3", "y initialized");
Assert.equal(findNodeValue(dbg, "y"), "3", "y initialized");
});
function findNodeValue(dbg, text) {

View file

@ -7,7 +7,9 @@
"use strict";
const SW_URL = EXAMPLE_URL + "service-worker.sjs";
// Use an URL with a specific port to check that Bug 1876533 is fixed
// We're using URL without port in other tests, like browser_dbg-windowless-service-workers-reload.js
const SW_URL = EXAMPLE_URL_WITH_PORT + "service-worker.sjs";
add_task(async function () {
info("Subtest #1");
@ -15,7 +17,9 @@ add_task(async function () {
await pushPref("devtools.debugger.threads-visible", true);
await pushPref("dom.serviceWorkers.testing.enabled", true);
const dbg = await initDebugger("doc-service-workers.html");
const dbg = await initDebuggerWithAbsoluteURL(
EXAMPLE_URL_WITH_PORT + "doc-service-workers.html"
);
invokeInTab("registerWorker");
await waitForSource(dbg, "service-worker.sjs");
@ -38,7 +42,7 @@ add_task(async function () {
info("Subtest #2");
const toolbox = await openNewTabAndToolbox(
`${EXAMPLE_URL}doc-service-workers.html`,
`${EXAMPLE_URL_WITH_PORT}doc-service-workers.html`,
"jsdebugger"
);
const dbg = createDebuggerContext(toolbox);
@ -73,7 +77,7 @@ add_task(async function () {
info("Subtest #3");
const toolbox = await openNewTabAndToolbox(
`${EXAMPLE_URL}doc-service-workers.html`,
`${EXAMPLE_URL_WITH_PORT}doc-service-workers.html`,
"jsdebugger"
);
const dbg = createDebuggerContext(toolbox);
@ -84,10 +88,14 @@ add_task(async function () {
const firstTab = gBrowser.selectedTab;
await addTab(`${EXAMPLE_URL}service-worker.sjs?setStatus=newServiceWorker`);
await addTab(
`${EXAMPLE_URL_WITH_PORT}service-worker.sjs?setStatus=newServiceWorker`
);
await removeTab(gBrowser.selectedTab);
const secondTab = await addTab(`${EXAMPLE_URL}doc-service-workers.html`);
const secondTab = await addTab(
`${EXAMPLE_URL_WITH_PORT}doc-service-workers.html`
);
await gBrowser.selectTabAtIndex(gBrowser.tabs.indexOf(firstTab));
await checkAdditionalThreadCount(dbg, 2);
@ -111,7 +119,7 @@ add_task(async function () {
await removeTab(secondTab);
// Reset the SJS in case we will be repeating the test.
await addTab(`${EXAMPLE_URL}service-worker.sjs?setStatus=`);
await addTab(`${EXAMPLE_URL_WITH_PORT}service-worker.sjs?setStatus=`);
await removeTab(gBrowser.selectedTab);
});
@ -124,7 +132,7 @@ add_task(async function () {
}
const toolbox = await openNewTabAndToolbox(
`${EXAMPLE_URL}doc-service-workers.html`,
`${EXAMPLE_URL_WITH_PORT}doc-service-workers.html`,
"jsdebugger"
);
const dbg = createDebuggerContext(toolbox);

View file

@ -15,7 +15,7 @@ add_task(async function () {
await waitForThreadCount(dbg, 2);
const workers = dbg.selectors.getThreads();
ok(workers.length == 2, "Got two workers");
Assert.equal(workers.length, 2, "Got two workers");
const thread1 = workers[0].actor;
const thread2 = workers[1].actor;
@ -43,7 +43,7 @@ add_task(async function () {
await addExpression(dbg, "count");
is(getWatchExpressionLabel(dbg, 1), "count");
const v = getWatchExpressionValue(dbg, 1);
ok(v == `${+v}`, "Value of count should be a number");
Assert.equal(v, `${+v}`, "Value of count should be a number");
info("StepOver in the first worker");
await stepOver(dbg);

View file

@ -11,5 +11,5 @@ add_task(async function () {
await waitForThreadCount(dbg, 2);
const workers = dbg.selectors.getThreads();
ok(workers.length == 2, "Got two workers");
Assert.equal(workers.length, 2, "Got two workers");
});

View file

@ -22,22 +22,22 @@ add_task(async function () {
assertPausedAtSourceAndLine(dbg, workerSource2.id, 11);
await toggleNode(dbg, "var_array");
ok(findNodeValue(dbg, "0") == '"mango"', "array elem0");
ok(findNodeValue(dbg, "1") == '"pamplemousse"', "array elem1");
ok(findNodeValue(dbg, "2") == '"pineapple"', "array elem2");
Assert.equal(findNodeValue(dbg, "0"), '"mango"', "array elem0");
Assert.equal(findNodeValue(dbg, "1"), '"pamplemousse"', "array elem1");
Assert.equal(findNodeValue(dbg, "2"), '"pineapple"', "array elem2");
await toggleNode(dbg, "var_array");
await toggleNode(dbg, "var_tarray");
ok(findNodeValue(dbg, "0") == "42", "tarray elem0");
ok(findNodeValue(dbg, "1") == "43", "tarray elem1");
ok(findNodeValue(dbg, "2") == "44", "tarray elem2");
Assert.equal(findNodeValue(dbg, "0"), "42", "tarray elem0");
Assert.equal(findNodeValue(dbg, "1"), "43", "tarray elem1");
Assert.equal(findNodeValue(dbg, "2"), "44", "tarray elem2");
await toggleNode(dbg, "var_tarray");
await toggleNode(dbg, "var_set");
await toggleNode(dbg, "<entries>");
ok(findNodeValue(dbg, "0") == '"papaya"', "set elem0");
ok(findNodeValue(dbg, "1") == '"banana"', "set elem1");
Assert.equal(findNodeValue(dbg, "0"), '"papaya"', "set elem0");
Assert.equal(findNodeValue(dbg, "1"), '"banana"', "set elem1");
await toggleNode(dbg, "var_set");
await toggleNode(dbg, "var_map");

View file

@ -19,6 +19,9 @@ const EXAMPLE_URL =
const EXAMPLE_REMOTE_URL =
"https://example.org/browser/devtools/client/debugger/test/mochitest/examples/";
const EXAMPLE_URL_WITH_PORT =
"http://mochi.test:8888/browser/devtools/client/debugger/test/mochitest/examples/";
// shared-head.js handles imports, constants, and utility functions
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",

View file

@ -60,7 +60,11 @@ add_task(async function () {
i++,
`index ${name} is correct and sorted into the correct position`
);
ok(typeof row.name === "number", "array index is displayed as a number");
Assert.strictEqual(
typeof row.name,
"number",
"array index is displayed as a number"
);
is(TEST_ARRAY[name], row.value, `value for array[${name}] is ${row.value}`);
}
});

View file

@ -22,7 +22,7 @@ add_task(async function () {
const inspector = toolbox.getPanel("inspector");
const openInInspectorIcon = node.querySelector(".open-inspector");
ok(node !== null, "Node was logged as expected");
Assert.notStrictEqual(node, null, "Node was logged as expected");
info(
"Clicking on the inspector icon and waiting for the " +

View file

@ -52,8 +52,9 @@ add_task(async function () {
null,
"With Fission or server side target switching, example.com target front is destroyed"
);
ok(
comTabTarget != newTarget,
Assert.notEqual(
comTabTarget,
newTarget,
"With Fission or server side target switching, a new target was created for example.org"
);

View file

@ -38,8 +38,9 @@ add_task(async function () {
);
iframe.style.height = "10000px"; // Set height to something unreasonably large.
ok(
iframe.clientHeight < panelHeight,
Assert.less(
iframe.clientHeight,
panelHeight,
`The iframe fits within the available space (${iframe.clientHeight} < ${panelHeight})`
);
@ -54,8 +55,9 @@ add_task(async function () {
const oldWidth = iframe.style.width;
iframe.style.width = "10000px"; // Set width to something unreasonably large.
ok(
iframe.clientWidth < panelWidth,
Assert.less(
iframe.clientWidth,
panelWidth,
`The iframe fits within the available space (${iframe.clientWidth} < ${panelWidth})`
);
iframe.style.width = oldWidth;

View file

@ -26,8 +26,9 @@ add_task(async function () {
const menuDockToBottom = toolbox.doc.getElementById(
"toolbox-meatball-menu-dock-bottom"
);
ok(
menuDockToBottom.getAttribute("aria-checked") === "true",
Assert.strictEqual(
menuDockToBottom.getAttribute("aria-checked"),
"true",
"menuDockToBottom has checked"
);

View file

@ -115,8 +115,9 @@ async function testJSDisabled() {
const output = doc.getElementById("output");
doc.querySelector("#logJSDisabled").click();
ok(
output.textContent !== "JavaScript Disabled",
Assert.notStrictEqual(
output.textContent,
"JavaScript Disabled",
'output is not "JavaScript Disabled"'
);
});
@ -131,8 +132,9 @@ async function testJSDisabledIframe() {
const iframeDoc = iframe.contentDocument;
const output = iframeDoc.getElementById("output");
iframeDoc.querySelector("#logJSDisabled").click();
ok(
output.textContent !== "JavaScript Disabled",
Assert.notStrictEqual(
output.textContent,
"JavaScript Disabled",
'output is not "JavaScript Disabled" in iframe'
);
});

View file

@ -95,7 +95,7 @@ async function checkResults() {
const expected = DATA[i];
// ignore timestamp
ok(timestamp > 0, "timestamp is greater than 0");
Assert.greater(timestamp, 0, "timestamp is greater than 0");
is(category, expected.category, "category is correct");
is(method, expected.method, "method is correct");
is(object, expected.object, "object is correct");

View file

@ -136,7 +136,7 @@ async function checkResults() {
const expected = DATA[i];
// ignore timestamp
ok(timestamp > 0, "timestamp is greater than 0");
Assert.greater(timestamp, 0, "timestamp is greater than 0");
is(category, expected.category, "category is correct");
is(method, expected.method, "method is correct");
is(object, expected.object, "object is correct");

View file

@ -113,7 +113,7 @@ async function checkResults() {
const expected = DATA[i];
// ignore timestamp
ok(timestamp > 0, "timestamp is greater than 0");
Assert.greater(timestamp, 0, "timestamp is greater than 0");
is(category, expected.category, "category is correct");
is(method, expected.method, "method is correct");
is(object, expected.object, "object is correct");

View file

@ -44,8 +44,9 @@ add_task(async function () {
const initialTabCount = toolbox.doc.querySelectorAll(".devtools-tab").length;
// Make all toolbox button to be visible.
setToolboxButtonsVisibility(checkButtons, true);
ok(
toolbox.doc.querySelectorAll(".devtools-tab").length < initialTabCount,
Assert.less(
toolbox.doc.querySelectorAll(".devtools-tab").length,
initialTabCount,
"Count of shown devtools tab should decreased"
);

View file

@ -78,8 +78,16 @@ add_task(async function () {
// vertical: IntID::ContextMenuOffsetVertical of macOS uses -6.
const xDelta = Math.abs(menuBounds.left - buttonBounds.left);
const yDelta = Math.abs(menuBounds.top - buttonBounds.bottom);
ok(xDelta < 2, "xDelta is lower than 2: " + xDelta + ". #" + menu.id);
ok(yDelta < 6, "yDelta is lower than 6: " + yDelta + ". #" + menu.id);
Assert.less(
xDelta,
2,
"xDelta is lower than 2: " + xDelta + ". #" + menu.id
);
Assert.less(
yDelta,
6,
"yDelta is lower than 6: " + yDelta + ". #" + menu.id
);
}
break;
@ -90,8 +98,9 @@ add_task(async function () {
const buttonCenter = buttonBounds.left + buttonBounds.width / 2;
const arrowCenter = arrowBounds.left + arrowBounds.width / 2;
const delta = Math.abs(arrowCenter - buttonCenter);
ok(
Math.round(delta) <= 1,
Assert.lessOrEqual(
Math.round(delta),
1,
"Center of arrow is within 1px of button center" +
` (delta: ${delta})`
);

View file

@ -41,9 +41,9 @@ add_task(async function () {
info("Checking timeline tick item elements after enlarge sidebar width");
await setSidebarWidth("100%", inspector);
await assertTickLabels(timeScale, listContainerEl);
ok(
timelineTickItemLength <
listContainerEl.querySelectorAll(".tick-label").length,
Assert.less(
timelineTickItemLength,
listContainerEl.querySelectorAll(".tick-label").length,
"The timeline tick item elements should increase"
);
});

View file

@ -25,9 +25,9 @@ add_task(async function () {
info("Check existed animations have different currentTime");
const animations = animationInspector.state.animations;
ok(
animations[0].state.currentTime + WAIT_TIME >
animations[1].state.currentTime,
Assert.greater(
animations[0].state.currentTime + WAIT_TIME,
animations[1].state.currentTime,
`The currentTime of added animation shold be ${WAIT_TIME}ms less than ` +
"at least that currentTime of first animation"
);

View file

@ -32,8 +32,9 @@ add_task(async function () {
const barBounds = barEl.getBoundingClientRect();
const barX = barBounds.x + barBounds.width / 2 - controllerBounds.x;
const expectedBarX = controllerBounds.width * 0.5;
ok(
Math.abs(barX - expectedBarX) < 1,
Assert.less(
Math.abs(barX - expectedBarX),
1,
"Progress bar should indicate at progress of 0.5"
);
});

View file

@ -60,8 +60,9 @@ function assertPlayState(animations, expectedState) {
function assertCurrentTimeLessThanDuration(animations) {
animations.forEach((animation, index) => {
ok(
animation.state.currentTime < animation.state.duration,
Assert.less(
animation.state.currentTime,
animation.state.duration,
`The current time of animation[${index}] should be less than its duration`
);
});
@ -70,8 +71,9 @@ function assertCurrentTimeLessThanDuration(animations) {
function assertScrubberPosition(panel) {
const scrubberEl = panel.querySelector(".current-time-scrubber");
const marginInlineStart = parseFloat(scrubberEl.style.marginInlineStart);
ok(
marginInlineStart >= 0,
Assert.greaterOrEqual(
marginInlineStart,
0,
"The translateX of scrubber position should be zero or more"
);
}

View file

@ -23,15 +23,16 @@ add_task(async function () {
animationInspector,
animationInspector.state.timeScale.getDuration() * 0.5
);
ok(
initialCurrentTime >
animationInspector.state.animations[0].state.currentTime,
Assert.greater(
initialCurrentTime,
animationInspector.state.animations[0].state.currentTime,
"currentTime should be decreased"
);
info("Check whether the progress bar was moved to left");
ok(
initialProgressBarX > getProgressBarX(panel),
Assert.greater(
initialProgressBarX,
getProgressBarX(panel),
"Progress bar should be moved to left"
);
});

View file

@ -1023,8 +1023,9 @@ function checkAdjustingTheTime(animation1, animation2) {
animation2.currentTime / animation2.playbackRate -
animation1.currentTime / animation1.playbackRate;
const createdTimeDiff = animation1.createdTime - animation2.createdTime;
ok(
Math.abs(adjustedCurrentTimeDiff - createdTimeDiff) < 0.1,
Assert.less(
Math.abs(adjustedCurrentTimeDiff - createdTimeDiff),
0.1,
"Adjusted time is correct"
);
}

View file

@ -42,5 +42,5 @@ add_task(async function () {
const sym = Symbol();
const onTimeout = wait(500).then(() => sym);
const raceResult = await Promise.any([onResetChanges, onTimeout]);
ok(raceResult === sym, "RESET_CHANGES has not been dispatched");
Assert.strictEqual(raceResult, sym, "RESET_CHANGES has not been dispatched");
});

View file

@ -27,7 +27,7 @@ add_task(async function () {
// Check that the outline is wider than it is tall in the configuration.
let bounds = flexOutline.getBoxQuads()[0].getBounds();
ok(bounds.width > bounds.height, "The outline looks like a row");
Assert.greater(bounds.width, bounds.height, "The outline looks like a row");
// Select a flex item in the column flexbox layout.
onFlexItemOutlineRendered = waitForDOM(
@ -45,5 +45,9 @@ add_task(async function () {
// Check that the outline is taller than it is wide in the configuration.
bounds = flexOutline.getBoxQuads()[0].getBounds();
ok(bounds.height > bounds.width, "The outline looks like a column");
Assert.greater(
bounds.height,
bounds.width,
"The outline looks like a column"
);
});

View file

@ -14,9 +14,10 @@ add_task(async function () {
await selectNode(".input-field", inspector);
const fontEls = getUsedFontsEls(viewDoc);
ok(fontEls.length == 1, `Used fonts found for styled input element`);
ok(
fontEls[0].textContent == "Ostrich Sans Medium",
Assert.equal(fontEls.length, 1, `Used fonts found for styled input element`);
Assert.equal(
fontEls[0].textContent,
"Ostrich Sans Medium",
`Proper font found: 'Ostrich Sans Medium' for styled input.`
);
});

View file

@ -29,7 +29,7 @@ add_task(async function () {
});
const bottomScrollPos = await waitForScrollStop(markup.doc);
ok(bottomScrollPos > 0, "The view was scrolled down");
Assert.greater(bottomScrollPos, 0, "The view was scrolled down");
info("Simulate a mousemove at the top and expect more scrolling");
@ -40,7 +40,7 @@ add_task(async function () {
});
const topScrollPos = await waitForScrollStop(markup.doc);
ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
Assert.less(topScrollPos, bottomScrollPos, "The view was scrolled up");
is(topScrollPos, 0, "The view was scrolled up to the top");
info("Simulate a mouseup to stop dragging");

View file

@ -28,7 +28,7 @@ add_task(async function () {
});
const bottomScrollPos = await waitForScrollStop(markup.doc);
ok(bottomScrollPos > 0, "The view was scrolled down");
Assert.greater(bottomScrollPos, 0, "The view was scrolled down");
info("Simulate a mousemove at the top and expect more scrolling");
markup._onMouseMove({
@ -38,7 +38,7 @@ add_task(async function () {
});
const topScrollPos = await waitForScrollStop(markup.doc);
ok(topScrollPos < bottomScrollPos, "The view was scrolled up");
Assert.less(topScrollPos, bottomScrollPos, "The view was scrolled up");
is(topScrollPos, 0, "The view was scrolled up to the top");
info("Simulate a mouseup to stop dragging");

View file

@ -197,8 +197,9 @@ const TEST_DATA = [
const container = await getContainerForSelector("#node1", inspector);
ok(!container.inlineTextChild, "Does not have single text child.");
ok(container.canExpand, "Can expand container with child nodes.");
ok(
container.editor.elt.querySelector(".text") == null,
Assert.equal(
container.editor.elt.querySelector(".text"),
null,
"Single text child editor removed."
);
},
@ -229,8 +230,9 @@ const TEST_DATA = [
const container = await getContainerForSelector("#node1", inspector);
ok(!container.inlineTextChild, "Does not have single text child.");
ok(!container.canExpand, "Can't expand empty container.");
ok(
container.editor.elt.querySelector(".text") == null,
Assert.equal(
container.editor.elt.querySelector(".text"),
null,
"Single text child editor removed."
);
},

View file

@ -19,16 +19,18 @@ add_task(async function () {
inspector
);
info("Checking the clipPath element");
ok(
clipPathContainer.editor.tag.textContent === "clipPath",
Assert.strictEqual(
clipPathContainer.editor.tag.textContent,
"clipPath",
"clipPath node name is not lowercased"
);
const divContainer = await getContainerForSelector("div", inspector);
info("Checking the div element");
ok(
divContainer.editor.tag.textContent === "div",
Assert.strictEqual(
divContainer.editor.tag.textContent,
"div",
"div node name is lowercased"
);
});

View file

@ -35,8 +35,9 @@ add_task(async function () {
inspector
);
info("Checking the clipPath element");
ok(
clipPathContainer.editor.tag.textContent === "svg:clipPath",
Assert.strictEqual(
clipPathContainer.editor.tag.textContent,
"svg:clipPath",
"svg:clipPath node is correctly displayed"
);
@ -45,8 +46,9 @@ add_task(async function () {
inspector
);
info("Checking the circle element");
ok(
circlePathContainer.editor.tag.textContent === "svg:circle",
Assert.strictEqual(
circlePathContainer.editor.tag.textContent,
"svg:circle",
"svg:circle node is correctly displayed"
);
});

View file

@ -64,8 +64,9 @@ add_task(async function () {
EventUtils.synthesizeKey("KEY_Tab", {}, win);
await tagFocused;
ok(
inspector.markup.doc.activeElement === tagSpan,
Assert.strictEqual(
inspector.markup.doc.activeElement,
tagSpan,
"Focus has gone back to first element"
);
});

View file

@ -38,8 +38,9 @@ add_task(async function () {
const iframe = await getNodeFront("iframe", inspector);
const { nodes } = await inspector.walker.children(iframe);
const documentFront = nodes[0];
ok(
documentFront.displayName === "#document",
Assert.strictEqual(
documentFront.displayName,
"#document",
"First child of IFRAME is #document"
);

View file

@ -16,8 +16,9 @@ add_task(async function () {
!editor.elt.classList.contains("void-element"),
"h1 element does not have void-element class"
);
ok(
!editor.elt.querySelector(".close").style.display !== "none",
Assert.notStrictEqual(
!editor.elt.querySelector(".close").style.display,
"none",
"h1 element tag is not hidden"
);
@ -29,7 +30,11 @@ add_task(async function () {
);
let closeElement = container.editor.elt.querySelector(".close");
let computedStyle = win.getComputedStyle(closeElement);
ok(computedStyle.display === "none", "img closing tag is hidden");
Assert.strictEqual(
computedStyle.display,
"none",
"img closing tag is hidden"
);
info("check void element with pseudo element");
const hrNodeFront = await getNodeFront("hr.before", inspector);
@ -40,12 +45,16 @@ add_task(async function () {
);
closeElement = container.editor.elt.querySelector(".close");
computedStyle = win.getComputedStyle(closeElement);
ok(computedStyle.display === "none", "hr closing tag is hidden");
Assert.strictEqual(computedStyle.display, "none", "hr closing tag is hidden");
info("check expanded void element closing tag is not hidden");
await inspector.markup.expandNode(hrNodeFront);
await waitForMultipleChildrenUpdates(inspector);
ok(container.expanded, "hr container is expanded");
computedStyle = win.getComputedStyle(closeElement);
ok(computedStyle.display === "none", "hr closing tag is not hidden anymore");
Assert.strictEqual(
computedStyle.display,
"none",
"hr closing tag is not hidden anymore"
);
});

View file

@ -16,8 +16,9 @@ add_task(async function () {
!editor.elt.classList.contains("void-element"),
"h1 element does not have void-element class"
);
ok(
!editor.elt.querySelector(".close").style.display !== "none",
Assert.notStrictEqual(
!editor.elt.querySelector(".close").style.display,
"none",
"h1 element tag is not hidden"
);
@ -29,5 +30,9 @@ add_task(async function () {
);
const closeElement = container.editor.elt.querySelector(".close");
const computedStyle = win.getComputedStyle(closeElement);
ok(computedStyle.display !== "none", "br closing tag is not hidden");
Assert.notStrictEqual(
computedStyle.display,
"none",
"br closing tag is not hidden"
);
});

View file

@ -175,8 +175,9 @@ async function checkEventsForNode(test, inspector) {
const shouldBeDisabled =
expected[i].attributes?.includes("React") ||
expected[i].attributes?.includes("jQuery");
ok(
disabled === shouldBeDisabled,
Assert.strictEqual(
disabled,
shouldBeDisabled,
`The checkbox is ${shouldBeDisabled ? "disabled" : "enabled"}\n`
);

View file

@ -60,7 +60,7 @@ add_task(async function () {
info("Check that UP/DOWN navigates in the input, even when next to a number");
EventUtils.synthesizeKey("VK_DOWN", {}, view.styleWindow);
ok(editor.input.selectionStart !== pos, "Input caret moved");
Assert.notStrictEqual(editor.input.selectionStart, pos, "Input caret moved");
is(editor.input.value, LONG_CSS_VALUE, "Input value was not decremented.");
info("Move the caret to the end of the gradient definition.");

View file

@ -184,9 +184,9 @@ function assertContainerQueryData(view, expectedRules) {
expectedRule.ancestorRulesData.join("\n"),
`Expected ancestor rules data displayed for ${selector}`
);
ok(
ancestorDataEl.querySelector(".container-query .open-inspector") !==
null,
Assert.notStrictEqual(
ancestorDataEl.querySelector(".container-query .open-inspector"),
null,
"An icon is displayed to select the container in the markup view"
);
}

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