Fix hydration, placeholders, and variable font geometry

This commit is contained in:
SGavrl committed 2026-08-04 03:18:09 +00:00
1 parent 66f04d0f43
commit b4d11b4c9e
9 files changed
+386 -95

No files matched your search

+31 -11
View File
@@ -8278,6 +8278,7 @@ globalThis.atob = globalThis.atob || ((s) => {
(() => {
const stack = [{state: null, url: undefined}]; // initial entry; url=undefined means "use document URL"
let idx = 0;
const historyToken = Symbol("History");
const resolveOrFallback = (url) => {
// A missing url (pushState/replaceState called with < 3 args) keeps the
// current document URL per the HTML spec — capture it so the entry does not
@@ -8301,10 +8302,19 @@ globalThis.atob = globalThis.atob || ((s) => {
}
} catch {}
};
globalThis.history = {
get length() { return stack.length; },
get state() { return stack[idx].state; },
scrollRestoration: "auto",
class History {
constructor(token) {
if (token !== historyToken) throw new TypeError("Illegal constructor");
}
get length() { return stack.length; }
get state() { return stack[idx].state; }
get scrollRestoration() { return this._scrollRestoration || "auto"; }
set scrollRestoration(value) {
const normalized = String(value);
if (normalized === "auto" || normalized === "manual") {
this._scrollRestoration = normalized;
}
}
pushState(state, _title, url) {
const prevUrl = __currentUrl();
const resolved = resolveOrFallback(url);
@@ -8315,14 +8325,14 @@ globalThis.atob = globalThis.atob || ((s) => {
idx = stack.length - 1;
applyVirtual();
fireHashChangeIfNeeded(prevUrl);
},
}
replaceState(state, _title, url) {
const prevUrl = __currentUrl();
const resolved = resolveOrFallback(url);
stack[idx] = {state: state ?? null, url: resolved};
applyVirtual();
fireHashChangeIfNeeded(prevUrl);
},
}
go(n) {
n = (n | 0);
if (n === 0) return; // real spec: go(0) reloads. We don't reload SPAs.
@@ -8337,10 +8347,17 @@ globalThis.atob = globalThis.atob || ((s) => {
globalThis.dispatchEvent(ev);
} catch {}
fireHashChangeIfNeeded(prevUrl);
},
back() { this.go(-1); },
forward() { this.go(1); },
};
}
back() { this.go(-1); }
forward() { this.go(1); }
}
Object.defineProperty(History.prototype, Symbol.toStringTag, {value: "History"});
Object.defineProperty(globalThis, "History", {
value: History, writable: true, configurable: true,
});
Object.defineProperty(globalThis, "history", {
value: new History(historyToken), writable: true, configurable: true,
});
})();
// Navigation API. New framework routers increasingly prefer `navigation`
@@ -12075,7 +12092,10 @@ globalThis.__obscura_init = function() {
? globalThis.__obscura_viewport_h : sh - 80;
globalThis.screen = new Screen(sw, sh);
globalThis.visualViewport = { width:vw, height:vh, offsetLeft:0, offsetTop:0, scale:1, addEventListener(){}, removeEventListener(){} };
globalThis.devicePixelRatio = sw >= 2560 ? 2 : 1;
// Screen dimensions do not determine the output device scale. The embedding
// browser applies an explicit device metric after page initialization; the
// standalone runtime has the same 1x default as Obscura's render surface.
globalThis.devicePixelRatio = 1;
globalThis.innerWidth = vw; globalThis.innerHeight = vh;
globalThis.outerWidth = sw; globalThis.outerHeight = sh - 40;
+100
View File
@@ -2706,6 +2706,35 @@ mod tests {
assert_eq!(path, serde_json::json!("/a"));
}
#[test]
fn history_exposes_the_web_platform_constructor_and_prototype() {
let mut rt = setup_runtime("<html><body></body></html>");
let result = rt
.evaluate(
r#"(function(){
const original = history.replaceState;
History.prototype.replaceState.call(history, {ok:true}, "", "/prototype");
let illegal = false;
try { new History(); } catch (error) { illegal = error instanceof TypeError; }
return {
instance: history instanceof History,
prototype: Object.getPrototypeOf(history) === History.prototype,
method: original === History.prototype.replaceState,
tag: Object.prototype.toString.call(history),
path: location.pathname,
illegal,
};
})()"#,
)
.unwrap();
assert_eq!(result["instance"], serde_json::json!(true));
assert_eq!(result["prototype"], serde_json::json!(true));
assert_eq!(result["method"], serde_json::json!(true));
assert_eq!(result["tag"], serde_json::json!("[object History]"));
assert_eq!(result["path"], serde_json::json!("/prototype"));
assert_eq!(result["illegal"], serde_json::json!(true));
}
#[test]
fn style_attribute_parses_into_style_object() {
// Inline styles present in the parsed HTML must be visible via el.style.*
@@ -6634,6 +6663,54 @@ mod tests {
assert!(later >= initial + 8.0, "initial={initial}, later={later}");
}
#[cfg(feature = "render")]
#[test]
fn fixed_animation_capture_is_invariant_after_geometry_flush() {
let make_runtime = || {
let mut rt = ObscuraJsRuntime::new();
rt.set_dom(parse_html(
r#"<html style="margin:0"><head><style>
@keyframes dismiss {
from { opacity:1; transform:translateY(0) }
to { opacity:0; transform:translateY(-80px) }
}
body { margin:0; width:160px; height:100px; background:#f5f7fa }
#content { width:120px; height:50px; margin:20px; background:#1769aa }
#shell { position:fixed; inset:0; background:#111827 }
#shell.dismissed { animation:dismiss 600ms linear forwards }
</style></head><body><div id="content"></div>
<div id="shell"></div></body></html>"#,
));
rt.set_url("http://example.test/github-like-shell");
rt.set_viewport(160.0, 100.0);
rt.run_page_init();
rt.evaluate("document.getElementById('shell').className='dismissed'")
.unwrap();
rt
};
let fixed = obscura_render::AnimationSample::local_override(750.0);
let direct_rt = make_runtime();
assert!(direct_rt.set_animation_sample(fixed));
let direct = direct_rt
.screenshot_prepared((160.0, 100.0), Some("http://example.test/github-like-shell"))
.expect("direct fixed-time capture");
let mut geometry_rt = make_runtime();
let rect = geometry_rt
.evaluate("document.getElementById('content').getBoundingClientRect().toJSON()")
.expect("geometry flush before capture");
assert_eq!(rect["width"].as_f64(), Some(120.0));
assert!(geometry_rt.set_animation_sample(fixed));
let after_geometry = geometry_rt
.screenshot_prepared((160.0, 100.0), Some("http://example.test/github-like-shell"))
.expect("fixed-time capture after geometry");
assert_eq!(
direct, after_geometry,
"a CSSOM geometry flush must not change fixed-time capture output"
);
}
#[cfg(feature = "render")]
#[test]
fn cssom_animation_sample_is_frozen_within_one_javascript_task() {
@@ -7058,6 +7135,29 @@ mod tests {
assert_eq!(values[2], values[3]);
}
#[cfg(feature = "render")]
#[tokio::test(flavor = "current_thread")]
async fn fingerprinted_screen_does_not_invent_a_device_scale_factor() {
let mut rt = ObscuraJsRuntime::new();
rt.set_dom(parse_html("<html><body></body></html>"));
rt.set_viewport(300.0, 200.0);
// Force the fingerprint seed whose screen-pool entry is 2560x1440.
// That physical screen must not silently turn a 1x render surface into
// a 2x devicePixelContentBoxSize surface.
rt.execute_script(
"deterministic-high-resolution-screen",
"Date.now = () => 0; Math.random = () => 2 / 0xFFFFFFFF;",
)
.unwrap();
rt.run_page_init();
assert_eq!(
rt.evaluate("[screen.width, screen.height, devicePixelRatio]")
.unwrap(),
serde_json::json!([2560, 1440, 1])
);
}
#[cfg(feature = "render")]
#[tokio::test(flavor = "current_thread")]
async fn resize_observer_reports_real_boxes_only_when_selected_size_changes() {