<!DOCTYPE html>
<title>Cross-Origin-Opener-Policy and a "javascript:" URL popup</title>
<meta charset="utf-8">
<meta name="timeout" content="long">
<meta name="variant" content="?1-2">
<meta name="variant" content="?3-4">
<meta name="variant" content="?5-6">
<meta name="variant" content="?7-8">
<meta name="variant" content="?9-10">
<meta name="variant" content="?11-12">
<meta name="variant" content="?13-14">
<meta name="variant" content="?15-16">
<meta name="variant" content="?17-last">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="/common/subset-tests.js"></script>
<script src="/common/utils.js"></script>
<script src="resources/common.js"></script>

<p>According to HTML's navigate algorithm, requests to <code>javascript:</code>
URLs should inherit the cross-origin opener policy of the active document. To
observe this, each subtest uses the following procedure.</p>

<ol>
  <li>create popup with a given COOP (the <code>parentCOOP</code>)</li>
  <li>navigate the popup to a <code>javascript:</code> URL (the new document is
  expected to inherit the <code>parentCOOP</code>)</li>
  <li>from the popup, create a second popup window with a given COOP (the
  <code>childCOOP</code>)</li>
</ol>

<p>Both popup windows inspect state and report back to the test.</p>

<pre>
    .---- test ----.
    | open(https:) |
    |  parentCOOP  |   .----- subject -------.
    |      '---------> | --------.           |
    |              |   |         v           |
    |              |   | assign(javascript:) |
    |              |   |  (COOP under test)  |
    |              |   |         |           |
    |              |   |    open(https:)     |
    |              |   |     childCOOP       |    .- child -.
    |              |   |         '--------------> |         |
    |              |   '---------------------'    '---------'
    |              |             |                     |
    |  validate    | <--status---+---------------------'
    '--------------'
</pre>

<script>
'use strict';

function getExecutorPath(uuid, origin, coopHeader) {
  const executorPath = '/common/dispatcher/executor.html?';
  const coopHeaderPipe =
    `|header(Cross-Origin-Opener-Policy,${encodeURIComponent(coopHeader)})`;
  return origin + executorPath + `uuid=${uuid}` + '&pipe=' + coopHeaderPipe;
}

function assert_isolated(results) {
  assert_equals(results.childName, '', 'child name');
  assert_false(results.childOpener, 'child opener');
  // The test subject's reference to the  "child" window must report "closed"
  // when COOP enforces isolation because the document initially created during
  // the window open steps must be discarded when a new document object is
  // created at the end of the navigation.
  assert_true(results.childClosed, 'child closed');
}

function assert_not_isolated(results, expectedName) {
  assert_equals(results.childName, expectedName, 'child name');
  assert_true(results.childOpener, 'child opener');
  assert_false(results.childClosed, 'child closed');
}

async function javascript_url_test(parentCOOP, childCOOP, origin, resultsVerification) {
  promise_test(async t => {
    const parentToken = token();
    const childToken = token();
    const responseToken = token();

    const parentURL = getExecutorPath(
      parentToken,
      SAME_ORIGIN.origin,
      parentCOOP);
    const childURL = getExecutorPath(
      childToken,
      origin.origin,
      childCOOP);

    // Open a first popup, referred to as the parent popup, and wait for it to
    // load.
    window.open(parentURL);
    send(parentToken, `send('${responseToken}', 'Done loading');`);
    assert_equals(await receive(responseToken), 'Done loading');

    // Make sure the parent popup is removed once the test has run, keeping a
    // clean state.
    add_completion_callback(() => {
      send(parentToken, 'close');
    });

    // Navigate the popup to the javascript URL. It should inherit the current
    // document's COOP. Because we're navigating to a page that is not an
    // executor, we lose access to easy messaging, making things a bit more
    // complicated. We use a predetermined scenario of communication that
    // enables us to retrieve whether the child popup appears closed from the
    // parent popup.
    //
    // Notes:
    // - Splitting the script tag prevents HTML parsing to kick in.
    // - The innermost double quotes need a triple backslash, because it goes
    //   through two rounds of consuming escape characters (\\\" -> \" -> ").
    // - The javascript URL does not accept \n characters so we need to use
    //   a new template literal for each line.
    send(parentToken,
      `location.assign("javascript:'` +
      // Include dispatcher.js to have access to send() and receive().
      `<script src=\\\"/common/dispatcher/dispatcher.js\\\"></scr` + `ipt>` +
      `<script> (async () => {` +

        // Open the child popup and keep a handle to it.
        `const w = open(\\\"${childURL}\\\", \\\"${childToken}\\\");` +

        // We wait for the main frame to query the w.closed property.
        `await receive(\\\"${parentToken}\\\");` +
        `send(\\\"${responseToken}\\\", w.closed);` +

        // Finally we wait for the cleanup indicating that this popup can be
        // closed.
        `await receive(\\\"${parentToken}\\\");` +
        `close();` +
        `})()</scr` + `ipt>'");`
    );

    // Make sure the javascript navigation ran, and the child popup was created.
    send(childToken, `send('${responseToken}', 'Done loading');`);
    assert_equals(await receive(responseToken), 'Done loading');

    // Make sure the child popup is removed once the test has run, keeping a
    // clean state.
    add_completion_callback(() => {
      send(childToken, `close()`);
    });

    // Give some time for things to settle across processes etc. before
    // proceeding with verifications.
    await new Promise(resolve => { t.step_timeout(resolve, 500); });

    // Gather information about the child popup and verify that they match what
    // we expect.
    const results = {};
    send(parentToken, 'query');
    results.childClosed = await receive(responseToken) === 'true';

    send(childToken, `send('${responseToken}', opener != null);`);
    results.childOpener = await receive(responseToken) === 'true';

    send(childToken, `send('${responseToken}', name);`);
    results.childName = await receive(responseToken);

    resultsVerification(results, childToken);
  }, `navigation: ${origin.name}; ` + `parentCOOP: ${parentCOOP}; ` +
     `childCOOP: ${childCOOP}`);
}

const tests = [
  ['unsafe-none', 'unsafe-none', SAME_ORIGIN, assert_not_isolated],
  ['unsafe-none', 'unsafe-none', SAME_SITE, assert_not_isolated],
  ['unsafe-none', 'same-origin-allow-popups', SAME_ORIGIN, assert_isolated],
  ['unsafe-none', 'same-origin-allow-popups', SAME_SITE, assert_isolated],
  ['unsafe-none', 'same-origin', SAME_ORIGIN, assert_isolated],
  ['unsafe-none', 'same-origin', SAME_SITE, assert_isolated],
  ['same-origin-allow-popups', 'unsafe-none', SAME_ORIGIN, assert_not_isolated],
  ['same-origin-allow-popups', 'unsafe-none', SAME_SITE, assert_not_isolated],
  ['same-origin-allow-popups', 'same-origin-allow-popups', SAME_ORIGIN, assert_not_isolated],
  ['same-origin-allow-popups', 'same-origin-allow-popups', SAME_SITE, assert_isolated],
  ['same-origin-allow-popups', 'same-origin', SAME_ORIGIN, assert_isolated],
  ['same-origin-allow-popups', 'same-origin', SAME_SITE, assert_isolated],
  ['same-origin', 'unsafe-none', SAME_ORIGIN, assert_isolated],
  ['same-origin', 'unsafe-none', SAME_SITE, assert_isolated],
  ['same-origin', 'same-origin-allow-popups', SAME_ORIGIN, assert_isolated],
  ['same-origin', 'same-origin-allow-popups', SAME_SITE, assert_isolated],
  ['same-origin', 'same-origin', SAME_ORIGIN, assert_not_isolated],
  ['same-origin', 'same-origin', SAME_SITE, assert_isolated],
].forEach(([parentCOOP, childCOOP, origin, expectation]) => {
  subsetTest(
    javascript_url_test,
    parentCOOP,
    childCOOP,
    origin,
    expectation);
});

</script>
