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(),