use super::support::{MockRdpServer, load_fixture};
use serde_json::json;
fn ff_rdp_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_ff-rdp"))
}
fn base_args(port: u16) -> Vec<String> {
vec![
"--host".to_owned(),
"127.0.0.1".to_owned(),
"--port".to_owned(),
port.to_string(),
"--no-daemon".to_owned(),
]
}
fn console_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on(
"getCachedMessages",
load_fixture("get_cached_messages_response.json"),
)
}
#[test]
fn console_shows_all_messages() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("console".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert_eq!(json["total"], 3);
let results = json["results"].as_array().expect("results is array");
assert_eq!(results[0]["level"], "error");
assert_eq!(results[0]["message"], "error msg");
assert_eq!(results[1]["level"], "warn");
assert_eq!(results[1]["message"], "warning msg");
assert_eq!(results[2]["level"], "log");
assert_eq!(results[2]["message"], "hello from test");
}
#[test]
fn console_filter_by_level() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--level".to_owned(),
"error".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 1);
let results = json["results"].as_array().unwrap();
assert_eq!(results[0]["level"], "error");
assert_eq!(results[0]["message"], "error msg");
}
#[test]
fn console_filter_by_pattern() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--pattern".to_owned(),
"warn".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 1);
let results = json["results"].as_array().unwrap();
assert_eq!(results[0]["message"], "warning msg");
}
#[test]
fn console_with_jq_filter() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--jq".to_owned(),
".results[] | select(.level == \"error\") | .message".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), r#""error msg""#);
}
#[test]
fn console_level_and_pattern_combined() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--level".to_owned(),
"log".to_owned(),
"--pattern".to_owned(),
"hello".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 1);
let results = json["results"].as_array().unwrap();
assert_eq!(results[0]["message"], "hello from test");
}
#[test]
fn console_no_match_returns_empty() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--level".to_owned(),
"debug".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 0);
assert_eq!(json["results"].as_array().unwrap().len(), 0);
}
#[test]
fn console_handles_page_error_messages() {
let response_with_page_errors = serde_json::json!({
"from": "server1.conn0.child2/consoleActor3",
"messages": [
{
"message": {
"arguments": ["hello"],
"level": "log",
"filename": "test.js",
"lineNumber": 1,
"columnNumber": 1,
"timeStamp": 1000.0
},
"type": "consoleAPICall"
},
{
"pageError": {
"errorMessage": "ReferenceError: foo is not defined",
"sourceName": "https://example.com/app.js",
"lineNumber": 42,
"columnNumber": 5,
"timeStamp": 2000.0
},
"type": "pageError"
}
]
});
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on("getCachedMessages", response_with_page_errors);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("console".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 2);
let results = json["results"].as_array().unwrap();
assert_eq!(results[0]["level"], "error");
assert_eq!(results[0]["message"], "ReferenceError: foo is not defined");
assert_eq!(results[1]["level"], "log");
assert_eq!(results[1]["message"], "hello");
}
fn large_console_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on(
"getCachedMessages",
load_fixture("get_cached_messages_large_response.json"),
)
}
#[test]
fn console_summary_present_in_output() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("console".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let summary = &json["summary"];
assert!(!summary.is_null(), "summary field must be present");
assert_eq!(summary["total"], 3, "total = raw message count");
assert_eq!(
summary["matched"], 3,
"matched = after filter, before limit"
);
assert_eq!(summary["shown"], 3, "shown = actual results length");
let by_level = summary["by_level"].as_object().unwrap();
assert_eq!(by_level["log"], 1);
assert_eq!(by_level["warn"], 1);
assert_eq!(by_level["error"], 1);
}
#[test]
fn console_limit_10_summary_reflects_true_total() {
let server = large_console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--limit".to_owned(), "10".to_owned(), "console".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let results = json["results"].as_array().unwrap();
assert_eq!(
results.len(),
10,
"results.length must be 10 with --limit 10"
);
let summary = &json["summary"];
assert_eq!(
summary["total"], 110,
"summary.total must reflect all 110 messages in the fixture"
);
assert_eq!(summary["matched"], 110, "matched = all 110 (no filter)");
assert_eq!(summary["shown"], 10, "shown = 10 after --limit");
}
#[test]
fn console_summary_matched_reflects_filter_count() {
let server = large_console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--level".to_owned(),
"error".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(output.status.success());
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let summary = &json["summary"];
assert_eq!(
summary["total"], 110,
"total is always the raw pre-filter count"
);
assert_eq!(
summary["matched"], 22,
"matched reflects count after --level filter"
);
assert_eq!(
summary["shown"], 22,
"all 22 matched fit within default limit"
);
}
#[test]
fn console_limit_truncates_results() {
let server = console_server();
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["--limit".to_owned(), "2".to_owned(), "console".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["total"], 3);
assert_eq!(json["results"].as_array().unwrap().len(), 2);
assert_eq!(json["truncated"], true);
assert!(json["hint"].as_str().unwrap().contains("--all"));
}
fn follow_server_with_events(console_event: serde_json::Value) -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![console_event],
)
.on(
"unwatchResources",
load_fixture("unwatch_resources_response.json"),
)
.close_after_followups()
}
#[test]
fn console_follow_streams_messages_as_ndjson() {
let console_event = json!({
"type": "resources-available-array",
"from": "server1.conn0.watcher4",
"array": [
["console-message", [
{
"resourceType": "console-message",
"message": {
"arguments": ["live message 1"],
"level": "log",
"filename": "test.js",
"lineNumber": 1,
"columnNumber": 1,
"timeStamp": 1000.0
}
},
{
"resourceType": "console-message",
"message": {
"arguments": ["live message 2"],
"level": "warn",
"filename": "test.js",
"lineNumber": 2,
"columnNumber": 1,
"timeStamp": 2000.0
}
}
]]
]
});
let server = follow_server_with_events(console_event);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["console".to_owned(), "--follow".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("server thread panicked");
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(lines.len(), 2, "expected 2 NDJSON lines, got: {stdout}");
let msg1: serde_json::Value =
serde_json::from_str(lines[0]).expect("line 1 must be valid JSON");
assert_eq!(msg1["level"], "log");
assert_eq!(msg1["message"], "live message 1");
assert_eq!(msg1["source"], "test.js");
let msg2: serde_json::Value =
serde_json::from_str(lines[1]).expect("line 2 must be valid JSON");
assert_eq!(msg2["level"], "warn");
assert_eq!(msg2["message"], "live message 2");
}
#[test]
fn console_follow_level_filter_applies_to_stream() {
let console_event = json!({
"type": "resources-available-array",
"from": "server1.conn0.watcher4",
"array": [
["console-message", [
{
"resourceType": "console-message",
"message": {
"arguments": ["debug noise"],
"level": "debug",
"filename": "app.js",
"lineNumber": 10,
"columnNumber": 1,
"timeStamp": 500.0
}
},
{
"resourceType": "console-message",
"message": {
"arguments": ["important warning"],
"level": "warn",
"filename": "app.js",
"lineNumber": 20,
"columnNumber": 1,
"timeStamp": 600.0
}
}
]]
]
});
let server = follow_server_with_events(console_event);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend([
"console".to_owned(),
"--follow".to_owned(),
"--level".to_owned(),
"warn".to_owned(),
]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("server thread panicked");
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(lines.len(), 1, "expected only 1 (warn) line, got: {stdout}");
let msg: serde_json::Value = serde_json::from_str(lines[0]).expect("output must be valid JSON");
assert_eq!(msg["level"], "warn");
assert_eq!(msg["message"], "important warning");
}
fn follow_server_with_direct_notification(notification: serde_json::Value) -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![notification],
)
.on(
"unwatchResources",
load_fixture("unwatch_resources_response.json"),
)
.close_after_followups()
}
#[test]
fn console_follow_handles_direct_consoleapicall_notification() {
let notification = json!({
"type": "consoleAPICall",
"from": "server1.conn0.child2/consoleActor3",
"message": {
"arguments": ["eval log output"],
"level": "log",
"filename": "debugger eval code",
"lineNumber": 1,
"columnNumber": 9,
"timeStamp": 1_775_439_071_165.699_f64
}
});
let server = follow_server_with_direct_notification(notification);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["console".to_owned(), "--follow".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("server thread panicked");
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(
lines.len(),
1,
"expected 1 NDJSON line from direct push, got: {stdout}"
);
let msg: serde_json::Value = serde_json::from_str(lines[0]).expect("output must be valid JSON");
assert_eq!(msg["level"], "log");
assert_eq!(msg["message"], "eval log output");
assert_eq!(msg["source"], "debugger eval code");
}
#[test]
fn console_follow_handles_direct_pageerror_notification() {
let notification = json!({
"type": "pageError",
"from": "server1.conn0.child2/consoleActor3",
"pageError": {
"errorMessage": "ReferenceError: x is not defined",
"sourceName": "https://example.com/app.js",
"lineNumber": 42,
"columnNumber": 5,
"timeStamp": 1_775_439_071_200.0_f64
}
});
let server = follow_server_with_direct_notification(notification);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.extend(["console".to_owned(), "--follow".to_owned()]);
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("server thread panicked");
assert!(
output.status.success(),
"expected success, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.trim().lines().collect();
assert_eq!(
lines.len(),
1,
"expected 1 NDJSON line from direct pageError push, got: {stdout}"
);
let msg: serde_json::Value = serde_json::from_str(lines[0]).expect("output must be valid JSON");
assert_eq!(msg["level"], "error");
assert_eq!(msg["message"], "ReferenceError: x is not defined");
assert_eq!(msg["source"], "https://example.com/app.js");
}