Fix hydration, placeholders, and variable font geometry
This commit is contained in:
1 parent
66f04d0f43
commit
b4d11b4c9e
9 files changed
+386
-95
No files matched your search
Vendored
+31
-11
@@ -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;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user