DevTools: sources for HTML files should be the whole HTML file (#37456)

To show the contents of inline scripts in the Sources panel, we need to
send the whole HTML file from script to devtools, not just the script
code. This is trickier than the external script case, but we can look to
[how Firefox does
it](https://servo.zulipchat.com/#narrow/channel/263398-general/topic/Getting.20the.20original.20page.20HTML.20from.20script/near/524392861)
for some inspiration. The process is as follows:

- when we execute a script
  - notify devtools to create the source actor
- if it’s an external script, send the script code to the devtools
server
  - if it’s an inline script, don’t send any source contents yet
  - devtools stores the contents in the source actor
- while loading a new document
  - buffer the markup, so we can send it to devtools
- when we finish loading a new document
  - send the buffered markup to the devtools server
- devtools stores the contents in any source actors with no contents yet
- when a source actor gets a `source` request
  - if we have the contents, send those contents to the client
- if we don’t have the contents (inline script that loaded while
devtools was closed)
    - FUTURE: try to fetch the markup out of cache
    - otherwise send `<!-- not available; please reload! -->`

Testing: Several tests added to test the changes, also updates an
existing test with correct assertion
Fixes: https://github.com/servo/servo/issues/36874

---------

Signed-off-by: atbrakhi <atbrakhi@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
atbrakhi 2025-06-21 20:46:35 +02:00 committed by GitHub
parent 3feec90528
commit c8ee11fe77
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 177 additions and 29 deletions

View file

@ -96,9 +96,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
self.assert_sources_list(1, set([tuple(["data:text/html,<script type=module>;</script>"])]))
def test_source_content_inline_script(self):
script_content = "console.log('Hello, world!');"
self.run_servoshell(url=f"data:text/html,<script>{script_content}</script>")
self.assert_source_content("data:text/html,<script>console.log('Hello, world!');</script>", script_content)
script_tag = "<script>console.log('Hello, world!')</script>"
self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag)
def test_source_content_external_script(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
@ -106,6 +106,41 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
expected_content = 'console.log("external classic");\n'
self.assert_source_content(f"{self.base_url}/classic.js", expected_content)
def test_source_content_html_file(self):
self.start_web_server(test_dir=self.get_test_path("sources"))
self.run_servoshell()
expected_content = open(self.get_test_path("sources/test.html")).read()
self.assert_source_content(f"{self.base_url}/test.html", expected_content)
# Test case that uses innerHTML and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
def test_source_content_inline_script_with_inner_html(self):
script_tag = '<div id="el"></div><script>el.innerHTML="<p>test"</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag)
# Test case that uses outerHTML and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
def test_source_content_inline_script_with_outer_html(self):
script_tag = '<div id="el"></div><script>el.outerHTML="<p>test"</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag)
# Test case that uses DOMParser and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
def test_source_content_inline_script_with_domparser(self):
script_tag = '<script>(new DOMParser).parseFromString("<p>test","text/html")</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag)
# Test case that uses XMLHttpRequest#responseXML and would actually need the HTML parser
# (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
def test_source_content_inline_script_with_responsexml(self):
self.start_web_server(test_dir=self.get_test_path("sources_content_with_responsexml"))
self.run_servoshell()
expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read()
self.assert_source_content(f"{self.base_url}/test.html", expected_content)
# Sets `base_url` and `web_server` and `web_server_thread`.
def start_web_server(self, *, test_dir=None):
if test_dir is None:
@ -270,6 +305,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
client.disconnect()
def get_test_path(self, path: str) -> str:
return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path))
def run_tests(script_path, build_type: BuildType):
DevtoolsTests.script_path = script_path

View file

@ -0,0 +1 @@
<!doctype html><meta charset=utf-8>

View file

@ -0,0 +1,11 @@
<!doctype html><meta charset=utf-8>
<script>
const req = new XMLHttpRequest;
req.responseType = "document";
req.open("GET", "empty.html");
req.send(null);
req.onreadystatechange = () => {
if (req.readyState == XMLHttpRequest.DONE)
console.log(req.responseXML);
};
</script>