use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use serde_json::{Value, json};
use tokio::time::{Instant, sleep};
use crate::browser::frame::Frame;
use crate::browser::keys::KeyInput;
use crate::browser::shadow::ShadowRoot;
use crate::browser::static_element::StaticElement;
use crate::browser::tab::{ImageFormat, TabCore, write_file};
use crate::locator::{self, Query};
use crate::{Error, Result};
#[derive(Clone)]
pub struct Element {
core: Arc<TabCore>,
object_id: String,
frame_id: Option<String>,
}
impl Element {
pub(crate) fn new(core: Arc<TabCore>, object_id: String) -> Self {
Self {
core,
object_id,
frame_id: None,
}
}
pub(crate) fn new_in_frame(core: Arc<TabCore>, object_id: String, frame_id: String) -> Self {
Self {
core,
object_id,
frame_id: Some(frame_id),
}
}
pub fn object_id(&self) -> &str {
&self.object_id
}
pub(crate) fn frame_id_ref(&self) -> &str {
self.frame_id.as_deref().unwrap_or(&self.core.main_frame_id)
}
fn self_arg(&self) -> Value {
json!({ "objectId": self.object_id })
}
async fn call(&self, declaration: &str, extra: Vec<Value>, by_value: bool) -> Result<Value> {
let mut args = vec![self.self_arg()];
args.extend(extra);
match &self.frame_id {
Some(fid) => {
self.core
.call_function_in(fid, declaration, args, by_value)
.await
}
None => self.core.call_function(declaration, args, by_value).await,
}
}
async fn call_to_element(
&self,
declaration: &str,
extra: Vec<Value>,
what: &str,
) -> Result<Element> {
let result = self.call(declaration, extra, false).await?;
match result.get("objectId").and_then(|v| v.as_str()) {
Some(oid) => Ok(Element {
core: self.core.clone(),
object_id: oid.to_string(),
frame_id: self.frame_id.clone(),
}),
None => Err(Error::ElementNotFound(what.to_string())),
}
}
async fn call_to_elements(&self, declaration: &str, extra: Vec<Value>) -> Result<Vec<Element>> {
let result = self.call(declaration, extra, false).await?;
let Some(array_object_id) = result.get("objectId").and_then(|v| v.as_str()) else {
return Ok(Vec::new());
};
let oids = self
.core
.node_array_object_ids(self.frame_id_ref(), array_object_id)
.await?;
Ok(oids
.into_iter()
.map(|oid| Element {
core: self.core.clone(),
object_id: oid,
frame_id: self.frame_id.clone(),
})
.collect())
}
pub async fn text(&self) -> Result<String> {
let v = self
.call(
"node => node.innerText ?? node.textContent ?? ''",
vec![],
true,
)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn attr(&self, name: &str) -> Result<Option<String>> {
let v = self
.call(
"(node, name) => node.getAttribute(name)",
vec![json!({ "value": name })],
true,
)
.await?;
Ok(v.as_str().map(str::to_string))
}
pub async fn value(&self) -> Result<String> {
let v = self.call("node => node.value ?? ''", vec![], true).await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn tag(&self) -> Result<String> {
let v = self
.call(
"node => node.tagName ? node.tagName.toLowerCase() : ''",
vec![],
true,
)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn is_displayed(&self) -> Result<bool> {
let v = self
.call(
"node => { const r = node.getClientRects(); const s = getComputedStyle(node); \
return r.length > 0 && s.visibility !== 'hidden' && s.display !== 'none'; }",
vec![],
true,
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_enabled(&self) -> Result<bool> {
let v = self.call("node => !node.disabled", vec![], true).await?;
Ok(v.as_bool().unwrap_or(true))
}
pub async fn is_in_viewport(&self) -> Result<bool> {
let v = self
.call(
"node => { const r = node.getBoundingClientRect(); \
const w = innerWidth || document.documentElement.clientWidth; \
const h = innerHeight || document.documentElement.clientHeight; \
return r.bottom > 0 && r.right > 0 && r.top < h && r.left < w; }",
vec![],
true,
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_covered(&self) -> Result<bool> {
let v = self
.call(
"node => { const r = node.getBoundingClientRect(); \
if (r.width<=0||r.height<=0) return false; \
const x = r.left + r.width/2, y = r.top + r.height/2; \
const top = document.elementFromPoint(x, y); \
if (!top) return false; \
return !(top === node || node.contains(top) || top.contains(node)); }",
vec![],
true,
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn is_clickable(&self) -> Result<bool> {
let v = self
.call(
"node => { const s = getComputedStyle(node); \
if (s.visibility==='hidden'||s.display==='none'||node.disabled) return false; \
const r = node.getBoundingClientRect(); \
if (r.width<=0||r.height<=0) return false; \
const w = innerWidth, h = innerHeight; \
if (r.bottom<=0||r.right<=0||r.top>=h||r.left>=w) return false; \
const x = Math.min(Math.max(r.left+r.width/2,0),w-1); \
const y = Math.min(Math.max(r.top+r.height/2,0),h-1); \
const top = document.elementFromPoint(x,y); \
return !!top && (top===node || node.contains(top)); }",
vec![],
true,
)
.await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn rect(&self) -> Result<ElementRect> {
let v = self
.call(
"node => { const r = node.getBoundingClientRect(); return { \
x: r.left + scrollX, y: r.top + scrollY, vx: r.left, vy: r.top, \
w: r.width, h: r.height }; }",
vec![],
true,
)
.await?;
let f = |k: &str| v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
Ok(ElementRect {
x: f("x"),
y: f("y"),
viewport_x: f("vx"),
viewport_y: f("vy"),
width: f("w"),
height: f("h"),
})
}
pub async fn location(&self) -> Result<(f64, f64)> {
let r = self.rect().await?;
Ok((r.x, r.y))
}
pub async fn size(&self) -> Result<(f64, f64)> {
let r = self.rect().await?;
Ok((r.width, r.height))
}
pub async fn attrs(&self) -> Result<HashMap<String, String>> {
let v = self
.call(
"node => { const o = {}; for (const a of (node.attributes||[])) o[a.name] = a.value; return o; }",
vec![],
true,
)
.await?;
let mut map = HashMap::new();
if let Some(o) = v.as_object() {
for (k, val) in o {
if let Some(s) = val.as_str() {
map.insert(k.clone(), s.to_string());
}
}
}
Ok(map)
}
pub async fn style(&self, name: &str) -> Result<String> {
let v = self
.call(
"(node, n) => getComputedStyle(node).getPropertyValue(n)",
vec![json!({ "value": name })],
true,
)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn property(&self, name: &str) -> Result<Value> {
self.call("(node, n) => node[n]", vec![json!({ "value": name })], true)
.await
}
pub async fn remove(&self) -> Result<()> {
self.call("node => node.remove()", vec![], false).await?;
Ok(())
}
pub fn wait(&self) -> ElementWait {
ElementWait { ele: self.clone() }
}
pub async fn run_js(&self, body: &str) -> Result<Value> {
let decl = format!("(node) => {{ {body} }}");
self.call(&decl, vec![], true).await
}
pub async fn html(&self) -> Result<String> {
let v = self
.call("node => node.outerHTML ?? ''", vec![], true)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn inner_html(&self) -> Result<String> {
let v = self
.call("node => node.innerHTML ?? ''", vec![], true)
.await?;
Ok(v.as_str().unwrap_or_default().to_string())
}
pub async fn screenshot_bytes(&self) -> Result<Vec<u8>> {
let clip = self.shot_clip(true).await?;
self.core.capture(clip, ImageFormat::Png, None).await
}
pub async fn get_screenshot(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
let path = path.as_ref().to_path_buf();
let format = ImageFormat::from_path(&path);
let clip = self.shot_clip(true).await?;
let bytes = self.core.capture(clip, format, None).await?;
write_file(&path, &bytes).await?;
Ok(path)
}
async fn shot_clip(&self, scroll_to_center: bool) -> Result<Value> {
if scroll_to_center {
self.call(
"node => { node.scrollIntoView({ block: 'center', inline: 'center' }); }",
vec![],
true,
)
.await?;
} else {
self.scroll_into_view().await?;
}
let r = self
.call(
"node => { const b = node.getBoundingClientRect(); \
return [b.left + window.scrollX, b.top + window.scrollY, b.width, b.height]; }",
vec![],
true,
)
.await?;
let x = r.get(0).and_then(Value::as_f64).unwrap_or(0.0);
let y = r.get(1).and_then(Value::as_f64).unwrap_or(0.0);
let w = r.get(2).and_then(Value::as_f64).unwrap_or(0.0).max(1.0);
let h = r.get(3).and_then(Value::as_f64).unwrap_or(0.0).max(1.0);
Ok(json!({ "x": x, "y": y, "width": w, "height": h }))
}
pub async fn table(&self) -> Result<Vec<Vec<String>>> {
StaticElement::parse(&self.html().await?)?.table()
}
pub async fn table_records(&self) -> Result<Vec<HashMap<String, String>>> {
StaticElement::parse(&self.html().await?)?.table_records()
}
pub async fn s_ele(&self, selector: &str) -> Result<StaticElement> {
let html = self.html().await?;
StaticElement::parse(&html)?.ele(selector)
}
pub async fn s_eles(&self, selector: &str) -> Result<Vec<StaticElement>> {
let html = self.html().await?;
StaticElement::parse(&html)?.eles(selector)
}
pub async fn ele(&self, selector: &str) -> Result<Element> {
let query = locator::parse(selector);
let (decl, arg) = match &query {
Query::Css(sel) => (
"(node, sel) => node.querySelector(sel)".to_string(),
json!({ "value": sel }),
),
Query::Xpath(xp) => (
"(node, xp) => document.evaluate(xp, node, null, 9, null).singleNodeValue"
.to_string(),
json!({ "value": xp }),
),
};
let result = self.call(&decl, vec![arg], false).await?;
match result.get("objectId").and_then(|v| v.as_str()) {
Some(oid) => Ok(Element {
core: self.core.clone(),
object_id: oid.to_string(),
frame_id: self.frame_id.clone(),
}),
None => Err(Error::ElementNotFound(selector.to_string())),
}
}
pub async fn parent(&self) -> Result<Element> {
self.call_to_element("node => node.parentElement", vec![], "parent")
.await
}
pub async fn parent_n(&self, level: usize) -> Result<Element> {
self.call_to_element(
"(node, n) => { let e = node; for (let i = 0; i < n && e; i++) e = e.parentElement; return e; }",
vec![json!({ "value": level })],
"parent_n",
)
.await
}
pub async fn parent_until(&self, selector: &str) -> Result<Element> {
let css = match locator::parse(selector) {
Query::Css(sel) => sel,
Query::Xpath(_) => {
return Err(Error::Other(
"parent_until 仅支持 CSS 系定位;xpath 祖先请用 tab.ele(\"xpath:...\")".into(),
));
}
};
self.call_to_element(
"(node, sel) => node.parentElement ? node.parentElement.closest(sel) : null",
vec![json!({ "value": css })],
selector,
)
.await
}
pub async fn children(&self) -> Result<Vec<Element>> {
self.call_to_elements("node => Array.from(node.children)", vec![])
.await
}
pub async fn child(&self, index: usize) -> Result<Element> {
self.call_to_element(
"(node, i) => node.children[i] || null",
vec![json!({ "value": index })],
"child",
)
.await
}
pub async fn next(&self) -> Result<Element> {
self.call_to_element("node => node.nextElementSibling", vec![], "next")
.await
}
pub async fn prev(&self) -> Result<Element> {
self.call_to_element("node => node.previousElementSibling", vec![], "prev")
.await
}
pub async fn nexts(&self) -> Result<Vec<Element>> {
self.call_to_elements(
"node => { const a = []; let e = node.nextElementSibling; \
while (e) { a.push(e); e = e.nextElementSibling; } return a; }",
vec![],
)
.await
}
pub async fn prevs(&self) -> Result<Vec<Element>> {
self.call_to_elements(
"node => { const a = []; let e = node.previousElementSibling; \
while (e) { a.unshift(e); e = e.previousElementSibling; } return a; }",
vec![],
)
.await
}
pub async fn siblings(&self) -> Result<Vec<Element>> {
self.call_to_elements(
"node => node.parentElement \
? Array.from(node.parentElement.children).filter(c => c !== node) : []",
vec![],
)
.await
}
pub async fn shadow_root(&self) -> Result<ShadowRoot> {
let result = self.call("node => node.shadowRoot", vec![], false).await?;
match result.get("objectId").and_then(|v| v.as_str()) {
Some(oid) => Ok(ShadowRoot::new(
self.core.clone(),
oid.to_string(),
self.frame_id.clone(),
)),
None => Err(Error::Other("该元素没有 open shadow root".into())),
}
}
pub async fn scroll_into_view(&self) -> Result<()> {
self.core
.send_page(
"Page.scrollIntoViewIfNeeded",
json!({ "frameId": self.frame_id_ref(), "objectId": self.object_id }),
)
.await?;
Ok(())
}
pub async fn click(&self) -> Result<()> {
self.scroll_into_view().await?;
let (x, y) = self.center_point().await?;
let base = |ty: &str, buttons: i64, click_count: i64| {
json!({
"type": ty,
"button": 0,
"buttons": buttons,
"x": x,
"y": y,
"modifiers": 0,
"clickCount": click_count,
})
};
self.core
.send_page("Page.dispatchMouseEvent", base("mousemove", 0, 0))
.await?;
self.core
.send_page("Page.dispatchMouseEvent", base("mousedown", 1, 1))
.await?;
self.core
.send_page("Page.dispatchMouseEvent", base("mouseup", 1, 1))
.await?;
Ok(())
}
pub async fn hover(&self) -> Result<()> {
self.scroll_into_view().await?;
let (x, y) = self.center_point().await?;
self.core.dispatch_mouse("mousemove", x, y, 0).await
}
pub async fn drag(&self, dx: f64, dy: f64, duration: f64) -> Result<()> {
self.scroll_into_view().await?;
let (x0, y0) = self.center_point().await?;
self.drag_from(x0, y0, dx, dy, duration).await
}
pub async fn drag_to(&self, x: f64, y: f64, duration: f64) -> Result<()> {
self.scroll_into_view().await?;
let (x0, y0) = self.center_point().await?;
self.drag_from(x0, y0, x - x0, y - y0, duration).await
}
async fn drag_from(&self, x0: f64, y0: f64, dx: f64, dy: f64, duration: f64) -> Result<()> {
let mut rng = Xorshift::new(seed_from_clock());
let steps = human_drag_track(dx, dy, duration, rng.next_u64());
self.core.dispatch_mouse("mousemove", x0, y0, 0).await?;
sleep(Duration::from_millis(rng.range_ms(20, 60))).await;
self.core.dispatch_mouse("mousedown", x0, y0, 1).await?;
sleep(Duration::from_millis(rng.range_ms(70, 140))).await;
for (ox, oy, delay) in steps {
self.core
.dispatch_mouse_fire("mousemove", x0 + ox, y0 + oy, 1)?;
if delay > 0 {
sleep(Duration::from_millis(delay)).await;
}
}
sleep(Duration::from_millis(rng.range_ms(60, 130))).await; self.core
.dispatch_mouse("mouseup", x0 + dx, y0 + dy, 0)
.await?;
Ok(())
}
pub async fn focus(&self) -> Result<()> {
self.call("node => node.focus()", vec![], false).await?;
Ok(())
}
pub async fn input(&self, text: &str) -> Result<()> {
self.focus().await?;
self.core
.send_page("Page.insertText", json!({ "text": text }))
.await?;
Ok(())
}
pub async fn input_human(&self, text: &str) -> Result<()> {
self.focus().await?;
let mut rng = Xorshift::new(seed_from_clock());
for ch in text.chars() {
let s = ch.to_string();
let _ = self
.core
.send_page(
"Page.dispatchKeyEvent",
json!({ "type": "keydown", "key": s }),
)
.await;
self.core
.send_page("Page.insertText", json!({ "text": s }))
.await?;
let _ = self
.core
.send_page(
"Page.dispatchKeyEvent",
json!({ "type": "keyup", "key": s }),
)
.await;
let delay = 30 + (rng.next_u64() % 110);
sleep(Duration::from_millis(delay)).await;
}
Ok(())
}
pub async fn input_keys(&self, parts: &[KeyInput]) -> Result<()> {
self.focus().await?;
for p in parts {
match p {
KeyInput::Text(t) => {
self.core
.send_page("Page.insertText", json!({ "text": t }))
.await?;
}
KeyInput::Key(k) => {
self.core.press_key(k).await?;
}
}
}
Ok(())
}
pub async fn select_value(&self, value: &str) -> Result<()> {
self.call(
"(node, val) => { node.value = val; \
node.dispatchEvent(new Event('input', { bubbles: true })); \
node.dispatchEvent(new Event('change', { bubbles: true })); }",
vec![json!({ "value": value })],
true,
)
.await?;
Ok(())
}
pub async fn select_text(&self, text: &str) -> Result<()> {
self.call(
"(node, t) => { for (const o of node.options) { \
if ((o.textContent || '').includes(t)) { node.value = o.value; \
node.dispatchEvent(new Event('input', { bubbles: true })); \
node.dispatchEvent(new Event('change', { bubbles: true })); return true; } } return false; }",
vec![json!({ "value": text })],
true,
)
.await?;
Ok(())
}
pub async fn set_checked(&self, checked: bool) -> Result<()> {
self.call(
"(node, c) => { if (node.checked !== c) { node.checked = c; \
node.dispatchEvent(new Event('input', { bubbles: true })); \
node.dispatchEvent(new Event('change', { bubbles: true })); } }",
vec![json!({ "value": checked })],
true,
)
.await?;
Ok(())
}
pub async fn is_checked(&self) -> Result<bool> {
let v = self.call("node => !!node.checked", vec![], true).await?;
Ok(v.as_bool().unwrap_or(false))
}
pub async fn clear(&self) -> Result<()> {
self.call(
"node => { node.focus(); if ('value' in node) { node.value = ''; \
node.dispatchEvent(new Event('input', { bubbles: true })); \
node.dispatchEvent(new Event('change', { bubbles: true })); } \
else if (node.isContentEditable) { node.textContent = ''; } }",
vec![],
false,
)
.await?;
Ok(())
}
pub async fn set_files(&self, paths: &[&str]) -> Result<()> {
let files: Vec<String> = paths.iter().map(|p| p.to_string()).collect();
self.core
.send_page(
"Page.setFileInputFiles",
json!({
"frameId": self.frame_id_ref(),
"objectId": self.object_id,
"files": files,
}),
)
.await?;
Ok(())
}
pub async fn upload(&self, path: &str) -> Result<()> {
self.set_files(&[path]).await
}
pub async fn click_to_upload(&self, paths: &[&str], timeout: Option<Duration>) -> Result<bool> {
if paths.is_empty() {
return Err(Error::Other("click_to_upload: 文件列表为空".into()));
}
let files = paths.iter().map(|p| p.to_string()).collect();
self.core.arm_upload(files).await?;
self.click().await?;
let d = timeout.unwrap_or_else(|| self.core.timeout());
self.core.wait_upload(d).await
}
pub async fn content_frame(&self) -> Result<Frame> {
let r = self
.core
.send_page(
"Page.describeNode",
json!({ "frameId": self.frame_id_ref(), "objectId": self.object_id }),
)
.await?;
let cfid = r["contentFrameId"]
.as_str()
.ok_or_else(|| Error::Other("该元素不是 iframe,或其内容帧尚不可用".into()))?;
Ok(Frame::new(self.core.clone(), cfid.to_string()))
}
pub(crate) async fn center_point(&self) -> Result<(f64, f64)> {
let r = self
.core
.send_page(
"Page.getContentQuads",
json!({ "frameId": self.frame_id_ref(), "objectId": self.object_id }),
)
.await?;
let quad = r["quads"]
.as_array()
.and_then(|a| a.first())
.ok_or_else(|| Error::Other("元素无可见内容区(可能不可见)".into()))?;
let mut sx = 0.0;
let mut sy = 0.0;
for p in ["p1", "p2", "p3", "p4"] {
sx += quad[p]["x"].as_f64().unwrap_or(0.0);
sy += quad[p]["y"].as_f64().unwrap_or(0.0);
}
Ok((sx / 4.0, sy / 4.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ElementRect {
pub x: f64,
pub y: f64,
pub viewport_x: f64,
pub viewport_y: f64,
pub width: f64,
pub height: f64,
}
pub struct ElementWait {
ele: Element,
}
impl ElementWait {
fn timeout_or_default(&self, t: Option<Duration>) -> Duration {
t.unwrap_or_else(|| self.ele.core.timeout())
}
pub async fn displayed(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, || self.ele.is_displayed()).await
}
pub async fn hidden(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, || async {
Ok(!self.ele.is_displayed().await.unwrap_or(false))
})
.await
}
pub async fn deleted(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, || async {
match self
.ele
.call("node => node.isConnected", vec![], true)
.await
{
Ok(v) => Ok(!v.as_bool().unwrap_or(false)),
Err(_) => Ok(true),
}
})
.await
}
pub async fn clickable(&self, timeout: Option<Duration>) -> Result<bool> {
self.poll(timeout, || self.ele.is_clickable()).await
}
async fn poll<F, Fut>(&self, timeout: Option<Duration>, check: F) -> Result<bool>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<bool>>,
{
let deadline = Instant::now() + self.timeout_or_default(timeout);
loop {
if check().await.unwrap_or(false) {
return Ok(true);
}
if Instant::now() >= deadline {
return Ok(false);
}
sleep(Duration::from_millis(100)).await;
}
}
}
struct Xorshift(u64);
impl Xorshift {
fn new(seed: u64) -> Self {
Xorshift(seed | 1)
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn unit(&mut self) -> f64 {
(self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
}
fn range_ms(&mut self, lo: u64, hi: u64) -> u64 {
if hi <= lo {
lo
} else {
lo + self.next_u64() % (hi - lo)
}
}
}
fn seed_from_clock() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x9E37_79B9_7F4A_7C15)
}
fn human_drag_track(dx: f64, dy: f64, duration_secs: f64, seed: u64) -> Vec<(f64, f64, u64)> {
let mut rng = Xorshift::new(seed);
let dist = (dx * dx + dy * dy).sqrt();
let dur_ms = if duration_secs > 0.0 {
(duration_secs * 1000.0).round()
} else {
(dist * 3.5 + 320.0).clamp(350.0, 1600.0)
};
let n = ((dur_ms / 13.0).round() as usize).clamp(24, 160);
let base = (dur_ms / n as f64).max(4.0); let overshoot = if dist > 40.0 {
1.0 + 0.02 + rng.unit() * 0.04
} else {
1.0
};
let fwd = ((n as f64) * 0.82) as usize;
let back = n.saturating_sub(fwd).max(3);
let drift_y = (rng.unit() - 0.5) * 6.0;
let mj = |t: f64| 10.0 * t.powi(3) - 15.0 * t.powi(4) + 6.0 * t.powi(5);
let delay = |rng: &mut Xorshift| -> u64 {
let jit = (rng.unit() - 0.5) * 0.8; ((base * (1.0 + jit)).round() as u64).max(3)
};
let mut out = Vec::with_capacity(n + 2);
for i in 1..=fwd {
let t = i as f64 / fwd as f64;
let frac = overshoot * mj(t);
let tremor_x = (rng.unit() - 0.5) * 1.0;
let tremor_y = (rng.unit() - 0.5) * 1.6;
out.push((
dx * frac + tremor_x,
dy * frac + drift_y * mj(t) + tremor_y,
delay(&mut rng),
));
}
for i in 1..=back {
let t = i as f64 / back as f64;
let frac = overshoot - (overshoot - 1.0) * mj(t);
let tremor_x = (rng.unit() - 0.5) * 0.8;
let tremor_y = (rng.unit() - 0.5) * 1.2;
out.push((
dx * frac + tremor_x,
dy * frac + drift_y + tremor_y,
delay(&mut rng) + 2,
));
}
out.push((dx, dy, base.round() as u64));
let pauses = 1 + (rng.unit() * 2.0) as usize;
for _ in 0..pauses {
if fwd > 4 {
let idx = 2 + (rng.unit() * (fwd as f64 - 4.0)) as usize;
if let Some(p) = out.get_mut(idx) {
p.2 += rng.range_ms(25, 75);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::human_drag_track;
#[test]
fn drag_track_lands_exactly_on_target() {
let track = human_drag_track(200.0, 0.0, 0.8, 12345);
let last = *track.last().unwrap();
assert_eq!((last.0, last.1), (200.0, 0.0));
assert!(track.len() >= 24);
}
#[test]
fn drag_track_overshoots_then_returns() {
let track = human_drag_track(200.0, 0.0, 0.8, 999);
let peak = track.iter().map(|p| p.0).fold(0.0_f64, f64::max);
assert!(peak > 200.0, "应有过冲,peak={peak}");
assert_eq!(track.last().unwrap().0, 200.0);
}
#[test]
fn drag_track_monotonic_delays_positive() {
let track = human_drag_track(120.0, 5.0, 0.0, 7);
assert!(track.iter().all(|p| p.2 > 0));
}
#[test]
fn drag_track_dense_sampling_matches_duration() {
let track = human_drag_track(180.0, 0.0, 0.8, 42);
assert!(track.len() >= 45, "采样应密集,len={}", track.len());
let total: u64 = track.iter().map(|p| p.2).sum();
assert!((700..=1100).contains(&total), "总时长≈0.8s,实得 {total}ms");
let xs: Vec<f64> = track.iter().map(|p| p.0).collect();
let mid = xs.len() / 2;
let v_mid = xs[mid] - xs[mid - 1];
let v_start = xs[1] - xs[0];
assert!(v_mid > v_start, "中段应比起步快(钟形速度)");
}
}