diff options
Diffstat (limited to 'testing/web-platform/tests/portals')
100 files changed, 2813 insertions, 0 deletions
diff --git a/testing/web-platform/tests/portals/META.yml b/testing/web-platform/tests/portals/META.yml new file mode 100644 index 0000000000..2b3241dd18 --- /dev/null +++ b/testing/web-platform/tests/portals/META.yml @@ -0,0 +1,4 @@ +spec: https://wicg.github.io/portals/ +suggested_reviewers: + - jeremyroman + - lucasgadani diff --git a/testing/web-platform/tests/portals/README.md b/testing/web-platform/tests/portals/README.md new file mode 100644 index 0000000000..29134d490f --- /dev/null +++ b/testing/web-platform/tests/portals/README.md @@ -0,0 +1,9 @@ +# Portals + +This directory contains tests for the portals feature, which seeks to enable +seamless navigation. For more information, see: + +* https://github.com/WICG/portals +* https://wicg.github.io/portals/ + +This feature is currently in early development. diff --git a/testing/web-platform/tests/portals/about-blank-cannot-host.html b/testing/web-platform/tests/portals/about-blank-cannot-host.html new file mode 100644 index 0000000000..c43fbc93ba --- /dev/null +++ b/testing/web-platform/tests/portals/about-blank-cannot-host.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +promise_test(async (t) => { + assert_implements("HTMLPortalElement" in self); + let hostWindow = window.open(); + assert_equals(hostWindow.location.href, "about:blank"); + + let portal = hostWindow.document.createElement("portal"); + portal.src = "resources/simple-portal.html"; + hostWindow.document.body.appendChild(portal); + + await promise_rejects_dom(t, "InvalidStateError", hostWindow.DOMException, portal.activate()); +}, "about:blank cannot host a portal"); + +</script> diff --git a/testing/web-platform/tests/portals/csp/frame-ancestors.sub.html b/testing/web-platform/tests/portals/csp/frame-ancestors.sub.html new file mode 100644 index 0000000000..096ed00c7a --- /dev/null +++ b/testing/web-platform/tests/portals/csp/frame-ancestors.sub.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<meta name="timeout" content="long"> +<head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <title>Blocked portals are reported correctly</title> +</head> +<body> + <portal src="/content-security-policy/frame-ancestors/support/content-security-policy.sub.html?policy=report-uri%20/reporting/resources/report.py%3Fop=put%26reportID={{$id:uuid()}}%3B%20frame-ancestors%20'none'"></portal> + <script async defer src="/content-security-policy/support/checkReport.sub.js?reportField=violated-directive&reportValue=frame-ancestors%20'none'&reportID={{$id}}"></script> +</body> +</html> diff --git a/testing/web-platform/tests/portals/csp/frame-src.sub.html b/testing/web-platform/tests/portals/csp/frame-src.sub.html new file mode 100644 index 0000000000..13d9e79667 --- /dev/null +++ b/testing/web-platform/tests/portals/csp/frame-src.sub.html @@ -0,0 +1,49 @@ +<!doctype html> +<title>Tests that portals respect the frame-src</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +</body> +<script> + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + var w = window.open("resources/frame-src.sub.html?frame_src_policy=%27none%27"); + w.onload = function() { + w.document.addEventListener("securitypolicyviolation", + t.step_func_done(function(e) { + assert_equals("frame-src", e.violatedDirective); + })); + var portal = w.document.createElement("portal"); + portal.src = new URL("/portals/resources/simple-portal.html", location.href); + portal.onmessage = t.unreached_func("Portal should not load."); + w.document.body.appendChild(portal); + } + }, "Tests that a portal can't be loaded when it violates frame-src"); + + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + var w = window.open(`resources/frame-src.sub.html?frame_src_policy=http://{{hosts[][www]}}:{{ports[http][0]}}`); + w.onload = function() { + w.document.onsecuritypolicyviolation = t.unreached_func("Portal should load."); + var portal = w.document.createElement("portal"); + portal.src = new URL("http://{{hosts[][www]}}:{{ports[http][0]}}/portals/resources/simple-portal.html", location.href); + portal.onmessage = t.step_func_done(); + w.document.body.appendChild(portal); + } + }, "Tests that a portal can be loaded when the origin matches the frame-src CSP header."); + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + var w = window.open(`resources/frame-src.sub.html?frame_src_policy=http://{{hosts[][www]}}:{{ports[http][0]}}`); + w.onload = function() { + var portal = w.document.createElement("portal"); + portal.src = new URL("http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/simple-portal.html", location.href); + w.document.onsecuritypolicyviolation = t.step_func(function(e) { + w.document.onsecuritypolicyviolation = null; + assert_equals("frame-src", e.violatedDirective); + portal.src = new URL("http://{{hosts[][www]}}:{{ports[http][0]}}/portals/resources/simple-portal.html", location.href); + portal.onmessage = t.step_func_done(); + }); + w.document.body.appendChild(portal); + } + }, "Tests that a portal will fail to load on an origin different than the one specified in the frame-src CSP, but that it can be loaded when the origin matches the frame-src CSP."); +</script> diff --git a/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html b/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html new file mode 100644 index 0000000000..c4f742a643 --- /dev/null +++ b/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html @@ -0,0 +1,4 @@ +<!doctype html> +<body> + <h1>Content Security Policy header containing "frame-src {{GET[frame_src_policy]}}"</h1> +</body> diff --git a/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html.sub.headers b/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html.sub.headers new file mode 100644 index 0000000000..d50520cd39 --- /dev/null +++ b/testing/web-platform/tests/portals/csp/resources/frame-src.sub.html.sub.headers @@ -0,0 +1,2 @@ +Content-Type: text/html; charset=UTF-8 +Content-Security-Policy: frame-src {{GET[frame_src_policy]}} diff --git a/testing/web-platform/tests/portals/history/history-manipulation-inside-portal-with-subframes.html b/testing/web-platform/tests/portals/history/history-manipulation-inside-portal-with-subframes.html new file mode 100644 index 0000000000..cb4c8d0f91 --- /dev/null +++ b/testing/web-platform/tests/portals/history/history-manipulation-inside-portal-with-subframes.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/run-test-in-portal.js"></script> +<body> +<script> + var portalSrc = + 'resources/portal-manipulate-history-with-subframes.sub.html'; + + // Runs before and after the history manipulation in the portal to confirm + // that the session history of the portal host is not affected by any history + // changes in the portal. + function assertInitialHistoryState() { + assert_equals(history.length, 1); + assert_false(!!history.state); + } + + promise_test(async () => { + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testIFrameSrcInPortal'); + assertInitialHistoryState(); + }, 'Setting iframe src navigates independently with replacement in a portal'); + + promise_test(async () => { + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testCrossSiteIFrameSrcInPortal'); + assertInitialHistoryState(); + }, 'Setting cross site iframe src navigates independently with replacement in a portal'); + + promise_test(async () => { + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testIFrameNavInPortal'); + assertInitialHistoryState(); + }, 'iframe navigates itself independently with replacement in a portal'); + + promise_test(async () => { + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testCrossSiteIFrameNavInPortal'); + assertInitialHistoryState(); + }, 'Cross site iframe navigates itself independently with replacement in a portal'); +</script> +</body> diff --git a/testing/web-platform/tests/portals/history/history-manipulation-inside-portal.html b/testing/web-platform/tests/portals/history/history-manipulation-inside-portal.html new file mode 100644 index 0000000000..d4b0cf4db9 --- /dev/null +++ b/testing/web-platform/tests/portals/history/history-manipulation-inside-portal.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/run-test-in-portal.js"></script> +<body> +<script> + var portalSrc = + 'resources/portal-manipulate-history.html'; + + // Runs before and after the history manipulation in the portal to confirm + // that the session history of the portal host is not affected by any history + // changes in the portal. + function assertInitialHistoryState() { + assert_equals(history.length, 1); + assert_false(!!history.state); + } + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testHistoryPushStateInPortal'); + assertInitialHistoryState(); + }, 'history.pushState navigates independently with replacement in a portal'); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testHistoryReplaceStateInPortal'); + assertInitialHistoryState(); + }, 'history.replaceState navigates independently in a portal'); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testLocationAssignInPortal'); + assertInitialHistoryState(); + }, 'location.assign navigates independently with replacement in a portal'); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testLocationReplaceInPortal'); + assertInitialHistoryState(); + }, 'location.replace navigates independently in a portal'); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testSetLocationHrefInPortal'); + assertInitialHistoryState(); + }, 'Setting location.href navigates independently with replacement in a portal'); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + assertInitialHistoryState(); + await runTestInPortal(portalSrc, 'testSyntheticAnchorClickInPortal'); + assertInitialHistoryState(); + }, 'Synthetic anchor click navigates independently with replacement in a portal'); +</script> +</body> diff --git a/testing/web-platform/tests/portals/history/resources/inner-iframe.html b/testing/web-platform/tests/portals/history/resources/inner-iframe.html new file mode 100644 index 0000000000..5c6daa22a5 --- /dev/null +++ b/testing/web-platform/tests/portals/history/resources/inner-iframe.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<body> + <script> + window.onmessage = (e) => { + if (e.data == 'reportHistoryLength') { + e.source.postMessage(history.length, '*'); + } else if (e.data == 'navigate') { + location.href = '#test'; + e.source.postMessage('Done', '*'); + } + }; + </script> +</body> diff --git a/testing/web-platform/tests/portals/history/resources/portal-harness.js b/testing/web-platform/tests/portals/history/resources/portal-harness.js new file mode 100644 index 0000000000..fa8c761afb --- /dev/null +++ b/testing/web-platform/tests/portals/history/resources/portal-harness.js @@ -0,0 +1,30 @@ +// We don't have the test harness in this context, so we roll our own +// which communicates with our host which is actually running the tests. + +window.onload = async () => { + let urlParams = new URLSearchParams(window.location.search); + let testName = urlParams.get('testName'); + let testFn = window[testName]; + if (!testFn) { + window.portalHost.postMessage('Missing test: ' + testName); + return; + } + + // The document load event is not finished at this point, so navigations + // would be done with replacement. This interferes with our tests. We wait + // for the next task before navigating to avoid this. + await new Promise((resolve) => { window.setTimeout(resolve); }); + + try { + await testFn(); + window.portalHost.postMessage('Passed'); + } catch (e) { + window.portalHost.postMessage( + 'Failed: ' + e.name + ': ' + e.message); + } +}; + +function assert(condition, message) { + if (!condition) + throw new Error('Assertion failed: ' + message); +} diff --git a/testing/web-platform/tests/portals/history/resources/portal-manipulate-history-with-subframes.sub.html b/testing/web-platform/tests/portals/history/resources/portal-manipulate-history-with-subframes.sub.html new file mode 100644 index 0000000000..bab83b444f --- /dev/null +++ b/testing/web-platform/tests/portals/history/resources/portal-manipulate-history-with-subframes.sub.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<script src="portal-harness.js"></script> +<body> +<script> + function messageFrameAndAwaitResponse(frame, message) { + return new Promise((resolve) => { + window.onmessage = (e) => { + resolve(e.data); + }; + frame.contentWindow.postMessage(message, '*'); + }); + } + + function innerFrameUrl(crossSite) { + return (crossSite ? + 'https://{{hosts[alt][www]}}:{{ports[https][0]}}' : '') + + '/portals/history/resources/inner-iframe.html' + } + + async function runTestIFrameSrcInPortal(crossSite) { + assert(history.length == 1, 'Initial history length'); + + let iframe = document.createElement('iframe'); + iframe.src = innerFrameUrl(crossSite); + await new Promise((resolve) => { + iframe.onload = resolve; + document.body.appendChild(iframe); + }); + + let frameHistoryLength = + await messageFrameAndAwaitResponse(iframe, 'reportHistoryLength'); + assert(history.length == 1, 'History length unchanged when iframe added'); + assert(frameHistoryLength == 1, 'History length in iframe when added'); + + iframe.src = iframe.src + '#test'; + + frameHistoryLength = + await messageFrameAndAwaitResponse(iframe, 'reportHistoryLength'); + assert( + history.length == 1, 'History length unchanged when iframe src set'); + assert( + frameHistoryLength == 1, + 'History length in iframe unchanged when iframe src set'); + } + + function testIFrameSrcInPortal() { + return runTestIFrameSrcInPortal(false); + } + + function testCrossSiteIFrameSrcInPortal() { + return runTestIFrameSrcInPortal(true); + } + + async function runTestIFrameNavInPortal(crossSite) { + assert(history.length == 1, 'Initial history length'); + + let iframe = document.createElement('iframe'); + iframe.src = innerFrameUrl(crossSite); + await new Promise((resolve) => { + iframe.onload = resolve; + document.body.appendChild(iframe); + }); + + await messageFrameAndAwaitResponse(iframe, 'navigate'); + + let frameHistoryLength = + await messageFrameAndAwaitResponse(iframe, 'reportHistoryLength'); + assert( + history.length == 1, 'History length unchanged when iframe navigates'); + assert( + frameHistoryLength == 1, + 'History length in iframe unchanged when iframe navigates'); + } + + function testIFrameNavInPortal() { + return runTestIFrameNavInPortal(false); + } + + function testCrossSiteIFrameNavInPortal() { + return runTestIFrameNavInPortal(true); + } +</script> +</body> diff --git a/testing/web-platform/tests/portals/history/resources/portal-manipulate-history.html b/testing/web-platform/tests/portals/history/resources/portal-manipulate-history.html new file mode 100644 index 0000000000..3e25f0e6f2 --- /dev/null +++ b/testing/web-platform/tests/portals/history/resources/portal-manipulate-history.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<script src="portal-harness.js"></script> +<body> +<script> + function testHistoryPushStateInPortal() { + assert(history.length == 1, 'Initial history length'); + assert(!history.state, 'Initial history state'); + + history.pushState('teststate', null, null); + + assert(history.length == 1, 'History length unchanged'); + assert(history.state == 'teststate', 'Update state'); + } + + function testHistoryReplaceStateInPortal() { + assert(history.length == 1, 'Initial history length'); + assert(!history.state, 'Initial history state'); + + history.replaceState('teststate', null, null); + + assert(history.length == 1, 'History length unchanged'); + assert(history.state == 'teststate', 'Update state'); + } + + function testLocationAssignInPortal() { + assert(history.length == 1, 'Initial history length'); + let initialLocation = location.href; + location.assign('#test'); + + assert(history.length == 1, 'History length unchanged'); + assert(location.href != initialLocation, 'Update location'); + } + + function testLocationReplaceInPortal() { + assert(history.length == 1, 'Initial history length'); + let initialLocation = location.href; + location.replace('#test'); + + assert(history.length == 1, 'History length unchanged'); + assert(location.href != initialLocation, 'Update location'); + } + + function testSetLocationHrefInPortal() { + assert(history.length == 1, 'Initial history length'); + let initialLocation = location.href; + location.href = '#test'; + + assert(history.length == 1, 'History length unchanged'); + assert(location.href != initialLocation, 'Update location'); + } + + function testSyntheticAnchorClickInPortal() { + assert(history.length == 1, 'Initial history length'); + let initialLocation = location.href; + + var anchor = document.createElement('a'); + anchor.href = '#test'; + document.body.appendChild(anchor); + + anchor.click(); + + assert(history.length == 1, 'History length unchanged'); + assert(location.href != initialLocation, 'Update location'); + } +</script> +</body> diff --git a/testing/web-platform/tests/portals/history/resources/run-test-in-portal.js b/testing/web-platform/tests/portals/history/resources/run-test-in-portal.js new file mode 100644 index 0000000000..c982a1fac8 --- /dev/null +++ b/testing/web-platform/tests/portals/history/resources/run-test-in-portal.js @@ -0,0 +1,16 @@ +// This is called from the portal host which is running with the test harness. +// This creates a portal and communicates with our ad hoc test harness in the +// portal context which performs the history manipulation in the portal. We +// confirm that the history manipulation works as expected in the portal. +async function runTestInPortal(portalSrc, testName) { + let portal = document.createElement('portal'); + portal.src = portalSrc + '?testName=' + testName; + let result = await new Promise((resolve) => { + portal.onmessage = (e) => { + resolve(e.data); + }; + document.body.appendChild(portal); + }); + + assert_equals(result, 'Passed'); +} diff --git a/testing/web-platform/tests/portals/htmlportalelement-event-handler-content-attributes.html b/testing/web-platform/tests/portals/htmlportalelement-event-handler-content-attributes.html new file mode 100644 index 0000000000..0836c8c00b --- /dev/null +++ b/testing/web-platform/tests/portals/htmlportalelement-event-handler-content-attributes.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// Dispatch of these events is tested elsewhere. +// This test merely ensures that the event handler content attributes work. +let eventNames = ["load", "message", "messageerror"]; +test(() => { + try { + assert_implements("HTMLPortalElement" in self); + let portal = document.createElement("portal"); + for (let eventName of eventNames) { + window.testValue = "not fired"; + portal.setAttribute("on" + eventName, "window.testValue = 'fired'"); + portal.dispatchEvent(new Event(eventName)); + assert_equals(window.testValue, "fired", `${eventName} should have fired`); + + window.testValue = "not fired"; + portal.removeAttribute("on" + eventName); + portal.dispatchEvent(new Event(eventName)); + assert_equals(window.testValue, "not fired", `${eventName} should not have fired`); + } + } finally { + delete window.testValue; + } +}, "Tests that event handler content attributes for supported event names work."); +</script> diff --git a/testing/web-platform/tests/portals/idlharness.window.js b/testing/web-platform/tests/portals/idlharness.window.js new file mode 100644 index 0000000000..b43d17dc56 --- /dev/null +++ b/testing/web-platform/tests/portals/idlharness.window.js @@ -0,0 +1,18 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +// https://wicg.github.io/portals/ + +'use strict'; + +idl_test( + ['portals'], + ['html', 'dom'], + async idl_array => { + idl_array.add_objects({ + HTMLPortalElement: ['document.createElement("portal")'], + PortalHost: ['window.portalHost'], + PortalActivateEvent: ['new PortalActivateEvent("portalactivate")'], + }); + } +); diff --git a/testing/web-platform/tests/portals/no-portal-in-sandboxed-popup.html b/testing/web-platform/tests/portals/no-portal-in-sandboxed-popup.html new file mode 100644 index 0000000000..b26b836467 --- /dev/null +++ b/testing/web-platform/tests/portals/no-portal-in-sandboxed-popup.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<body> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +promise_test(async t => { + let sandboxFlags = 'allow-scripts allow-same-origin'; + let w = window.open(`resources/attempt-portal-load.html?pipe=header(Content-Security-Policy,sandbox ${sandboxFlags})`); + await new Promise((resolve, reject) => w.addEventListener('load', resolve)); + let result = await Promise.race([ + w.portalLoaded.then(() => 'loaded'), + new Promise(resolve => t.step_timeout(() => resolve('timed out'), 5000))]); + assert_equals(result, 'timed out', 'expected portal not to load due to sandbox flags'); +}); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portal-activate-data.html b/testing/web-platform/tests/portals/portal-activate-data.html new file mode 100644 index 0000000000..54fdca5d8c --- /dev/null +++ b/testing/web-platform/tests/portals/portal-activate-data.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<title>Tests passing of data along with portal activation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<body> +<canvas id="canvas"></canvas> +<script> +function nextMessage(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', e => resolve(e), {once: true}); + }); +} + +async function openPortalAndActivate(logic, activateOptions, testWindow) { + assert_implements("HTMLPortalElement" in self); + const w = testWindow || await openBlankPortalHost(); + try { + const portal = w.document.createElement('portal'); + portal.src = new URL('resources/portal-activate-data-portal.html?logic=' + encodeURIComponent(logic), location.href); + w.document.body.appendChild(portal); + assert_equals((await nextMessage(portal)).data, 'ready'); + await portal.activate(activateOptions); + return (await nextMessage(w.portalHost)).data; + } finally { + w.close(); + } +} + +promise_test(async () => { + const {echo} = await openPortalAndActivate( + 'return {echo: event.data}', + {data: 'banana'}); + assert_equals(echo, 'banana'); +}, "A string can be passed through activate data."); + +promise_test(async () => { + let aBuff = new ArrayBuffer(5); + let arr = new Int8Array(aBuff); + for (var i = 0; i < 5; i++) + arr[i] = i; + const {array} = await openPortalAndActivate( + 'return {array: Array.prototype.slice.call(new Int8Array(event.data))}', + {data: aBuff, transfer: [aBuff]}); + assert_equals(arr.length, 0); + assert_array_equals(array, [0, 1, 2, 3, 4]); +}, "An array buffer can be transferred through activate data."); + +promise_test(async () => { + let canvas = document.getElementById("canvas"); + let ctx = canvas.getContext("2d"); + ctx.fillStyle = "green"; + ctx.fillRect(0, 0, 150, 100); + let imageBitmap = await createImageBitmap(canvas, 0, 0, 150, 100); + const {height, width} = await openPortalAndActivate( + 'return {height: event.data.height, width: event.data.width}', + {data: imageBitmap, transfer: [imageBitmap]}); + assert_equals(height, 100); + assert_equals(width, 150); +}, "An image bitmap can be transferred through activate data."); + +promise_test(async () => { + let {port1, port2} = new MessageChannel(); + let replyViaPort = nextMessage(port1); + port1.start(); + let ok = await openPortalAndActivate( + 'let port2 = event.data; port2.postMessage(42); return true;', + {data: port2, transfer: [port2]}); + assert_true(ok); + assert_equals((await replyViaPort).data, 42); +}, "A message port can be passed through activate data."); + +promise_test(async t => { + const w = await openBlankPortalHost(); + await promise_rejects_dom( + t, 'DataCloneError', w.DOMException, + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + openPortalAndActivate('', {data: new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer}, w)); +}, "A SharedArrayBuffer cannot be passed through activate data."); + +promise_test(async t => { + await promise_rejects_js( + t, Error, + openPortalAndActivate('', {data: {get a() { throw new Error; }}})); +}, "Uncloneable data has its exception propagated."); + +promise_test(async t => { + const w = await openBlankPortalHost(); + await promise_rejects_js( + t, w.TypeError, + openPortalAndActivate('', {data: null, transfer: [null]}, w)); +}, "Errors during transfer list processing are propagated."); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portal-activate-default.html b/testing/web-platform/tests/portals/portal-activate-default.html new file mode 100644 index 0000000000..b1a8feb1f4 --- /dev/null +++ b/testing/web-platform/tests/portals/portal-activate-default.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<script> +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + const w = await openBlankPortalHost(); + try { + const bc = new BroadcastChannel('click-activate'); + const portal = w.document.createElement('portal'); + portal.src = new URL(`resources/portal-activate-broadcastchannel.html?bc=${bc.name}`, location.href); + w.document.body.appendChild(portal); + await new Promise(resolve => portal.onload = resolve); + let activated = new Promise(resolve => bc.onmessage = e => resolve(e.data)); + portal.click(); + let {event, data} = await activated; + assert_equals(event, 'portalactivate'); + assert_equals(data, undefined); + } finally { + w.close(); + } +}, "Clicking should activate with undefined data."); + +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + const w = await openBlankPortalHost(); + try { + const bc = new BroadcastChannel('prevent-no-activate'); + const portal = w.document.createElement('portal'); + portal.src = new URL(`resources/portal-activate-broadcastchannel.html?bc=${bc.name}`, location.href); + portal.onclick = e => e.preventDefault(); + w.document.body.appendChild(portal); + await new Promise(resolve => portal.onload = resolve); + bc.onmessage = t.unreached_func('activation should not occur'); + portal.click(); + await new Promise(resolve => t.step_timeout(resolve, 3000)); + } finally { + w.close(); + } +}, "Clicking shouldn't activate if prevented."); + +// Script didn't create the promise so it shouldn't observe one. +// This forecloses a naive implementation of this behavior that simply calls the WebIDL operation. +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + const w = await openBlankPortalHost(); + try { + const portal = w.document.createElement('portal'); + w.onunhandledrejection = t.unreached_func('unhandledrejection event should not fire'); + portal.click(); + await new Promise(resolve => t.step_timeout(resolve, 3000)); + } finally { + w.close(); + } +}, "Failed activation should not surface as an unhandled promise rejection."); +</script> diff --git a/testing/web-platform/tests/portals/portal-activate-event-constructor.html b/testing/web-platform/tests/portals/portal-activate-event-constructor.html new file mode 100644 index 0000000000..1931e8fc86 --- /dev/null +++ b/testing/web-platform/tests/portals/portal-activate-event-constructor.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(() => { + // Even though UA-generated portalactivate events are different, the + // properties supplied should be used. + const e = new PortalActivateEvent("eventtype", { bubbles: true, cancelable: true }); + assert_equals(e.type, "eventtype"); + assert_true(e.bubbles); + assert_true(e.cancelable); + assert_equals(null, e.data); +}, "It should be possible to construct a PortalActivateEvent with a dictionary"); + +test(() => { + const data = {}; + const e = new PortalActivateEvent("portalactivate", { data }); + assert_equals(data, e.data); +}, "A PortalActivateEvent should expose exactly the data object supplied in the original realm"); + +test(() => { + const e = new PortalActivateEvent("portalactivate"); + assert_throws_dom("InvalidStateError", () => e.adoptPredecessor()); +}, "Invoking adoptPredecessor on a synthetic PortalActivateEvent should throw"); +</script> diff --git a/testing/web-platform/tests/portals/portal-activate-event.html b/testing/web-platform/tests/portals/portal-activate-event.html new file mode 100644 index 0000000000..69d8a7c930 --- /dev/null +++ b/testing/web-platform/tests/portals/portal-activate-event.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Tests that the PortalActivateEvent is dispatched when a portal is activated</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + let test = "eventlistener"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + const portalUrl = encodeURIComponent(`portal-activate-event-portal.html?test=${test}`); + window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`); + }, "Tests that the PortalActivateEvent is dispatched when a portal is activated."); + + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + let test = "eventhandler"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + const portalUrl = encodeURIComponent(`portal-activate-event-portal.html?test=${test}`); + window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`); + }, "Tests that the portalactivate event handler is dispatched when a portal is activated."); + + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + let test = "bodyeventhandler"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + const portalUrl = encodeURIComponent(`portal-activate-event-portal.html?test=${test}`); + window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`); + }, "Tests that the HTMLBodyElement has the portalactivate event handler."); +</script> diff --git a/testing/web-platform/tests/portals/portal-non-http-navigation.html b/testing/web-platform/tests/portals/portal-non-http-navigation.html new file mode 100644 index 0000000000..aa02c15efa --- /dev/null +++ b/testing/web-platform/tests/portals/portal-non-http-navigation.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<title>Tests that portal don't navigate to non-http schemes.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "data:text/html,empty portal"; + portal.onload = t.unreached_func("Portal loaded data URL."); + document.body.appendChild(portal); + t.step_timeout(() => { portal.remove(); t.done(); }, 3000); +}, "Tests that a portal can't navigate to a data URL."); + +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "about:blank"; + portal.onload = t.unreached_func("Portal loaded about:blank."); + document.body.appendChild(portal); + t.step_timeout(() => { portal.remove(); t.done(); }, 3000); +}, "Tests that a portal can't navigate to about:blank."); + +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "resources/simple-portal.html"; + portal.onload = t.step_func(() => { + portal.onmessage = t.unreached_func("Portal execute javascript."); + portal.src = "javascript:window.portalHost.postMessage('executed', '*')"; + t.step_timeout(() => { portal.remove(); t.done(); }, 3000); + }); + document.body.appendChild(portal); +}, "Tests that a portal can't navigate to javascript URLs."); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portal-onload-event.html b/testing/web-platform/tests/portals/portal-onload-event.html new file mode 100644 index 0000000000..f6b97a814e --- /dev/null +++ b/testing/web-platform/tests/portals/portal-onload-event.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<title>Tests that the load is dispatched when a portal finishes loading.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + var w = window.open("resources/simple-portal.html"); + w.onload = function() { + var portal = w.document.createElement("portal"); + portal.src = "resources/simple-portal.html"; + portal.onload = t.step_func_done(); + w.document.body.appendChild(portal); + } + }, "Tests that the load event is dispatched when a portal finishes loading."); +</script> diff --git a/testing/web-platform/tests/portals/portals-activate-empty-browsing-context.html b/testing/web-platform/tests/portals/portals-activate-empty-browsing-context.html new file mode 100644 index 0000000000..0c63e38497 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-empty-browsing-context.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let portal = document.createElement('portal'); + document.body.appendChild(portal); + t.add_cleanup(() => { document.body.removeChild(portal); }); + + await promise_rejects_dom(t, 'InvalidStateError', portal.activate()); +}, "A portal that has never been navigated cannot be activated"); + +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let portal = document.createElement('portal'); + document.body.appendChild(portal); + t.add_cleanup(() => { document.body.removeChild(portal); }); + + // We use a status of 204 (No Content) as that couldn't possibly mature. + portal.src = "/common/blank.html?pipe=status(204)" + await promise_rejects_dom(t, 'InvalidStateError', portal.activate()); +}, "A portal that has not completed an initial navigation cannot be activated"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-activate-inside-iframe.html b/testing/web-platform/tests/portals/portals-activate-inside-iframe.html new file mode 100644 index 0000000000..f403954096 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-inside-iframe.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <script> + promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + var iframe = document.createElement("iframe"); + iframe.src = "resources/portal-inside-iframe.html" + var waitForLoad = new Promise((resolve, reject) => { + iframe.onload = resolve; + }); + document.body.appendChild(iframe); + await waitForLoad; + const portal = iframe.contentDocument.getElementById("portal"); + return promise_rejects_dom(t, "InvalidStateError", + iframe.contentWindow.DOMException, + portal.activate()); + }, "activating portal inside iframe should fail"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-activate-inside-portal.html b/testing/web-platform/tests/portals/portals-activate-inside-portal.html new file mode 100644 index 0000000000..19b57b3e42 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-inside-portal.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <script> + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "resources/portal-activate-inside-portal.html"; + let waitForMessage = new Promise((resolve, reject) => { + portal.onmessage = e => resolve(e.data); + document.body.appendChild(portal); + }); + var error = await waitForMessage; + assert_equals(error, "InvalidStateError"); + }, "activating a nested portal should throw an error"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-activate-network-error.html b/testing/web-platform/tests/portals/portals-activate-network-error.html new file mode 100644 index 0000000000..60ee5c902d --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-network-error.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +async_test(t => { + assert_implements("HTMLPortalElement" in self); + let portal = document.createElement('portal'); + portal.src = "resources/invalid.asis"; + document.body.appendChild(portal); + t.add_cleanup(() => { document.body.removeChild(portal); }); + t.step_timeout(async () => { + await promise_rejects_dom(t, 'InvalidStateError', portal.activate()); + t.done(); + }, 2000); +}, "A portal that is showing inline content for a network error cannot be activated"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-activate-no-browsing-context.html b/testing/web-platform/tests/portals/portals-activate-no-browsing-context.html new file mode 100644 index 0000000000..ccf1e9504b --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-no-browsing-context.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let activatePromise = document.createElement('portal').activate(); + await promise_rejects_dom(t, 'InvalidStateError', activatePromise); +}, "A portal with nothing in it cannot be activated"); +</script> diff --git a/testing/web-platform/tests/portals/portals-activate-resolution.html b/testing/web-platform/tests/portals/portals-activate-resolution.html new file mode 100644 index 0000000000..7094768a4f --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-resolution.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<script> + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var win = await openBlankPortalHost(); + var portal = win.document.createElement("portal"); + portal.src = new URL("resources/simple-portal.html", location.href) + + await new Promise((resolve, reject) => { + portal.onload = resolve; + win.document.body.appendChild(portal); + }); + + return portal.activate(); + }); +</script> diff --git a/testing/web-platform/tests/portals/portals-activate-twice.html b/testing/web-platform/tests/portals/portals-activate-twice.html new file mode 100644 index 0000000000..0eea5465a2 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-twice.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let waitForMessage = new Promise((resolve, reject) => { + window.onmessage = e => resolve(e.data); + }); + window.open("resources/portal-activate-twice-window-1.html"); + let error = await waitForMessage; + assert_equals(error, "InvalidStateError"); +}, "Calling activate when a portal is already activating should fail"); + +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let waitForMessage = new Promise((resolve, reject) => { + window.onmessage = e => resolve(e.data); + }); + window.open("resources/portal-activate-twice-window-2.html"); + let error = await waitForMessage; + assert_equals(error, "InvalidStateError"); +}); +</script> diff --git a/testing/web-platform/tests/portals/portals-activate-while-unloading.html b/testing/web-platform/tests/portals/portals-activate-while-unloading.html new file mode 100644 index 0000000000..5abb164b3b --- /dev/null +++ b/testing/web-platform/tests/portals/portals-activate-while-unloading.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> + <head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + <script> + function childReady() { + return new Promise((resolve) => { + window.onmessage = resolve; + }); + } + + const handlers = ['beforeunload', 'pagehide', 'unload']; + for (let handler of handlers) { + promise_test(async (test) => { + let popup; + + // Open a popup that has a portal, wait for both to be loaded. + { + await test_driver.bless('Open a popup', () => { + popup = open(`resources/portal-activate-in-handler.html?${handler}`, + '_blank'); + }); + await childReady(); + } + + // We need the exception type below to ensure the activate() call + // throws but the popup global may be gone by then so stash it here. + const exception_type = popup.DOMException; + + // Navigate the popup away. + const cur_path = popup.location.pathname; + popup.location = 'resources/blank-host.html'; + + // We need to wait until the handler is called but because of the + // nature of these handlers, we can't reliably communicate with the + // popup while they're running so we use a promise established + // earlier to wait until a time we know the portal has been activated + // and the returned promise stored on this global. + await window.handler_called_promise; + assert_not_equals(typeof(window.portal_promise), 'undefined', + 'Portal.activate() must be called'); + + // The popup should have called activate from the handler, and placed + // the promise returned from that call into this window in the + // |portal_promise| variable. We expect that this call should reject, + // however, if it does activate, it's timing dependent whether the + // handler will be run to completion so we may never fulfil the + // promise. In that case timeout and fail the test. + { + test.step_timeout(() => { + assert_unreached('Activation didn\'t fulfil.'); + }, 3000); + + await promise_rejects_dom(test, + "InvalidStateError", + exception_type, + window.portal_promise, + "Portal activation must fail."); + } + popup.close(); + }, `cannot activate portal from ${handler}`); + } + </script> + </head> + <body> + </body> +</html> diff --git a/testing/web-platform/tests/portals/portals-adopt-predecessor.html b/testing/web-platform/tests/portals/portals-adopt-predecessor.html new file mode 100644 index 0000000000..04c6196062 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-adopt-predecessor.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<title>Tests that a portal can adopt its predecessor</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + function waitForCompletion(targetTest) { + return new Promise((resolve, reject) => { + window.addEventListener("message", ({data: {test, message}}) => { + if (test === targetTest) + resolve(message); + }); + }); + } + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-once"; + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + var message = await waitForCompletion(test); + assert_equals(message, "adopted"); + }, "Tests that a portal can adopt its predecessor."); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-twice"; + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + var message = await waitForCompletion(test); + assert_equals(message, "passed"); + }, "Tests that trying to adopt the predecessor twice will throw an exception."); + + async_test(function(t) { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-after-event"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + }, "Tests that trying to adopt the predecessor after the PortalActivateEvent will throw an exception."); + + promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-and-activate"; + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + var message = await waitForCompletion(test); + assert_equals(message, "passed"); + }, "Tests that activating an adopted predecessor without inserting it works"); + + async_test(t => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-attach-remove"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + }, "Tests that an adopting, inserting and then removing a predecessor works correctly"); + + async_test(t => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-and-discard"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + }, "Tests that the adopted predecessor is destroyed if it isn't inserted"); + + async_test(t => { + assert_implements("HTMLPortalElement" in self); + var test = "adopt-to-disconnected-node"; + var bc = new BroadcastChannel(`test-${test}`); + bc.onmessage = t.step_func_done(function(e) { + assert_equals(e.data, "passed"); + bc.close(); + }); + window.open(`resources/portals-adopt-predecessor.html?test=${test}`); + }, "Tests that an adopted portal can be inserted into a disconnected node."); +</script> diff --git a/testing/web-platform/tests/portals/portals-api.html b/testing/web-platform/tests/portals/portals-api.html new file mode 100644 index 0000000000..79d2d526bd --- /dev/null +++ b/testing/web-platform/tests/portals/portals-api.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<title>Portals API test</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <script> + test(function() { + assert_true(document.createElement('portal') instanceof HTMLPortalElement); + }, "portal element exists"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-close-window.html b/testing/web-platform/tests/portals/portals-close-window.html new file mode 100644 index 0000000000..e3a66c0bf1 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-close-window.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + let portal = document.createElement('portal'); + portal.src = "resources/portal-close-window.html"; + let waitForMessage = new Promise((resolve, reject) => { + portal.onmessage = e => resolve(e.data); + document.body.appendChild(portal); + }); + document.body.appendChild(portal); + var message = await waitForMessage; + assert_equals(message, false); + t.add_cleanup(() => { document.body.removeChild(portal); }); +}, "A portal's window cannot be closed"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-cross-origin-load.sub.html b/testing/web-platform/tests/portals/portals-cross-origin-load.sub.html new file mode 100644 index 0000000000..04db38a8e9 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-cross-origin-load.sub.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/simple-portal.html"; + return new Promise((resolve, reject) => { + portal.onload = resolve; + document.body.appendChild(portal); + }); + }); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-focus.sub.html b/testing/web-platform/tests/portals/portals-focus.sub.html new file mode 100644 index 0000000000..54b4312be0 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-focus.sub.html @@ -0,0 +1,184 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/open-blank-host.js"></script> +<body> +<script> + async function createPortal(doc, url) { + assert_implements("HTMLPortalElement" in self); + let portal = doc.createElement("portal"); + portal.src = url; + doc.body.appendChild(portal); + await new Promise(r => portal.onload = r); + return portal; + } + + promise_test(async t => { + let portal = await createPortal(document, new URL("resources/focus-page-with-button.html", location.href)); + try { + portal.onmessage = t.step_func(e => { + assert_unreached("button inside portal should not be focused"); + }); + portal.postMessage("focus"); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + document.body.removeChild(portal); + } + }, "test that an element inside a portal cannot steal focus"); + + promise_test(async () => { + let portal = await createPortal(document, new URL("resources/focus-page-with-button.html", location.href)); + try { + let activeElementUpdated = new Promise(r => { + portal.onmessage = e => r(e.data.activeElementUpdated) + }); + portal.postMessage('focus-update-active-element'); + assert_true(await activeElementUpdated); + } finally { + document.body.removeChild(portal); + } + }, "test that activeElement inside a portal is updated after focus() is called"); + + promise_test(async t => { + let portal = await createPortal(document, new URL("resources/focus-page-with-x-origin-iframe.sub.html", location.href)); + try { + portal.onmessage = t.step_func(e => { + assert_unreached("button inside portal should not be focused"); + }); + portal.postMessage("focus"); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + document.body.removeChild(portal); + } + }, "test that an element inside a portal's x-origin subframe cannot steal focus"); + + promise_test(async () => { + let portal = await createPortal(document, new URL("resources/focus-page-with-x-origin-iframe.sub.html", location.href)); + try { + portal.postMessage("focus-update-active-element"); + let {activeElementUpdated} = await new Promise(r => { + portal.onmessage = e => r(e.data); + }); + assert_true(activeElementUpdated); + } finally { + document.body.removeChild(portal); + } + }, "test that a portal's x-origin subframe becomes active element on focus"); + + promise_test(async t => { + let win = await openBlankPortalHost(); + let doc = win.document; + try { + let portal = await createPortal(doc, new URL("resources/simple-portal-adopts-predecessor.html", location.href)); + let button = doc.createElement("button"); + doc.body.appendChild(button); + + await portal.activate(); + doc.body.removeChild(portal); + + button.onfocus = t.step_func(() => { + assert_unreached("button inside adopted portal should not be focused"); + }); + button.focus(); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + win.close(); + } + }, "test that an element inside an adopted portal cannot steal focus"); + + promise_test(async t => { + let win = await openBlankPortalHost(); + let doc = win.document; + try { + let portal = await createPortal(doc, new URL("resources/simple-portal-adopts-predecessor.html", location.href)); + let iframe = doc.createElement("iframe"); + iframe.src = new URL("resources/focus-page-with-button.html", + "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/"); + doc.body.appendChild(iframe); + await new Promise(r => iframe.onload = r); + + await portal.activate(); + doc.body.removeChild(portal); + + iframe.contentWindow.postMessage("focus", "*"); + window.onmessage = t.step_func(() => { + assert_unreached("button inside x-origin iframe inside a portal should not be focused"); + }); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + win.close(); + } + }, "test that a x-origin iframe inside an adopted portal cannot steal focus"); + + promise_test(async () => { + let win = await openBlankPortalHost(); + let doc = win.document; + try { + let portal = await createPortal(doc, new URL("resources/focus-page-with-autofocus.html", location.href)); + portal.postMessage('check-active-element'); + let result = await new Promise(r => { + portal.onmessage = e => r(e.data); + }); + assert_true(result, "autofocused element is active element"); + + await portal.activate(); + win.portalHost.postMessage('check-active-element'); + result = await new Promise(r => { + win.portalHost.onmessage = e => r(e.data) + }); + assert_true(result, "autofocused element is still active element"); + } finally { + win.close(); + } + }, "test that autofocus inside a portal works"); + + const TAB = "\ue004"; // https://w3c.github.io/webdriver/#keyboard-actions + const SPACE = " " + const RETURN = "\r"; + + promise_test(async t => { + let portal = await createPortal(document, "resources/focus-page-with-button.html"); + try { + await test_driver.send_keys(document.body, TAB); + portal.onmessage = t.unreached_func("button inside portal should not be focused"); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + document.body.removeChild(portal); + } + }, "test that a portal is keyboard focusable"); + + promise_test(async t => { + let portal = await createPortal(document, "resources/focus-page-with-button.html"); + try { + let portalFocusPromise = new Promise(r => portal.onfocus = r); + portal.onmessage = t.unreached_func("button inside portal should not be focused"); + await test_driver.send_keys(document.body, TAB); + await portalFocusPromise; + await test_driver.send_keys(document.body, TAB); + await new Promise(r => t.step_timeout(r, 500)); + } finally { + document.body.removeChild(portal); + } + }, "test that we cannot tab into a portal's contents"); + + promise_test(async t => { + let portal = await createPortal(document, "resources/simple-portal.html"); + try { + portal.focus(); + for (let key of [SPACE, RETURN]) { + let clickPromise = new Promise((resolve) => { + portal.onclick = e => { e.preventDefault(); resolve(); }; + }); + await test_driver.send_keys(document.body, key); + await clickPromise; + } + } finally { + document.body.removeChild(portal); + } + }, "test that a portal is keyboard activatable"); + +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-host-exposure.sub.html b/testing/web-platform/tests/portals/portals-host-exposure.sub.html new file mode 100644 index 0000000000..fd3ac18f69 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-host-exposure.sub.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/stash-utils.sub.js"></script> +<script src="/common/utils.js"></script> +<body> +<script> + function openPortal(portalSrc) { + assert_implements("HTMLPortalElement" in self); + const portal = document.createElement('portal'); + portal.src = portalSrc; + return portal; + } + + async function openPortalAndReceiveMessage(portalSrc) { + const key = token(); + const portal = openPortal(`${portalSrc}?key=${key}`); + document.body.appendChild(portal); + return StashUtils.takeValue(key); + } + + promise_test(async () => { + const result = await openPortalAndReceiveMessage("resources/portal-host.html"); + assert_equals(result, "passed"); + }, "window.portalHost should be exposed in same-origin portal"); + + promise_test(async () => { + const result = await openPortalAndReceiveMessage( + "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host.html"); + assert_equals(result, "passed"); + }, "window.portalHost should be exposed in cross-origin portal"); + + promise_test(async () => { + const result = await openPortalAndReceiveMessage( + 'resources/portal-host-cross-origin-navigate.sub.html'); + assert_equals(result, "passed"); + }, "window.portalHost should be exposed in portal after cross-origin navigation"); + +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-host-hidden-after-activation.html b/testing/web-platform/tests/portals/portals-host-hidden-after-activation.html new file mode 100644 index 0000000000..9638a6c7c6 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-host-hidden-after-activation.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + // Waits for 2 messages from portal, one before activation and one after. + function waitForMessages() { + return new Promise((resolve, reject) => { + var results = []; + var bc = new BroadcastChannel("portals-host-hidden-after-activation"); + bc.onmessage = e => { + results.push(e.data.hasHost); + if (results.length == 2) { + bc.close(); + resolve(results); + } + }; + }); + } + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + const portalUrl = encodeURIComponent("portal-host-hidden-after-activation-portal.html"); + window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`); + var results = await waitForMessages(); + assert_true(results[0], "portalHost exposed before calling activate()"); + assert_false(results[1], "portalHost hidden after receiving portalactivate event"); + }, "window.portalHost should be null after portal is activated"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-host-null.html b/testing/web-platform/tests/portals/portals-host-null.html new file mode 100644 index 0000000000..e0f1d63743 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-host-null.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + test(t => { + assert_equals(window.portalHost, null, "window.portalHost should be null"); + }); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-host-post-message.sub.html b/testing/web-platform/tests/portals/portals-host-post-message.sub.html new file mode 100644 index 0000000000..d589235ec3 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-host-post-message.sub.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<title>Test postMessage on PortalHost</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <script> + function createPortal(portalSrc) { + var portal = document.createElement("portal"); + portal.src = portalSrc; + return new Promise((resolve, reject) => { + portal.onload = () => { + resolve(portal); + }; + document.body.appendChild(portal); + }); + } + + async function createPortalAndLoopMessage(portalSrc, params) { + assert_implements("HTMLPortalElement" in self); + var portal = await createPortal(portalSrc); + var waitForResponse = new Promise((resolve, reject) => { + portal.addEventListener("message", e => { resolve(e); }); + }); + portal.postMessage(params); + return waitForResponse; + } + + const sameOriginUrl = "resources/portal-host-post-message.html"; + const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host-post-message-x-origin.html"; + + promise_test(async () => { + var {data, origin} = await createPortalAndLoopMessage(sameOriginUrl, + ["test"]); + assert_equals(data, "test"); + assert_equals(origin, "http://{{host}}:{{ports[http][0]}}"); + }, "Message received after postMessage from portal host"); + + promise_test(async () => { + var message = { + prop1: "value1", + prop2: 2.5, + prop3: [1, 2, "3"], + prop4: { + prop4_1: "value4_1" + } + }; + var {data} = await createPortalAndLoopMessage(sameOriginUrl, + [message]); + assert_object_equals(data, message); + }, "postMessage with object message"); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + function checkPort(port) { + return new Promise((resolve, reject) => { + var channel = new MessageChannel(); + channel.port1.onmessage = resolve; + port.postMessage("sending port", {transfer: [channel.port2]}); + }); + } + + var {ports} = await createPortalAndLoopMessage(sameOriginUrl, { + type: "message-port" + }); + await checkPort(ports[0]); + }, "postMessage with message ports"); + + promise_test(async () => { + var {data} = await createPortalAndLoopMessage(sameOriginUrl, { + type: "array-buffer-without-transfer", + array: [0, 1, 2, 3, 4] + }); + assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer)); + }, "postMessage with array buffer without transfer"); + + promise_test(async () => { + var {data} = await createPortalAndLoopMessage(sameOriginUrl, { + type: "array-buffer-with-transfer", + array: [0, 1, 2, 3, 4] + }); + assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer)); + }, "postMessage with array buffer with transfer"); + + promise_test(async () => { + var {data} = await createPortalAndLoopMessage(sameOriginUrl, { + type: "invalid-message" + }); + assert_equals(data.errorType, "DataCloneError"); + }, "postMessage should throw error when serialization fails"); + + promise_test(async () => { + var {data} = await createPortalAndLoopMessage(sameOriginUrl,{ + type: "invalid-port" + }); + assert_equals(data.errorType, "TypeError"); + }, "postMessage with invalid transferable should throw error"); + + promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + var receiveMessage = new Promise((resolve, reject) => { + var bc = new BroadcastChannel("portal-host-post-message-after-activate"); + bc.onmessage = e => { resolve(e); }; + }); + const portalUrl = encodeURIComponent( + "portal-host-post-message-after-activate.html"); + window.open(`resources/portal-embed-and-activate.html?url=${portalUrl}`); + var message = await receiveMessage; + assert_equals(message.data, "InvalidStateError"); + }, "Calling postMessage after receiving onactivate event should fail"); + + promise_test(() => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "resources/portal-host-post-message-navigate-1.html"; + var count = 0; + var waitForMessages = new Promise((resolve, reject) => { + portal.addEventListener("message", e => { + count++; + if (count == 2) + resolve(); + }); + }); + document.body.appendChild(portal); + return waitForMessages; + }, "postMessage before and after portal navigation should work"); + + const TIMEOUT_DURATION_MS = 1000; + + promise_test(t => new Promise((resolve, reject) => { + const portal = document.createElement('portal'); + portal.src = crossOriginUrl; + portal.onmessage = () => reject('should not have received message'); + document.body.appendChild(portal); + t.step_timeout(resolve, TIMEOUT_DURATION_MS); + }), "postMessage from portal host in cross-origin-portal should be blocked"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-navigate-after-adoption.html b/testing/web-platform/tests/portals/portals-navigate-after-adoption.html new file mode 100644 index 0000000000..1ca1cfb79f --- /dev/null +++ b/testing/web-platform/tests/portals/portals-navigate-after-adoption.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<script> +function nextMessage(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', e => resolve(e), {once: true}); + }); +} + +async function openPortalAndActivate(logic) { + let {port1, port2} = new MessageChannel(); + const w = await openBlankPortalHost(); + try { + const portal = w.document.createElement('portal'); + portal.src = new URL('resources/eval-portal.html?logic=' + encodeURIComponent(logic), location.href); + w.document.body.appendChild(portal); + assert_equals((await nextMessage(portal)).data, 'ready'); + const replyPromise = nextMessage(port2); + await portal.activate({data: {replyPort: port1}, transfer: [port1]}); + port2.start(); + return (await nextMessage(port2)).data; + } finally { + w.close(); + } +} + +promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + let messageFromNewSrc = await openPortalAndActivate( + 'let predecessor = event.adoptPredecessor();' + + 'let readyPromise = new Promise((resolve, reject) => {' + + ' predecessor.onmessage = e => resolve(e.data + " via new src");' + + '});' + + 'predecessor.src = "/portals/resources/eval-portal.html";' + + 'document.body.appendChild(predecessor);' + + 'return readyPromise;'); + assert_equals(messageFromNewSrc, 'ready via new src'); +}, "can set portal src during portalactivate"); +</script> diff --git a/testing/web-platform/tests/portals/portals-nested.html b/testing/web-platform/tests/portals/portals-nested.html new file mode 100644 index 0000000000..b4b396ff8d --- /dev/null +++ b/testing/web-platform/tests/portals/portals-nested.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <script> + promise_test(() => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = "resources/portals-nested-portal.html"; + document.body.appendChild(portal); + var waitForMessage = new Promise((resolve, reject) => { + portal.onmessage = resolve; + }); + return waitForMessage; + }, "nested portals shouldn't crash"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-no-frame-crash.html b/testing/web-platform/tests/portals/portals-no-frame-crash.html new file mode 100644 index 0000000000..cc94a772c3 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-no-frame-crash.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> + test(() => { + let portal = document.createElement("portal"); + let xmlDoc = document.implementation.createDocument("", null); + xmlDoc.appendChild(portal); + }, "inserting a portal element into an XML document shouldn't crash or throw"); + + test(() => { + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + let doc = iframe.contentDocument; + iframe.remove(); + let portal = document.createElement("portal"); + doc.body.appendChild(portal); + }, "inserting a portal element into a detached iframe's document shouldn't crash or throw"); + + test(() => { + let iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + let doc = iframe.contentDocument; + iframe.remove(); + let portal = doc.createElement("portal"); + doc.body.appendChild(portal); + }, "creating a portal element with a detached iframe's document shouldn't crash or throw"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-post-message.sub.html b/testing/web-platform/tests/portals/portals-post-message.sub.html new file mode 100644 index 0000000000..d556dd43d8 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-post-message.sub.html @@ -0,0 +1,189 @@ +<!DOCTYPE html> +<title>Test postMessage on HTMLPortalElement</title> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="resources/stash-utils.sub.js"></script> +<script src="/common/utils.js"></script> +<body> + <input id="input"/> + <script> + const sameOriginUrl = "resources/portal-post-message-portal.html" + const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-post-message-x-origin-portal.html" + + async function createAndInsertPortal(portalSrc) { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement("portal"); + portal.src = portalSrc; + document.body.append(portal); + + var loadPromise = new Promise((resolve, reject) => { + portal.onload = resolve; + }); + await loadPromise; + return portal; + } + + function postMessage(portal, ...postMessageArgs) { + return new Promise((resolve, reject) => { + portal.postMessage(...postMessageArgs); + portal.onmessage = e => { resolve(e.data); }; + }); + } + + function postMessageWithMessagePorts(portal, message) { + return new Promise((resolve, reject) => { + var channel = new MessageChannel(); + channel.port1.onmessage = e => { + channel.port1.close(); + resolve(e.data); + }; + portal.postMessage(message, {transfer: [channel.port2]}); + }); + } + + promise_test(async () => { + var portal = await createAndInsertPortal(sameOriginUrl); + var message = "test message"; + var {origin, data, sourceIsPortalHost} = await postMessage(portal, message); + assert_equals(data, message); + assert_equals(origin, window.location.origin); + assert_true(sourceIsPortalHost); + }, "postMessage message received by portalHost"); + + promise_test(async () => { + var portal = await createAndInsertPortal(sameOriginUrl); + var message = { + prop1: "value1", + prop2: 2.5, + prop3: [1, 2, "3"], + prop4: { + prop4_1: "value4_1" + } + } + var {data} = await postMessage(portal, message); + assert_object_equals(data, message); + }, "postMessage with message object"); + + promise_test(async () => { + var portal = await createAndInsertPortal(sameOriginUrl); + var message = "test message"; + var {data} = await postMessageWithMessagePorts(portal, message); + assert_equals(data, message); + }, "postMessage with message ports and same-origin portal"); + + promise_test(async () => { + var portal = await createAndInsertPortal(sameOriginUrl); + var arrayBuffer = new ArrayBuffer(5); + var int8View = new Int8Array(arrayBuffer); + for (var i = 0; i < int8View.length; i++) + int8View[i] = i; + var message = { + arrayBuffer: arrayBuffer + }; + var {data} = await postMessage(portal, message); + assert_array_equals([0, 1, 2, 3, 4], int8View); + assert_array_equals([0, 1, 2, 3, 4], data.array); + }, "postMessage with array buffer without transfer"); + + promise_test(async () => { + var portal = await createAndInsertPortal(sameOriginUrl); + var arrayBuffer = new ArrayBuffer(5); + var int8View = new Int8Array(arrayBuffer); + for (var i = 0; i < int8View.length; i++) + int8View[i] = i; + var message = { + arrayBuffer: arrayBuffer + }; + var {data} = await postMessage(portal, message, {transfer: [arrayBuffer]}); + assert_equals(int8View.length, 0); + assert_array_equals(data.array, [0, 1, 2, 3, 4]); + }, "postMessage with transferred array buffer"); + + promise_test(async t => { + var portal = await createAndInsertPortal(sameOriginUrl); + + var {gotUserActivation} = await postMessage(portal, "test"); + assert_false(gotUserActivation); + + var {gotUserActivation, userActivation} = await postMessage(portal, "test", {includeUserActivation: true}); + assert_true(gotUserActivation); + assert_false(userActivation.isActive); + assert_false(userActivation.hasBeenActive); + + await test_driver.click(document.getElementById("input")); + assert_true(navigator.userActivation.isActive); + assert_true(navigator.userActivation.hasBeenActive); + + var {userActivation} = await postMessage(portal, "test", {includeUserActivation: true}); + assert_true(userActivation.isActive, "should have sent gesture"); + assert_true(userActivation.hasBeenActive); + }, "postMessage with includeUserActivation"); + + promise_test(async t => { + var portal = document.createElement("portal"); + return promise_rejects_dom(t, "InvalidStateError", + postMessage(portal, "test message")); + }, "cannot call postMessage on portal without portal browsing context"); + + promise_test(async t => { + var portal = await createAndInsertPortal(sameOriginUrl); + return promise_rejects_dom(t, "DataCloneError", + postMessage(portal, document.body)); + }, "postMessage should fail if message serialization fails"); + + promise_test(async t => { + var portal = await createAndInsertPortal(sameOriginUrl); + return promise_rejects_js(t, TypeError, + postMessage(portal, "test", {transfer: [null]})); + }, "postMessage should fail with invalid ports"); + + async function waitForMessage(channelName) { + var bc = new BroadcastChannel(channelName); + return new Promise((resolve, reject) => { + bc.onmessage = e => { + bc.close(); + resolve(e.data); + } + }); + } + + promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + window.open("resources/portal-post-message-before-activate-window.html"); + let {postMessageTS, activateTS} = await waitForMessage( + "portals-post-message-before-activate"); + assert_less_than_equal(postMessageTS, activateTS); + }, "postMessage before activate should work and preserve order"); + + promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + window.open("resources/portal-post-message-during-activate-window.html"); + let error = await waitForMessage("portals-post-message-during-activate"); + assert_equals(error, "InvalidStateError"); + }, "postMessage during activate throws error"); + + promise_test(async t => { + assert_implements("HTMLPortalElement" in self); + window.open("resources/portal-post-message-after-activate-window.html"); + let error = await waitForMessage("portals-post-message-after-activate"); + assert_equals(error, "InvalidStateError"); + }, "postMessage after activate throws error"); + + const TIMEOUT_DURATION_MS = 1000; + + promise_test(async t => { + const key = token(); + const portal = await createAndInsertPortal(`${crossOriginUrl}?key=${key}`); + portal.postMessage('test message'); + t.step_timeout(() => { + StashUtils.putValue(key, 'passed'); + }, TIMEOUT_DURATION_MS); + const result = await StashUtils.takeValue(key); + assert_equals(result, 'passed'); + }, 'postMessage should be blocked for cross-origin portals'); + + </script> +</body> diff --git a/testing/web-platform/tests/portals/portals-referrer-inherit-header.html b/testing/web-platform/tests/portals/portals-referrer-inherit-header.html new file mode 100644 index 0000000000..1fbd88893e --- /dev/null +++ b/testing/web-platform/tests/portals/portals-referrer-inherit-header.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +promise_test(async () => { + assert_implements('HTMLPortalElement' in self, 'HTMLPortalElement is required for this test'); + let portal = document.createElement('portal'); + let referrerPromise = new Promise((resolve, reject) => { + portal.addEventListener('message', e => resolve(e.data), {once: true}); + }); + portal.src = 'resources/postmessage-referrer.sub.html'; + document.body.appendChild(portal); + try { + let {httpReferrer, documentReferrer} = await referrerPromise; + assert_equals(httpReferrer, 'no-http-referrer', 'No HTTP Referer header should be sent'); + assert_equals(documentReferrer, 'no-document-referrer', 'No document.referrer should be present'); + } finally { + document.body.removeChild(portal); + } +}, "portal contents should be loaded with no referrer if document requests it"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-referrer-inherit-header.html.headers b/testing/web-platform/tests/portals/portals-referrer-inherit-header.html.headers new file mode 100644 index 0000000000..7ffbf17d6b --- /dev/null +++ b/testing/web-platform/tests/portals/portals-referrer-inherit-header.html.headers @@ -0,0 +1 @@ +Referrer-Policy: no-referrer diff --git a/testing/web-platform/tests/portals/portals-referrer-inherit-meta.html b/testing/web-platform/tests/portals/portals-referrer-inherit-meta.html new file mode 100644 index 0000000000..e77894cfa4 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-referrer-inherit-meta.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<meta name="referrer" content="no-referrer"> +<body> +<script> +promise_test(async () => { + assert_implements('HTMLPortalElement' in self, 'HTMLPortalElement is required for this test'); + let portal = document.createElement('portal'); + let referrerPromise = new Promise((resolve, reject) => { + portal.addEventListener('message', e => resolve(e.data), {once: true}); + }); + portal.src = 'resources/postmessage-referrer.sub.html'; + document.body.appendChild(portal); + try { + let {httpReferrer, documentReferrer} = await referrerPromise; + assert_equals(httpReferrer, 'no-http-referrer', 'No HTTP Referer header should be sent'); + assert_equals(documentReferrer, 'no-document-referrer', 'No document.referrer should be present'); + } finally { + document.body.removeChild(portal); + } +}, "portal contents should be loaded with no referrer if document requests it"); +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-referrer.html b/testing/web-platform/tests/portals/portals-referrer.html new file mode 100644 index 0000000000..4cd3b90895 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-referrer.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +promise_test(async () => { + assert_implements('HTMLPortalElement' in self, 'HTMLPortalElement is required for this test'); + let portal = document.createElement('portal'); + let referrerPromise = new Promise((resolve, reject) => { + portal.addEventListener('message', e => resolve(e.data), {once: true}); + }); + portal.src = 'resources/postmessage-referrer.sub.html'; + document.body.appendChild(portal); + try { + let {httpReferrer, documentReferrer} = await referrerPromise; + assert_equals(httpReferrer, location.href, 'HTTP Referer header should be sent by default'); + assert_equals(documentReferrer, location.href, 'document.referrer should be present by default'); + } finally { + document.body.removeChild(portal); + } +}, "portal contents should be loaded with referrer"); + +promise_test(async () => { + assert_implements('HTMLPortalElement' in self, 'HTMLPortalElement is required for this test'); + let portal = document.createElement('portal'); + portal.referrerPolicy = 'no-referrer'; + let referrerPromise = new Promise((resolve, reject) => { + portal.addEventListener('message', e => resolve(e.data), {once: true}); + }); + portal.src = 'resources/postmessage-referrer.sub.html'; + document.body.appendChild(portal); + try { + let {httpReferrer, documentReferrer} = await referrerPromise; + assert_equals(httpReferrer, 'no-http-referrer', 'No HTTP Referer header should be sent'); + assert_equals(documentReferrer, 'no-document-referrer', 'No document.referrer should be present'); + } finally { + document.body.removeChild(portal); + } +}, "portal contents should be loaded with no referrer if referrerpolicy=no-referrer"); + +promise_test(async () => { + assert_implements('HTMLPortalElement' in self, 'HTMLPortalElement is required for this test'); + let portal = document.createElement('portal'); + portal.referrerPolicy = 'origin'; + let referrerPromise = new Promise((resolve, reject) => { + portal.addEventListener('message', e => resolve(e.data), {once: true}); + }); + portal.src = 'resources/postmessage-referrer.sub.html'; + document.body.appendChild(portal); + try { + let {httpReferrer, documentReferrer} = await referrerPromise; + assert_equals(httpReferrer, location.origin + '/', 'HTTP Referer header should contain origin'); + assert_equals(documentReferrer, location.origin + '/', 'document.referrer should contain origin'); + } finally { + document.body.removeChild(portal); + } +}, "portal contents should be loaded with origin only if referrerpolicy=origin"); + +// This is not exhaustive coverage of all possible policies, which are tested elsewhere. +</script> +</body> diff --git a/testing/web-platform/tests/portals/portals-rendering.html b/testing/web-platform/tests/portals/portals-rendering.html new file mode 100644 index 0000000000..229dacf4e6 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-rendering.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>Portals rendering test</title> +<link rel="match" href="references/portals-rendering.html"> +<body></body> +<script> +if (!("HTMLPortalElement" in self)) { + document.body.textContent = "PRECONDITION FAILED"; + document.documentElement.classList.remove('reftest-wait'); +} else { + var portal = document.createElement('portal'); + portal.src = 'resources/portals-rendering-portal.html'; + portal.style = 'background-color: red; width: 100px; height: 100px'; + portal.onmessage = e => { + window.requestAnimationFrame(function(ts) { + document.documentElement.classList.remove('reftest-wait'); + }); + }; + document.body.appendChild(portal); +} +</script> + diff --git a/testing/web-platform/tests/portals/portals-repeated-activate.html b/testing/web-platform/tests/portals/portals-repeated-activate.html new file mode 100644 index 0000000000..f2f36cb768 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-repeated-activate.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + async_test(t => { + assert_implements("HTMLPortalElement" in self); + let win = window.open("resources/portal-repeated-activate-window.html"); + win.onload = () => win.activate(); + window.onmessage = t.step_func_done(() => {}); + }, "test activation in page that has been reactivated") +</script> diff --git a/testing/web-platform/tests/portals/portals-set-src-after-activate.html b/testing/web-platform/tests/portals/portals-set-src-after-activate.html new file mode 100644 index 0000000000..e485ef4d51 --- /dev/null +++ b/testing/web-platform/tests/portals/portals-set-src-after-activate.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<script> +function nextMessage(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', e => resolve(e), {once: true}); + }); +} + +promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + const w = await openBlankPortalHost(); + try { + const portal = w.document.createElement('portal'); + portal.src = new URL('resources/simple-portal-adopts-predecessor.html', location.href); + w.document.body.appendChild(portal); + assert_equals((await nextMessage(portal)).data, 'ready'); + + // Intentionally don't await activation finishing; this should work + // even if activation is ongoing. + let activateDone = portal.activate(); + + // TODO(jbroman): It shouldn't be necessary to reinsert the element to + // navigate it again, either. + w.document.body.removeChild(portal); + portal.src = new URL('resources/simple-portal.html', location.href); + w.document.body.appendChild(portal); + assert_equals((await nextMessage(portal)).data, 'ready'); + + // But activation should still resolve, eventually. + await activateDone; + } finally { + w.close(); + } +}, "Tests that a portal element can be fully reused after activate has detached it"); +</script> diff --git a/testing/web-platform/tests/portals/predecessor-fires-unload.html b/testing/web-platform/tests/portals/predecessor-fires-unload.html new file mode 100644 index 0000000000..cb6d98c01d --- /dev/null +++ b/testing/web-platform/tests/portals/predecessor-fires-unload.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/open-blank-host.js"></script> +<script> +function nextEvent(target, type) { + return new Promise((resolve, reject) => target.addEventListener(type, e => resolve(e), {once: true})); +} + +function timePasses(delay) { + return new Promise((resolve, reject) => step_timeout(() => resolve(), delay)); +} + +promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + const w = await openBlankPortalHost(); + try { + const portal = w.document.createElement('portal'); + portal.src = new URL('resources/simple-portal.html', location.href); + w.document.body.appendChild(portal); + await nextEvent(portal, 'load'); + const pagehideFired = nextEvent(w, 'pagehide'); + const unloadFired = nextEvent(w, 'unload'); + await portal.activate(); + assert_true((await pagehideFired) instanceof w.PageTransitionEvent); + assert_true((await unloadFired) instanceof w.Event); + } finally { + w.close(); + } +}, "pagehide and unload should fire if the predecessor is not adopted"); + +promise_test(async () => { + assert_implements("HTMLPortalElement" in self); + localStorage.setItem('predecessor-fires-unload-events', ''); + window.open('resources/predecessor-fires-unload-watch-unload.html', '_blank', 'noopener'); + while (localStorage.getItem('predecessor-fires-unload-events') != 'pagehide unload') { + await timePasses(50); + } +}, "pagehide and unload should fire if the predecessor is not adopted, even without a window/opener association"); +</script> diff --git a/testing/web-platform/tests/portals/references/portals-rendering.html b/testing/web-platform/tests/portals/references/portals-rendering.html new file mode 100644 index 0000000000..4a8414ab56 --- /dev/null +++ b/testing/web-platform/tests/portals/references/portals-rendering.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<title>Portals rendering test</title> +<body> + <div style="background-color: green; width: 100px; height: 100px"> +</body> diff --git a/testing/web-platform/tests/portals/resources/attempt-portal-load.html b/testing/web-platform/tests/portals/resources/attempt-portal-load.html new file mode 100644 index 0000000000..183178006f --- /dev/null +++ b/testing/web-platform/tests/portals/resources/attempt-portal-load.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<body> +<script> +portalLoaded = new Promise((resolve, reject) => { + let portal = document.createElement('portal'); + portal.src = 'simple-portal.html'; + portal.onload = resolve; + document.body.appendChild(portal); +}); +</script> +</body> diff --git a/testing/web-platform/tests/portals/resources/blank-host.html b/testing/web-platform/tests/portals/resources/blank-host.html new file mode 100644 index 0000000000..d9f3a61eb8 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/blank-host.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<!-- + This is a blank page used when a test needs a new window to host and activate + a portal. Tests cannot simply use window.open() without a URL as about:blank + may not host a portal. +--> +<body> +</body> diff --git a/testing/web-platform/tests/portals/resources/eval-portal.html b/testing/web-platform/tests/portals/resources/eval-portal.html new file mode 100644 index 0000000000..a473501b01 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/eval-portal.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> +let logic = new Function('event', (new URL(location)).searchParams.get('logic')); +onload = () => window.portalHost.postMessage('ready'); +onportalactivate = event => { + Promise.resolve(event) + .then(logic) + .then(reply => event.data.replyPort.postMessage(reply)); +}; +</script> diff --git a/testing/web-platform/tests/portals/resources/focus-page-with-autofocus.html b/testing/web-platform/tests/portals/resources/focus-page-with-autofocus.html new file mode 100644 index 0000000000..d498ef6335 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/focus-page-with-autofocus.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<body> + <button id="one">one</button> + <button id="two" autofocus>two</button> + <button id="three">three</button> + <script> + function messageHandler(e) { + if (e.data === 'check-active-element') { + window.requestAnimationFrame(() => { + let autofocusedButton = document.querySelector('#two'); + e.source.postMessage(document.activeElement === autofocusedButton); + }); + } + } + + window.portalHost.onmessage = messageHandler; + window.onportalactivate = e => { + let portal = e.adoptPredecessor(); + portal.onmessage = messageHandler; + document.body.appendChild(portal); + } + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/focus-page-with-button.html b/testing/web-platform/tests/portals/resources/focus-page-with-button.html new file mode 100644 index 0000000000..81ed5465ab --- /dev/null +++ b/testing/web-platform/tests/portals/resources/focus-page-with-button.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<body> + <script> + function handleMessage(e) { + if (e.data == "focus") { + let button = document.querySelector("button"); + button.onfocus = () => e.source.postMessage({focused: true}, {targetOrigin: "*"}); + button.focus(); + } + + if (e.data == "focus-update-active-element") { + let button = document.querySelector("button"); + button.focus(); + e.source.postMessage({activeElementUpdated: document.activeElement === button}, {targetOrigin: "*"}); + } + } + + if (window.portalHost) + window.portalHost.onmessage = handleMessage; + + window.onmessage = handleMessage; + + window.onportalactivate = e => { + let portal = e.adoptPredecessor(); + document.body.appendChild(portal); + portal.onmessage = handleMessage; + }; + + window.onfocus = () => { + if (window.portalHost) + window.portalHost.postMessage("window focused"); + }; + </script> + <button>A</button> +</body> diff --git a/testing/web-platform/tests/portals/resources/focus-page-with-x-origin-iframe.sub.html b/testing/web-platform/tests/portals/resources/focus-page-with-x-origin-iframe.sub.html new file mode 100644 index 0000000000..df7974e75b --- /dev/null +++ b/testing/web-platform/tests/portals/resources/focus-page-with-x-origin-iframe.sub.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<body> + <script> + async function handleMessage(e) { + if (e.data == "focus" || e.data == "focus-update-active-element") { + let iframe = document.querySelector("iframe"); + iframe.contentWindow.postMessage(e.data, "*"); + } + } + + if (window.portalHost) + window.portalHost.onmessage = handleMessage; + + window.onportalactivate = e => { + var portal = e.adoptPredecessor(); + document.body.appendChild(portal); + portal.onmessage = handleMessage; + } + + window.onmessage = e => { + if (window.portalHost) + window.portalHost.postMessage(e.data); + else + document.querySelector("portal").postMessage(e.data); + } + </script> + <iframe src="http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/focus-page-with-button.html"></iframe> +</body> diff --git a/testing/web-platform/tests/portals/resources/invalid.asis b/testing/web-platform/tests/portals/resources/invalid.asis new file mode 100644 index 0000000000..20f7c7f7e5 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/invalid.asis @@ -0,0 +1 @@ +This is an invalid HTTP response used to produce a network error. diff --git a/testing/web-platform/tests/portals/resources/open-blank-host.js b/testing/web-platform/tests/portals/resources/open-blank-host.js new file mode 100644 index 0000000000..f7580bd152 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/open-blank-host.js @@ -0,0 +1,14 @@ +// Portal tests often need to create portals in a context other than the one +// in which the tests are running. This is because the host context may be +// discarded during the course of the test. + +// Opens a blank page for use as a portal host. +// Tests cannot simply use window.open() without a URL as about:blank may not +// host a portal. +async function openBlankPortalHost() { + let hostWindow = window.open('/portals/resources/blank-host.html'); + await new Promise((resolve) => { + hostWindow.addEventListener('load', resolve, {once: true}); + }); + return hostWindow; +} diff --git a/testing/web-platform/tests/portals/resources/portal-activate-broadcastchannel.html b/testing/web-platform/tests/portals/resources/portal-activate-broadcastchannel.html new file mode 100644 index 0000000000..b922afaec2 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-broadcastchannel.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> +onportalactivate = e => { + let bc = new BroadcastChannel(new URL(location).searchParams.get('bc')); + bc.postMessage({event: 'portalactivate', data: e.data}); + bc.close(); +}; +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-data-portal.html b/testing/web-platform/tests/portals/resources/portal-activate-data-portal.html new file mode 100644 index 0000000000..0842ad82ef --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-data-portal.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> +let logic = new Function('event', (new URL(location)).searchParams.get('logic')); +onload = () => window.portalHost.postMessage('ready'); +onportalactivate = event => { + var portal = event.adoptPredecessor(); + portal.postMessage(logic(event)); +}; +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-event-portal.html b/testing/web-platform/tests/portals/resources/portal-activate-event-portal.html new file mode 100644 index 0000000000..6de5aafca7 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-event-portal.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<title>Tests that the PortalActivateEvent is dispatched when a portal is activated</title> +<script> + var test = (new URL(location)).searchParams.get("test"); + + function portalActivate(e) { + var bc = new BroadcastChannel("test-" + test); + bc.postMessage("passed"); + bc.close(); + } + + if (test == "bodyeventhandler") { + document.write('<body onportalactivate="portalActivate()"></body>'); + } else if (test == "eventhandler") { + window.onportalactivate = portalActivate; + } else if (test == "eventlistener") { + window.addEventListener("portalactivate", portalActivate); + } + + window.portalHost.postMessage("loaded"); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-in-handler.html b/testing/web-platform/tests/portals/resources/portal-activate-in-handler.html new file mode 100644 index 0000000000..746ffa2b39 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-in-handler.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html> + <head> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/testdriver.js"></script> + <script src="/resources/testdriver-vendor.js"></script> + </head> + <body> + </body> + <script> + // This page is reused with a different query parameter indicating which + // handler to register and activate a portal from. + const handler_name = window.location.search.substring(1); + + const portal_element = document.createElement('portal'); + portal_element.src = 'simple-portal.html'; + document.body.appendChild(portal_element); + + let page_loaded = false; + let portal_loaded = false; + + function notifyReady() { + if (page_loaded && portal_loaded) { + window.opener.postMessage('done', '*'); + } + } + + portal_element.addEventListener('load', () => { + portal_loaded = true; + notifyReady(); + }); + + window.addEventListener('load', () => { + page_loaded = true; + notifyReady(); + }); + + // This will be used to let the parent page know the handler has run and + // |portal_promise| is now valid. + window.opener.handler_called_promise = new Promise((resolve) => { + window.addEventListener(handler_name, () => { + window.opener.portal_promise = portal_element.activate(); + + // Let the parent page know it can now look at |portal_promise|. + resolve(); + }, {once: true}); + }); + + </script> +</html> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-inside-portal.html b/testing/web-platform/tests/portals/resources/portal-activate-inside-portal.html new file mode 100644 index 0000000000..ff8bead324 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-inside-portal.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "simple-portal.html"; + portal.onload = () => { + portal.activate().catch(e => window.portalHost.postMessage(e.name)); + } + document.body.appendChild(portal); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-twice-window-1.html b/testing/web-platform/tests/portals/resources/portal-activate-twice-window-1.html new file mode 100644 index 0000000000..fbc5a6e93d --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-twice-window-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "simple-portal.html" + portal.onload = () => { + portal.activate(); + portal.activate().catch(e => window.opener.postMessage(e.name, "*")); + } + document.body.append(portal); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-activate-twice-window-2.html b/testing/web-platform/tests/portals/resources/portal-activate-twice-window-2.html new file mode 100644 index 0000000000..6ba8dc5839 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-activate-twice-window-2.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<body> + <script> + var portal1 = document.createElement("portal"); + portal1.src = "simple-portal.html" + var portal2 = document.createElement("portal"); + portal2.src = "simple-portal.html" + + var waitForPortalToLoad = portal => new Promise((resolve, reject) => { + portal.onload = resolve; + }); + + Promise.all([waitForPortalToLoad(portal1), + waitForPortalToLoad(portal2)]).then(() => { + portal1.activate(); + portal2.activate().catch(e => window.opener.postMessage(e.name, "*")); + }); + + document.body.append(portal1); + document.body.append(portal2); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-close-window.html b/testing/web-platform/tests/portals/resources/portal-close-window.html new file mode 100644 index 0000000000..a12af3cd7a --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-close-window.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> +onload = () => { + window.close(); + window.portalHost.postMessage(window.closed); +}; +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-embed-and-activate.html b/testing/web-platform/tests/portals/resources/portal-embed-and-activate.html new file mode 100644 index 0000000000..04f15b7fda --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-embed-and-activate.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<!-- + Embeds a portal (src specified by query parameter "url") and activates it after + receiving a message from the portal. +--> +</title> +<body> + <script> + var searchParams = new URL(location).searchParams; + let portal = document.createElement("portal"); + portal.src = searchParams.get("url"); + portal.onmessage = () => { portal.activate(); } + document.body.appendChild(portal); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-host-cross-origin-navigate.sub.html b/testing/web-platform/tests/portals/resources/portal-host-cross-origin-navigate.sub.html new file mode 100644 index 0000000000..26f655a0db --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-cross-origin-navigate.sub.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<body> + <script> + let key = (new URLSearchParams(window.location.search)).get('key'); + window.location.href = `http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host.html?key=${key}`; + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-host-hidden-after-activation-portal.html b/testing/web-platform/tests/portals/resources/portal-host-hidden-after-activation-portal.html new file mode 100644 index 0000000000..491d184f97 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-hidden-after-activation-portal.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + window.addEventListener("portalactivate", function(e) { + var bc = new BroadcastChannel("portals-host-hidden-after-activation"); + bc.postMessage({ hasHost: !!window.portalHost }); + bc.close(); + }); + + var bc = new BroadcastChannel("portals-host-hidden-after-activation"); + bc.postMessage({hasHost: !!window.portalHost }); + bc.close(); + + window.portalHost.postMessage("loaded"); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host-post-message-after-activate.html b/testing/web-platform/tests/portals/resources/portal-host-post-message-after-activate.html new file mode 100644 index 0000000000..7b03ac0294 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-post-message-after-activate.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script> + window.portalHost.postMessage("loaded"); + + var ph = window.portalHost; + + window.onportalactivate = e => { + var exception_name = "" + try { + ph.postMessage("message"); + } + catch (error) { + exception_name = error.name; + } + bc = new BroadcastChannel("portal-host-post-message-after-activate"); + bc.postMessage(exception_name, "*"); + bc.close(); + }; +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-1.html b/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-1.html new file mode 100644 index 0000000000..a59144e7e1 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.portalHost.postMessage("loaded"); + window.location.href = "portal-host-post-message-navigate-2.html" +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-2.html b/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-2.html new file mode 100644 index 0000000000..571c4f122e --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-2.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.portalHost.postMessage("loaded"); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host-post-message-x-origin.html b/testing/web-platform/tests/portals/resources/portal-host-post-message-x-origin.html new file mode 100644 index 0000000000..6cbc7f4b88 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-post-message-x-origin.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.portalHost.postMessage('test message'); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host-post-message.html b/testing/web-platform/tests/portals/resources/portal-host-post-message.html new file mode 100644 index 0000000000..1935ee898e --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host-post-message.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<script> + function postMessageWithMessagePorts() { + var channel = new MessageChannel(); + channel.port1.onmessage = e => { + e.ports[0].postMessage("received"); + } + window.portalHost.postMessage("sending port", {transfer: [channel.port2]}); + } + + function postMessageWithArrayBuffer(array, withTransfer) { + var arrayBuffer = new Int8Array(array).buffer; + if (withTransfer) { + window.portalHost.postMessage({arrayBuffer}, {transfer: [arrayBuffer]}); + } else { + window.portalHost.postMessage({arrayBuffer}); + } + } + + function postMessageAndCatchException(...params) { + try { + window.portalHost.postMessage(...params); + } catch (e) { + window.portalHost.postMessage({errorType: e.name}); + } + } + + window.portalHost.addEventListener("message", e => { + if (e.data.type) { + var type = e.data.type; + switch (type) { + case "message-port": + postMessageWithMessagePorts(); + return; + case "array-buffer-without-transfer": + postMessageWithArrayBuffer(e.data.array, false); + return; + case "array-buffer-with-transfer": + postMessageWithArrayBuffer(e.data.array, true); + return; + case "invalid-message": + postMessageAndCatchException(document.body); + return; + case "invalid-port": + postMessageAndCatchException("", {transfer: [null]}); + return; + } + } + window.portalHost.postMessage(...e.data); + }); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-host.html b/testing/web-platform/tests/portals/resources/portal-host.html new file mode 100644 index 0000000000..e577208236 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-host.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script src="stash-utils.sub.js"></script> +<body> + <script> + let queryParams = new URLSearchParams(window.location.search); + let key = queryParams.get('key'); + if (key) { + StashUtils.putValue(key, window.portalHost ? "passed" : "failed"); + } + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-inside-iframe.html b/testing/web-platform/tests/portals/resources/portal-inside-iframe.html new file mode 100644 index 0000000000..5db75d5b5f --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-inside-iframe.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<body> + <portal id="portal" /> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-after-activate-window.html b/testing/web-platform/tests/portals/resources/portal-post-message-after-activate-window.html new file mode 100644 index 0000000000..73d2c11558 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-after-activate-window.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "portal-post-message-portal.html"; + document.body.appendChild(portal); + + portal.onload = () => { + portal.activate().then(() => { + error = ""; + try { + portal.postMessage("message"); + } + catch(err) { + error = err.name; + } + bc = new BroadcastChannel("portals-post-message-after-activate"); + bc.postMessage(error); + bc.close(); + }); + } + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-portal.html b/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-portal.html new file mode 100644 index 0000000000..d34875f981 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-portal.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<script> + var postMessagePromise = new Promise((resolve, reject) => { + window.portalHost.addEventListener("message", () => { + resolve(performance.now()); + }); + }); + + var activatePromise = new Promise((resolve, reject) => { + window.onportalactivate = () => { + resolve(performance.now()); + } + }); + + Promise.all([postMessagePromise, activatePromise]) + .then(values => { + bc = new BroadcastChannel("portals-post-message-before-activate"); + bc.postMessage({ + postMessageTS: values[0], + activateTS: values[1] + }); + bc.close(); + }); +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-window.html b/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-window.html new file mode 100644 index 0000000000..6389829c7c --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-before-activate-window.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "portal-post-message-before-activate-portal.html"; + document.body.appendChild(portal); + + portal.onload = () => { + portal.postMessage("message"); + portal.activate(); + } + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-during-activate-window.html b/testing/web-platform/tests/portals/resources/portal-post-message-during-activate-window.html new file mode 100644 index 0000000000..6e220277d9 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-during-activate-window.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "simple-portal.html"; + document.body.appendChild(portal); + + portal.onload = () => { + portal.activate(); + error = ""; + try { + portal.postMessage("message"); + } catch (err) { + error = err.name; + } + bc = new BroadcastChannel("portals-post-message-during-activate"); + bc.postMessage(error); + bc.close(); + } + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-portal.html b/testing/web-platform/tests/portals/resources/portal-post-message-portal.html new file mode 100644 index 0000000000..e83ae56e08 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-portal.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> + window.portalHost.onmessage = e => { + var message = { + origin: e.origin, + data: e.data, + sourceIsPortalHost: e.source === window.portalHost, + gotUserActivation: !!e.userActivation, + userActivation: { + isActive: e.userActivation && e.userActivation.isActive, + hasBeenActive: e.userActivation && e.userActivation.hasBeenActive + } + }; + + if (e.data.arrayBuffer) { + message.data = { + array: Array.from(new Uint8Array(e.data.arrayBuffer)) + }; + } + + if (e.ports.length > 0) { + e.ports[0].postMessage(message); + e.ports[0].close(); + return; + } + + window.portalHost.postMessage(message); + }; +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-post-message-x-origin-portal.html b/testing/web-platform/tests/portals/resources/portal-post-message-x-origin-portal.html new file mode 100644 index 0000000000..57631f385c --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-post-message-x-origin-portal.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script src="stash-utils.sub.js"></script> +<script> + const queryParams = new URLSearchParams(window.location.search); + const key = queryParams.get('key'); + if (key) { + window.portalHost.onmessage = () => { + StashUtils.putValue(key, 'failed'); + }; + } +</script> diff --git a/testing/web-platform/tests/portals/resources/portal-repeated-activate-window.html b/testing/web-platform/tests/portals/resources/portal-repeated-activate-window.html new file mode 100644 index 0000000000..e716034eff --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portal-repeated-activate-window.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<body> + <portal src="simple-portal-adopts-and-activates-predecessor.html"> + <script> + function activate() { + var portal = document.querySelector("portal"); + portal.activate().then(() => document.body.removeChild(portal)); + } + + var count = 0; + window.onportalactivate = e => { + ++count; + if (count == 1) { + e.adoptPredecessor().activate(); + } else { + window.opener.postMessage("done", "*"); + } + }; + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portals-adopt-predecessor-portal.html b/testing/web-platform/tests/portals/resources/portals-adopt-predecessor-portal.html new file mode 100644 index 0000000000..b838b38be1 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portals-adopt-predecessor-portal.html @@ -0,0 +1,77 @@ +<!doctype html> +<script> + var searchParams = new URL(location).searchParams; + var test = searchParams.get("test"); + + window.onportalactivate = function(e) { + if (test == "adopt-once") { + var portal = e.adoptPredecessor(); + document.body.appendChild(portal); + + if (portal instanceof HTMLPortalElement) { + portal.postMessage("adopted"); + } + } + if (test == "adopt-twice") { + var portal = e.adoptPredecessor(); + document.body.appendChild(portal); + + try { + e.adoptPredecessor(); + } catch(e) { + if (e.name == "InvalidStateError") { + portal.postMessage("passed"); + } + } + } + if (test == "adopt-after-event") { + setTimeout(function() { + try { + e.adoptPredecessor(); + } catch(e) { + if (e.name == "InvalidStateError") { + var bc_test = new BroadcastChannel(`test-${test}`); + bc_test.postMessage("passed"); + bc_test.close(); + } + } + }); + } + if (test == "adopt-and-activate") { + var portal = e.adoptPredecessor(); + portal.activate(); + } + if (test == "adopt-attach-remove") { + var portal = e.adoptPredecessor(); + document.body.appendChild(portal); + setTimeout(() => { + document.body.removeChild(portal); + var bc_test = new BroadcastChannel(`test-${test}`); + bc_test.postMessage("passed"); + bc_test.close(); + }); + } + if (test == "adopt-and-discard") { + var portal = e.adoptPredecessor(); + setTimeout(() => { + // portal should be inactive and activate should fail. + portal.activate().catch(e => { + if (e.name == "InvalidStateError") { + var bc_test = new BroadcastChannel(`test-${test}`); + bc_test.postMessage("passed"); + bc_test.close(); + } + }); + }); + } + if (test == "adopt-to-disconnected-node") { + var portal = e.adoptPredecessor(); + document.body.appendChild(portal); + var node = document.createElement("div"); + node.appendChild(portal); + var bc_test = new BroadcastChannel(`test-${test}`); + bc_test.postMessage("passed"); + bc_test.close(); + } + } +</script> diff --git a/testing/web-platform/tests/portals/resources/portals-adopt-predecessor.html b/testing/web-platform/tests/portals/resources/portals-adopt-predecessor.html new file mode 100644 index 0000000000..66d47d12ac --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portals-adopt-predecessor.html @@ -0,0 +1,20 @@ +<!doctype html> +<body> +</body> +<script> + var searchParams = new URL(location).searchParams; + var test = searchParams.get("test"); + var portal = document.createElement("portal"); + portal.src = `portals-adopt-predecessor-portal.html?test=${test}`; + portal.onload = () => { + portal.activate().then(() => { + window.addEventListener("portalactivate", e => { + window.opener.postMessage({test, message: "passed"}, "*"); + }); + window.portalHost.addEventListener("message", e => { + window.opener.postMessage({test, message: e.data}, "*"); + }); + }); + } + document.body.appendChild(portal); +</script> diff --git a/testing/web-platform/tests/portals/resources/portals-nested-portal.html b/testing/web-platform/tests/portals/resources/portals-nested-portal.html new file mode 100644 index 0000000000..278b32eea0 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portals-nested-portal.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<body> + <script> + var portal = document.createElement("portal"); + portal.src = "simple-portal.html"; + portal.onload = e => window.portalHost.postMessage(e.data); + document.body.appendChild(portal); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/portals-rendering-portal.html b/testing/web-platform/tests/portals/resources/portals-rendering-portal.html new file mode 100644 index 0000000000..31b3f4a990 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/portals-rendering-portal.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<body style="background-color: green"> + <script> + window.requestAnimationFrame(function(ts) { + window.portalHost.postMessage("loaded"); + }); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/postmessage-referrer.sub.html b/testing/web-platform/tests/portals/resources/postmessage-referrer.sub.html new file mode 100644 index 0000000000..c3837dc79d --- /dev/null +++ b/testing/web-platform/tests/portals/resources/postmessage-referrer.sub.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> + let message = { + httpReferrer: '{{header_or_default(Referer, no-http-referrer)}}', + documentReferrer: document.referrer || 'no-document-referrer', + }; + window.portalHost.postMessage(message); +</script> diff --git a/testing/web-platform/tests/portals/resources/predecessor-fires-unload-watch-unload.html b/testing/web-platform/tests/portals/resources/predecessor-fires-unload-watch-unload.html new file mode 100644 index 0000000000..f58da48ca1 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/predecessor-fires-unload-watch-unload.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<body> +<script> +function nextEvent(target, type) { + return new Promise((resolve, reject) => target.addEventListener(type, e => resolve(e), {once: true})); +} + +onload = async function() { + const portal = document.createElement('portal'); + portal.src = new URL('simple-portal.html', location.href); + document.body.appendChild(portal); + await nextEvent(portal, 'load'); + + let firedEvents = []; + for (let type of ['pagehide', 'unload']) { + nextEvent(window, type).then(() => { + firedEvents.push(type); + localStorage.setItem('predecessor-fires-unload-events', firedEvents.join(' ')); + }); + } + + portal.activate(); +}; +</script> +</body> diff --git a/testing/web-platform/tests/portals/resources/simple-portal-adopts-and-activates-predecessor.html b/testing/web-platform/tests/portals/resources/simple-portal-adopts-and-activates-predecessor.html new file mode 100644 index 0000000000..56bfd10f64 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/simple-portal-adopts-and-activates-predecessor.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<body> + <script> + window.onportalactivate = e => e.adoptPredecessor().activate(); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/simple-portal-adopts-predecessor.html b/testing/web-platform/tests/portals/resources/simple-portal-adopts-predecessor.html new file mode 100644 index 0000000000..b5ea9f029d --- /dev/null +++ b/testing/web-platform/tests/portals/resources/simple-portal-adopts-predecessor.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<body> + <script> + window.portalHost.postMessage("ready"); + onportalactivate = e => document.body.appendChild(e.adoptPredecessor()); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/simple-portal.html b/testing/web-platform/tests/portals/resources/simple-portal.html new file mode 100644 index 0000000000..7d7b678cad --- /dev/null +++ b/testing/web-platform/tests/portals/resources/simple-portal.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<body> + <script> + window.portalHost.postMessage("ready"); + </script> +</body> diff --git a/testing/web-platform/tests/portals/resources/stash-utils.sub.js b/testing/web-platform/tests/portals/resources/stash-utils.sub.js new file mode 100644 index 0000000000..30efe83633 --- /dev/null +++ b/testing/web-platform/tests/portals/resources/stash-utils.sub.js @@ -0,0 +1,43 @@ +const STASH_RESPONDER = "wss://{{host}}:{{ports[wss][0]}}/stash_responder_blocking"; + +class StashUtils { + /** + * Sends a request to store (|key|, |tuple|) in Stash + * (https://web-platform-tests.org/tools/wptserve/docs/stash.html). + * @param {string} key A UUID that acts as a key that can be used to retrieve |value| later. + * @param {string} value Value to be stored in Stash. + * @returns {Promise} Promise that resolves once the server responds. + */ + static putValue(key, value) { + return new Promise(resolve => { + const ws = new WebSocket(STASH_RESPONDER); + ws.onopen = () => { + ws.send(JSON.stringify({action: 'set', key: key, value: value})); + }; + ws.onmessage = e => { + ws.close(); + resolve(); + }; + }); + } + + /** + * Retrieves value associated with |key| in Stash. If no value has been + * associated with |key| yet, the method waits for putValue to be called with + * |key|, and a value to be associated, before resolving the return promise. + * @param {string} key A UUID that uniquely identifies the value to retrieve. + * @returns {Promise<string>} A promise that resolves with the value associated with |key|. + */ + static takeValue(key) { + return new Promise(resolve => { + const ws = new WebSocket(STASH_RESPONDER); + ws.onopen = () => { + ws.send(JSON.stringify({action: 'get', key: key})); + }; + ws.onmessage = e => { + ws.close(); + resolve(JSON.parse(e.data).value); + }; + }); + } +} diff --git a/testing/web-platform/tests/portals/xfo/portals-xfo-deny.sub.html b/testing/web-platform/tests/portals/xfo/portals-xfo-deny.sub.html new file mode 100644 index 0000000000..efc925276c --- /dev/null +++ b/testing/web-platform/tests/portals/xfo/portals-xfo-deny.sub.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +// TODO(jbroman): Ideally these would also check that the portal element gets a +// completion event. + +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement('portal'); + portal.src = "/portals/xfo/resources/xfo-deny.asis"; + portal.onmessage = t.unreached_func("should not have received a message"); + document.body.appendChild(portal); + t.add_cleanup(() => portal.remove()); + t.step_timeout(() => t.done(), 2000); +}, "`XFO: DENY` blocks same-origin portals."); + +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement('portal'); + portal.src = "http://{{domains[www]}}:{{ports[http][0]}}/portals/xfo/resources/xfo-deny.asis"; + portal.onmessage = t.unreached_func("should not have received a message"); + document.body.appendChild(portal); + t.add_cleanup(() => portal.remove()); + t.step_timeout(() => t.done(), 2000); +}, "`XFO: DENY` blocks cross-origin portals."); + +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement('portal'); + portal.src = "/portals/xfo/resources/xfo-deny.asis"; + portal.onmessage = t.unreached_func("should not have received a message"); + document.body.appendChild(portal); + t.add_cleanup(() => portal.remove()); + t.step_timeout(async () => { + await promise_rejects_dom(t, 'InvalidStateError', portal.activate()); + t.done(); + }, 2000); +}, "Portals blocked by `XFO: DENY` cannot be activated."); +</script> +</body> diff --git a/testing/web-platform/tests/portals/xfo/portals-xfo-sameorigin.html b/testing/web-platform/tests/portals/xfo/portals-xfo-sameorigin.html new file mode 100644 index 0000000000..2482476782 --- /dev/null +++ b/testing/web-platform/tests/portals/xfo/portals-xfo-sameorigin.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta name="timeout" content="long"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +async_test(t => { + assert_implements("HTMLPortalElement" in self); + var portal = document.createElement('portal'); + portal.src = get_host_info().HTTP_REMOTE_HOST + "/portals/xfo/resources/xfo-sameorigin.asis"; + portal.onmessage = t.unreached_func("should not have received a message"); + document.body.appendChild(portal); + t.add_cleanup(() => portal.remove()); + t.step_timeout(() => t.done(), 2000); +}, "`XFO: SAMEORIGIN` blocks cross-origin portals."); +</script> +</body> diff --git a/testing/web-platform/tests/portals/xfo/resources/xfo-deny.asis b/testing/web-platform/tests/portals/xfo/resources/xfo-deny.asis new file mode 100644 index 0000000000..7779830852 --- /dev/null +++ b/testing/web-platform/tests/portals/xfo/resources/xfo-deny.asis @@ -0,0 +1,8 @@ +HTTP/1.1 200 OK +Content-Type: text/html +X-Frame-Options: DENY + +<!DOCTYPE html> +<script> +window.portalHost.postMessage('loaded', '*'); +</script> diff --git a/testing/web-platform/tests/portals/xfo/resources/xfo-sameorigin.asis b/testing/web-platform/tests/portals/xfo/resources/xfo-sameorigin.asis new file mode 100644 index 0000000000..8f3982bd84 --- /dev/null +++ b/testing/web-platform/tests/portals/xfo/resources/xfo-sameorigin.asis @@ -0,0 +1,8 @@ +HTTP/1.1 200 OK +Content-Type: text/html +X-Frame-Options: SAMEORIGIN + +<!DOCTYPE html> +<script> +window.portalHost.postMessage('loaded', '*'); +</script> |