diff --git a/moli-protocol/src/domains/input.rs b/moli-protocol/src/domains/input.rs index 2b37ce0551..4e99ce69de 100644 --- a/moli-protocol/src/domains/input.rs +++ b/moli-protocol/src/domains/input.rs @@ -416,13 +416,13 @@ fn validate_coordinate_input_params( if cdp_modifiers(params.modifiers).is_err() { return Err(PendingInputCommandStartError::invalid_params()); } - let valid_point_count = - match params.r#type { - CdpDispatchTouchEventType::TouchStart - | CdpDispatchTouchEventType::TouchMove => !params.touch_points.is_empty(), - CdpDispatchTouchEventType::TouchEnd - | CdpDispatchTouchEventType::TouchCancel => params.touch_points.is_empty(), - }; + let valid_point_count = match params.r#type { + CdpDispatchTouchEventType::TouchStart | CdpDispatchTouchEventType::TouchMove => { + !params.touch_points.is_empty() + } + CdpDispatchTouchEventType::TouchEnd => true, + CdpDispatchTouchEventType::TouchCancel => params.touch_points.is_empty(), + }; let point_ids_are_consistent = params.touch_points.is_empty() || params.touch_points.iter().all(|point| point.id.is_some()) || params.touch_points.iter().all(|point| point.id.is_none()); diff --git a/moli-protocol/src/domains/input/tests.rs b/moli-protocol/src/domains/input/tests.rs index 8195ef2809..be662b1fc7 100644 --- a/moli-protocol/src/domains/input/tests.rs +++ b/moli-protocol/src/domains/input/tests.rs @@ -1878,6 +1878,66 @@ async fn coordinate_touch_commands_complete_through_pending_layout_dispatch() { ); } +#[tokio::test(flavor = "multi_thread")] +async fn coordinate_touch_end_releases_only_the_requested_contact() { + let mut ctx = TestContext::new(); + with_loaded_document( + &mut ctx, + r#"
+ + + "#, + ) + .await; + ctx.capture_fixture_layout(None).await; + + for (id, event_type, touch_points) in [ + ( + 4110, + "touchStart", + json!([ + { "id": 11, "x": INPUT_HIT_X, "y": INPUT_HIT_Y }, + { "id": 12, "x": INPUT_HIT_X + 4, "y": INPUT_HIT_Y } + ]), + ), + ( + 4111, + "touchEnd", + json!([{ "id": 11, "x": INPUT_HIT_X, "y": INPUT_HIT_Y }]), + ), + ( + 4112, + "touchMove", + json!([{ "id": 12, "x": INPUT_HIT_X + 8, "y": INPUT_HIT_Y }]), + ), + ( + 4113, + "touchEnd", + json!([{ "id": 12, "x": INPUT_HIT_X + 8, "y": INPUT_HIT_Y }]), + ), + ] { + ctx.process_async(json!({ + "id": id, + "method": "Input.dispatchTouchEvent", + "params": { "type": event_type, "touchPoints": touch_points } + })) + .await; + ctx.expect_result(id, json!({}), None); + } + assert_eq!( + evaluate_string(&mut ctx, "JSON.stringify(__touchContacts)").await, + r#"[["touchend",[12],[11]],["touchmove",[12],[12]],["touchend",[],[12]]]"# + ); +} + #[tokio::test(flavor = "multi_thread")] async fn coordinate_drag_event_completes_through_pending_layout_dispatch() { let mut ctx = TestContext::new(); diff --git a/moli-renderer-v8/src/script_vm/input_dispatch.rs b/moli-renderer-v8/src/script_vm/input_dispatch.rs index 77d9f2a1cf..5d02da7797 100644 --- a/moli-renderer-v8/src/script_vm/input_dispatch.rs +++ b/moli-renderer-v8/src/script_vm/input_dispatch.rs @@ -1708,7 +1708,7 @@ impl ScriptVm { let ending_points = self.active_touch_contact_samples(); return self.dispatch_touch_changed_event(&ending_points, event_name, false); } - let points = if event_name == "touchend" { + let points = if event_name == "touchend" && points.is_empty() { self.active_touch_contact_samples() } else { // CDP orders changed contacts by identifier, independently of the diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/pointer_capture.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/pointer_capture.rs index fe3a4f71b0..5cc8ad4eef 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/pointer_capture.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/pointer_capture.rs @@ -303,6 +303,75 @@ fn contact_updates_preserve_existing_touches_and_emit_only_changed_contacts() { } } +#[test] +fn ending_contact_updates_preserves_other_touches_and_their_capture() { + for first_id in [0, 11] { + let mut vm = new_touch_contact_test_vm(); + vm.eval( + r#" + globalThis.__touchEnds = []; + globalThis.__touchPointers = {}; + document.addEventListener('pointerdown', e => __touchPointers[e.target.id] = e.pointerId); + for (const type of ['touchmove', 'touchend']) { + document.addEventListener(type, e => __touchEnds.push({ + type, target:e.target.id, + touches:Array.from(e.touches, t => t.identifier), + targetTouches:Array.from(e.targetTouches, t => t.identifier), + changed:Array.from(e.changedTouches, t => t.identifier) + })); + } + 'ready' + "#, + ).unwrap(); + let first = crate::runtime::RendererTouchPoint { + id: first_id, + x: 80.0, + y: 80.0, + }; + let second = crate::runtime::RendererTouchPoint { + id: first_id + 1, + x: 260.0, + y: 80.0, + }; + vm.dispatch_touch_contact_updates(&[second, first], "touchstart") + .unwrap(); + vm.dispatch_touch_contact_updates(&[first], "touchend") + .unwrap(); + assert_eq!( + vm.eval( + r#"(() => { + const first = document.getElementById('first'); + const second = document.getElementById('second'); + let retired = false; + try { first.releasePointerCapture(__touchPointers.first); } + catch (e) { retired = e instanceof DOMException && e.name === 'NotFoundError'; } + return retired && !first.hasPointerCapture(__touchPointers.first) + && second.hasPointerCapture(__touchPointers.second); + })()"#, + ) + .unwrap(), + "true" + ); + let moved_second = crate::runtime::RendererTouchPoint { x: 264.0, ..second }; + vm.dispatch_touch_contact_updates(&[moved_second], "touchmove") + .unwrap(); + vm.dispatch_touch_contact_updates(&[moved_second], "touchend") + .unwrap(); + assert_eq!( + vm.eval("JSON.stringify(__touchEnds)").unwrap(), + format!( + r#"[{{"type":"touchend","target":"first","touches":[{second_id}],"targetTouches":[],"changed":[{first_id}]}},{{"type":"touchmove","target":"second","touches":[{second_id}],"targetTouches":[{second_id}],"changed":[{second_id}]}},{{"type":"touchend","target":"second","touches":[],"targetTouches":[],"changed":[{second_id}]}}]"#, + second_id = first_id + 1 + ) + ); + assert_eq!( + vm.eval("document.getElementById('second').hasPointerCapture(__touchPointers.second)") + .unwrap(), + "false" + ); + } +} + #[test] fn contact_updates_on_move_can_start_an_additional_contact() { let mut vm = new_pointer_activity_test_vm("touch", false, 2);