fix(parser): restore child insertion points before script error handlers

This commit is contained in:
ldm0
2026-09-26 09:51:03 +08:00
parent c3c279cf36
commit 291b613b37
3 changed files with 108 additions and 8 deletions
+3
View File
@@ -13,6 +13,9 @@ use moli_core::{
use support::FixtureServer;
use tokio::time::Duration;
#[path = "scripts/child_error_insertion.rs"]
mod child_error_insertion;
fn diagnostic_global<'a>(
page: &'a moli_core::page::Page,
name: &str,
@@ -0,0 +1,75 @@
use super::*;
async fn child_script_error_write_probe(
nested: bool,
unknown_scheme: bool,
) -> Result<serde_json::Value> {
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
let mut page = browser.fetch(&server.url("/static")).await?;
let source = if unknown_scheme {
"no-such-script-scheme:missing".to_owned()
} else {
server.url("/missing-child-parser-script.js")
};
let expression = r#"new Promise(resolve => {
let during = null;
globalThis.childErrorObserved = value => { during = value; };
const frame = document.createElement('iframe');
frame.onload = () => resolve(JSON.stringify({
during, after: frame.contentDocument.body.textContent,
nestedRan: frame.contentWindow.nestedRan === true
}));
const written = NESTED
? '<script>globalThis.nestedRan = true; document.write("text");<' + '/script>'
: 'text';
const handler = `function failed(event) {
document.write(${JSON.stringify(written).replaceAll('<', '\\u003c')});
parent.childErrorObserved({text: document.body.textContent,
currentScriptIsNull: document.currentScript === null,
tailMissing: document.getElementById('tail') === null,
eventType: event.type,
nestedRan: globalThis.nestedRan === true});
}`;
frame.srcdoc = '<!doctype html><head><script>' + handler + '<' + '/script></head>' +
'<body>Some <script src="' + SOURCE + '" onerror="failed(event)"><' + '/script>' +
'<span id="tail"> tail</span>';
document.body.append(frame);
})"#
.replace("NESTED", if nested { "true" } else { "false" })
.replace("SOURCE", &serde_json::to_string(&source)?);
let result = page
.evaluate_runtime_expression_with_await_async(&expression, true)
.await?;
server.shutdown().await;
serde_json::from_str(result["value"].as_str().expect("child error probe")).map_err(Into::into)
}
#[tokio::test(flavor = "multi_thread")]
async fn child_parser_fetch_error_write_is_synchronous() -> Result<()> {
let result = child_script_error_write_probe(false, false).await?;
assert_eq!(result["during"]["text"], "Some text", "{result}");
assert_eq!(result["during"]["currentScriptIsNull"], true, "{result}");
assert_eq!(result["during"]["tailMissing"], true, "{result}");
assert_eq!(result["during"]["eventType"], "error", "{result}");
assert_eq!(result["after"], "Some text tail", "{result}");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn child_parser_fetch_error_write_handles_unknown_schemes() -> Result<()> {
let result = child_script_error_write_probe(false, true).await?;
assert_eq!(result["during"]["text"], "Some text", "{result}");
assert_eq!(result["during"]["tailMissing"], true, "{result}");
assert_eq!(result["after"], "Some text tail", "{result}");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn child_parser_fetch_error_write_executes_nested_inline_scripts() -> Result<()> {
let result = child_script_error_write_probe(true, false).await?;
assert_eq!(result["during"]["nestedRan"], true, "{result}");
assert_eq!(result["during"]["tailMissing"], true, "{result}");
assert_eq!(result["nestedRan"], true, "{result}");
Ok(())
}
@@ -7,7 +7,7 @@ use crate::frame_owner_model::{
FrameDocumentClassicCompletionScriptEventAction,
FrameDocumentClassicCompletionScriptEventFollowup, FrameDocumentClassicParserResumeApplication,
FrameDocumentClassicScriptCompletionAction, FrameDocumentClassicScriptCompletionTarget,
FrameDocumentClassicScriptScheduling,
FrameDocumentClassicScriptScheduling, FrameDocumentScriptElementEventKind,
};
use super::super::{ScriptVm, child_document_script_owner_hooks::ChildDocumentScriptOwnerHooks};
@@ -80,15 +80,37 @@ impl<'vm> ChildClassicCompletionOwner<'vm> {
let target = action.target();
let event = action.event();
let realm_id = target.realm_id();
let _parser_script_nesting = matches!(
let parser_blocking = matches!(
target.scheduling(),
FrameDocumentClassicScriptScheduling::ParserBlocking
)
.then(|| {
ChildDocumentScriptOwnerHooks::new(self.vm)
.enter_parser_script_nesting(target.child_handle(), target.task_owner())
})
.flatten();
);
// Fetch failures bypass script execution, which normally unblocks the
// tokenizer before invoking page code. The error handler still runs at
// that parser insertion point, including synchronous document.write().
// Do not resume again for load: the script may have installed a new
// nested parser blocker while executing.
if parser_blocking
&& matches!(event.kind, FrameDocumentScriptElementEventKind::Error)
&& !ChildDocumentScriptOwnerHooks::new(self.vm).resume_parser_for_classic_execution(
target.child_handle(),
target.task_owner(),
event.script_handle,
)
{
tracing::debug!(
?target,
script_handle = ?event.script_handle,
"dropping child classic error event with a stale parser resume permit"
);
followup.note_script_event_dispatch_failed();
return;
}
let _parser_script_nesting = parser_blocking
.then(|| {
ChildDocumentScriptOwnerHooks::new(self.vm)
.enter_parser_script_nesting(target.child_handle(), target.task_owner())
})
.flatten();
let dispatch = ChildDocumentScriptOwnerHooks::new(self.vm)
.dispatch_script_element_event_for_parts_selected_task_body(
target.task_owner(),