devtools: Expose introductionType to devtools clients (#38541)

in the devtools protocol, [source
forms](https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources)
announced in `resources-available-array` messages can include the
`introductionType`, which more or less mirrors the field of the same
name in SpiderMonkey’s CompileOptions.

this patch exposes `introductionType` accordingly, allowing us to check
for the correct values in automated tests.

Testing: new coverage in devtools tests
Fixes: part of #36027

---------

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
shuppy 2025-08-08 20:20:30 +08:00 committed by GitHub
parent 23c0947072
commit c9541f2906
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 232 additions and 42 deletions

View file

@ -27,6 +27,8 @@ pub(crate) struct SourceForm {
/// URL of the script, or URL of the page for inline scripts. /// URL of the script, or URL of the page for inline scripts.
pub url: String, pub url: String,
pub is_black_boxed: bool, pub is_black_boxed: bool,
/// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
pub introduction_type: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -53,6 +55,9 @@ pub struct SourceActor {
pub content: Option<String>, pub content: Option<String>,
pub content_type: Option<String>, pub content_type: Option<String>,
/// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
pub introduction_type: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -91,6 +96,7 @@ impl SourceActor {
url: ServoUrl, url: ServoUrl,
content: Option<String>, content: Option<String>,
content_type: Option<String>, content_type: Option<String>,
introduction_type: String,
) -> SourceActor { ) -> SourceActor {
SourceActor { SourceActor {
name, name,
@ -98,6 +104,7 @@ impl SourceActor {
content, content,
content_type, content_type,
is_black_boxed: false, is_black_boxed: false,
introduction_type,
} }
} }
@ -107,10 +114,17 @@ impl SourceActor {
url: ServoUrl, url: ServoUrl,
content: Option<String>, content: Option<String>,
content_type: Option<String>, content_type: Option<String>,
introduction_type: String,
) -> &SourceActor { ) -> &SourceActor {
let source_actor_name = actors.new_name("source"); let source_actor_name = actors.new_name("source");
let source_actor = SourceActor::new(source_actor_name.clone(), url, content, content_type); let source_actor = SourceActor::new(
source_actor_name.clone(),
url,
content,
content_type,
introduction_type,
);
actors.register(Box::new(source_actor)); actors.register(Box::new(source_actor));
actors.register_source_actor(pipeline_id, &source_actor_name); actors.register_source_actor(pipeline_id, &source_actor_name);
@ -122,6 +136,7 @@ impl SourceActor {
actor: self.name.clone(), actor: self.name.clone(),
url: self.url.to_string(), url: self.url.to_string(),
is_black_boxed: self.is_black_boxed, is_black_boxed: self.is_black_boxed,
introduction_type: self.introduction_type.clone(),
} }
} }
} }

View file

@ -552,6 +552,7 @@ impl DevtoolsInstance {
source_info.url, source_info.url,
source_content, source_content,
source_info.content_type, source_info.content_type,
source_info.introduction_type,
); );
let source_actor_name = source_actor.name.clone(); let source_actor_name = source_actor.name.clone();
let source_form = source_actor.source_form(); let source_form = source_actor.source_form();

View file

@ -59,7 +59,9 @@ use crate::fetch::{CspViolationsProcessor, load_whole_resource};
use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender}; use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender};
use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
use crate::script_runtime::ScriptThreadEventCategory::WorkerEvent; use crate::script_runtime::ScriptThreadEventCategory::WorkerEvent;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext}; use crate::script_runtime::{
CanGc, IntroductionType, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext,
};
use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue}; use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
use crate::task_source::{SendableTaskSource, TaskSourceName}; use crate::task_source::{SendableTaskSource, TaskSourceName};
@ -517,6 +519,10 @@ impl DedicatedWorkerGlobalScope {
let pipeline_id = global_scope.pipeline_id(); let pipeline_id = global_scope.pipeline_id();
let source_info = SourceInfo { let source_info = SourceInfo {
url: metadata.final_url, url: metadata.final_url,
introduction_type: IntroductionType::WORKER
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
external: true, // Worker scripts are always external. external: true, // Worker scripts are always external.
worker_id: Some(global.upcast::<WorkerGlobalScope>().get_worker_id()), worker_id: Some(global.upcast::<WorkerGlobalScope>().get_worker_id()),
content: Some(source.to_string()), content: Some(source.to_string()),

View file

@ -1095,14 +1095,23 @@ impl HTMLScriptElement {
if let Some(chan) = self.global().devtools_chan() { if let Some(chan) = self.global().devtools_chan() {
let pipeline_id = self.global().pipeline_id(); let pipeline_id = self.global().pipeline_id();
let (url, content, content_type, is_external) = if script.external { let (url, content, content_type, introduction_type, is_external) = if script.external {
let content = match &script.code { let content = match &script.code {
SourceCode::Text(text) => text.to_string(), SourceCode::Text(text) => text.to_string(),
SourceCode::Compiled(compiled) => compiled.original_text.to_string(), SourceCode::Compiled(compiled) => compiled.original_text.to_string(),
}; };
// content_type: https://html.spec.whatwg.org/multipage/#scriptingLanguages // content_type: https://html.spec.whatwg.org/multipage/#scriptingLanguages
(script.url.clone(), Some(content), "text/javascript", true) (
script.url.clone(),
Some(content),
"text/javascript",
IntroductionType::SRC_SCRIPT
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
true,
)
} else { } else {
// TODO: if needed, fetch the page again, in the same way as in the original request. // TODO: if needed, fetch the page again, in the same way as in the original request.
// Fetch it from cache, even if the original request was non-idempotent (e.g. POST). // Fetch it from cache, even if the original request was non-idempotent (e.g. POST).
@ -1110,11 +1119,21 @@ impl HTMLScriptElement {
// fetch, the server could return a different response. // fetch, the server could return a different response.
// TODO: handle cases where Content-Type is not text/html. // TODO: handle cases where Content-Type is not text/html.
(doc.url(), None, "text/html", false) (
doc.url(),
None,
"text/html",
IntroductionType::INLINE_SCRIPT
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
false,
)
}; };
let source_info = SourceInfo { let source_info = SourceInfo {
url, url,
introduction_type: introduction_type.to_owned(),
external: is_external, external: is_external,
worker_id: None, worker_id: None,
content, content,

View file

@ -72,7 +72,7 @@ use crate::dom::workernavigator::WorkerNavigator;
use crate::fetch::{CspViolationsProcessor, Fetch, load_whole_resource}; use crate::fetch::{CspViolationsProcessor, Fetch, load_whole_resource};
use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender}; use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender};
use crate::realms::{InRealm, enter_realm}; use crate::realms::{InRealm, enter_realm};
use crate::script_runtime::{CanGc, JSContext, JSContextHelper, Runtime}; use crate::script_runtime::{CanGc, IntroductionType, JSContext, JSContextHelper, Runtime};
use crate::task::TaskCanceller; use crate::task::TaskCanceller;
use crate::timers::{IsInterval, TimerCallback}; use crate::timers::{IsInterval, TimerCallback};
@ -644,12 +644,13 @@ impl WorkerGlobalScope {
let _aes = AutoEntryScript::new(self.upcast()); let _aes = AutoEntryScript::new(self.upcast());
let cx = self.runtime.borrow().as_ref().unwrap().cx(); let cx = self.runtime.borrow().as_ref().unwrap().cx();
rooted!(in(cx) let mut rval = UndefinedValue()); rooted!(in(cx) let mut rval = UndefinedValue());
let options = self let mut options = self
.runtime .runtime
.borrow() .borrow()
.as_ref() .as_ref()
.unwrap() .unwrap()
.new_compile_options(self.worker_url.borrow().as_str(), 1); .new_compile_options(self.worker_url.borrow().as_str(), 1);
options.set_introduction_type(IntroductionType::WORKER);
match self.runtime.borrow().as_ref().unwrap().evaluate_script( match self.runtime.borrow().as_ref().unwrap().evaluate_script(
self.reflector().get_jsobject(), self.reflector().get_jsobject(),
&source, &source,

View file

@ -1249,7 +1249,21 @@ impl Runnable {
pub(crate) use script_bindings::script_runtime::CanGc; pub(crate) use script_bindings::script_runtime::CanGc;
/// `introductionType` values in SpiderMonkey TransitiveCompileOptions. /// `introductionType` values in SpiderMonkey TransitiveCompileOptions.
///
/// Value definitions are based on the SpiderMonkey Debugger API docs:
/// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Source.html#introductiontype>
// TODO: squish `scriptElement` <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/server/actors/source.js#199-201>
pub(crate) struct IntroductionType; pub(crate) struct IntroductionType;
impl IntroductionType { impl IntroductionType {
pub const INLINE_SCRIPT: &'static CStr = c"inlineScript"; /// `introductionType` for code belonging to `<script src="file.js">` elements.
/// This includes `<script type="module" src="...">`.
pub const SRC_SCRIPT: &CStr = c"srcScript";
/// `introductionType` for code belonging to `<script>code;</script>` elements.
/// This includes `<script type="module" src="...">`.
pub const INLINE_SCRIPT: &CStr = c"inlineScript";
/// `introductionType` for web workers.
/// <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/docs/user/debugger-api/debugger.source/index.rst#96>
pub const WORKER: &CStr = c"Worker";
} }

View file

@ -583,6 +583,7 @@ impl fmt::Display for ShadowRootMode {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct SourceInfo { pub struct SourceInfo {
pub url: ServoUrl, pub url: ServoUrl,
pub introduction_type: String,
pub external: bool, pub external: bool,
pub worker_id: Option<WorkerId>, pub worker_id: Option<WorkerId>,
pub content: Option<String>, pub content: Option<String>,

View file

@ -8,6 +8,7 @@
# except according to those terms. # except according to those terms.
from concurrent.futures import Future from concurrent.futures import Future
from dataclasses import dataclass
import logging import logging
from geckordp.actors.root import RootActor from geckordp.actors.root import RootActor
from geckordp.actors.descriptors.tab import TabActor from geckordp.actors.descriptors.tab import TabActor
@ -30,6 +31,12 @@ from servo.command_base import BuildType
LOG_REQUESTS = False LOG_REQUESTS = False
@dataclass(frozen=True)
class Source:
introduction_type: str
url: str
class DevtoolsTests(unittest.IsolatedAsyncioTestCase): class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# /path/to/servo/python/servo # /path/to/servo/python/servo
script_path = None script_path = None
@ -50,67 +57,163 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# - <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree> # - <https://html.spec.whatwg.org/multipage/#fetch-a-module-worker-script-tree>
# Non-worker(?) script sources can be inline, external, or blob. # Non-worker(?) script sources can be inline, external, or blob.
# Worker script sources can be external or blob. # Worker script sources can be external or blob.
# Sources list
def test_sources_list(self): def test_sources_list(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell() self.run_servoshell()
self.assert_sources_list( self.assert_sources_list(
2,
set( set(
[ [
# TODO: update expectations when we fix ES modules
tuple( tuple(
[ [
f"{self.base_urls[0]}/classic.js", Source("srcScript", f"{self.base_urls[0]}/classic.js"),
f"{self.base_urls[0]}/test.html", Source("inlineScript", f"{self.base_urls[0]}/test.html"),
f"{self.base_urls[1]}/classic.js", Source("srcScript", f"{self.base_urls[1]}/classic.js"),
f"{self.base_urls[0]}/test.html", Source("inlineScript", f"{self.base_urls[0]}/test.html"),
] ]
), ),
tuple([f"{self.base_urls[0]}/worker.js"]), tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]),
] ]
), ),
) )
def test_sources_list_with_data_no_scripts(self): def test_sources_list_with_data_no_scripts(self):
self.run_servoshell(url="data:text/html,") self.run_servoshell(url="data:text/html,")
self.assert_sources_list(1, set([tuple()])) self.assert_sources_list(set([tuple()]))
# Sources list for `introductionType` = `inlineScript` and `srcScript`
def test_sources_list_with_data_empty_inline_classic_script(self): def test_sources_list_with_data_empty_inline_classic_script(self):
self.run_servoshell(url="data:text/html,<script></script>") self.run_servoshell(url="data:text/html,<script></script>")
self.assert_sources_list(1, set([tuple()])) self.assert_sources_list(set([tuple()]))
def test_sources_list_with_data_inline_classic_script(self): def test_sources_list_with_data_inline_classic_script(self):
self.run_servoshell(url="data:text/html,<script>;</script>") self.run_servoshell(url="data:text/html,<script>;</script>")
self.assert_sources_list(1, set([tuple(["data:text/html,<script>;</script>"])])) self.assert_sources_list(set([tuple([Source("inlineScript", "data:text/html,<script>;</script>")])]))
def test_sources_list_with_data_external_classic_script(self): def test_sources_list_with_data_external_classic_script(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>') self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>')
self.assert_sources_list(1, set([tuple([f"{self.base_urls[0]}/classic.js"])])) self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/classic.js")])]))
def test_sources_list_with_data_empty_inline_module_script(self): def test_sources_list_with_data_empty_inline_module_script(self):
self.run_servoshell(url="data:text/html,<script type=module></script>") self.run_servoshell(url="data:text/html,<script type=module></script>")
self.assert_sources_list(1, set([tuple()])) self.assert_sources_list(set([tuple()]))
def test_sources_list_with_data_inline_module_script(self): def test_sources_list_with_data_inline_module_script(self):
self.run_servoshell(url="data:text/html,<script type=module>;</script>") self.run_servoshell(url="data:text/html,<script type=module>;</script>")
self.assert_sources_list(1, set([tuple(["data:text/html,<script type=module>;</script>"])])) self.assert_sources_list(
set([tuple([Source("inlineScript", "data:text/html,<script type=module>;</script>")])])
)
def test_sources_list_with_data_external_module_script(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_data_external_module_script.html")
self.assert_sources_list(set([tuple([Source("srcScript", f"{self.base_urls[0]}/module.js")])]))
# Sources list for `introductionType` = `importedModule`
@unittest.expectedFailure
def test_sources_list_with_static_import_module(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html")
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript", f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html"
),
Source("importedModule", f"{self.base_urls[0]}/module.js"),
]
)
]
),
)
@unittest.expectedFailure
def test_sources_list_with_dynamic_import_module(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html")
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript", f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html"
),
Source("importedModule", f"{self.base_urls[0]}/module.js"),
]
)
]
),
)
# Sources list for `introductionType` = `Worker`
def test_sources_list_with_classic_worker(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_classic_worker.html")
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_classic_worker.html"),
]
),
tuple(
[
Source("Worker", f"{self.base_urls[0]}/classic_worker.js"),
]
),
]
),
)
def test_sources_list_with_module_worker(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_module_worker.html")
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", f"{self.base_urls[0]}/test_sources_list_with_module_worker.html"),
]
),
tuple(
[
Source("Worker", f"{self.base_urls[0]}/module_worker.js"),
]
),
]
),
)
# Source contents
def test_source_content_inline_script(self): def test_source_content_inline_script(self):
script_tag = "<script>console.log('Hello, world!')</script>" script_tag = "<script>console.log('Hello, world!')</script>"
self.run_servoshell(url=f"data:text/html,{script_tag}") self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag) self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag)
def test_source_content_external_script(self): def test_source_content_external_script(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources")) self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>') self.run_servoshell(url=f'data:text/html,<script src="{self.base_urls[0]}/classic.js"></script>')
expected_content = 'console.log("external classic");\n' expected_content = 'console.log("external classic");\n'
self.assert_source_content(f"{self.base_urls[0]}/classic.js", expected_content) self.assert_source_content(Source("srcScript", f"{self.base_urls[0]}/classic.js"), expected_content)
def test_source_content_html_file(self): def test_source_content_html_file(self):
self.start_web_server(test_dir=self.get_test_path("sources")) self.start_web_server(test_dir=self.get_test_path("sources"))
self.run_servoshell() self.run_servoshell()
expected_content = open(self.get_test_path("sources/test.html")).read() expected_content = open(self.get_test_path("sources/test.html")).read()
self.assert_source_content(f"{self.base_urls[0]}/test.html", expected_content) self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content)
def test_source_content_with_inline_module_import_external(self): def test_source_content_with_inline_module_import_external(self):
self.start_web_server(test_dir=self.get_test_path("sources_content_with_inline_module_import_external")) self.start_web_server(test_dir=self.get_test_path("sources_content_with_inline_module_import_external"))
@ -118,28 +221,28 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
expected_content = open( expected_content = open(
self.get_test_path("sources_content_with_inline_module_import_external/test.html") self.get_test_path("sources_content_with_inline_module_import_external/test.html")
).read() ).read()
self.assert_source_content(f"{self.base_urls[0]}/test.html", expected_content) self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content)
# Test case that uses innerHTML and would actually need the HTML parser # 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') # (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): def test_source_content_inline_script_with_inner_html(self):
script_tag = '<div id="el"></div><script>el.innerHTML="<p>test"</script>' script_tag = '<div id="el"></div><script>el.innerHTML="<p>test"</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}") self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag) self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag)
# Test case that uses outerHTML and would actually need the HTML parser # 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') # (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): def test_source_content_inline_script_with_outer_html(self):
script_tag = '<div id="el"></div><script>el.outerHTML="<p>test"</script>' script_tag = '<div id="el"></div><script>el.outerHTML="<p>test"</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}") self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag) self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag)
# Test case that uses DOMParser and would actually need the HTML parser # 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') # (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): def test_source_content_inline_script_with_domparser(self):
script_tag = '<script>(new DOMParser).parseFromString("<p>test","text/html")</script>' script_tag = '<script>(new DOMParser).parseFromString("<p>test","text/html")</script>'
self.run_servoshell(url=f"data:text/html,{script_tag}") self.run_servoshell(url=f"data:text/html,{script_tag}")
self.assert_source_content(f"data:text/html,{script_tag}", script_tag) self.assert_source_content(Source("inlineScript", f"data:text/html,{script_tag}"), script_tag)
# Test case that uses XMLHttpRequest#responseXML and would actually need the HTML parser # 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') # (innerHTML has a fast path for values that dont contain b'&' | b'\0' | b'<' | b'\r')
@ -147,7 +250,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
self.start_web_server(test_dir=self.get_test_path("sources_content_with_responsexml")) self.start_web_server(test_dir=self.get_test_path("sources_content_with_responsexml"))
self.run_servoshell() self.run_servoshell()
expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read() expected_content = open(self.get_test_path("sources_content_with_responsexml/test.html")).read()
self.assert_source_content(f"{self.base_urls[0]}/test.html", expected_content) self.assert_source_content(Source("inlineScript", f"{self.base_urls[0]}/test.html"), expected_content)
# Sets `base_url` and `web_server` and `web_server_thread`. # Sets `base_url` and `web_server` and `web_server_thread`.
def start_web_server(self, *, test_dir=None, num_servers=2): def start_web_server(self, *, test_dir=None, num_servers=2):
@ -254,21 +357,22 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
return client, watcher, targets return client, watcher, targets
def assert_sources_list(self, expected_targets: int, expected_urls_by_target: set[tuple[str]]): def assert_sources_list(self, expected_sources_by_target: set[tuple[Source]]):
expected_targets = len(expected_sources_by_target)
client, watcher, targets = self._setup_devtools_client(expected_targets) client, watcher, targets = self._setup_devtools_client(expected_targets)
done = Future() done = Future()
# NOTE: breaks if two targets have the same list of source urls. # NOTE: breaks if two targets have the same list of source urls.
# This should really be a multiset, but Python does not have multisets. # This should really be a multiset, but Python does not have multisets.
actual_urls_by_target: set[tuple[str]] = set() actual_sources_by_target: set[tuple[Source]] = set()
def on_source_resource(data): def on_source_resource(data):
for [resource_type, sources] in data["array"]: for [resource_type, sources] in data["array"]:
try: try:
self.assertEqual(resource_type, "source") self.assertEqual(resource_type, "source")
source_urls = tuple([source["url"] for source in sources]) source_urls = tuple([Source(source["introductionType"], source["url"]) for source in sources])
self.assertFalse(source_urls in sources) # See NOTE above self.assertFalse(source_urls in actual_sources_by_target) # See NOTE above
actual_urls_by_target.add(source_urls) actual_sources_by_target.add(source_urls)
if len(actual_urls_by_target) == expected_targets: if len(actual_sources_by_target) == expected_targets:
done.set_result(None) done.set_result(None)
except Exception as e: except Exception as e:
# Raising here does nothing, for some reason. # Raising here does nothing, for some reason.
@ -286,10 +390,10 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
result: Optional[Exception] = done.result(1) result: Optional[Exception] = done.result(1)
if result: if result:
raise result raise result
self.assertEqual(actual_urls_by_target, expected_urls_by_target) self.assertEqual(actual_sources_by_target, expected_sources_by_target)
client.disconnect() client.disconnect()
def assert_source_content(self, source_url: str, expected_content: str): def assert_source_content(self, expected_source: Source, expected_content: str):
client, watcher, targets = self._setup_devtools_client() client, watcher, targets = self._setup_devtools_client()
done = Future() done = Future()
@ -300,8 +404,8 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
try: try:
self.assertEqual(resource_type, "source") self.assertEqual(resource_type, "source")
for source in sources: for source in sources:
if source["url"] == source_url: if Source(source["introductionType"], source["url"]) == expected_source:
source_actors[source_url] = source["actor"] source_actors[expected_source] = source["actor"]
done.set_result(None) done.set_result(None)
except Exception as e: except Exception as e:
done.set_result(e) done.set_result(e)
@ -319,8 +423,8 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
raise result raise result
# We found at least one source with the given url. # We found at least one source with the given url.
self.assertIn(source_url, source_actors) self.assertIn(expected_source, source_actors)
source_actor = source_actors[source_url] source_actor = source_actors[expected_source]
response = client.send_receive({"to": source_actor, "type": "source"}) response = client.send_receive({"to": source_actor, "type": "source"})

View file

@ -0,0 +1,4 @@
console.log("external classic worker");
// Prevent worker exiting before devtools client can query sources
setInterval(() => {}, 0);

View file

@ -0,0 +1,4 @@
console.log("external module worker");
// Prevent worker exiting before devtools client can query sources
setInterval(() => {}, 0);

View file

@ -2,7 +2,7 @@
<script src="classic.js"></script> <script src="classic.js"></script>
<script> <script>
console.log("inline classic"); console.log("inline classic");
new Worker("worker.js"); new Worker("classic_worker.js");
</script> </script>
<script type="module"> <script type="module">
import module from "./module.js"; import module from "./module.js";

View file

@ -0,0 +1,5 @@
<!doctype html><meta charset=utf-8>
<script>
console.log("inline classic");
new Worker("classic_worker.js");
</script>

View file

@ -0,0 +1,2 @@
<!doctype html><meta charset=utf-8>
<script type="module" src="module.js"></script>

View file

@ -0,0 +1,5 @@
<!doctype html><meta charset=utf-8>
<script type="module">
console.log("inline module");
import("./module.js");
</script>

View file

@ -0,0 +1,5 @@
<!doctype html><meta charset=utf-8>
<script>
console.log("inline classic");
new Worker("module_worker.js");
</script>

View file

@ -0,0 +1,5 @@
<!doctype html><meta charset=utf-8>
<script type="module">
import module from "./module.js";
console.log("inline module");
</script>

View file

@ -1 +0,0 @@
console.log("external classic worker");