Update On Sat Jan 27 19:40:48 CET 2024
This commit is contained in:
parent
c893f323ba
commit
06956fe708
651 changed files with 10059 additions and 4887 deletions
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
398
browser/components/firefoxview/OpenTabs.sys.mjs
Normal file
398
browser/components/firefoxview/OpenTabs.sys.mjs
Normal 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 };
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>`
|
||||
)}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>`
|
||||
)}
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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");
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -60,6 +60,7 @@ const URLs = [
|
|||
"https://example.net/",
|
||||
"https://example.org/",
|
||||
"about:robots",
|
||||
"https://www.mozilla.org/",
|
||||
];
|
||||
|
||||
const syncedTabsData1 = [
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
94
caps/ContentPrincipalJSONHandler.h
Normal file
94
caps/ContentPrincipalJSONHandler.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
130
caps/ExpandedPrincipalJSONHandler.h
Normal file
130
caps/ExpandedPrincipalJSONHandler.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
91
caps/NullPrincipalJSONHandler.h
Normal file
91
caps/NullPrincipalJSONHandler.h
Normal 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
116
caps/PrincipalJSONHandler.h
Normal 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
114
caps/SharedJSONHandler.h
Normal 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
|
96
caps/SubsumedPrincipalJSONHandler.h
Normal file
96
caps/SubsumedPrincipalJSONHandler.h
Normal 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
|
|
@ -24,10 +24,6 @@
|
|||
|
||||
class nsScriptSecurityManager;
|
||||
|
||||
namespace Json {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
class SystemPrincipal final : public BasePrincipal, public nsISerializable {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 " +
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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})`
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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.`
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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."
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue