fix(workers): share intrinsic bootstrap error events

This commit is contained in:
ldm0
2026-09-24 15:30:24 +08:00
committed by Donough Liu
parent f5a934c033
commit 1ebdb981fe
24 changed files with 374 additions and 298 deletions
+2 -3
View File
@@ -461,9 +461,8 @@ use self::window_runtime::global_caches_getter_callback;
pub(crate) use self::window_runtime::install_child_window_own_methods;
pub(crate) use self::window_template::install_window_own_template_bindings;
pub(crate) use self::worker_host::{
dispatch_worker_error_event_with_error, dispatch_worker_error_event_with_kind,
dispatch_worker_event, flush_pending_worker_messages_for_listener,
worker_has_message_delivery_listener,
dispatch_worker_error_event_with_kind, dispatch_worker_event,
flush_pending_worker_messages_for_listener, worker_has_message_delivery_listener,
};
pub(crate) use self::worker_location_runtime::install_worker_location_runtime_state;
pub(super) use super::{
@@ -1,7 +1,7 @@
use super::{
CHILD_BROWSING_CONTEXT_HANDLE_SLOT, MessagePortRealmBinding,
ensure_message_port_wrapper_for_id_in_realm,
events::{clear_event_dispatch_fields, set_event_dispatch_fields},
events::{clear_event_dispatch_fields, construct_original_event, set_event_dispatch_fields},
invoke_simple_event_listener,
navigation_serialize::{
current_document_content_security_policies, current_document_referrer_policy,
@@ -87,22 +87,6 @@ struct SharedWorkerObjectDeclaration<'scope> {
port: v8::Local<'scope, v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(plain)]
struct SharedWorkerHostEventInitDeclaration {
#[webapi(data_property, enumerable)]
cancelable: bool,
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::Event, prototype = "Object")]
struct SharedWorkerHostEventFallbackDeclaration {
#[webapi(data_property, enumerable)]
r#type: String,
#[webapi(data_property, enumerable)]
cancelable: bool,
}
#[derive(WebApiObject)]
#[webapi(plain)]
struct SharedWorkerHostErrorEventInitDeclaration<'scope> {
@@ -137,16 +121,6 @@ struct SharedWorkerHostErrorEventFallbackDeclaration<'scope, 'text> {
error: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
#[webapi(plain, scope_lifetime = 'scope, data_properties, enumerable)]
struct SharedWorkerHostErrorEventDetailsDeclaration<'scope, 'text> {
message: &'text str,
filename: &'text str,
lineno: u32,
colno: u32,
error: v8::Local<'scope, v8::Value>,
}
struct SharedWorkerOptions {
name: String,
script_kind: WorkerScriptKind,
@@ -785,15 +759,16 @@ fn dispatch_shared_worker_error_event<'s>(
event_kind: crate::worker::WorkerParentErrorEventKind,
) -> bool {
let event = match event_kind {
// Fetch/parse failures fire a plain Event with the default flags.
crate::worker::WorkerParentErrorEventKind::Event => {
let event = new_event(scope, "error", true);
set_error_event_details(scope, event, message, filename, lineno, colno, error);
event
}
crate::worker::WorkerParentErrorEventKind::ErrorEvent => {
new_error_event(scope, message, filename, lineno, colno, error)
construct_original_event(scope, "error")
}
crate::worker::WorkerParentErrorEventKind::ErrorEvent => Some(new_error_event(
scope, message, filename, lineno, colno, error,
)),
};
let Some(event) = event else { return false };
crate::context_bootstrap::mark_event_trusted(scope, event);
set_event_dispatch_fields(scope, worker, event);
let listeners = simple_object_event_listeners_snapshot(
@@ -815,7 +790,8 @@ fn dispatch_shared_worker_error_event<'s>(
&[event.into()],
event,
);
if listener.handler_slot.as_deref() == Some(SHARED_WORKER_ONERROR_SLOT)
if event_kind == crate::worker::WorkerParentErrorEventKind::ErrorEvent
&& listener.handler_slot.as_deref() == Some(SHARED_WORKER_ONERROR_SLOT)
&& let Some(returned) = callback_result
&& v8::Local::new(scope, &returned).boolean_value(scope)
{
@@ -845,35 +821,6 @@ fn dispatch_shared_worker_error_event<'s>(
dispatched
}
fn new_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event_type: &str,
cancelable: bool,
) -> v8::Local<'s, v8::Object> {
let global = scope.get_current_context().global(scope);
if let Some(event_ctor) = global
.get(scope, v8str(scope, "Event").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
{
let init = SharedWorkerHostEventInitDeclaration::new(cancelable)
.bind(scope)
.expect("SharedWorker host Event init declaration should bind");
if let Some(event) = event_ctor.new_instance(
scope,
&[
v8::String::new(scope, event_type).unwrap().into(),
init.into(),
],
) {
return event;
}
}
SharedWorkerHostEventFallbackDeclaration::new(event_type.to_owned(), cancelable)
.bind(scope)
.expect("SharedWorker host Event fallback declaration should bind")
}
fn new_error_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
message: &str,
@@ -911,17 +858,3 @@ fn new_error_event<'s>(
.bind(scope)
.expect("SharedWorker host ErrorEvent fallback declaration should bind")
}
fn set_error_event_details<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
message: &str,
filename: &str,
lineno: u32,
colno: u32,
error: v8::Local<'s, v8::Value>,
) {
SharedWorkerHostErrorEventDetailsDeclaration::new(message, filename, lineno, colno, error)
.initialize(scope, event)
.expect("SharedWorker host ErrorEvent details declaration should initialize");
}
@@ -488,7 +488,7 @@ pub(in crate::context_bootstrap) fn worker_constructor_callback<'s>(
lineno: 0,
colno: 0,
event_kind: crate::worker::WorkerParentErrorEventKind::Event,
phase: crate::worker::WorkerErrorPhase::Runtime,
phase: crate::worker::WorkerErrorPhase::Bootstrap,
source: crate::worker::WorkerErrorSource::Runtime,
}),
},
@@ -6,7 +6,7 @@ use moli_webapi_declare::WebApiObject;
use super::{WORKER_LISTENERS_SLOT, WORKER_ONERROR_SLOT};
use crate::context_bootstrap::{
dispatch_simple_event_target_event,
events::{clear_event_dispatch_fields, set_event_dispatch_fields},
events::{clear_event_dispatch_fields, construct_original_event, set_event_dispatch_fields},
invoke_simple_event_listener, simple_object_event_listeners_snapshot,
simple_object_event_remove_listener_value_for_type,
};
@@ -14,22 +14,6 @@ use crate::structured_clone::V8StructuredClonePayload;
use crate::util::v8str;
use crate::worker::{WorkerParentErrorEventKind, WorkerToParentMessage};
#[derive(WebApiObject)]
#[webapi(plain)]
struct WorkerHostEventInitDeclaration {
#[webapi(data_property, enumerable)]
cancelable: bool,
}
#[derive(WebApiObject)]
#[webapi(interface = web_api_interfaces::Event, prototype = "Object")]
struct WorkerHostEventFallbackDeclaration {
#[webapi(data_property, enumerable)]
r#type: String,
#[webapi(data_property, enumerable)]
cancelable: bool,
}
#[derive(WebApiObject)]
#[webapi(plain)]
struct WorkerHostMessageEventInitDeclaration<'scope> {
@@ -84,16 +68,6 @@ struct WorkerHostErrorEventFallbackDeclaration<'scope, 'text> {
error: v8::Local<'scope, v8::Value>,
}
#[derive(WebApiObject)]
#[webapi(plain, scope_lifetime = 'scope, data_properties, enumerable)]
struct WorkerHostErrorEventDetailsDeclaration<'scope, 'text> {
message: &'text str,
filename: &'text str,
lineno: u32,
colno: u32,
error: v8::Local<'scope, v8::Value>,
}
pub(crate) fn dispatch_worker_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
worker: v8::Local<'s, v8::Object>,
@@ -255,27 +229,6 @@ pub(crate) fn flush_pending_worker_messages_for_listener<'s>(
let _ = (scope, worker);
}
pub(crate) fn dispatch_worker_error_event_with_error<'s>(
scope: &mut v8::PinScope<'s, '_>,
worker: v8::Local<'s, v8::Object>,
message: &str,
filename: &str,
lineno: u32,
colno: u32,
error: v8::Local<'s, v8::Value>,
) -> bool {
dispatch_worker_error_event_with_kind(
scope,
worker,
message,
filename,
lineno,
colno,
error,
WorkerParentErrorEventKind::ErrorEvent,
)
}
pub(crate) fn dispatch_worker_error_event_with_kind<'s>(
scope: &mut v8::PinScope<'s, '_>,
worker: v8::Local<'s, v8::Object>,
@@ -287,15 +240,14 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>(
event_kind: WorkerParentErrorEventKind,
) -> bool {
let event = match event_kind {
WorkerParentErrorEventKind::Event => {
let event = new_event(scope, "error", true);
set_error_event_details(scope, event, message, filename, lineno, colno, error);
event
}
WorkerParentErrorEventKind::ErrorEvent => {
new_error_event(scope, message, filename, lineno, colno, error)
}
// Fetch/parse failures fire a plain Event with the default flags.
WorkerParentErrorEventKind::Event => construct_original_event(scope, "error"),
WorkerParentErrorEventKind::ErrorEvent => Some(new_error_event(
scope, message, filename, lineno, colno, error,
)),
};
let Some(event) = event else { return false };
crate::context_bootstrap::mark_event_trusted(scope, event);
set_event_dispatch_fields(scope, worker, event);
let listeners =
@@ -311,7 +263,8 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>(
&[event.into()],
event,
);
if listener.handler_slot.as_deref() == Some(WORKER_ONERROR_SLOT)
if event_kind == WorkerParentErrorEventKind::ErrorEvent
&& listener.handler_slot.as_deref() == Some(WORKER_ONERROR_SLOT)
&& let Some(returned) = callback_result
&& v8::Local::new(scope, &returned).boolean_value(scope)
{
@@ -351,35 +304,6 @@ pub(crate) fn dispatch_worker_error_event_with_kind<'s>(
!default_prevented
}
fn new_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event_type: &str,
cancelable: bool,
) -> v8::Local<'s, v8::Object> {
let global = scope.get_current_context().global(scope);
if let Some(event_ctor) = global
.get(scope, v8str(scope, "Event").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
{
let init = WorkerHostEventInitDeclaration::new(cancelable)
.bind(scope)
.expect("worker host Event init declaration should bind");
if let Some(event) = event_ctor.new_instance(
scope,
&[
v8::String::new(scope, event_type).unwrap().into(),
init.into(),
],
) {
return event;
}
}
WorkerHostEventFallbackDeclaration::new(event_type.to_owned(), cancelable)
.bind(scope)
.expect("worker host Event fallback declaration should bind")
}
fn new_message_event<'s>(
scope: &mut v8::PinScope<'s, '_>,
event_type: &str,
@@ -441,17 +365,3 @@ fn new_error_event<'s>(
.bind(scope)
.expect("worker host ErrorEvent fallback declaration should bind")
}
fn set_error_event_details<'s>(
scope: &mut v8::PinScope<'s, '_>,
event: v8::Local<'s, v8::Object>,
message: &str,
filename: &str,
lineno: u32,
colno: u32,
error: v8::Local<'s, v8::Value>,
) {
WorkerHostErrorEventDetailsDeclaration::new(message, filename, lineno, colno, error)
.initialize(scope, event)
.expect("worker host ErrorEvent details declaration should initialize");
}
@@ -35,9 +35,8 @@ pub(in crate::context_bootstrap) use constructor::{
pub(super) use constructor::worker_constructor_callback;
pub(crate) use dispatch::{
dispatch_worker_error_event_with_error, dispatch_worker_error_event_with_kind,
dispatch_worker_event, flush_pending_worker_messages_for_listener,
worker_has_message_delivery_listener,
dispatch_worker_error_event_with_kind, dispatch_worker_event,
flush_pending_worker_messages_for_listener, worker_has_message_delivery_listener,
};
pub(super) use methods::{worker_post_message_callback, worker_terminate_callback};
@@ -266,7 +266,7 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => {
page_vm
.vm_mut()
.eval("__dedicatedWorkerErrorPhases.join('|')")?,
"worker:Event:bootstrap syntax:true",
"worker:Event:undefined:false",
"initial script errors must stop after the Worker error event"
);
@@ -296,7 +296,7 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => {
page_vm
.vm_mut()
.eval("__dedicatedWorkerErrorPhases.join('|')")?,
"worker:Event:bootstrap syntax:true|worker:ErrorEvent:runtime boom:true|window:ErrorEvent:runtime boom",
"worker:Event:undefined:false|worker:ErrorEvent:runtime boom:true|window:ErrorEvent:runtime boom",
"uncanceled runtime errors must retain their owning Window propagation"
);
@@ -306,6 +306,87 @@ __dedicatedWorkerErrorPhaseWorker.onerror = event => {
.expect("DedicatedWorker bootstrap/runtime error propagation test should run");
}
#[tokio::test(flavor = "current_thread")]
async fn dedicated_worker_bootstrap_error_uses_intrinsic_event_without_author_hooks() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let (mut page_vm, _resource_source, _owner_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(
&loader,
Url::parse("https://worker-bootstrap-event.test/").unwrap(),
);
page_vm.vm_mut().eval(
r#"
globalThis.__bootstrapEventWorker = new Worker("data:text/javascript,onmessage = () => {}");
(() => {
const originalEvent = Event;
const originalEventDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Event');
const flags = ['bubbles', 'cancelable', 'composed'];
const originalFlags = flags.map(name => Object.getOwnPropertyDescriptor(Object.prototype, name));
let reads = 0;
const poison = {configurable: true, get() { ++reads; throw new Error('author event hook'); }};
globalThis.__bootstrapEventFailures = 'pending';
__bootstrapEventWorker.onerror = function(event) {
const failures = [];
try {
if (Object.getPrototypeOf(event) !== originalEvent.prototype) failures.push('wrong Event prototype');
if (event.type !== 'error' || event.isTrusted !== true) failures.push('wrong type or trust');
if (event.target !== __bootstrapEventWorker || event.currentTarget !== this || this !== __bootstrapEventWorker) failures.push('wrong dispatch target');
if (arguments.length !== 1) failures.push('wrong callback arguments');
for (const name of flags) if (event[name] !== false) failures.push(name);
for (const name of ['message', 'filename', 'lineno', 'colno', 'error']) {
if (name in event) failures.push('unexpected ' + name);
}
if (event.defaultPrevented) failures.push('initially canceled');
event.preventDefault();
if (event.defaultPrevented) failures.push('cancelable bootstrap event');
if (reads !== 0) failures.push('called author event hooks');
globalThis.__bootstrapEventFailures = JSON.stringify(failures);
} finally {
Object.defineProperty(globalThis, 'Event', originalEventDescriptor);
flags.forEach((name, index) => {
if (originalFlags[index]) Object.defineProperty(Object.prototype, name, originalFlags[index]);
else delete Object.prototype[name];
});
}
};
Object.defineProperty(globalThis, 'Event', poison);
for (const name of flags) Object.defineProperty(Object.prototype, name, poison);
})()
"#,
)?;
let (_worker_id, producer) = page_vm
.vm()
.only_dedicated_worker_client_event_producer_for_test()?;
producer
.send(RendererDedicatedWorkerClientEvent::Message(
RendererDedicatedWorkerMessageEvent::Error {
message: "bootstrap parse error".to_owned(),
filename: "https://worker-bootstrap-event.test/broken.js".to_owned(),
lineno: 3,
colno: 5,
event_kind: WorkerParentErrorEventKind::Event,
phase: WorkerErrorPhase::Bootstrap,
source: WorkerErrorSource::InitialScriptEvaluation,
},
))
.expect("bootstrap error should enter the typed Worker source");
assert!(
page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::DedicatedWorkerClientEvent,
&loader,
)
.await?
);
assert_eq!(page_vm.vm_mut().eval("__bootstrapEventFailures")?, "[]");
Ok::<_, anyhow::Error>(())
})
.await
.expect("Worker bootstrap errors should use the intrinsic Event");
}
#[tokio::test(flavor = "current_thread")]
async fn dedicated_worker_relay_terminal_waits_for_both_selected_source_fifos() {
run_page_vm_async_test(async move {
@@ -6337,7 +6337,7 @@ async fn worker_url_fetch_failure_dispatches_error_event() {
let mut page_vm = test_page_vm_with_document_url(document_url);
let local_executor = page_vm.local_executor.clone();
let error_message = local_executor
let error_event = local_executor
.run(async move {
page_vm.vm_mut().eval(
r#"
@@ -6346,7 +6346,13 @@ async fn worker_url_fetch_failure_dispatches_error_event() {
globalThis.__workerDone = false;
const worker = new Worker("/missing-worker.js");
worker.onerror = (event) => {
globalThis.__workerError = event.message;
globalThis.__workerError = [
event.type,
Object.getPrototypeOf(event) === Event.prototype,
event.target === worker,
event.bubbles, event.cancelable, event.composed, event.isTrusted,
['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event)
];
globalThis.__workerDone = true;
};
})()
@@ -6358,7 +6364,7 @@ async fn worker_url_fetch_failure_dispatches_error_event() {
"worker url load failure should dispatch an error event",
)
.await?;
page_vm.vm_mut().eval("String(globalThis.__workerError)")
page_vm.vm_mut().eval("JSON.stringify(globalThis.__workerError)")
})
.await
.expect("worker url failure test should run on owner lane");
@@ -6366,11 +6372,9 @@ async fn worker_url_fetch_failure_dispatches_error_event() {
server
.await
.expect("worker script failure server should finish");
assert!(
error_message.contains("HTTP request")
&& error_message.contains("404")
&& error_message.contains("/missing-worker.js"),
"unexpected worker load error: {error_message}"
assert_eq!(
error_event,
r#"["error",true,true,false,false,false,true,false]"#
);
})
.await;
@@ -170,6 +170,95 @@ async fn shared_worker_error_body_leaves_reactions_for_selected_completion() {
.expect("SharedWorker current-owner event should run through its typed executor");
}
#[tokio::test(flavor = "current_thread")]
async fn shared_worker_bootstrap_error_uses_intrinsic_event_without_author_hooks() {
run_page_vm_async_test(async move {
for (setup, remaining_clients) in [
(r#"
globalThis.__bootstrapEventWorker = new SharedWorker(
"data:text/javascript," + encodeURIComponent("function ("),
"intrinsic-bootstrap-event"
);
"#, 0),
(r#"
const url = "data:text/javascript,onconnect = () => {}";
globalThis.__runningSharedWorker = new SharedWorker(url, "intrinsic-connection-event");
globalThis.__bootstrapEventWorker = new SharedWorker(url, {
name: "intrinsic-connection-event", type: "module"
});
"#, 1),
] {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let (mut page_vm, _resource_source, mut page_wake_rx) =
page_vm_with_bound_task_sources_and_owner_wake(
&loader,
Url::parse("https://shared-worker-bootstrap-event.test/").unwrap(),
);
let mut shared_worker_wake_rx = install_shared_worker_service_wake(&page_vm);
page_vm.vm_mut().eval(setup)?;
page_vm.vm_mut().eval(
r#"
(() => {
const originalEvent = Event;
const originalEventDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'Event');
const flags = ['bubbles', 'cancelable', 'composed'];
const originalFlags = flags.map(name => Object.getOwnPropertyDescriptor(Object.prototype, name));
let reads = 0;
const poison = {configurable: true, get() { ++reads; throw new Error('author event hook'); }};
globalThis.__bootstrapEventFailures = 'pending';
__bootstrapEventWorker.onerror = function(event) {
const failures = [];
try {
if (Object.getPrototypeOf(event) !== originalEvent.prototype) failures.push('wrong Event prototype');
if (event.type !== 'error' || event.isTrusted !== true) failures.push('wrong type or trust');
if (event.target !== __bootstrapEventWorker || event.currentTarget !== this || this !== __bootstrapEventWorker) failures.push('wrong dispatch target');
if (arguments.length !== 1) failures.push('wrong callback arguments');
for (const name of flags) if (event[name] !== false) failures.push(name);
for (const name of ['message', 'filename', 'lineno', 'colno', 'error']) {
if (name in event) failures.push('unexpected ' + name);
}
if (event.defaultPrevented) failures.push('initially canceled');
event.preventDefault();
if (event.defaultPrevented) failures.push('cancelable bootstrap event');
if (reads !== 0) failures.push('called author event hooks');
globalThis.__bootstrapEventFailures = JSON.stringify(failures);
} finally {
Object.defineProperty(globalThis, 'Event', originalEventDescriptor);
flags.forEach((name, index) => {
if (originalFlags[index]) Object.defineProperty(Object.prototype, name, originalFlags[index]);
else delete Object.prototype[name];
});
}
};
Object.defineProperty(globalThis, 'Event', poison);
for (const name of flags) Object.defineProperty(Object.prototype, name, poison);
})()
"#,
)?;
wait_for_shared_worker_client_event(
&mut page_vm,
&mut shared_worker_wake_rx,
&mut page_wake_rx,
)
.await?;
assert!(
page_vm
.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::SharedWorkerClientEvent,
&loader,
)
.await?
);
assert_eq!(page_vm.vm_mut().eval("__bootstrapEventFailures")?, "[]");
assert_eq!(page_vm.vm().shared_worker_client_count_for_test(), remaining_clients);
}
Ok::<_, anyhow::Error>(())
})
.await
.expect("SharedWorker bootstrap errors should use the intrinsic Event");
}
#[tokio::test(flavor = "current_thread")]
async fn shared_worker_nonterminal_error_runs_through_selected_completion_and_retains_endpoint() {
run_page_vm_async_test(async move {
@@ -2914,13 +2914,9 @@ async fn worker_pending_activity_diagnostics_split_loading_and_running_worker_is
#[tokio::test]
async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports() {
run_page_vm_async_test(async move {
let (dependency_base_url, dependency_server) = spawn_path_response_http_server(vec![(
"/dependency.js",
"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *",
"export const value = 'unexpected';".to_owned(),
Duration::ZERO,
)])
.await;
let (dependency_base_url, mut dependency_request, dependency_server) =
spawn_shared_worker_script_capture_http_server("export const value = 'unexpected';")
.await;
let dependency_url = format!("{dependency_base_url}/dependency.js");
let worker_source = format!(
r#"
@@ -2962,7 +2958,13 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports
};
worker.onerror = event => {
event.preventDefault();
globalThis.__moduleWorkerCspResult = "error:" + event.message;
globalThis.__moduleWorkerCspResult = JSON.stringify([
event.type,
Object.getPrototypeOf(event) === Event.prototype,
event.bubbles, event.cancelable, event.composed, event.isTrusted,
event.defaultPrevented,
['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event)
]);
globalThis.__moduleWorkerCspDone = true;
};
})()
@@ -2977,9 +2979,10 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports
let result = page_vm
.vm_mut()
.eval("globalThis.__moduleWorkerCspResult")?;
assert!(
result.starts_with("error:") && result.contains("Content Security Policy"),
"static module import should be blocked by creator worker-src: {result:?}"
assert_eq!(
result,
r#"["error",true,false,false,false,true,false,false]"#,
"blocked static import should fire a bootstrap Event before evaluation"
);
anyhow::Ok(())
})
@@ -2988,6 +2991,10 @@ async fn external_dedicated_module_worker_retains_creator_csp_for_static_imports
server
.await
.expect("external module worker CSP server should finish");
assert!(
matches!(dependency_request.try_recv(), Err(tokio::sync::oneshot::error::TryRecvError::Empty)),
"creator CSP must block the dependency before any HTTP request is sent"
);
dependency_server.abort();
})
.await;
@@ -3935,7 +3942,6 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() {
)])
.await;
let document_url = Url::parse(&format!("{base_url}/page.html")).expect("document url");
let missing_worker_url = format!("{base_url}/does-not-exist.js");
let mut page_vm = test_page_vm_with_document_url(document_url);
let local_executor = page_vm.local_executor.clone();
@@ -3952,7 +3958,17 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() {
});
const worker = new Worker("/does-not-exist.js");
worker.onerror = event => {
globalThis.__missingWorkerEvents.push("worker:" + event.message);
event.preventDefault();
globalThis.__missingWorkerEvents.push({
target: "worker",
type: event.type,
constructor: event.constructor.name,
trusted: event.isTrusted,
cancelable: event.cancelable,
defaultPrevented: event.defaultPrevented,
hasErrorDetails: ["message", "filename", "lineno", "colno", "error"]
.some(name => name in event)
});
globalThis.__missingWorkerDone = true;
};
})()
@@ -3973,9 +3989,7 @@ async fn worker_script_load_failure_does_not_dispatch_window_error() {
page_vm
.vm_mut()
.eval("JSON.stringify(globalThis.__missingWorkerEvents)")?,
format!(
r#"["worker:HTTP request `{missing_worker_url}` returned 404 Not Found"]"#
)
r#"[{"target":"worker","type":"error","constructor":"Event","trusted":true,"cancelable":false,"defaultPrevented":false,"hasErrorDetails":false}]"#
);
anyhow::Ok(())
})
@@ -4006,7 +4020,10 @@ async fn shared_worker_rejects_cross_origin_redirected_script() {
globalThis.__sharedWorkerRedirectDone = false;
const worker = new SharedWorker("/redirect-source.js", "cross-origin-redirect-script");
worker.onerror = (event) => {
globalThis.__sharedWorkerRedirectOutcome = "error:" + event.message;
globalThis.__sharedWorkerRedirectOutcome = JSON.stringify([
event.type, event.constructor.name, event.cancelable,
"message" in event, event.isTrusted
]);
globalThis.__sharedWorkerRedirectDone = true;
};
worker.port.onmessage = (event) => {
@@ -4024,9 +4041,10 @@ async fn shared_worker_rejects_cross_origin_redirected_script() {
)
.await?;
let outcome = page_vm.vm_mut().eval("globalThis.__sharedWorkerRedirectOutcome")?;
assert!(
outcome.starts_with("error:"),
"redirected cross-origin script must not execute, got {outcome:?}"
assert_eq!(
outcome,
r#"["error","Event",false,false,true]"#,
"redirected cross-origin script must fail with a plain Event"
);
anyhow::Ok(())
})
@@ -4933,7 +4951,7 @@ async fn shared_worker_terminal_error_forgets_page_client_wrapper_tracking() {
page_vm
.vm_mut()
.eval("JSON.stringify(globalThis.__sharedWorkerTerminalErrorRecord)")?,
r#"{"type":"error","cancelable":true,"hasMessage":true}"#
r#"{"type":"error","cancelable":false,"hasMessage":false}"#
);
wait_for_shared_worker_client_count(
&mut page_vm,
+6 -3
View File
@@ -5095,8 +5095,9 @@ async fn dedicated_worker_script_load_failure_does_not_dispatch_window_error() {
globalThis.__lm_worker_script_error_events.push([
"worker",
event.type,
String(event.message).includes("HTTP request"),
String(event.filename).endsWith("/does-not-exist.js")
Object.getPrototypeOf(event) === Event.prototype,
event.bubbles, event.cancelable, event.composed, event.isTrusted,
['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event)
].join(":"));
};
return "installed";
@@ -5129,7 +5130,9 @@ async fn dedicated_worker_script_load_failure_does_not_dispatch_window_error() {
.expect("worker script load failure events should evaluate");
assert_eq!(
renderer_json_value(events),
Some(serde_json::json!("[\"worker:error:true:true\"]")),
Some(serde_json::json!(
"[\"worker:error:true:false:false:false:true:false\"]"
)),
"worker script load failure must not bubble to window.onerror"
);
@@ -1,7 +1,7 @@
//! DedicatedWorker error-event dispatch and propagation.
//!
//! This is deliberately separate from ordinary Worker message dispatch.
//! `dispatch_worker_error_event_with_*` retains the established inner
//! `dispatch_worker_error_event_with_kind` retains the established inner
//! checkpoint used to settle listener cancellation before deciding whether an
//! uncanceled Worker error propagates to the owning Window. That semantic
//! checkpoint is not the HTML task-end checkpoint: the selected Page-task
@@ -16,7 +16,7 @@ pub(super) fn dispatch_script_load_failure<'s>(
script_url: &str,
) {
let worker_error = v8::null(scope).into();
crate::context_bootstrap::dispatch_worker_error_event_with_error(
crate::context_bootstrap::dispatch_worker_error_event_with_kind(
scope,
worker,
error_message,
@@ -24,6 +24,7 @@ pub(super) fn dispatch_script_load_failure<'s>(
0,
0,
worker_error,
crate::worker::WorkerParentErrorEventKind::Event,
);
}
@@ -67,7 +67,7 @@ fn connect_with_runtime_service(
client.send_error(
"Failed to connect SharedWorker: loading host is unavailable.",
params.key.script_url(),
WorkerParentErrorEventKind::ErrorEvent,
WorkerParentErrorEventKind::Event,
);
client.close_ports();
runtime_service.remove_client(client_id);
@@ -88,7 +88,7 @@ fn connect_with_runtime_service(
client.send_error(
shared_worker_compatibility_error_message(&error),
params.key.script_url(),
WorkerParentErrorEventKind::ErrorEvent,
WorkerParentErrorEventKind::Event,
);
client.close_ports();
client_id
@@ -140,7 +140,7 @@ impl RendererSharedWorkerHost {
client_id,
"Failed to connect SharedWorker: worker runtime is unavailable.",
failure_filename,
WorkerParentErrorEventKind::ErrorEvent,
WorkerParentErrorEventKind::Event,
);
false
}
@@ -156,7 +156,7 @@ impl RendererSharedWorkerHost {
client_id,
"Failed to connect SharedWorker: worker runtime is unavailable.",
"",
WorkerParentErrorEventKind::ErrorEvent,
WorkerParentErrorEventKind::Event,
);
failed.push(client_id);
}
@@ -92,7 +92,7 @@ fn finish_loading_with_runtime_service(
clients,
message,
params.key.script_url(),
WorkerParentErrorEventKind::ErrorEvent,
WorkerParentErrorEventKind::Event,
);
}
}
@@ -335,6 +335,7 @@ mod tests {
SharedWorkerClientEvent::Error(error)
if error.endpoint_disposition()
== SharedWorkerClientEndpointDisposition::Retire
&& error.event_kind() == WorkerParentErrorEventKind::Event
));
assert!(
message_port_owner
@@ -665,6 +666,7 @@ mod tests {
SharedWorkerClientEvent::Error(error)
if error.endpoint_disposition()
== SharedWorkerClientEndpointDisposition::Retire
&& error.event_kind() == WorkerParentErrorEventKind::Event
));
assert!(
message_port_owner
@@ -2957,12 +2957,16 @@ pub(super) fn dispatch_nested_worker_event(
lineno,
colno,
event_kind,
phase,
..
} => {
let unhandled = crate::context_bootstrap::dispatch_worker_event(scope, worker, message);
// Bootstrap failures only fire an Event at the child Worker. Only
// uncanceled runtime errors propagate to its owner's global scope.
let propagate = unhandled && *phase == super::handle::WorkerErrorPhase::Runtime;
NestedWorkerDispatchResult {
dispatched: true,
unhandled_error: unhandled.then(|| NestedWorkerUnhandledError {
unhandled_error: propagate.then(|| NestedWorkerUnhandledError {
message: error_message.clone(),
filename: filename.clone(),
lineno: *lineno,
@@ -9335,17 +9335,25 @@ async fn nested_worker_script_load_failure_is_async_error_event() {
let mut handle = spawn_worker(
r#"
let result = "not-run";
let globalErrors = 0;
onerror = () => { ++globalErrors; return true; };
try {
const child = new Worker("missing-child.js");
child.onerror = event => {
event.preventDefault();
postMessage({
const observation = {
constructed: result === "constructed",
type: event.type,
messageIsNonEmpty: event.message.length > 0,
filename: event.filename
});
close();
intrinsicEvent: Object.getPrototypeOf(event) === Event.prototype,
target: event.target === child,
trusted: event.isTrusted,
flags: [event.bubbles, event.cancelable, event.composed, event.defaultPrevented],
hasErrorDetails: ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event)
};
setTimeout(() => {
postMessage({ ...observation, globalErrors });
close();
}, 0);
};
result = "constructed";
} catch (error) {
@@ -9363,9 +9371,7 @@ async fn nested_worker_script_load_failure_is_async_error_event() {
.expect("channel closed");
assert_eq!(
expect_post_json(msg),
format!(
r#"{{"constructed":true,"type":"error","messageIsNonEmpty":true,"filename":"{base_url}/missing-child.js"}}"#
)
r#"{"constructed":true,"type":"error","intrinsicEvent":true,"target":true,"trusted":true,"flags":[false,false,false,false],"hasErrorDetails":false,"globalErrors":0}"#
);
timeout(TIMEOUT, server)
.await
@@ -9373,6 +9379,48 @@ async fn nested_worker_script_load_failure_is_async_error_event() {
.expect("nested worker script server should finish");
}
#[tokio::test]
async fn nested_worker_parse_errors_use_intrinsic_events_without_global_propagation() {
ensure_v8();
for child_type in ["classic", "module"] {
let mut handle = spawn_worker(
r#"
const originalEvent = Event;
let globalErrors = 0;
let authorReads = 0;
onerror = () => { ++globalErrors; return true; };
const child = new Worker("data:text/javascript,function%20(", { type: "CHILD_TYPE" });
child.onerror = event => {
event.preventDefault();
const observation = {
type: event.type,
intrinsicEvent: Object.getPrototypeOf(event) === originalEvent.prototype,
target: event.target === child,
trusted: event.isTrusted,
flags: [event.bubbles, event.cancelable, event.composed, event.defaultPrevented],
hasErrorDetails: ['message', 'filename', 'lineno', 'colno', 'error'].some(name => name in event)
};
setTimeout(() => {
postMessage({ ...observation, globalErrors, authorReads });
close();
}, 0);
};
Object.defineProperty(globalThis, "Event", {
configurable: true,
get() { ++authorReads; throw new Error("author Event getter"); }
});
"#
.replace("CHILD_TYPE", child_type),
"test://nested_worker_parse_error".into(),
);
assert_eq!(
recv_post_json(&mut handle).await,
r#"{"type":"error","intrinsicEvent":true,"target":true,"trusted":true,"flags":[false,false,false,false],"hasErrorDetails":false,"globalErrors":0,"authorReads":0}"#,
"{child_type} child bootstrap must not invoke author hooks or parent onerror"
);
}
}
#[tokio::test]
async fn worker_onmessage_exception_routes_through_worker_global_onerror() {
ensure_v8();
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function shared_worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -295,8 +296,7 @@ promise_test(async function () {
new SharedWorker(url, { name: "type-check", type: "classic" });
const worker = new SharedWorker(url, { name: "type-check", type: "module" });
const event = await next_worker_error(worker);
assert_equals(event.type, "error");
assert_true(String(event.message).includes("script type"));
assert_worker_bootstrap_error(event, worker);
}, "SharedWorker type mismatch for an existing key dispatches error");
promise_test(async function () {
@@ -304,8 +304,7 @@ promise_test(async function () {
new SharedWorker(url, { name: "credentials-check", credentials: "same-origin" });
const worker = new SharedWorker(url, { name: "credentials-check", credentials: "include" });
const event = await next_worker_error(worker);
assert_equals(event.type, "error");
assert_true(String(event.message).includes("credentials"));
assert_worker_bootstrap_error(event, worker);
}, "SharedWorker credentials mismatch for an existing key dispatches error");
promise_test(async function () {
@@ -313,8 +312,7 @@ promise_test(async function () {
let onerrorCalled = false;
worker.onerror = function (event) {
onerrorCalled = true;
assert_equals(event.type, "error");
assert_true(String(event.message).includes("shared worker script"));
assert_worker_bootstrap_error(event, worker);
};
await next_worker_error(worker);
assert_true(onerrorCalled, "onerror handler is invoked for script load failure");
@@ -326,8 +324,7 @@ promise_test(async function () {
"invalid-data-url-script",
);
const event = await next_worker_error(worker);
assert_equals(event.type, "error");
assert_true(String(event.message).includes("data URL"));
assert_worker_bootstrap_error(event, worker);
}, "SharedWorker invalid data URL script dispatches an async error event");
test(function () {
@@ -0,0 +1,16 @@
function assert_worker_bootstrap_error(event, worker) {
assert_equals(Object.getPrototypeOf(event), Event.prototype,
"bootstrap failure uses Event, not ErrorEvent");
assert_equals(event.type, "error");
assert_equals(event.target, worker);
assert_true(event.isTrusted);
assert_false(event.bubbles);
assert_false(event.cancelable);
assert_false(event.composed);
assert_false(event.defaultPrevented);
event.preventDefault();
assert_false(event.defaultPrevented, "bootstrap errors cannot be canceled");
for (const name of ["message", "filename", "lineno", "colno", "error"]) {
assert_false(name in event, "bootstrap Event has no ErrorEvent." + name);
}
}
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function wait(ms) {
return new Promise(function (resolve) {
@@ -39,19 +40,7 @@ promise_test(async function () {
"worker load failure onerror",
);
assert_equals(event.type, "error", "load failures should dispatch error events");
assert_true(
event.message.indexOf("HTTP request") !== -1,
"load failure should mention the HTTP request",
);
assert_true(
event.message.indexOf("404") !== -1,
"load failure should surface the HTTP status",
);
assert_true(
event.message.indexOf("/wpt/ported/worker/resources/missing-worker.js") !== -1,
"load failure should mention the missing worker URL",
);
assert_worker_bootstrap_error(event, worker);
} finally {
window.onerror = previousOnerror;
worker.terminate();
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -23,7 +24,7 @@ function wait_for(promise, description) {
]);
}
async function assert_module_parse_failure(source, expectedFragments, description) {
async function assert_module_parse_failure(source, description) {
const previousOnerror = window.onerror;
let messageSeen = false;
const worker = new Worker(worker_url(source), { type: "module" });
@@ -46,11 +47,7 @@ async function assert_module_parse_failure(source, expectedFragments, descriptio
"module worker " + description + " early error",
);
assert_equals(event.type, "error", "module parse failure should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.some(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), "parse failure should expose a syntax error message");
assert_worker_bootstrap_error(event, worker);
assert_false(messageSeen, "module evaluation should not continue after the early error");
} finally {
window.onerror = previousOnerror;
@@ -63,14 +60,14 @@ promise_test(async function () {
"return;",
"postMessage('unexpected');",
].join("\n");
await assert_module_parse_failure(topLevelSource, ["return", "SyntaxError"], "top-level return");
await assert_module_parse_failure(topLevelSource, "top-level return");
const withSource = [
"with ({ value: 1 }) {",
" postMessage(value);",
"}",
].join("\n");
await assert_module_parse_failure(withSource, ["with", "strict", "SyntaxError"], "with statement");
await assert_module_parse_failure(withSource, "with statement");
const deleteSource = [
"var target = 1;",
@@ -79,7 +76,6 @@ promise_test(async function () {
].join("\n");
await assert_module_parse_failure(
deleteSource,
["delete", "strict", "SyntaxError"],
"delete identifier",
);
@@ -94,7 +90,6 @@ promise_test(async function () {
].join("\n");
await assert_module_parse_failure(
dependencySource,
["with", "strict", "SyntaxError"],
"dependency with statement",
);
}, "Dedicated module workers reject parse-time early errors before evaluation");
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -39,11 +40,16 @@ async function assert_json_module_failure(source, expectedFragments, description
}),
]);
assert_equals(event.type, "error", "JSON module attribute failure should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.every(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), description + " should expose the JSON import-attributes reason; got: " + event.message);
if (expectedFragments === null) {
assert_worker_bootstrap_error(event, worker);
} else {
// JSON decoding remains an evaluation error on the current loader.
assert_equals(event.type, "error", "JSON module attribute failure should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.every(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), description + " should expose the JSON import-attributes reason; got: " + event.message);
}
assert_false(messageSeen, "module evaluation should not continue after JSON import-attributes failure");
} finally {
window.onerror = previousOnerror;
@@ -61,7 +67,7 @@ promise_test(async function () {
"import config from " + JSON.stringify(jsonUrl) + ";",
"postMessage(config);",
].join("\n"),
["syntax"],
null,
"static JSON import without type",
);
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -17,7 +18,7 @@ function next_message(worker) {
});
}
async function assert_worker_error(source, expectedFragments, description) {
async function assert_worker_error(source, description) {
const previousOnerror = window.onerror;
let messageSeen = false;
const worker = new Worker(worker_url(source), { type: "module" });
@@ -37,11 +38,7 @@ async function assert_worker_error(source, expectedFragments, description) {
};
});
assert_equals(event.type, "error", description + " should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.every(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), description + " should expose the expected error reason");
assert_worker_bootstrap_error(event, worker);
assert_false(messageSeen, description + " should stop module evaluation");
} finally {
window.onerror = previousOnerror;
@@ -107,7 +104,6 @@ promise_test(async function () {
"import config from " + JSON.stringify(jsonUrl) + ";",
"postMessage(config);",
].join("\n"),
["json"],
"HTTP JSON MIME without type=json",
);
@@ -116,7 +112,6 @@ promise_test(async function () {
"import value from " + JSON.stringify(javascriptJsonPathUrl) + " with { type: 'json' };",
"postMessage(value);",
].join("\n"),
["json"],
"HTTP JavaScript MIME with type=json",
);
@@ -125,7 +120,6 @@ promise_test(async function () {
"import value from " + JSON.stringify(plainJsonUrl) + " with { type: 'json' };",
"postMessage(value);",
].join("\n"),
["json"],
"HTTP text/plain JSON-like body with type=json",
);
}, "Dedicated module workers reject HTTP JSON import-attribute mismatches by Content-Type");
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -23,7 +24,7 @@ function wait_for(promise, description) {
]);
}
async function assert_module_link_failure(source, expectedFragments, description) {
async function assert_module_link_failure(source, description) {
const previousOnerror = window.onerror;
let messageSeen = false;
const worker = new Worker(worker_url(source), { type: "module" });
@@ -46,11 +47,7 @@ async function assert_module_link_failure(source, expectedFragments, description
"module worker " + description + " link error",
);
assert_equals(event.type, "error", "module link failure should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.some(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), "link failure should expose a syntax/link error message");
assert_worker_bootstrap_error(event, worker);
assert_false(messageSeen, "module evaluation should not continue after the link failure");
} finally {
window.onerror = previousOnerror;
@@ -67,7 +64,6 @@ promise_test(async function () {
].join("\n");
await assert_module_link_failure(
missingNamedSource,
["missing export", "syntaxerror"],
"missing named import",
);
@@ -77,7 +73,6 @@ promise_test(async function () {
].join("\n");
await assert_module_link_failure(
missingDefaultSource,
["missing export", "syntaxerror"],
"missing default import",
);
@@ -90,7 +85,6 @@ promise_test(async function () {
].join("\n");
await assert_module_link_failure(
missingReexportSource,
["missing export", "syntaxerror"],
"missing re-export",
);
}, "Dedicated module workers reject missing static import exports before evaluation");
@@ -3,6 +3,7 @@
<script src="/wpt/resources/testharness.js"></script>
<script src="/wpt/resources/testharnessreport.js"></script>
<script src="/wpt/resources/moli-wpt-adapter.js"></script>
<script src="/wpt/ported/worker/resources/assert-bootstrap-error.js"></script>
<script>
function worker_url(source) {
return "data:text/javascript," + encodeURIComponent(source);
@@ -23,7 +24,7 @@ function wait_for(promise, description) {
]);
}
async function assert_module_graph_failure(workerUrl, expectedFragments, description) {
async function assert_module_graph_failure(workerUrl, description) {
const previousOnerror = window.onerror;
let messageSeen = false;
const worker = new Worker(workerUrl, { type: "module" });
@@ -46,11 +47,7 @@ async function assert_module_graph_failure(workerUrl, expectedFragments, descrip
"module worker " + description + " graph failure",
);
assert_equals(event.type, "error", "module graph failure should dispatch an error event");
const lowerMessage = String(event.message).toLowerCase();
assert_true(expectedFragments.some(function (fragment) {
return lowerMessage.indexOf(fragment.toLowerCase()) !== -1;
}), "graph failure should expose the dependency load or resolution reason");
assert_worker_bootstrap_error(event, worker);
assert_false(messageSeen, "module evaluation should not continue after the graph failure");
} finally {
window.onerror = previousOnerror;
@@ -65,7 +62,6 @@ promise_test(async function () {
).href;
await assert_module_graph_failure(
missingHttpMainUrl,
["404", "not found"],
"HTTP dependency 404",
);
@@ -75,7 +71,6 @@ promise_test(async function () {
].join("\n");
await assert_module_graph_failure(
worker_url(unsupportedSchemeSource),
["unsupported scheme", "ftp"],
"unsupported dependency scheme",
);
@@ -85,7 +80,6 @@ promise_test(async function () {
].join("\n");
await assert_module_graph_failure(
worker_url(invalidSpecifierSource),
["failed to resolve", "invalid"],
"invalid dependency specifier",
);
}, "Dedicated module workers report dependency load and resolution failures before evaluation");