Attach activate data to the portalactivate event.

This allows authors to pass a serializable value along with the activation
event, so that the contexts can coordinate and transfer state across
the activation. Transfer is supported, so message ports and similar
objects can also be passed (subject to bug 940021).

The existing TransferableMessage mojo struct (used for postMessage elsewhere)
is reused, and is plumbed along the existing portal activation path.

Web platform test included.

Bug: 938549
Change-Id: Ib9a35a58c6317523b74fdd47cdd8e68c1d6ddfbd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1504046
Commit-Queue: Jeremy Roman <jbroman@chromium.org>
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Reviewed-by: Kentaro Hara <haraken@chromium.org>
Reviewed-by: Lucas Gadani <lfg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#639981}
diff --git a/portals/portal-activate-data.html b/portals/portal-activate-data.html
new file mode 100644
index 0000000..e2417cb
--- /dev/null
+++ b/portals/portal-activate-data.html
@@ -0,0 +1,65 @@
+<!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>
+function nextMessage(target) {
+  return new Promise((resolve, reject) => {
+    target.addEventListener('message', e => resolve(e), {once: true});
+  });
+}
+
+async function openPortalAndActivate(logic, activateOptions) {
+  const bc = new BroadcastChannel('portal-activate-data');
+  const w = window.open();
+  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(bc)).data, 'ready');
+    await portal.activate(activateOptions);
+    return (await nextMessage(bc)).data;
+  } finally {
+    w.close();
+    bc.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 {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.");
+
+if (window.SharedArrayBuffer) {
+  promise_test(async t => {
+  await promise_rejects(
+      t, 'DataCloneError',
+      openPortalAndActivate('', {data: new SharedArrayBuffer}));
+  }, "A SharedArrayBuffer cannot be passed through activate data.");
+}
+
+promise_test(async t => {
+  await promise_rejects(
+      t, new Error,
+      openPortalAndActivate('', {data: {get a() { throw new Error; }}}));
+}, "Uncloneable data has its exception propagated.");
+
+promise_test(async t => {
+  await promise_rejects(
+      t, new TypeError,
+      openPortalAndActivate('', {data: null, transfer: [null]}));
+}, "Errors during transfer list processing are propagated.");
+</script>
diff --git a/portals/resources/portal-activate-data-portal.html b/portals/resources/portal-activate-data-portal.html
new file mode 100644
index 0000000..cba38a0
--- /dev/null
+++ b/portals/resources/portal-activate-data-portal.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<script>
+const bc = new BroadcastChannel('portal-activate-data');
+let logic = new Function('event', (new URL(location)).searchParams.get('logic'));
+onload = () => bc.postMessage('ready');
+onportalactivate = event => {
+  try {
+    bc.postMessage(logic(event));
+  } finally {
+    bc.close();
+  }
+};
+</script>