commit 1208012e44a40593781726e1026787463ac8d08e parent 1aadc7314b7b4624dc24817868dcc7ebf70219d6 Author: Vincent Hilla <vhilla@mozilla.com> Date: Fri, 31 Oct 2025 07:31:19 +0000 Bug 1997073 - Add more WPTs for Document Picture in Picture. r=edgar,dom-core,smaug Differential Revision: https://phabricator.services.mozilla.com/D265289 Diffstat:
28 files changed, 809 insertions(+), 55 deletions(-)
diff --git a/testing/web-platform/meta/document-picture-in-picture/base-uri.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/base-uri.https.html.ini @@ -0,0 +1,3 @@ +[base-uri.https.html] + [Test that a document picture-in-picture window has the base URI of the initiator] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/closes-on-navigation-or-destroy.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/closes-on-navigation-or-destroy.https.html.ini @@ -0,0 +1,19 @@ +[closes-on-navigation-or-destroy.https.html] + expected: ERROR + [PIP window can be closed] + expected: TIMEOUT + + [PIP window closes when opener closes] + expected: NOTRUN + + [window.closed becomes true after pagehide if not window.close() initiated] + expected: NOTRUN + + [PIP window closes when navigated] + expected: NOTRUN + + [PIP window closes when navigated by name] + expected: NOTRUN + + [PIP window closes when opener navigates] + expected: NOTRUN diff --git a/testing/web-platform/meta/document-picture-in-picture/copy-document-mode-quirks.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/copy-document-mode-quirks.https.html.ini @@ -0,0 +1,3 @@ +[copy-document-mode-quirks.https.html] + [Test document picture-in-picture copies Document mode when it's quirks mode] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/copy-document-mode.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/copy-document-mode.https.html.ini @@ -0,0 +1,3 @@ +[copy-document-mode.https.html] + [Test document picture-in-picture copies Document mode] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/pip-fullscreen.tentative.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/pip-fullscreen.tentative.https.html.ini @@ -0,0 +1,6 @@ +[pip-fullscreen.tentative.https.html] + [A pip window cannot be fullscreened] + expected: FAIL + + [A pip window can fullscreen it's opener] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/pip-move.tentative.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/pip-move.tentative.https.html.ini @@ -0,0 +1,3 @@ +[pip-move.tentative.https.html] + [Test that a moveTo and moveBy are disabled for a document picture-in-picture window] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/pip-receives-focus.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/pip-receives-focus.https.html.ini @@ -0,0 +1,3 @@ +[pip-receives-focus.https.html] + [PiP recieves system focus after being opened] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/pip-resize.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/pip-resize.https.html.ini @@ -0,0 +1,6 @@ +[pip-resize.https.html] + [Test resizeTo PiP] + expected: FAIL + + [Test resizeBy PiP] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/pip-size.optional.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/pip-size.optional.https.html.ini @@ -0,0 +1,9 @@ +[pip-size.optional.https.html] + [Requesting PIP with width and height] + expected: FAIL + + [Test maximum size is restricted] + expected: FAIL + + [PiP remembers size] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/propagate-user-activation-from-opener.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/propagate-user-activation-from-opener.https.html.ini @@ -0,0 +1,9 @@ +[propagate-user-activation-from-opener.https.html] + [user activation propagates from opener to PiP] + expected: FAIL + + [user activation propagates from cross-origin iframe in opener to PiP] + expected: FAIL + + [user activation propagates from opener to iframe PiP] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/propagate-user-activation-to-opener.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/propagate-user-activation-to-opener.https.html.ini @@ -0,0 +1,12 @@ +[propagate-user-activation-to-opener.https.html] + [user activation propagates from PiP to opener] + expected: FAIL + + [user activation propagates from cross-origin iframe in PiP to opener] + expected: FAIL + + [user activation does not propagate from PiP to iframe in opener] + expected: FAIL + + [Consuming activation in PiP also consumes activation in iframes in opener] + expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/resize-requires-user-gesture.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/resize-requires-user-gesture.https.html.ini @@ -1,3 +0,0 @@ -[resize-requires-user-gesture.https.html] - [Test that calling resizeTo() or resizeBy() on a document\n picture-in-picture window requires user gesture] - expected: FAIL diff --git a/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini b/testing/web-platform/meta/document-picture-in-picture/returns-window-with-document.https.html.ini @@ -1,3 +1,9 @@ [returns-window-with-document.https.html] - [Test that documentPictureInPicture.requestWindow()\n returns a Window object] + [requestWindow resolves with the PiP window] + expected: FAIL + + [Elements can be moved from opener to PiP document] + expected: FAIL + + [PiP document is fully loaded] expected: FAIL diff --git a/testing/web-platform/tests/document-picture-in-picture/base-uri.https.html b/testing/web-platform/tests/document-picture-in-picture/base-uri.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<title>Test that a document picture-in-picture window has the base URI of the initiator</title> +<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> +<body> +<script> +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The initial placeholder is in many ways not initialized, e.g. doesn't have the right base + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + assert_equals(pipWindow.document.baseURI, document.baseURI, "Base URIs match"); +}); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/closes-on-navigation-or-destroy.https.html b/testing/web-platform/tests/document-picture-in-picture/closes-on-navigation-or-destroy.https.html @@ -0,0 +1,156 @@ +<!DOCTYPE html> +<title>Test that a document picture-in-picture window closes when itself or the opener is navigated / destroyed</title> +<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> +<link rel="help" href="https://wicg.github.io/document-picture-in-picture/#close-on-destroy"/> +<link rel="help" href="https://wicg.github.io/document-picture-in-picture/#close-on-navigate"/> +<body> +<script> +function waitForEvent(target, event) { + return new Promise(resolve => target.addEventListener(event, resolve, {once: true})) +} + +async function waitForMessage(target, type) { + const { data } = await waitForEvent(target, 'message'); + assert_equals(data.type, type, "Got the expected message"); + return data; +} + +window.addEventListener('message', ({data}) => { + if (data.type == 'error') { + throw data; + } +}); + +let last_id = 0; +async function new_opener_with_pip_window(t) { + // We cannot navigate this window, so do the tests in a popup. + const channelName = `pip channel ${++last_id}`; + + const opener = window.open(`support/popup-opens-pip.html`); + await waitForEvent(opener, "load"); + assert_true(true, "Popup loaded"); + + const pipChannel = new BroadcastChannel(channelName); + pipChannel.addEventListener('message', ({data}) => { + if (data.type == 'error') { + throw new Error(`${data.name}: ${data.message}`); + } + }); + + await test_driver.bless('activate popup window', null, opener); + opener.postMessage({ type: 'request-pip', channelName }); + await waitForMessage(pipChannel, 'pip-ready'); + + t.add_cleanup(function() { + pipChannel.close(); + opener.close(); + }); + + return { opener, pipChannel }; +} + +function evalInPIP(pipChannel, code, args = []) { + pipChannel.postMessage({ type: "exec", code: code.toString(), args }); + return waitForMessage(pipChannel, "exec-result"); +} + +promise_test(async (t) => { + // Trivial test case, mostly a sanity-check for this test infrastructure + const { opener, pipChannel } = await new_opener_with_pip_window(t); + assert_true(true, 'Succeded in getting an opener with pip window'); + + opener.postMessage({ type: 'close-pip' }); + // This also tests that a PIP window gets a pagehide + await waitForMessage(pipChannel, 'pip-pagehide'); + + opener.postMessage({ type: 'get-pip-status' }); + const statusMsg = await waitForMessage(window, 'pip-status'); + assert_true(statusMsg.closed, 'Succeeded in closing the pip window'); +}, 'PIP window can be closed'); + +promise_test(async (t) => { + const { opener, pipChannel } = await new_opener_with_pip_window(t); + + opener.close(); + await waitForMessage(pipChannel, 'pip-pagehide'); +}, 'PIP window closes when opener closes'); + +function waitTillPiPClosed(t, opener) { + let closed = false; + let shouldPing = true; + + const listener = ({ data }) => { + if (data.type == "pip-status") { + closed = data.closed; + shouldPing = true; + } + } + window.addEventListener("message", listener); + + const condition = () => { + if (shouldPing && !closed) { + opener.postMessage({ type: 'get-pip-status' }); + shouldPing = false; + } + return closed; + } + return t.step_wait(condition, "PiP window .closed becomes true") + .finally(() => { + window.removeEventListener("message", listener) + }) +} + +promise_test(async (t) => { + const { opener, pipChannel } = await new_opener_with_pip_window(t); + + // Per spec, #close-a-top-level-traversable doesn't set #is-closing and thus + // window.closed only becomes true when the traversable is destroyed. This happens + // via `afterAllUnloads` after pagehide. This is different for window.close(). + // https://github.com/whatwg/html/issues/11853 + opener.postMessage({ type: 'navigate-pip', href: "/common/blank.html" }); + const statusMsg = await waitForMessage(pipChannel, 'pip-pagehide'); + assert_false(statusMsg.closed, "window.closed is false during pagehide"); + await waitTillPiPClosed(t, opener); +}, "window.closed becomes true after pagehide if not window.close() initiated"); + +promise_test(async (t) => { + const { opener, pipChannel } = await new_opener_with_pip_window(t); + + opener.postMessage({ type: 'navigate-pip', href: "about:blank#0" }); + + opener.postMessage({ type: 'get-pip-status' }); + const statusMsg = await waitForMessage(window, 'pip-status'); + assert_false(statusMsg.closed, 'Same-page navigation did not close PIP'); + + opener.postMessage({ type: 'navigate-pip', href: "/common/blank.html" }); + await waitForMessage(pipChannel, 'pip-pagehide'); + await waitTillPiPClosed(t, opener); +}, 'PIP window closes when navigated'); + +promise_test(async (t) => { + const { opener, pipChannel } = await new_opener_with_pip_window(t); + + await evalInPIP(pipChannel, () => window.name = "pipwindow" ) + + const w = window.open("/common/blank.html", "pipwindow"); + await waitForMessage(pipChannel, "pip-pagehide"); + await waitTillPiPClosed(t, opener); +}, 'PIP window closes when navigated by name'); + +promise_test(async (t) => { + const { opener, pipChannel } = await new_opener_with_pip_window(t); + + opener.location = opener.location.href + "#0"; + + opener.postMessage({ type: 'get-pip-status' }); + const statusMsg = await waitForMessage(window, 'pip-status'); + assert_false(statusMsg.closed, 'Same-page navigation of opener did not close PIP'); + + opener.location = "/common/blank.html"; + await waitForMessage(pipChannel, 'pip-pagehide'); +}, 'PIP window closes when opener navigates'); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/display-mode.https.html b/testing/web-platform/tests/document-picture-in-picture/display-mode.https.html @@ -9,8 +9,29 @@ promise_test(async (t) => { await test_driver.bless('request PiP window'); const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // Ensure the async load doesn't blow away the iframe later on. + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + await new Promise(requestAnimationFrame); - assert_true(!!pipWindow.matchMedia('(display-mode: picture-in-picture)'.matches)); + assert_true( + !!pipWindow.matchMedia('(display-mode: picture-in-picture)'.matches), + 'PIP matches display mode' + ); + + const iframe = pipWindow.document.createElement("iframe"); + iframe.src = '/common/blank.html'; + pipWindow.document.body.append(iframe); + await new Promise(res => iframe.addEventListener("load", res, { once: true })); + + assert_true( + !!iframe.contentWindow.matchMedia('(display-mode: picture-in-picture)'.matches), + 'iframe in PIP matches display mode' + ); }); </script> </body> diff --git a/testing/web-platform/tests/document-picture-in-picture/enter-event.https.html b/testing/web-platform/tests/document-picture-in-picture/enter-event.https.html @@ -9,7 +9,10 @@ <script> async_test((t) => { test_driver.bless('request PiP window').then(t.step_func(_ => { - documentPictureInPicture.onenter = t.step_func_done(); + documentPictureInPicture.onenter = t.step_func_done((e) => { + assert_true(e.isTrusted, "is trusted"); + assert_false(e.cancelable, "is not cancelable"); + }); documentPictureInPicture.requestWindow(); })); }); diff --git a/testing/web-platform/tests/document-picture-in-picture/focus-opener.https.html b/testing/web-platform/tests/document-picture-in-picture/focus-opener.https.html @@ -27,7 +27,9 @@ promise_test(async (t) => { window.addEventListener('focus', resolve, { once: true }); await test_driver.bless('focus opener window', pipWindow); const focusScript = pipWindow.document.createElement('script'); - focusScript.setAttribute('src', 'support/focus-opener.js'); + // Resolve URI so that we don't implicitly test the base URI of the PIP + const scriptURI = new URL('support/focus-opener.js', document.baseURI).href; + focusScript.setAttribute('src', scriptURI); pipWindow.document.body.append(focusScript); }); return focusPromise; diff --git a/testing/web-platform/tests/document-picture-in-picture/pip-fullscreen.tentative.https.html b/testing/web-platform/tests/document-picture-in-picture/pip-fullscreen.tentative.https.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<title>Test how document picture-in-picture interacts with fullscreen</title> +<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> +<link rel="help" href="https://github.com/WICG/document-picture-in-picture/issues/133"> +<body> +<script> +function waitForEvent(target, event) { + return new Promise(resolve => target.addEventListener(event, resolve, {once: true})) +} + +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + await test_driver.bless('request fullscreen for PiP'); + try { + await pipWindow.document.body.requestFullscreen(); + assert_unreached('request fullscreen should fail'); + } catch (e) { + assert_equals(e.name, "TypeError", "fullscreening PiP throws an exception"); + } +}, "A pip window cannot be fullscreened"); + +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The initial placeholder is in many ways not initialized, e.g. doesn't have the right base + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const fsResult = new Promise((res, rej) => { + document.body.addEventListener('fullscreenchange', res); + document.body.addEventListener('fullscreenerror', rej); + }); + + await test_driver.bless('request fullscreen from opener', null, pipWindow); + const script = pipWindow.document.createElement("script"); + script.textContent = `opener.document.body.requestFullscreen()`; + pipWindow.document.body.append(script); + + const res = await fsResult; + assert_true(document.fullscreen, 'opener entered fullscreen'); + await document.exitFullscreen(); +}, "A pip window can fullscreen it's opener"); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/pip-move.tentative.https.html b/testing/web-platform/tests/document-picture-in-picture/pip-move.tentative.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<title>Test that a moveTo and moveBy are disabled for a document picture-in-picture window</title> +<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> +<body> +<script> +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + const initialX = pipWindow.screenX, initialY = pipWindow.screenY; + + // Unlikely, but let's make sure move doesn't fail due to a lack of space + // I.e. if PiP is in top left, move down right, otherwise up left. + const x = initialX > 50 ? 20 : 60; + const y = initialY > 50 ? 20 : 60; + + try { + pipWindow.moveTo(x, y); + pipWindow.moveBy(x, y); + } catch (e) { + // Test is tentative because it's not specified whether to throw + assert_unreached("moveTo and moveBy should not throw"); + } + + assert_equals(pipWindow.screenX, initialX, 'PiP screenX did not change'); + assert_equals(pipWindow.screenY, initialY, 'PiP screenY did not change'); +}); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/pip-receives-focus.https.html b/testing/web-platform/tests/document-picture-in-picture/pip-receives-focus.https.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<title>Test that the document Picture in picture window receives system focus</title> +<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> +<body> +<script> +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + if (!pipWindow.document.hasFocus()) { + await new Promise(resolve => pipWindow.onfocus = resolve); + } + assert_true(pipWindow.document.hasFocus(), 'PiP has focus'); + pipWindow.close(); +}, 'PiP recieves system focus after being opened'); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/pip-resize.https.html b/testing/web-platform/tests/document-picture-in-picture/pip-resize.https.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Test resizeTo and resizeBy for a PiP window</title> +<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> +<body> +<script> +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + let pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true + }); + + const iniWidth = pipWindow.innerWidth; + assert_true(true, `PIP has default inner width ${iniWidth}`); + + await assert_throws_dom('NotAllowedError', pipWindow.DOMException, () => + pipWindow.resizeTo(pipWindow.outerWidth, pipWindow.outerHeight + 100) + , 'resizeTo requires user acivation'); + + await test_driver.bless('resize window'); + let resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeTo(pipWindow.outerWidth + 100, pipWindow.outerHeight); + await resized; + + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'PIP was resized'); +}, 'Test resizeTo PiP'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + let pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true + }); + + const iniWidth = pipWindow.innerWidth; + assert_true(true, `PIP has default inner width ${iniWidth}`); + + await assert_throws_dom('NotAllowedError', pipWindow.DOMException, () => + pipWindow.resizeBy(100, 0) + , 'resizeBy requires user acivation'); + + await test_driver.bless('resize window'); + let resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeBy(100, 0); + await resized; + + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'Width was resized with activation'); +}, 'Test resizeBy PiP'); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/pip-size.optional.https.html b/testing/web-platform/tests/document-picture-in-picture/pip-size.optional.https.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<title>Optional: Test modifying document picture-in-picture window's width and height</title> +<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> +<body> +<script> +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + let pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true + }); + + const iniWidth = pipWindow.innerWidth, iniHeight = pipWindow.innerHeight; + assert_true(true, `PIP has default inner width ${iniWidth}, height ${iniHeight}`); + + await test_driver.bless('request PiP window from top window'); + pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true, width: iniWidth + 100, height: iniHeight + }); + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'Got the requested width'); +}, 'Requesting PIP with width and height'); + +// Restricting max size is mandatory, but depends on optional size parameters +promise_test(async (t) => { + const { availWidth, availHeight } = window.screen; + + await test_driver.bless('request PiP window from top window'); + // We request them as inner size, so the outer size would be even larger + // but it only matters that we would cover the screen. + const pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true, width: availWidth, height: availHeight + }); + + assert_less_than(pipWindow.outerWidth, availWidth, "PIP window width smaller than screen (initial)"); + assert_less_than(pipWindow.outerHeight, availHeight, "PIP window height smaller than screen"); + + // shrink and test resizeTo is restricted + await test_driver.bless('resize window'); + let resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeTo(100, 100); + await resized; + + await test_driver.bless('resize window'); + resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeTo(availWidth, availHeight); + await resized; + + assert_less_than(pipWindow.outerWidth, availWidth, "PIP window width smaller than screen (resizeTo)"); + assert_less_than(pipWindow.outerHeight, availHeight, "PIP window height smaller than screen (resizeTo)"); + + // shrink and test resizeBy is restricted + await test_driver.bless('resize window'); + resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeTo(100, 100); + await resized; + + await test_driver.bless('resize window'); + resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeBy(availWidth, availHeight); + await resized; + + assert_less_than(pipWindow.outerWidth, availWidth, "PIP window width smaller than screen (resizyBy)"); + assert_less_than(pipWindow.outerHeight, availHeight, "PIP window height smaller than screen (resizeBy)"); +}, 'Test maximum size is restricted'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window from top window'); + let pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true + }); + + const iniWidth = pipWindow.innerWidth; + assert_true(true, `PIP has default inner width ${iniWidth}`); + + // First, test that preferInitialWindowPlacement forces initial non-remembered position + await test_driver.bless('resize window'); + let resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeBy(100, 0); + await resized; + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'PiP was resized'); + pipWindow.close(); + + await test_driver.bless('request PiP window from top window'); + pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: true + }); + assert_equals(pipWindow.innerWidth, iniWidth, + 'preferInitialWindowPlacement causes PiP to open at initial, non-remembered size'); + + // Now, test that the API does remember the position when explicitly closing the PiP + await test_driver.bless('resize window'); + resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeBy(100, 0); + await resized; + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'PiP was resized'); + pipWindow.close(); + + await test_driver.bless('request PiP window from top window'); + pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: false + }); + assert_equals(pipWindow.innerWidth, iniWidth + 100, 'PiP was restored at previous size'); + + // Now, test that the API does remember the position when requesting a PiP while one is still open + await test_driver.bless('resize window'); + resized = new Promise(res => pipWindow.addEventListener("resize", res, { once: true })); + pipWindow.resizeBy(-50, 0); + await resized; + assert_equals(pipWindow.innerWidth, iniWidth + 50, 'PiP was resized'); + + await test_driver.bless('request PiP window from top window'); + pipWindow = await documentPictureInPicture.requestWindow({ + preferInitialWindowPlacement: false + }); + assert_equals(pipWindow.innerWidth, iniWidth + 50, 'PiP was restored at previous size'); +}, `PiP remembers size`); +</script> +</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/propagate-user-activation-from-opener.https.html b/testing/web-platform/tests/document-picture-in-picture/propagate-user-activation-from-opener.https.html @@ -5,6 +5,7 @@ <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> <script src="/resources/testdriver-vendor.js"></script> +<iframe id="cross-origin-iframe" src="https://{{hosts[alt][www]}}:{{ports[https][0]}}/common/blank.html"></iframe> <body> <script> promise_test(async (t) => { @@ -16,12 +17,60 @@ promise_test(async (t) => { // Activating this window should also activate the picture-in-picture window. await test_driver.bless('activate opener window'); - assert_true(navigator.userActivation.isActive, 'the opener should be activated when the PiP window is activated'); - assert_true(pipWindow.navigator.userActivation.isActive, 'the PiP window should be activated'); + assert_true(navigator.userActivation.isActive, 'the opener should be activated'); + assert_true(pipWindow.navigator.userActivation.isActive, 'the PiP window should be activated when the opener is activated'); // Consuming activation in the opener should also consume it in the picture-in-picture window. - window.open(); + window.open().close(); assert_false(navigator.userActivation.isActive, 'the opener should no longer be active once it consumes activation'); assert_false(pipWindow.navigator.userActivation.isActive, 'the PiP window should no longer be active once the opener consumes activation'); -}); +}, 'user activation propagates from opener to PiP'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + assert_false(navigator.userActivation.isActive, 'opener initially not active'); + assert_false(pipWindow.navigator.userActivation.isActive, 'PiP initially not active'); + + const ifr = document.getElementById("cross-origin-iframe"); + await test_driver.bless('activate cross-origin iframe', null, ifr.contentWindow); + + assert_true(pipWindow.navigator.userActivation.isActive, 'activation propagated to PiP'); + + window.open().close(); + + assert_false(navigator.userActivation.isActive, 'activation was consumed in opener'); + assert_false(pipWindow.navigator.userActivation.isActive, 'activation was consumed in PiP'); + +}, 'user activation propagates from cross-origin iframe in opener to PiP'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const ifr = pipWindow.document.createElement("iframe"); + pipWindow.document.body.append(ifr); + + assert_false(navigator.userActivation.isActive, 'opener initially not active'); + assert_false(pipWindow.navigator.userActivation.isActive, 'PiP initially not active'); + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'iframe in PiP initially not active'); + + await test_driver.bless('activate opener window'); + + assert_true(pipWindow.navigator.userActivation.isActive, 'activation propagated to PiP'); + assert_true(ifr.contentWindow.navigator.userActivation.isActive, 'activation propagated to iframe in PiP'); + + ifr.contentWindow.open().close(); + + assert_false(navigator.userActivation.isActive, 'activation was consumed in opener'); + assert_false(pipWindow.navigator.userActivation.isActive, 'activation was consumed in PiP'); + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'activation was consumed in iframe in PiP'); +}, 'user activation propagates from opener to iframe PiP'); </script> diff --git a/testing/web-platform/tests/document-picture-in-picture/propagate-user-activation-to-opener.https.html b/testing/web-platform/tests/document-picture-in-picture/propagate-user-activation-to-opener.https.html @@ -4,12 +4,20 @@ <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> <script src="/resources/testdriver-vendor.js"></script> +<iframe id="same-origin-iframe" src="/common/blank.html"></iframe> <body> <script> promise_test(async (t) => { await test_driver.bless('request PiP window'); const pipWindow = await documentPictureInPicture.requestWindow(); + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The async load could blow away our document while blessing causing an error + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + assert_false(navigator.userActivation.isActive, 'the opener should initially not have user activation'); assert_false(pipWindow.navigator.userActivation.isActive, 'the PiP window should initially not have user activation'); @@ -19,8 +27,90 @@ promise_test(async (t) => { assert_true(pipWindow.navigator.userActivation.isActive, 'the PiP window should be activated'); // Consuming activation in the picture-in-picture window should also consume it in the opener. - pipWindow.open(); + pipWindow.open().close(); assert_false(navigator.userActivation.isActive, 'the opener should no longer be active once the PiP window consumes activation'); assert_false(pipWindow.navigator.userActivation.isActive, 'the PiP window should no longer be active once it consumes activation'); -}); +}, 'user activation propagates from PiP to opener'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The async load could blow away our document while blessing causing an error + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const ifr = pipWindow.document.createElement("iframe"); + ifr.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/common/blank.html'; + pipWindow.document.body.append(ifr); + await new Promise(res => ifr.addEventListener('load', res, { once: true })); + + assert_false(navigator.userActivation.isActive, 'opener initially not active'); + assert_false(pipWindow.navigator.userActivation.isActive, 'PiP initially not active'); + + await test_driver.bless('activate cross-origin iframe', null, ifr.contentWindow); + + assert_true(navigator.userActivation.isActive, 'activation propagated to opener'); + + pipWindow.open().close(); + + assert_false(navigator.userActivation.isActive, 'activation was consumed in opener'); + assert_false(pipWindow.navigator.userActivation.isActive, 'activation was consumed in PiP'); +}, 'user activation propagates from cross-origin iframe in PiP to opener'); + +promise_test(async (t) => { +await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The async load could blow away our document while blessing causing an error + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const ifr = document.getElementById("same-origin-iframe"); + + assert_false(navigator.userActivation.isActive, 'opener initially not active'); + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'iframe in opener initially not active'); + assert_false(pipWindow.navigator.userActivation.isActive, 'PiP initially not active'); + + await test_driver.bless('activate pip window', null, pipWindow); + + assert_true(navigator.userActivation.isActive, 'activation propagated to opener'); + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'activation did not propagate to iframe in opener'); +}, 'user activation does not propagate from PiP to iframe in opener'); + +// Note how activation is not propagated to an iframe in the opener, but consumption is +promise_test(async (t) => { +await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + // The async load could blow away our document while blessing causing an error + assert_true(true, "Waiting for pip window to load"); + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const ifr = document.getElementById("same-origin-iframe"); + + assert_false(navigator.userActivation.isActive, 'opener initially not active'); + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'iframe in opener initially not active'); + assert_false(pipWindow.navigator.userActivation.isActive, 'PiP initially not active'); + + await test_driver.bless('activate iframe in opener', null, ifr.contentWindow); + await test_driver.bless('activate pip window', null, pipWindow); + + assert_true(ifr.contentWindow.navigator.userActivation.isActive, 'iframe in opener is active'); + assert_true(pipWindow.navigator.userActivation.isActive, 'PiP is active'); + + pipWindow.open().close(); + + assert_false(ifr.contentWindow.navigator.userActivation.isActive, 'activation was consumed in iframe in opener'); + assert_false(pipWindow.navigator.userActivation.isActive, 'activation was consumed in PiP'); +}, 'Consuming activation in PiP also consumes activation in iframes in opener'); </script> diff --git a/testing/web-platform/tests/document-picture-in-picture/resize-requires-user-gesture.https.html b/testing/web-platform/tests/document-picture-in-picture/resize-requires-user-gesture.https.html @@ -1,22 +0,0 @@ -<!DOCTYPE html> -<title>Test that calling resizeTo() or resizeBy() on a document - picture-in-picture window requires user gesture</title> -<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> -<body> -<script> -promise_test(async (t) => { - await test_driver.bless('request PiP window from top window'); - const pipWindow = await documentPictureInPicture.requestWindow(); - - await assert_throws_dom('NotAllowedError', pipWindow.DOMException, () => { - pipWindow.resizeBy(10, 10); - }, 'resizeBy() requires a user gesture for document picture-in-picture'); - await assert_throws_dom('NotAllowedError', pipWindow.DOMException, () => { - pipWindow.resizeTo(400, 400); - }, 'resizeTo() requires a user gesture for document picture-in-picture'); -}); -</script> -</body> diff --git a/testing/web-platform/tests/document-picture-in-picture/returns-window-with-document.https.html b/testing/web-platform/tests/document-picture-in-picture/returns-window-with-document.https.html @@ -8,28 +8,38 @@ <body> <div id="div"></div> <script> -const div = document.getElementById('div'); -async_test((t) => { - test_driver.bless('request PiP window').then(t.step_func(_ => { - documentPictureInPicture.requestWindow().then(t.step_func_done((pipWindow) => { - assert_true(!!pipWindow.document, +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + + assert_true(!!pipWindow.document, 'Window should contain a document'); - assert_true(documentPictureInPicture.window === pipWindow, - 'DocumentPictureInPicture.window should match the current window'); + assert_true(documentPictureInPicture.window === pipWindow, + 'DocumentPictureInPicture.window should match the current window'); +}, 'requestWindow resolves with the PiP window'); + +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + const div = document.getElementById('div'); + + // We should be able to move an element to that document. + assert_true(document.body.contains(div), + 'The original document should start with the div'); + assert_false(pipWindow.document.body.contains(div), + 'The PiP document should not start with the div'); + pipWindow.document.body.append(div); + assert_false(document.body.contains(div), + 'The div should have moved away from the original document'); + assert_true(pipWindow.document.body.contains(div), + 'The div should have moved to the PiP document'); +}, 'Elements can be moved from opener to PiP document'); - // We should be able to move an element to that document. - assert_true(document.body.contains(div), - 'The original document should start with the div'); - assert_false(pipWindow.document.body.contains(div), - 'The PiP document should not start with the div'); - pipWindow.document.body.append(div); - assert_false(document.body.contains(div), - 'The div should have moved away from the original document'); - assert_true(pipWindow.document.body.contains(div), - 'The div should have moved to the PiP document'); - })); - })); -}); +promise_test(async (t) => { + await test_driver.bless('request PiP window'); + const pipWindow = await documentPictureInPicture.requestWindow(); + assert_equals(pipWindow.document.readyState, "complete", "PiP document ready state is complete"); +}, 'PiP document is fully loaded'); </script> </body> diff --git a/testing/web-platform/tests/document-picture-in-picture/support/popup-opens-pip.html b/testing/web-platform/tests/document-picture-in-picture/support/popup-opens-pip.html @@ -0,0 +1,68 @@ +<script> +function get_pip_code(channelName) { + return ` + const channel = new BroadcastChannel("${channelName}"); + + window.addEventListener("pagehide", () => { + channel.postMessage({ type: "pip-pagehide", closed: window.closed }); + }); + + channel.addEventListener("message", async ({data}) => { + if (data.type == 'exec') { + const { code, args = [] } = data; + try { + // indirect eval call to evaluate in global scope + const fn = (0, eval)('(' + code + ')'); + const value = await fn(...args); + channel.postMessage({ type: "exec-result", value }); + } catch (err) { + channel.postMessage({ type: "error", name: err?.name, message: err?.message, stack: err?.stack }); + } + } else { + channel.postMessage({ type: "error", message: "unknown message " + data.type }); + } + }); + + channel.postMessage({ type: "pip-ready" }); + `; +} + +let pipWindow = null; + +async function action(data) { + if (data.type == 'request-pip') { + pipWindow = await documentPictureInPicture.requestWindow(); + + if (pipWindow.document.readyState != "complete") { + // about:blank should load synchronous, but Gecko is still working on that... + await new Promise(res => pipWindow.addEventListener("load", res, { once: true })); + } + + const script = pipWindow.document.createElement('script'); + script.innerHTML = get_pip_code(data.channelName); + pipWindow.document.body.append(script); + } else if (data.type == 'close-pip') { + pipWindow.close(); + } else if (data.type == 'navigate-pip') { + pipWindow.location = data.href; + } else if (data.type == 'get-pip-status') { + window.opener.postMessage({ + type: 'pip-status', + closed: pipWindow.closed + }); + } else { + throw new Error("Unknown message"); + } +} + +window.addEventListener("message", async ({data}) => { + try { + await action(data); + } catch (e) { + window.opener.postMessage({ + msg: `Unknown error ${e.name}: ${e.message} at ${e.stack}`, + type: 'error' + }) + } +}) +</script>