use std::path::PathBuf;
use std::time::Duration;
use vs_protocol::{Op, Ref, Tree};
pub const DEFAULT_USER_AGENT: &str =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 \
(KHTML, like Gecko) Version/17.5 Safari/605.1.15";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PageHandle(pub u64);
#[derive(Debug, Clone)]
pub enum ActTarget {
Ref(Ref),
Mark(String),
}
#[derive(Debug, Clone)]
pub enum Action {
Click,
Fill { value: String },
Scroll,
Key { chord: String },
Submit,
Hover,
Focus,
}
impl Action {
#[must_use]
pub fn op(&self) -> Op {
match self {
Self::Click => Op::Click,
Self::Fill { .. } => Op::Fill,
Self::Scroll => Op::Scroll,
Self::Key { .. } => Op::Key,
Self::Submit => Op::Submit,
Self::Hover => Op::Hover,
Self::Focus => Op::Focus,
}
}
}
#[derive(Debug, Clone)]
pub enum WaitCondition {
Stable,
NetIdle,
RefAppears(Ref),
RefGone(Ref),
Text(String),
TokenChange,
}
#[derive(Debug, Clone)]
pub enum CaptureScope {
Viewport,
Element(Ref),
FullPage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Viewport {
pub width: u32,
pub height: u32,
pub dpr: u32,
}
impl Viewport {
pub const MOBILE_S: Self = Self::new(320, 568, 2);
pub const MOBILE: Self = Self::new(390, 844, 2);
pub const MOBILE_L: Self = Self::new(414, 896, 2);
pub const TABLET: Self = Self::new(768, 1024, 2);
pub const TABLET_L: Self = Self::new(1024, 768, 2);
pub const LAPTOP: Self = Self::new(1366, 768, 2);
pub const DESKTOP: Self = Self::new(1920, 1080, 2);
pub const DESKTOP_XL: Self = Self::new(2560, 1440, 2);
pub const WIDE: Self = Self::new(3440, 1440, 2);
#[must_use]
pub const fn new(width: u32, height: u32, dpr: u32) -> Self {
Self { width, height, dpr }
}
#[must_use]
pub fn preset(name: &str) -> Option<Self> {
match name {
"mobile-s" => Some(Self::MOBILE_S),
"mobile" => Some(Self::MOBILE),
"mobile-l" => Some(Self::MOBILE_L),
"tablet" => Some(Self::TABLET),
"tablet-l" => Some(Self::TABLET_L),
"laptop" => Some(Self::LAPTOP),
"desktop" => Some(Self::DESKTOP),
"desktop-xl" => Some(Self::DESKTOP_XL),
"wide" => Some(Self::WIDE),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LayoutBox {
pub r: Ref,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub visible: bool,
pub z_index: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::struct_excessive_bools)]
pub struct EngineCapabilities {
pub renders: bool,
pub honors_viewport: bool,
pub measures_layout: bool,
pub persists_auth: bool,
pub inspector_console: bool,
pub inspector_network: bool,
pub name: &'static str,
pub version: &'static str,
}
impl EngineCapabilities {
pub const TEST: Self = Self {
renders: true,
honors_viewport: true,
measures_layout: true,
persists_auth: true,
inspector_console: true,
inspector_network: true,
name: "test",
version: "",
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthBlob {
pub bytes: Vec<u8>,
}
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum EngineError {
#[error("engine `{engine}` does not support `{primitive}`")]
Unsupported {
engine: &'static str,
primitive: &'static str,
},
#[error("timeout after {budget:?}: {primitive}")]
Timeout {
budget: Duration,
primitive: &'static str,
},
#[error("not found: {kind} {id}")]
NotFound { kind: &'static str, id: String },
#[error("engine thread crashed")]
Crashed,
#[error("engine has shut down")]
Closed,
#[error("engine `{engine}` has not implemented `{primitive}`")]
NotImplemented {
engine: &'static str,
primitive: &'static str,
},
#[error("{0}")]
Other(String),
}
pub type EngineResult<T> = std::result::Result<T, EngineError>;
pub trait Engine {
fn open(&mut self, url: &str) -> EngineResult<PageHandle>;
fn close(&mut self, page: PageHandle) -> EngineResult<()>;
fn snapshot(&mut self, page: PageHandle) -> EngineResult<Tree>;
fn act(&mut self, page: PageHandle, target: ActTarget, action: Action) -> EngineResult<()>;
fn wait(&mut self, page: PageHandle, cond: WaitCondition, budget: Duration)
-> EngineResult<()>;
fn capture(&mut self, page: PageHandle, scope: CaptureScope) -> EngineResult<PathBuf>;
fn layout(&mut self, page: PageHandle, refs: &[Ref]) -> EngineResult<Vec<LayoutBox>>;
fn set_viewport(&mut self, page: PageHandle, viewport: Viewport) -> EngineResult<()>;
fn save_auth(&mut self, page: PageHandle) -> EngineResult<AuthBlob>;
fn load_auth(&mut self, page: PageHandle, blob: &AuthBlob) -> EngineResult<()>;
fn console_entries(
&mut self,
_page: PageHandle,
) -> EngineResult<Vec<crate::inspector::ConsoleEntry>> {
Ok(Vec::new())
}
fn network_entries(
&mut self,
_page: PageHandle,
) -> EngineResult<Vec<crate::inspector::NetworkEntry>> {
Ok(Vec::new())
}
fn request_detail(
&mut self,
_page: PageHandle,
_seq: u64,
) -> EngineResult<Option<crate::inspector::RequestDetail>> {
Ok(None)
}
fn eval_js(
&mut self,
_page: PageHandle,
_expr: &str,
) -> EngineResult<crate::inspector::EvalResult> {
Ok(crate::inspector::EvalResult::Thrown {
kind: "NotImplemented".into(),
message: "engine does not implement vs_inspect eval".into(),
})
}
fn storage(
&mut self,
_page: PageHandle,
_scope: crate::inspector::StorageScope,
) -> EngineResult<Vec<crate::inspector::StorageEntry>> {
Ok(Vec::new())
}
fn scripts(&mut self, _page: PageHandle) -> EngineResult<Vec<crate::inspector::ScriptEntry>> {
Ok(Vec::new())
}
fn script_source(
&mut self,
_page: PageHandle,
_seq: u64,
) -> EngineResult<Option<crate::inspector::ScriptSource>> {
Ok(None)
}
fn dom(
&mut self,
_page: PageHandle,
_r: vs_protocol::Ref,
_extra_props: &[String],
) -> EngineResult<Option<crate::inspector::DomDetail>> {
Ok(None)
}
fn performance(
&mut self,
_page: PageHandle,
) -> EngineResult<crate::inspector::PerformanceMetrics> {
Ok(crate::inspector::PerformanceMetrics::default())
}
fn capabilities(&self) -> EngineCapabilities;
}
pub use vs_protocol::Role as NodeRole;
#[cfg(test)]
mod tests {
use super::DEFAULT_USER_AGENT;
#[test]
fn default_user_agent_has_safari_suffix() {
let ua = DEFAULT_USER_AGENT;
assert!(
ua.starts_with("Mozilla/5.0 "),
"UA should start with `Mozilla/5.0 `; got {ua:?}",
);
assert!(
ua.contains("AppleWebKit/"),
"UA should advertise WebKit; got {ua:?}",
);
assert!(
ua.contains("Version/"),
"UA missing `Version/X` — anti-bot will flag it; got {ua:?}",
);
assert!(
ua.contains("Safari/"),
"UA missing `Safari/X` — anti-bot will flag it; got {ua:?}",
);
assert!(
!ua.contains('\n') && !ua.contains('\r') && !ua.contains('\t'),
"UA must be a single line; got {ua:?}",
);
}
}