diff options
Diffstat (limited to 'remote/test/browser/page')
30 files changed, 3921 insertions, 0 deletions
diff --git a/remote/test/browser/page/browser.ini b/remote/test/browser/page/browser.ini new file mode 100644 index 0000000000..30f5638e7e --- /dev/null +++ b/remote/test/browser/page/browser.ini @@ -0,0 +1,39 @@ +[DEFAULT] +tags = remote +subsuite = remote +prefs = + remote.enabled=true +support-files = + !/remote/test/browser/chrome-remote-interface.js + !/remote/test/browser/head.js + head.js + doc_empty.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] +[browser_loadEventFired.js] +[browser_navigate.js] +[browser_navigateToHistoryEntry.js] +[browser_navigationEvents.js] +[browser_printToPDF.js] +[browser_reload.js] +[browser_runtimeEvents.js] +[browser_scriptToEvaluateOnNewDocument.js] +skip-if = socketprocess_networking diff --git a/remote/test/browser/page/browser_bringToFront.js b/remote/test/browser/page/browser_bringToFront.js new file mode 100644 index 0000000000..ed00071922 --- /dev/null +++ b/remote/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/test/browser/page/browser_captureScreenshot.js b/remote/test/browser/page/browser_captureScreenshot.js new file mode 100644 index 0000000000..075456955d --- /dev/null +++ b/remote/test/browser/page/browser_captureScreenshot.js @@ -0,0 +1,563 @@ +/* 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.devicePixelRatio; + }); +} + +async function setDevicePixelRatio(dppx) { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [dppx], function(dppx) { + content.docShell.contentViewer.overrideDPPX = dppx; + is(content.devicePixelRatio, dppx, "devicePixelRatio override set"); + }); +} + +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/test/browser/page/browser_createIsolatedWorld.js b/remote/test/browser/page/browser_createIsolatedWorld.js new file mode 100644 index 0000000000..8272beb044 --- /dev/null +++ b/remote/test/browser/page/browser_createIsolatedWorld.js @@ -0,0 +1,494 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test Page.createIsolatedWorld + +const DOC = toDataURL("default-test-page"); +const DOC_IFRAME = toDataURL(`<iframe src="data:text/html,${DOC}"></iframe>`); + +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: DOC }); + 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: DOC }); + await loadEvent; + + let errorThrown = ""; + try { + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEventOrder({ 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: DOC }); + await loadEvent; + + await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_2, + grantUniversalAccess: true, + }); + await assertEventOrder({ 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: DOC }); + await loadEvent; + + const { executionContextId: isolatedId } = await Page.createIsolatedWorld({ + frameId, + worldName: WORLD_NAME_1, + grantUniversalAccess: true, + }); + await assertEventOrder({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, DOC + CREATED, // isolated, DOC + ], + }); + + 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, DOC, "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: DOC }); + await frameNavigated; + + await assertEventOrder({ + history, + expectedEvents: [ + DESTROYED, // default, about:blank + DESTROYED, // isolated, about:blank + CLEARED, + CREATED, // default, DOC + ], + }); + + 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: DOC_IFRAME }); + 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 assertEventOrder({ + history: historyTo, + expectedEvents: [ + DESTROYED, // default, about:blank + CREATED, // default, DOC_IFRAME + CREATED, // default, DOC + CREATED, // isolated, DOC_IFRAME + CREATED, // isolated, DOC + ], + }); + + 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: DOC }); + await loadEventFrom; + + await assertEventOrder({ + history: historyFrom, + expectedEvents: [ + DESTROYED, // default, DOC + DESTROYED, // isolated, DOC + DESTROYED, // default, DOC_IFRAME + DESTROYED, // isolated, DOC_IFRAME + CREATED, // default, DOC + ], + }); + + 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 assertEventOrder(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, + expectedEvents, + "Received Runtime context events in expected order" + ); +} diff --git a/remote/test/browser/page/browser_domContentEventFired.js b/remote/test/browser/page/browser_domContentEventFired.js new file mode 100644 index 0000000000..9959f711be --- /dev/null +++ b/remote/test/browser/page/browser_domContentEventFired.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runContentEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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 timeStart = Date.now() / 1000; + await callback(); + const domContentEventFiredEvents = await history.record(); + const timeEnd = 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 >= timeStart && timestamp <= timeEnd, + `Timestamp in expected range [${timeStart} - ${timeEnd}]` + ); +} diff --git a/remote/test/browser/page/browser_frameAttached.js b/remote/test/browser/page/browser_frameAttached.js new file mode 100644 index 0000000000..bb181feadb --- /dev/null +++ b/remote/test/browser/page/browser_frameAttached.js @@ -0,0 +1,147 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + + await runFrameAttachedTest(client, 0, async () => { + info("Navigate to a page with no iframes"); + await loadURL(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +add_task(async function eventWhenAttachingFrame({ client }) { + const { Page } = client; + + await Page.enable(); + + await runFrameAttachedTest(client, 1, async () => { + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => { + const frame = content.document.createElement("iframe"); + frame.src = "data:text/html,frame content"; + 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/test/browser/page/browser_frameDetached.js b/remote/test/browser/page/browser_frameDetached.js new file mode 100644 index 0000000000..659898c283 --- /dev/null +++ b/remote/test/browser/page/browser_frameDetached.js @@ -0,0 +1,180 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +// 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(DOC_IFRAME_NESTED); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away from a page with an iframe"); + await loadURL(DOC); + }); +}); + +add_task(async function noEventAfterPageDomainDisabled({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + await Page.disable(); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function noEventWhenNavigatingWithNoFrames({ client }) { + const { Page } = client; + + await Page.enable(); + + info("Navigate to a page with no iframes"); + await loadURL(DOC); + + await runFrameDetachedTest(client, 0, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + + await Page.enable(); + + await runFrameDetachedTest(client, 2, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenNavigatingWithNestedFrames({ client }) { + const { Page } = client; + + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + + await Page.enable(); + + await runFrameDetachedTest(client, 3, async () => { + info("Navigate away to a page with no iframes"); + await loadURL(DOC); + }); +}); + +add_task(async function eventWhenDetachingFrame({ client }) { + const { Page } = client; + + info("Navigate to a page with iframes"); + await loadURL(DOC_IFRAME_MULTI); + + 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(DOC_IFRAME_NESTED); + + 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/test/browser/page/browser_frameNavigated.js b/remote/test/browser/page/browser_frameNavigated.js new file mode 100644 index 0000000000..a74bb2449f --- /dev/null +++ b/remote/test/browser/page/browser_frameNavigated.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameNavigatedTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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/test/browser/page/browser_frameStartedLoading.js b/remote/test/browser/page/browser_frameStartedLoading.js new file mode 100644 index 0000000000..e8de1339b2 --- /dev/null +++ b/remote/test/browser/page/browser_frameStartedLoading.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStartedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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/test/browser/page/browser_frameStoppedLoading.js b/remote/test/browser/page/browser_frameStoppedLoading.js new file mode 100644 index 0000000000..31287eadc7 --- /dev/null +++ b/remote/test/browser/page/browser_frameStoppedLoading.js @@ -0,0 +1,113 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runFrameStoppedLoadingTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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/test/browser/page/browser_getFrameTree.js b/remote/test/browser/page/browser_getFrameTree.js new file mode 100644 index 0000000000..b7055b8d57 --- /dev/null +++ b/remote/test/browser/page/browser_getFrameTree.js @@ -0,0 +1,158 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function pageWithoutFrames({ client }) { + const { Page } = client; + + info("Navigate to a page without a frame"); + await loadURL(DOC); + + 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(DOC_IFRAME_MULTI); + + 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(DOC_IFRAME_NESTED); + + 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/test/browser/page/browser_getLayoutMetrics.js b/remote/test/browser/page/browser_getLayoutMetrics.js new file mode 100644 index 0000000000..db8b3e8f3c --- /dev/null +++ b/remote/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/test/browser/page/browser_getNavigationHistory.js b/remote/test/browser/page/browser_getNavigationHistory.js new file mode 100644 index 0000000000..27f9e30c94 --- /dev/null +++ b/remote/test/browser/page/browser_getNavigationHistory.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { SessionStore } = ChromeUtils.import( + "resource:///modules/sessionstore/SessionStore.jsm" +); + +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 = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + const sjsURL = + "http://example.com/browser/remote/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/test/browser/page/browser_javascriptDialog_alert.js b/remote/test/browser/page/browser_javascriptDialog_alert.js new file mode 100644 index 0000000000..51cb764e7c --- /dev/null +++ b/remote/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/test/browser/page/browser_javascriptDialog_beforeunload.js b/remote/test/browser/page/browser_javascriptDialog_beforeunload.js new file mode 100644 index 0000000000..18bfcba201 --- /dev/null +++ b/remote/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/test/browser/page/browser_javascriptDialog_confirm.js b/remote/test/browser/page/browser_javascriptDialog_confirm.js new file mode 100644 index 0000000000..da126ecc71 --- /dev/null +++ b/remote/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/test/browser/page/browser_javascriptDialog_otherTarget.js b/remote/test/browser/page/browser_javascriptDialog_otherTarget.js new file mode 100644 index 0000000000..60b49719b1 --- /dev/null +++ b/remote/test/browser/page/browser_javascriptDialog_otherTarget.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// 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. + const onOtherPageDialog = new Promise(r => { + Services.obs.addObserver(function onDialogLoaded(promptContainer) { + Services.obs.removeObserver(onDialogLoaded, "tabmodal-dialog-loaded"); + promptContainer.querySelector(".tabmodalprompt-button0").click(); + r(); + }, "tabmodal-dialog-loaded"); + }); + + 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/test/browser/page/browser_javascriptDialog_prompt.js b/remote/test/browser/page/browser_javascriptDialog_prompt.js new file mode 100644 index 0000000000..5cf8cff58a --- /dev/null +++ b/remote/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/test/browser/page/browser_lifecycleEvent.js b/remote/test/browser/page/browser_lifecycleEvent.js new file mode 100644 index 0000000000..42e80583b3 --- /dev/null +++ b/remote/test/browser/page/browser_lifecycleEvent.js @@ -0,0 +1,206 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +const PAGE_URL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + +add_task(async function noEventsWhenPageDomainDisabled({ client }) { + await runPageLifecycleTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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 = lifecycleEvents[0].payload.timestamp; + lifecycleEvents.forEach(({ payload }, index) => { + ok( + payload.timestamp >= lastTimestamp, + "timestamp succeeds the one from the former event" + ); + lastTimestamp = payload.timestamp; + + // Bug 1632007 return a loaderId for data: url + if (frame.url.startsWith("data:")) { + todo(!!payload.loaderId, "Event has a loaderId"); + } else { + is(payload.loaderId, frame.loaderId, `event has expected loaderId`); + } + }); + } +} diff --git a/remote/test/browser/page/browser_loadEventFired.js b/remote/test/browser/page/browser_loadEventFired.js new file mode 100644 index 0000000000..48ba83eefe --- /dev/null +++ b/remote/test/browser/page/browser_loadEventFired.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DOC = toDataURL("<div>foo</div>"); +const DOC_IFRAME_MULTI = toDataURL(` + <iframe src='data:text/html,foo'></iframe> + <iframe src='data:text/html,bar'></iframe> +`); +const DOC_IFRAME_NESTED = toDataURL(` + <iframe src="${DOC_IFRAME_MULTI}"></iframe> +`); + +add_task(async function noEventWhenPageDomainDisabled({ client }) { + await runLoadEventFiredTest(client, 0, async () => { + info("Navigate to a page with nested iframes"); + await loadURL(DOC_IFRAME_NESTED); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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(DOC); + }); +}); + +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(DOC_IFRAME_MULTI); + }); +}); + +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(DOC_IFRAME_NESTED); + }); +}); + +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 timeStart = Date.now() / 1000; + await callback(); + const loadEventFiredEvents = await history.record(); + const timeEnd = 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 >= timeStart && timestamp <= timeEnd, + `Timestamp in expected range [${timeStart} - ${timeEnd}]` + ); +} diff --git a/remote/test/browser/page/browser_navigate.js b/remote/test/browser/page/browser_navigate.js new file mode 100644 index 0000000000..d377f353d5 --- /dev/null +++ b/remote/test/browser/page/browser_navigate.js @@ -0,0 +1,264 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const pageEmptyURL = + "http://example.com/browser/remote/test/browser/page/doc_empty.html"; + +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: pageEmptyURL, + }); + 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, + pageEmptyURL, + "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: pageEmptyURL, + }); + const { loaderId: requestLoaderId } = await requestEvent; + await loadEventFired; + is( + gBrowser.selectedBrowser.currentURI.spec, + pageEmptyURL, + "Expected URL loaded" + ); + + loadEventFired = Page.loadEventFired(); + requestEvent = Network.requestWillBeSent(); + const { + frameId: frameId2, + loaderId: loaderId2, + errorText: errorText2, + } = await Page.navigate({ + url: pageEmptyURL, + }); + 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, + pageEmptyURL, + "Expected URL loaded" + ); +}); + +add_task(async function testRedirect({ client }) { + const { Page, Network } = client; + const sjsURL = + "http://example.com/browser/remote/test/browser/page/sjs_redirect.sjs"; + const redirectURL = `${sjsURL}?${pageEmptyURL}`; + 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, + pageEmptyURL, + "Expected URL loaded" + ); +}); + +add_task(async function testUnknownHost({ client }) { + const { Page } = client; + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "http://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: "http://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 { frameId, loaderId, errorText } = await Page.navigate({ url }); + is(errorText, undefined, "No errorText on a successful navigation"); + todo(!!loaderId, "Page.navigate returns loaderId"); + + await loadEventFired; + 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 { 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"); + todo(!!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(); + const { frameId, loaderId, errorText } = await Page.navigate({ + url: "about:blank", + }); + todo(!!loaderId, "Page.navigate returns loaderId"); + 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, + "about:blank", + "Expected URL loaded" + ); +}); + +async function getTopFrame(client) { + const frames = await getFlattenedFrameTree(client); + return Array.from(frames.values())[0]; +} diff --git a/remote/test/browser/page/browser_navigateToHistoryEntry.js b/remote/test/browser/page/browser_navigateToHistoryEntry.js new file mode 100644 index 0000000000..4a6e1b4fb6 --- /dev/null +++ b/remote/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/test/browser/page/browser_navigationEvents.js b/remote/test/browser/page/browser_navigationEvents.js new file mode 100644 index 0000000000..ab79d209f2 --- /dev/null +++ b/remote/test/browser/page/browser_navigationEvents.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test the Page navigation events + +const INITIAL_DOC = toDataURL("default-test-page"); +const IFRAME_DOC = toDataURL( + `<iframe src="data:text/html,somecontent"></iframe>` +); +const RANDOM_ID_DOC = 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(INITIAL_DOC); + + 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("navigatedWithinDocument", Page.navigatedWithinDocument()); + recordPromise("frameStoppedLoading", Page.frameStoppedLoading()); + } + + info("Test Page.navigate"); + recordPromises(); + + const url = RANDOM_ID_DOC; + 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" + ); +}); + +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: IFRAME_DOC }); + await onStoppedLoading; + + is(frameNavigatedEvents.length, 2, "Received 2 frameNavigated events"); + is( + frameNavigatedEvents[0].frame.id, + frameId, + "Received the correct frameId for the frameNavigated event" + ); +}); + +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", + "navigatedWithinDocument", + "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 navigatedWithinDocument = resolutions.get("navigatedWithinDocument"); + is( + navigatedWithinDocument.frameId, + frameId, + "navigatedWithinDocument frameId is the same one" + ); + is( + navigatedWithinDocument.url, + url, + "navigatedWithinDocument url is the same 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/test/browser/page/browser_printToPDF.js b/remote/test/browser/page/browser_printToPDF.js new file mode 100644 index 0000000000..fed1a6162e --- /dev/null +++ b/remote/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/test/browser/page/browser_reload.js b/remote/test/browser/page/browser_reload.js new file mode 100644 index 0000000000..0872337551 --- /dev/null +++ b/remote/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/test/browser/page/browser_runtimeEvents.js b/remote/test/browser/page/browser_runtimeEvents.js new file mode 100644 index 0000000000..8869ddad59 --- /dev/null +++ b/remote/test/browser/page/browser_runtimeEvents.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global getCDP */ + +// Assert the order of Runtime.executionContextDestroyed, +// Page.frameNavigated, and Runtime.executionContextCreated + +const TEST_DOC = toDataURL("default-test-page"); + +add_task(async function testCDP({ client }) { + await loadURL(TEST_DOC); + + 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/test/browser/page/browser_scriptToEvaluateOnNewDocument.js b/remote/test/browser/page/browser_scriptToEvaluateOnNewDocument.js new file mode 100644 index 0000000000..39feb77ffc --- /dev/null +++ b/remote/test/browser/page/browser_scriptToEvaluateOnNewDocument.js @@ -0,0 +1,172 @@ +/* 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 DOC = toDataURL("default-test-page"); +const WORLD = "testWorld"; + +add_task(async function uniqueIdForAddedScripts({ client }) { + const { Page, Runtime } = client; + + await loadURL(DOC); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length > 0, "Script id is non-empty"); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + ok(id2.length > 0, "Script id is non-empty"); + isnot(id1, id2, "Two scripts should have different ids"); + + await Runtime.enable(); + + // flush event for DOC default context + await Runtime.executionContextCreated(); + await checkIsolatedContextAfterLoad(client, toDataURL("<p>Hello"), []); +}); + +add_task(async function addScriptAfterNavigation({ client }) { + const { Page } = client; + + await loadURL(DOC); + + const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 1;", + }); + is(typeof id1, "string", "Script id should be a string"); + ok(id1.length > 0, "Script id is non-empty"); + + await loadURL(toDataURL("<p>Hello")); + + const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({ + source: "1 + 2;", + }); + ok(id2.length > 0, "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: DOC }); + 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, + toDataURL("<p>Next") + ); + + 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, DOC); + await checkIsolatedContextAfterLoad(client, toDataURL("<p>Hello")); +}); + +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, DOC, 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}` + + `\n\turl ${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/test/browser/page/doc_empty.html b/remote/test/browser/page/doc_empty.html new file mode 100644 index 0000000000..f8a2a9d02b --- /dev/null +++ b/remote/test/browser/page/doc_empty.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> + <title>Empty page</title> +</head> +<body> +</body> +</html> diff --git a/remote/test/browser/page/head.js b/remote/test/browser/page/head.js new file mode 100644 index 0000000000..311a8148f1 --- /dev/null +++ b/remote/test/browser/page/head.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/remote/test/browser/head.js", + this +); + +const { + clearInterval, + clearTimeout, + setInterval, + setTimeout, +} = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +const { PollPromise } = ChromeUtils.import("chrome://remote/content/Sync.jsm"); + +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/test/browser/page/sjs_redirect.sjs b/remote/test/browser/page/sjs_redirect.sjs new file mode 100644 index 0000000000..b3dbf44f53 --- /dev/null +++ b/remote/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); +} |