LibWeb: Repaint iframe containers after render unblocking
When an embedded document was render-blocked, the parent display list recording skipped drawing that iframe's compositor surface. Once the child document later unblocked, only the child surface was repainted, so the parent could keep replaying a display list that had no surface draw command until another invalidation, such as resize, forced a recording. Invalidate the containing iframe when a document's render-blocking set becomes empty so the parent display list records the child surface. Add a deterministic reftest that gates a render-blocking stylesheet on a test-server signal, forcing the parent to paint the blocked iframe before unblocking it.
This commit is contained in:
parent
ca97f68cb7
commit
412a66ad5e
5 changed files with 132 additions and 4 deletions
|
|
@ -7721,8 +7721,15 @@ void Document::add_render_blocking_element(GC::Ref<Element> element)
|
|||
void Document::remove_render_blocking_element(GC::Ref<Element> element)
|
||||
{
|
||||
m_render_blocking_elements.remove(element);
|
||||
if (m_render_blocking_elements.is_empty())
|
||||
page().client().request_frame();
|
||||
if (!m_render_blocking_elements.is_empty())
|
||||
return;
|
||||
|
||||
if (auto navigable = this->navigable()) {
|
||||
if (auto container = navigable->container())
|
||||
container->set_needs_repaint(InvalidateDisplayList::Yes);
|
||||
}
|
||||
|
||||
page().client().request_frame();
|
||||
}
|
||||
|
||||
// https://fullscreen.spec.whatwg.org/#run-the-fullscreen-steps
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import os
|
|||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ class Echo:
|
|||
reason_phrase: Optional[str]
|
||||
reflect_headers_in_body: bool
|
||||
close_connection: bool
|
||||
wait_for_unblock: Optional[str]
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Echo):
|
||||
|
|
@ -51,6 +53,7 @@ class Echo:
|
|||
and self.reason_phrase == other.reason_phrase
|
||||
and self.reflect_headers_in_body == other.reflect_headers_in_body
|
||||
and self.close_connection == other.close_connection
|
||||
and self.wait_for_unblock == other.wait_for_unblock
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -60,6 +63,9 @@ echo_store: Dict[str, Echo] = {}
|
|||
# Headers from the most recent request at each echo path, queryable via GET /recorded-request-headers<echo-path>.
|
||||
recorded_request_headers: Dict[str, Dict[str, list]] = {}
|
||||
|
||||
# Named events used by tests that need deterministic delayed responses.
|
||||
unblock_events: Dict[str, threading.Event] = {}
|
||||
|
||||
|
||||
class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
static_directory: str
|
||||
|
|
@ -115,7 +121,9 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
super().do_GET()
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/echo"):
|
||||
if self.path.startswith("/unblock/"):
|
||||
self._serve_unblock()
|
||||
elif self.path.startswith("/echo"):
|
||||
self.handle_echo()
|
||||
elif self.path.startswith("/recorded-request-headers/"):
|
||||
self._serve_recorded_request_headers()
|
||||
|
|
@ -173,6 +181,7 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
echo.reason_phrase = data.get("reason_phrase", None)
|
||||
echo.reflect_headers_in_body = data.get("reflect_headers_in_body", False)
|
||||
echo.close_connection = data.get("close_connection", False)
|
||||
echo.wait_for_unblock = data.get("wait_for_unblock", None)
|
||||
|
||||
is_invalid_echo_path = echo.path is None or not echo.path.startswith("/echo/")
|
||||
|
||||
|
|
@ -205,6 +214,8 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
return
|
||||
|
||||
echo_store[key] = echo
|
||||
if echo.wait_for_unblock is not None:
|
||||
unblock_events[echo.wait_for_unblock] = threading.Event()
|
||||
|
||||
host = self.headers.get("host", "localhost")
|
||||
path = echo.path.lstrip("/")
|
||||
|
|
@ -222,6 +233,14 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
self.wfile.write(json.dumps(fetch_config).encode("utf-8"))
|
||||
|
||||
def _serve_unblock(self):
|
||||
token = urllib.parse.unquote(self.path[len("/unblock/") :])
|
||||
event = unblock_events.setdefault(token, threading.Event())
|
||||
event.set()
|
||||
self.send_response(204)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
|
||||
def _serve_recorded_request_headers(self):
|
||||
echo_path = self.path[len("/recorded-request-headers") :]
|
||||
headers = recorded_request_headers.get(echo_path)
|
||||
|
|
@ -268,6 +287,10 @@ class TestHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|||
self.connection.close()
|
||||
return
|
||||
|
||||
if echo.wait_for_unblock is not None:
|
||||
event = unblock_events.setdefault(echo.wait_for_unblock, threading.Event())
|
||||
event.wait()
|
||||
|
||||
response_headers = echo.headers.copy()
|
||||
|
||||
if echo.delay_ms is not None:
|
||||
|
|
@ -363,7 +386,8 @@ def start_server(port, static_directory):
|
|||
TestHTTPRequestHandler.wpt_directory = os.path.join(
|
||||
TestHTTPRequestHandler.static_directory, "Text", "input", "wpt-import"
|
||||
)
|
||||
httpd = socketserver.TCPServer(("127.0.0.1", port), TestHTTPRequestHandler)
|
||||
httpd = socketserver.ThreadingTCPServer(("127.0.0.1", port), TestHTTPRequestHandler)
|
||||
httpd.daemon_threads = True
|
||||
|
||||
print(httpd.socket.getsockname()[1])
|
||||
sys.stdout.flush()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
#box {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
<div id="box"></div>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="reftest-wait">
|
||||
<head>
|
||||
<link
|
||||
rel="match"
|
||||
href="../expected/iframe-render-blocked-child-paints-after-unblock-ref.html"
|
||||
/>
|
||||
<script src="../../Text/input/include.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: white;
|
||||
}
|
||||
|
||||
iframe {
|
||||
display: block;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 0;
|
||||
background: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<iframe></iframe>
|
||||
<script>
|
||||
const nextFrame = () => new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
const server = httpTestServer();
|
||||
const unblockToken = "iframe-render-blocked-child-paints-after-unblock";
|
||||
const stylesheetURL = await server.createEcho("GET", "/iframe-render-blocked-child-paints-after-unblock.css", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/css",
|
||||
},
|
||||
body: "body { margin: 0; }",
|
||||
wait_for_unblock: unblockToken,
|
||||
});
|
||||
|
||||
const childHTML = "\x3C!DOCTYPE html>"
|
||||
+ '\x3Clink id="blocker" rel="stylesheet" href="' + stylesheetURL + '">'
|
||||
+ '\x3Cbody style="margin: 0; background: green;">\x3C/body>';
|
||||
const childURL = await server.createEcho("GET", "/iframe-render-blocked-child-paints-after-unblock-child.html", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/html",
|
||||
},
|
||||
body: childHTML,
|
||||
});
|
||||
|
||||
const iframe = document.querySelector("iframe");
|
||||
const iframeLoaded = new Promise(resolve => {
|
||||
iframe.addEventListener("load", resolve, { once: true });
|
||||
});
|
||||
iframe.src = childURL;
|
||||
|
||||
let blocker = null;
|
||||
while (!blocker) {
|
||||
blocker = iframe.contentDocument && iframe.contentDocument.getElementById("blocker");
|
||||
if (!blocker)
|
||||
await nextFrame();
|
||||
}
|
||||
|
||||
const blockerLoaded = new Promise(resolve => {
|
||||
blocker.addEventListener("load", resolve);
|
||||
});
|
||||
|
||||
await nextFrame();
|
||||
await nextFrame();
|
||||
|
||||
await fetch(`${server.baseURL}/unblock/${unblockToken}`);
|
||||
await blockerLoaded;
|
||||
await iframeLoaded;
|
||||
|
||||
await nextFrame();
|
||||
await nextFrame();
|
||||
document.documentElement.className = "";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1 @@
|
|||
Content-Type: text/html
|
||||
Loading…
Reference in a new issue