<!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>
