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
This commit is contained in:
ldm0
2026-10-14 23:57:25 +08:00
parent b985af6fc5
commit ac15bd3d32
13 changed files with 315 additions and 81 deletions
+4 -1
View File
@@ -550,7 +550,10 @@ impl InlineFormattingContext {
fn allocation<T>(values: &Vec<T>) -> usize {
values.capacity().saturating_mul(std::mem::size_of::<T>())
}
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))
@@ -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();
@@ -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::<Vec<_>>();
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::<usize>()
.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()
}
+80 -5
View File
@@ -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<StyloStyleEnvironment>,
}
pub(crate) enum PrintCaptureRequest {
Raster(RendererCaptureScreenshotRequest),
Pdf(RendererPrintToPdfRequest),
}
pub(crate) enum PrintCaptureReply {
Raster(RendererCaptureScreenshotReply),
Pdf(RendererPrintToPdfReply),
}
impl From<PrintCaptureReply> for RendererPageReply {
fn from(reply: PrintCaptureReply) -> Self {
match reply {
PrintCaptureReply::Raster(reply) => Self::CaptureScreenshot(reply),
PrintCaptureReply::Pdf(reply) => Self::PrintToPdf(reply),
}
}
}
impl From<RendererCaptureScreenshotRequest> for PrintCaptureRequest {
fn from(request: RendererCaptureScreenshotRequest) -> Self {
Self::Raster(request)
}
}
impl From<RendererPrintToPdfRequest> 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<moli_layout::PaintCaptureRequest> {
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<PrintCaptureContext>,
pub(crate) members: Vec<CaptureLayoutIdentity>,
@@ -187,9 +261,10 @@ pub(crate) struct PrintCaptureJob {
}
impl PrintCaptureJob {
pub(crate) fn new(request: RendererCaptureScreenshotRequest) -> anyhow::Result<Self> {
pub(crate) fn new(request: impl Into<PrintCaptureRequest>) -> anyhow::Result<Self> {
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)]
+3 -2
View File
@@ -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,
@@ -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,
+8 -10
View File
@@ -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),
@@ -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<Option<PrintCaptureContext>> {
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()),
}))
}
@@ -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<PaintCaptureRequest> {
+1 -1
View File
@@ -226,7 +226,7 @@ impl RendererPageSlotHandle {
pub(super) fn begin_print_capture(
&self,
request: RendererCaptureScreenshotRequest,
request: impl Into<crate::capture_resources::PrintCaptureRequest>,
) -> Result<crate::capture_resources::PrintCaptureJob> {
let mut entry = self.inner.lock();
ensure!(
@@ -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()),
@@ -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 {
@@ -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(),