fix(renderer): isolate print media from live screen state

Pass temporary media through native layout and child style observations, and
require transient publication. Printing preserves screen geometry, observer
samples, pending frame callbacks and live FontFaceSet identities on errors too.

Separate source style generations from computed-context cleanup so temporary
print materialization does not dirty screencast tokens. Live media, CSSOM,
interaction and resource changes still invalidate output. Keep live font
reconciliation screen-owned; print-specific resource integration remains
experimental. Scope the diagnostic deadline helper to debug/test builds.

Validation: cargo fmt --all; workspace/all-targets/all-features Clippy with
-D warnings; cargo nextest run --no-fail-fast (20,101 passed, 17 skipped).
Four new regressions cover isolation, pending work, live media and failures.
This commit is contained in:
ldm0
2026-11-13 06:00:48 +08:00
parent 25a357d562
commit 3d0e8776fd
17 changed files with 428 additions and 62 deletions
+1 -1
View File
@@ -307,7 +307,7 @@ Both `fetch` and `serve` accept the flag; it takes precedence when combined with
`--layout`. Optional resources remain independent: add `--image --font` or
`--resource` when needed. Native scrollbars remain opt-in with `--scrollbars`.
The mode is experimental: the core pipeline has regression coverage, while
broader observer/frame compatibility, temporary print-environment isolation
broader observer/frame compatibility, print-specific resource integration
and release performance validation remain in progress.
## Architecture
+10 -9
View File
@@ -129,15 +129,16 @@ pub use moli_renderer_v8::{
RendererRuntimeInspectorMessage, RendererRuntimeInspectorMessageBatch,
RendererRuntimeInspectorProtocolMessage, RendererRuntimeObservableSourceItem,
RendererRuntimeObservableSourceSummary, RendererRuntimeRealmInfo, RendererScreenshotClip,
RendererScreenshotFormat, RendererScreenshotRegion, RendererScrollIntoViewResult,
RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage,
RendererServiceWorkerFetchDiagnostic, RendererServiceWorkerFetchDiagnosticResult,
RendererServiceWorkerRunIdentity, RendererServiceWorkerTargetEvent,
RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus,
RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage,
RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody,
RendererTextSearchMatch, RendererTouchPoint, RendererVisualStateToken,
RendererWindowDocumentSource, RuntimeConsoleMessageSnapshot, WorkerInspectorError,
RendererScreenshotFormat, RendererScreenshotPurpose, RendererScreenshotRegion,
RendererScrollIntoViewResult, RendererServiceWorkerConsoleMessage,
RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic,
RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerRunIdentity,
RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo,
RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult,
RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent,
RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody, RendererTextSearchMatch,
RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource,
RuntimeConsoleMessageSnapshot, WorkerInspectorError,
};
pub use moli_renderer_v8::{
RendererAppManifest, RendererAppManifestDisplayMode, RendererAppManifestError,
+2 -2
View File
@@ -29,8 +29,8 @@ use moli_core::page::{
RendererDocumentSourcedSameDocumentNavigation,
RendererDocumentSourcedTopLevelLocationNavigation, RendererLayoutMetrics,
RendererPendingTopLevelHistoryTraversal, RendererPendingWindowOpenEvent,
RendererScreenshotClip, RendererScreenshotFormat, RendererScreenshotRegion,
RendererSetDocumentContentResult, RendererVisualStateToken,
RendererScreenshotClip, RendererScreenshotFormat, RendererScreenshotPurpose,
RendererScreenshotRegion, RendererSetDocumentContentResult, RendererVisualStateToken,
};
use serde::Deserialize;
use serde_json::{Value, json};
@@ -406,6 +406,7 @@ pub(super) fn renderer_screenshot_request(
None => RendererScreenshotRegion::Viewport,
};
Ok(RendererCaptureScreenshotRequest {
purpose: RendererScreenshotPurpose::Screenshot,
base_background_color,
format,
quality: command.quality.unwrap_or(80),
+13 -13
View File
@@ -328,19 +328,19 @@ pub use runtime::{
RendererRuntimeInspectorResponseChannel, RendererRuntimeInspectorResponseSender,
RendererRuntimeObservableSourceItem, RendererRuntimeObservableSourceSummary,
RendererRuntimeRealmInfo, RendererRuntimeRemoteObject, RendererRuntimeRemoteObjectResolution,
RendererScreenshotClip, RendererScreenshotFormat, RendererScreenshotRegion,
RendererScriptExecutionMemoryDiagnostics, RendererScriptSourceMemoryDiagnostics,
RendererScrollIntoViewResult, RendererServiceWorkerConsoleMessage,
RendererServiceWorkerExceptionMessage, RendererServiceWorkerFetchDiagnostic,
RendererServiceWorkerFetchDiagnosticResult, RendererServiceWorkerMainResourceFetch,
RendererServiceWorkerRunIdentity, RendererServiceWorkerTargetEvent,
RendererServiceWorkerTargetInfo, RendererServiceWorkerVersionStatus,
RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage,
RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, RendererStyleSheetHeader,
RendererStyleSheetInventoryUpdate, RendererStyleSheetPayload, RendererSyntheticResponseBody,
RendererTextSearchMatch, RendererTouchPoint, RendererVisualStateToken,
RendererWindowDocumentSource, RuntimeConsoleMessageSnapshot, WorkerInspectorError,
renderer_output_transport_channel,
RendererScreenshotClip, RendererScreenshotFormat, RendererScreenshotPurpose,
RendererScreenshotRegion, RendererScriptExecutionMemoryDiagnostics,
RendererScriptSourceMemoryDiagnostics, RendererScrollIntoViewResult,
RendererServiceWorkerConsoleMessage, RendererServiceWorkerExceptionMessage,
RendererServiceWorkerFetchDiagnostic, RendererServiceWorkerFetchDiagnosticResult,
RendererServiceWorkerMainResourceFetch, RendererServiceWorkerRunIdentity,
RendererServiceWorkerTargetEvent, RendererServiceWorkerTargetInfo,
RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult,
RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent,
RendererSharedWorkerTargetInfo, RendererStyleSheetHeader, RendererStyleSheetInventoryUpdate,
RendererStyleSheetPayload, RendererSyntheticResponseBody, RendererTextSearchMatch,
RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource,
RuntimeConsoleMessageSnapshot, WorkerInspectorError, renderer_output_transport_channel,
};
pub use runtime::{
RendererElementClickError, RendererElementClickTarget, RendererPreparedPointerClick,
@@ -232,13 +232,15 @@ impl PageScriptHost {
self.style_viewport_generation.get()
}
/// Marks the exact viewport selected by an active layout pass before that
/// pass reads the Document's styles.
/// Marks the exact style environment selected by an active layout pass
/// before that pass reads the Document's styles.
///
/// An embedded Document's persistent frame viewport is published only
/// after the recursive pass succeeds. Advancing this input generation
/// first also distinguishes an explicit main-Document capture viewport
/// from ordinary DOM APIs selecting conflicting implicit viewports.
/// or media from ordinary DOM APIs selecting conflicting implicit inputs.
/// This observation epoch does not invalidate canonical screen geometry or
/// schedule lifecycle work: the live source inputs have not changed.
pub(crate) fn note_layout_style_environment(
&self,
document: DomHandle,
@@ -240,6 +240,7 @@ impl RendererPageRenderingUpdateSource {
})
}
#[cfg(any(test, debug_assertions))]
pub(crate) fn local_deadline(
&self,
current: Option<RendererPageRenderingUpdateTask>,
+2 -1
View File
@@ -335,7 +335,8 @@ pub use self::page_screenshot::{
RendererCaptureScreencastFrameReply, RendererCaptureScreencastFrameRequest,
RendererCaptureScreenshotReply, RendererCaptureScreenshotRequest,
RendererCapturedScreencastFrame, RendererCapturedScreenshot, RendererScreenshotClip,
RendererScreenshotFormat, RendererScreenshotRegion, RendererVisualStateToken,
RendererScreenshotFormat, RendererScreenshotPurpose, RendererScreenshotRegion,
RendererVisualStateToken,
};
pub(super) use self::page_state::RendererPageEntry;
pub use self::page_state::RendererPageRecord;
+10 -12
View File
@@ -46,18 +46,16 @@ impl PageVm {
surface.inner_height,
surface.device_pixel_ratio as f32,
);
let result = self
.vm_mut()
.paint_layout_snapshot_with_capture_in_environment(
viewport,
moli_layout::LayoutFlushReason::Print,
PaintCaptureRequest {
include_backgrounds: request.print_background,
..PaintCaptureRequest::print_document()
},
crate::native_bridge::LayoutPublicationRequest::Transient,
environment_override,
);
let result = self.vm_mut().paint_layout_snapshot_with_capture(
viewport,
moli_layout::LayoutFlushReason::Print,
PaintCaptureRequest {
include_backgrounds: request.print_background,
..PaintCaptureRequest::print_document()
},
crate::native_bridge::LayoutPublicationRequest::Transient,
environment_override,
);
Ok(match result? {
Some(snapshot) => match moli_paint::pdf::pdf_snapshot(&snapshot, &request.options) {
Ok(bytes) => Reply::Printed(bytes),
@@ -1,6 +1,7 @@
//! Screenshot and screencast requests, replies and pixel capture.
use crate::native_bridge::LayoutPublicationRequest;
use crate::style_engine::StyloStyleEnvironment;
use moli_layout::{
LayoutRect, LayoutTransform2D, PaintCaptureRegion, PaintCaptureRequest, PaintCaptureSurface,
PaintViewport,
@@ -114,6 +115,15 @@ pub enum RendererScreenshotFormat {
Jpeg,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RendererScreenshotPurpose {
Screenshot,
/// Pixel capture under print media, independent of native PDF output.
Print {
print_background: bool,
},
}
/// A CDP page-coordinate clip. Validation remains at the renderer boundary so
/// every protocol frontend shares the same finite/range checks.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -138,6 +148,7 @@ pub enum RendererScreenshotRegion {
#[derive(Clone, Debug, PartialEq)]
pub struct RendererCaptureScreenshotRequest {
pub purpose: RendererScreenshotPurpose,
pub format: RendererScreenshotFormat,
pub quality: u8,
pub region: RendererScreenshotRegion,
@@ -197,6 +208,7 @@ pub enum RendererCaptureScreencastFrameReply {
impl RendererCaptureScreenshotRequest {
pub fn viewport_png() -> Self {
Self {
purpose: RendererScreenshotPurpose::Screenshot,
format: RendererScreenshotFormat::Png,
quality: 100,
region: RendererScreenshotRegion::Viewport,
@@ -215,7 +227,11 @@ impl PageVm {
&mut self,
request: RendererCaptureScreenshotRequest,
) -> anyhow::Result<RendererCaptureScreenshotReply> {
self.flush_page_action_window(moli_action_window::ActionBarrier::Screenshot)?;
let barrier = match request.purpose {
RendererScreenshotPurpose::Screenshot => moli_action_window::ActionBarrier::Screenshot,
RendererScreenshotPurpose::Print { .. } => moli_action_window::ActionBarrier::Explicit,
};
self.flush_page_action_window(barrier)?;
let paint_capture = request
.paint_capture_request(self.document_settings.viewport_surface.unwrap_or_default())?;
self.capture_image(
@@ -223,12 +239,34 @@ impl PageVm {
request.quality,
request.optimize_for_speed,
paint_capture,
moli_layout::LayoutFlushReason::Screenshot,
LayoutPublicationRequest::Screen,
match request.purpose {
RendererScreenshotPurpose::Screenshot => moli_layout::LayoutFlushReason::Screenshot,
RendererScreenshotPurpose::Print { .. } => moli_layout::LayoutFlushReason::Print,
},
match request.purpose {
RendererScreenshotPurpose::Screenshot => LayoutPublicationRequest::Screen,
RendererScreenshotPurpose::Print { .. } => LayoutPublicationRequest::Transient,
},
self.capture_style_environment_override(request.purpose),
)
.map(RendererImageCaptureOutcome::into_screenshot_reply)
}
pub(super) fn capture_style_environment_override(
&self,
purpose: RendererScreenshotPurpose,
) -> Option<StyloStyleEnvironment> {
if matches!(purpose, RendererScreenshotPurpose::Print { .. })
&& self.document_settings.emulated_media.media.is_none()
{
let mut print = self.document_settings.emulated_media.clone();
print.media = Some("print".to_owned());
Some(StyloStyleEnvironment::from_emulated_media(&print))
} else {
None
}
}
/// Polls one screencast subscription. Only this request accepts a known
/// visual state and only this reply can report `Unchanged`.
pub(super) fn capture_screencast_frame(
@@ -286,6 +324,7 @@ impl PageVm {
paint_capture,
moli_layout::LayoutFlushReason::Screencast,
LayoutPublicationRequest::Screen,
None,
)? {
RendererImageCaptureOutcome::Captured(image) => image,
RendererImageCaptureOutcome::LayoutDisabled => {
@@ -319,6 +358,7 @@ impl PageVm {
paint_capture: PaintCaptureRequest,
reason: moli_layout::LayoutFlushReason,
publication: LayoutPublicationRequest,
environment_override: Option<StyloStyleEnvironment>,
) -> anyhow::Result<RendererImageCaptureOutcome> {
let profile_enabled = moli_trace::cpu_profile_enabled();
let total_started = profile_enabled.then(Instant::now);
@@ -337,6 +377,7 @@ impl PageVm {
reason,
paint_capture,
publication,
environment_override,
)?
else {
return Ok(RendererImageCaptureOutcome::NoDocument);
@@ -485,7 +526,10 @@ impl RendererCaptureScreenshotRequest {
};
Ok(PaintCaptureRequest {
region,
include_backgrounds: true,
include_backgrounds: match self.purpose {
RendererScreenshotPurpose::Screenshot => true,
RendererScreenshotPurpose::Print { print_background } => print_background,
},
include_viewport_controls,
base_background_color: paint_background_color(self.base_background_color),
max_width: self.max_width,
@@ -62,6 +62,7 @@ mod realtime_capture;
mod realtime_input;
mod realtime_intersection_observer;
mod realtime_opportunity;
mod realtime_print;
mod realtime_resize_observer;
mod scrollbars;
mod text_stroke;
@@ -462,6 +462,7 @@ async fn contenteditable_atomic_inline_selection_keeps_order_effects_and_culling
moli_layout::LayoutFlushReason::Screenshot,
capture,
crate::native_bridge::LayoutPublicationRequest::Screen,
None,
)?
.expect("atomic range effects layout");
let image = moli_paint::raster_snapshot(&snapshot)?;
@@ -0,0 +1,270 @@
use super::realtime_opportunity::run_opportunity;
use super::*;
use crate::runtime::{
RendererCaptureScreencastFrameReply as CastReply, RendererCaptureScreenshotReply,
RendererCaptureScreenshotRequest, RendererScreenshotPurpose,
};
use moli_page_types::LayoutPolicy;
async fn fixture(loader: &crate::network::ResourceRequestClient) -> PageVm {
let (mut page, _, _) = page_vm_with_bound_task_sources_and_owner_wake(
loader,
Url::parse("https://example.com/realtime-print").unwrap(),
);
page.layout_configuration.policy = LayoutPolicy::Realtime;
page.vm_mut().set_layout_policy(LayoutPolicy::Realtime);
page.set_viewport_surface(Some(crate::protocol_types::ViewportSurface {
inner_width: 320,
inner_height: 100,
device_pixel_ratio: 1.0,
..Default::default()
}))
.unwrap();
assert!(
page.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::RenderingUpdate,
loader,
)
.await
.unwrap()
);
page.vm_mut().eval(r#"
document.head.innerHTML=`<style>
html,body { margin:0; background:white }
#target,iframe { position:absolute; left:0; top:0; width:40px; height:40px; border:0 }
#target { background:red } iframe { top:50px }
@media print { #target,iframe { left:160px; width:80px } #target { background:lime } }
</style>`;
document.body.innerHTML='<div id=target></div><iframe id=frame></iframe>';
frame.contentDocument.head.innerHTML='<style>html,body{margin:0}body{height:40px;background:red}@media print{body{background:blue}}</style>';
window.events=[];window.ticks=0;
window.ro=new ResizeObserver(es=>events.push('ro:'+es[0].contentRect.width));ro.observe(target);
window.io=new IntersectionObserver(es=>events.push('io:'+es[0].boundingClientRect.width));io.observe(target);
matchMedia('print').onchange=()=>events.push('media');
frame.contentWindow.matchMedia('print').onchange=()=>events.push('child-media');
window.fontSheet=new CSSStyleSheet();
fontSheet.replaceSync('@media screen{@font-face{font-family:ScreenOnly;src:local(ScreenOnly)}}@media print{@font-face{font-family:PrintOnly;src:local(PrintOnly)}}');
document.adoptedStyleSheets=[fontSheet];
window.liveFonts=document.fonts;window.screenFace=[...liveFonts][0];
"#).unwrap();
run_opportunity(&mut page, loader).await.unwrap();
assert!(
page.run_exact_selected_page_task_for_test(
PageSelectedTaskTestSelector::IntersectionObserver,
loader,
)
.await
.unwrap()
);
assert_eq!(
page.vm_mut().eval("events.join('|')").unwrap(),
"ro:40|io:40"
);
page
}
fn print_request() -> RendererCaptureScreenshotRequest {
RendererCaptureScreenshotRequest {
purpose: RendererScreenshotPurpose::Print {
print_background: true,
},
..RendererCaptureScreenshotRequest::viewport_png()
}
}
fn screen_token(page: &PageVm) -> crate::runtime::RendererVisualStateToken {
page.vm().visual_state_token(
page.document_lifecycle.identity(),
moli_layout::PaintViewport::new(320, 100, 1.0),
[255; 4],
)
}
fn assert_screen_state(page: &mut PageVm) {
assert_eq!(page.vm_mut().eval("JSON.stringify([[...liveFonts].map(face=>face.family),liveFonts.has(screenFace),document.fonts===liveFonts])").unwrap(), r#"[["ScreenOnly"],true,true]"#);
assert_eq!(
page.vm_mut()
.eval(
r#"JSON.stringify([
target.offsetWidth,frame.offsetWidth,frame.contentWindow.innerWidth,
frame.contentDocument.body.offsetWidth,getComputedStyle(target).width,
frame.contentWindow.getComputedStyle(frame.contentDocument.body).backgroundColor,
matchMedia('print').matches,frame.contentWindow.matchMedia('print').matches,
events.join('|')
])"#
)
.unwrap(),
r#"[40,40,40,40,"40px","rgb(255, 0, 0)",false,false,"ro:40|io:40"]"#
);
}
#[test]
fn realtime_print_preserves_screen_observers_and_screencast_without_an_extra_frame() {
run_page_vm_large_stack_async_test("realtime-print-isolation", || async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page = fixture(&loader).await;
let CastReply::Captured(first) = page
.capture_screencast_frame(viewport_screencast_request(None))
.unwrap()
else {
panic!("screen capture");
};
// Screen publication asks existing IO targets to be checked. Settle
// this genuine screen work before attributing any work to printing.
run_opportunity(&mut page, &loader).await.unwrap();
assert_screen_state(&mut page);
assert!(
page.next_rendering_opportunity_deadline_for_test()
.is_none()
);
let builds = page.vm().layout_pass_observability_for_test().1;
let publishes = page.vm().layout_snapshot_cache_observability_for_test().2;
assert_eq!(
screen_token(&page),
first.visual_state,
"clean frame and reads preserve the captured token"
);
let RendererCaptureScreenshotReply::Captured(print) =
page.capture_screenshot(print_request()).unwrap()
else {
panic!("print capture");
};
assert_eq!(
screen_token(&page),
first.visual_state,
"printing preserves the captured screen token"
);
let image = moli_image::decode_png(&print.bytes).unwrap();
let pixel = |x: usize, y: usize| {
let offset = (y * image.width as usize + x) * 4;
&image.rgba[offset..offset + 4]
};
assert_eq!(pixel(20, 20), [255, 255, 255, 255]);
assert_eq!(pixel(230, 20), [0, 255, 0, 255]);
assert_eq!(
pixel(230, 70),
[0, 0, 255, 255],
"child uses print media too"
);
assert_screen_state(&mut page);
assert_eq!(
screen_token(&page),
first.visual_state,
"screen reads after printing preserve the token"
);
assert_eq!(
page.vm().layout_snapshot_cache_observability_for_test().2,
publishes
);
assert_eq!(page.vm().layout_pass_observability_for_test().1, builds + 1);
assert!(
page.next_rendering_opportunity_deadline_for_test()
.is_none()
);
for selector in [
PageSelectedTaskTestSelector::RenderingUpdate,
PageSelectedTaskTestSelector::IntersectionObserver,
] {
assert!(
page.claim_exact_selected_page_task_for_test(selector)
.is_none()
);
}
assert!(matches!(
page.capture_screencast_frame(viewport_screencast_request(Some(first.visual_state)))
.unwrap(),
CastReply::Unchanged
));
assert_eq!(page.vm().layout_pass_observability_for_test().1, builds + 1);
});
}
#[test]
fn realtime_print_success_and_failure_preserve_pending_animation_work() {
run_page_vm_large_stack_async_test("realtime-print-pending", || async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page = fixture(&loader).await;
page.vm_mut()
.eval("requestAnimationFrame(()=>ticks++)")
.unwrap();
let ticket = page.current_realtime_rendering_opportunity().unwrap();
let deadline = page.next_rendering_opportunity_deadline_for_test();
let builds = page.vm().layout_pass_observability_for_test().1;
page.capture_screenshot(print_request()).unwrap();
assert_eq!(page.current_realtime_rendering_opportunity(), Some(ticket));
assert_eq!(
page.next_rendering_opportunity_deadline_for_test(),
deadline
);
assert_eq!(page.vm_mut().eval("String(ticks)").unwrap(), "0");
let host = page.vm().context_host_weak_for_test().upgrade().unwrap();
host.borrow().simulate_active_layout_pass_for_test(true);
let error = page.capture_screenshot(print_request()).unwrap_err();
host.borrow().simulate_active_layout_pass_for_test(false);
assert_eq!(
error.downcast_ref::<moli_layout::LayoutError>(),
Some(&moli_layout::LayoutError::ReentrantLayoutPass)
);
assert_eq!(page.current_realtime_rendering_opportunity(), Some(ticket));
assert_eq!(
page.next_rendering_opportunity_deadline_for_test(),
deadline
);
assert_screen_state(&mut page);
run_opportunity(&mut page, &loader).await.unwrap();
assert_eq!(page.vm_mut().eval("String(ticks)").unwrap(), "1");
assert_eq!(page.vm().layout_pass_observability_for_test().1, builds + 1);
assert!(
page.next_rendering_opportunity_deadline_for_test()
.is_none()
);
assert_screen_state(&mut page);
});
}
#[test]
fn realtime_print_respects_explicit_live_media_and_live_changes_invalidate_screencast() {
run_page_vm_large_stack_async_test("realtime-print-live-media", || async move {
let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default()).unwrap();
let mut page = fixture(&loader).await;
let CastReply::Captured(first) = page
.capture_screencast_frame(viewport_screencast_request(None))
.unwrap()
else {
panic!("screen capture");
};
for (media, x, color) in [
("screen", 20, [255, 0, 0, 255]),
("print", 230, [0, 255, 0, 255]),
] {
let mut emulation = page.document_settings.emulated_media.clone();
emulation.media = Some(media.to_owned());
page.set_emulated_media(&emulation).unwrap();
let RendererCaptureScreenshotReply::Captured(print) =
page.capture_screenshot(print_request()).unwrap()
else {
panic!("explicit-media print capture");
};
let image = moli_image::decode_png(&print.bytes).unwrap();
let offset = (20 * image.width as usize + x) * 4;
assert_eq!(&image.rgba[offset..offset + 4], color);
assert_eq!(
page.vm_mut()
.eval("String(matchMedia('print').matches)")
.unwrap(),
(media == "print").to_string()
);
}
let CastReply::Captured(changed) = page
.capture_screencast_frame(viewport_screencast_request(Some(
first.visual_state.clone(),
)))
.unwrap()
else {
panic!("live media change must invalidate the screen token");
};
assert_ne!(changed.visual_state, first.visual_state);
});
}
@@ -230,6 +230,7 @@ async fn bounded_viewport_clip_keeps_root_controls_while_page_clip_omits_them()
let viewport_clip = capture_screenshot_with_request(
&page,
super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Png,
quality: 100,
@@ -259,6 +260,7 @@ async fn bounded_viewport_clip_keeps_root_controls_while_page_clip_omits_them()
let page_clip = capture_screenshot_with_request(
&page,
super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Png,
quality: 100,
@@ -326,6 +326,7 @@ async fn capture_screenshot_encodes_jpeg_and_limits_device_dimensions() {
.expect("viewport should update");
let request = super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Jpeg,
quality: 80,
@@ -492,6 +493,7 @@ async fn capture_screenshot_clip_and_full_document_keep_the_live_layout_viewport
let full = capture_screenshot_with_request(
&page,
super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Png,
quality: 100,
@@ -509,6 +511,7 @@ async fn capture_screenshot_clip_and_full_document_keep_the_live_layout_viewport
let clip = capture_screenshot_with_request(
&page,
super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Png,
quality: 100,
@@ -542,6 +545,7 @@ async fn capture_screenshot_rejects_full_document_at_the_128k_css_boundary() {
)
.await;
let request = super::RendererCaptureScreenshotRequest {
purpose: crate::runtime::RendererScreenshotPurpose::Screenshot,
base_background_color: [255; 4],
format: super::RendererScreenshotFormat::Png,
quality: 100,
+8 -17
View File
@@ -168,26 +168,11 @@ impl ScriptVm {
reason,
moli_layout::PaintCaptureRequest::viewport(),
publication,
)
}
pub(crate) fn paint_layout_snapshot_with_capture(
&mut self,
viewport: moli_layout::PaintViewport,
reason: moli_layout::LayoutFlushReason,
capture: moli_layout::PaintCaptureRequest,
publication: LayoutPublicationRequest,
) -> anyhow::Result<Option<moli_layout::PaintSnapshot>> {
self.paint_layout_snapshot_with_capture_in_environment(
viewport,
reason,
capture,
publication,
None,
)
}
pub(crate) fn paint_layout_snapshot_with_capture_in_environment(
pub(crate) fn paint_layout_snapshot_with_capture(
&mut self,
viewport: moli_layout::PaintViewport,
reason: moli_layout::LayoutFlushReason,
@@ -235,7 +220,7 @@ impl ScriptVm {
self.with_fresh_layout_pass_in_environment(request, publication, None, consume)
}
fn with_fresh_layout_pass_in_environment<T>(
pub(super) fn with_fresh_layout_pass_in_environment<T>(
&mut self,
request: moli_layout::LayoutPassRequest,
publication: LayoutPublicationRequest,
@@ -244,6 +229,12 @@ impl ScriptVm {
&mut moli_layout::LayoutPassResult<DomHandle>,
) -> Result<T, moli_layout::LayoutError>,
) -> Result<Option<T>, moli_layout::LayoutError> {
if environment_override.is_some() && publication != LayoutPublicationRequest::Transient {
return Err(moli_layout::LayoutError::source_contract(
"capture environment",
"temporary style environments require transient layout publication",
));
}
// Font resources are reconciled by the document resource lifecycle.
// Layout consumes registered data and performs no font loading work.
let requests_paint = request.requests_paint();
@@ -113,3 +113,52 @@ fn realtime_publication_rejects_changed_inputs_without_overwriting_the_screen_tr
assert_eq!(vm.eval("String(target.offsetWidth)").unwrap(), "100");
assert_eq!(vm.layout_pass_observability_for_test().1, before + 2);
}
#[test]
fn realtime_publication_temporary_media_failure_preserves_screen_geometry() {
let (mut vm, viewport) = fixture(
"<!doctype html><style>#target{width:40px}@media print{#target{width:80px}}</style><div id=target></div>",
);
assert_eq!(vm.eval("String(target.offsetWidth)").unwrap(), "40");
let builds = vm.layout_pass_observability_for_test().1;
let publishes = vm.layout_snapshot_cache_observability_for_test().2;
let environment = {
let host = vm._context_host.borrow();
let mut media = host.emulated_media().clone();
media.media = Some("print".to_owned());
crate::style_engine::StyloStyleEnvironment::from_emulated_media(&media)
};
let error = vm
.with_fresh_layout_pass_in_environment::<()>(
LayoutPassRequest::new(viewport, LayoutFlushReason::Print),
LayoutPublicationRequest::Transient,
Some(environment),
|_| {
Err(LayoutError::source_contract(
"test capture",
"output consumption failed",
))
},
)
.unwrap_err();
assert!(matches!(error, LayoutError::SourceContract { .. }));
assert_eq!(vm.eval("JSON.stringify([target.offsetWidth,getComputedStyle(target).width,matchMedia('print').matches])").unwrap(), "[40,\"40px\",false]");
assert_eq!(vm.layout_pass_observability_for_test().1, builds + 1);
assert_eq!(
vm.layout_snapshot_cache_observability_for_test().2,
publishes
);
// A temporary environment must never receive a canonical screen stamp,
// even when a caller provides an ordinary screenshot reason.
let error = vm
.with_fresh_layout_pass_in_environment(
LayoutPassRequest::new(viewport, LayoutFlushReason::Screenshot),
LayoutPublicationRequest::Screen,
Some(environment),
|_| Ok(()),
)
.unwrap_err();
assert!(matches!(error, LayoutError::SourceContract { .. }));
assert_eq!(vm.layout_pass_observability_for_test().1, builds + 1);
}