1use anyhow::{anyhow, Context, Result};
18use serde_json::{json, Value};
19
20use crate::cdp::CdpClient;
21use crate::dom::scripts::SELECT_ALL_JS;
22use crate::errors::{is_cdp_node_gone, SessionError};
23use crate::session::keys::{self, Chord};
24
25#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct Point {
28 pub x: f64,
29 pub y: f64,
30}
31
32fn node_err(backend_node_id: u64, op: &'static str) -> impl FnOnce(anyhow::Error) -> anyhow::Error {
36 move |e| {
37 let msg = format!("{e:#}");
38 if is_cdp_node_gone(&msg) {
39 SessionError::NodeGone {
40 backend_node_id,
41 details: msg,
42 }
43 .into()
44 } else {
45 e.context(format!("{op} on node {backend_node_id}"))
46 }
47 }
48}
49
50fn viewport_size(metrics: &Value) -> (f64, f64) {
53 let vp = metrics
54 .get("cssLayoutViewport")
55 .or_else(|| metrics.get("layoutViewport"));
56 let w = vp
57 .and_then(|v| v.get("clientWidth"))
58 .and_then(Value::as_f64)
59 .unwrap_or(f64::MAX);
60 let h = vp
61 .and_then(|v| v.get("clientHeight"))
62 .and_then(Value::as_f64)
63 .unwrap_or(f64::MAX);
64 (w, h)
65}
66
67fn viewport_offset(metrics: &Value) -> (f64, f64) {
69 let vp = metrics
70 .get("cssLayoutViewport")
71 .or_else(|| metrics.get("layoutViewport"));
72 let x = vp
73 .and_then(|v| v.get("pageX"))
74 .and_then(Value::as_f64)
75 .unwrap_or(0.0);
76 let y = vp
77 .and_then(|v| v.get("pageY"))
78 .and_then(Value::as_f64)
79 .unwrap_or(0.0);
80 (x, y)
81}
82
83pub fn pick_point(quads: &Value, vw: f64, vh: f64) -> Option<Point> {
86 let quads = quads.as_array()?;
87 for q in quads {
88 let nums: Vec<f64> = q.as_array()?.iter().filter_map(Value::as_f64).collect();
89 if nums.len() != 8 {
90 continue;
91 }
92 let pts: Vec<(f64, f64)> = (0..4)
93 .map(|i| (nums[i * 2].clamp(0.0, vw), nums[i * 2 + 1].clamp(0.0, vh)))
94 .collect();
95 let mut area = 0.0;
97 for i in 0..4 {
98 let (x1, y1) = pts[i];
99 let (x2, y2) = pts[(i + 1) % 4];
100 area += x1 * y2 - x2 * y1;
101 }
102 if area.abs() / 2.0 <= 1.0 {
103 continue;
104 }
105 let x = pts.iter().map(|p| p.0).sum::<f64>() / 4.0;
106 let y = pts.iter().map(|p| p.1).sum::<f64>() / 4.0;
107 return Some(Point { x, y });
108 }
109 None
110}
111
112pub async fn node_center(c: &CdpClient, sid: &str, backend_node_id: u64) -> Result<Point> {
114 let _ = c
115 .send_with_session("DOM.enable", json!({}), Some(sid))
116 .await;
117 c.send_with_session(
118 "DOM.scrollIntoViewIfNeeded",
119 json!({ "backendNodeId": backend_node_id }),
120 Some(sid),
121 )
122 .await
123 .map_err(node_err(backend_node_id, "scrollIntoViewIfNeeded"))?;
124 let quads = c
125 .send_with_session(
126 "DOM.getContentQuads",
127 json!({ "backendNodeId": backend_node_id }),
128 Some(sid),
129 )
130 .await
131 .map_err(node_err(backend_node_id, "getContentQuads"))?;
132 let metrics = c
133 .send_with_session("Page.getLayoutMetrics", json!({}), Some(sid))
134 .await?;
135 let (vw, vh) = viewport_size(&metrics);
136 pick_point(&quads["quads"], vw, vh)
137 .ok_or_else(|| anyhow!("element has no visible box (hidden, zero-size, or outside the viewport after scrolling)"))
138}
139
140async fn mouse(c: &CdpClient, sid: &str, kind: &str, p: Point, pressed: bool) -> Result<()> {
141 let mut params = json!({ "type": kind, "x": p.x, "y": p.y });
142 if pressed {
143 params["button"] = json!("left");
144 params["clickCount"] = json!(1);
145 }
146 c.send_with_session("Input.dispatchMouseEvent", params, Some(sid))
147 .await
148 .context("Input.dispatchMouseEvent")?;
149 Ok(())
150}
151
152pub async fn click(c: &CdpClient, sid: &str, backend_node_id: u64) -> Result<Point> {
154 let p = node_center(c, sid, backend_node_id).await?;
155 mouse(c, sid, "mouseMoved", p, false).await?;
156 mouse(c, sid, "mousePressed", p, true).await?;
157 mouse(c, sid, "mouseReleased", p, true).await?;
158 Ok(p)
159}
160
161pub async fn hover(c: &CdpClient, sid: &str, backend_node_id: u64) -> Result<Point> {
163 let p = node_center(c, sid, backend_node_id).await?;
164 mouse(c, sid, "mouseMoved", p, false).await?;
165 Ok(p)
166}
167
168pub async fn type_text(
172 c: &CdpClient,
173 sid: &str,
174 backend_node_id: u64,
175 text: &str,
176 press_sequentially: bool,
177 submit: bool,
178) -> Result<()> {
179 let _ = c
181 .send_with_session(
182 "Emulation.setFocusEmulationEnabled",
183 json!({ "enabled": true }),
184 Some(sid),
185 )
186 .await;
187 c.send_with_session(
188 "DOM.focus",
189 json!({ "backendNodeId": backend_node_id }),
190 Some(sid),
191 )
192 .await
193 .map_err(node_err(backend_node_id, "focus"))?;
194 let resolved = c
195 .send_with_session(
196 "DOM.resolveNode",
197 json!({ "backendNodeId": backend_node_id }),
198 Some(sid),
199 )
200 .await
201 .map_err(node_err(backend_node_id, "resolveNode"))?;
202 let object_id = resolved["object"]["objectId"]
203 .as_str()
204 .ok_or_else(|| anyhow!("DOM.resolveNode returned no objectId"))?
205 .to_string();
206 let select = c
207 .send_with_session(
208 "Runtime.callFunctionOn",
209 json!({
210 "objectId": object_id,
211 "functionDeclaration": SELECT_ALL_JS,
212 "arguments": [{ "value": text.is_empty() }],
213 "returnByValue": true,
214 }),
215 Some(sid),
216 )
217 .await;
218 let _ = c
219 .send_with_session(
220 "Runtime.releaseObject",
221 json!({ "objectId": object_id }),
222 Some(sid),
223 )
224 .await;
225 select.context("selecting existing content")?;
226 if !text.is_empty() {
227 if press_sequentially {
228 for ch in text.chars() {
229 insert_text(c, sid, &ch.to_string()).await?;
230 }
231 } else {
232 insert_text(c, sid, text).await?;
233 }
234 }
235 if submit {
236 press_enter(c, sid).await?;
237 }
238 Ok(())
239}
240
241pub async fn type_focused(
252 c: &CdpClient,
253 sid: &str,
254 text: &str,
255 press_sequentially: bool,
256 submit: bool,
257) -> Result<()> {
258 let _ = c
260 .send_with_session(
261 "Emulation.setFocusEmulationEnabled",
262 json!({ "enabled": true }),
263 Some(sid),
264 )
265 .await;
266 let resolved = c
267 .send_with_session(
268 "Runtime.evaluate",
269 json!({ "expression": "document.activeElement", "returnByValue": false }),
270 Some(sid),
271 )
272 .await
273 .context("resolving document.activeElement")?;
274 if let Some(object_id) = resolved["result"]["objectId"].as_str() {
275 let object_id = object_id.to_string();
276 let select = c
277 .send_with_session(
278 "Runtime.callFunctionOn",
279 json!({
280 "objectId": object_id,
281 "functionDeclaration": SELECT_ALL_JS,
282 "arguments": [{ "value": text.is_empty() }],
283 "returnByValue": true,
284 }),
285 Some(sid),
286 )
287 .await;
288 let _ = c
289 .send_with_session(
290 "Runtime.releaseObject",
291 json!({ "objectId": object_id }),
292 Some(sid),
293 )
294 .await;
295 select.context("selecting existing content")?;
296 }
297 if !text.is_empty() {
298 if press_sequentially {
299 for ch in text.chars() {
300 insert_text(c, sid, &ch.to_string()).await?;
301 }
302 } else {
303 insert_text(c, sid, text).await?;
304 }
305 }
306 if submit {
307 press_enter(c, sid).await?;
308 }
309 Ok(())
310}
311
312async fn insert_text(c: &CdpClient, sid: &str, text: &str) -> Result<()> {
313 c.send_with_session("Input.insertText", json!({ "text": text }), Some(sid))
314 .await
315 .context("Input.insertText")?;
316 Ok(())
317}
318
319pub async fn press_enter(c: &CdpClient, sid: &str) -> Result<()> {
323 press_key(c, sid, &Chord::plain(keys::ENTER)).await
324}
325
326pub async fn press_key(c: &CdpClient, sid: &str, chord: &Chord) -> Result<()> {
336 let events = key_events(chord);
337 let release_from = events.len() - chord.modifiers.len();
341
342 let mut first_error = None;
343 for (i, ev) in events.iter().enumerate() {
344 if first_error.is_some() && i < release_from {
345 continue; }
347 if let Err(e) = c
348 .send_with_session("Input.dispatchKeyEvent", ev.clone(), Some(sid))
349 .await
350 {
351 let described = e.context(format!(
352 "Input.dispatchKeyEvent {} {}",
353 ev["type"].as_str().unwrap_or("?"),
354 ev["key"].as_str().unwrap_or("?")
355 ));
356 if first_error.is_none() {
357 first_error = Some(described);
358 }
359 }
360 }
361 match first_error {
362 Some(e) => Err(e),
363 None => Ok(()),
364 }
365}
366
367pub fn key_events(chord: &Chord) -> Vec<Value> {
377 let mut events = Vec::with_capacity(chord.modifiers.len() * 2 + 2);
378 let mut mask = 0u32;
379
380 for m in &chord.modifiers {
381 mask |= *m as u32;
382 events.push(event("rawKeyDown", &m.def(), mask));
383 }
384 let kind = if chord.key.text.is_some() {
387 "keyDown"
388 } else {
389 "rawKeyDown"
390 };
391 events.push(event(kind, &chord.key, mask));
392 events.push(event("keyUp", &chord.key, mask));
393
394 for m in chord.modifiers.iter().rev() {
395 mask &= !(*m as u32);
396 events.push(event("keyUp", &m.def(), mask));
397 }
398 events
399}
400
401fn event(kind: &str, key: &keys::KeyDef, modifiers: u32) -> Value {
402 let mut params = json!({
403 "type": kind,
404 "key": key.key,
405 "code": key.code,
406 "windowsVirtualKeyCode": key.vk,
407 "nativeVirtualKeyCode": key.vk,
408 });
409 if modifiers != 0 {
410 params["modifiers"] = json!(modifiers);
411 }
412 if kind != "keyUp" {
415 if let Some(text) = key.text {
416 params["text"] = json!(text);
417 params["unmodifiedText"] = json!(text);
418 }
419 }
420 params
421}
422
423pub async fn drag(c: &CdpClient, sid: &str, from: u64, to: u64) -> Result<()> {
426 let a = node_center(c, sid, from).await?;
427 let b = node_center(c, sid, to).await?;
428 mouse(c, sid, "mouseMoved", a, false).await?;
429 mouse(c, sid, "mousePressed", a, true).await?;
430 const STEPS: usize = 5;
431 for i in 1..=STEPS {
432 let t = i as f64 / STEPS as f64;
433 let p = Point {
434 x: a.x + (b.x - a.x) * t,
435 y: a.y + (b.y - a.y) * t,
436 };
437 mouse(c, sid, "mouseMoved", p, true).await?;
438 }
439 mouse(c, sid, "mouseReleased", b, true).await?;
440 Ok(())
441}
442
443pub async fn document_token(c: &CdpClient, sid: &str) -> Result<u64> {
446 let doc = c
447 .send_with_session("DOM.getDocument", json!({ "depth": 0 }), Some(sid))
448 .await
449 .context("DOM.getDocument")?;
450 doc["root"]["backendNodeId"]
451 .as_u64()
452 .ok_or_else(|| anyhow!("DOM.getDocument returned no root backendNodeId"))
453}
454
455pub async fn node_clip_rect(c: &CdpClient, sid: &str, backend_node_id: u64) -> Result<Value> {
458 let _ = c
459 .send_with_session("DOM.enable", json!({}), Some(sid))
460 .await;
461 c.send_with_session(
462 "DOM.scrollIntoViewIfNeeded",
463 json!({ "backendNodeId": backend_node_id }),
464 Some(sid),
465 )
466 .await
467 .map_err(node_err(backend_node_id, "scrollIntoViewIfNeeded"))?;
468 let model = c
469 .send_with_session(
470 "DOM.getBoxModel",
471 json!({ "backendNodeId": backend_node_id }),
472 Some(sid),
473 )
474 .await
475 .map_err(node_err(backend_node_id, "getBoxModel"))?;
476 let border: Vec<f64> = model["model"]["border"]
477 .as_array()
478 .map(|a| a.iter().filter_map(Value::as_f64).collect())
479 .unwrap_or_default();
480 if border.len() != 8 {
481 return Err(anyhow!("element has no box model (hidden or detached)"));
482 }
483 let xs = [border[0], border[2], border[4], border[6]];
484 let ys = [border[1], border[3], border[5], border[7]];
485 let min_x = xs.iter().cloned().fold(f64::MAX, f64::min);
486 let max_x = xs.iter().cloned().fold(f64::MIN, f64::max);
487 let min_y = ys.iter().cloned().fold(f64::MAX, f64::min);
488 let max_y = ys.iter().cloned().fold(f64::MIN, f64::max);
489 if max_x - min_x <= 0.0 || max_y - min_y <= 0.0 {
490 return Err(anyhow!("element has zero area"));
491 }
492 let metrics = c
493 .send_with_session("Page.getLayoutMetrics", json!({}), Some(sid))
494 .await?;
495 let (sx, sy) = viewport_offset(&metrics);
496 Ok(json!({
497 "x": min_x + sx,
498 "y": min_y + sy,
499 "width": max_x - min_x,
500 "height": max_y - min_y,
501 }))
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507 use futures_util::{SinkExt, StreamExt};
508 use std::sync::Arc;
509 use tokio::sync::Mutex;
510 use tokio_tungstenite::tungstenite::Message;
511
512 async fn spawn_mock(node_gone: bool) -> (String, Arc<Mutex<Vec<Value>>>) {
515 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
516 let addr = listener.local_addr().unwrap();
517 let seen = Arc::new(Mutex::new(Vec::<Value>::new()));
518 tokio::spawn({
519 let seen = seen.clone();
520 async move {
521 let (stream, _) = listener.accept().await.unwrap();
522 let mut ws = tokio_tungstenite::accept_async(stream).await.unwrap();
523 while let Some(Ok(Message::Text(t))) = ws.next().await {
524 let req: Value = serde_json::from_str(&t).unwrap();
525 seen.lock().await.push(req.clone());
526 let id = req["id"].as_u64().unwrap();
527 let method = req["method"].as_str().unwrap_or("");
528 if node_gone && method.starts_with("DOM.") && method != "DOM.enable" {
529 let resp = json!({"id": id, "error": {"code": -32000, "message": "No node with given id found"}});
530 ws.send(Message::Text(resp.to_string())).await.unwrap();
531 continue;
532 }
533 let result = match method {
534 "DOM.getContentQuads" => json!({"quads": [
535 [-50, -50, -10, -50, -10, -40, -50, -40],
537 [10, 30, 110, 30, 110, 50, 10, 50],
538 ]}),
539 "Page.getLayoutMetrics" => json!({"cssLayoutViewport": {
540 "pageX": 0, "pageY": 400, "clientWidth": 800, "clientHeight": 600
541 }}),
542 "DOM.resolveNode" => json!({"object": {"objectId": "obj-1"}}),
543 "DOM.getDocument" => json!({"root": {"backendNodeId": 4242}}),
544 "DOM.getBoxModel" => {
545 json!({"model": {"border": [10, 30, 110, 30, 110, 50, 10, 50]}})
546 }
547 _ => json!({}),
548 };
549 let resp = json!({"id": id, "result": result});
550 ws.send(Message::Text(resp.to_string())).await.unwrap();
551 }
552 }
553 });
554 (format!("ws://{addr}"), seen)
555 }
556
557 fn methods(seen: &[Value]) -> Vec<String> {
558 seen.iter()
559 .filter_map(|v| v["method"].as_str().map(String::from))
560 .collect()
561 }
562
563 #[test]
564 fn pick_point_skips_offscreen_and_clips() {
565 let quads = json!([
566 [-50, -50, -10, -50, -10, -40, -50, -40],
567 [700, 10, 900, 10, 900, 30, 700, 30],
568 ]);
569 let p = pick_point(&quads, 800.0, 600.0).unwrap();
570 assert_eq!(p, Point { x: 750.0, y: 20.0 });
571 assert!(pick_point(&json!([[0, 0, 0, 0, 0, 0, 0, 0]]), 800.0, 600.0).is_none());
572 assert!(pick_point(&json!(null), 800.0, 600.0).is_none());
573 }
574
575 #[tokio::test]
576 async fn click_dispatches_move_press_release_at_centre() {
577 let (url, seen) = spawn_mock(false).await;
578 let c = CdpClient::connect(&url).await.unwrap();
579 let p = click(&c, "S1", 77).await.unwrap();
580 assert_eq!(p, Point { x: 60.0, y: 40.0 });
581 let calls = seen.lock().await;
582 assert_eq!(
583 methods(&calls),
584 vec![
585 "DOM.enable",
586 "DOM.scrollIntoViewIfNeeded",
587 "DOM.getContentQuads",
588 "Page.getLayoutMetrics",
589 "Input.dispatchMouseEvent",
590 "Input.dispatchMouseEvent",
591 "Input.dispatchMouseEvent",
592 ]
593 );
594 let press = &calls[5];
595 assert_eq!(press["sessionId"], "S1");
596 assert_eq!(press["params"]["type"], "mousePressed");
597 assert_eq!(press["params"]["button"], "left");
598 assert_eq!(press["params"]["x"], 60.0);
599 assert_eq!(press["params"]["y"], 40.0);
600 assert_eq!(calls[1]["params"]["backendNodeId"], 77);
601 }
602
603 #[tokio::test]
604 async fn type_text_focuses_selects_inserts_and_submits() {
605 let (url, seen) = spawn_mock(false).await;
606 let c = CdpClient::connect(&url).await.unwrap();
607 type_text(&c, "S1", 5, "hi", false, true).await.unwrap();
608 let calls = seen.lock().await;
609 assert_eq!(
610 methods(&calls),
611 vec![
612 "Emulation.setFocusEmulationEnabled",
613 "DOM.focus",
614 "DOM.resolveNode",
615 "Runtime.callFunctionOn",
616 "Runtime.releaseObject",
617 "Input.insertText",
618 "Input.dispatchKeyEvent",
619 "Input.dispatchKeyEvent",
620 ]
621 );
622 assert_eq!(calls[3]["params"]["arguments"][0]["value"], false);
623 assert_eq!(calls[5]["params"]["text"], "hi");
624 assert_eq!(calls[6]["params"]["text"], "\r");
625 assert_eq!(calls[7]["params"]["type"], "keyUp");
626 }
627
628 #[tokio::test]
629 async fn type_text_sequential_and_clear() {
630 let (url, seen) = spawn_mock(false).await;
631 let c = CdpClient::connect(&url).await.unwrap();
632 type_text(&c, "S1", 5, "ab", true, false).await.unwrap();
633 type_text(&c, "S1", 5, "", false, false).await.unwrap();
634 let calls = seen.lock().await;
635 let inserts: Vec<&Value> = calls
636 .iter()
637 .filter(|v| v["method"] == "Input.insertText")
638 .collect();
639 assert_eq!(inserts.len(), 2);
640 assert_eq!(inserts[0]["params"]["text"], "a");
641 assert_eq!(inserts[1]["params"]["text"], "b");
642 let clears: Vec<&Value> = calls
643 .iter()
644 .filter(|v| v["method"] == "Runtime.callFunctionOn")
645 .collect();
646 assert_eq!(clears[1]["params"]["arguments"][0]["value"], true);
647 }
648
649 #[tokio::test]
650 async fn drag_presses_moves_and_releases() {
651 let (url, seen) = spawn_mock(false).await;
652 let c = CdpClient::connect(&url).await.unwrap();
653 drag(&c, "S1", 1, 2).await.unwrap();
654 let calls = seen.lock().await;
655 let types: Vec<&str> = calls
656 .iter()
657 .filter(|v| v["method"] == "Input.dispatchMouseEvent")
658 .map(|v| v["params"]["type"].as_str().unwrap())
659 .collect();
660 assert_eq!(types[0], "mouseMoved");
661 assert_eq!(types[1], "mousePressed");
662 assert_eq!(*types.last().unwrap(), "mouseReleased");
663 assert_eq!(types.len(), 2 + 5 + 1);
664 }
665
666 #[tokio::test]
667 async fn node_gone_maps_to_typed_error() {
668 let (url, _seen) = spawn_mock(true).await;
669 let c = CdpClient::connect(&url).await.unwrap();
670 let err = click(&c, "S1", 9).await.unwrap_err();
671 match err.downcast_ref::<SessionError>() {
672 Some(SessionError::NodeGone {
673 backend_node_id, ..
674 }) => assert_eq!(*backend_node_id, 9),
675 other => panic!("expected NodeGone, got {other:?}"),
676 }
677 }
678
679 #[tokio::test]
680 async fn document_token_and_clip_rect() {
681 let (url, _seen) = spawn_mock(false).await;
682 let c = CdpClient::connect(&url).await.unwrap();
683 assert_eq!(document_token(&c, "S1").await.unwrap(), 4242);
684 let rect = node_clip_rect(&c, "S1", 3).await.unwrap();
685 assert_eq!(
687 rect,
688 json!({"x": 10.0, "y": 430.0, "width": 100.0, "height": 20.0})
689 );
690 }
691}
692
693#[cfg(test)]
694mod key_tests {
695 use super::*;
696 use crate::session::keys::parse_chord;
697
698 fn kinds(events: &[Value]) -> Vec<(String, String)> {
699 events
700 .iter()
701 .map(|e| {
702 (
703 e["type"].as_str().unwrap().to_string(),
704 e["key"].as_str().unwrap().to_string(),
705 )
706 })
707 .collect()
708 }
709
710 #[test]
711 fn a_plain_named_key_is_two_events() {
712 let events = key_events(&parse_chord("ArrowDown").unwrap());
713 assert_eq!(
714 kinds(&events),
715 vec![
716 ("rawKeyDown".into(), "ArrowDown".into()),
717 ("keyUp".into(), "ArrowDown".into()),
718 ]
719 );
720 }
721
722 #[test]
723 fn a_non_inserting_key_carries_no_text() {
724 for ev in key_events(&parse_chord("ArrowDown").unwrap()) {
727 assert!(ev.get("text").is_none(), "{ev}");
728 }
729 }
730
731 #[test]
732 fn enter_still_carries_the_carriage_return_that_submits_forms() {
733 let events = key_events(&parse_chord("Enter").unwrap());
734 assert_eq!(events[0]["type"], "keyDown");
735 assert_eq!(events[0]["text"], "\r");
736 assert_eq!(events[0]["unmodifiedText"], "\r");
737 assert_eq!(events[0]["windowsVirtualKeyCode"], 13);
738 assert!(events[1].get("text").is_none());
740 }
741
742 #[test]
743 fn press_enter_payload_is_unchanged() {
744 let events = key_events(&Chord::plain(keys::ENTER));
746 assert_eq!(events.len(), 2);
747 assert_eq!(events[0]["key"], "Enter");
748 assert_eq!(events[0]["code"], "Enter");
749 assert_eq!(events[0]["nativeVirtualKeyCode"], 13);
750 assert_eq!(events[1]["type"], "keyUp");
751 }
752
753 #[test]
754 fn control_a_holds_the_modifier_across_the_key() {
755 let events = key_events(&parse_chord("Control+A").unwrap());
756 assert_eq!(
757 kinds(&events),
758 vec![
759 ("rawKeyDown".into(), "Control".into()),
760 ("keyDown".into(), "A".into()),
761 ("keyUp".into(), "A".into()),
762 ("keyUp".into(), "Control".into()),
763 ]
764 );
765 assert_eq!(events[1]["modifiers"], 2);
767 assert_eq!(events[2]["modifiers"], 2);
768 assert!(events[3].get("modifiers").is_none());
770 }
771
772 #[test]
773 fn modifiers_release_in_reverse_order() {
774 let events = key_events(&parse_chord("Control+Shift+K").unwrap());
776 let seq = kinds(&events);
777 assert_eq!(seq[0].1, "Control");
778 assert_eq!(seq[1].1, "Shift");
779 assert_eq!(seq[4].1, "Shift");
780 assert_eq!(seq[5].1, "Control");
781 assert_eq!(events[1]["modifiers"], 2 | 8);
783 assert_eq!(events[2]["modifiers"], 2 | 8);
784 assert_eq!(events[4]["modifiers"], 2); }
786
787 #[test]
788 fn printable_keys_insert_themselves() {
789 let events = key_events(&parse_chord("a").unwrap());
790 assert_eq!(events[0]["type"], "keyDown");
791 assert_eq!(events[0]["text"], "a");
792 assert_eq!(events[0]["code"], "KeyA");
793 }
794}