diff --git a/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html b/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html
index 5d33988ac0..e79aaa7ae9 100644
--- a/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html
+++ b/moli-core/tests/fixtures/runtime/runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch.html
@@ -3,17 +3,18 @@
+
diff --git a/moli-core/tests/scripts.rs b/moli-core/tests/scripts.rs
index 61d18e76aa..40e15f4ddb 100644
--- a/moli-core/tests/scripts.rs
+++ b/moli-core/tests/scripts.rs
@@ -830,6 +830,10 @@ async fn runtime_inserted_stylesheet_load_can_trigger_location_replace() -> Resu
#[tokio::test(flavor = "multi_thread")]
async fn runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch_for_parser_blocking_progress()
-> Result<()> {
+ // The stale stylesheet response is gated until the following parser script
+ // requests its probe. The probe reports whether it arrived before the
+ // stale-response fallback fired, so this checks parser progress without a
+ // wall-clock threshold in page script.
let server = FixtureServer::spawn().await?;
let browser = Browser::new(AppConfig::default())?;
@@ -845,6 +849,12 @@ async fn runtime_inserted_stylesheet_href_mutation_uses_fresh_fetch_for_parser_b
diagnostic_global(&page, "runtimeInsertedHrefMutationSawLate"),
Some(&JsValueSnapshot::Bool(false))
);
+ assert!(
+ page.subresource_network_records().iter().any(|record| {
+ record.url().path() == "/assets/runtime_inserted_stylesheet_href_mutation_fresh.css"
+ }),
+ "the retargeted stylesheet must start the fresh fetch"
+ );
server.shutdown().await;
Ok(())
diff --git a/moli-test-support/src/routes_core.rs b/moli-test-support/src/routes_core.rs
index 560ab5fa34..b936a6d13e 100644
--- a/moli-test-support/src/routes_core.rs
+++ b/moli-test-support/src/routes_core.rs
@@ -1,7 +1,7 @@
use super::*;
use parking_lot::Mutex;
use std::collections::HashMap;
-use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, OnceLock};
static NET_UPSTREAM_XHR_404_THEN_200_REQUESTS: AtomicUsize = AtomicUsize::new(0);
@@ -21,6 +21,25 @@ static RUNTIME_OWNED_IN_ORDER_ERROR_AFTER_DCL_GATES: OnceLock<
static RUNTIME_OWNED_IN_ORDER_LOAD_AFTER_DCL_GATES: OnceLock<
Mutex>>,
> = OnceLock::new();
+static RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES: OnceLock<
+ Mutex>>,
+> = OnceLock::new();
+
+struct RuntimeInsertedStylesheetHrefMutationGate {
+ release_stale_response: tokio::sync::Notify,
+ stale_request_started: AtomicBool,
+ stale_response_timed_out: AtomicBool,
+}
+
+impl RuntimeInsertedStylesheetHrefMutationGate {
+ fn new() -> Self {
+ Self {
+ release_stale_response: tokio::sync::Notify::new(),
+ stale_request_started: AtomicBool::new(false),
+ stale_response_timed_out: AtomicBool::new(false),
+ }
+ }
+}
struct ConcurrentSharedStateRequestGuard;
@@ -169,6 +188,34 @@ pub(crate) fn notify_runtime_owned_in_order_load_after_dcl_gate(host_key: &str)
runtime_owned_in_order_load_after_dcl_gate(host_key).notify_one();
}
+fn runtime_inserted_stylesheet_href_mutation_gate(
+ host_key: &str,
+) -> Arc {
+ let gates =
+ RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get_or_init(|| Mutex::new(HashMap::new()));
+ let mut gates = gates.lock();
+ gates
+ .entry(host_key.to_owned())
+ .or_insert_with(|| Arc::new(RuntimeInsertedStylesheetHrefMutationGate::new()))
+ .clone()
+}
+
+fn remove_runtime_inserted_stylesheet_href_mutation_gate(host_key: &str) {
+ let Some(gates) = RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get() else {
+ return;
+ };
+ gates.lock().remove(host_key);
+}
+
+pub(crate) fn clear_runtime_inserted_stylesheet_href_mutation_gate(host_key: &str) {
+ let Some(gates) = RUNTIME_INSERTED_STYLESHEET_HREF_MUTATION_GATES.get() else {
+ return;
+ };
+ if let Some(gate) = gates.lock().remove(host_key) {
+ gate.release_stale_response.notify_one();
+ }
+}
+
pub(super) async fn static_page() -> Html<&'static str> {
Html(STATIC_HTML)
}
@@ -5526,6 +5573,45 @@ pub(super) async fn asset_dynamic_blocking_stylesheet_gated_css(
css_response(BLOCKING_STYLESHEET_SLOW_CSS)
}
+pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_stale_css(
+ headers: HeaderMap,
+) -> Response {
+ let host_key = request_host_key(&headers).unwrap_or_default();
+ let gate = runtime_inserted_stylesheet_href_mutation_gate(&host_key);
+ gate.stale_request_started.store(true, Ordering::SeqCst);
+ let released_by_probe = tokio::time::timeout(
+ Duration::from_secs(2),
+ gate.release_stale_response.notified(),
+ )
+ .await
+ .is_ok();
+ if released_by_probe {
+ remove_runtime_inserted_stylesheet_href_mutation_gate(&host_key);
+ } else {
+ gate.stale_response_timed_out.store(true, Ordering::SeqCst);
+ }
+ css_response(BLOCKING_STYLESHEET_SLOW_CSS)
+}
+
+pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_fresh_css() -> Response {
+ css_response(CHROME_STYLESHEETLIST_1_CSS)
+}
+
+pub(super) async fn asset_runtime_inserted_stylesheet_href_mutation_probe_script(
+ headers: HeaderMap,
+) -> Response {
+ let host_key = request_host_key(&headers).unwrap_or_default();
+ let gate = runtime_inserted_stylesheet_href_mutation_gate(&host_key);
+ let parser_not_blocked = !gate.stale_response_timed_out.load(Ordering::SeqCst);
+ gate.release_stale_response.notify_one();
+ if gate.stale_request_started.load(Ordering::SeqCst) {
+ remove_runtime_inserted_stylesheet_href_mutation_gate(&host_key);
+ }
+ javascript_string_response(format!(
+ "window.runtimeInsertedHrefMutationParserNotBlocked = {parser_not_blocked};"
+ ))
+}
+
pub(super) async fn asset_runtime_connected_preload_very_slow_css() -> Response {
sleep(Duration::from_millis(250)).await;
css_response(BLOCKING_STYLESHEET_SLOW_CSS)
diff --git a/moli-test-support/src/server.rs b/moli-test-support/src/server.rs
index 2e19fc5efb..85d2cdfcc5 100644
--- a/moli-test-support/src/server.rs
+++ b/moli-test-support/src/server.rs
@@ -52,6 +52,9 @@ impl FixtureServer {
}
pub async fn shutdown(mut self) {
+ crate::routes_core::clear_runtime_inserted_stylesheet_href_mutation_gate(
+ &self.addr.to_string(),
+ );
if let Some(shutdown_tx) = self.shutdown_tx.take() {
let _ = shutdown_tx.send(());
}
@@ -64,6 +67,9 @@ impl FixtureServer {
impl Drop for FixtureServer {
fn drop(&mut self) {
+ crate::routes_core::clear_runtime_inserted_stylesheet_href_mutation_gate(
+ &self.addr.to_string(),
+ );
if let Some(shutdown_tx) = self.shutdown_tx.take() {
let _ = shutdown_tx.send(());
}
diff --git a/moli-test-support/src/server_routes.rs b/moli-test-support/src/server_routes.rs
index c715e09733..10d90d5747 100644
--- a/moli-test-support/src/server_routes.rs
+++ b/moli-test-support/src/server_routes.rs
@@ -2393,6 +2393,18 @@ pub(super) fn build_router() -> Router {
"/assets/blocking_stylesheet_slow.css",
get(asset_blocking_stylesheet_slow_css),
)
+ .route(
+ "/assets/runtime_inserted_stylesheet_href_mutation_stale.css",
+ get(asset_runtime_inserted_stylesheet_href_mutation_stale_css),
+ )
+ .route(
+ "/assets/runtime_inserted_stylesheet_href_mutation_fresh.css",
+ get(asset_runtime_inserted_stylesheet_href_mutation_fresh_css),
+ )
+ .route(
+ "/assets/runtime_inserted_stylesheet_href_mutation_probe.js",
+ get(asset_runtime_inserted_stylesheet_href_mutation_probe_script),
+ )
.route(
"/assets/runtime_connected_modulepreload_slow.mjs",
get(asset_runtime_connected_modulepreload_slow_module),