use crate::element::Element;
use crate::error::Result;
use crate::input::keyboard::{self, Key, KeyModifiers, KeyPress, KeySequence};
impl Element {
pub async fn type_text(&self, text: impl AsRef<str>) -> Result<()> {
let text = text.as_ref().to_string();
self.with_refresh(|| {
let text = text.clone();
async move {
self.focus().await?;
let input = self.inner.tab.input().clone();
keyboard::type_text_realistic(&input, &self.inner.tab, &text).await
}
})
.await
}
pub async fn type_text_fast(&self, text: impl AsRef<str>) -> Result<()> {
let text = text.as_ref().to_string();
self.with_refresh(|| {
let text = text.clone();
async move {
self.focus().await?;
let input = self.inner.tab.input().clone();
keyboard::type_text_fast(&input, &self.inner.tab, &text).await
}
})
.await
}
pub async fn press(&self, key: Key) -> Result<()> {
self.with_refresh(|| async move {
self.focus().await?;
let input = self.inner.tab.input().clone();
let mods = input.state.lock().await.modifiers_held;
dispatch_key(&self.inner.tab, key, mods).await
})
.await
}
pub async fn press_with(&self, key: Key, mods: KeyModifiers) -> Result<()> {
self.with_refresh(|| async move {
self.focus().await?;
dispatch_key(&self.inner.tab, key, mods).await
})
.await
}
pub async fn type_keys(&self, seq: KeySequence) -> Result<()> {
self.with_refresh(|| {
let seq = seq.clone();
async move {
self.focus().await?;
let events = seq.to_events();
keyboard::dispatch_key_events(&self.inner.tab, &events).await
}
})
.await
}
}
async fn dispatch_key(tab: &crate::tab::Tab, key: Key, mods: KeyModifiers) -> Result<()> {
let events = keyboard::key_events(key, mods, KeyPress::DownAndUp);
keyboard::dispatch_key_events(tab, &events).await
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
use crate::input::keyboard::SpecialKey;
use crate::tab::Tab;
use serde_json::{Value, json};
use zendriver_transport::SessionHandle;
use zendriver_transport::testing::MockConnection;
#[tokio::test]
async fn type_text_fast_emits_two_dispatchkeyevent_per_char() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab.clone(), 17, "R17".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.type_text_fast("hi").await }
});
for _ in 0..2 {
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
mock.reply(
id,
json!({ "result": { "value": true, "type": "boolean" } }),
)
.await;
}
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
let sent = mock.last_sent();
assert!(
sent["params"]["functionDeclaration"]
.as_str()
.unwrap()
.contains("this.focus()")
);
mock.reply(id, json!({ "result": { "type": "undefined" } }))
.await;
let expected = [
("h", "keyDown"),
("h", "keyUp"),
("i", "keyDown"),
("i", "keyUp"),
];
for (ch, kind) in expected {
let id = mock.expect_cmd("Input.dispatchKeyEvent").await;
let last = mock.last_sent();
assert_eq!(last["params"]["text"].as_str().unwrap(), ch);
assert_eq!(last["params"]["type"].as_str().unwrap(), kind);
mock.reply(id, Value::Null).await;
}
fut.await.unwrap().unwrap();
conn.shutdown();
}
async fn drain_focus(mock: &mut MockConnection) {
for _ in 0..2 {
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
mock.reply(
id,
json!({ "result": { "value": true, "type": "boolean" } }),
)
.await;
}
let id = mock.expect_cmd("Runtime.callFunctionOn").await;
mock.reply(id, json!({ "result": { "type": "undefined" } }))
.await;
}
#[tokio::test]
async fn press_with_ctrl_a_emits_control_wrapper_events() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab.clone(), 17, "R17".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.press_with(Key::Char('a'), KeyModifiers::CTRL).await }
});
drain_focus(&mut mock).await;
let expected = [
("ControlLeft", "keyDown", 2),
("KeyA", "keyDown", 2),
("KeyA", "keyUp", 2),
("ControlLeft", "keyUp", 0),
];
for (code, kind, mods) in expected {
let id = mock.expect_cmd("Input.dispatchKeyEvent").await;
let last = mock.last_sent();
assert_eq!(last["params"]["code"].as_str().unwrap(), code);
assert_eq!(last["params"]["type"].as_str().unwrap(), kind);
assert_eq!(last["params"]["modifiers"].as_i64().unwrap(), mods);
mock.reply(id, Value::Null).await;
}
fut.await.unwrap().unwrap();
conn.shutdown();
}
#[tokio::test]
async fn type_keys_flattens_text_key_and_chord_in_order() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab.clone(), 17, "R17".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move {
e.type_keys(
KeySequence::new()
.text("hi")
.key(SpecialKey::Enter)
.chord(Key::Char('a'), KeyModifiers::CTRL),
)
.await
}
});
drain_focus(&mut mock).await;
let expected = [
("h", "keyDown"),
("h", "keyUp"),
("i", "keyDown"),
("i", "keyUp"),
("Enter", "rawKeyDown"),
("Enter", "keyUp"),
("Control", "keyDown"),
("a", "keyDown"),
("a", "keyUp"),
("Control", "keyUp"),
];
for (key, kind) in expected {
let id = mock.expect_cmd("Input.dispatchKeyEvent").await;
let last = mock.last_sent();
assert_eq!(last["params"]["key"].as_str().unwrap(), key);
assert_eq!(last["params"]["type"].as_str().unwrap(), kind);
mock.reply(id, Value::Null).await;
}
fut.await.unwrap().unwrap();
conn.shutdown();
}
#[tokio::test]
async fn type_text_fast_emoji_emits_single_char_event() {
let (mut mock, conn) = MockConnection::pair();
let sess = SessionHandle::new(conn.clone(), "S1");
let tab = Tab::new_for_test(sess);
let el = Element::from_jsret(tab.clone(), 17, "R17".to_string());
let fut = tokio::spawn({
let e = el.clone();
async move { e.type_text_fast("🚀").await }
});
drain_focus(&mut mock).await;
let id = mock.expect_cmd("Input.dispatchKeyEvent").await;
let last = mock.last_sent();
assert_eq!(last["params"]["type"].as_str().unwrap(), "char");
assert_eq!(last["params"]["text"].as_str().unwrap(), "🚀");
assert!(last["params"].get("code").is_none());
mock.reply(id, Value::Null).await;
fut.await.unwrap().unwrap();
conn.shutdown();
}
}