use crate::core::engine::EngineType;
pub const AUTOMATION_CONTROLLED_OFF_ARG: &str = "--disable-blink-features=AutomationControlled";
const BLINK_FEATURES_SWITCH: &str = "--disable-blink-features";
const REMOTE_DEBUGGING_PORT_SWITCH: &str = "--remote-debugging-port";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AutomationTrigger {
pub switch: &'static str,
pub reason: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedTrigger {
pub switch: &'static str,
pub argument: String,
pub reason: &'static str,
}
pub const AUTOMATION_CONTROLLED_TRIGGERS: &[AutomationTrigger] = &[
AutomationTrigger {
switch: "--enable-automation",
reason: "Mapped onto AutomationControlled in content/child/runtime_features.cc; also shows the \"controlled by automated test software\" infobar.",
},
AutomationTrigger {
switch: "--remote-debugging-pipe",
reason: "Mapped onto AutomationControlled in content/child/runtime_features.cc. Playwright always passes it, and Puppeteer passes it whenever pipe transport is selected.",
},
AutomationTrigger {
switch: "--remote-debugging-port=0",
reason: "An ephemeral debugging port is how ChromeDriver launches Chrome, so runtime_features.cc treats it as automation. A fixed non-zero port is deliberately not treated that way. Puppeteer defaults to port 0.",
},
];
pub const PLAYWRIGHT_HEADLESS_POINTER_ARG: &str =
"--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4";
pub const PLAYWRIGHT_SOFTWARE_WEBGL_ARG: &str = "--enable-unsafe-swiftshader";
fn switch_name(argument: &str) -> &str {
match argument.find('=') {
Some(index) => &argument[..index],
None => argument,
}
}
fn is_ephemeral_debugging_port(argument: &str) -> bool {
if switch_name(argument) != REMOTE_DEBUGGING_PORT_SWITCH {
return false;
}
match argument.split_once('=') {
Some((_, value)) => value.trim().parse::<i64>() == Ok(0),
None => false,
}
}
fn is_trigger(argument: &str, trigger: &AutomationTrigger) -> bool {
match trigger.switch {
"--remote-debugging-port=0" => is_ephemeral_debugging_port(argument),
other => switch_name(argument) == other,
}
}
pub fn detect_automation_controlled_triggers(args: &[String]) -> Vec<DetectedTrigger> {
let mut found = Vec::new();
for argument in args {
for trigger in AUTOMATION_CONTROLLED_TRIGGERS {
if is_trigger(argument, trigger) {
found.push(DetectedTrigger {
switch: trigger.switch,
argument: argument.clone(),
reason: trigger.reason,
});
}
}
}
found
}
pub fn disables_automation_controlled(args: &[String]) -> bool {
args.iter().any(|argument| {
if switch_name(argument) != BLINK_FEATURES_SWITCH {
return false;
}
match argument.split_once('=') {
Some((_, features)) => features
.split(',')
.any(|feature| feature.trim() == "AutomationControlled"),
None => false,
}
})
}
pub fn apply_automation_parity_args(args: &[String]) -> Vec<String> {
if disables_automation_controlled(args) {
return args.to_vec();
}
let mut merged = args.to_vec();
if let Some(existing) = merged
.iter_mut()
.find(|argument| switch_name(argument) == BLINK_FEATURES_SWITCH)
{
existing.push_str(",AutomationControlled");
return merged;
}
merged.push(AUTOMATION_CONTROLLED_OFF_ARG.to_string());
merged
}
pub fn parity_ignored_default_args(engine: EngineType, headless: bool) -> Vec<String> {
let mut ignored: Vec<String> = Vec::new();
match engine {
EngineType::Playwright => {
ignored.push("--enable-automation".to_string());
ignored.push(PLAYWRIGHT_SOFTWARE_WEBGL_ARG.to_string());
if headless {
ignored.push(PLAYWRIGHT_HEADLESS_POINTER_ARG.to_string());
}
}
EngineType::Puppeteer => ignored.push("--enable-automation".to_string()),
EngineType::Chromiumoxide | EngineType::Fantoccini => {}
}
ignored
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
#[test]
fn trigger_table_covers_every_switch_runtime_features_maps() {
let switches: Vec<&str> = AUTOMATION_CONTROLLED_TRIGGERS
.iter()
.map(|trigger| trigger.switch)
.collect();
assert_eq!(
switches,
[
"--enable-automation",
"--remote-debugging-pipe",
"--remote-debugging-port=0",
]
);
}
#[test]
fn every_trigger_explains_itself() {
for trigger in AUTOMATION_CONTROLLED_TRIGGERS {
assert!(trigger.reason.contains("runtime_features.cc"));
}
}
#[test]
fn detects_no_triggers_in_plain_arguments() {
assert!(detect_automation_controlled_triggers(&args(&["--no-first-run"])).is_empty());
}
#[test]
fn detects_enable_automation() {
let found = detect_automation_controlled_triggers(&args(&["--enable-automation"]));
assert_eq!(found.len(), 1);
assert_eq!(found[0].switch, "--enable-automation");
assert_eq!(found[0].argument, "--enable-automation");
}
#[test]
fn does_not_treat_headless_as_a_trigger() {
assert!(detect_automation_controlled_triggers(&args(&[
"--headless",
"--headless=new",
"--headless=old",
]))
.is_empty());
}
#[test]
fn detects_remote_debugging_pipe() {
let found = detect_automation_controlled_triggers(&args(&["--remote-debugging-pipe"]));
assert_eq!(found.len(), 1);
assert_eq!(found[0].switch, "--remote-debugging-pipe");
}
#[test]
fn detects_an_ephemeral_debugging_port() {
let found = detect_automation_controlled_triggers(&args(&["--remote-debugging-port=0"]));
assert_eq!(found.len(), 1);
assert_eq!(found[0].switch, "--remote-debugging-port=0");
}
#[test]
fn leaves_a_fixed_debugging_port_alone() {
assert!(
detect_automation_controlled_triggers(&args(&["--remote-debugging-port=9222"]))
.is_empty()
);
}
#[test]
fn ignores_a_debugging_port_that_is_not_a_number() {
assert!(
detect_automation_controlled_triggers(&args(&["--remote-debugging-port=auto"]))
.is_empty()
);
}
#[test]
fn does_not_match_a_switch_that_merely_starts_the_same() {
assert!(
detect_automation_controlled_triggers(&args(&["--enable-automation-extra"])).is_empty()
);
}
#[test]
fn reports_every_trigger_in_order() {
let found = detect_automation_controlled_triggers(&args(&[
"--remote-debugging-pipe",
"--headless",
"--no-first-run",
"--enable-automation",
]));
let switches: Vec<&str> = found.iter().map(|trigger| trigger.switch).collect();
assert_eq!(switches, ["--remote-debugging-pipe", "--enable-automation"]);
}
#[test]
fn disables_is_false_for_plain_arguments() {
assert!(!disables_automation_controlled(&args(&["--no-first-run"])));
}
#[test]
fn disables_is_true_for_the_exact_switch() {
assert!(disables_automation_controlled(&args(&[
AUTOMATION_CONTROLLED_OFF_ARG
])));
}
#[test]
fn disables_is_true_when_listed_among_other_features() {
assert!(disables_automation_controlled(&args(&[
"--disable-blink-features=AcceleratedSmallCanvases, AutomationControlled"
])));
}
#[test]
fn disables_is_false_for_a_different_feature() {
assert!(!disables_automation_controlled(&args(&[
"--disable-blink-features=Foo"
])));
}
#[test]
fn disables_is_false_when_the_feature_is_enabled_rather_than_disabled() {
assert!(!disables_automation_controlled(&args(&[
"--enable-blink-features=AutomationControlled"
])));
}
#[test]
fn appends_the_switch_to_plain_arguments() {
assert_eq!(
apply_automation_parity_args(&args(&["--no-first-run"])),
args(&["--no-first-run", AUTOMATION_CONTROLLED_OFF_ARG])
);
}
#[test]
fn appends_to_an_empty_list() {
assert_eq!(
apply_automation_parity_args(&[]),
args(&[AUTOMATION_CONTROLLED_OFF_ARG])
);
}
#[test]
fn applying_parity_is_idempotent() {
let once = apply_automation_parity_args(&args(&["--no-first-run"]));
assert_eq!(apply_automation_parity_args(&once), once);
}
#[test]
fn extends_an_existing_blink_feature_list_in_place() {
assert_eq!(
apply_automation_parity_args(&args(&[
"--disable-blink-features=Foo",
"--no-first-run"
])),
args(&[
"--disable-blink-features=Foo,AutomationControlled",
"--no-first-run"
])
);
}
#[test]
fn extends_only_the_first_blink_feature_list() {
assert_eq!(
apply_automation_parity_args(&args(&[
"--disable-blink-features=Foo",
"--disable-blink-features=Bar"
])),
args(&[
"--disable-blink-features=Foo,AutomationControlled",
"--disable-blink-features=Bar"
])
);
}
#[test]
fn playwright_headful_excludes_the_automation_switch() {
assert_eq!(
parity_ignored_default_args(EngineType::Playwright, false),
args(&["--enable-automation", PLAYWRIGHT_SOFTWARE_WEBGL_ARG])
);
}
#[test]
fn playwright_headless_also_excludes_the_pointer_switch() {
assert_eq!(
parity_ignored_default_args(EngineType::Playwright, true),
args(&[
"--enable-automation",
PLAYWRIGHT_SOFTWARE_WEBGL_ARG,
PLAYWRIGHT_HEADLESS_POINTER_ARG
])
);
}
#[test]
fn playwright_excludes_the_software_webgl_switch_in_both_modes() {
for headless in [false, true] {
assert!(
parity_ignored_default_args(EngineType::Playwright, headless)
.iter()
.any(|argument| argument == PLAYWRIGHT_SOFTWARE_WEBGL_ARG)
);
}
}
#[test]
fn puppeteer_has_no_headless_pointer_switch() {
assert_eq!(
parity_ignored_default_args(EngineType::Puppeteer, true),
args(&["--enable-automation"])
);
}
#[test]
fn engines_that_add_no_automation_defaults_exclude_nothing() {
assert!(parity_ignored_default_args(EngineType::Chromiumoxide, true).is_empty());
assert!(parity_ignored_default_args(EngineType::Fantoccini, true).is_empty());
}
}