From 9ffc071ce0e894ccfa5a395227b615f464b6adf2 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:31:14 +0000 Subject: [PATCH] dom: settle async inline style updates --- crates/obscura-js/js/bootstrap.js | 3 +- crates/obscura-js/src/runtime.rs | 62 +++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/crates/obscura-js/js/bootstrap.js b/crates/obscura-js/js/bootstrap.js index c468791..98ea994 100644 --- a/crates/obscura-js/js/bootstrap.js +++ b/crates/obscura-js/js/bootstrap.js @@ -693,7 +693,8 @@ const _styleProxy = (decl) => new Proxy(decl, { if (typeof p === "symbol") { t[p] = v; return true; } if (p === "_loaded") { t._loaded = v; return true; } if (p === "cssText") { t.cssText = v; return true; } - if (/^\d+$/.test(p) || p in Object.getPrototypeOf(t)) return true; + if (p in t) { Reflect.set(t, p, v); return true; } + if (/^\d+$/.test(p)) return true; t.setProperty(p, v); return true; }, diff --git a/crates/obscura-js/src/runtime.rs b/crates/obscura-js/src/runtime.rs index d1b1106..ea2f291 100644 --- a/crates/obscura-js/src/runtime.rs +++ b/crates/obscura-js/src/runtime.rs @@ -881,10 +881,18 @@ impl ObscuraJsRuntime { } pub async fn run_event_loop(&mut self) -> Result<(), String> { - self.runtime + // A browser performs a microtask checkpoint at the end of each task. + // deno_core's event loop may return immediately when no async op is + // pending, leaving an already-resolved Promise continuation stranded + // (document.fonts.load(...).then(...), framework post-render hooks, + // and hydration follow-ups all rely on this boundary). + self.runtime.v8_isolate().perform_microtask_checkpoint(); + let result = self.runtime .run_event_loop(deno_core::PollEventLoopOptions::default()) .await - .map_err(|e| format!("Event loop error: {}", e)) + .map_err(|e| format!("Event loop error: {}", e)); + self.runtime.v8_isolate().perform_microtask_checkpoint(); + result } /// Whether the serialized dynamic-script queue is still fetching or @@ -1845,6 +1853,24 @@ mod tests { ); } + #[tokio::test(flavor = "current_thread")] + async fn idle_event_loop_flushes_resolved_promise_continuations() { + let mut rt = setup_runtime("
pending
"); + rt.execute_script( + "font-ready", + "document.fonts.load('normal 1px Example').then(() => {\ + document.getElementById('state').textContent = 'ready';\ + });", + ) + .unwrap(); + rt.run_event_loop_bounded(100).await.unwrap(); + assert_eq!( + rt.evaluate("document.getElementById('state').textContent") + .unwrap(), + serde_json::json!("ready") + ); + } + #[test] fn test_document_url() { let mut rt = setup_runtime(""); @@ -3021,7 +3047,8 @@ mod tests { datasetKeys: Object.keys(el.dataset), cssText: el.style.cssText, length: el.style.length, - getByDash: el.style.getPropertyValue('font-size') + getByDash: el.style.getPropertyValue('font-size'), + reflectedAttribute: el.getAttribute('style') }); })()"#, ) @@ -3036,6 +3063,35 @@ mod tests { assert_eq!(p["cssText"], "color: red; font-size: 14px;"); assert_eq!(p["length"], 2); assert_eq!(p["getByDash"], "14px"); + assert_eq!(p["reflectedAttribute"], "color: red; font-size: 14px;"); + } + + #[test] + fn style_declaration_reflects_and_removes_parsed_attributes() { + let mut rt = setup_runtime( + "
", + ); + let result = rt + .evaluate( + r#"(() => { + const el = document.getElementById('icon'); + const before = [el.style.fontSize, el.style.color, el.style.length]; + const removed = el.style.removeProperty('font-size'); + return JSON.stringify({ + before, + removed, + after: el.style.cssText, + attribute: el.getAttribute('style') + }); + })()"#, + ) + .unwrap(); + let value: serde_json::Value = + serde_json::from_str(result.as_str().unwrap()).unwrap(); + assert_eq!(value["before"], serde_json::json!(["0px", "red", 2])); + assert_eq!(value["removed"], "0px"); + assert_eq!(value["after"], "color: red;"); + assert_eq!(value["attribute"], "color: red;"); } /// Regression for #105: `element.querySelector` and `querySelectorAll`