chromium/third_party/blink/web_tests/wpt_internal/fenced_frame/unfenced-top.https.html

<!DOCTYPE html>
<title>Test top-level navigation from fenced frames with _unfencedTop</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-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/common/utils.js"></script>
<script src="/common/dispatcher/dispatcher.js"></script>
<script src="resources/utils.js"></script>

<body>
<script>

// Creates a new window and validates its opener/referrer, then creates an
// opaque-ads fenced frame inside of it, and an iframe nested inside of that.
// Returns the window, the URL of the fenced frame, and the URL of the iframe.
async function createAndValidateWindow() {
  const new_window = attachWindowContext();

  await new_window.execute(async (original_referrer) => {
    assert_equals(document.referrer, original_referrer,
                  "Initially, the window's referrer is the test file.");
    assert_not_equals(window.opener, null,
                      `The window has an opener, because it was created from
                       outside a fenced frame.`);

    // We have to store the frame under 'window' explicitly, because otherwise
    // the variable wouldn't persist between script executions.
    window.fenced_frame = await attachFencedFrameContext({generator_api: 'fledge'});

    // Wait for the fenced frame to load.
    await window.fenced_frame.execute(async () => {
      assert_false(navigator.userActivation.isActive,
                   "The fenced frame should initially be inactive.");

      window.nested_iframe = attachIFrameContext();
      await window.nested_iframe.execute(() => {
        assert_false(navigator.userActivation.isActive,
                     "The nested iframe should initially be inactive.");
      });
    });

    // Send a click to user-activate the fenced frame.
    // Note: this is tricky because we are in a fenced frame inside an
    // auxiliary browsing context.
    // The normal cross-platform test_driver APIs don't seem to work in
    // auxiliary browsing contexts.
    // The Blink-internal API `eventSender` works differently in ShadowDOM
    // and MPArch. In ShadowDOM, it only works when called from the top-level
    // frame. In MPArch it only works when called from inside the fenced frame.
    // So we do both.

    // User activate ShadowDOM. TODO(crbug.com/1262022): Remove
    // ShadowDOM-specific tests and comments.
    assert_not_equals(eventSender, null);
    var rect = window.fenced_frame.element.getBoundingClientRect();
    eventSender.mouseMoveTo(rect.x+rect.width/2, rect.y+rect.height/2);
    eventSender.mouseDown();

    await window.fenced_frame.execute(async () => {
      // User activate MPArch.
      assert_not_equals(eventSender, null);
      eventSender.mouseDown();

      assert_true(navigator.userActivation.isActive,
                  "Now the fenced frame should be active.");

      await window.nested_iframe.execute(() => {
        assert_true(navigator.userActivation.isActive,
                    "Now the nested iframe should be active.");
      });
    });
  }, [location.href]);

  return new_window;
}

async function checkNavigationSucceeded(
    new_window, expected_history_length) {
  await new_window.execute((expected_history_length) => {
    assert_equals(window.opener, null,
                  `The frame has no window.opener anymore, because _unfencedTop
                   forces a new browsing context group.`);
    assert_equals(document.referrer, "",
                  `The new document.referrer should be empty.`);
    assert_equals(window.fenced_frame, undefined,
                  `The browsing context has been refreshed, so old variables
                   are gone.`);
    assert_equals(history.length, expected_history_length,
                  `The history length should be as expected.`);
  }, [expected_history_length]);

  assert_true(new_window.closed,
              `The popup's handle is now closed, because _unfencedTop forces
               a new browsing context group.`);
}

// Test successful top-level navigation (non-refresh) from an opaque-ads
// fenced frame.
promise_test(async () => {
  // Create a new window, where we'll navigate the outermost frame.
  // (We can't do it in this window, or we'd lose the testing harness.)
  // Create a fenced frame in the window, and return its URL (which will
  // become the document.referrer of the navigated outermost frame).
  const new_window = await createAndValidateWindow();

  // Check the history length before navigation.
  const history_length = await new_window.execute(() => {
    return history.length;
  });

  // Navigate the window's top-level frame from inside the fenced frame.
  // (Navigate to the original URL, i.e. refresh the page, so that we can
  // still communicate via RemoteContext.)
  // Suspend the remote executor so that the next command we send will be
  // handled by the refreshed page, not the moribund old page.
  await new_window.execute(() => {
    window.executor.suspend(() => {
      window.fenced_frame.execute((refresh_url) => {
        var norefresh_url = new URL(refresh_url);
        norefresh_url.searchParams.append('norefresh', '');
        const window_handle = window.open(norefresh_url, '_unfencedTop');
        assert_equals(window_handle, null,
                      `There should be no window handle returned from
                       navigations through _unfencedTop.`);
      }, [location.href]);
    });
  });

  // Because this is a non-refresh navigation, the history should be extended.
  await checkNavigationSucceeded(new_window, history_length+1);
}, '_unfencedTop opaque-ads non-refresh success case');

// Test successful top-level refresh from an opaque-ads fenced frame.
promise_test(async () => {
  const new_window = await createAndValidateWindow();

  const history_length = await new_window.execute(() => {
    return history.length;
  });

  await new_window.execute(() => {
    window.executor.suspend(() => {
      window.fenced_frame.execute((refresh_url) => {
        const window_handle = window.open(refresh_url, '_unfencedTop');
        assert_equals(window_handle, null,
                      `There should be no window handle returned from
                       navigations through _unfencedTop.`);
      }, [location.href]);
    });
  });

  // Even though this is a refresh, we want to treat it as if it's coming from
  // a cross-origin frame.
  // await checkNavigationSucceeded(new_window, history_length+1);
  // TODO(crbug.com/1315802): Uncomment the above when we null out the initiator
  // origin, frame token, and site instance.
  await checkNavigationSucceeded(new_window, history_length);
}, '_unfencedTop opaque-ads refresh success case');

// Test successful top-level navigation (non-refresh) out of an iframe nested
// inside an opaque-ads fenced frame.
promise_test(async () => {
  const new_window = await createAndValidateWindow();

  // Check the history length before navigation.
  const history_length = await new_window.execute(() => {
    return history.length;
  });

  // Navigate the window's top-level frame from inside the fenced frame's
  // nested iframe.
  await new_window.execute(() => {
    window.executor.suspend(() => {
      window.fenced_frame.execute((refresh_url) => {
        window.nested_iframe.execute((refresh_url) => {
          var norefresh_url = new URL(refresh_url);
          norefresh_url.searchParams.append('norefresh', '');
          const window_handle = window.open(norefresh_url, '_unfencedTop');
          assert_equals(window_handle, null,
                        `There should be no window handle returned from
                         navigations through _unfencedTop.`);
        }, [refresh_url]);
      }, [location.href]);
    });
  });

  // Because this is a non-refresh navigation, the history should be extended.
  await checkNavigationSucceeded(new_window, history_length+1);
}, '_unfencedTop opaque-ads nested iframe success case');

// Test unsuccessful navigation out of a fenced frame using _unfencedTop and
// a javascript: URL.
promise_test(async() => {
  // Create a new window.
  const new_window = await createAndValidateWindow();

  await new_window.execute(async () => {

    // Navigate the fenced frame to a JS URL.
    await window.fenced_frame.execute(() => {
      window.open('javascript:window.secret=true', '_self');
    });

    // In a separate remote script execution (to ensure the navigation has
    // finished), observe that JS URL navigations work in fenced frames.
    await window.fenced_frame.execute(() => {
      assert_equals(window.secret, true,
                    "The JS URL navigation worked inside the fenced frame.");
    });

    // Now try to navigate _unfencedTop using a JS URL.
    await window.fenced_frame.execute(() => {
      const result = window.open('javascript:window.secret=false', '_unfencedTop');
      assert_equals(result, null, "_unfencedTop didn't return a window.");
    });
  });

  // In a separate remote script execution (to ensure the navigation has
  // finished), observe that JS URL navigations don't work for _unfencedTop.
  await new_window.execute(() => {
    assert_equals(window.secret, undefined,
                  "The JS URL navigation to _unfencedTop failed");
  });
}, '_unfencedTop :javascript URL failure');

// Test unsuccessful navigation out of a fenced frame using _unfencedTop and
// a blob: URL.
promise_test(async() => {
  // Create a new window.
  const new_window = await createAndValidateWindow();

  await new_window.execute(async () => {
    // Try to navigate _unfencedTop using a Blob URL.
    await window.fenced_frame.execute(() => {
      const blob_url = URL.createObjectURL(new Blob([1,2,3]));
      const result = window.open(blob_url, '_unfencedTop');
      assert_equals(result, null, "_unfencedTop didn't return a window.");
    });
  });

  // In a separate remote script execution (to ensure the navigation has
  // finished), observe that the context is still there.
  await new_window.execute(() => {});
}, '_unfencedTop :blob URL failure');

// Test that fragment navigations out of a fenced frame using _unfencedTop
// do not act as same-document navigations.
promise_test(async() => {
  // Create a new window.
  const new_window = await createAndValidateWindow();

  await new_window.execute(async () => {
    // Save some state in the document.
    window.foo = "foo";

    // Navigate _unfencedTop using a fragment URL.
    window.executor.suspend(() => {
      window.fenced_frame.execute((parent_url) => {
        const result = window.open(parent_url+'#foo', '_unfencedTop');
      }, [location.href]);
    });
  });

  // Observe that there is a new document (the old state is lost).
  await new_window.execute(async () => {
    assert_equals(window.foo, undefined);
  });
}, '_unfencedTop fragment navigation');

async function click(frame) {
  var actions = new test_driver.Actions();
  await actions.pointerMove(0, 0, {origin: frame})
               .pointerDown()
               .pointerUp()
               .send();
}

// Test that _unfencedTop behaves like any other target name outside of fenced
// frames.
promise_test(async() => {
  await click(document.documentElement);
  assert_true(navigator.userActivation.isActive,
              "The frame should be active before opening a window.");
  const openee = window.open('resources/dummy.html', '_unfencedTop');
  assert_false(navigator.userActivation.isActive,
               "The frame should be inactive after opening a window.");

  assert_not_equals(openee.document, document,
                    "_unfencedTop outside a fenced frame opens a new document.");
  assert_not_equals(openee.window, window,
                    "_unfencedTop outside a fenced frame opens a new window.");

  openee.close();
}, '_unfencedTop outside a fenced frame');

// Test that _unfencedTop behaves like any other target name inside fenced
// frames that aren't in opaque-ads mode. Note that window.open in fenced
// frames uses noopener.
promise_test(async () => {
  frame = attachFencedFrameContext();

  // Wait for the frame to load to avoid nondeterminism with user activation.
  await frame.execute(() => {});
  await click(frame.element);

  // Use _unfencedTop inside the default mode fenced frame.
  await frame.execute(() => {
    // User activate MPArch.
    assert_not_equals(eventSender, null);
    eventSender.mouseDown();

    assert_true(navigator.userActivation.isActive,
                "The frame should be active before opening a window.");
    const openee = window.open('resources/dummy.html', '_unfencedTop');
    assert_false(navigator.userActivation.isActive,
                 "The frame should be inactive after opening a window.");
  });

  // In a separate remote context (so that the navigation would have completed
  // if it were navigating the top-level frame), check that the frame still
  // exists.
  await frame.execute(() => {});
}, '_unfencedTop in default fenced frame');

// Test that _unfencedTop behaves like any other target name inside fenced
// frames that were previously in opaque-ads mode.
promise_test(async () => {
  const frame = await attachFencedFrameContext({generator_api: "fledge"});
  await frame.execute(() => {});

  const navigated_frame = await replaceFrameContext(frame);
  // Wait for the frame to load to avoid nondeterminism with user activation.
  await navigated_frame.execute(() => {});
  await click(frame.element);

  // Use _unfencedTop inside the default mode fenced frame.
  await navigated_frame.execute(() => {
    // User activate MPArch.
    assert_not_equals(eventSender, null);
    eventSender.mouseDown();

    assert_true(navigator.userActivation.isActive,
                "The frame should be active before opening a window.");
    const openee = window.open('resources/dummy.html', '_unfencedTop');
    assert_false(navigator.userActivation.isActive,
                 "The frame should be inactive after opening a window.");
  });

  // In a separate remote context (so that the navigation would have completed
  // if it were navigating the top-level frame), check that the frame still
  // exists.
  await navigated_frame.execute(() => {});
}, '_unfencedTop in opaque-ads -> default fenced frame');

</script>
</body>