Improve inter-document focus handling (#36649)

*Describe the changes that this pull request makes here. This will be
the commit message.*
rewritten the PR #28571
Implement
[Window#focus](https://html.spec.whatwg.org/multipage/#dom-window-focus),
[Window#blur](https://html.spec.whatwg.org/multipage/#dom-window-blur)
Testing: WPT
Fixes: #8981 #9421

---------

Signed-off-by: kongbai1996 <1782765876@qq.com>
Co-authored-by: yvt <i@yvt.jp>
This commit is contained in:
Fuguo 2025-04-30 12:37:53 +08:00 committed by GitHub
parent 27570987fd
commit 0c0ee04b8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1123 additions and 242 deletions

View file

@ -1,3 +0,0 @@
[activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html]
[Check focus event and active element after focusing different site iframe then immediately focusing back]
expected: FAIL

View file

@ -1,3 +0,0 @@
[activeelement-after-focusing-different-site-iframe.html]
[Check trailing events]
expected: FAIL

View file

@ -1,2 +1,3 @@
[activeelement-after-focusing-same-site-iframe-contentwindow.html]
expected: TIMEOUT
[Check result]
expected: FAIL

View file

@ -1,3 +0,0 @@
[activeelement-after-focusing-same-site-iframe.html]
[Check trailing events]
expected: FAIL

View file

@ -1,2 +1,3 @@
[activeelement-after-immediately-focusing-different-site-iframe-contentwindow.html]
expected: TIMEOUT
[Check result]
expected: FAIL

View file

@ -1,2 +1,3 @@
[activeelement-after-immediately-focusing-same-site-iframe-contentwindow.html]
expected: TIMEOUT
[Check result]
expected: FAIL

View file

@ -1,2 +1,3 @@
[focus-restoration-in-different-site-iframes-window.html]
expected: TIMEOUT
[Check result]
expected: FAIL

View file

@ -1,2 +1,3 @@
[focus-restoration-in-same-site-iframes-window.html]
expected: TIMEOUT
[Check result]
expected: FAIL

View file

@ -1,2 +0,0 @@
[iframe-focuses-parent-same-site.html]
expected: TIMEOUT

View file

@ -1,7 +1,4 @@
[cross-origin-objects-function-caching.html]
[Cross-origin Window methods are cached]
expected: FAIL
[Cross-origin Location `replace` method is cached]
expected: FAIL

View file

@ -1,3 +0,0 @@
[focus.window.html]
[focus]
expected: FAIL

View file

@ -328,9 +328,3 @@
[A SecurityError exception must be thrown when window.stop is accessed from a different origin.]
expected: FAIL
[A SecurityError exception should not be thrown when window.blur is accessed from a different origin.]
expected: FAIL
[A SecurityError exception should not be thrown when window.focus is accessed from a different origin.]
expected: FAIL

View file

@ -1,9 +1,4 @@
[window-properties.https.html]
[Window method: focus]
expected: FAIL
[Window method: blur]
expected: FAIL
[Window method: print]
expected: FAIL

View file

@ -1738,9 +1738,6 @@
[Document interface: attribute all]
expected: FAIL
[Window interface: operation focus()]
expected: FAIL
[Window interface: attribute scrollbars]
expected: FAIL
@ -1870,9 +1867,6 @@
[Document interface: new Document() must inherit property "dir" with the proper type]
expected: FAIL
[Window interface: window must inherit property "blur()" with the proper type]
expected: FAIL
[Document interface: operation execCommand(DOMString, optional boolean, optional DOMString)]
expected: FAIL
@ -1897,9 +1891,6 @@
[Document interface: iframe.contentDocument must inherit property "queryCommandEnabled(DOMString)" with the proper type]
expected: FAIL
[Window interface: operation blur()]
expected: FAIL
[Document interface: iframe.contentDocument must inherit property "onslotchange" with the proper type]
expected: FAIL
@ -1924,9 +1915,6 @@
[Document interface: documentWithHandlers must inherit property "onauxclick" with the proper type]
expected: FAIL
[Window interface: window must inherit property "focus()" with the proper type]
expected: FAIL
[Document interface: documentWithHandlers must inherit property "onwebkitanimationend" with the proper type]
expected: FAIL

View file

@ -1,12 +1,6 @@
[event-listeners.window.html]
[Standard event listeners are to be removed from Window]
expected: FAIL
[Standard event listeners are to be removed from Window for an active but not fully active document]
expected: FAIL
[Standard event listeners are to be removed from Window for a non-active document that is the associated Document of a Window (frame is removed)]
expected: FAIL
[Custom event listeners are to be removed from Window for an active but not fully active document]
expected: FAIL

View file

@ -1,6 +1,3 @@
[global-object-implicit-this-value-cross-realm.html]
[Cross-realm global object's operation throws when called on incompatible object]
expected: FAIL
[Cross-realm global object's operation called on null / undefined]
expected: FAIL

View file

@ -12744,7 +12744,7 @@
]
},
"FocusEvent.html": [
"9e002c1088de060b5e7f94c4152bf9fb779c04cc",
"7fb7aebf2afbac7f68a16308b9cc5d4588b7022f",
[
null,
{}
@ -13278,6 +13278,13 @@
{}
]
],
"focus_inter_documents.html": [
"5c759772367e844066d1df0081917c9e129d09ec",
[
null,
{}
]
],
"follow-hyperlink.html": [
"6ac9eaeb5814a663988ed8c664c113072e329dc5",
[

View file

@ -48,13 +48,6 @@
]
},
{
element: document.body,
expected_events: [
{element: input3, event_name: "blur"},
]
}
];
var idx = 0;

View file

@ -0,0 +1,207 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<iframe id="f1"></iframe>
<iframe id="f2"></iframe>
<input id="d0">
<script>
/** Wait for an `event` event to be fired on `element`. Resolves to a boolean
* value indicating whether the event was fired within a predetermined period. */
async function waitForEvent(element, event) {
let listener;
try {
return await new Promise(resolve => {
setTimeout(() => resolve(false), 1000);
listener = () => resolve(true);
element.addEventListener(event, listener);
});
} finally {
if (listener) {
element.removeEventListener(event, listener);
}
}
}
promise_test(async t => {
await new Promise(r => window.onload = r);
const d0 = document.getElementById("d0");
// This test requires the document to have focus as a starting condition.
if (!document.hasFocus() || document.activeElement !== d0) {
const p = new Promise(r => d0.onfocus = r);
d0.focus();
await p;
}
assert_true(document.hasFocus(), "Document has focus as starting condition.");
assert_equals(document.activeElement, d0, "`d0` has focus as starting condition.");
}, "Starting condition");
promise_test(async t => {
const d0 = document.getElementById("d0");
const f1 = document.getElementById("f1");
f1.contentDocument.body.innerHTML = '<input id=d1>';
const d1 = f1.contentDocument.getElementById("d1");
const p0 = waitForEvent(d1, 'focus');
const p1 = waitForEvent(f1, 'focus');
const p2 = waitForEvent(f1.contentWindow, 'focus');
const p3 = waitForEvent(d0, 'blur');
d1.focus();
assert_true(await p0, "`d1.focus` fires in time");
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
assert_true(await p2, "`f1.contentWindow.focus` fires in time");
assert_true(await p3, "`d0.blur` fires in time");
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
assert_equals(f1.contentDocument.activeElement, d1, "f1's contentDocument's activeElement is `d1`");
}, "Focusing an element in a nested browsing context also focuses the container");
promise_test(async t => {
const f1 = document.getElementById("f1");
const d1 = f1.contentDocument.getElementById("d1");
const f2 = document.getElementById("f2");
f2.contentDocument.body.innerHTML = '<input id=d2>';
const d2 = f2.contentDocument.getElementById("d2");
const p0 = waitForEvent(d1, 'blur');
const p1 = waitForEvent(f1, 'blur');
const p2 = waitForEvent(f1.contentWindow, 'blur');
d2.focus();
assert_true(await p0, "`d1.blur` fires in time");
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
assert_true(await p2, "`f1.contentWindow.blur` fires in time");
// Wait for any ongoing execution of the focus update steps to complete
await new Promise(r => window.setTimeout(r, 0));
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
assert_equals(f2.contentDocument.activeElement, d2, "f2's contentDocument's activeElement is `d2`");
assert_false(f1.contentDocument.hasFocus(), "f1's contentDocument does not have focus");
assert_equals(f1.contentDocument.activeElement, f1.contentDocument.body, "f1's contentDocument's activeElement is its body");
}, "Focusing an element in a different container also unfocuses the previously focused element and its container");
promise_test(async t => {
const d0 = document.getElementById("d0");
const f2 = document.getElementById("f2");
const d2 = f2.contentDocument.getElementById("d2");
const p0 = waitForEvent(d2, 'blur');
const p1 = waitForEvent(f2, 'blur');
const p2 = waitForEvent(f2.contentWindow, 'blur');
const p3 = waitForEvent(d0, 'focus');
d0.focus();
assert_true(await p0, "`d2.blur` fires in time");
await p1; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
assert_true(await p2, "`f2.contentWindow.blur` fires in time");
assert_true(await p3, "`d0.focus` fires in time");
// Wait for any ongoing execution of the focus update steps to complete
await new Promise(r => window.setTimeout(r, 0));
assert_equals(document.activeElement, d0, "The top-level document's activeElement is `d0`");
assert_false(f2.contentDocument.hasFocus(), "f2's contentDocument does not have focus");
assert_equals(f2.contentDocument.activeElement, f2.contentDocument.body, "f2's contentDocument's activeElement is its body");
}, "Unfocusing a container also unfocuses any focused elements within");
promise_test(async t => {
const f1 = document.getElementById("f1");
const p0 = waitForEvent(f1, 'focus');
const p1 = waitForEvent(f1.contentWindow, 'focus');
f1.focus();
await p0; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
assert_true(await p1, "`f1.contentWindow.focus` fires in time");
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
}, "Focusing a container changes the contained document's 'has focus steps' result");
promise_test(async t => {
const f1 = document.getElementById("f1");
// `f1` should be focused because of the previous step
assert_equals(document.activeElement, f1, "The top-level document's activeElement is `f1`");
// Navigate the focused container
const pLoad = new Promise(resolve => window.subframeIsReady = resolve);
f1.srcdoc = "<script>window.parent.subframeIsReady();</" + "script>";
await pLoad;
// Allow some delay before the document finally receives focus
if (!f1.contentDocument.hasFocus()) {
await waitForEvent(f1.contentWindow, 'focus');
}
assert_true(f1.contentDocument.hasFocus(), "f1's contentDocument has focus");
}, "When a focused container navigates, the new document should receive focus");
promise_test(async t => {
const f2 = document.getElementById("f2");
const p0 = waitForEvent(f2, 'focus');
const p1 = waitForEvent(f2.contentWindow, 'focus');
f2.contentWindow.focus();
await p0; // FIXME: doesn't fire on Firefox, Blink, Edge, and WebKit
assert_true(await p1, "`f2.contentWindow.focus` fires in time");
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
}, "Focusing the window of a nested browsing context also focuses the container");
promise_test(async t => {
const f2 = document.getElementById("f2");
const d2 = f2.contentDocument.getElementById("d2");
{
const p = waitForEvent(d2, 'focus');
f2.focus();
d2.focus();
await p;
}
const p0 = waitForEvent(d2, 'blur');
d2.blur();
assert_true(await p0, "`d2.blur` fires in time");
// FIXME: This passes on Firefox, Blink, and WebKit but is not spec-
// compliant. Per spec, the top-level document's viewport should be
// focused instead.
//
// <https://html.spec.whatwg.org/multipage/#get-the-focusable-area>
//
// > The unfocusing steps for an object `old focus target`` that is either a
// > focusable area or an element that is not a focusable area are as
// > follows: [...]
// >
// > 7. If `topDocument`'s browsing context has system focus, then run the
// > focusing steps for topDocument's viewport.
assert_equals(document.activeElement, f2, "The top-level document's activeElement is `f2`");
assert_equals(f2.contentDocument.activeElement, f2.contentDocument.body, "f2's contentDocument's activeElement is its body");
assert_true(f2.contentDocument.hasFocus(), "f2's contentDocument has focus");
}, "Blurring an element in a nested browsing context focuses its node document");
</script>
</body>
</html>