Bug 1763598 - Fix docshell_helpers.js for session history in the parent. r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D143159
This commit is contained in:
Peter Van der Beken 2022-04-15 20:44:38 +00:00
parent 14be294b2d
commit 926498605f
8 changed files with 305 additions and 81 deletions

View File

@ -0,0 +1,78 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var EXPORTED_SYMBOLS = ["DocShellHelpersParent", "DocShellHelpersChild"];
class DocShellHelpersParent extends JSWindowActorParent {
static eventListener;
// These static variables should be set when registering the actor
// (currently doPageNavigation in docshell_helpers.js).
static eventsToListenFor;
static observers;
constructor() {
super();
}
receiveMessage({ name, data }) {
if (name == "docshell_helpers:event") {
let { event, originalTargetIsHTMLDocument } = data;
if (this.constructor.eventsToListenFor.includes(event.type)) {
this.constructor.eventListener(event, originalTargetIsHTMLDocument);
}
} else if (name == "docshell_helpers:observe") {
let { topic } = data;
this.constructor.observers.get(topic).call();
}
}
}
class DocShellHelpersChild extends JSWindowActorChild {
constructor() {
super();
}
receiveMessage({ name, data }) {
if (name == "docshell_helpers:preventBFCache") {
// Add an RTCPeerConnection to prevent the page from being bfcached.
let win = this.contentWindow;
win.blockBFCache = new win.RTCPeerConnection();
}
}
handleEvent(event) {
if (
Document.isInstance(event.originalTarget) &&
event.originalTarget.isInitialDocument
) {
dump(`TEST: ignoring a ${event.type} event for an initial about:blank\n`);
return;
}
this.sendAsyncMessage("docshell_helpers:event", {
event: {
type: event.type,
persisted: event.persisted,
originalTarget: {
title: event.originalTarget.title,
location: event.originalTarget.location.href,
visibilityState: event.originalTarget.visibilityState,
hidden: event.originalTarget.hidden,
},
},
originalTargetIsHTMLDocument: HTMLDocument.isInstance(
event.originalTarget
),
});
}
observe(subject, topic) {
if (Window.isInstance(subject) && subject.document.isInitialDocument) {
dump(`TEST: ignoring a topic notification for an initial about:blank\n`);
return;
}
this.sendAsyncMessage("docshell_helpers:observe", { topic });
}
}

View File

@ -8,6 +8,7 @@
onload="startup();"
title="bug 311007 test">
<script src="chrome://mochikit/content/chrome-harness.js" />
<script type="application/javascript" src="docshell_helpers.js"></script>
<script type="application/javascript"><![CDATA[
// `content` is the id of the browser element used for the test.

View File

@ -8,7 +8,7 @@
onload="nextTestAsync();"
title="bug 582176 test">
<script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
<script type="application/javascript" src="docshell_helpers.js" />
<script type="application/javascript"><![CDATA[
// Define the generator-iterator for the tests.

View File

@ -8,10 +8,8 @@
onload="setTimeout(nextTest, 0);"
title="bug 662200 test">
<script type="application/javascript"
src="docshell_helpers.js">
</script>
<script type="application/javascript" src= "chrome://mochikit/content/chrome-harness.js" />
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
<script type="application/javascript" src="docshell_helpers.js" />
<script type="application/javascript"><![CDATA[
Services.prefs.setBoolPref("browser.navigation.requireUserInteraction", false);

View File

@ -43,6 +43,7 @@ support-files =
bug909218.js
bug92598_window.xhtml
docshell_helpers.js
DocShellHelpers.jsm
file_viewsource_forbidden_in_iframe.html
generic.html
mozFrameType_window.xhtml

View File

@ -23,6 +23,9 @@ const { BrowserTestUtils } = ChromeUtils.import(
);
var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const ACTOR_MODULE_URI =
"chrome://mochitests/content/chrome/docshell/test/chrome/DocShellHelpers.jsm";
const { DocShellHelpersParent } = ChromeUtils.import(ACTOR_MODULE_URI);
// Some functions assume chrome-harness.js has been loaded.
/* import-globals-from ../../../testing/mochitest/chrome-harness.js */
@ -32,8 +35,9 @@ var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const NAV_NONE = 0;
const NAV_BACK = 1;
const NAV_FORWARD = 2;
const NAV_URI = 3;
const NAV_RELOAD = 4;
const NAV_GOTOINDEX = 3;
const NAV_URI = 4;
const NAV_RELOAD = 5;
var gExpectedEvents; // an array of events which are expected to
// be triggered by this navigation
@ -61,6 +65,9 @@ var gExtractedPath = null; // used to cache file path for extracting files from
*
* forward: if true, the browser will execute goForward()
*
* gotoIndex: if a number, the browser will execute gotoIndex() with
* the number as index
*
* reload: if true, the browser will execute reload()
*
* eventsToListenFor: an array containing one or more of the following event
@ -91,13 +98,16 @@ var gExtractedPath = null; // used to cache file path for extracting files from
* This property is ignored if eventsToListenFor is
* undefined or [].
*
* preventBFCache: if true, an unload handler will be added to the loaded
* preventBFCache: if true, an RTCPeerConnection will be added to the loaded
* page to prevent it from being bfcached. This property
* has no effect when eventsToListenFor is [].
*
* onNavComplete: a callback which is notified after all expected events
* have occurred, or after a timeout has elapsed. This
* callback is not notified if eventsToListenFor is [].
* onGlobalCreation: a callback which is notified when a DOMWindow is created
* (implemented by observing
* "content-document-global-created")
*
* There must be an expectedEvent object for each event of the types in
* eventsToListenFor which is triggered by this navigation. For example, if
@ -110,6 +120,7 @@ function doPageNavigation(params) {
// Parse the parameters.
let back = params.back ? params.back : false;
let forward = params.forward ? params.forward : false;
let gotoIndex = params.gotoIndex ? params.gotoIndex : false;
let reload = params.reload ? params.reload : false;
let uri = params.uri ? params.uri : false;
let eventsToListenFor =
@ -134,20 +145,15 @@ function doPageNavigation(params) {
typeof params.waitForEventsOnly == "boolean" && params.waitForEventsOnly;
// Do some sanity checking on arguments.
if (back && forward) {
throw new Error("Can't specify both back and forward");
}
if (back && uri) {
throw new Error("Can't specify both back and a uri");
}
if (forward && uri) {
throw new Error("Can't specify both forward and a uri");
}
if (reload && (forward || back || uri)) {
throw new Error("Can't specify reload and another navigation type");
}
if (!back && !forward && !uri && !reload && !waitOnly) {
throw new Error("Must specify back or foward or reload or uri");
let navigation = ["back", "forward", "gotoIndex", "reload", "uri"].filter(k =>
params.hasOwnProperty(k)
);
if (navigation.length > 1) {
throw new Error(`Can't specify both ${navigation[0]} and ${navigation[1]}`);
} else if (navigation.length == 0 && !waitOnly) {
throw new Error(
"Must specify back or forward or gotoIndex or reload or uri"
);
}
if (params.onNavComplete && eventsToListenFor.length == 0) {
throw new Error("Can't use onNavComplete when eventsToListenFor == []");
@ -163,7 +169,7 @@ function doPageNavigation(params) {
"Must specify onNavComplete when specifying waitForEventsOnly"
);
}
if (waitOnly && (back || forward || reload || uri)) {
if (waitOnly && navigation.length > 0) {
throw new Error(
"Can't specify a navigation type when using waitForEventsOnly"
);
@ -199,15 +205,83 @@ function doPageNavigation(params) {
// events.
gFinalEvent = eventsToListenFor.length == 0;
// Add observers as needed.
let observers = new Map();
if (params.hasOwnProperty("onGlobalCreation")) {
observers.set("content-document-global-created", params.onGlobalCreation);
}
// Add an event listener for each type of event in the .eventsToListenFor
// property of the input parameters.
for (let eventType of eventsToListenFor) {
dump("TEST: registering a listener for " + eventType + " events\n");
TestWindow.getBrowser().addEventListener(
eventType,
pageEventListener,
true
);
// property of the input parameters, and add an observer for all the topics
// in the observers map.
let cleanup;
let useActor = TestWindow.getBrowser().isRemoteBrowser;
if (useActor) {
ChromeUtils.registerWindowActor("DocShellHelpers", {
parent: {
moduleURI: ACTOR_MODULE_URI,
},
child: {
moduleURI: ACTOR_MODULE_URI,
events: {
pageshow: { createActor: true, capture: true },
pagehide: { createActor: true, capture: true },
load: { createActor: true, capture: true },
unload: { createActor: true, capture: true },
visibilitychange: { createActor: true, capture: true },
},
observers: observers.keys(),
},
allFrames: true,
});
DocShellHelpersParent.eventsToListenFor = eventsToListenFor;
DocShellHelpersParent.observers = observers;
cleanup = () => {
DocShellHelpersParent.eventsToListenFor = null;
DocShellHelpersParent.observers = null;
ChromeUtils.unregisterWindowActor("DocShellHelpers");
};
} else {
for (let eventType of eventsToListenFor) {
dump("TEST: registering a listener for " + eventType + " events\n");
TestWindow.getBrowser().addEventListener(
eventType,
pageEventListener,
true
);
}
if (observers.size > 0) {
let observer = (_, topic) => {
observers.get(topic).call();
};
for (let topic of observers.keys()) {
Services.obs.addObserver(observer, topic);
}
// We only need to do cleanup for the observer, the event listeners will
// go away with the window.
cleanup = () => {
for (let topic of observers.keys()) {
Services.obs.removeObserver(observer, topic);
}
};
}
}
if (cleanup) {
// Register a cleanup function on domwindowclosed, to avoid contaminating
// other tests if we bail out early because of an error.
Services.ww.registerNotification(function windowClosed(
subject,
topic,
data
) {
if (topic == "domwindowclosed" && subject == window) {
Services.ww.unregisterNotification(windowClosed);
cleanup();
}
});
}
// Perform the specified navigation.
@ -217,6 +291,9 @@ function doPageNavigation(params) {
} else if (forward) {
gNavType = NAV_FORWARD;
TestWindow.getBrowser().goForward();
} else if (typeof gotoIndex == "number") {
gNavType = NAV_GOTOINDEX;
TestWindow.getBrowser().gotoIndex(gotoIndex);
} else if (uri) {
gNavType = NAV_URI;
BrowserTestUtils.loadURI(TestWindow.getBrowser(), uri);
@ -240,10 +317,14 @@ function doPageNavigation(params) {
doPageNavigation_complete(
eventsToListenFor,
params.onNavComplete,
preventBFCache
preventBFCache,
useActor,
cleanup
);
}
);
} else if (cleanup) {
cleanup();
}
}
@ -256,33 +337,42 @@ function doPageNavigation(params) {
function doPageNavigation_complete(
eventsToListenFor,
onNavComplete,
preventBFCache
preventBFCache,
useActor,
cleanup
) {
// Unregister our event listeners.
dump("TEST: removing event listeners\n");
for (let eventType of eventsToListenFor) {
TestWindow.getBrowser().removeEventListener(
eventType,
pageEventListener,
true
);
if (useActor) {
if (preventBFCache) {
let actor = TestWindow.getBrowser().browsingContext.currentWindowGlobal.getActor(
"DocShellHelpers"
);
actor.sendAsyncMessage("docshell_helpers:preventBFCache");
}
} else {
// Unregister our event listeners.
dump("TEST: removing event listeners\n");
for (let eventType of eventsToListenFor) {
TestWindow.getBrowser().removeEventListener(
eventType,
pageEventListener,
true
);
}
// If the .preventBFCache property was set, add an RTCPeerConnection to
// prevent the page from being bfcached.
if (preventBFCache) {
let win = TestWindow.getWindow();
win.blockBFCache = new win.RTCPeerConnection();
}
}
if (cleanup) {
cleanup();
}
// If the .preventBFCache property was set, add an empty unload handler to
// prevent the page from being bfcached.
let uri = TestWindow.getBrowser().currentURI.spec;
if (preventBFCache) {
TestWindow.getWindow().addEventListener(
"unload",
function() {
dump(
"TEST: Called dummy unload function to prevent page from " +
"being bfcached.\n"
);
},
true
);
// Save the current uri in an array of uri's which shouldn't be
// stored in the bfcache, for later verification.
if (!(uri in gUrisNotInBFCache)) {
@ -302,6 +392,18 @@ function doPageNavigation_complete(
onNavComplete.call();
}
function promisePageNavigation(params) {
if (params.hasOwnProperty("onNavComplete")) {
throw new Error(
"Can't use a onNavComplete completion callback with promisePageNavigation."
);
}
return new Promise(resolve => {
params.onNavComplete = resolve;
doPageNavigation(params);
});
}
/**
* Allows a test to wait for page navigation events, and notify a
* callback when they've all been received. This works exactly the
@ -312,10 +414,26 @@ function waitForPageEvents(params) {
doPageNavigation(params);
}
function promisePageEvents(params) {
if (params.hasOwnProperty("onNavComplete")) {
throw new Error(
"Can't use a onNavComplete completion callback with promisePageEvents."
);
}
return new Promise(resolve => {
params.waitForEventsOnly = true;
params.onNavComplete = resolve;
doPageNavigation(params);
});
}
/**
* The event listener which listens for expectedEvents.
*/
function pageEventListener(event) {
function pageEventListener(
event,
originalTargetIsHTMLDocument = event.originalTarget instanceof HTMLDocument
) {
try {
dump(
"TEST: eventListener received a " +
@ -336,7 +454,9 @@ function pageEventListener(event) {
// for .persisted.
if (
event.type == "pageshow" &&
(gNavType == NAV_BACK || gNavType == NAV_FORWARD)
(gNavType == NAV_BACK ||
gNavType == NAV_FORWARD ||
gNavType == NAV_GOTOINDEX)
) {
let uri = TestWindow.getBrowser().currentURI.spec;
if (uri in gUrisNotInBFCache) {
@ -389,7 +509,7 @@ function pageEventListener(event) {
if (typeof expected.title != "undefined") {
ok(
event.originalTarget instanceof HTMLDocument,
originalTargetIsHTMLDocument,
"originalTarget for last " + event.type + " event not an HTMLDocument"
);
is(
@ -444,21 +564,24 @@ function pageEventListener(event) {
}
}
DocShellHelpersParent.eventListener = pageEventListener;
/**
* End a test.
*/
function finish() {
// Work around bug 467960.
let history;
let historyPurged;
if (SpecialPowers.Services.appinfo.sessionHistoryInParent) {
history = TestWindow.getBrowser().browsingContext?.sessionHistory;
} else {
history = TestWindow.getBrowser().webNavigation.sessionHistory
.legacySHistory;
}
if (history) {
let history = TestWindow.getBrowser().browsingContext?.sessionHistory;
history.purgeHistory(history.count);
historyPurged = Promise.resolve();
} else {
historyPurged = SpecialPowers.spawn(TestWindow.getBrowser(), [], () => {
let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory
.legacySHistory;
history.purgeHistory(history.count);
});
}
// If the test changed the value of max_total_viewers via a call to
@ -482,7 +605,9 @@ function finish() {
}
});
window.close();
historyPurged.then(_ => {
window.close();
});
}
/**
@ -503,7 +628,12 @@ function finish() {
* time out.
*/
function waitForTrue(fn, onWaitComplete, timeout) {
var start = new Date().valueOf();
promiseTrue(fn, timeout).then(() => {
onWaitComplete.call();
});
}
function promiseTrue(fn, timeout) {
if (typeof timeout != "undefined") {
// If timeoutWait is less than 500, assume it represents seconds, and
// convert to ms.
@ -514,27 +644,43 @@ function waitForTrue(fn, onWaitComplete, timeout) {
// Loop until the test function returns true, or until a timeout occurs,
// if a timeout is defined.
var intervalid;
intervalid = setInterval(function() {
var timeoutHit = false;
if (typeof timeout != "undefined") {
timeoutHit = new Date().valueOf() - start >= timeout;
if (timeoutHit) {
ok(false, "Timed out waiting for condition");
let intervalid, timeoutid;
let condition = new Promise(resolve => {
intervalid = setInterval(async () => {
if (await fn.call()) {
resolve();
}
}
if (timeoutHit || fn.call()) {
// Stop calling the test function and notify the callback.
}, 20);
});
if (typeof timeout != "undefined") {
condition = Promise.race([
condition,
new Promise((_, reject) => {
timeoutid = setTimeout(() => {
reject();
}, timeout);
}),
]);
}
return condition
.finally(() => {
clearInterval(intervalid);
onWaitComplete.call();
}
}, 20);
})
.then(() => {
clearTimeout(timeoutid);
});
}
function waitForNextPaint(cb) {
requestAnimationFrame(_ => requestAnimationFrame(cb));
}
function promiseNextPaint() {
return new Promise(resolve => {
waitForNextPaint(resolve);
});
}
/**
* Enable or disable the bfcache.
*

View File

@ -9,7 +9,6 @@
<html:iframe id="normalFrame"/>
<html:iframe id="typeContentFrame" mozframetype="content"/>
<script type="application/javascript" src="docshell_helpers.js" />
<script type="application/javascript"><![CDATA[
function runTests() {
let opener = window.arguments[0];

View File

@ -12,6 +12,7 @@
window.opener = window.arguments[0];
]]></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js" />
<script src="chrome://mochikit/content/tests/SimpleTest/docshell_helpers.js">
</script>