summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/portals
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/portals')
-rw-r--r--testing/web-platform/tests/portals/META.yml4
-rw-r--r--testing/web-platform/tests/portals/README.md9
-rw-r--r--testing/web-platform/tests/portals/about-blank-cannot-host.html18
-rw-r--r--testing/web-platform/tests/portals/csp/frame-ancestors.sub.html13
-rw-r--r--testing/web-platform/tests/portals/csp/frame-src.sub.html49
-rw-r--r--testing/web-platform/tests/portals/csp/resources/frame-src.sub.html4
-rw-r--r--testing/web-platform/tests/portals/csp/resources/frame-src.sub.html.sub.headers2
-rw-r--r--testing/web-platform/tests/portals/history/history-manipulation-inside-portal-with-subframes.html42
-rw-r--r--testing/web-platform/tests/portals/history/history-manipulation-inside-portal.html60
-rw-r--r--testing/web-platform/tests/portals/history/resources/inner-iframe.html13
-rw-r--r--testing/web-platform/tests/portals/history/resources/portal-harness.js30
-rw-r--r--testing/web-platform/tests/portals/history/resources/portal-manipulate-history-with-subframes.sub.html83
-rw-r--r--testing/web-platform/tests/portals/history/resources/portal-manipulate-history.html66
-rw-r--r--testing/web-platform/tests/portals/history/resources/run-test-in-portal.js16
-rw-r--r--testing/web-platform/tests/portals/htmlportalelement-event-handler-content-attributes.html27
-rw-r--r--testing/web-platform/tests/portals/idlharness.window.js18
-rw-r--r--testing/web-platform/tests/portals/no-portal-in-sandboxed-popup.html17
-rw-r--r--testing/web-platform/tests/portals/portal-activate-data.html94
-rw-r--r--testing/web-platform/tests/portals/portal-activate-default.html58
-rw-r--r--testing/web-platform/tests/portals/portal-activate-event-constructor.html25
-rw-r--r--testing/web-platform/tests/portals/portal-activate-event.html41
-rw-r--r--testing/web-platform/tests/portals/portal-non-http-navigation.html37
-rw-r--r--testing/web-platform/tests/portals/portal-onload-event.html16
-rw-r--r--testing/web-platform/tests/portals/portals-activate-empty-browsing-context.html26
-rw-r--r--testing/web-platform/tests/portals/portals-activate-inside-iframe.html21
-rw-r--r--testing/web-platform/tests/portals/portals-activate-inside-portal.html18
-rw-r--r--testing/web-platform/tests/portals/portals-activate-network-error.html18
-rw-r--r--testing/web-platform/tests/portals/portals-activate-no-browsing-context.html10
-rw-r--r--testing/web-platform/tests/portals/portals-activate-resolution.html19
-rw-r--r--testing/web-platform/tests/portals/portals-activate-twice.html24
-rw-r--r--testing/web-platform/tests/portals/portals-activate-while-unloading.html70
-rw-r--r--testing/web-platform/tests/portals/portals-adopt-predecessor.html82
-rw-r--r--testing/web-platform/tests/portals/portals-api.html11
-rw-r--r--testing/web-platform/tests/portals/portals-close-window.html20
-rw-r--r--testing/web-platform/tests/portals/portals-cross-origin-load.sub.html16
-rw-r--r--testing/web-platform/tests/portals/portals-focus.sub.html184
-rw-r--r--testing/web-platform/tests/portals/portals-host-exposure.sub.html40
-rw-r--r--testing/web-platform/tests/portals/portals-host-hidden-after-activation.html30
-rw-r--r--testing/web-platform/tests/portals/portals-host-null.html10
-rw-r--r--testing/web-platform/tests/portals/portals-host-post-message.sub.html138
-rw-r--r--testing/web-platform/tests/portals/portals-navigate-after-adoption.html41
-rw-r--r--testing/web-platform/tests/portals/portals-nested.html17
-rw-r--r--testing/web-platform/tests/portals/portals-no-frame-crash.html30
-rw-r--r--testing/web-platform/tests/portals/portals-post-message.sub.html189
-rw-r--r--testing/web-platform/tests/portals/portals-referrer-inherit-header.html23
-rw-r--r--testing/web-platform/tests/portals/portals-referrer-inherit-header.html.headers1
-rw-r--r--testing/web-platform/tests/portals/portals-referrer-inherit-meta.html24
-rw-r--r--testing/web-platform/tests/portals/portals-referrer.html61
-rw-r--r--testing/web-platform/tests/portals/portals-rendering.html22
-rw-r--r--testing/web-platform/tests/portals/portals-repeated-activate.html11
-rw-r--r--testing/web-platform/tests/portals/portals-set-src-after-activate.html38
-rw-r--r--testing/web-platform/tests/portals/predecessor-fires-unload.html40
-rw-r--r--testing/web-platform/tests/portals/references/portals-rendering.html5
-rw-r--r--testing/web-platform/tests/portals/resources/attempt-portal-load.html11
-rw-r--r--testing/web-platform/tests/portals/resources/blank-host.html8
-rw-r--r--testing/web-platform/tests/portals/resources/eval-portal.html10
-rw-r--r--testing/web-platform/tests/portals/resources/focus-page-with-autofocus.html23
-rw-r--r--testing/web-platform/tests/portals/resources/focus-page-with-button.html35
-rw-r--r--testing/web-platform/tests/portals/resources/focus-page-with-x-origin-iframe.sub.html28
-rw-r--r--testing/web-platform/tests/portals/resources/invalid.asis1
-rw-r--r--testing/web-platform/tests/portals/resources/open-blank-host.js14
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-broadcastchannel.html8
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-data-portal.html9
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-event-portal.html21
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-in-handler.html51
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-inside-portal.html11
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-twice-window-1.html12
-rw-r--r--testing/web-platform/tests/portals/resources/portal-activate-twice-window-2.html22
-rw-r--r--testing/web-platform/tests/portals/resources/portal-close-window.html7
-rw-r--r--testing/web-platform/tests/portals/resources/portal-embed-and-activate.html15
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-cross-origin-navigate.sub.html7
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-hidden-after-activation-portal.html14
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-post-message-after-activate.html19
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-1.html5
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-post-message-navigate-2.html4
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-post-message-x-origin.html4
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host-post-message.html51
-rw-r--r--testing/web-platform/tests/portals/resources/portal-host.html11
-rw-r--r--testing/web-platform/tests/portals/resources/portal-inside-iframe.html4
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-after-activate-window.html23
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-before-activate-portal.html24
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-before-activate-window.html13
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-during-activate-window.html21
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-portal.html29
-rw-r--r--testing/web-platform/tests/portals/resources/portal-post-message-x-origin-portal.html11
-rw-r--r--testing/web-platform/tests/portals/resources/portal-repeated-activate-window.html20
-rw-r--r--testing/web-platform/tests/portals/resources/portals-adopt-predecessor-portal.html77
-rw-r--r--testing/web-platform/tests/portals/resources/portals-adopt-predecessor.html20
-rw-r--r--testing/web-platform/tests/portals/resources/portals-nested-portal.html9
-rw-r--r--testing/web-platform/tests/portals/resources/portals-rendering-portal.html8
-rw-r--r--testing/web-platform/tests/portals/resources/postmessage-referrer.sub.html8
-rw-r--r--testing/web-platform/tests/portals/resources/predecessor-fires-unload-watch-unload.html25
-rw-r--r--testing/web-platform/tests/portals/resources/simple-portal-adopts-and-activates-predecessor.html6
-rw-r--r--testing/web-platform/tests/portals/resources/simple-portal-adopts-predecessor.html7
-rw-r--r--testing/web-platform/tests/portals/resources/simple-portal.html6
-rw-r--r--testing/web-platform/tests/portals/resources/stash-utils.sub.js43
-rw-r--r--testing/web-platform/tests/portals/xfo/portals-xfo-deny.sub.html43
-rw-r--r--testing/web-platform/tests/portals/xfo/portals-xfo-sameorigin.html18
-rw-r--r--testing/web-platform/tests/portals/xfo/resources/xfo-deny.asis8
-rw-r--r--testing/web-platform/tests/portals/xfo/resources/xfo-sameorigin.asis8
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>