Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
273e4df
feat: add compare-heapsnapshots command for offline heap snapshot com…
aeroxy Jul 3, 2026
612a303
fix: improve sorting of snapshot diffs by class name as a secondary c…
aeroxy Jul 4, 2026
9f5098e
refactor: improve readability and consistency
aeroxy Jul 5, 2026
e91d0ee
perf(memory): reduce allocations and peak memory during snapshot diff
aeroxy Jul 6, 2026
2b3f4fd
feat: improve Chrome devtools connection handling and safeguard daemo…
aeroxy Jul 7, 2026
15b1efb
fix(memory): validate snapshot integrity and standardize error handling
aeroxy Jul 7, 2026
ef2fcb6
refactor(memory): simplify class aggregate handling and update snapsh…
aeroxy Jul 7, 2026
3f178a6
refactor(memory): optimize class aggregate handling in build_class_ag…
aeroxy Jul 7, 2026
2e24379
refactor(memory): optimize snapshot diff processing by modifying argu…
aeroxy Jul 7, 2026
f1a4712
refactor(memory): streamline class aggregate handling and improve CSV…
aeroxy Jul 8, 2026
093bfa6
refactor(memory): optimize class aggregate insertion to reduce alloca…
aeroxy Jul 8, 2026
cfb3705
refactor(memory): extract node field offset resolution to improve cla…
aeroxy Jul 9, 2026
41f6c67
refactor(memory): enhance class aggregation by introducing type-based…
aeroxy Jul 9, 2026
5a52e58
feat: enhance heap snapshot diffing, validation, and offline tooling
aeroxy Jul 10, 2026
32dea10
perf: optimize heap snapshot diff detail computation and ID overlap d…
aeroxy Jul 10, 2026
9ae27ee
refactor: simplify error handling in heap snapshot capture
aeroxy Jul 10, 2026
0c9120b
refactor: improve kill daemon decision logic for interactive prompts
aeroxy Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 72 additions & 26 deletions src/commands/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ pub async fn run_script(
}
}

let nav_url = if interpolated_url.starts_with("http://") || interpolated_url.starts_with("https://") {
let nav_url = if interpolated_url.starts_with("http://")
|| interpolated_url.starts_with("https://")
{
interpolated_url.clone()
} else if is_local_host(&interpolated_url) {
format!("http://{}", interpolated_url)
Expand All @@ -258,7 +260,10 @@ pub async fn run_script(

let current_url = client.current_url(session_id).await?;
if current_url.trim_end_matches('/') != nav_url.trim_end_matches('/') {
eprintln!("[script] Current URL '{}' does not match target URL '{}'. Auto-navigating...", current_url, nav_url);
eprintln!(
"[script] Current URL '{}' does not match target URL '{}'. Auto-navigating...",
current_url, nav_url
);

crate::commands::navigate::navigate(
client,
Expand Down Expand Up @@ -327,7 +332,10 @@ fn parse_adapter_domains(content: &str) -> Vec<String> {
if trimmed.starts_with("import ") || trimmed.starts_with("import(") {
continue;
}
if matches!(trimmed, "\"use strict\";" | "'use strict';" | "\"use strict\"" | "'use strict'") {
if matches!(
trimmed,
"\"use strict\";" | "'use strict';" | "\"use strict\"" | "'use strict'"
) {
continue;
}
break;
Expand Down Expand Up @@ -516,26 +524,29 @@ pub async fn run_adapter(
let domains = parse_adapter_domains(&script_content);
if !domains.is_empty() {
let current_url = client.current_url(session_id).await?;
let matched = domains.iter().any(|domain| url_matches_domain(&current_url, domain));
let matched = domains
.iter()
.any(|domain| url_matches_domain(&current_url, domain));

if !matched {
let target_domain = &domains[0];
// Preserve the host exactly as declared in `@domain`; only supply a
// scheme when one is missing. Forcing a `www.` subdomain breaks apex
// hosts and adapters that target an existing subdomain
// (e.g. `creator.xiaohongshu.com`).
let target_url = if target_domain.starts_with("http://") || target_domain.starts_with("https://") {
// An explicit scheme always wins, so authors can force http/https
// by writing it in `@domain` (e.g. `@domain http://localhost:3000`).
target_domain.clone()
} else if is_local_host(target_domain) {
// Local dev servers generally speak http, not https.
format!("http://{}", target_domain)
} else {
format!("https://{}", target_domain)
};
let target_url =
if target_domain.starts_with("http://") || target_domain.starts_with("https://") {
// An explicit scheme always wins, so authors can force http/https
// by writing it in `@domain` (e.g. `@domain http://localhost:3000`).
target_domain.clone()
} else if is_local_host(target_domain) {
// Local dev servers generally speak http, not https.
format!("http://{}", target_domain)
} else {
format!("https://{}", target_domain)
};
eprintln!("[adapter] Current URL '{}' does not match adapter domains {:?}. Auto-navigating to '{}'...", current_url, domains, target_url);

crate::commands::navigate::navigate(
client,
session_id,
Expand All @@ -549,7 +560,9 @@ pub async fn run_adapter(
.await?;

let post_nav_url = client.current_url(session_id).await?;
let post_matched = domains.iter().any(|domain| url_matches_domain(&post_nav_url, domain));
let post_matched = domains
.iter()
.any(|domain| url_matches_domain(&post_nav_url, domain));
if !post_matched {
anyhow::bail!(
"Auto-navigation to '{}' resulted in URL '{}' which does not match adapter domains {:?}",
Expand Down Expand Up @@ -720,7 +733,10 @@ mod tests {
// (only declarations and bare re-exports are handled).
let src = "export * from './x';\nexport const ok = 1;\nexport constants = 2;";
let out = strip_export_keywords(src);
assert_eq!(out, "export * from './x';\nconst ok = 1;\nexport constants = 2;");
assert_eq!(
out,
"export * from './x';\nconst ok = 1;\nexport constants = 2;"
);
}

#[test]
Expand Down Expand Up @@ -748,9 +764,15 @@ mod tests {

#[test]
fn test_normalize_host() {
assert_eq!(normalize_host("http://user:pass@example.com/some/path"), "example.com");
assert_eq!(
normalize_host("http://user:pass@example.com/some/path"),
"example.com"
);
assert_eq!(normalize_host("http://user:pass@[::1]:8080"), "[::1]");
assert_eq!(normalize_host("http://user:pass@127.0.0.1:8080"), "127.0.0.1");
assert_eq!(
normalize_host("http://user:pass@127.0.0.1:8080"),
"127.0.0.1"
);
assert_eq!(normalize_host("https://foo:bar@localhost"), "localhost");
assert_eq!(normalize_host("example.com"), "example.com");
assert_eq!(normalize_host("http://example.com:3000/"), "example.com");
Expand All @@ -772,19 +794,37 @@ mod tests {

#[test]
fn test_url_matches_domain() {
assert!(url_matches_domain("https://www.xiaohongshu.com/explore", "xiaohongshu.com"));
assert!(url_matches_domain("http://creator.xiaohongshu.com", "creator.xiaohongshu.com"));
assert!(url_matches_domain("https://xiaohongshu.com:8080/path", "xiaohongshu.com"));
assert!(url_matches_domain(
"https://www.xiaohongshu.com/explore",
"xiaohongshu.com"
));
assert!(url_matches_domain(
"http://creator.xiaohongshu.com",
"creator.xiaohongshu.com"
));
assert!(url_matches_domain(
"https://xiaohongshu.com:8080/path",
"xiaohongshu.com"
));
assert!(url_matches_domain("http://[::1]:3000", "[::1]"));
assert!(!url_matches_domain("https://google.com", "xiaohongshu.com"));
}

#[test]
fn test_url_matches_domain_normalizes_domain() {
// `@domain` written with a scheme and/or path still matches the host.
assert!(url_matches_domain("https://www.example.com/page", "https://example.com"));
assert!(url_matches_domain("https://example.com/explore", "example.com/path"));
assert!(url_matches_domain("https://example.com", "http://example.com:443/"));
assert!(url_matches_domain(
"https://www.example.com/page",
"https://example.com"
));
assert!(url_matches_domain(
"https://example.com/explore",
"example.com/path"
));
assert!(url_matches_domain(
"https://example.com",
"http://example.com:443/"
));
assert!(!url_matches_domain("https://example.com", ""));
}

Expand All @@ -800,7 +840,13 @@ mod tests {
let ctx = build_ctx_object(r#"{"query":"hi"}"#);
assert!(ctx.starts_with("const ctx = {"));
assert!(ctx.contains(r#"args: {"query":"hi"}"#));
for helper in ["wait:", "waitForText:", "waitForSelector:", "click:", "fill:"] {
for helper in [
"wait:",
"waitForText:",
"waitForSelector:",
"click:",
"fill:",
] {
assert!(ctx.contains(helper), "missing helper: {helper}");
}
// fill must special-case checkable inputs instead of setting `value`.
Expand Down
28 changes: 24 additions & 4 deletions src/commands/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub fn known_args(cmd: &str) -> &'static [&'static str] {
"read-page" => &["output"],
"take-heapsnapshot" => &["output"],
"inspect-heapsnapshot-node" => &["file_path", "node_id"],
"compare-heapsnapshots" => &["base", "current", "class_index"],
"emulate" => &[
"viewport",
"device_scale_factor",
Expand All @@ -72,8 +73,21 @@ pub fn known_args(cmd: &str) -> &'static [&'static str] {
"console" => &["duration", "type"],
"network" => &["duration", "type"],
"sw-logs" => &["duration", "extension_id"],
"run-script" => &["file_path", "script_args", "raw_args", "output", "track_navigation"],
"adapter" => &["file_path", "function_name", "script_args", "raw_args", "output", "track_navigation"],
"run-script" => &[
"file_path",
"script_args",
"raw_args",
"output",
"track_navigation",
],
"adapter" => &[
"file_path",
"function_name",
"script_args",
"raw_args",
"output",
"track_navigation",
],
"kill-daemon" => &[],
_ => &[],
}
Expand Down Expand Up @@ -325,7 +339,10 @@ fn script_exec_args(args: &serde_json::Value) -> Result<ScriptExecArgs<'_>> {
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("file_path required"))?;
let script_args = args.get("script_args").cloned().unwrap_or_else(|| json!({}));
let script_args = args
.get("script_args")
.cloned()
.unwrap_or_else(|| json!({}));
let output = args.get("output").and_then(|v| v.as_str());
let track_navigation = args
.get("track_navigation")
Expand Down Expand Up @@ -412,7 +429,10 @@ async fn inner_execute(
client,
session_id,
commands::screenshot::ScreenshotOptions {
output: args.get("output").and_then(|v| v.as_str()).map(String::from),
output: args
.get("output")
.and_then(|v| v.as_str())
.map(String::from),
format: args
.get("format")
.and_then(|v| v.as_str())
Expand Down
Loading