summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/close-watcher
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/close-watcher
parentInitial commit. (diff)
downloadfirefox-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')
-rw-r--r--testing/web-platform/tests/close-watcher/META.yml4
-rw-r--r--testing/web-platform/tests/close-watcher/abortsignal.html123
-rw-r--r--testing/web-platform/tests/close-watcher/basic.html82
-rw-r--r--testing/web-platform/tests/close-watcher/closewatcher-dialog-popover.html98
-rw-r--r--testing/web-platform/tests/close-watcher/esc-key.html77
-rw-r--r--testing/web-platform/tests/close-watcher/event-properties.html27
-rw-r--r--testing/web-platform/tests/close-watcher/frame-removal.html69
-rw-r--r--testing/web-platform/tests/close-watcher/inside-event-listeners.html94
-rw-r--r--testing/web-platform/tests/close-watcher/popover-closewatcher-multiple-plus-free.html43
-rw-r--r--testing/web-platform/tests/close-watcher/popover-closewatcher.html64
-rw-r--r--testing/web-platform/tests/close-watcher/resources/helpers.js61
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation-CloseWatcher.html75
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation-multiple-plus-free.html32
-rw-r--r--testing/web-platform/tests/close-watcher/user-activation-shared.html201
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>