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