Align load readiness and paginated fixed content
This commit is contained in:
1 parent
03f506b01a
commit
1f8a4f0baa
7 files changed
+610
-90
No files matched your search
Vendored
+45
-3
@@ -16,7 +16,10 @@
|
||||
'__obscura_objects', '__obscura_oid', '__obscura_ua',
|
||||
'__obscura_platform', '__obscura_ua_platform', '__obscura_ua_platform_version',
|
||||
'__obscura_stealth', '__obscura_markTrusted',
|
||||
'__obscura_hw', '__obscura_mem', '__markParserScripts',
|
||||
'__markParserScripts', '__obscura_hasPendingDynamicScripts',
|
||||
'__obscura_hasPendingLoadDelayingScripts',
|
||||
'__obscura_nextPendingTimeoutDelay',
|
||||
'__obscura_hw', '__obscura_mem',
|
||||
'__documentReadyState__', '__currentUrl',
|
||||
// internal helpers (var-declared throughout the file)
|
||||
'__processDynScriptQueue', '_decodeDataScriptUrl', '_markNative', '_fpRand', '_fpNoise',
|
||||
@@ -199,6 +202,7 @@ let _fpSeed = 0;
|
||||
let __dynScriptQueue = [];
|
||||
let __dynScriptBusy = false;
|
||||
let __dynClassicPending = 0;
|
||||
let __dynLoadDelayingPending = 0;
|
||||
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
||||
value: function() {
|
||||
return __dynClassicPending > 0 || __dynScriptBusy || __dynScriptQueue.length > 0;
|
||||
@@ -207,6 +211,17 @@ Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
});
|
||||
// HTML tracks scripts which delay the document load event separately from
|
||||
// arbitrary asynchronous script work. A connected external script prepared
|
||||
// before `load` joins that set until its load/error processing finishes;
|
||||
// dynamic import() and scripts created by a load handler are post-load work.
|
||||
// Keep this bridge hidden for the same reason as the general queue status.
|
||||
Object.defineProperty(globalThis, '__obscura_hasPendingLoadDelayingScripts', {
|
||||
value: function() { return __dynLoadDelayingPending > 0; },
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
});
|
||||
function _decodeDataScriptUrl(url) {
|
||||
const comma = url.indexOf(',');
|
||||
if (!url.startsWith('data:') || comma < 5) {
|
||||
@@ -305,6 +320,11 @@ async function __runDynScriptTask(task) {
|
||||
} catch(e) {
|
||||
console.error('Dynamic script fetch error:', e.message);
|
||||
try { task.dispatchEvent(new Event('error')); } catch(ex) {}
|
||||
} finally {
|
||||
if (task.delaysLoad) {
|
||||
task.delaysLoad = false;
|
||||
__dynLoadDelayingPending = Math.max(0, __dynLoadDelayingPending - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function __runAsyncClassicScript(task) {
|
||||
@@ -658,6 +678,19 @@ const _clearedTimers = new Set();
|
||||
const _intervals = new Set();
|
||||
const _nativeTimerIds = new Map();
|
||||
const __obscuraPendingTimeoutDeadlines = new Map();
|
||||
Object.defineProperty(globalThis, '__obscura_nextPendingTimeoutDelay', {
|
||||
value: function() {
|
||||
const now = performance.now();
|
||||
let nearest = Infinity;
|
||||
for (const deadline of __obscuraPendingTimeoutDeadlines.values()) {
|
||||
nearest = Math.min(nearest, Math.max(0, deadline - now));
|
||||
}
|
||||
return Number.isFinite(nearest) ? nearest : -1;
|
||||
},
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
const _scheduleAfter = (delay, fn) => {
|
||||
const d = Math.max(0, Number(delay) || 0);
|
||||
@@ -1327,6 +1360,12 @@ function __prepareInsertedScript(script) {
|
||||
pageOrigin,
|
||||
dispatchEvent: (ev) => { try { script.dispatchEvent(ev); } catch(e) {} },
|
||||
};
|
||||
// Non-parser-inserted external scripts are async by default, but scripts
|
||||
// prepared while the document is still loading still delay window.load.
|
||||
// Snapshot the flag at preparation time: changing readyState later must
|
||||
// not turn already-prepared work into a post-load enhancement.
|
||||
task.delaysLoad = globalThis.document?.readyState !== 'complete';
|
||||
if (task.delaysLoad) __dynLoadDelayingPending++;
|
||||
// A non-parser-inserted classic script is force-async unless script code
|
||||
// explicitly assigned `.async = false`. Keep that opt-out in insertion
|
||||
// order; default/async=true scripts fetch concurrently and execute as soon
|
||||
@@ -1342,14 +1381,17 @@ function __prepareInsertedScript(script) {
|
||||
}
|
||||
} else if (isModule) {
|
||||
const dataUrl = 'data:text/javascript;base64,' + btoa(unescape(encodeURIComponent(code)));
|
||||
__dynScriptQueue.push({
|
||||
const task = {
|
||||
url: dataUrl,
|
||||
isModule: true,
|
||||
nid: script._nid,
|
||||
prevNid,
|
||||
pageOrigin: "",
|
||||
dispatchEvent: (ev) => { try { script.dispatchEvent(ev); } catch(e) {} },
|
||||
});
|
||||
delaysLoad: globalThis.document?.readyState !== 'complete',
|
||||
};
|
||||
if (task.delaysLoad) __dynLoadDelayingPending++;
|
||||
__dynScriptQueue.push(task);
|
||||
__processDynScriptQueue();
|
||||
} else {
|
||||
globalThis.__currentScriptNid = script._nid;
|
||||
|
||||
@@ -605,6 +605,52 @@ impl ObscuraJsRuntime {
|
||||
})
|
||||
}
|
||||
|
||||
/// Capture one immutable document slice as if its origin were the root
|
||||
/// scroll position of a virtual viewport. This leaves the live page scroll
|
||||
/// untouched while giving fixed and sticky descendants page-local paint
|
||||
/// geometry, which paginated raster PDF export requires.
|
||||
#[cfg(feature = "render")]
|
||||
pub fn screenshot_prepared_region_at_scroll_with_backgrounds(
|
||||
&self,
|
||||
region: obscura_render::CaptureRegion,
|
||||
root_scroll: (f32, f32),
|
||||
paint_backgrounds: bool,
|
||||
) -> Result<Vec<u8>, obscura_render::CaptureError> {
|
||||
let mut state = self.state.borrow_mut();
|
||||
with_sync_render_loading_disabled(&mut state, |state| {
|
||||
ensure_resolved_scroll(state).ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
let ObscuraState {
|
||||
dom,
|
||||
prepared_render,
|
||||
render_resources,
|
||||
element_scroll_offsets,
|
||||
..
|
||||
} = state;
|
||||
let dom = dom
|
||||
.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?;
|
||||
let scroll = prepared_render
|
||||
.as_ref()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?
|
||||
.resolve_scroll_state_for_viewport(
|
||||
dom,
|
||||
root_scroll,
|
||||
element_scroll_offsets,
|
||||
(region.width, region.height),
|
||||
);
|
||||
obscura_render::screenshot_prepared_region_with_scroll_and_backgrounds(
|
||||
dom,
|
||||
prepared_render
|
||||
.as_mut()
|
||||
.ok_or(obscura_render::CaptureError::PaintFailed)?,
|
||||
render_resources,
|
||||
&scroll,
|
||||
region,
|
||||
paint_backgrounds,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the retained layout's scrollable document size without changing
|
||||
/// the live viewport or scroll position. PDF/full-document consumers use
|
||||
/// this to paginate document-space captures from the same geometry.
|
||||
@@ -1492,6 +1538,19 @@ impl ObscuraJsRuntime {
|
||||
.is_pending_or_recent(std::time::Duration::from_millis(100))
|
||||
}
|
||||
|
||||
/// Whether a connected dynamic script prepared before the document load
|
||||
/// event still has fetch/evaluation/load-or-error work outstanding.
|
||||
///
|
||||
/// This intentionally excludes `import()` and scripts created by a load
|
||||
/// handler. Those are ordinary post-load enhancement work and should only
|
||||
/// be driven when an automation caller explicitly asks the page to settle.
|
||||
pub fn has_pending_load_delaying_scripts(&mut self) -> bool {
|
||||
self.evaluate("globalThis.__obscura_hasPendingLoadDelayingScripts?.() === true")
|
||||
.ok()
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Generation of observable connected-document mutations. This excludes
|
||||
/// detached-tree construction and no-op writes, which cannot affect a
|
||||
/// screenshot or DOM dump.
|
||||
@@ -8379,6 +8438,8 @@ mod tests {
|
||||
fn dynamic_script_status_bridge_is_hidden_and_idle() {
|
||||
let mut rt = setup_runtime("<html><body></body></html>");
|
||||
assert!(!rt.has_pending_dynamic_scripts());
|
||||
assert!(!rt.has_pending_load_delaying_scripts());
|
||||
assert_eq!(rt.next_pending_timeout_delay_ms(), None);
|
||||
assert_eq!(
|
||||
rt.evaluate("typeof __dynScriptBusy").unwrap(),
|
||||
serde_json::json!("undefined")
|
||||
@@ -8397,6 +8458,27 @@ mod tests {
|
||||
.unwrap(),
|
||||
serde_json::json!(false)
|
||||
);
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
"Reflect.ownKeys(globalThis).includes('__obscura_hasPendingLoadDelayingScripts')"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!(false)
|
||||
);
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
"Object.getOwnPropertyNames(globalThis).includes('__obscura_nextPendingTimeoutDelay')"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!(false)
|
||||
);
|
||||
assert_eq!(
|
||||
rt.evaluate(
|
||||
"Reflect.ownKeys(globalThis).includes('__obscura_nextPendingTimeoutDelay')"
|
||||
)
|
||||
.unwrap(),
|
||||
serde_json::json!(false)
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for #147: a TypeError in one script must not poison
|
||||
|
||||
Reference in new issue
Block a user