From ac15bd3d324822f134e89b93e9ecb2ec8076b33b Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 14 Oct 2026 23:57:25 +0800 Subject: [PATCH] fix(renderer): preserve realtime contracts after upstream rebase Route native PDF commands through the existing finite print resource job so Fetch pauses are published before the final reply and the first PDF uses its prepared images and fonts. Share owner admission, overlays, deadlines and cancellation with raster print while retaining native vector PDF encoding. Use transient capture environments for native PDF, charge the upstream IFC processed-text buffer in retained-state budgets, and cover screen/rAF isolation and raster/PDF mutual exclusion. Decode the first PDF's lossless image streams in the interception regression and assert exact RGBA pixels. Update the eviction regression's implicit pointer release ordering to match Chromium's compatibility mouseup before capture release; preserve its target, buttons, explicit-release and current-geometry assertions. Validation with CARGO_BUILD_JOBS=24: - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast: 20,516 passed, 17 existing skipped - print/pointer integration subset: 68 passed --- moli-layout/src/inline.rs | 5 +- moli-layout/src/working_set/numeric_tests.rs | 37 ++++++ .../page/tests/capture/print_resources.rs | 111 ++++++++++++------ moli-renderer-v8/src/capture_resources.rs | 85 +++++++++++++- moli-renderer-v8/src/native_bridge/mod.rs | 5 +- .../src/runtime/owner/page_commands.rs | 10 +- moli-renderer-v8/src/runtime/page_print.rs | 18 ++- .../src/runtime/page_print_capture.rs | 52 +++++--- .../src/runtime/page_screenshot.rs | 2 +- moli-renderer-v8/src/runtime/page_state.rs | 2 +- .../runtime/page_state/print_capture_tests.rs | 10 +- .../tests/rendering_update/realtime_print.rs | 55 ++++++++- .../tests/dom_xhr/dom/pointer_and_mouse.rs | 4 +- 13 files changed, 315 insertions(+), 81 deletions(-) diff --git a/moli-layout/src/inline.rs b/moli-layout/src/inline.rs index e87a6e31c0..eacb183d23 100644 --- a/moli-layout/src/inline.rs +++ b/moli-layout/src/inline.rs @@ -550,7 +550,10 @@ impl InlineFormattingContext { fn allocation(values: &Vec) -> usize { values.capacity().saturating_mul(std::mem::size_of::()) } - let mut bytes = allocation(&self.text_units) + let mut bytes = self + .text + .capacity() + .saturating_add(allocation(&self.text_units)) .saturating_add(allocation(&self.source_map)) .saturating_add(allocation(&self.objects)) .saturating_add(allocation(&self.font_metrics)) diff --git a/moli-layout/src/working_set/numeric_tests.rs b/moli-layout/src/working_set/numeric_tests.rs index a7723b9a94..7d9b8e25d5 100644 --- a/moli-layout/src/working_set/numeric_tests.rs +++ b/moli-layout/src/working_set/numeric_tests.rs @@ -36,6 +36,7 @@ fn numeric_world( world.inline_numeric_reuse = Some(Box::new(numeric)); world.prepare_inline_storage(); world.inline_layouts[owner.index()] = Some(InlineFormattingContext { + text: String::new(), root_style: owner, measurement_layout: Some(parley::Layout::default()), laid_out: accepted.then(parley::Layout::default), @@ -59,6 +60,42 @@ fn numeric_world( world } +#[test] +fn retained_processed_text_capacity_counts_towards_admission() { + let fonts = DocumentLayoutServices::new().font_inputs(); + let mut baseline = LayoutWorkingSet::default(); + baseline.publish( + numeric_world(0, true, taffy::Style::default()), + fonts.clone(), + 0, + 0, + 1, + ); + let baseline_stats = baseline.stats(); + let mut work = LayoutWorkingSet::new(LayoutWorkingSetLimits { + max_estimated_retained_bytes: baseline_stats.estimated_retained_bytes + 1024, + ..LayoutWorkingSetLimits::default() + }); + let mut pass = numeric_world(0, true, taffy::Style::default()); + let text = &mut pass.inline_layouts[0].as_mut().unwrap().text; + *text = String::with_capacity(8192); + let owned_text_bytes = text.capacity(); + assert!(text.is_empty()); + work.publish(pass, fonts, 0, 0, 1); + let stats = work.stats(); + assert_eq!( + stats.admission, + LayoutWorkingSetAdmission::EstimatedByteLimit + ); + assert_eq!(stats.retained_boxes, 0); + assert_eq!(stats.estimated_retained_bytes, 0); + assert_eq!( + stats.estimated_inline_bytes, + baseline_stats.estimated_inline_bytes + owned_text_bytes + ); + assert!(work.retained.is_none()); +} + #[test] fn retained_numeric_output_capacity_counts_towards_admission() { let fonts = DocumentLayoutServices::new().font_inputs(); diff --git a/moli-protocol/src/domains/page/tests/capture/print_resources.rs b/moli-protocol/src/domains/page/tests/capture/print_resources.rs index be8dc6191d..f33a7462c1 100644 --- a/moli-protocol/src/domains/page/tests/capture/print_resources.rs +++ b/moli-protocol/src/domains/page/tests/capture/print_resources.rs @@ -171,21 +171,15 @@ async fn print_to_pdf_publishes_fetch_pause_before_reply_and_uses_first_resource let pdf = screenshot_bytes(response); assert!(pdf.starts_with(b"%PDF-1.7")); assert!(pdf.ends_with(b"%%EOF\n")); - let image = first_pdf_jpeg(&pdf); + let image = first_pdf_image(&pdf); assert!(image.width >= 48 && image.height >= 28); // A missing resource paints magenta; only the continued PNG supplies - // this blue. Inspect the first PDF's JPEG, not a second capture. + // this blue. Inspect the first PDF's image, not a second capture. for y in 16..24 { for x in 16..32 { let index = ((y * image.width + x) * 4) as usize; let pixel = &image.rgba[index..index + 4]; - for (actual, expected) in pixel[..3].iter().zip([17_u8, 85, 221]) { - assert!( - actual.abs_diff(expected) <= 10, - "pixel ({x}, {y})={pixel:?}" - ); - } - assert_eq!(pixel[3], 255); + assert_eq!(pixel, [17, 85, 221, 255], "pixel ({x}, {y})"); } } assert_eq!(image_requests.load(Ordering::SeqCst), 1); @@ -200,34 +194,79 @@ async fn print_to_pdf_publishes_fetch_pause_before_reply_and_uses_first_resource result.expect("bounded public Print/Fetch integration"); } -// The existing raster-PDF serializer stores the captured JPEG as one image -// XObject with an explicit byte length. Decode that payload rather than use a -// PDF magic-header check as evidence that the print resource was painted. -fn first_pdf_jpeg(pdf: &[u8]) -> moli_image::RgbaImage { - let marker = b"/Subtype /Image"; - let dictionary_start = pdf - .windows(marker.len()) - .position(|part| part == marker) - .unwrap(); - let stream_marker = b"\nstream\n"; - let dictionary_end = dictionary_start - + pdf[dictionary_start..] - .windows(stream_marker.len()) - .position(|part| part == stream_marker) - .unwrap(); - let dictionary = std::str::from_utf8(&pdf[dictionary_start..dictionary_end]).unwrap(); - assert!(dictionary.contains("/Filter /DCTDecode")); - let length = dictionary - .split_once("/Length ") +// Native PDF stores the bounded repeated-background raster as an image +// XObject. Decode the first PDF's lossless color and alpha streams so this +// still proves resource pixels were painted, independently of serialization. +fn first_pdf_image(pdf: &[u8]) -> moli_image::RgbaImage { + let document = lopdf::Document::load_mem(pdf).expect("valid first PDF"); + let images = document + .objects + .values() + .filter_map(|object| object.as_stream().ok()) + .filter(|stream| { + stream + .dict + .get(b"Subtype") + .and_then(|value| value.as_name()) + .ok() + == Some(b"Image") + && stream + .dict + .get(b"ColorSpace") + .and_then(|value| value.as_name()) + .ok() + != Some(b"DeviceGray") + }) + .collect::>(); + assert_eq!(images.len(), 1, "one printed background image"); + let image = images[0]; + assert_eq!( + image + .dict + .get(b"BitsPerComponent") + .unwrap() + .as_i64() + .unwrap(), + 8 + ); + let width: u32 = image + .dict + .get(b"Width") .unwrap() - .1 - .split_whitespace() - .next() + .as_i64() .unwrap() - .parse::() + .try_into() .unwrap(); - let start = dictionary_end + stream_marker.len(); - let end = start.checked_add(length).unwrap(); - assert_eq!(&pdf[end..end + b"\nendstream".len()], b"\nendstream"); - moli_image::decode_jpeg(&pdf[start..end]).expect("the first PDF's image stream must decode") + let height: u32 = image + .dict + .get(b"Height") + .unwrap() + .as_i64() + .unwrap() + .try_into() + .unwrap(); + let rgb = image + .decompressed_content() + .expect("decode first PDF's color stream"); + let pixel_count = width as usize * height as usize; + assert_eq!(rgb.len(), pixel_count * 3); + let alpha = if let Ok(mask) = image.dict.get(b"SMask") { + document + .dereference(mask) + .unwrap() + .1 + .as_stream() + .unwrap() + .decompressed_content() + .expect("decode first PDF's alpha stream") + } else { + vec![255; pixel_count] + }; + assert_eq!(alpha.len(), pixel_count); + let mut rgba = Vec::with_capacity(pixel_count * 4); + for (color, alpha) in rgb.as_chunks::<3>().0.iter().zip(alpha) { + rgba.extend_from_slice(color); + rgba.push(alpha); + } + moli_image::RgbaImage::try_new(width, height, rgba).unwrap() } diff --git a/moli-renderer-v8/src/capture_resources.rs b/moli-renderer-v8/src/capture_resources.rs index 32f77ee2fc..40ba63d389 100644 --- a/moli-renderer-v8/src/capture_resources.rs +++ b/moli-renderer-v8/src/capture_resources.rs @@ -17,7 +17,8 @@ use crate::{ native_bridge::{CaptureReadyImage, WindowDocumentTaskTarget}, runtime::{ RendererCaptureScreenshotReply, RendererCaptureScreenshotRequest, - RendererDocumentLifecycleIdentity, RendererScreenshotPurpose, + RendererDocumentLifecycleIdentity, RendererPageReply, RendererPrintToPdfReply, + RendererPrintToPdfRequest, RendererScreenshotPurpose, }, style_engine::StyloStyleEnvironment, }; @@ -173,8 +174,81 @@ pub(crate) struct PrintCaptureContext { pub(crate) environment_override: Option, } +pub(crate) enum PrintCaptureRequest { + Raster(RendererCaptureScreenshotRequest), + Pdf(RendererPrintToPdfRequest), +} + +pub(crate) enum PrintCaptureReply { + Raster(RendererCaptureScreenshotReply), + Pdf(RendererPrintToPdfReply), +} + +impl From for RendererPageReply { + fn from(reply: PrintCaptureReply) -> Self { + match reply { + PrintCaptureReply::Raster(reply) => Self::CaptureScreenshot(reply), + PrintCaptureReply::Pdf(reply) => Self::PrintToPdf(reply), + } + } +} + +impl From for PrintCaptureRequest { + fn from(request: RendererCaptureScreenshotRequest) -> Self { + Self::Raster(request) + } +} + +impl From for PrintCaptureRequest { + fn from(request: RendererPrintToPdfRequest) -> Self { + Self::Pdf(request) + } +} + +impl PrintCaptureRequest { + pub(crate) fn purpose(&self) -> RendererScreenshotPurpose { + match self { + Self::Raster(request) => request.purpose, + Self::Pdf(request) => RendererScreenshotPurpose::Print { + print_background: request.print_background, + }, + } + } + + pub(crate) fn paint_capture_request( + &self, + surface: moli_page_types::ViewportSurface, + ) -> anyhow::Result { + match self { + Self::Raster(request) => request.paint_capture_request(surface), + Self::Pdf(request) => Ok(moli_layout::PaintCaptureRequest { + include_backgrounds: request.print_background, + ..moli_layout::PaintCaptureRequest::print_document() + }), + } + } + + pub(crate) fn layout_disabled_reply(&self) -> PrintCaptureReply { + match self { + Self::Raster(_) => { + PrintCaptureReply::Raster(RendererCaptureScreenshotReply::LayoutDisabled) + } + Self::Pdf(_) => PrintCaptureReply::Pdf(RendererPrintToPdfReply::LayoutDisabled), + } + } + + pub(crate) fn no_document_reply(&self) -> PrintCaptureReply { + match self { + Self::Raster(_) => { + PrintCaptureReply::Raster(RendererCaptureScreenshotReply::NoDocument) + } + Self::Pdf(_) => PrintCaptureReply::Pdf(RendererPrintToPdfReply::NoDocument), + } + } +} + pub(crate) struct PrintCaptureJob { - pub(crate) request: RendererCaptureScreenshotRequest, + pub(crate) request: PrintCaptureRequest, deadline: Instant, pub(crate) context: Option, pub(crate) members: Vec, @@ -187,9 +261,10 @@ pub(crate) struct PrintCaptureJob { } impl PrintCaptureJob { - pub(crate) fn new(request: RendererCaptureScreenshotRequest) -> anyhow::Result { + pub(crate) fn new(request: impl Into) -> anyhow::Result { + let request = request.into(); anyhow::ensure!( - matches!(request.purpose, RendererScreenshotPurpose::Print { .. }), + matches!(request.purpose(), RendererScreenshotPurpose::Print { .. }), "print preparation requires a Print capture" ); let deadline = Instant::now() @@ -240,7 +315,7 @@ impl Drop for PrintCaptureJob { pub(crate) enum PrintCaptureAdvance { Waiting, - Complete(RendererCaptureScreenshotReply), + Complete(PrintCaptureReply), } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs index 3a4984f168..ff26169a7e 100644 --- a/moli-renderer-v8/src/native_bridge/mod.rs +++ b/moli-renderer-v8/src/native_bridge/mod.rs @@ -7,8 +7,9 @@ mod collections; mod context_host; mod current_input; pub(crate) use context_host::{ - JsContextHost, JsContextHostPageTaskCapabilities, LayoutPublicationRequest, MouseReleaseFollowUp, - PendingScrollObservableEffects, PostParseAutofocusAdmission, ServiceWorkerWindowOwner, + JsContextHost, JsContextHostPageTaskCapabilities, LayoutPublicationRequest, + MouseReleaseFollowUp, PendingScrollObservableEffects, PostParseAutofocusAdmission, + ServiceWorkerWindowOwner, }; pub(crate) use current_input::{ CurrentInputEvent, CurrentInputEventScope, InputNavigationPolicy, navigation_policy_from_event, diff --git a/moli-renderer-v8/src/runtime/owner/page_commands.rs b/moli-renderer-v8/src/runtime/owner/page_commands.rs index 6e87488faa..5d84981594 100644 --- a/moli-renderer-v8/src/runtime/owner/page_commands.rs +++ b/moli-renderer-v8/src/runtime/owner/page_commands.rs @@ -11,7 +11,11 @@ impl RendererOwnerHandle { RendererPageCommand::CaptureScreenshot(request) if matches!(request.purpose, RendererScreenshotPurpose::Print { .. }) => { - self.begin_live_page_print_capture_turn(token, request, capture_policy) + self.begin_live_page_print_capture_turn(token, request.into(), capture_policy) + .await + } + RendererPageCommand::PrintToPdf(request) => { + self.begin_live_page_print_capture_turn(token, request.into(), capture_policy) .await } RendererPageCommand::EvaluateExpression { @@ -180,7 +184,7 @@ impl RendererOwnerHandle { async fn begin_live_page_print_capture_turn( &self, token: RendererPageToken, - request: RendererCaptureScreenshotRequest, + request: crate::capture_resources::PrintCaptureRequest, capture_policy: super::RendererPageStateCapturePolicy, ) -> RenderRuntimeDispatchOutcome { let entry = match take_entry_for_command_on_bound_owner_local_store(token) { @@ -290,7 +294,7 @@ impl RendererOwnerHandle { self.complete_live_page_command_turn( token, entry, - RendererPageReply::CaptureScreenshot(reply), + reply.into(), false, command_output_cursor, replacement_lifecycle, diff --git a/moli-renderer-v8/src/runtime/page_print.rs b/moli-renderer-v8/src/runtime/page_print.rs index 44acccc45d..1738fddbf9 100644 --- a/moli-renderer-v8/src/runtime/page_print.rs +++ b/moli-renderer-v8/src/runtime/page_print.rs @@ -1,6 +1,8 @@ //! Native PDF requests, replies and isolated print-layout lifecycle. use super::PageVm; +use crate::native_bridge::LayoutPublicationRequest; +use crate::runtime::RendererScreenshotPurpose; use moli_layout::{PaintCaptureRequest, PaintViewport}; use moli_page_types::LayoutPolicy; @@ -33,13 +35,10 @@ impl PageVm { if self.layout_configuration.policy == LayoutPolicy::Mock { return Ok(Reply::LayoutDisabled); } - let previous = self.document_settings.emulated_media.clone(); - let restore = previous.media.is_none(); - if restore { - let mut print = previous.clone(); - print.media = Some("print".to_owned()); - self.set_emulated_media(&print)?; - } + let environment_override = + self.capture_style_environment_override(RendererScreenshotPurpose::Print { + print_background: request.print_background, + }); let surface = self.document_settings.viewport_surface.unwrap_or_default(); let viewport = PaintViewport::new( surface.inner_width, @@ -53,10 +52,9 @@ impl PageVm { include_backgrounds: request.print_background, ..PaintCaptureRequest::print_document() }, + LayoutPublicationRequest::Transient, + environment_override, ); - if restore { - self.set_emulated_media(&previous)?; - } Ok(match result? { Some(snapshot) => match moli_paint::pdf::pdf_snapshot(&snapshot, &request.options) { Ok(bytes) => Reply::Printed(bytes), diff --git a/moli-renderer-v8/src/runtime/page_print_capture.rs b/moli-renderer-v8/src/runtime/page_print_capture.rs index a8ddbc24c6..88e31076ec 100644 --- a/moli-renderer-v8/src/runtime/page_print_capture.rs +++ b/moli-renderer-v8/src/runtime/page_print_capture.rs @@ -10,12 +10,14 @@ use crate::{ CaptureResourceSpec, CaptureResourceState, MAX_CAPTURE_DISCOVERY_WAVES, MAX_CAPTURE_FONT_DECODED_BYTES, MAX_CAPTURE_RESOURCE_CONCURRENCY, MAX_CAPTURE_RESOURCE_ENTRIES, MAX_CAPTURE_TOTAL_FONT_DECODED_BYTES, PrintCaptureAdvance, - PrintCaptureContext, PrintCaptureJob, + PrintCaptureContext, PrintCaptureJob, PrintCaptureReply, PrintCaptureRequest, }, style_engine::StyloStyleEnvironment, }; -use super::{PageVm, RendererCaptureScreenshotReply, RendererScreenshotPurpose}; +use super::{ + PageVm, RendererCaptureScreenshotReply, RendererPrintToPdfReply, RendererScreenshotPurpose, +}; impl PageVm { /// One synchronous step. Pending network/decode work is always returned to @@ -30,7 +32,7 @@ impl PageVm { ); if !self.layout_configuration.policy.uses_real_layout() { return Ok(PrintCaptureAdvance::Complete( - RendererCaptureScreenshotReply::LayoutDisabled, + job.request.layout_disabled_reply(), )); } @@ -38,7 +40,7 @@ impl PageVm { self.flush_page_action_window(moli_action_window::ActionBarrier::Explicit)?; let Some(context) = self.current_print_capture_context(job)? else { return Ok(PrintCaptureAdvance::Complete( - RendererCaptureScreenshotReply::NoDocument, + job.request.no_document_reply(), )); }; // Validate clip/output options before admitting any network work. @@ -102,7 +104,7 @@ impl PageVm { )? else { return Ok(PrintCaptureAdvance::Complete( - RendererCaptureScreenshotReply::NoDocument, + job.request.no_document_reply(), )); }; let actual_viewports = actual @@ -130,18 +132,27 @@ impl PageVm { let layout_us = profile_started .map(|started| started.elapsed().as_micros()) .unwrap_or_default(); - let image = Self::encode_capture_snapshot( - &snapshot, - job.request.format, - job.request.quality, - job.request.optimize_for_speed, - LayoutFlushReason::Print, - layout_us, - profile_started, - )?; - return Ok(PrintCaptureAdvance::Complete( - RendererCaptureScreenshotReply::Captured(image), - )); + let reply = match &job.request { + PrintCaptureRequest::Raster(request) => { + let image = Self::encode_capture_snapshot( + &snapshot, + request.format, + request.quality, + request.optimize_for_speed, + LayoutFlushReason::Print, + layout_us, + profile_started, + )?; + PrintCaptureReply::Raster(RendererCaptureScreenshotReply::Captured(image)) + } + PrintCaptureRequest::Pdf(request) => PrintCaptureReply::Pdf( + match moli_paint::pdf::pdf_snapshot(&snapshot, &request.options) { + Ok(bytes) => RendererPrintToPdfReply::Printed(bytes), + Err(error) => RendererPrintToPdfReply::Failed(error.to_string()), + }, + ), + }; + return Ok(PrintCaptureAdvance::Complete(reply)); } } @@ -150,7 +161,10 @@ impl PageVm { job: &PrintCaptureJob, ) -> Result> { ensure!( - matches!(job.request.purpose, RendererScreenshotPurpose::Print { .. }), + matches!( + job.request.purpose(), + RendererScreenshotPurpose::Print { .. } + ), "print preparation requires a Print capture" ); let Some(identity) = self.vm().current_capture_layout_identity() else { @@ -170,7 +184,7 @@ impl PageVm { live_environment: StyloStyleEnvironment::from_emulated_media( &self.document_settings.emulated_media, ), - environment_override: self.capture_style_environment_override(job.request.purpose), + environment_override: self.capture_style_environment_override(job.request.purpose()), })) } diff --git a/moli-renderer-v8/src/runtime/page_screenshot.rs b/moli-renderer-v8/src/runtime/page_screenshot.rs index 500f7471a3..a272999364 100644 --- a/moli-renderer-v8/src/runtime/page_screenshot.rs +++ b/moli-renderer-v8/src/runtime/page_screenshot.rs @@ -519,7 +519,7 @@ fn visual_state_for_captured_screencast_frame( } impl RendererCaptureScreenshotRequest { - pub(super) fn paint_capture_request( + pub(crate) fn paint_capture_request( &self, surface: ViewportSurface, ) -> anyhow::Result { diff --git a/moli-renderer-v8/src/runtime/page_state.rs b/moli-renderer-v8/src/runtime/page_state.rs index 5f06e29788..e0e178e4d4 100644 --- a/moli-renderer-v8/src/runtime/page_state.rs +++ b/moli-renderer-v8/src/runtime/page_state.rs @@ -226,7 +226,7 @@ impl RendererPageSlotHandle { pub(super) fn begin_print_capture( &self, - request: RendererCaptureScreenshotRequest, + request: impl Into, ) -> Result { let mut entry = self.inner.lock(); ensure!( diff --git a/moli-renderer-v8/src/runtime/page_state/print_capture_tests.rs b/moli-renderer-v8/src/runtime/page_state/print_capture_tests.rs index f25af315e3..48c284fbdc 100644 --- a/moli-renderer-v8/src/runtime/page_state/print_capture_tests.rs +++ b/moli-renderer-v8/src/runtime/page_state/print_capture_tests.rs @@ -66,6 +66,11 @@ fn print_admission_survives_replacement_view_until_its_job_is_dropped() { assert_eq!(same_slot.entry().vm_creation_id(), 12); same_slot.validate_print_capture(&first).unwrap(); assert!(same_slot.begin_print_capture(print_request()).is_err()); + assert!( + same_slot + .begin_print_capture(RendererPrintToPdfRequest::default()) + .is_err() + ); let unadmitted = PrintCaptureJob::new(print_request()).unwrap(); assert!(same_slot.validate_print_capture(&unadmitted).is_err()); @@ -76,8 +81,11 @@ fn print_admission_survives_replacement_view_until_its_job_is_dropped() { drop(first); assert!(old_mailbox.upgrade().is_none()); assert!(!old_endpoint.is_active()); - let second = same_slot.begin_print_capture(print_request()).unwrap(); + let second = same_slot + .begin_print_capture(RendererPrintToPdfRequest::default()) + .unwrap(); slot.validate_print_capture(&second).unwrap(); + assert!(same_slot.begin_print_capture(print_request()).is_err()); assert!(!old_endpoint.deliver( 0, CaptureResourceResult::Failed("late old producer".to_owned()), diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_print.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_print.rs index 10a7a6593a..70b7eb6b6f 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_print.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/realtime_print.rs @@ -2,7 +2,8 @@ use super::realtime_opportunity::run_opportunity; use super::*; use crate::runtime::{ RendererCaptureScreencastFrameReply as CastReply, RendererCaptureScreenshotReply, - RendererCaptureScreenshotRequest, RendererScreenshotPurpose, + RendererCaptureScreenshotRequest, RendererPrintToPdfReply, RendererPrintToPdfRequest, + RendererScreenshotPurpose, }; use moli_page_types::LayoutPolicy; @@ -219,6 +220,58 @@ fn realtime_print_success_and_failure_preserve_pending_animation_work() { }); } +#[test] +fn realtime_native_pdf_preserves_screen_state_and_pending_animation_work() { + run_page_vm_large_stack_async_test("realtime-native-pdf-isolation", || 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 token = screen_token(&page); + let publishes = page.vm().layout_snapshot_cache_observability_for_test().2; + let builds = page.vm().layout_pass_observability_for_test().1; + + let RendererPrintToPdfReply::Printed(bytes) = + page.print_to_pdf(Default::default()).unwrap() + else { + panic!("native PDF capture"); + }; + assert!( + !lopdf::Document::load_mem(&bytes) + .unwrap() + .get_pages() + .is_empty() + ); + let mut invalid = RendererPrintToPdfRequest::default(); + invalid.options.width = 0.0; + assert!(matches!( + page.print_to_pdf(invalid).unwrap(), + RendererPrintToPdfReply::Failed(_) + )); + + 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"); + assert_eq!(screen_token(&page), token); + assert_screen_state(&mut page); + assert_eq!( + page.vm().layout_snapshot_cache_observability_for_test().2, + publishes + ); + assert_eq!(page.vm().layout_pass_observability_for_test().1, builds + 2); + 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 + 2); + 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 { diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/pointer_and_mouse.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/pointer_and_mouse.rs index 70202fa226..5fe1940fe7 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/pointer_and_mouse.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom/pointer_and_mouse.rs @@ -1268,7 +1268,9 @@ fn realtime_pointer_capture_survives_geometry_changes_and_working_set_eviction() let expected = if explicit_release { "pointerdown@captured:true:1|gotpointercapture@captured:true:1|pointermove@captured:true:1|lostpointercapture@captured:false:0|pointerup@destination:false:0|mouseup@destination:false:0|pointermove@destination:false:0" } else { - "pointerdown@captured:true:1|gotpointercapture@captured:true:1|pointermove@captured:true:1|pointerup@captured:true:0|lostpointercapture@captured:false:0|mouseup@captured:false:0|pointermove@destination:false:0" + // Chromium sends the compatibility mouseup before implicitly + // releasing capture, then processes lost capture before click. + "pointerdown@captured:true:1|gotpointercapture@captured:true:1|pointermove@captured:true:1|pointerup@captured:true:0|mouseup@captured:true:0|lostpointercapture@captured:false:0|pointermove@destination:false:0" }; assert_eq!( vm.eval("__captureLog.join('|')").unwrap(),