mirror of
https://github.com/lexmount/moli.git
synced 2026-10-08 16:01:05 +00:00
fix(script): queue dynamic preparation errors at insertion
This commit is contained in:
@@ -2973,7 +2973,6 @@ html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job
|
||||
html/webappapis/scripting/processing-model-2/integration-with-the-javascript-job-queue/promise-job-incumbent.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-event-during-parse.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-events.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-resolution-order.html
|
||||
inert/inert-and-find-flat-tree.html
|
||||
inert/inert-and-find.html
|
||||
inert/inert-on-non-html.html
|
||||
|
||||
@@ -7654,6 +7654,7 @@ html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promis
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-events-onerror.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-events.dedicatedworker.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-rejection-events.sharedworker.html
|
||||
html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/promise-resolution-order.html
|
||||
html/webappapis/scripting/processing-model-2/window-onerror-parse-error.html
|
||||
html/webappapis/scripting/processing-model-2/window-onerror-runtime-error-throw.html
|
||||
html/webappapis/scripting/processing-model-2/window-onerror-runtime-error.html
|
||||
|
||||
@@ -188,14 +188,17 @@ impl DomHost {
|
||||
let source_attribute = self.node(handle).and_then(Node::as_element).is_some_and(
|
||||
|element| match element.namespace() {
|
||||
"http://www.w3.org/1999/xhtml" => name.eq_ignore_ascii_case("src"),
|
||||
"http://www.w3.org/2000/svg" => name.eq_ignore_ascii_case("href"),
|
||||
"http://www.w3.org/2000/svg" => {
|
||||
name.eq_ignore_ascii_case("href")
|
||||
&& prior_value.as_deref().is_none_or(str::is_empty)
|
||||
&& !value.is_empty()
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
if source_attribute
|
||||
&& prior_value.as_deref().is_none_or(str::is_empty)
|
||||
&& !value.is_empty()
|
||||
{
|
||||
// Every HTML src assignment invokes preparation, including
|
||||
// an empty value. The loader owns the already-started guard.
|
||||
if source_attribute {
|
||||
effects.mark_script_prepare_trigger(
|
||||
handle,
|
||||
ScriptPrepareTriggerKind::SourceAttributeAdded,
|
||||
|
||||
@@ -16,11 +16,13 @@ use crate::page_task_queue::{
|
||||
PageTaskSender, PostParseLifecycleWork, RendererPageMainDocumentRuntimeProducer,
|
||||
WindowScriptFailureReportTask,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::planning::ScriptSource;
|
||||
use crate::{
|
||||
dom::{NodeId, native::NativeNodeId},
|
||||
{
|
||||
module_runtime::ModuleOwnerState,
|
||||
planning::{PreparedScript, ScriptSource},
|
||||
planning::PreparedScript,
|
||||
types::{ScriptErrorValue, ScriptKind, ScriptMode, ScriptSourceKind},
|
||||
},
|
||||
};
|
||||
@@ -41,6 +43,7 @@ pub(crate) use loader::*;
|
||||
pub(crate) use runtime::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(not(test), derive(Default))]
|
||||
pub(crate) struct HostScriptScheduler {
|
||||
#[cfg(test)]
|
||||
pending_dynamic_in_order_scripts: VecDeque<PreparedScript>,
|
||||
@@ -59,6 +62,7 @@ pub(crate) struct HostScriptScheduler {
|
||||
native_module_owner_event_turn_queued: bool,
|
||||
module_owner: ModuleOwnerState,
|
||||
script_handles: HashMap<String, ScriptHandleState>,
|
||||
#[cfg(test)]
|
||||
next_virtual_script_node_index: usize,
|
||||
next_dynamic_script_position: usize,
|
||||
}
|
||||
@@ -285,6 +289,7 @@ pub(crate) enum QueuedScriptFailureKind {
|
||||
ModuleTopLevelLoad,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl Default for HostScriptScheduler {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -305,6 +310,7 @@ impl Default for HostScriptScheduler {
|
||||
native_module_owner_event_turn_queued: false,
|
||||
module_owner: ModuleOwnerState::default(),
|
||||
script_handles: HashMap::new(),
|
||||
#[cfg(test)]
|
||||
next_virtual_script_node_index: 1_000_000,
|
||||
next_dynamic_script_position: 0,
|
||||
}
|
||||
@@ -1324,6 +1330,7 @@ impl HostScriptScheduler {
|
||||
self.main_document_completion_recheck_turn_queued = false;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn enqueue_script_event_lifecycle_work(&mut self, kind: ScriptEventKind, handle: &str) {
|
||||
if let Some(work) = self.plan_script_event_lifecycle_work(kind, handle) {
|
||||
self.enqueue_post_parse_lifecycle_work(work);
|
||||
@@ -1555,6 +1562,7 @@ impl HostScriptScheduler {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn prepare_failed_dynamic_script(
|
||||
&mut self,
|
||||
preparation: &RuntimeScriptPreparationContext,
|
||||
@@ -1611,6 +1619,7 @@ impl HostScriptScheduler {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn queued_script_failure_kind(
|
||||
kind: ScriptKind,
|
||||
source_kind: ScriptSourceKind,
|
||||
@@ -1639,6 +1648,7 @@ impl HostScriptScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn next_virtual_node_id(&mut self) -> NodeId {
|
||||
let node_id = NodeId::new(self.next_virtual_script_node_index);
|
||||
self.next_virtual_script_node_index += 1;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#[cfg(test)]
|
||||
use super::ScriptEventKind;
|
||||
use super::{
|
||||
HostScriptScheduler, PreparedScriptElementStart, RuntimeScriptPreparationContext,
|
||||
RuntimeScriptStartDecision, ScriptElementLoader, ScriptElementLoaderOptions, ScriptEventKind,
|
||||
ScriptEventTask, ScriptHandleSource, ScriptStartCommitKind,
|
||||
RuntimeScriptStartDecision, ScriptElementLoader, ScriptElementLoaderOptions, ScriptEventTask,
|
||||
ScriptHandleSource, ScriptStartCommitKind,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::types::ScriptSourceKind;
|
||||
@@ -37,14 +39,12 @@ impl RuntimeScriptStartPlan {
|
||||
matches!(
|
||||
self.prepared.decision,
|
||||
RuntimeScriptStartDecision::Queue { .. }
|
||||
| RuntimeScriptStartDecision::QueueFailed { .. }
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn load_delay_kind(&self) -> Option<MainDocumentScriptLoadDelayKind> {
|
||||
let kind = match &self.prepared.decision {
|
||||
RuntimeScriptStartDecision::Queue { kind, .. }
|
||||
| RuntimeScriptStartDecision::QueueFailed { kind, .. } => *kind,
|
||||
RuntimeScriptStartDecision::Queue { kind, .. } => *kind,
|
||||
_ => return None,
|
||||
};
|
||||
Some(if kind == crate::types::ScriptKind::Module {
|
||||
@@ -65,6 +65,9 @@ pub(crate) struct RuntimeScriptStartReservation {
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PreparedRuntimeScriptStartCommit {
|
||||
Noop,
|
||||
PreparationError {
|
||||
node: NativeNodeId,
|
||||
},
|
||||
InlineClassic {
|
||||
node: NativeNodeId,
|
||||
host_script_handle: String,
|
||||
@@ -441,10 +444,7 @@ pub(crate) fn prepare_runtime_script_start_commit(
|
||||
&host_script_handle,
|
||||
ScriptStartCommitKind::RejectImportMap,
|
||||
) {
|
||||
scripts.enqueue_script_event_lifecycle_work(
|
||||
ScriptEventKind::Error,
|
||||
&host_script_handle,
|
||||
);
|
||||
return Ok(PreparedRuntimeScriptStartCommit::PreparationError { node });
|
||||
}
|
||||
Ok(PreparedRuntimeScriptStartCommit::Noop)
|
||||
}
|
||||
@@ -478,38 +478,21 @@ pub(crate) fn prepare_runtime_script_start_commit(
|
||||
payload: Box::new(RuntimeScriptAdmissionPayload::Script(script)),
|
||||
})
|
||||
}
|
||||
RuntimeScriptStartDecision::QueueFailed {
|
||||
source,
|
||||
kind,
|
||||
mode,
|
||||
source_kind,
|
||||
message,
|
||||
} => {
|
||||
let node_id = scripts.next_virtual_node_id();
|
||||
let failed = match scripts.prepare_failed_dynamic_script(
|
||||
&preparation,
|
||||
node_id,
|
||||
RuntimeScriptStartDecision::QueueFailed { .. } => {
|
||||
// URL preparation failed before fetching or load-delay admission.
|
||||
// Commit already-started now; the caller queues the element error
|
||||
// in the DOM-manipulation FIFO at this insertion boundary.
|
||||
if finish_local_runtime_script_start(
|
||||
dom_host,
|
||||
scripts,
|
||||
node,
|
||||
&host_script_handle,
|
||||
&source,
|
||||
source_kind,
|
||||
kind,
|
||||
mode,
|
||||
&message,
|
||||
ScriptStartCommitKind::QueueFailed,
|
||||
) {
|
||||
Ok(failed) => failed,
|
||||
Err(error) => {
|
||||
scripts.cancel_script_start(&host_script_handle, node);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
Ok(PreparedRuntimeScriptStartCommit::Admission {
|
||||
reservation: RuntimeScriptStartReservation {
|
||||
node,
|
||||
host_script_handle,
|
||||
commit_kind: ScriptStartCommitKind::QueueFailed,
|
||||
},
|
||||
payload: Box::new(RuntimeScriptAdmissionPayload::Failed(failed)),
|
||||
})
|
||||
Ok(PreparedRuntimeScriptStartCommit::PreparationError { node })
|
||||
} else {
|
||||
Ok(PreparedRuntimeScriptStartCommit::Noop)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,9 +510,18 @@ impl MutationCoordinator {
|
||||
),
|
||||
}
|
||||
}
|
||||
RuntimeScriptStartDecision::RegisterImportMap { .. }
|
||||
| RuntimeScriptStartDecision::RejectExternalImportMap
|
||||
| RuntimeScriptStartDecision::QueueFailed { .. } => {}
|
||||
RuntimeScriptStartDecision::RejectExternalImportMap
|
||||
| RuntimeScriptStartDecision::QueueFailed { .. } => {
|
||||
let _ = dom_host.set_script_already_started(node, true);
|
||||
if !unsafe { &mut *host_ptr }.queue_script_preparation_error(scope, node) {
|
||||
tracing::debug!(
|
||||
node = ?node,
|
||||
owner_document_handle = ?owner_document_handle,
|
||||
"child script preparation error route rejected the element task"
|
||||
);
|
||||
}
|
||||
}
|
||||
RuntimeScriptStartDecision::RegisterImportMap { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,15 @@ impl JsContextHost {
|
||||
};
|
||||
|
||||
match prepared {
|
||||
PreparedRuntimeScriptStartCommit::PreparationError { node } => {
|
||||
debug_assert!(load_delay_binding.is_none());
|
||||
if !self.queue_script_preparation_error(scope, node) {
|
||||
return Err(
|
||||
"script preparation error route rejected the element task".to_owned()
|
||||
);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
PreparedRuntimeScriptStartCommit::Noop => {
|
||||
if let Some(binding) = load_delay_binding {
|
||||
let _ = self.release_main_document_script_load_delay(binding);
|
||||
|
||||
@@ -24,6 +24,112 @@ fn queue_error(page_vm: &mut PageVm, element_id: &str) -> anyhow::Result<()> {
|
||||
page_vm.vm_mut().queue_script_preparation_error(element)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn dynamic_script_preparation_errors_enter_dom_fifo_at_insertion() {
|
||||
run_page_vm_async_test(async move {
|
||||
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?;
|
||||
for child in [false, true] {
|
||||
let (mut page_vm, _, _) = page_vm_with_bound_task_sources_and_owner_wake(
|
||||
&loader, Url::parse("https://example.com/dynamic-script-errors")?,
|
||||
);
|
||||
if child {
|
||||
page_vm.vm_mut().eval(r#"
|
||||
globalThis.errorFrame = document.createElement('iframe');
|
||||
document.body.append(errorFrame);
|
||||
void errorFrame.contentWindow;
|
||||
"#)?;
|
||||
assert!(page_vm.run_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::ChildRealmMaterialization, &loader,
|
||||
).await?);
|
||||
}
|
||||
page_vm.vm_mut().eval(r#"
|
||||
globalThis.events = [];
|
||||
globalThis.details = [];
|
||||
globalThis.exceptions = [];
|
||||
const target = globalThis.errorFrame?.contentWindow || window;
|
||||
const targetDocument = target.document;
|
||||
target.addEventListener('error', event => {
|
||||
if (event.target === target) exceptions.push(event.message);
|
||||
});
|
||||
onunhandledrejection = event => {
|
||||
events.push('rejection');
|
||||
event.preventDefault();
|
||||
};
|
||||
globalThis.appendInvalid = (id, src, type = '', ordered = false, connected = false) => {
|
||||
const element = targetDocument.createElement('script');
|
||||
element.id = id;
|
||||
element.type = type;
|
||||
if (ordered) element.async = false;
|
||||
element.onerror = event => {
|
||||
events.push(id);
|
||||
details.push([event instanceof target.Event, event instanceof target.ErrorEvent,
|
||||
event.isTrusted, event.bubbles, event.cancelable, event.composed,
|
||||
event.target === element, event.currentTarget === element]);
|
||||
Promise.resolve().then(() => events.push('microtask:' + id));
|
||||
// Preparation failure still consumes already-started, including in a child.
|
||||
element.src = 'http://[';
|
||||
element.remove();
|
||||
targetDocument.body.append(element);
|
||||
};
|
||||
if (connected) targetDocument.body.append(element);
|
||||
element.src = src;
|
||||
if (!connected) targetDocument.body.append(element);
|
||||
return element;
|
||||
};
|
||||
appendInvalid('first', '').remove();
|
||||
Promise.reject('sentinel');
|
||||
"#)?;
|
||||
page_vm.vm_mut().eval(r#"
|
||||
appendInvalid('whitespace', ' ');
|
||||
appendInvalid('invalid', 'http://[');
|
||||
appendInvalid('module-empty', '', 'module');
|
||||
appendInvalid('module-invalid', 'http://[', 'module');
|
||||
appendInvalid('ordered', 'http://[', '', true);
|
||||
appendInvalid('importmap', 'data:application/json,{}', 'importmap');
|
||||
appendInvalid('connected', '', '', false, true);
|
||||
"#)?;
|
||||
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(events)")?, "[]",
|
||||
"insertion queues errors without dispatching them: child={child}");
|
||||
let mut expected = Vec::new();
|
||||
for id in ["first", "rejection", "whitespace", "invalid", "module-empty",
|
||||
"module-invalid", "ordered", "importmap", "connected"] {
|
||||
let family = if id == "rejection" {
|
||||
PageDomManipulationTestFamily::PromiseRejection
|
||||
} else {
|
||||
PageDomManipulationTestFamily::ScriptPreparationError
|
||||
};
|
||||
assert!(page_vm.run_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::DomManipulation(family), &loader,
|
||||
).await?, "expected DOM FIFO head {id}: child={child}");
|
||||
expected.push(id.to_owned());
|
||||
if id != "rejection" {
|
||||
expected.push(format!("microtask:{id}"));
|
||||
}
|
||||
assert_eq!(page_vm.vm_mut().eval("events.join('|')")?, expected.join("|"),
|
||||
"each task finishes its listeners and reactions: child={child}");
|
||||
}
|
||||
let details: serde_json::Value = serde_json::from_str(
|
||||
&page_vm.vm_mut().eval("JSON.stringify(details)")?,
|
||||
)?;
|
||||
assert_eq!(details, serde_json::json!(vec![
|
||||
[true, false, true, false, false, false, true, true]; 8
|
||||
]), "trusted plain Events belong to the element's realm: child={child}");
|
||||
assert_eq!(page_vm.vm_mut().eval("JSON.stringify(exceptions)")?, "[]");
|
||||
assert!(!page_vm.has_ready_dom_manipulation_task_for_test(),
|
||||
"changing src and reinserting already-started failures must not queue more errors");
|
||||
assert!(!page_vm.run_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::MainDocumentRuntime(
|
||||
PageMainDocumentRuntimeActionKind::RuntimeScriptAdmission,
|
||||
), &loader,
|
||||
).await?, "preparation failures must not enter runtime script admission");
|
||||
assert!(!page_vm.run_exact_selected_page_task_for_test(
|
||||
PageSelectedTaskTestSelector::ChildClassicScriptSourceLoad, &loader,
|
||||
).await?, "child preparation failures must not start a script fetch");
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}).await.expect("dynamic script preparation error admission");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn script_preparation_errors_share_dom_fifo_and_complete_listener_reactions() {
|
||||
run_page_vm_async_test(async move {
|
||||
|
||||
Reference in New Issue
Block a user