fix(input): release individual CDP touch contacts

This commit is contained in:
ldm0
2026-10-11 18:06:47 +08:00
parent 08c366892d
commit 5e5f90e0f0
4 changed files with 137 additions and 8 deletions
+7 -7
View File
@@ -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());
+60
View File
@@ -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#"<html><body style='margin:0'>
<button id='btn' style='width:80px;height:80px'>tap</button>
<script>
window.__touchContacts = [];
for (const type of ['touchmove', 'touchend']) {
document.addEventListener(type, e => __touchContacts.push([
type, Array.from(e.touches, t => t.identifier),
Array.from(e.changedTouches, t => t.identifier)
]));
}
</script>
</body></html>"#,
)
.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();
@@ -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
@@ -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);