diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/close-watcher | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/close-watcher')
14 files changed, 1050 insertions, 0 deletions
diff --git a/testing/web-platform/tests/close-watcher/META.yml b/testing/web-platform/tests/close-watcher/META.yml new file mode 100644 index 0000000000..4534ab8abe --- /dev/null +++ b/testing/web-platform/tests/close-watcher/META.yml @@ -0,0 +1,4 @@ +spec: https://wicg.github.io/close-watcher/ +suggested_reviewers: + - domenic + - natechapin diff --git a/testing/web-platform/tests/close-watcher/abortsignal.html b/testing/web-platform/tests/close-watcher/abortsignal.html new file mode 100644 index 0000000000..9229b37cf6 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/abortsignal.html @@ -0,0 +1,123 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> +// TODO(domenic): maybe update createRecordingCloseWatcher() to allow passing args and use it? + +test(() => { + let watcher = new CloseWatcher({ signal: AbortSignal.abort() }); + let oncancel_called = false; + let onclose_called = false; + watcher.oncancel = () => oncancel_called = true; + watcher.onclose = () => onclose_called = true; + + watcher.requestClose(); + + assert_false(oncancel_called); + assert_false(onclose_called); +}, "already-aborted AbortSignal then requestClose() fires no events"); + +test(() => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + let oncancel_called = false; + let onclose_called = false; + watcher.oncancel = () => oncancel_called = true; + watcher.onclose = () => onclose_called = true; + + controller.abort(); + watcher.requestClose(); + + assert_false(oncancel_called); + assert_false(onclose_called); +}, "abortController.abort() then requestClose() fires no events"); + +test(() => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + let oncancel_call_count_ = 0; + let onclose_call_count_ = 0; + watcher.oncancel = () => oncancel_call_count_++; + watcher.onclose = () => onclose_call_count_++; + + watcher.requestClose(); + controller.abort(); + + assert_equals(oncancel_call_count_, 0); + assert_equals(onclose_call_count_, 1); +}, "requestClose() then abortController.abort() fires only one close event"); + +promise_test(async () => { + let watcher = new CloseWatcher({ signal: AbortSignal.abort() }); + let oncancel_called = false; + let onclose_called = false; + watcher.oncancel = () => oncancel_called = true; + watcher.onclose = () => onclose_called = true; + + await sendCloseRequest(); + + assert_false(oncancel_called); + assert_false(onclose_called); +}, "already-aborted AbortSignal then Esc key fires no events"); + +promise_test(async t => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + let oncancel_called = false; + let onclose_called = false; + watcher.oncancel = () => oncancel_called = true; + watcher.onclose = () => onclose_called = true; + + controller.abort(); + await sendCloseRequest(); + + assert_false(oncancel_called); + assert_false(onclose_called); +}, "abortController.abort() then close via Esc key fires no events"); + +promise_test(async t => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + let oncancel_call_count_ = 0; + let onclose_call_count_ = 0; + watcher.oncancel = () => oncancel_call_count_++; + watcher.onclose = () => onclose_call_count_++; + + await sendCloseRequest(); + controller.abort(); + + assert_equals(oncancel_call_count_, 0); + assert_equals(onclose_call_count_, 1); +}, "Esc key then abortController.abort() fires only one close event"); + +test(t => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + controller.abort(); + let watcher2 = new CloseWatcher(); + t.add_cleanup(() => watcher2.destroy()); +}, "abortController.abort()ing a free CloseWatcher allows a new one to be created without a user activation"); + +promise_test(async t => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + watcher.oncancel = () => { controller.abort(); } + watcher.onclose = t.unreached_func("onclose"); + await test_driver.bless("give user activation so that cancel will fire", () => { + watcher.requestClose(); + }); +}, "abortController.abort() inside oncancel"); + +test(t => { + let controller = new AbortController(); + let watcher = new CloseWatcher({ signal: controller.signal }); + watcher.onclose = () => { controller.abort(); } + watcher.requestClose(); +}, "abortController.abort() inside onclose is benign"); +</script> diff --git a/testing/web-platform/tests/close-watcher/basic.html b/testing/web-platform/tests/close-watcher/basic.html new file mode 100644 index 0000000000..9951e54031 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/basic.html @@ -0,0 +1,82 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.requestClose(); + + assert_array_equals(events, ["close"]); +}, "requestClose() with no user activation only fires close"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.destroy(); + watcher.requestClose(); + + assert_array_equals(events, []); +}, "destroy() then requestClose() fires no events"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.close(); + assert_array_equals(events, ["close"]); + + watcher.requestClose(); + assert_array_equals(events, ["close"]); +}, "close() then requestClose() fires only one close event"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.requestClose(); + assert_array_equals(events, ["close"]); + + watcher.destroy(); + assert_array_equals(events, ["close"]); +}, "requestClose() then destroy() fires only one close event"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.close(); + assert_array_equals(events, ["close"]); + + watcher.destroy(); + assert_array_equals(events, ["close"]); +}, "close() then destroy() fires only one close event"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.destroy(); + await sendCloseRequest(); + + assert_array_equals(events, []); +}, "destroy() then close request fires no events"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + await sendCloseRequest(); + watcher.destroy(); + + assert_array_equals(events, ["close"]); +}, "Close request then destroy() fires only one close event"); +</script> diff --git a/testing/web-platform/tests/close-watcher/closewatcher-dialog-popover.html b/testing/web-platform/tests/close-watcher/closewatcher-dialog-popover.html new file mode 100644 index 0000000000..50d5cb7a4c --- /dev/null +++ b/testing/web-platform/tests/close-watcher/closewatcher-dialog-popover.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel=help href="https://github.com/whatwg/html/pull/9462"> +<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/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="resources/helpers.js"></script> + +<button id=b0>button</button> + +<dialog id=dialog> + <button id=b1>button</button> + <div id=popover popover=auto>popover</div> +</dialog> + +<script> +const waitForPotentialCloseEvent = () => { + // CloseWatchers fire close events synchronously, but dialog elements wait + // for a rAF before firing them. + return new Promise(resolve => requestAnimationFrame(resolve)); +}; + +promise_test(async t => { + const events = []; + const closeWatcher = createRecordingCloseWatcher(t, events, 'CloseWatcher', 'CloseWatcher'); + const dialog = createRecordingCloseWatcher(t, events, 'dialog', 'dialog'); + const popover = createRecordingCloseWatcher(t, events, 'popover', 'popover'); + assert_true(dialog.hasAttribute('open'), 'The dialog should be open.'); + assert_true(popover.matches(':popover-open'), 'The popover should be open.'); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + + assert_false(popover.matches(':popover-open'), 'The popover should be closed.'); + assert_false(dialog.hasAttribute('open'), 'The dialog should be closed.'); + assert_array_equals(events, ['CloseWatcher close', 'dialog close']); +}, 'Opening a CloseWatcher, modal dialog, and popover without user activation causes them all to be closed with one close request.'); + +promise_test(async t => { + const events = []; + const closeWatcher = await createBlessedRecordingCloseWatcher(t, events, 'CloseWatcher', 'CloseWatcher'); + const dialog = await createBlessedRecordingCloseWatcher(t, events, 'dialog', 'dialog'); + const popover = await createBlessedRecordingCloseWatcher(t, events, 'popover', 'popover', dialog); + assert_true(dialog.hasAttribute('open'), 'The dialog should be open.'); + assert_true(popover.matches(':popover-open'), 'The popover should be open.'); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'First close request: The popover should be closed.'); + assert_true(dialog.hasAttribute('open'), 'First close request: The dialog should be open.'); + assert_array_equals(events, []); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.'); + assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.'); + assert_array_equals(events, ['dialog close']); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.'); + assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.'); + assert_array_equals(events, ['dialog close', 'CloseWatcher close']); +}, 'Opening a CloseWatcher, modal dialog, and popover with user activation for each should close one at a time with close requests.'); + +promise_test(async t => { + const events = []; + const closeWatcher = await createBlessedRecordingCloseWatcher(t, events, 'CloseWatcher', 'CloseWatcher'); + const dialog = await createBlessedRecordingCloseWatcher(t, events, 'dialog', 'dialog'); + const popover = await createBlessedRecordingCloseWatcher(t, events, 'popover', 'popover', dialog); + assert_true(dialog.hasAttribute('open'), 'The dialog should be open.'); + assert_true(popover.matches(':popover-open'), 'The popover should be open.'); + + await blessTopLayer(popover); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'First close request: The popover should be closed.'); + assert_true(dialog.hasAttribute('open'), 'First close request: The dialog should be open.'); + assert_array_equals(events, []); + + await blessTopLayer(dialog); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.'); + assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.'); + assert_array_equals(events, ['dialog cancel', 'dialog close']); + + await test_driver.bless(); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.'); + assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.'); + assert_array_equals(events, ['dialog cancel', 'dialog close', 'CloseWatcher cancel', 'CloseWatcher close']); +}, 'Opening a CloseWatcher, modal dialog, and popover with user activation for each and sending close requests with user activation should close one at a time and have cancel events.'); +</script> diff --git a/testing/web-platform/tests/close-watcher/esc-key.html b/testing/web-platform/tests/close-watcher/esc-key.html new file mode 100644 index 0000000000..16fcce6917 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/esc-key.html @@ -0,0 +1,77 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<!-- + Tests in this file are around the interaction of the Esc key specifically, not + the general concept of close requests. Ideally, all other tests would work + as-is if you changed the implementation of sendCloseRequest(). These tests + assume that Esc is the close request for the platform being tested. +--> + +<body> +<script> +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + await sendEscKey(); + + assert_array_equals(events, ["close"]); +}, "Esc key does not count as user activation, so if it is the sole user interaction, that fires close but not cancel"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + window.onkeydown = e => e.preventDefault(); + + await sendEscKey(); + + assert_array_equals(events, []); +}, "A keydown listener can prevent the Esc keypress from being interpreted as a close request"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + window.onkeyup = e => e.preventDefault(); + + await sendEscKey(); + + assert_array_equals(events, []); +}, "A keyup listener can prevent the Esc keypress from being interpreted as a close request"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + window.onkeypress = e => e.preventDefault(); + + await sendEscKey(); + + assert_array_equals(events, []); +}, "A keypress listener can prevent the Esc keypress from being interpreted as a close request"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + let keydown = new KeyboardEvent('keydown', {'key': 'Escape', 'keyCode': 27}); + window.dispatchEvent(keydown); + let keyup = new KeyboardEvent('keyup', {'key': 'Escape', 'keyCode': 27}); + window.dispatchEvent(keyup); + + assert_array_equals(events, []); + + let keyup2 = document.createEvent("Event"); + keyup2.initEvent("keyup", true); + window.dispatchEvent(keyup2); + + assert_array_equals(events, []); +}, "close via synthesized Esc key must not work"); +</script> diff --git a/testing/web-platform/tests/close-watcher/event-properties.html b/testing/web-platform/tests/close-watcher/event-properties.html new file mode 100644 index 0000000000..6a3dbebad7 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/event-properties.html @@ -0,0 +1,27 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> +promise_test(async t => { + let closeEvent, cancelEvent; + const watcher = new CloseWatcher(); + watcher.oncancel = e => { cancelEvent = e; }; + watcher.onclose = e => { closeEvent = e; }; + + await test_driver.bless("call requestClose()", () => watcher.requestClose()); + + assert_equals(cancelEvent.constructor, Event, "cancel constructor"); + assert_false(cancelEvent.bubbles, "cancel bubles"); + assert_true(cancelEvent.cancelable, "cancel cancelable"); + + assert_equals(closeEvent.constructor, Event, "close constructor"); + assert_false(closeEvent.bubbles, "close bubles"); + assert_false(closeEvent.cancelable, "close cancelable"); +}, "cancel and close event properties are correct"); +</script> diff --git a/testing/web-platform/tests/close-watcher/frame-removal.html b/testing/web-platform/tests/close-watcher/frame-removal.html new file mode 100644 index 0000000000..573b16bb44 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/frame-removal.html @@ -0,0 +1,69 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> +<script> +promise_test(async (t) => { + const i = await setupIframe(); + const watcher = new i.contentWindow.CloseWatcher(); + watcher.oncancel = () => i.remove(); + watcher.onclose = () => t.unreached_func("close event must not fire"); + + watcher.requestClose(); +}, "detaching the iframe during the cancel event"); + +promise_test(async (t) => { + const i = await setupIframe(); + const watcher = new i.contentWindow.CloseWatcher(); + watcher.onclose = () => i.remove(); + + watcher.requestClose(); +}, "detaching the iframe during the close event"); + +promise_test(async (t) => { + const i = await setupIframe(); + const watcher = new i.contentWindow.CloseWatcher(); + i.remove(); + + watcher.destroy(); +}, "detaching the iframe then calling destroy()"); + +promise_test(async (t) => { + const i = await setupIframe(); + const watcher = new i.contentWindow.CloseWatcher(); + watcher.oncancel = () => t.unreached_func("cancel event must not fire"); + watcher.onclose = () => t.unreached_func("close event must not fire"); + i.remove(); + + watcher.close(); +}, "detaching the iframe then calling close()"); + +promise_test(async (t) => { + const i = await setupIframe(); + const watcher = new i.contentWindow.CloseWatcher(); + watcher.oncancel = () => t.unreached_func("cancel event must not fire"); + watcher.onclose = () => t.unreached_func("close event must not fire"); + i.remove(); + + watcher.requestClose(); +}, "detaching the iframe then calling requestClose()"); + +promise_test(async (t) => { + const i = await setupIframe(); + const iCloseWatcher = i.contentWindow.CloseWatcher; + const iDOMException = i.contentWindow.DOMException; + i.remove(); + + assert_throws_dom("InvalidStateError", iDOMException, () => new iCloseWatcher()); +}, "detaching the iframe then constructing a CloseWatcher"); + +function setupIframe() { + return new Promise(resolve => { + const i = document.createElement("iframe"); + i.onload = () => resolve(i); + i.src = "/common/blank.html"; + document.body.append(i); + }); +} +</script> diff --git a/testing/web-platform/tests/close-watcher/inside-event-listeners.html b/testing/web-platform/tests/close-watcher/inside-event-listeners.html new file mode 100644 index 0000000000..ac037fc147 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/inside-event-listeners.html @@ -0,0 +1,94 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.oncancel = () => { watcher.destroy(); } + + await test_driver.bless("give user activation so that cancel will fire", () => { + watcher.requestClose(); + }); + assert_array_equals(events, ["cancel"]); + + watcher.requestClose(); + assert_array_equals(events, ["cancel"], "since it was inactive, no more events fired"); +}, "destroy() inside oncancel"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.onclose = () => { watcher.destroy(); } + + watcher.requestClose(); + assert_array_equals(events, ["close"]); + + watcher.requestClose(); + assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); +}, "destroy() inside onclose"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.oncancel = () => { watcher.close(); } + + await test_driver.bless("give user activation so that cancel will fire", () => { + watcher.requestClose(); + }); + assert_array_equals(events, ["cancel", "close"]); + + watcher.requestClose(); + assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired"); +}, "close() inside oncancel"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.onclose = () => { watcher.close(); } + + watcher.requestClose(); + assert_array_equals(events, ["close"]); + + watcher.requestClose(); + assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); +}, "close() inside onclose"); + +promise_test(async t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.oncancel = () => { watcher.requestClose(); } + + await test_driver.bless("give user activation so that cancel will fire", () => { + watcher.requestClose(); + }); + assert_array_equals(events, ["cancel", "close"]); + + watcher.requestClose(); + assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired"); +}, "requestClose() inside oncancel"); + +test(t => { + let events = []; + let watcher = createRecordingCloseWatcher(t, events); + + watcher.onclose = () => { watcher.requestClose(); } + + watcher.requestClose(); + assert_array_equals(events, ["close"]); + + watcher.requestClose(); + assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); +}, "requestClose() inside onclose"); +</script> diff --git a/testing/web-platform/tests/close-watcher/popover-closewatcher-multiple-plus-free.html b/testing/web-platform/tests/close-watcher/popover-closewatcher-multiple-plus-free.html new file mode 100644 index 0000000000..4913b1454e --- /dev/null +++ b/testing/web-platform/tests/close-watcher/popover-closewatcher-multiple-plus-free.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel=help href="https://github.com/whatwg/html/pull/9462"> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<button id=b0>b0</button> + +<div id=p1 popover=auto> + <button id=b1>b1</button> + + <div id=p2 popover=auto> + <button id=b2>b2</button> + + <div id=p3 popover=auto>p3</div> + </div> +</div> + +<script> +promise_test(async () => { + p1.showPopover(); + await test_driver.click(b1); + p2.showPopover(); + p3.showPopover(); + assert_true(p1.matches(':popover-open'), 'p1 should be open.'); + assert_true(p2.matches(':popover-open'), 'p2 should be open.'); + assert_true(p3.matches(':popover-open'), 'p3 should be open.'); + + await sendCloseRequest(); + assert_true(p1.matches(':popover-open'), 'first escape: p1 should be open.'); + assert_false(p2.matches(':popover-open'), 'first escape: p2 should be closed.'); + assert_false(p3.matches(':popover-open'), 'first escape: p3 should be closed.'); + + await sendCloseRequest(); + assert_false(p1.matches(':popover-open'), 'second escape: p1 should be closed.'); + assert_false(p2.matches(':popover-open'), 'second escape: p2 should be closed.'); + assert_false(p3.matches(':popover-open'), 'second escape: p3 should be closed.'); +}, 'Multiple popovers opened from a single user activation close together, but original popover closes separately.'); +</script> diff --git a/testing/web-platform/tests/close-watcher/popover-closewatcher.html b/testing/web-platform/tests/close-watcher/popover-closewatcher.html new file mode 100644 index 0000000000..b40ea2ec7c --- /dev/null +++ b/testing/web-platform/tests/close-watcher/popover-closewatcher.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel=help href="https://github.com/whatwg/html/pull/9462"> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<button id=b0>b0</button> + +<div id=p1 popover=auto> + <button id=b1>b1</button> + + <div id=p2 popover=auto> + <button id=b2>b2</button> + + <div id=p3 popover=auto>p3</div> + </div> +</div> + +<script> +promise_test(async () => { + p1.showPopover(); + p2.showPopover(); + p3.showPopover(); + assert_true(p1.matches(':popover-open'), 'p1 should be open.'); + assert_true(p2.matches(':popover-open'), 'p2 should be open.'); + assert_true(p3.matches(':popover-open'), 'p3 should be open.'); + + await sendCloseRequest(); + assert_false(p1.matches(':popover-open'), 'p1 should be closed.'); + assert_false(p2.matches(':popover-open'), 'p2 should be closed.'); + assert_false(p3.matches(':popover-open'), 'p3 should be closed.'); +}, 'Opening multiple popovers without user activation causes them all to be closed with one close request.'); + +promise_test(async () => { + await test_driver.click(b0); + p1.showPopover(); + await test_driver.click(b1); + p2.showPopover(); + await test_driver.click(b2); + p3.showPopover(); + assert_true(p1.matches(':popover-open'), 'p1 should be open.'); + assert_true(p2.matches(':popover-open'), 'p2 should be open.'); + assert_true(p3.matches(':popover-open'), 'p3 should be open.'); + + await sendCloseRequest(); + assert_true(p1.matches(':popover-open'), 'first escape: p1 should be open.'); + assert_true(p2.matches(':popover-open'), 'first escape: p2 should be open.'); + assert_false(p3.matches(':popover-open'), 'first escape: p3 should be closed.'); + + await sendCloseRequest(); + assert_true(p1.matches(':popover-open'), 'second escape: p1 should be open.'); + assert_false(p2.matches(':popover-open'), 'second escape: p2 should be closed.'); + assert_false(p3.matches(':popover-open'), 'second escape: p3 should be closed.'); + + await sendCloseRequest(); + assert_false(p1.matches(':popover-open'), 'third escape: p1 should be closed.'); + assert_false(p2.matches(':popover-open'), 'third escape: p2 should be closed.'); + assert_false(p3.matches(':popover-open'), 'third escape: p3 should be closed.'); +}, 'Opening multiple popovers with user activation should close one at a time with close requests.'); +</script> diff --git a/testing/web-platform/tests/close-watcher/resources/helpers.js b/testing/web-platform/tests/close-watcher/resources/helpers.js new file mode 100644 index 0000000000..97a62309cd --- /dev/null +++ b/testing/web-platform/tests/close-watcher/resources/helpers.js @@ -0,0 +1,61 @@ +window.createRecordingCloseWatcher = (t, events, name, type, parentWatcher) => { + let watcher = null; + if (type === 'dialog') { + watcher = document.createElement('dialog'); + watcher.textContent = 'hello world'; + t.add_cleanup(() => watcher.remove()); + if (parentWatcher?.appendChild) { + parentWatcher.appendChild(watcher); + } else { + document.body.appendChild(watcher); + } + watcher.showModal(); + } else if (type === 'popover') { + watcher = document.createElement('div'); + watcher.setAttribute('popover', 'auto'); + watcher.textContent = 'hello world'; + t.add_cleanup(() => watcher.remove()); + if (parentWatcher?.appendChild) { + parentWatcher.appendChild(watcher); + } else { + document.body.appendChild(watcher); + } + watcher.showPopover(); + } else { + watcher = new CloseWatcher(); + t.add_cleanup(() => watcher.destroy()); + } + + const prefix = name === undefined ? "" : name + " "; + watcher.addEventListener('cancel', () => events.push(prefix + "cancel")); + watcher.addEventListener('close', () => events.push(prefix + "close")); + + return watcher; +}; + +window.createBlessedRecordingCloseWatcher = async (t, events, name, type, parentWatcher) => { + await maybeTopLayerBless(parentWatcher); + return createRecordingCloseWatcher(t, events, name, type, parentWatcher); +}; + +window.sendEscKey = () => { + // Esc is \uE00C, *not* \uu001B; see https://w3c.github.io/webdriver/#keyboard-actions. + // + // It's important to target document.body, and not any element that might stop receiving events + // if a popover or dialog is making that element inert. + return test_driver.send_keys(document.body, '\uE00C'); +}; + +// For now, we always use the Esc keypress as our close request. In +// theory, in the future, we could add a WebDriver command or similar +// for the close request, which would allow different tests on platforms +// with different close requests. In that case, we'd update this +// function, but not update the sendEscKey function above. +window.sendCloseRequest = window.sendEscKey; + +window.maybeTopLayerBless = (watcher) => { + if (watcher instanceof HTMLElement) { + return blessTopLayer(watcher); + } + return test_driver.bless(); +}; diff --git a/testing/web-platform/tests/close-watcher/user-activation-CloseWatcher.html b/testing/web-platform/tests/close-watcher/user-activation-CloseWatcher.html new file mode 100644 index 0000000000..70435993f5 --- /dev/null +++ b/testing/web-platform/tests/close-watcher/user-activation-CloseWatcher.html @@ -0,0 +1,75 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<!-- The dialog element does not support requestClose(), so these tests + are not shared between dialog elements and CloseWatchers. --> + +<body> +<script> +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher"); + + await test_driver.bless("call requestClose()", () => freeWatcher.requestClose()); + + assert_array_equals(events, ["freeWatcher cancel", "freeWatcher close"]); +}, "CloseWatchers created without user activation, but requestClose()d via user activation, fires cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher"); + freeWatcher.addEventListener("cancel", e => e.preventDefault()); + + await test_driver.bless("call requestClose()", () => freeWatcher.requestClose()); + + assert_array_equals(events, ["freeWatcher cancel"]); +}, "CloseWatchers created without user activation, but requestClose()d via user activation, fires cancel, which can be preventDefault()ed"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher"); + const activationWatcher = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher"); + + await test_driver.bless("call activationWatcher.requestClose()", () => activationWatcher.requestClose()); + assert_array_equals(events, ["activationWatcher cancel", "activationWatcher close"]); + + await test_driver.bless("call freeWatcher.requestClose()", () => freeWatcher.requestClose()); + assert_array_equals(events, ["activationWatcher cancel", "activationWatcher close", "freeWatcher cancel", "freeWatcher close"]); +}, "Creating a CloseWatcher from user activation, and requestClose()ing CloseWatchers with user activation, fires cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher"); + const activationWatcher1 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher1"); + activationWatcher1.addEventListener("cancel", e => e.preventDefault()); + + await test_driver.bless("call activationWatcher1.requestClose()", () => activationWatcher1.requestClose()); + assert_array_equals(events, ["activationWatcher1 cancel"]); + + // This time we go straight to close, without a second cancel. + activationWatcher1.requestClose(); + assert_array_equals(events, ["activationWatcher1 cancel", "activationWatcher1 close"]); + + freeWatcher.requestClose(); + assert_array_equals(events, ["activationWatcher1 cancel", "activationWatcher1 close", "freeWatcher close"]); +}, "3 user activations let you have 2 close watchers with 1 cancel event, even if the first cancel event is prevented"); + +promise_test(async t => { + const events = []; + const freeWatcher1 = createRecordingCloseWatcher(t, events, "freeWatcher1"); + + freeWatcher1.requestClose(); + assert_array_equals(events, ["freeWatcher1 close"]); + + const freeWatcher2 = createRecordingCloseWatcher(t, events, "freeWatcher2"); + + await sendCloseRequest(); + assert_array_equals(events, ["freeWatcher1 close", "freeWatcher2 close"]); +}, "requestClose()ing the free CloseWatcher allows a new free one to be created without user activation, and it receives the close request"); + +</script> diff --git a/testing/web-platform/tests/close-watcher/user-activation-multiple-plus-free.html b/testing/web-platform/tests/close-watcher/user-activation-multiple-plus-free.html new file mode 100644 index 0000000000..a94b47904a --- /dev/null +++ b/testing/web-platform/tests/close-watcher/user-activation-multiple-plus-free.html @@ -0,0 +1,32 @@ +<!doctype html> +<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/testdriver-actions.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> + +// This test needs to be separate from user-activation.html since, unlike those, +// it relies on there not being any lingering user activation from previous +// tests hanging around. That is, we need to be sure freeWatcher is created with +// no user activation, to ensure that activationWatcher1 and activationWatcher2 +// get grouped as expected. +promise_test(async t => { + const events = []; + createRecordingCloseWatcher(t, events, "freeWatcher"); + + await test_driver.bless("create two more CloseWatchers", () => { + createRecordingCloseWatcher(t, events, "activationWatcher1"); + createRecordingCloseWatcher(t, events, "activationWatcher2"); + }); + + await sendCloseRequest(); + assert_array_equals(events, ["activationWatcher2 close", "activationWatcher1 close"]); + + await sendCloseRequest(); + assert_array_equals(events, ["activationWatcher2 close", "activationWatcher1 close", "freeWatcher close"]); +}, "Multiple CloseWatchers created from a single user activation close together, but original free CloseWatcher closes separately"); +</script> diff --git a/testing/web-platform/tests/close-watcher/user-activation-shared.html b/testing/web-platform/tests/close-watcher/user-activation-shared.html new file mode 100644 index 0000000000..77e748532a --- /dev/null +++ b/testing/web-platform/tests/close-watcher/user-activation-shared.html @@ -0,0 +1,201 @@ +<!doctype html> +<meta name=variant content="?dialog"> +<meta name=variant content="?CloseWatcher"> +<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/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="resources/helpers.js"></script> + +<body> +<script> +const type = location.search.substring(1); +const waitForPotentialCloseEvent = () => { + // CloseWatchers fire close events synchronously, but dialog elements wait + // for a rAF before firing them. + return new Promise(resolve => requestAnimationFrame(resolve)); +}; + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + freeWatcher.addEventListener("cancel", e => e.preventDefault()); + + await maybeTopLayerBless(freeWatcher); + freeWatcher.close(); + await waitForPotentialCloseEvent(); + + assert_array_equals(events, ["freeWatcher close"]); +}, "Close watchers created without user activation, but close()d via user activation, do not fire cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + + await maybeTopLayerBless(freeWatcher); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + + assert_array_equals(events, ["freeWatcher cancel", "freeWatcher close"]); +}, "Close watchers created without user activation, but closed via a close request after user activation, fires cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + freeWatcher.addEventListener("cancel", e => e.preventDefault()); + + await maybeTopLayerBless(freeWatcher); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + + assert_array_equals(events, ["freeWatcher cancel"]); +}, "Close watchers created without user activation, but closed via a close request after user activation, fires cancel, which can be preventDefault()ed"); + +promise_test(async t => { + const events = []; + createRecordingCloseWatcher(t, events, "freeWatcher", type); + createRecordingCloseWatcher(t, events, "watcher1", type); + createRecordingCloseWatcher(t, events, "watcher2", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher2 close", "watcher1 close", "freeWatcher close"]); +}, "Multiple close watchers created without user activation close together (with no cancel)"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + await createBlessedRecordingCloseWatcher(t, events, "activationWatcher", type, freeWatcher); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher close", "freeWatcher close"]); +}, "Creating a close watcher from user activation keeps it separate from the free close watcher, but they don't fire cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + const activationWatcher = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher", type, freeWatcher); + + await maybeTopLayerBless(activationWatcher); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher cancel", "activationWatcher close"]); + + await maybeTopLayerBless(freeWatcher); + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher cancel", "activationWatcher close", "freeWatcher cancel", "freeWatcher close"]); +}, "Creating a close watcher from user activation, and closing close watchers with a close request after user activation, fires cancel"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + const activationWatcher1 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher1", type, freeWatcher); + await createBlessedRecordingCloseWatcher(t, events, "activationWatcher2", type, activationWatcher1); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["activationWatcher2 close", "activationWatcher1 close"]); +}, "Multiple close watchers created with user activation close in reverse order"); + +promise_test(async t => { + const events = []; + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + const activationWatcher1 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher1", type, freeWatcher); + const activationWatcher2 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher2", type, activationWatcher1); + await createBlessedRecordingCloseWatcher(t, events, "activationWatcher3", type, activationWatcher2); + createRecordingCloseWatcher(t, events, "watcher4", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 close", "activationWatcher3 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 close", "activationWatcher3 close", "activationWatcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 close", "activationWatcher3 close", "activationWatcher2 close", "activationWatcher1 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 close", "activationWatcher3 close", "activationWatcher2 close", "activationWatcher1 close", "freeWatcher close"]); +}, "3 user activations let you have 3 + 1 = 4 ungrouped close watchers/0 cancel events"); + +promise_test(async t => { + const events = []; + const freeWatcher1 = createRecordingCloseWatcher(t, events, "freeWatcher1", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher1 close"]); + + const freeWatcher2 = createRecordingCloseWatcher(t, events, "freeWatcher2", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher1 close", "freeWatcher2 close"]); +}, "closing the free close watcher via a close request allows a new free one to be created without user activation, and it receives a second close request"); + +promise_test(async t => { + const events = []; + const activationWatcher = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher", type); + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher close", "activationWatcher close"]); +}, "The second watcher can be the free watcher, if the first is created with user activation"); + +promise_test(async t => { + const events = []; + const activationWatcher1 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher1", type); + const activationWatcher2 = await createBlessedRecordingCloseWatcher(t, events, "activationWatcher2", type, activationWatcher1); + const freeWatcher = createRecordingCloseWatcher(t, events, "freeWatcher", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher close", "activationWatcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["freeWatcher close", "activationWatcher2 close", "activationWatcher1 close"]); +}, "The third watcher can be the free watcher, if the first two are created with user activation"); + +promise_test(async t => { + const events = []; + const freeWatcher1 = createRecordingCloseWatcher(t, events, "freeWatcher1"); + + if (freeWatcher1 instanceof HTMLDialogElement) { + freeWatcher1.close(); + } else { + freeWatcher1.destroy(); + } + assert_array_equals(events, []); + + const freeWatcher2 = createRecordingCloseWatcher(t, events, "freeWatcher2"); + + await sendCloseRequest(); + assert_array_equals(events, ["freeWatcher2 close"]); +}, "destroy()ing the free close watcher allows a new free one to be created without user activation, and it receives the close request"); +</script> |