script: Chain up keyboard scrolling to parent <iframe>s (#39469)

When an `<iframe>` cannot scroll because the size of the frame is
greater than or
equal to the size of page contents, chain up the keyboard scroll
operation to the parent frame.

Testing: A new Servo-only WPT tests is added, though needs to be
manually
run with `--product servodriver`.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-25 13:16:41 +02:00 committed by GitHub
parent 75e32ba5a4
commit ffdb7d3663
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 406 additions and 132 deletions

View file

@ -12757,15 +12757,6 @@
{}
]
],
"keyboard-scrolling.html": [
"2d9a0c40272d8af49f26de8dc49283df68b2d7b0",
[
null,
{
"testdriver": true
}
]
],
"matchMedia.html": [
"45a7ea268b1ebdba69e947b79d675cc9221428d4",
[
@ -13811,6 +13802,24 @@
{}
]
],
"keyboard-scrolling-iframe.sub.html": [
"e002eb6f2a35ed4f73a079acb05b3d99eb04813b",
[
null,
{
"testdriver": true
}
]
],
"keyboard-scrolling.html": [
"45916c5d11f351f0ca10cde5e52d5ee60c11bd9d",
[
null,
{
"testdriver": true
}
]
],
"keyframe-infinite-percentage.html": [
"36ba83eeac401653356fa38edf30c94d38fd8542",
[

View file

@ -0,0 +1,165 @@
<!doctype html>
<meta charset="utf-8">
<title>A test to verify that keyboard scrolling works properly in Servo in IFRAMEs.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<style>
body {
margin: 0;
}
iframe {
position: fixed;
width: 200px;
height: 200px;
outline: solid;
}
</style>
<!-- This is an IFRAME that should be scrollable via keyboard. -->
<iframe id="iframe" style="left: 100px; top: 100px;" srcdoc='
<!DOCTYPE html>
<body style="margin: 0;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<!-- This IFRAME does not scroll, so keyboard events should chain to the main frame. -->
<iframe id="iframeWithSmallContent" style="left: 300px; top: 100px;" src='iframe_child1.html'></iframe>
<!-- This IFRAME does not scroll and is also cross origin, so keyboard events should chain to the main frame. -->
<iframe id="iframeCrossOriginWithSmallContent" style="left: 100px; top: 300px;" src='//{{hosts[alt][]}}:{{ports[http][0]}}/_mozilla/mozilla/iframe_child1.html'></iframe>
<!-- This IFRAME does not scroll because the body has overflow: hidden, so keyboard events should chain to the main frame. -->
<iframe id="iframeWithOverflowHiddenBody" style="left: 300px; top: 300px;" srcdoc='
<!DOCTYPE html>
<body style="overflow:hidden;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<div style="width: 300vw; height: 300vh; background: green;">
Lorem ipsum dolor sit amet,
</div>
<script>
const end = "\uE010";
const home = "\uE011";
const arrowDown = "\uE015";
const arrowUp = "\uE013";
const arrowRight = "\uE014";
const arrowLeft = "\uE012";
const pageDown = "\uE00F";
const pageUp = "\uE00E";
const lineSize = 76;
const pageSize = scrollportHeight => scrollportHeight - 2 * lineSize;
const pressKeyAndAssert = async (key, element, [expectedX, expectedY], description) => {
await test_driver.send_keys(document.body, key);
const actualX =
element == null ? scrollX
: element.nodeName == "IFRAME" ? element.contentWindow.scrollX
: element.scrollLeft;
const actualY =
element == null ? scrollY
: element.nodeName == "IFRAME" ? element.contentWindow.scrollY
: element.scrollTop;
assert_array_equals([actualX, actualY], [expectedX, expectedY], description);
};
promise_test(async () => {
await test_driver.click(iframe);
let bottom = iframe.contentDocument.documentElement.scrollHeight - iframe.contentWindow.innerHeight;
await pressKeyAndAssert(end, iframe, [0, bottom], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize * 2], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, lineSize], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, 0], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize * 2, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowLeft, iframe, [lineSize, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(arrowLeft, iframe, [0, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight) * 2], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageUp key scrolls #iframe up by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, 0], "PageUp key scrolls #iframe up by almost a screenful");
// At the bottom of the IFRAME, we should not chain up to scrolling the document.
await pressKeyAndAssert(end, iframe, [0, bottom], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
await pressKeyAndAssert(arrowDown, iframe, [0, bottom], "ArrowDown should not move the iframe past the max Y position");
assert_array_equals([scrollX, scrollY], [0, 0], "Keyboard scroll on a div should not chain to body");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
}, "Keyboard scrolling works in #iframe");
promise_test(async () => {
await test_driver.click(iframeWithOverflowHiddenBody);
await pressKeyAndAssert(arrowDown, iframeWithOverflowHiddenBody, [0, 0], "Arrow down key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowDown, iframeWithOverflowHiddenBody, [0, 0], "Arrow down key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowUp, iframeWithOverflowHiddenBody, [0, 0], "Arrow up key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithOverflowHiddenBody");
await pressKeyAndAssert(arrowUp, iframeWithOverflowHiddenBody, [0, 0], "Arrow up key should not scroll #iframeWithOverflowHiddenBody");
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeWithOverflowHiddenBody");
}, "Keyboard scrolling on iframe with a body that has `overflow:hidden` chains to main document");
promise_test(async () => {
await test_driver.click(iframeCrossOriginWithSmallContent);
function waitForScrollEvent() {
return new Promise((resolve) => {
addEventListener("scroll", () => {
resolve();
}, { "once": true })
}
)
}
// We must create the promise before actually triggering the event, as the event might
// be fired while we are awaiting the keyboard action.
let promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowDown);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowDown);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowUp);
await promise;
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
promise = waitForScrollEvent();
await test_driver.send_keys(document.body, arrowUp);
await promise;
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeCrossOriginWithSmallContent");
}, "Keyboard scrolling on cross-origin iframe with small content chains to main document");
promise_test(async () => {
await test_driver.click(iframeWithSmallContent);
await pressKeyAndAssert(arrowDown, iframeWithSmallContent, [0, 0], "Arrow down key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowDown, iframeWithSmallContent, [0, 0], "Arrow down key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize * 2], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowUp, iframeWithSmallContent, [0, 0], "Arrow up key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, lineSize], "The body should scroll instead of #iframeWithSmallContent");
await pressKeyAndAssert(arrowUp, iframeWithSmallContent, [0, 0], "Arrow up key should not scroll #iframeWithSmallContent");
assert_array_equals([scrollX, scrollY], [0, 0], "The body should scroll instead of #iframeWithSmallContent");
}, "Keyboard scrolling on iframe with small content chains to main document");
</script>

View file

@ -1,6 +1,6 @@
<!doctype html>
<meta charset="utf-8">
<title>CSS test: Calc expressions with numbers should still serialize as calc()</title>
<title>A test to verify that keyboard scrolling works properly in Servo.</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
@ -36,15 +36,8 @@
<div style="width: 100px; height: 100px; background: blue;">Lorem ipsum dolor sit amet,</div>
</div>
<!-- This is an IFRAME that should be scrollable via keyboard. -->
<iframe id="iframe" style="left: 300px; top: 300px;" srcdoc='
<!doctype html><meta charset="utf-8">
<body style="margin: 0;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
'></iframe>
<!-- This is a DIV with `overflow: hidden` that should not be keyboard scrollable as its content is smaller than the DIV. -->
<div id="boxWithOverflowHidden" class="scroller" style="overflow: hidden; left: 500px; top: 300px;">
<div id="boxWithOverflowHidden" class="scroller" style="overflow: hidden; left: 300px; top: 300px;">
<div style="width: 600px; height: 600px; background: blue;">Lorem ipsum dolor sit amet,</div>
</div>
@ -66,13 +59,9 @@ const pageSize = scrollportHeight => scrollportHeight - 2 * lineSize;
const pressKeyAndAssert = async (key, element, [expectedX, expectedY], description) => {
await test_driver.send_keys(document.body, key);
const actualX =
element == null ? scrollX
: element.nodeName == "IFRAME" ? element.contentWindow.scrollX
: element.scrollLeft;
element == null ? scrollX : element.scrollLeft;
const actualY =
element == null ? scrollY
: element.nodeName == "IFRAME" ? element.contentWindow.scrollY
: element.scrollTop;
element == null ? scrollY : element.scrollTop;
assert_array_equals([actualX, actualY], [expectedX, expectedY], description);
};
@ -156,27 +145,6 @@ promise_test(async () => {
await pressKeyAndAssert(home, null, [0, 0], "Home key scrolls viewport to top");
}, "Keyboard scrolling chains past inactive overflow:scroll DIVs");
promise_test(async () => {
await test_driver.click(iframe);
await pressKeyAndAssert(end, iframe, [0, iframe.contentDocument.documentElement.scrollHeight - iframe.contentWindow.innerHeight], "End key scrolls #iframe to bottom");
await pressKeyAndAssert(home, iframe, [0, 0], "Home key scrolls #iframe to top");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowDown, iframe, [0, lineSize * 2], "ArrowDown key scrolls #iframe down by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, lineSize], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowUp, iframe, [0, 0], "ArrowUp key scrolls #iframe up by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowRight, iframe, [lineSize * 2, 0], "ArrowRight key scrolls #iframe right by a line");
await pressKeyAndAssert(arrowLeft, iframe, [lineSize, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(arrowLeft, iframe, [0, 0], "ArrowLeft key scrolls #iframe left by a line");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageDown, iframe, [0, pageSize(iframe.contentWindow.innerHeight) * 2], "PageDown key scrolls #iframe down by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, pageSize(iframe.contentWindow.innerHeight)], "PageUp key scrolls #iframe up by almost a screenful");
await pressKeyAndAssert(pageUp, iframe, [0, 0], "PageUp key scrolls #iframe up by almost a screenful");
// TODO: test that scrolls chain up from iframe when they fail.
}, "Keyboard scrolling works in #iframe");
promise_test(async () => {
await test_driver.click(boxWithOverflowHidden);