summaryrefslogtreecommitdiffstats
path: root/remote/cdp/test/browser/page
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /remote/cdp/test/browser/page
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'remote/cdp/test/browser/page')
-rw-r--r--remote/cdp/test/browser/page/browser.ini49
-rw-r--r--remote/cdp/test/browser/page/browser_bringToFront.js64
-rw-r--r--remote/cdp/test/browser/page/browser_captureScreenshot.js557
-rw-r--r--remote/cdp/test/browser/page/browser_createIsolatedWorld.js491
-rw-r--r--remote/cdp/test/browser/page/browser_domContentEventFired.js95
-rw-r--r--remote/cdp/test/browser/page/browser_frameAttached.js144
-rw-r--r--remote/cdp/test/browser/page/browser_frameDetached.js171
-rw-r--r--remote/cdp/test/browser/page/browser_frameNavigated.js93
-rw-r--r--remote/cdp/test/browser/page/browser_frameStartedLoading.js104
-rw-r--r--remote/cdp/test/browser/page/browser_frameStoppedLoading.js104
-rw-r--r--remote/cdp/test/browser/page/browser_getFrameTree.js149
-rw-r--r--remote/cdp/test/browser/page/browser_getLayoutMetrics.js118
-rw-r--r--remote/cdp/test/browser/page/browser_getNavigationHistory.js65
-rw-r--r--remote/cdp/test/browser/page/browser_javascriptDialog_alert.js59
-rw-r--r--remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js54
-rw-r--r--remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js43
-rw-r--r--remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js50
-rw-r--r--remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js45
-rw-r--r--remote/cdp/test/browser/page/browser_lifecycleEvent.js191
-rw-r--r--remote/cdp/test/browser/page/browser_loadEventFired.js95
-rw-r--r--remote/cdp/test/browser/page/browser_navigate.js309
-rw-r--r--remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js133
-rw-r--r--remote/cdp/test/browser/page/browser_navigatedWithinDocument.js133
-rw-r--r--remote/cdp/test/browser/page/browser_navigationEvents.js223
-rw-r--r--remote/cdp/test/browser/page/browser_printToPDF.js53
-rw-r--r--remote/cdp/test/browser/page/browser_reload.js34
-rw-r--r--remote/cdp/test/browser/page/browser_runtimeEvents.js118
-rw-r--r--remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js167
-rw-r--r--remote/cdp/test/browser/page/doc_empty.html9
-rw-r--r--remote/cdp/test/browser/page/doc_frame.html9
-rw-r--r--remote/cdp/test/browser/page/doc_frameset_multi.html11
-rw-r--r--remote/cdp/test/browser/page/doc_frameset_nested.html10
-rw-r--r--remote/cdp/test/browser/page/doc_frameset_single.html10
-rw-r--r--remote/cdp/test/browser/page/head.js117
-rw-r--r--remote/cdp/test/browser/page/sjs_redirect.sjs7
35 files changed, 4084 insertions, 0 deletions
diff --git a/remote/cdp/test/browser/page/browser.ini b/remote/cdp/test/browser/page/browser.ini
new file mode 100644
index 0000000000..621a58c3ae
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser.ini
@@ -0,0 +1,49 @@
+[DEFAULT]
+tags = cdp
+subsuite = remote
+args =
+ --remote-debugging-port
+ --remote-allow-origins=null
+prefs = # Bug 1600054: Make CDP Fission compatible
+ fission.bfcacheInParent=false
+ fission.webContentIsolationStrategy=0
+support-files =
+ !/remote/cdp/test/browser/chrome-remote-interface.js
+ !/remote/cdp/test/browser/head.js
+ head.js
+ doc_empty.html
+ doc_frame.html
+ doc_frameset_multi.html
+ doc_frameset_nested.html
+ doc_frameset_single.html
+ sjs_redirect.sjs
+
+[browser_bringToFront.js]
+[browser_captureScreenshot.js]
+[browser_createIsolatedWorld.js]
+[browser_domContentEventFired.js]
+[browser_frameAttached.js]
+[browser_frameDetached.js]
+[browser_frameNavigated.js]
+[browser_frameStartedLoading.js]
+[browser_frameStoppedLoading.js]
+[browser_getFrameTree.js]
+[browser_getLayoutMetrics.js]
+[browser_getNavigationHistory.js]
+[browser_javascriptDialog_alert.js]
+[browser_javascriptDialog_beforeunload.js]
+[browser_javascriptDialog_confirm.js]
+[browser_javascriptDialog_otherTarget.js]
+[browser_javascriptDialog_prompt.js]
+[browser_lifecycleEvent.js]
+https_first_disabled = true
+[browser_loadEventFired.js]
+[browser_navigate.js]
+https_first_disabled = true
+[browser_navigateToHistoryEntry.js]
+[browser_navigatedWithinDocument.js]
+[browser_navigationEvents.js]
+[browser_printToPDF.js]
+[browser_reload.js]
+[browser_runtimeEvents.js]
+[browser_scriptToEvaluateOnNewDocument.js]
diff --git a/remote/cdp/test/browser/page/browser_bringToFront.js b/remote/cdp/test/browser/page/browser_bringToFront.js
new file mode 100644
index 0000000000..ed00071922
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_bringToFront.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const FIRST_DOC = toDataURL("first");
+const SECOND_DOC = toDataURL("second");
+
+add_task(async function testBringToFrontUpdatesSelectedTab({ client }) {
+ const tab = gBrowser.selectedTab;
+
+ await loadURL(FIRST_DOC);
+
+ info("Open another tab that should become the front tab");
+ const otherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ SECOND_DOC
+ );
+
+ try {
+ is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab");
+
+ const { Page } = client;
+ info(
+ "Call Page.bringToFront() and check that the test tab becomes the selected tab"
+ );
+ await Page.bringToFront();
+ is(gBrowser.selectedTab, tab, "Selected tab is the target tab again");
+ is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused");
+ } finally {
+ BrowserTestUtils.removeTab(otherTab);
+ }
+});
+
+add_task(async function testBringToFrontUpdatesFocusedWindow({ client }) {
+ const tab = gBrowser.selectedTab;
+
+ await loadURL(FIRST_DOC);
+
+ is(tab.ownerGlobal, getFocusedNavigator(), "The initial window is focused");
+
+ const otherWindow = await BrowserTestUtils.openNewBrowserWindow();
+
+ try {
+ is(otherWindow, getFocusedNavigator(), "The new window is focused");
+
+ const { Page } = client;
+ info(
+ "Call Page.bringToFront() and check that the tab window is focused again"
+ );
+ await Page.bringToFront();
+ is(
+ tab.ownerGlobal,
+ getFocusedNavigator(),
+ "The initial window is focused again"
+ );
+ } finally {
+ await BrowserTestUtils.closeWindow(otherWindow);
+ }
+});
+
+function getFocusedNavigator() {
+ return Services.wm.getMostRecentWindow("navigator:browser");
+}
diff --git a/remote/cdp/test/browser/page/browser_captureScreenshot.js b/remote/cdp/test/browser/page/browser_captureScreenshot.js
new file mode 100644
index 0000000000..1ff296e784
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_captureScreenshot.js
@@ -0,0 +1,557 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function documentSmallerThanViewport({ client }) {
+ const { Page } = client;
+
+ await loadURLWithElement();
+
+ info("Check that captureScreenshot() captures the viewport by default");
+ const { data } = await Page.captureScreenshot();
+ ok(!!data, "Screenshot data is not empty");
+
+ const scale = await getDevicePixelRatio();
+ const viewport = await getViewportSize();
+ const { mimeType, width, height } = await getImageDetails(data);
+
+ is(mimeType, "image/png", "Screenshot has correct MIME type");
+ is(width, (viewport.width - viewport.x) * scale, "Image has expected width");
+ is(
+ height,
+ (viewport.height - viewport.y) * scale,
+ "Image has expected height"
+ );
+});
+
+add_task(async function documentLargerThanViewport({ client }) {
+ const { Page } = client;
+
+ await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world"));
+
+ info("Check that captureScreenshot() captures the viewport by default");
+ const { data } = await Page.captureScreenshot();
+ ok(!!data, "Screenshot data is not empty");
+
+ const scale = await getDevicePixelRatio();
+ const scrollbarSize = await getScrollbarSize();
+ const viewport = await getViewportSize();
+ const { mimeType, width, height } = await getImageDetails(data);
+
+ is(mimeType, "image/png", "Screenshot has correct MIME type");
+ is(
+ width,
+ (viewport.width - viewport.x - scrollbarSize.width) * scale,
+ "Image has expected width"
+ );
+ is(
+ height,
+ (viewport.height - viewport.y - scrollbarSize.height) * scale,
+ "Image has expected height"
+ );
+});
+
+add_task(async function invalidFormat({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div>Hello world"));
+
+ let errorThrown = false;
+ try {
+ await Page.captureScreenshot({ format: "foo" });
+ } catch (e) {
+ errorThrown = true;
+ }
+ ok(errorThrown, "captureScreenshot raised error for invalid image format");
+});
+
+add_task(async function asJPEGFormat({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div>Hello world"));
+
+ info("Check that captureScreenshot() captures as JPEG format");
+ const { data } = await Page.captureScreenshot({ format: "jpeg" });
+ ok(!!data, "Screenshot data is not empty");
+
+ const scale = await getDevicePixelRatio();
+ const viewport = await getViewportSize();
+ const { mimeType, height, width } = await getImageDetails(data);
+
+ is(mimeType, "image/jpeg", "Screenshot has correct MIME type");
+ is(width, (viewport.width - viewport.x) * scale);
+ is(height, (viewport.height - viewport.y) * scale);
+});
+
+add_task(async function asJPEGFormatAndQuality({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div>Hello world"));
+
+ info("Check that captureScreenshot() captures as JPEG format");
+ const imageDefault = await Page.captureScreenshot({ format: "jpeg" });
+ ok(!!imageDefault, "Screenshot data with default quality is not empty");
+
+ const image100 = await Page.captureScreenshot({
+ format: "jpeg",
+ quality: 100,
+ });
+ ok(!!image100, "Screenshot data with quality 100 is not empty");
+
+ const image10 = await Page.captureScreenshot({
+ format: "jpeg",
+ quality: 10,
+ });
+ ok(!!image10, "Screenshot data with quality 10 is not empty");
+
+ const infoDefault = await getImageDetails(imageDefault.data);
+ const info100 = await getImageDetails(image100.data);
+ const info10 = await getImageDetails(image10.data);
+
+ // All screenshots are of mimeType JPEG
+ is(
+ infoDefault.mimeType,
+ "image/jpeg",
+ "Screenshot with default quality has correct MIME type"
+ );
+ is(
+ info100.mimeType,
+ "image/jpeg",
+ "Screenshot with quality 100 has correct MIME type"
+ );
+ is(
+ info10.mimeType,
+ "image/jpeg",
+ "Screenshot with quality 10 has correct MIME type"
+ );
+
+ const scale = await getDevicePixelRatio();
+ const viewport = await getViewportSize();
+
+ // Images are all of the same dimension
+ is(infoDefault.width, (viewport.width - viewport.x) * scale);
+ is(infoDefault.height, (viewport.height - viewport.y) * scale);
+
+ is(info100.width, (viewport.width - viewport.x) * scale);
+ is(info100.height, (viewport.height - viewport.y) * scale);
+
+ is(info10.width, (viewport.width - viewport.x) * scale);
+ is(info10.height, (viewport.height - viewport.y) * scale);
+
+ // Images of different quality result in different content sizes
+ ok(
+ info100.length > infoDefault.length,
+ "Size of quality 100 is larger than default"
+ );
+ ok(
+ info10.length < infoDefault.length,
+ "Size of quality 10 is smaller than default"
+ );
+});
+
+add_task(async function clipMissingProperties({ client }) {
+ const { Page } = client;
+ const contentSize = await getContentSize();
+
+ for (const prop of ["x", "y", "width", "height", "scale"]) {
+ console.info(`Check for missing ${prop}`);
+
+ const clip = {
+ x: 0,
+ y: 0,
+ width: contentSize.width,
+ height: contentSize.height,
+ };
+ clip[prop] = undefined;
+
+ let errorThrown = false;
+ try {
+ await Page.captureScreenshot({ clip });
+ } catch (e) {
+ errorThrown = true;
+ }
+ ok(errorThrown, `raised error for missing clip.${prop} property`);
+ }
+});
+
+add_task(async function clipOutOfBoundsXAndY({ client }) {
+ const { Page } = client;
+
+ const ratio = await getDevicePixelRatio();
+ const size = 50;
+
+ await loadURLWithElement();
+ const contentSize = await getContentSize();
+
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: size,
+ height: size,
+ scale: 1,
+ },
+ });
+
+ for (const x of [-1, contentSize.width]) {
+ console.info(`Check out-of-bounds x for ${x}`);
+ const { data } = await Page.captureScreenshot({
+ clip: {
+ x,
+ y: 0,
+ width: size,
+ height: size,
+ scale: 1,
+ },
+ });
+ const { width, height } = await getImageDetails(data);
+
+ is(width, size * ratio, "Image has expected width");
+ is(height, size * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+
+ for (const y of [-1, contentSize.height]) {
+ console.info(`Check out-of-bounds y for ${y}`);
+ const { data } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y,
+ width: size,
+ height: size,
+ scale: 1,
+ },
+ });
+ const { width, height } = await getImageDetails(data);
+
+ is(width, size * ratio, "Image has expected width");
+ is(height, size * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+add_task(async function clipOutOfBoundsWidthAndHeight({ client }) {
+ const { Page } = client;
+ const ratio = await getDevicePixelRatio();
+
+ await loadURL(toDataURL("<div style='margin: 100vh 100vw'>Hello world"));
+ const contentSize = await getContentSize();
+
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: contentSize.width,
+ height: contentSize.height,
+ scale: 1,
+ },
+ });
+
+ for (const value of [-1, 0]) {
+ console.info(`Check out-of-bounds width for ${value}`);
+ const clip = {
+ x: 0,
+ y: 0,
+ width: value,
+ height: contentSize.height,
+ scale: 1,
+ };
+
+ const { data } = await Page.captureScreenshot({ clip });
+ const { width, height } = await getImageDetails(data);
+ is(width, contentSize.width * ratio, "Image has expected width");
+ is(height, contentSize.height * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+
+ for (const value of [-1, 0]) {
+ console.info(`Check out-of-bounds height for ${value}`);
+ const clip = {
+ x: 0,
+ y: 0,
+ width: contentSize.width,
+ height: value,
+ scale: 1,
+ };
+
+ const { data } = await Page.captureScreenshot({ clip });
+ const { width, height } = await getImageDetails(data);
+ is(width, contentSize.width * ratio, "Image has expected width");
+ is(height, contentSize.height * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+add_task(async function clipOutOfBoundsScale({ client }) {
+ const { Page } = client;
+ const ratio = await getDevicePixelRatio();
+
+ await loadURLWithElement();
+ const contentSize = await getContentSize();
+
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: contentSize.width,
+ height: contentSize.height,
+ scale: 1,
+ },
+ });
+
+ for (const value of [-1, 0]) {
+ console.info(`Check out-of-bounds scale for ${value}`);
+ var { data } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 50,
+ scale: value,
+ },
+ });
+
+ const { width, height } = await getImageDetails(data);
+ is(width, contentSize.width * ratio, "Image has expected width");
+ is(height, contentSize.height * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+add_task(async function clipScale({ client }) {
+ const { Page } = client;
+ const ratio = await getDevicePixelRatio();
+
+ for (const scale of [1.5, 2]) {
+ console.info(`Check scale for ${scale}`);
+ await loadURLWithElement({ width: 100 * scale, height: 100 * scale });
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 100 * scale,
+ height: 100 * scale,
+ scale: 1,
+ },
+ });
+
+ await loadURLWithElement({ width: 100, height: 100 });
+ var { data } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ scale,
+ },
+ });
+
+ const { width, height } = await getImageDetails(data);
+ is(width, 100 * ratio * scale, "Image has expected width");
+ is(height, 100 * ratio * scale, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+add_task(async function clipScaleAndDevicePixelRatio({ client }) {
+ const { Page } = client;
+
+ const originalRatio = await getDevicePixelRatio();
+
+ const ratio = 2;
+ const scale = 1.5;
+ const size = 100;
+
+ const expectedSize = size * ratio * scale;
+
+ console.info(`Create reference screenshot: ${expectedSize}x${expectedSize}`);
+ await loadURLWithElement({
+ width: expectedSize,
+ height: expectedSize,
+ });
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: expectedSize,
+ height: expectedSize,
+ scale: 1,
+ },
+ });
+
+ await setDevicePixelRatio(originalRatio * ratio);
+
+ await loadURLWithElement({ width: size, height: size });
+ var { data } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: size,
+ height: size,
+ scale,
+ },
+ });
+
+ const { width, height } = await getImageDetails(data);
+ is(width, expectedSize * originalRatio, "Image has expected width");
+ is(height, expectedSize * originalRatio, "Image has expected height");
+ is(data, refData, "Image is equal");
+});
+
+add_task(async function clipPosition({ client }) {
+ const { Page } = client;
+ const ratio = await getDevicePixelRatio();
+
+ await loadURLWithElement();
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ scale: 1,
+ },
+ });
+
+ for (const [x, y] of [
+ [10, 20],
+ [20, 10],
+ [20, 20],
+ ]) {
+ console.info(`Check postion for ${x} and ${y}`);
+ await loadURLWithElement({ x, y });
+ var { data } = await Page.captureScreenshot({
+ clip: {
+ x,
+ y,
+ width: 100,
+ height: 100,
+ scale: 1,
+ },
+ });
+
+ const { width, height } = await getImageDetails(data);
+ is(width, 100 * ratio, "Image has expected width");
+ is(height, 100 * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+add_task(async function clipDimension({ client }) {
+ const { Page } = client;
+ const ratio = await getDevicePixelRatio();
+
+ for (const [width, height] of [
+ [10, 20],
+ [20, 10],
+ [20, 20],
+ ]) {
+ console.info(`Check width and height for ${width} and ${height}`);
+
+ // Get reference image as section from a larger image
+ await loadURLWithElement({ width: 50, height: 50 });
+ var { data: refData } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width,
+ height,
+ scale: 1,
+ },
+ });
+
+ await loadURLWithElement({ width, height });
+ var { data } = await Page.captureScreenshot({
+ clip: {
+ x: 0,
+ y: 0,
+ width,
+ height,
+ scale: 1,
+ },
+ });
+
+ const dimension = await getImageDetails(data);
+ is(dimension.width, width * ratio, "Image has expected width");
+ is(dimension.height, height * ratio, "Image has expected height");
+ is(data, refData, "Image is equal");
+ }
+});
+
+async function loadURLWithElement(options = {}) {
+ const { x = 0, y = 0, width = 100, height = 100 } = options;
+
+ const doc = `
+ <style>
+ body {
+ margin: 0;
+ }
+ div {
+ margin-left: ${x}px;
+ margin-top: ${y}px;
+ width: ${width}px;
+ height: ${height}px;
+ background: green;
+ }
+ </style>
+ <body>
+ <div></div>
+ `;
+
+ await loadURL(toDataURL(doc));
+}
+
+async function getDevicePixelRatio() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.browsingContext.overrideDPPX || content.devicePixelRatio;
+ });
+}
+
+async function setDevicePixelRatio(dppx) {
+ gBrowser.selectedBrowser.browsingContext.overrideDPPX = dppx;
+}
+
+async function getImageDetails(image) {
+ const mimeType = getMimeType(image);
+
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ mimeType, image }],
+ async function ({ mimeType, image }) {
+ return new Promise(resolve => {
+ const img = new content.Image();
+ img.addEventListener(
+ "load",
+ () => {
+ resolve({
+ mimeType,
+ width: img.width,
+ height: img.height,
+ length: image.length,
+ });
+ },
+ { once: true }
+ );
+
+ img.src = `data:${mimeType};base64,${image}`;
+ });
+ }
+ );
+}
+
+function getMimeType(image) {
+ // Decode from base64 and convert the first 4 bytes to hex
+ const raw = atob(image).slice(0, 4);
+ let magicBytes = "";
+ for (let i = 0; i < raw.length; i++) {
+ magicBytes += raw.charCodeAt(i).toString(16).toUpperCase();
+ }
+
+ switch (magicBytes) {
+ case "89504E47":
+ return "image/png";
+ case "FFD8FFDB":
+ case "FFD8FFE0":
+ return "image/jpeg";
+ default:
+ throw new Error("Unknown MIME type");
+ }
+}
diff --git a/remote/cdp/test/browser/page/browser_createIsolatedWorld.js b/remote/cdp/test/browser/page/browser_createIsolatedWorld.js
new file mode 100644
index 0000000000..4dc569f8c5
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_createIsolatedWorld.js
@@ -0,0 +1,491 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test Page.createIsolatedWorld
+
+const WORLD_NAME_1 = "testWorld1";
+const WORLD_NAME_2 = "testWorld2";
+
+const DESTROYED = "Runtime.executionContextDestroyed";
+const CREATED = "Runtime.executionContextCreated";
+const CLEARED = "Runtime.executionContextsCleared";
+
+add_task(async function frameIdMissing({ client }) {
+ const { Page } = client;
+
+ let errorThrown = "";
+ try {
+ await Page.createIsolatedWorld({
+ worldName: WORLD_NAME_1,
+ grantUniversalAccess: true,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.match(/frameId: string value expected/),
+ `Fails with missing frameId`
+ );
+});
+
+add_task(async function frameIdInvalidTypes({ client }) {
+ const { Page } = client;
+
+ for (const frameId of [null, true, 1, [], {}]) {
+ let errorThrown = "";
+ try {
+ await Page.createIsolatedWorld({
+ frameId,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.match(/frameId: string value expected/),
+ `Fails with invalid type: ${frameId}`
+ );
+ }
+});
+
+add_task(async function worldNameInvalidTypes({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ info("Page notifications are enabled");
+
+ const loadEvent = Page.loadEventFired();
+ const { frameId } = await Page.navigate({ url: PAGE_URL });
+ await loadEvent;
+
+ for (const worldName of [null, true, 1, [], {}]) {
+ let errorThrown = "";
+ try {
+ await Page.createIsolatedWorld({
+ frameId,
+ worldName,
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.match(/worldName: string value expected/),
+ `Fails with invalid type: ${worldName}`
+ );
+ }
+});
+
+add_task(async function noEventsWhenRuntimeDomainDisabled({ client }) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+ info("Page notifications are enabled");
+
+ const history = recordEvents(Runtime, 0);
+ const loadEvent = Page.loadEventFired();
+ const { frameId } = await Page.navigate({ url: PAGE_URL });
+ await loadEvent;
+
+ let errorThrown = "";
+ try {
+ await Page.createIsolatedWorld({
+ frameId,
+ worldName: WORLD_NAME_1,
+ grantUniversalAccess: true,
+ });
+ await assertEvents({ history, expectedEvents: [] });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ todo(
+ errorThrown === "",
+ "No contexts tracked internally without Runtime enabled (Bug 1623482)"
+ );
+});
+
+add_task(async function noEventsAfterRuntimeDomainDisabled({ client }) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+ info("Page notifications are enabled");
+
+ await enableRuntime(client);
+ await Runtime.disable();
+ info("Runtime notifications are disabled");
+
+ const history = recordEvents(Runtime, 0);
+ const loadEvent = Page.loadEventFired();
+ const { frameId } = await Page.navigate({ url: PAGE_URL });
+ await loadEvent;
+
+ await Page.createIsolatedWorld({
+ frameId,
+ worldName: WORLD_NAME_2,
+ grantUniversalAccess: true,
+ });
+ await assertEvents({ history, expectedEvents: [] });
+});
+
+add_task(async function contextCreatedAfterNavigation({ client }) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+ info("Page notifications are enabled");
+
+ await enableRuntime(client);
+
+ const history = recordEvents(Runtime, 3);
+ const loadEvent = Page.loadEventFired();
+ const { frameId } = await Page.navigate({ url: PAGE_URL });
+ await loadEvent;
+
+ const { executionContextId: isolatedId } = await Page.createIsolatedWorld({
+ frameId,
+ worldName: WORLD_NAME_1,
+ grantUniversalAccess: true,
+ });
+ await assertEvents({
+ history,
+ expectedEvents: [
+ DESTROYED, // default, about:blank
+ CREATED, // default, PAGE_URL
+ CREATED, // isolated, PAGE_URL
+ ],
+ });
+
+ const contexts = history
+ .findEvents(CREATED)
+ .map(event => event.payload.context);
+ const defaultContext = contexts[0];
+ const isolatedContext = contexts[1];
+ is(defaultContext.auxData.isDefault, true, "Default context is default");
+ is(
+ defaultContext.auxData.type,
+ "default",
+ "Default context has type 'default'"
+ );
+ is(defaultContext.origin, BASE_ORIGIN, "Default context has expected origin");
+ checkIsolated(isolatedContext, isolatedId, WORLD_NAME_1, frameId);
+ compareContexts(isolatedContext, defaultContext);
+});
+
+add_task(async function contextDestroyedForNavigation({ client }) {
+ const { Page, Runtime } = client;
+
+ const defaultContext = await enableRuntime(client);
+ const isolatedContext = await createIsolatedContext(client, defaultContext);
+
+ await Page.enable();
+
+ const history = recordEvents(Runtime, 4, true);
+ const frameNavigated = Page.frameNavigated();
+ await Page.navigate({ url: PAGE_URL });
+ await frameNavigated;
+
+ await assertEvents({
+ history,
+ expectedEvents: [
+ DESTROYED, // default, about:blank
+ DESTROYED, // isolated, about:blank
+ CLEARED,
+ CREATED, // default, PAGE_URL
+ ],
+ });
+
+ const destroyed = history
+ .findEvents(DESTROYED)
+ .map(event => event.payload.executionContextId);
+ ok(destroyed.includes(isolatedContext.id), "Isolated context destroyed");
+ ok(destroyed.includes(defaultContext.id), "Default context destroyed");
+
+ const { context: newContext } = history.findEvent(CREATED).payload;
+ is(newContext.auxData.isDefault, true, "The new context is a default one");
+ ok(!!newContext.id, "The new context has an id");
+ ok(
+ ![defaultContext.id, isolatedContext.id].includes(newContext.id),
+ "The new context has a new id"
+ );
+});
+
+add_task(async function contextsForFramesetNavigation({ client }) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+ info("Page notifications are enabled");
+
+ await enableRuntime(client);
+
+ // check creation when navigating to a frameset
+ const historyTo = recordEvents(Runtime, 5);
+ const loadEventTo = Page.loadEventFired();
+ const { frameId: frameIdTo } = await Page.navigate({
+ url: FRAMESET_SINGLE_URL,
+ });
+ await loadEventTo;
+
+ const { frameTree } = await Page.getFrameTree();
+ const subFrame = frameTree.childFrames[0].frame;
+
+ const { executionContextId: contextIdParent } =
+ await Page.createIsolatedWorld({
+ frameId: frameIdTo,
+ worldName: WORLD_NAME_1,
+ grantUniversalAccess: true,
+ });
+ const { executionContextId: contextIdSubFrame } =
+ await Page.createIsolatedWorld({
+ frameId: subFrame.id,
+ worldName: WORLD_NAME_2,
+ grantUniversalAccess: true,
+ });
+
+ await assertEvents({
+ history: historyTo,
+ expectedEvents: [
+ DESTROYED, // default, about:blank
+ CREATED, // default, FRAMESET_SINGLE_URL
+ CREATED, // default, PAGE_URL
+ CREATED, // isolated, FRAMESET_SINGLE_URL
+ CREATED, // isolated, PAGE_URL
+ ],
+ });
+
+ const contextsCreated = historyTo
+ .findEvents(CREATED)
+ .map(event => event.payload.context);
+ const parentDefaultContextCreated = contextsCreated[0];
+ const frameDefaultContextCreated = contextsCreated[1];
+ const parentIsolatedContextCreated = contextsCreated[2];
+ const frameIsolatedContextCreated = contextsCreated[3];
+
+ checkIsolated(
+ parentIsolatedContextCreated,
+ contextIdParent,
+ WORLD_NAME_1,
+ frameIdTo
+ );
+ compareContexts(parentIsolatedContextCreated, parentDefaultContextCreated);
+
+ checkIsolated(
+ frameIsolatedContextCreated,
+ contextIdSubFrame,
+ WORLD_NAME_2,
+ subFrame.id
+ );
+ compareContexts(frameIsolatedContextCreated, frameDefaultContextCreated);
+
+ // check destroying when navigating away from a frameset
+ const historyFrom = recordEvents(Runtime, 6);
+ const loadEventFrom = Page.loadEventFired();
+ await Page.navigate({ url: PAGE_URL });
+ await loadEventFrom;
+
+ await assertEvents({
+ history: historyFrom,
+ expectedEvents: [
+ DESTROYED, // default, PAGE_URL
+ DESTROYED, // isolated, PAGE_URL
+ DESTROYED, // default, FRAMESET_SINGLE_URL
+ DESTROYED, // isolated, FRAMESET_SINGLE_URL
+ CREATED, // default, PAGE_URL
+ ],
+ });
+
+ const contextsDestroyed = historyFrom
+ .findEvents(DESTROYED)
+ .map(event => event.payload.executionContextId);
+ contextsCreated.forEach(context => {
+ ok(
+ contextsDestroyed.includes(context.id),
+ `Context with id ${context.id} destroyed`
+ );
+ });
+
+ const { context: newContext } = historyFrom.findEvent(CREATED).payload;
+ is(newContext.auxData.isDefault, true, "The new context is a default one");
+ ok(!!newContext.id, "The new context has an id");
+ ok(
+ ![parentDefaultContextCreated.id, frameDefaultContextCreated.id].includes(
+ newContext.id
+ ),
+ "The new context has a new id"
+ );
+});
+
+add_task(async function evaluateInIsolatedAndDefault({ client }) {
+ const { Runtime } = client;
+
+ const defaultContext = await enableRuntime(client);
+ const isolatedContext = await createIsolatedContext(client, defaultContext);
+
+ const { result: objDefault } = await Runtime.evaluate({
+ contextId: defaultContext.id,
+ expression: "({ foo: 1 })",
+ });
+ const { result: objIsolated } = await Runtime.evaluate({
+ contextId: isolatedContext.id,
+ expression: "({ foo: 10 })",
+ });
+ const { result: result1 } = await Runtime.callFunctionOn({
+ executionContextId: isolatedContext.id,
+ functionDeclaration: "arg => ++arg.foo",
+ arguments: [{ objectId: objIsolated.objectId }],
+ });
+ is(result1.value, 11, "Isolated context incremented the expected value");
+
+ let errorThrown = "";
+ try {
+ await Runtime.callFunctionOn({
+ executionContextId: isolatedContext.id,
+ functionDeclaration: "arg => ++arg.foo",
+ arguments: [{ objectId: objDefault.objectId }],
+ });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.match(/Could not find object with given id/),
+ "Contexts do not share objects"
+ );
+});
+
+add_task(async function contextEvaluationIsIsolated({ client }) {
+ const { Runtime } = client;
+
+ // If a document makes changes to standard global object, an isolated
+ // world should not be affected
+ await loadURL(toDataURL("<script>window.Node = null</script>"));
+
+ const defaultContext = await enableRuntime(client);
+ const isolatedContext = await createIsolatedContext(client, defaultContext);
+
+ const { result: result1 } = await Runtime.callFunctionOn({
+ executionContextId: defaultContext.id,
+ functionDeclaration: "arg => window.Node",
+ });
+ const { result: result2 } = await Runtime.callFunctionOn({
+ executionContextId: isolatedContext.id,
+ functionDeclaration: "arg => window.Node",
+ });
+ is(result1.value, null, "Default context sees content changes to global");
+ todo_isnot(
+ result2.value,
+ null,
+ "Isolated context is not affected by changes to global, Bug 1601421"
+ );
+});
+
+function checkIsolated(context, expectedId, expectedName, expectedFrameId) {
+ is(
+ expectedId,
+ context.id,
+ "createIsolatedWorld returns id of isolated context"
+ );
+ is(
+ context.auxData.frameId,
+ expectedFrameId,
+ "Isolated context has expected frameId"
+ );
+ is(context.auxData.isDefault, false, "Isolated context is not default");
+ is(context.auxData.type, "isolated", "Isolated context has type 'isolated'");
+ is(context.name, expectedName, "Isolated context is named as requested");
+ ok(!!context.origin, "Isolated context has an origin");
+}
+
+function compareContexts(isolatedContext, defaultContext) {
+ isnot(
+ defaultContext.name,
+ isolatedContext.name,
+ "The contexts have different names"
+ );
+ isnot(
+ defaultContext.id,
+ isolatedContext.id,
+ "The contexts have different ids"
+ );
+ is(
+ defaultContext.origin,
+ isolatedContext.origin,
+ "The contexts have same origin"
+ );
+ is(
+ defaultContext.auxData.frameId,
+ isolatedContext.auxData.frameId,
+ "The contexts have same frameId"
+ );
+}
+
+async function createIsolatedContext(
+ client,
+ defaultContext,
+ worldName = WORLD_NAME_1
+) {
+ const { Page, Runtime } = client;
+
+ const frameId = defaultContext.auxData.frameId;
+
+ const isolatedContextCreated = Runtime.executionContextCreated();
+ const { executionContextId: isolatedId } = await Page.createIsolatedWorld({
+ frameId,
+ worldName,
+ grantUniversalAccess: true,
+ });
+ const { context: isolatedContext } = await isolatedContextCreated;
+ info("Isolated world created");
+
+ checkIsolated(isolatedContext, isolatedId, worldName, frameId);
+ compareContexts(isolatedContext, defaultContext);
+
+ return isolatedContext;
+}
+
+function recordEvents(Runtime, total, cleared = false) {
+ const history = new RecordEvents(total);
+
+ history.addRecorder({
+ event: Runtime.executionContextDestroyed,
+ eventName: DESTROYED,
+ messageFn: payload => {
+ return `Received ${DESTROYED} for id ${payload.executionContextId}`;
+ },
+ });
+ history.addRecorder({
+ event: Runtime.executionContextCreated,
+ eventName: CREATED,
+ messageFn: payload => {
+ return (
+ `Received ${CREATED} for id ${payload.context.id}` +
+ ` type: ${payload.context.auxData.type}` +
+ ` name: ${payload.context.name}` +
+ ` origin: ${payload.context.origin}`
+ );
+ },
+ });
+ if (cleared) {
+ history.addRecorder({
+ event: Runtime.executionContextsCleared,
+ eventName: CLEARED,
+ });
+ }
+
+ return history;
+}
+
+async function assertEvents(options = {}) {
+ const { history, expectedEvents, timeout = 1000 } = options;
+ const events = await history.record(timeout);
+ const eventNames = events.map(item => item.eventName);
+ info(`Expected events: ${expectedEvents}`);
+ info(`Received events: ${eventNames}`);
+ is(
+ events.length,
+ expectedEvents.length,
+ "Received expected number of Runtime context events"
+ );
+ Assert.deepEqual(
+ eventNames.sort(),
+ expectedEvents.sort(),
+ "Received expected Runtime context events"
+ );
+}
diff --git a/remote/cdp/test/browser/page/browser_domContentEventFired.js b/remote/cdp/test/browser/page/browser_domContentEventFired.js
new file mode 100644
index 0000000000..7bdf8f127d
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_domContentEventFired.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runContentEventFiredTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runContentEventFiredTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runContentEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runContentEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runContentEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runContentEventFiredTest(client, expectedEventCount, callback) {
+ const { Page } = client;
+
+ if (![0, 1].includes(expectedEventCount)) {
+ throw new Error(`Invalid value for expectedEventCount`);
+ }
+
+ const DOM_CONTENT_EVENT_FIRED = "Page.domContentEventFired";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.domContentEventFired,
+ eventName: DOM_CONTENT_EVENT_FIRED,
+ messageFn: payload => {
+ return `Received ${DOM_CONTENT_EVENT_FIRED} at time ${payload.timestamp}`;
+ },
+ });
+
+ const timeBefore = Date.now() / 1000;
+ await callback();
+ const domContentEventFiredEvents = await history.record();
+ const timeAfter = Date.now() / 1000;
+
+ is(
+ domContentEventFiredEvents.length,
+ expectedEventCount,
+ "Got expected amount of domContentEventFired events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const timestamp = domContentEventFiredEvents[0].payload.timestamp;
+ ok(
+ timestamp >= timeBefore && timestamp <= timeAfter,
+ `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]`
+ );
+}
diff --git a/remote/cdp/test/browser/page/browser_frameAttached.js b/remote/cdp/test/browser/page/browser_frameAttached.js
new file mode 100644
index 0000000000..bda66b2814
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_frameAttached.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runFrameAttachedTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runFrameAttachedTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await Page.enable();
+
+ await runFrameAttachedTest(client, 0, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameAttachedTest(client, 2, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameAttachedTest(client, 3, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenAttachingFrame({ client }) {
+ const { Page } = client;
+
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await Page.enable();
+
+ await runFrameAttachedTest(client, 1, async () => {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [PAGE_FRAME_URL],
+ async frameURL => {
+ const frame = content.document.createElement("iframe");
+ frame.src = frameURL;
+ const loaded = new Promise(resolve => (frame.onload = resolve));
+ content.document.body.appendChild(frame);
+ await loaded;
+ }
+ );
+ });
+});
+
+async function runFrameAttachedTest(client, expectedEventCount, callback) {
+ const { Page } = client;
+
+ const ATTACHED = "Page.frameAttached";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.frameAttached,
+ eventName: ATTACHED,
+ messageFn: payload => {
+ return `Received ${ATTACHED} for frame id ${payload.frameId}`;
+ },
+ });
+
+ const framesBefore = await getFlattenedFrameTree(client);
+ await callback();
+ const framesAfter = await getFlattenedFrameTree(client);
+
+ const frameAttachedEvents = await history.record();
+
+ if (expectedEventCount == 0) {
+ is(frameAttachedEvents.length, 0, "Got no frame attached event");
+ return;
+ }
+
+ // check how many frames were attached or detached
+ const count = Math.abs(framesBefore.size - framesAfter.size);
+
+ is(count, expectedEventCount, "Expected amount of frames attached");
+ is(
+ frameAttachedEvents.length,
+ count,
+ "Received the expected amount of frameAttached events"
+ );
+
+ // extract the new or removed frames
+ const framesAll = new Map([...framesBefore, ...framesAfter]);
+ const expectedFrames = new Map(
+ [...framesAll].filter(([key, _value]) => {
+ return !framesBefore.has(key) && framesAfter.has(key);
+ })
+ );
+
+ frameAttachedEvents.forEach(({ payload }) => {
+ const { frameId, parentFrameId } = payload;
+
+ info(`Check frame id ${frameId}`);
+ const expectedFrame = expectedFrames.get(frameId);
+
+ ok(expectedFrame, `Found expected frame with id ${frameId}`);
+ is(
+ frameId,
+ expectedFrame.id,
+ "Got expected frame id for frameAttached event"
+ );
+ is(
+ parentFrameId,
+ expectedFrame.parentId,
+ "Got expected parent frame id for frameAttached event"
+ );
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_frameDetached.js b/remote/cdp/test/browser/page/browser_frameDetached.js
new file mode 100644
index 0000000000..90db5087b1
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_frameDetached.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Disable bfcache to force documents to be destroyed on navigation
+Services.prefs.setIntPref("browser.sessionhistory.max_total_viewers", 0);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.sessionhistory.max_total_viewers");
+});
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await runFrameDetachedTest(client, 0, async () => {
+ info("Navigate away from a page with an iframe");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await Page.enable();
+ await Page.disable();
+
+ await runFrameDetachedTest(client, 0, async () => {
+ info("Navigate away to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function noEventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+
+ await runFrameDetachedTest(client, 0, async () => {
+ info("Navigate away to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+
+ await Page.enable();
+
+ await runFrameDetachedTest(client, 2, async () => {
+ info("Navigate away to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await Page.enable();
+
+ await runFrameDetachedTest(client, 3, async () => {
+ info("Navigate away to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenDetachingFrame({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+
+ await Page.enable();
+
+ await runFrameDetachedTest(client, 1, async () => {
+ // Remove the single frame from the page
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const frame = content.document.getElementsByTagName("iframe")[0];
+ frame.remove();
+ });
+ });
+});
+
+add_task(async function eventWhenDetachingNestedFrames({ client }) {
+ const { Page, Runtime } = client;
+
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await Page.enable();
+ await Runtime.enable();
+
+ const { context } = await Runtime.executionContextCreated();
+
+ await runFrameDetachedTest(client, 3, async () => {
+ // Remove top-frame, which also removes any nested frames
+ await evaluate(client, context.id, async () => {
+ const frame = document.getElementsByTagName("iframe")[0];
+ frame.remove();
+ });
+ });
+});
+
+async function runFrameDetachedTest(client, expectedEventCount, callback) {
+ const { Page } = client;
+
+ const DETACHED = "Page.frameDetached";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.frameDetached,
+ eventName: DETACHED,
+ messageFn: payload => {
+ return `Received ${DETACHED} for frame id ${payload.frameId}`;
+ },
+ });
+
+ const framesBefore = await getFlattenedFrameTree(client);
+ await callback();
+ const framesAfter = await getFlattenedFrameTree(client);
+
+ const frameDetachedEvents = await history.record();
+
+ if (expectedEventCount == 0) {
+ is(frameDetachedEvents.length, 0, "Got no frame detached event");
+ return;
+ }
+
+ // check how many frames were attached or detached
+ const count = Math.abs(framesBefore.size - framesAfter.size);
+
+ is(count, expectedEventCount, "Expected amount of frames detached");
+ is(
+ frameDetachedEvents.length,
+ count,
+ "Received the expected amount of frameDetached events"
+ );
+
+ // extract the new or removed frames
+ const framesAll = new Map([...framesBefore, ...framesAfter]);
+ const expectedFrames = new Map(
+ [...framesAll].filter(([key, _value]) => {
+ return framesBefore.has(key) && !framesAfter.has(key);
+ })
+ );
+
+ frameDetachedEvents.forEach(({ payload }) => {
+ const { frameId } = payload;
+
+ info(`Check frame id ${frameId}`);
+ const expectedFrame = expectedFrames.get(frameId);
+
+ is(
+ frameId,
+ expectedFrame.id,
+ "Got expected frame id for frameDetached event"
+ );
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_frameNavigated.js b/remote/cdp/test/browser/page/browser_frameNavigated.js
new file mode 100644
index 0000000000..4453c63749
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_frameNavigated.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runFrameNavigatedTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runFrameNavigatedTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameNavigatedTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameNavigatedTest(client, 3, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameNavigatedTest(client, 4, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runFrameNavigatedTest(client, expectedEventCount, callback) {
+ const { Page } = client;
+
+ const NAVIGATED = "Page.frameNavigated";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.frameNavigated,
+ eventName: NAVIGATED,
+ messageFn: payload => {
+ return `Received ${NAVIGATED} for frame id ${payload.frame.id}`;
+ },
+ });
+
+ await callback();
+
+ const frameNavigatedEvents = await history.record();
+
+ is(
+ frameNavigatedEvents.length,
+ expectedEventCount,
+ "Got expected amount of frameNavigated events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const frames = await getFlattenedFrameTree(client);
+
+ frameNavigatedEvents.forEach(({ payload }) => {
+ const { frame } = payload;
+
+ const expectedFrame = frames.get(frame.id);
+ Assert.deepEqual(frame, expectedFrame, "Got expected frame details");
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_frameStartedLoading.js b/remote/cdp/test/browser/page/browser_frameStartedLoading.js
new file mode 100644
index 0000000000..d5bb91a952
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_frameStartedLoading.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runFrameStartedLoadingTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runFrameStartedLoadingTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStartedLoadingTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStartedLoadingTest(client, 3, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStartedLoadingTest(client, 4, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runFrameStartedLoadingTest(
+ client,
+ expectedEventCount,
+ callback
+) {
+ const { Page } = client;
+
+ const STARTED_LOADING = "Page.frameStartedLoading";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.frameStartedLoading,
+ eventName: STARTED_LOADING,
+ messageFn: payload => {
+ return `Received ${STARTED_LOADING} for frame id ${payload.frameId}`;
+ },
+ });
+
+ await callback();
+
+ const frameStartedLoadingEvents = await history.record();
+
+ is(
+ frameStartedLoadingEvents.length,
+ expectedEventCount,
+ "Got expected amount of frameStartedLoading events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const frames = await getFlattenedFrameTree(client);
+
+ frameStartedLoadingEvents.forEach(({ payload }) => {
+ const { frameId } = payload;
+
+ info(`Check frame id ${frameId}`);
+ const expectedFrame = frames.get(frameId);
+
+ ok(expectedFrame, `Found expected frame with id ${frameId}`);
+ is(
+ frameId,
+ expectedFrame.id,
+ "Got expected frame id for frameStartedLoading event"
+ );
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_frameStoppedLoading.js b/remote/cdp/test/browser/page/browser_frameStoppedLoading.js
new file mode 100644
index 0000000000..9d7c37ddc4
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_frameStoppedLoading.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runFrameStoppedLoadingTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runFrameStoppedLoadingTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStoppedLoadingTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStoppedLoadingTest(client, 3, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runFrameStoppedLoadingTest(client, 4, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runFrameStoppedLoadingTest(
+ client,
+ expectedEventCount,
+ callback
+) {
+ const { Page } = client;
+
+ const STOPPED_LOADING = "Page.frameStoppedLoading";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.frameStoppedLoading,
+ eventName: STOPPED_LOADING,
+ messageFn: payload => {
+ return `Received ${STOPPED_LOADING} for frame id ${payload.frameId}`;
+ },
+ });
+
+ await callback();
+
+ const frameStoppedLoadingEvents = await history.record();
+
+ is(
+ frameStoppedLoadingEvents.length,
+ expectedEventCount,
+ "Got expected amount of frameStoppedLoading events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const frames = await getFlattenedFrameTree(client);
+
+ frameStoppedLoadingEvents.forEach(({ payload }) => {
+ const { frameId } = payload;
+
+ info(`Check frame id ${frameId}`);
+ const expectedFrame = frames.get(frameId);
+
+ ok(expectedFrame, `Found expected frame with id ${frameId}`);
+ is(
+ frameId,
+ expectedFrame.id,
+ "Got expected frame id for frameStartedLoading event"
+ );
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_getFrameTree.js b/remote/cdp/test/browser/page/browser_getFrameTree.js
new file mode 100644
index 0000000000..e96dc26d45
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_getFrameTree.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function pageWithoutFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page without a frame");
+ await loadURL(PAGE_URL);
+
+ const { frameTree } = await Page.getFrameTree();
+ ok(!!frameTree.frame, "Expected frame details found");
+
+ const expectedFrames = await getFlattenedFrameList();
+
+ // Check top-level frame
+ const expectedFrame = expectedFrames.get(frameTree.frame.id);
+ is(frameTree.frame.id, expectedFrame.id, "Expected frame id found");
+ is(frameTree.frame.parentId, undefined, "Parent frame doesn't exist");
+ is(frameTree.name, undefined, "Top frame doens't contain name property");
+ is(frameTree.frame.url, expectedFrame.url, "Expected url found");
+ is(frameTree.childFrames, undefined, "No sub frames found");
+});
+
+add_task(async function PageWithFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with frames");
+ await loadURL(FRAMESET_MULTI_URL);
+
+ const { frameTree } = await Page.getFrameTree();
+ ok(!!frameTree.frame, "Expected frame details found");
+
+ const expectedFrames = await getFlattenedFrameList();
+
+ let frame = frameTree.frame;
+ let expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check top frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, undefined, "Parent frame doesn't exist");
+ is(frame.name, undefined, "Top frame doesn't contain name property");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+
+ is(frameTree.childFrames.length, 2, "Expected two sub frames");
+ for (const childFrameTree of frameTree.childFrames) {
+ let frame = childFrameTree.frame;
+ let expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check sub frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, expectedFrame.parentId, "Expected parent id found");
+ is(frame.name, expectedFrame.name, "Frame has expected name set");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+ is(childFrameTree.childFrames, undefined, "No sub frames found");
+ }
+});
+
+add_task(async function pageWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with nested frames");
+ await loadURL(FRAMESET_NESTED_URL);
+
+ const { frameTree } = await Page.getFrameTree();
+ ok(!!frameTree.frame, "Expected frame details found");
+
+ const expectedFrames = await getFlattenedFrameList();
+
+ let frame = frameTree.frame;
+ let expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check top frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, undefined, "Parent frame doesn't exist");
+ is(frame.name, undefined, "Top frame doesn't contain name property");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+ is(frameTree.childFrames.length, 1, "Expected a single sub frame");
+
+ const childFrameTree = frameTree.childFrames[0];
+ frame = childFrameTree.frame;
+ expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check sub frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, expectedFrame.parentId, "Expected parent id found");
+ is(frame.name, expectedFrame.name, "Frame has expected name set");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+ is(childFrameTree.childFrames.length, 2, "Expected two sub frames");
+
+ let nestedChildFrameTree = childFrameTree.childFrames[0];
+ frame = nestedChildFrameTree.frame;
+ expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check first nested frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, expectedFrame.parentId, "Expected parent id found");
+ is(frame.name, expectedFrame.name, "Frame has expected name set");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+ is(nestedChildFrameTree.childFrames, undefined, "No sub frames found");
+
+ nestedChildFrameTree = childFrameTree.childFrames[1];
+ frame = nestedChildFrameTree.frame;
+ expectedFrame = expectedFrames.get(frame.id);
+
+ info(`Check second nested frame with id: ${frame.id}`);
+ is(frame.id, expectedFrame.id, "Expected frame id found");
+ is(frame.parentId, expectedFrame.parentId, "Expected parent id found");
+ is(frame.name, expectedFrame.name, "Frame has expected name set");
+ is(frame.url, expectedFrame.url, "Expected URL found");
+ is(nestedChildFrameTree.childFrames, undefined, "No sub frames found");
+});
+
+/**
+ * Retrieve all frames for the current tab as flattened list.
+ */
+function getFlattenedFrameList() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const frames = new Map();
+
+ function getFrameDetails(context) {
+ const frameElement = context.embedderElement;
+
+ const frame = {
+ id: context.id.toString(),
+ parentId: context.parent ? context.parent.id.toString() : null,
+ loaderId: null,
+ name: frameElement?.id || frameElement?.name,
+ url: context.docShell.domWindow.location.href,
+ securityOrigin: null,
+ mimeType: null,
+ };
+
+ if (context.parent) {
+ frame.parentId = context.parent.id.toString();
+ }
+
+ frames.set(context.id.toString(), frame);
+
+ for (const childContext of context.children) {
+ getFrameDetails(childContext);
+ }
+ }
+
+ getFrameDetails(content.docShell.browsingContext);
+ return frames;
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_getLayoutMetrics.js b/remote/cdp/test/browser/page/browser_getLayoutMetrics.js
new file mode 100644
index 0000000000..db8b3e8f3c
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_getLayoutMetrics.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function documentSmallerThanViewport({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div>Hello world"));
+
+ const { contentSize, layoutViewport } = await Page.getLayoutMetrics();
+ await checkContentSize(contentSize);
+ await checkLayoutViewport(layoutViewport);
+
+ is(
+ contentSize.x,
+ layoutViewport.pageX,
+ "X position of content is equal to layout viewport"
+ );
+ is(
+ contentSize.y,
+ layoutViewport.pageY,
+ "Y position of content is equal to layout viewport"
+ );
+ ok(
+ contentSize.width <= layoutViewport.clientWidth,
+ "Width of content is smaller than the layout viewport"
+ );
+ ok(
+ contentSize.height <= layoutViewport.clientHeight,
+ "Height of content is smaller than the layout viewport"
+ );
+});
+
+add_task(async function documentLargerThanViewport({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world"));
+
+ const { contentSize, layoutViewport } = await Page.getLayoutMetrics();
+ await checkContentSize(contentSize);
+ await checkLayoutViewport(layoutViewport, { scrollbars: true });
+
+ is(
+ contentSize.x,
+ layoutViewport.pageX,
+ "X position of content is equal to layout viewport"
+ );
+ is(
+ contentSize.y,
+ layoutViewport.pageY,
+ "Y position of content is equal to layout viewport"
+ );
+ ok(
+ contentSize.width > layoutViewport.clientWidth,
+ "Width of content is larger than the layout viewport"
+ );
+ ok(
+ contentSize.height > layoutViewport.clientHeight,
+ "Height of content is larger than the layout viewport"
+ );
+});
+
+add_task(async function documentLargerThanViewportScrolledXY({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("<div style='margin: 150vh 0 0 150vw'>Hello world"));
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.scrollTo(50, 100);
+ });
+
+ const { contentSize, layoutViewport } = await Page.getLayoutMetrics();
+ await checkContentSize(contentSize);
+ await checkLayoutViewport(layoutViewport, { scrollbars: true });
+
+ is(
+ layoutViewport.pageX,
+ contentSize.x + 50,
+ "X position of content is equal to layout viewport"
+ );
+ is(
+ layoutViewport.pageY,
+ contentSize.y + 100,
+ "Y position of content is equal to layout viewport"
+ );
+ ok(
+ contentSize.width > layoutViewport.clientWidth,
+ "Width of content is larger than the layout viewport"
+ );
+ ok(
+ contentSize.height > layoutViewport.clientHeight,
+ "Height of content is larger than the layout viewport"
+ );
+});
+
+async function checkContentSize(rect) {
+ const expected = await getContentSize();
+
+ is(rect.x, expected.x, "Expected x position returned");
+ is(rect.y, expected.y, "Expected y position returned");
+ is(rect.width, expected.width, "Expected width returned");
+ is(rect.height, expected.height, "Expected height returned");
+}
+
+async function checkLayoutViewport(viewport, options = {}) {
+ const { scrollbars = false } = options;
+
+ const expected = await getViewportSize();
+
+ if (scrollbars) {
+ const { width, height } = await getScrollbarSize();
+ expected.width -= width;
+ expected.height -= height;
+ }
+
+ is(viewport.pageX, expected.x, "Expected x position returned");
+ is(viewport.pageY, expected.y, "Expected y position returned");
+ is(viewport.clientWidth, expected.width, "Expected width returned");
+ is(viewport.clientHeight, expected.height, "Expected height returned");
+}
diff --git a/remote/cdp/test/browser/page/browser_getNavigationHistory.js b/remote/cdp/test/browser/page/browser_getNavigationHistory.js
new file mode 100644
index 0000000000..1067629828
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_getNavigationHistory.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function singleEntry({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(1);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, 0);
+});
+
+add_task(async function multipleEntriesWithLastIndex({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, data.length - 1);
+});
+
+add_task(async function multipleEntriesWithFirstIndex({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ await gotoHistoryIndex(0);
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, 0);
+});
+
+add_task(async function locationRedirect({ client }) {
+ const { Page } = client;
+
+ const pageEmptyURL =
+ "https://example.com/browser/remote/cdp/test/browser/page/doc_empty.html";
+ const sjsURL =
+ "https://example.com/browser/remote/cdp/test/browser/page/sjs_redirect.sjs";
+ const redirectURL = `${sjsURL}?${pageEmptyURL}`;
+
+ const data = [
+ {
+ url: pageEmptyURL,
+ userTypedURL: redirectURL,
+ title: "Empty page",
+ },
+ ];
+
+ await loadURL(redirectURL, pageEmptyURL);
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, 0);
+});
diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js b/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js
new file mode 100644
index 0000000000..40c5e4898a
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_javascriptDialog_alert.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test a browser alert is detected via Page.javascriptDialogOpening and can be
+// closed with Page.handleJavaScriptDialog
+add_task(async function ({ client }) {
+ const { Page } = client;
+
+ info("Enable the page domain");
+ await Page.enable();
+
+ info("Set window.alertIsClosed to false in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ // This boolean will be flipped after closing the dialog
+ content.alertIsClosed = false;
+ });
+
+ info("Create an alert dialog again");
+ const { message, type } = await createAlertDialog(Page);
+ is(type, "alert", "dialog event contains the correct type");
+ is(message, "test-1234", "dialog event contains the correct text");
+
+ info("Close the dialog with accept:false");
+ await Page.handleJavaScriptDialog({ accept: false });
+
+ info("Retrieve the alertIsClosed boolean on the content window");
+ let alertIsClosed = await getContentProperty("alertIsClosed");
+ ok(alertIsClosed, "The content process is no longer blocked on the alert");
+
+ info("Reset window.alertIsClosed to false in the content page");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.alertIsClosed = false;
+ });
+
+ info("Create an alert dialog again");
+ await createAlertDialog(Page);
+
+ info("Close the dialog with accept:true");
+ await Page.handleJavaScriptDialog({ accept: true });
+
+ alertIsClosed = await getContentProperty("alertIsClosed");
+ ok(alertIsClosed, "The content process is no longer blocked on the alert");
+});
+
+function createAlertDialog(Page) {
+ const onDialogOpen = Page.javascriptDialogOpening();
+
+ info("Trigger an alert in the test page");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.alert("test-1234");
+ // Flip a boolean in the content page to check if the content process resumed
+ // after the alert was opened.
+ content.alertIsClosed = true;
+ });
+
+ return onDialogOpen;
+}
diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js b/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js
new file mode 100644
index 0000000000..7cab579e2c
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_javascriptDialog_beforeunload.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test beforeunload dialog events.
+add_task(async function ({ client, tab }) {
+ info("Allow to trigger onbeforeunload without user interaction");
+ await new Promise(resolve => {
+ const options = {
+ set: [["dom.require_user_interaction_for_beforeunload", false]],
+ };
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+
+ const { Page } = client;
+
+ info("Enable the page domain");
+ await Page.enable();
+
+ info("Attach a valid onbeforeunload handler");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.onbeforeunload = () => true;
+ });
+
+ info("Trigger the beforeunload again but reject the prompt");
+ const { type } = await triggerBeforeUnload(Page, tab, false);
+ is(type, "beforeunload", "dialog event contains the correct type");
+
+ info("Trigger the beforeunload again and accept the prompt");
+ const onTabClose = BrowserTestUtils.waitForEvent(tab, "TabClose");
+ await triggerBeforeUnload(Page, tab, true);
+
+ info("Wait for the TabClose event");
+ await onTabClose;
+});
+
+function triggerBeforeUnload(Page, tab, accept) {
+ // We use then here because after clicking on the close button, nothing
+ // in the main block of the function will be executed until the prompt
+ // is accepted or rejected. Attaching a then to this promise still works.
+
+ const onDialogOpen = Page.javascriptDialogOpening().then(
+ async dialogEvent => {
+ await Page.handleJavaScriptDialog({ accept });
+ return dialogEvent;
+ }
+ );
+
+ info("Click on the tab close icon");
+ tab.closeButton.click();
+
+ return onDialogOpen;
+}
diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js b/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js
new file mode 100644
index 0000000000..eec7e9828d
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_javascriptDialog_confirm.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for window.confirm(). Check that the dialog is correctly detected and that it can
+// be rejected or accepted.
+add_task(async function ({ client }) {
+ const { Page } = client;
+
+ info("Enable the page domain");
+ await Page.enable();
+
+ info("Create a confirm dialog to open");
+ const { message, type } = await createConfirmDialog(Page);
+
+ is(type, "confirm", "dialog event contains the correct type");
+ is(message, "confirm-1234?", "dialog event contains the correct text");
+
+ info("Accept the dialog");
+ await Page.handleJavaScriptDialog({ accept: true });
+ let isConfirmed = await getContentProperty("isConfirmed");
+ ok(isConfirmed, "The confirm dialog was accepted");
+
+ await createConfirmDialog(Page);
+ info("Trigger another confirm in the test page");
+
+ info("Reject the dialog");
+ await Page.handleJavaScriptDialog({ accept: false });
+ isConfirmed = await getContentProperty("isConfirmed");
+ ok(!isConfirmed, "The confirm dialog was rejected");
+});
+
+function createConfirmDialog(Page) {
+ const onDialogOpen = Page.javascriptDialogOpening();
+
+ info("Trigger a confirm in the test page");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.isConfirmed = content.confirm("confirm-1234?");
+ });
+
+ return onDialogOpen;
+}
diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js b/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js
new file mode 100644
index 0000000000..c57f5d5151
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_javascriptDialog_otherTarget.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+
+// Test that javascript dialog events are emitted by the page domain only if
+// the dialog is created for the window of the target.
+add_task(async function ({ client }) {
+ const { Page } = client;
+
+ info("Enable the page domain");
+ await Page.enable();
+
+ // Add a listener for dialogs on the test page.
+ Page.javascriptDialogOpening(() => {
+ ok(false, "Should never receive this event");
+ });
+
+ info("Open another tab");
+ const otherTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ toDataURL("test-page")
+ );
+ is(gBrowser.selectedTab, otherTab, "Selected tab is now the new tab");
+
+ // Create a promise that resolve when dialog prompt is created.
+ // It will also take care of closing the dialog.
+ let onOtherPageDialog = PromptTestUtils.handleNextPrompt(
+ gBrowser.selectedBrowser,
+ { modalType: Services.prompt.MODAL_TYPE_CONTENT, promptType: "alert" },
+ { buttonNumClick: 0 }
+ );
+
+ info("Trigger an alert in the second page");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.alert("test");
+ });
+
+ info("Wait for the alert to be detected and closed");
+ await onOtherPageDialog;
+
+ info("Call bringToFront on the test page to make sure we received");
+ await Page.bringToFront();
+
+ BrowserTestUtils.removeTab(otherTab);
+});
diff --git a/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js b/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js
new file mode 100644
index 0000000000..c3678e9295
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_javascriptDialog_prompt.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for window.prompt(). Check that the dialog is correctly detected and that it can
+// be rejected or accepted, with a custom prompt text.
+add_task(async function ({ client }) {
+ const { Page } = client;
+
+ info("Enable the page domain");
+ await Page.enable();
+
+ info("Create a prompt dialog to open");
+ const { message, type } = await createPromptDialog(Page);
+
+ is(type, "prompt", "dialog event contains the correct type");
+ is(message, "prompt-1234", "dialog event contains the correct text");
+
+ info("Accept the prompt");
+ await Page.handleJavaScriptDialog({ accept: true, promptText: "some-text" });
+
+ let promptResult = await getContentProperty("promptResult");
+ is(promptResult, "some-text", "The prompt text was correctly applied");
+
+ await createPromptDialog(Page);
+ info("Trigger another prompt in the test page");
+
+ info("Reject the prompt");
+ await Page.handleJavaScriptDialog({ accept: false, promptText: "new-text" });
+
+ promptResult = await getContentProperty("promptResult");
+ ok(!promptResult, "The prompt dialog was rejected");
+});
+
+function createPromptDialog(Page) {
+ const onDialogOpen = Page.javascriptDialogOpening();
+
+ info("Trigger a prompt in the test page");
+ SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.promptResult = content.prompt("prompt-1234");
+ });
+
+ return onDialogOpen;
+}
diff --git a/remote/cdp/test/browser/page/browser_lifecycleEvent.js b/remote/cdp/test/browser/page/browser_lifecycleEvent.js
new file mode 100644
index 0000000000..da5f5da9e8
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_lifecycleEvent.js
@@ -0,0 +1,191 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+add_task(async function noEventsWhenPageDomainDisabled({ client }) {
+ await runPageLifecycleTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runPageLifecycleTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventWhenLifeCycleDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runPageLifecycleTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterLifeCycleDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+ await Page.setLifecycleEventsEnabled({ enabled: false });
+
+ await runPageLifecycleTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingToURLWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 1, async () => {
+ info("Navigate to a URL with no frames");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingToSameURLWithNoFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 1, async () => {
+ info("Navigate to the same page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventsWhenReloadingSameURLWithNoFrames({ client }) {
+ const { Page } = client;
+
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 1, async () => {
+ info("Reload page with no iframes");
+ const pageLoaded = Page.loadEventFired();
+ await Page.reload();
+ await pageLoaded;
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 3, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventsWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.setLifecycleEventsEnabled({ enabled: true });
+
+ await runPageLifecycleTest(client, 4, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runPageLifecycleTest(client, expectedEventSets, callback) {
+ const { Page } = client;
+
+ const LIFECYCLE = "Page.lifecycleEvent";
+ const LIFECYCLE_EVENTS = ["init", "DOMContentLoaded", "load"];
+
+ const expectedEventCount = expectedEventSets * LIFECYCLE_EVENTS.length;
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.lifecycleEvent,
+ eventName: LIFECYCLE,
+ messageFn: payload => {
+ return (
+ `Received "${payload.name}" ${LIFECYCLE} ` +
+ `for frame id ${payload.frameId}`
+ );
+ },
+ });
+
+ await callback();
+
+ const flattenedFrameTree = await getFlattenedFrameTree(client);
+
+ const lifecycleEvents = await history.record();
+ is(
+ lifecycleEvents.length,
+ expectedEventCount,
+ "Got expected amount of lifecycle events"
+ );
+
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ // Check lifecycle events for each frame
+ for (const frame of flattenedFrameTree.values()) {
+ info(`Check frame id ${frame.id}`);
+
+ const frameEvents = lifecycleEvents.filter(({ payload }) => {
+ return payload.frameId == frame.id;
+ });
+
+ Assert.deepEqual(
+ frameEvents.map(event => event.payload.name),
+ LIFECYCLE_EVENTS,
+ "Received various lifecycle events in the expected order"
+ );
+
+ // Check data as exposed by each of these events
+ let lastTimestamp = frameEvents[0].payload.timestamp;
+ frameEvents.forEach(({ payload }, index) => {
+ ok(
+ payload.timestamp >= lastTimestamp,
+ "timestamp succeeds the one from the former event"
+ );
+ lastTimestamp = payload.timestamp;
+
+ is(payload.loaderId, frame.loaderId, `event has expected loaderId`);
+ });
+ }
+}
diff --git a/remote/cdp/test/browser/page/browser_loadEventFired.js b/remote/cdp/test/browser/page/browser_loadEventFired.js
new file mode 100644
index 0000000000..e85b298feb
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_loadEventFired.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await runLoadEventFiredTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await runLoadEventFiredTest(client, 0, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNoFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runLoadEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with no iframes");
+ await loadURL(PAGE_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runLoadEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with iframes");
+ await loadURL(FRAMESET_MULTI_URL);
+ });
+});
+
+add_task(async function eventWhenNavigatingWithNestedFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await runLoadEventFiredTest(client, 1, async () => {
+ info("Navigate to a page with nested iframes");
+ await loadURL(FRAMESET_NESTED_URL);
+ });
+});
+
+async function runLoadEventFiredTest(client, expectedEventCount, callback) {
+ const { Page } = client;
+
+ if (![0, 1].includes(expectedEventCount)) {
+ throw new Error(`Invalid value for expectedEventCount`);
+ }
+
+ const LOAD_EVENT_FIRED = "Page.loadEventFired";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.loadEventFired,
+ eventName: LOAD_EVENT_FIRED,
+ messageFn: payload => {
+ return `Received ${LOAD_EVENT_FIRED} at time ${payload.timestamp}`;
+ },
+ });
+
+ const timeBefore = Date.now() / 1000;
+ await callback();
+ const loadEventFiredEvents = await history.record();
+ const timeAfter = Date.now() / 1000;
+
+ is(
+ loadEventFiredEvents.length,
+ expectedEventCount,
+ "Got expected amount of loadEventFired events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const timestamp = loadEventFiredEvents[0].payload.timestamp;
+ ok(
+ timestamp >= timeBefore && timestamp <= timeAfter,
+ `Timestamp ${timestamp} in expected range [${timeBefore} - ${timeAfter}]`
+ );
+}
diff --git a/remote/cdp/test/browser/page/browser_navigate.js b/remote/cdp/test/browser/page/browser_navigate.js
new file mode 100644
index 0000000000..592313a3cb
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_navigate.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testBasicNavigation({ client }) {
+ const { Page, Network } = client;
+ await Page.enable();
+ await Network.enable();
+ const loadEventFired = Page.loadEventFired();
+ const requestEvent = Network.requestWillBeSent();
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: PAGE_URL,
+ });
+ const { loaderId: requestLoaderId } = await requestEvent;
+
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(
+ loaderId,
+ requestLoaderId,
+ "Page.navigate returns same loaderId as corresponding request"
+ );
+ is(errorText, undefined, "No errorText on a successful navigation");
+
+ await loadEventFired;
+ const currentFrame = await getTopFrame(client);
+ is(frameId, currentFrame.id, "Page.navigate returns expected frameId");
+
+ is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded");
+});
+
+add_task(async function testTwoNavigations({ client }) {
+ const { Page, Network } = client;
+ await Page.enable();
+ await Network.enable();
+ let requestEvent = Network.requestWillBeSent();
+ let loadEventFired = Page.loadEventFired();
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: PAGE_URL,
+ });
+ const { loaderId: requestLoaderId } = await requestEvent;
+ await loadEventFired;
+ is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded");
+
+ loadEventFired = Page.loadEventFired();
+ requestEvent = Network.requestWillBeSent();
+ const {
+ frameId: frameId2,
+ loaderId: loaderId2,
+ errorText: errorText2,
+ } = await Page.navigate({
+ url: PAGE_URL,
+ });
+ const { loaderId: requestLoaderId2 } = await requestEvent;
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ ok(!!loaderId2, "Page.navigate returns loaderId");
+ isnot(loaderId, loaderId2, "Page.navigate returns different loaderIds");
+ is(
+ loaderId,
+ requestLoaderId,
+ "Page.navigate returns same loaderId as corresponding request"
+ );
+ is(
+ loaderId2,
+ requestLoaderId2,
+ "Page.navigate returns same loaderId as corresponding request"
+ );
+ is(errorText, undefined, "No errorText on a successful navigation");
+ is(errorText2, undefined, "No errorText on a successful navigation");
+ is(frameId, frameId2, "Page.navigate return same frameId");
+
+ await loadEventFired;
+ is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded");
+});
+
+add_task(async function testRedirect({ client }) {
+ const { Page, Network } = client;
+ const sjsURL =
+ "https://example.com/browser/remote/cdp/test/browser/page/sjs_redirect.sjs";
+ const redirectURL = `${sjsURL}?${PAGE_URL}`;
+ await Page.enable();
+ await Network.enable();
+ const requestEvent = Network.requestWillBeSent();
+ const loadEventFired = Page.loadEventFired();
+
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: redirectURL,
+ });
+ const { loaderId: requestLoaderId } = await requestEvent;
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(
+ loaderId,
+ requestLoaderId,
+ "Page.navigate returns same loaderId as original request"
+ );
+ is(errorText, undefined, "No errorText on a successful navigation");
+ ok(!!frameId, "Page.navigate returns frameId");
+
+ await loadEventFired;
+ is(gBrowser.selectedBrowser.currentURI.spec, PAGE_URL, "Expected URL loaded");
+});
+
+add_task(async function testUnknownHost({ client }) {
+ const { Page } = client;
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: "https://example-does-not-exist.com",
+ });
+ ok(!!frameId, "Page.navigate returns frameId");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(errorText, "NS_ERROR_UNKNOWN_HOST", "Failed navigation returns errorText");
+});
+
+add_task(async function testExpiredCertificate({ client }) {
+ const { Page } = client;
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: "https://expired.example.com",
+ });
+ ok(!!frameId, "Page.navigate returns frameId");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(
+ errorText,
+ "SEC_ERROR_EXPIRED_CERTIFICATE",
+ "Failed navigation returns errorText"
+ );
+});
+
+add_task(async function testUnknownCertificate({ client }) {
+ const { Page, Network } = client;
+ await Network.enable();
+ const requestEvent = Network.requestWillBeSent();
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: "https://self-signed.example.com",
+ });
+ const { loaderId: requestLoaderId } = await requestEvent;
+ ok(!!frameId, "Page.navigate returns frameId");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(
+ loaderId,
+ requestLoaderId,
+ "Page.navigate returns same loaderId as original request"
+ );
+ is(errorText, "SSL_ERROR_UNKNOWN", "Failed navigation returns errorText");
+});
+
+add_task(async function testNotFound({ client }) {
+ const { Page } = client;
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: "https://example.com/browser/remote/doesnotexist.html",
+ });
+ ok(!!frameId, "Page.navigate returns frameId");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(errorText, undefined, "No errorText on a 404");
+});
+
+add_task(async function testInvalidURL({ client }) {
+ const { Page } = client;
+ let message = "";
+ for (let url of ["blah.com", "foo", "https\n//", "http", ""]) {
+ message = "";
+ try {
+ await Page.navigate({ url });
+ } catch (e) {
+ message = e.response.message;
+ }
+ ok(message.includes("invalid URL"), `Invalid url ${url} causes error`);
+ }
+
+ for (let url of [2, {}, true]) {
+ message = "";
+ try {
+ await Page.navigate({ url });
+ } catch (e) {
+ message = e.response.message;
+ }
+ ok(
+ message.includes("string value expected"),
+ `Invalid url ${url} causes error`
+ );
+ }
+});
+
+add_task(async function testDataURL({ client }) {
+ const { Page } = client;
+ const url = toDataURL("first");
+ await Page.enable();
+ const loadEventFired = Page.loadEventFired();
+ const frameNavigatedFired = Page.frameNavigated();
+ const { frameId, loaderId, errorText } = await Page.navigate({ url });
+ is(errorText, undefined, "No errorText on a successful navigation");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+
+ await loadEventFired;
+ const { frame } = await frameNavigatedFired;
+ is(frame.loaderId, loaderId, "Page.navigate returns expected loaderId");
+ const currentFrame = await getTopFrame(client);
+ is(frameId, currentFrame.id, "Page.navigate returns expected frameId");
+ is(gBrowser.selectedBrowser.currentURI.spec, url, "Expected URL loaded");
+});
+
+add_task(async function testFileURL({ client }) {
+ const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ if (AppConstants.DEBUG) {
+ // Bug 1634695 Navigating to a file URL forces the TabSession to destroy
+ // abruptly and content domains are not properly destroyed, which creates
+ // window leaks and fails the test in DEBUG mode.
+ return;
+ }
+
+ const { Page } = client;
+ const dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append("doc_empty.html");
+
+ // The file can be a symbolic link on local build. Normalize it to make sure
+ // the path matches to the actual URI opened in the new tab.
+ dir.normalize();
+ const url = Services.io.newFileURI(dir).spec;
+ const browser = gBrowser.selectedTab.linkedBrowser;
+ const loaded = BrowserTestUtils.browserLoaded(browser, false, url);
+
+ const { /* frameId, */ loaderId, errorText } = await Page.navigate({ url });
+ is(errorText, undefined, "No errorText on a successful navigation");
+ ok(!!loaderId, "Page.navigate returns loaderId");
+
+ // Bug 1634693 Page.loadEventFired isn't emitted after file: navigation
+ await loaded;
+ is(browser.currentURI.spec, url, "Expected URL loaded");
+ // Bug 1634695 Navigating to file: returns wrong frame id and hangs
+ // content page domain methods
+ // const currentFrame = await getTopFrame(client);
+ // ok(frameId === currentFrame.id, "Page.navigate returns expected frameId");
+});
+
+add_task(async function testAbout({ client }) {
+ const { Page } = client;
+ await Page.enable();
+ let loadEventFired = Page.loadEventFired();
+ let frameNavigatedFired = Page.frameNavigated();
+ const { frameId, loaderId, errorText } = await Page.navigate({
+ url: "about:blank",
+ });
+ ok(!!loaderId, "Page.navigate returns loaderId");
+ is(errorText, undefined, "No errorText on a successful navigation");
+
+ await loadEventFired;
+ const { frame } = await frameNavigatedFired;
+ is(frame.loaderId, loaderId, "Page.navigate returns expected loaderId");
+ const currentFrame = await getTopFrame(client);
+ is(frameId, currentFrame.id, "Page.navigate returns expected frameId");
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ "about:blank",
+ "Expected URL loaded"
+ );
+});
+
+add_task(async function testSameDocumentNavigation({ client }) {
+ const { Page } = client;
+ const { frameId, loaderId } = await Page.navigate({
+ url: PAGE_URL,
+ });
+ ok(!!loaderId, "Page.navigate returns loaderId");
+
+ await Page.enable();
+ const navigatedWithinDocument = Page.navigatedWithinDocument();
+
+ info("Check that Page.navigate can navigate to an anchor");
+ const sameDocumentURL = `${PAGE_URL}#hash`;
+ const { frameId: sameDocumentFrameId, loaderId: sameDocumentLoaderId } =
+ await Page.navigate({ url: sameDocumentURL });
+ ok(
+ !sameDocumentLoaderId,
+ "Page.navigate does not return a loaderId for same document navigation"
+ );
+ is(
+ sameDocumentFrameId,
+ frameId,
+ "Page.navigate returned the expected frame id"
+ );
+
+ const { frameId: navigatedFrameId, url } = await navigatedWithinDocument;
+ is(
+ frameId,
+ navigatedFrameId,
+ "navigatedWithinDocument returns the expected frameId"
+ );
+ is(url, sameDocumentURL, "navigatedWithinDocument returns the expected url");
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ sameDocumentURL,
+ "Expected URL loaded"
+ );
+
+ info("Check that navigating to the same hash URL does not timeout");
+ const { frameId: sameHashFrameId, loaderId: sameHashLoaderId } =
+ await Page.navigate({ url: sameDocumentURL });
+ ok(
+ !sameHashLoaderId,
+ "Page.navigate does not return a loaderId for same document navigation"
+ );
+ is(sameHashFrameId, frameId, "Page.navigate returned the expected frame id");
+});
+
+async function getTopFrame(client) {
+ const frames = await getFlattenedFrameTree(client);
+ return Array.from(frames.values())[0];
+}
diff --git a/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js b/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js
new file mode 100644
index 0000000000..4a6e1b4fb6
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_navigateToHistoryEntry.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function toUnknownEntryId({ client }) {
+ const { Page } = client;
+
+ const { entries } = await Page.getNavigationHistory();
+ const ids = entries.map(entry => entry.id);
+
+ let errorThrown = "";
+ try {
+ await Page.navigateToHistoryEntry({ entryId: Math.max(...ids) + 1 });
+ } catch (e) {
+ errorThrown = e.message;
+ }
+ ok(
+ errorThrown.match(/No entry with passed id/),
+ "Unknown entry id raised error"
+ );
+});
+
+add_task(async function toSameEntry({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(1);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ const { currentIndex, entries } = await Page.getNavigationHistory();
+ await Page.navigateToHistoryEntry({ entryId: entries[currentIndex].id });
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, 0);
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ data[0].url,
+ "Expected URL loaded"
+ );
+});
+
+add_task(async function oneEntryBackInHistory({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ const { currentIndex, entries } = await Page.getNavigationHistory();
+ await Page.navigateToHistoryEntry({ entryId: entries[currentIndex - 1].id });
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, currentIndex - 1);
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ data[currentIndex - 1].url,
+ "Expected URL loaded"
+ );
+});
+
+add_task(async function oneEntryForwardInHistory({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ await gotoHistoryIndex(0);
+
+ const { currentIndex, entries } = await Page.getNavigationHistory();
+ await Page.navigateToHistoryEntry({ entryId: entries[currentIndex + 1].id });
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, currentIndex + 1);
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ data[currentIndex + 1].url,
+ "Expected URL loaded"
+ );
+});
+
+add_task(async function toFirstEntryInHistory({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ const { entries } = await Page.getNavigationHistory();
+ await Page.navigateToHistoryEntry({ entryId: entries[0].id });
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, 0);
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ data[0].url,
+ "Expected URL loaded"
+ );
+});
+
+add_task(async function toLastEntryInHistory({ client }) {
+ const { Page } = client;
+
+ const data = generateHistoryData(3);
+ for (const entry of data) {
+ await loadURL(entry.userTypedURL);
+ }
+
+ await gotoHistoryIndex(0);
+
+ const { entries } = await Page.getNavigationHistory();
+ await Page.navigateToHistoryEntry({
+ entryId: entries[entries.length - 1].id,
+ });
+
+ const history = await Page.getNavigationHistory();
+ assertHistoryEntries(history, data, data.length - 1);
+
+ is(
+ gBrowser.selectedBrowser.currentURI.spec,
+ data[data.length - 1].url,
+ "Expected URL loaded"
+ );
+});
diff --git a/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js b/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js
new file mode 100644
index 0000000000..f9dd25d9a7
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_navigatedWithinDocument.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function noEventWhenPageDomainDisabled({ client }) {
+ await loadURL(PAGE_URL);
+ await runNavigatedWithinDocumentTest(client, 0, async () => {
+ info("Navigate to the '#hash' anchor in the page");
+ await navigateToAnchor(PAGE_URL, "hash");
+ });
+});
+
+add_task(async function noEventAfterPageDomainDisabled({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+ await Page.disable();
+
+ await loadURL(PAGE_URL);
+
+ await runNavigatedWithinDocumentTest(client, 0, async () => {
+ info("Navigate to the '#hash' anchor in the page");
+ await navigateToAnchor(PAGE_URL, "hash");
+ });
+});
+
+add_task(async function eventWhenNavigatingToHash({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await loadURL(PAGE_URL);
+
+ await runNavigatedWithinDocumentTest(client, 1, async () => {
+ info("Navigate to the '#hash' anchor in the page");
+ await navigateToAnchor(PAGE_URL, "hash");
+ });
+});
+
+add_task(async function eventWhenNavigatingToDifferentHash({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await navigateToAnchor(PAGE_URL, "hash");
+
+ await runNavigatedWithinDocumentTest(client, 1, async () => {
+ info("Navigate to the '#hash' anchor in the page");
+ await navigateToAnchor(PAGE_URL, "other-hash");
+ });
+});
+
+add_task(async function eventWhenNavigatingToHashInFrames({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ await loadURL(FRAMESET_NESTED_URL);
+
+ await runNavigatedWithinDocumentTest(client, 1, async () => {
+ info("Navigate to the '#hash' anchor in the first iframe");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const iframe = content.frames[0];
+ const baseUrl = iframe.location.href;
+ iframe.location.href = baseUrl + "#hash-first-frame";
+ });
+ });
+
+ await runNavigatedWithinDocumentTest(client, 2, async () => {
+ info("Navigate to the '#hash' anchor in the nested iframes");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const nestedFrame1 = content.frames[0].frames[0];
+ const baseUrl1 = nestedFrame1.location.href;
+ nestedFrame1.location.href = baseUrl1 + "#hash-nested-frame-1";
+ const nestedFrame2 = content.frames[0].frames[0];
+ const baseUrl2 = nestedFrame2.location.href;
+ nestedFrame2.location.href = baseUrl2 + "#hash-nested-frame-2";
+ });
+ });
+});
+
+async function runNavigatedWithinDocumentTest(
+ client,
+ expectedEventCount,
+ callback
+) {
+ const { Page } = client;
+
+ const NAVIGATED = "Page.navigatedWithinDocument";
+
+ const history = new RecordEvents(expectedEventCount);
+ history.addRecorder({
+ event: Page.navigatedWithinDocument,
+ eventName: NAVIGATED,
+ messageFn: payload => {
+ return `Received ${NAVIGATED} for frame id ${payload.frameId}`;
+ },
+ });
+
+ await callback();
+
+ const navigatedWithinDocumentEvents = await history.record();
+
+ is(
+ navigatedWithinDocumentEvents.length,
+ expectedEventCount,
+ "Got expected amount of navigatedWithinDocument events"
+ );
+ if (expectedEventCount == 0) {
+ return;
+ }
+
+ const frames = await getFlattenedFrameTree(client);
+
+ navigatedWithinDocumentEvents.forEach(({ payload }) => {
+ const { frameId, url } = payload;
+
+ const frame = frames.get(frameId);
+ ok(frame, "Returned a valid frame id");
+ is(url, frame.url, "Returned the expectedUrl");
+ });
+}
+
+function navigateToAnchor(baseUrl, hash) {
+ const url = `${baseUrl}#${hash}`;
+ const onLocationChange = BrowserTestUtils.waitForLocationChange(
+ gBrowser,
+ url
+ );
+ BrowserTestUtils.loadURIString(gBrowser.selectedBrowser, url);
+ return onLocationChange;
+}
diff --git a/remote/cdp/test/browser/page/browser_navigationEvents.js b/remote/cdp/test/browser/page/browser_navigationEvents.js
new file mode 100644
index 0000000000..1e589a3970
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_navigationEvents.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the Page navigation events
+
+const RANDOM_ID_PAGE_URL = toDataURL(
+ `<script>window.randomId = Math.random() + "-" + Date.now();</script>`
+);
+
+const promises = new Set();
+const resolutions = new Map();
+
+add_task(async function pageWithoutFrame({ client }) {
+ await loadURL(PAGE_URL);
+
+ const { Page } = client;
+
+ // turn on navigation related events, such as DOMContentLoaded et al.
+ await Page.enable();
+ info("Page domain has been enabled");
+
+ const { frameTree } = await Page.getFrameTree();
+
+ // Save the given `promise` resolution into the `promises` global Set
+ function recordPromise(name, promise) {
+ promise.then(event => {
+ info(`Received Page.${name}`);
+ resolutions.set(name, event);
+ });
+ promises.add(promise);
+ }
+ // Record all Page events that we assert in this test
+ function recordPromises() {
+ recordPromise("frameStartedLoading", Page.frameStartedLoading());
+ recordPromise("frameNavigated", Page.frameNavigated());
+ recordPromise("domContentEventFired", Page.domContentEventFired());
+ recordPromise("loadEventFired", Page.loadEventFired());
+ recordPromise("frameStoppedLoading", Page.frameStoppedLoading());
+ }
+
+ info("Test Page.navigate");
+ recordPromises();
+
+ let navigatedWithinDocumentResolved = false;
+ Page.navigatedWithinDocument().finally(
+ () => (navigatedWithinDocumentResolved = true)
+ );
+
+ const url = RANDOM_ID_PAGE_URL;
+ const { frameId } = await Page.navigate({ url });
+ info("A new page has been requested");
+
+ ok(frameId, "Page.navigate returned a frameId");
+ is(
+ frameId,
+ frameTree.frame.id,
+ "The Page.navigate's frameId is the same than getFrameTree's one"
+ );
+
+ await assertNavigationEvents({ url, frameId });
+
+ const randomId1 = await getTestTabRandomId();
+ ok(!!randomId1, "Test tab has a valid randomId");
+
+ info("Test Page.reload");
+ recordPromises();
+
+ await Page.reload();
+ info("The page has been reloaded");
+
+ await assertNavigationEvents({ url, frameId });
+
+ const randomId2 = await getTestTabRandomId();
+ ok(!!randomId2, "Test tab has a valid randomId");
+ isnot(
+ randomId2,
+ randomId1,
+ "Test tab randomId has been updated after reload"
+ );
+
+ info("Test Page.navigate with the same URL still reloads the current page");
+ recordPromises();
+
+ await Page.navigate({ url });
+ info("The page has been reloaded");
+
+ await assertNavigationEvents({ url, frameId });
+
+ const randomId3 = await getTestTabRandomId();
+ ok(!!randomId3, "Test tab has a valid randomId");
+ isnot(
+ randomId3,
+ randomId2,
+ "Test tab randomId has been updated after reload"
+ );
+
+ ok(
+ !navigatedWithinDocumentResolved,
+ "navigatedWithinDocument never resolved during the test"
+ );
+});
+
+add_task(async function pageWithSingleFrame({ client }) {
+ const { Page } = client;
+
+ await Page.enable();
+
+ // Store all frameNavigated events in an array
+ const frameNavigatedEvents = [];
+ Page.frameNavigated(e => frameNavigatedEvents.push(e));
+
+ info("Navigate to a page containing an iframe");
+ const onStoppedLoading = Page.frameStoppedLoading();
+ const { frameId } = await Page.navigate({ url: FRAMESET_SINGLE_URL });
+ await onStoppedLoading;
+
+ is(frameNavigatedEvents.length, 2, "Received 2 frameNavigated events");
+ is(
+ frameNavigatedEvents[0].frame.id,
+ frameId,
+ "Received the correct frameId for the frameNavigated event"
+ );
+});
+
+add_task(async function sameDocumentNavigation({ client }) {
+ await loadURL(PAGE_URL);
+
+ const { Page } = client;
+
+ // turn on navigation related events, such as DOMContentLoaded et al.
+ await Page.enable();
+ info("Page domain has been enabled");
+
+ const { frameTree } = await Page.getFrameTree();
+
+ info("Test Page.navigate for a same document navigation");
+ const onNavigatedWithinDocument = Page.navigatedWithinDocument();
+
+ let unexpectedEventResolved = false;
+ Promise.race([
+ Page.frameStartedLoading(),
+ Page.frameNavigated(),
+ Page.domContentEventFired(),
+ Page.loadEventFired(),
+ Page.frameStoppedLoading(),
+ ]).then(() => (unexpectedEventResolved = true));
+
+ const url = `${PAGE_URL}#some-hash`;
+ const { frameId } = await Page.navigate({ url });
+ ok(frameId, "Page.navigate returned a frameId");
+ is(
+ frameId,
+ frameTree.frame.id,
+ "The Page.navigate's frameId is the same than getFrameTree's one"
+ );
+
+ const event = await onNavigatedWithinDocument;
+ is(
+ event.frameId,
+ frameId,
+ "The navigatedWithinDocument frameId is the same as in Page.navigate"
+ );
+ is(event.url, url, "The navigatedWithinDocument url is the expected url");
+ ok(!unexpectedEventResolved, "No unexpected navigation event resolved.");
+});
+
+async function assertNavigationEvents({ url, frameId }) {
+ // Wait for all the promises to resolve
+ await Promise.all(promises);
+
+ // Assert the order in which they resolved
+ const expectedResolutions = [
+ "frameStartedLoading",
+ "frameNavigated",
+ "domContentEventFired",
+ "loadEventFired",
+ "frameStoppedLoading",
+ ];
+ Assert.deepEqual(
+ [...resolutions.keys()],
+ expectedResolutions,
+ "Received various Page navigation events in the expected order"
+ );
+
+ // Now assert the data exposed by each of these events
+ const frameStartedLoading = resolutions.get("frameStartedLoading");
+ is(
+ frameStartedLoading.frameId,
+ frameId,
+ "frameStartedLoading frameId is the same one"
+ );
+
+ const frameNavigated = resolutions.get("frameNavigated");
+ ok(
+ !frameNavigated.frame.parentId,
+ "frameNavigated is for the top level document and has a null parentId"
+ );
+ is(frameNavigated.frame.id, frameId, "frameNavigated id is the right one");
+ is(
+ frameNavigated.frame.name,
+ undefined,
+ "frameNavigated name isn't implemented yet"
+ );
+ is(frameNavigated.frame.url, url, "frameNavigated url is the right one");
+
+ const frameStoppedLoading = resolutions.get("frameStoppedLoading");
+ is(
+ frameStoppedLoading.frameId,
+ frameId,
+ "frameStoppedLoading frameId is the same one"
+ );
+
+ promises.clear();
+ resolutions.clear();
+}
+
+async function getTestTabRandomId() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.randomId;
+ });
+}
diff --git a/remote/cdp/test/browser/page/browser_printToPDF.js b/remote/cdp/test/browser/page/browser_printToPDF.js
new file mode 100644
index 0000000000..fed1a6162e
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_printToPDF.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const DOC = toDataURL("<div style='background-color: green'>Hello world</div>");
+
+add_task(async function transferModes({ client }) {
+ const { IO, Page } = client;
+ await loadURL(DOC);
+
+ // as base64 encoded data
+ const base64 = await Page.printToPDF({ transferMode: "ReturnAsBase64" });
+ is(base64.stream, null, "No stream handle is returned");
+ ok(!!base64.data, "Base64 encoded data is returned");
+ verifyPDF(atob(base64.data).trimEnd());
+
+ // defaults to base64 encoded data
+ const defaults = await Page.printToPDF();
+ is(defaults.stream, null, "By default no stream handle is returned");
+ ok(!!defaults.data, "By default base64 encoded data is returned");
+ verifyPDF(atob(defaults.data).trimEnd());
+
+ // unknown transfer modes default to base64
+ const fallback = await Page.printToPDF({ transferMode: "ReturnAsFoo" });
+ is(fallback.stream, null, "Unknown mode doesn't return a stream");
+ ok(!!fallback.data, "Unknown mode defaults to base64 encoded data");
+ verifyPDF(atob(fallback.data).trimEnd());
+
+ // as stream handle
+ const stream = await Page.printToPDF({ transferMode: "ReturnAsStream" });
+ ok(!!stream.stream, "Stream handle is returned");
+ is(stream.data, null, "No base64 encoded data is returned");
+ let streamData = "";
+
+ while (true) {
+ const { data, base64Encoded, eof } = await IO.read({
+ handle: stream.stream,
+ });
+ streamData += base64Encoded ? atob(data) : data;
+ if (eof) {
+ await IO.close({ handle: stream.stream });
+ break;
+ }
+ }
+
+ verifyPDF(streamData.trimEnd());
+});
+
+function verifyPDF(data) {
+ is(data.slice(0, 5), "%PDF-", "Decoded data starts with the PDF signature");
+ is(data.slice(-5), "%%EOF", "Decoded data ends with the EOF flag");
+}
diff --git a/remote/cdp/test/browser/page/browser_reload.js b/remote/cdp/test/browser/page/browser_reload.js
new file mode 100644
index 0000000000..0872337551
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_reload.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testReload({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("halløj"));
+
+ info("Reloading document");
+ await Page.enable();
+ const loaded = Page.loadEventFired();
+ await Page.reload();
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ ok(!content.docShell.isForceReloading, "Document is not force-reloaded");
+ });
+});
+
+add_task(async function testReloadIgnoreCache({ client }) {
+ const { Page } = client;
+ await loadURL(toDataURL("halløj"));
+
+ info("Force-reloading document");
+ await Page.enable();
+ const loaded = Page.loadEventFired();
+ await Page.reload({ ignoreCache: true });
+ await loaded;
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ ok(content.docShell.isForceReloading, "Document is force-reloaded");
+ });
+});
diff --git a/remote/cdp/test/browser/page/browser_runtimeEvents.js b/remote/cdp/test/browser/page/browser_runtimeEvents.js
new file mode 100644
index 0000000000..7f6d7ec926
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_runtimeEvents.js
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Assert the order of Runtime.executionContextDestroyed,
+// Page.frameNavigated, and Runtime.executionContextCreated
+
+add_task(async function testCDP({ client }) {
+ await loadURL(PAGE_URL);
+
+ const { Page, Runtime } = client;
+
+ const events = [];
+ function assertReceivedEvents(expected, message) {
+ Assert.deepEqual(events, expected, message);
+ // Empty the list of received events
+ events.splice(0);
+ }
+ Page.frameNavigated(() => {
+ events.push("frameNavigated");
+ });
+ Runtime.executionContextCreated(() => {
+ events.push("executionContextCreated");
+ });
+ Runtime.executionContextDestroyed(() => {
+ events.push("executionContextDestroyed");
+ });
+
+ // turn on navigation related events, such as DOMContentLoaded et al.
+ await Page.enable();
+ info("Page domain has been enabled");
+
+ const onExecutionContextCreated = Runtime.executionContextCreated();
+ await Runtime.enable();
+ info("Runtime domain has been enabled");
+
+ // Runtime.enable will dispatch `executionContextCreated` for the existing document
+ let { context } = await onExecutionContextCreated;
+ ok(!!context.id, `The execution context has an id ${context.id}`);
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ ok(!!context.auxData.frameId, "The execution context has a frame id set");
+
+ assertReceivedEvents(
+ ["executionContextCreated"],
+ "Received only executionContextCreated event after Runtime.enable call"
+ );
+
+ const { frameTree } = await Page.getFrameTree();
+ is(
+ frameTree.frame.id,
+ context.auxData.frameId,
+ "getFrameTree and executionContextCreated refers about the same frame Id"
+ );
+
+ const onFrameNavigated = Page.frameNavigated();
+ const onExecutionContextDestroyed = Runtime.executionContextDestroyed();
+ const onExecutionContextCreated2 = Runtime.executionContextCreated();
+ const url = toDataURL("test-page");
+ const { frameId } = await Page.navigate({ url });
+ info("A new page has been requested");
+ ok(frameId, "Page.navigate returned a frameId");
+ is(
+ frameId,
+ frameTree.frame.id,
+ "The Page.navigate's frameId is the same than getFrameTree's one"
+ );
+
+ const frameNavigated = await onFrameNavigated;
+ ok(
+ !frameNavigated.frame.parentId,
+ "frameNavigated is for the top level document and has a null parentId"
+ );
+ is(
+ frameNavigated.frame.id,
+ frameId,
+ "frameNavigated id is the same than the one returned by Page.navigate"
+ );
+ is(
+ frameNavigated.frame.name,
+ undefined,
+ "frameNavigated name isn't implemented yet"
+ );
+ is(
+ frameNavigated.frame.url,
+ url,
+ "frameNavigated url is the same being given to Page.navigate"
+ );
+
+ const { executionContextId } = await onExecutionContextDestroyed;
+ ok(executionContextId, "The destroyed event reports an id");
+ is(
+ executionContextId,
+ context.id,
+ "The destroyed event is for the first reported execution context"
+ );
+
+ ({ context } = await onExecutionContextCreated2);
+ ok(!!context.id, "The execution context has an id");
+ ok(context.auxData.isDefault, "The execution context is the default one");
+ is(
+ context.auxData.frameId,
+ frameId,
+ "The execution context frame id is the same " +
+ "the one returned by Page.navigate"
+ );
+
+ isnot(
+ executionContextId,
+ context.id,
+ "The destroyed id is different from the created one"
+ );
+
+ assertReceivedEvents(
+ ["executionContextDestroyed", "frameNavigated", "executionContextCreated"],
+ "Received frameNavigated between the two execution context events during navigation to another URL"
+ );
+});
diff --git a/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js b/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js
new file mode 100644
index 0000000000..274119ffd4
--- /dev/null
+++ b/remote/cdp/test/browser/page/browser_scriptToEvaluateOnNewDocument.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test Page.addScriptToEvaluateOnNewDocument and Page.removeScriptToEvaluateOnNewDocument
+//
+// TODO Bug 1601695 - Schedule script evaluation and check for correct frame id
+
+const WORLD = "testWorld";
+
+add_task(async function uniqueIdForAddedScripts({ client }) {
+ const { Page, Runtime } = client;
+
+ await loadURL(PAGE_URL);
+
+ const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ });
+ is(typeof id1, "string", "Script id should be a string");
+ ok(id1.length, "Script id is non-empty");
+
+ const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ });
+ ok(id2.length, "Script id is non-empty");
+ isnot(id1, id2, "Two scripts should have different ids");
+
+ await Runtime.enable();
+
+ // flush event for PAGE_URL default context
+ await Runtime.executionContextCreated();
+ await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL, []);
+});
+
+add_task(async function addScriptAfterNavigation({ client }) {
+ const { Page } = client;
+
+ await loadURL(PAGE_URL);
+
+ const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ });
+ is(typeof id1, "string", "Script id should be a string");
+ ok(id1.length, "Script id is non-empty");
+
+ await loadURL(PAGE_FRAME_URL);
+
+ const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 2;",
+ });
+ ok(id2.length, "Script id is non-empty");
+ isnot(id1, id2, "Two scripts should have different ids");
+});
+
+add_task(async function addWithIsolatedWorldAndNavigate({ client }) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+ await Runtime.enable();
+
+ const contextsCreated = recordContextCreated(Runtime, 3);
+
+ const loadEventFired = Page.loadEventFired();
+ const { frameId } = await Page.navigate({ url: PAGE_URL });
+ await loadEventFired;
+
+ // flush context-created events for the steps above
+ await contextsCreated;
+
+ await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ worldName: WORLD,
+ });
+
+ const isolatedId = await Page.createIsolatedWorld({
+ frameId,
+ worldName: WORLD,
+ grantUniversalAccess: true,
+ });
+
+ const contexts = await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL);
+ isnot(contexts[1].id, isolatedId, "The context has a new id");
+});
+
+add_task(async function addWithIsolatedWorldNavigateTwice({ client }) {
+ const { Page, Runtime } = client;
+
+ await Runtime.enable();
+
+ await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ worldName: WORLD,
+ });
+
+ await checkIsolatedContextAfterLoad(client, PAGE_URL);
+ await checkIsolatedContextAfterLoad(client, PAGE_FRAME_URL);
+});
+
+add_task(async function addTwoScriptsWithIsolatedWorld({ client }) {
+ const { Page, Runtime } = client;
+
+ await Runtime.enable();
+
+ const names = [WORLD, "A_whole_new_world"];
+ await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 1;",
+ worldName: names[0],
+ });
+ await Page.addScriptToEvaluateOnNewDocument({
+ source: "1 + 8;",
+ worldName: names[1],
+ });
+
+ await checkIsolatedContextAfterLoad(client, PAGE_URL, names);
+});
+
+function recordContextCreated(Runtime, expectedCount) {
+ return new Promise(resolve => {
+ const ctx = [];
+ const unsubscribe = Runtime.executionContextCreated(payload => {
+ ctx.push(payload.context);
+ info(
+ `Runtime.executionContextCreated: ${payload.context.auxData.type} ` +
+ `(${payload.context.origin})`
+ );
+ if (ctx.length > expectedCount) {
+ unsubscribe();
+ resolve(ctx);
+ }
+ });
+ timeoutPromise(1000).then(() => {
+ unsubscribe();
+ resolve(ctx);
+ });
+ });
+}
+
+async function checkIsolatedContextAfterLoad(client, url, names = [WORLD]) {
+ const { Page, Runtime } = client;
+
+ await Page.enable();
+
+ // At least the default context will get created
+ const expected = names.length + 1;
+
+ const contextsCreated = recordContextCreated(Runtime, expected);
+ const frameNavigated = Page.frameNavigated();
+ const { frameId } = await Page.navigate({ url });
+ await frameNavigated;
+ const contexts = await contextsCreated;
+
+ is(contexts.length, expected, "Expected number of contexts got created");
+ is(contexts[0].auxData.frameId, frameId, "Expected frame id found");
+ is(contexts[0].auxData.isDefault, true, "Got default context");
+ is(contexts[0].auxData.type, "default", "Got default context");
+ is(contexts[0].name, "", "Get context with empty name");
+
+ names.forEach((name, index) => {
+ is(contexts[index + 1].name, name, "Get context with expected name");
+ is(contexts[index + 1].auxData.frameId, frameId, "Expected frame id found");
+ is(contexts[index + 1].auxData.isDefault, false, "Got isolated context");
+ is(contexts[index + 1].auxData.type, "isolated", "Got isolated context");
+ });
+
+ return contexts;
+}
diff --git a/remote/cdp/test/browser/page/doc_empty.html b/remote/cdp/test/browser/page/doc_empty.html
new file mode 100644
index 0000000000..e59d2d8901
--- /dev/null
+++ b/remote/cdp/test/browser/page/doc_empty.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Empty page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/page/doc_frame.html b/remote/cdp/test/browser/page/doc_frame.html
new file mode 100644
index 0000000000..e2efd61554
--- /dev/null
+++ b/remote/cdp/test/browser/page/doc_frame.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frame page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/page/doc_frameset_multi.html b/remote/cdp/test/browser/page/doc_frameset_multi.html
new file mode 100644
index 0000000000..dd59a60431
--- /dev/null
+++ b/remote/cdp/test/browser/page/doc_frameset_multi.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frameset with multiple frames</title>
+</head>
+<body>
+ <iframe src="doc_empty.html"></iframe>
+ <iframe src="doc_frame.html"></iframe>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/page/doc_frameset_nested.html b/remote/cdp/test/browser/page/doc_frameset_nested.html
new file mode 100644
index 0000000000..bd0b4b48c9
--- /dev/null
+++ b/remote/cdp/test/browser/page/doc_frameset_nested.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frameset with nested frames</title>
+</head>
+<body>
+ <iframe src="doc_frameset_multi.html"></iframe>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/page/doc_frameset_single.html b/remote/cdp/test/browser/page/doc_frameset_single.html
new file mode 100644
index 0000000000..2ad56a140e
--- /dev/null
+++ b/remote/cdp/test/browser/page/doc_frameset_single.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Frameset with a single frame</title>
+</head>
+<body>
+ <iframe src="doc_frame.html"></iframe>
+</body>
+</html>
diff --git a/remote/cdp/test/browser/page/head.js b/remote/cdp/test/browser/page/head.js
new file mode 100644
index 0000000000..46a4bdc21b
--- /dev/null
+++ b/remote/cdp/test/browser/page/head.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/remote/cdp/test/browser/head.js",
+ this
+);
+
+const { PollPromise } = ChromeUtils.importESModule(
+ "chrome://remote/content/shared/Sync.sys.mjs"
+);
+
+const BASE_ORIGIN = "https://example.com";
+const BASE_PATH = `${BASE_ORIGIN}/browser/remote/cdp/test/browser/page`;
+const FRAMESET_MULTI_URL = `${BASE_PATH}/doc_frameset_multi.html`;
+const FRAMESET_NESTED_URL = `${BASE_PATH}/doc_frameset_nested.html`;
+const FRAMESET_SINGLE_URL = `${BASE_PATH}/doc_frameset_single.html`;
+const PAGE_FRAME_URL = `${BASE_PATH}/doc_frame.html`;
+const PAGE_URL = `${BASE_PATH}/doc_empty.html`;
+
+const TIMEOUT_SET_HISTORY_INDEX = 1000;
+
+function assertHistoryEntries(history, expectedData, expectedIndex) {
+ const { currentIndex, entries } = history;
+
+ is(currentIndex, expectedIndex, "Got expected current index");
+ is(
+ entries.length,
+ expectedData.length,
+ "Found expected count of history entries"
+ );
+
+ entries.forEach((entry, index) => {
+ ok(!!entry.id, "History entry has an id set");
+ is(
+ entry.url,
+ expectedData[index].url,
+ "History entry has the correct URL set"
+ );
+ is(
+ entry.userTypedURL,
+ expectedData[index].userTypedURL,
+ "History entry has the correct user typed URL set"
+ );
+ is(
+ entry.title,
+ expectedData[index].title,
+ "History entry has the correct title set"
+ );
+ });
+}
+
+function generateHistoryData(count) {
+ const data = [];
+
+ for (let index = 0; index < count; index++) {
+ const url = toDataURL(`<head><title>Test ${index + 1}</title></head>`);
+ data.push({
+ url,
+ userTypedURL: url,
+ title: `Test ${index + 1}`,
+ });
+ }
+
+ return data;
+}
+
+async function getContentSize() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const docEl = content.document.documentElement;
+
+ return {
+ x: 0,
+ y: 0,
+ width: docEl.scrollWidth,
+ height: docEl.scrollHeight,
+ };
+ });
+}
+
+async function getViewportSize() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return {
+ x: content.pageXOffset,
+ y: content.pageYOffset,
+ width: content.innerWidth,
+ height: content.innerHeight,
+ };
+ });
+}
+
+function getCurrentHistoryIndex() {
+ return new Promise(resolve => {
+ SessionStore.getSessionHistory(window.gBrowser.selectedTab, history => {
+ resolve(history.index);
+ });
+ });
+}
+
+async function gotoHistoryIndex(index) {
+ gBrowser.gotoIndex(index);
+
+ // On some platforms the requested index isn't set immediately.
+ await PollPromise(
+ async (resolve, reject) => {
+ const currentIndex = await getCurrentHistoryIndex();
+ if (currentIndex == index) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ { timeout: TIMEOUT_SET_HISTORY_INDEX }
+ );
+}
diff --git a/remote/cdp/test/browser/page/sjs_redirect.sjs b/remote/cdp/test/browser/page/sjs_redirect.sjs
new file mode 100644
index 0000000000..b3dbf44f53
--- /dev/null
+++ b/remote/cdp/test/browser/page/sjs_redirect.sjs
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}