use crate::error::Error;
use crate::server::connection::{ErrorPayload, ExpectErrorDetails};
fn is_browser_installation_error(message: &str) -> bool {
message.contains("Looks like Playwright")
|| message.contains("Executable doesn't exist")
|| message.contains("not installed")
|| message.contains("Please run")
}
fn extract_browser_name(message: &str) -> &str {
if message.contains("chromium") {
"chromium"
} else if message.contains("firefox") {
"firefox"
} else if message.contains("webkit") {
"webkit"
} else {
"browsers"
}
}
pub(crate) fn parse_protocol_error(
payload: ErrorPayload,
details: Option<serde_json::Value>,
) -> Error {
if let Some(details) =
details.and_then(|v| serde_json::from_value::<ExpectErrorDetails>(v).ok())
&& (details.timed_out.is_some()
|| details.custom_error_message.is_some()
|| details.received.is_some())
{
let message = details.custom_error_message.unwrap_or(payload.message);
return if details.timed_out.unwrap_or(false) {
Error::AssertionTimeout(message)
} else {
Error::AssertionFailed(message)
};
}
let message = &payload.message;
if is_browser_installation_error(message) {
let browser_name = extract_browser_name(message);
return Error::BrowserNotInstalled {
browser_name: browser_name.to_string(),
message: message.clone(),
playwright_version: crate::PLAYWRIGHT_VERSION.to_string(),
};
}
Error::ProtocolError(format!(
"{} \n {}",
payload.message,
payload.stack.unwrap_or_default()
))
}
pub(crate) fn legacy_expect_verdict(result: &serde_json::Value, is_not: bool) -> Option<bool> {
let matches = result.get("matches")?.as_bool()?;
Some(matches != is_not)
}
#[cfg(test)]
mod tests {
use super::*;
fn payload(message: &str) -> ErrorPayload {
ErrorPayload {
message: message.to_string(),
name: None,
stack: None,
}
}
fn details(
timed_out: Option<bool>,
custom: Option<&str>,
received: Option<serde_json::Value>,
) -> serde_json::Value {
serde_json::to_value(ExpectErrorDetails {
timed_out,
custom_error_message: custom.map(str::to_string),
received,
})
.unwrap()
}
#[test]
fn expect_details_timed_out_maps_to_assertion_timeout() {
let err = parse_protocol_error(
payload("timeout"),
Some(details(Some(true), Some("nope"), None)),
);
assert!(matches!(err, Error::AssertionTimeout(msg) if msg == "nope"));
}
#[test]
fn expect_details_timed_out_alone_maps_to_assertion_timeout() {
let err = parse_protocol_error(payload("timed out"), Some(details(Some(true), None, None)));
assert!(matches!(err, Error::AssertionTimeout(msg) if msg == "timed out"));
}
#[test]
fn expect_details_without_timeout_maps_to_assertion_failed() {
let err = parse_protocol_error(
payload("base"),
Some(details(Some(false), Some("nope"), None)),
);
assert!(matches!(err, Error::AssertionFailed(msg) if msg == "nope"));
}
#[test]
fn expect_details_falls_back_to_payload_message_when_no_custom() {
let err = parse_protocol_error(
payload("base message"),
Some(details(
None,
None,
Some(serde_json::json!({ "value": "x" })),
)),
);
assert!(matches!(err, Error::AssertionFailed(msg) if msg == "base message"));
}
#[test]
fn expect_details_with_only_received_maps_to_assertion_failed() {
let err = parse_protocol_error(
payload("mismatch"),
Some(details(None, None, Some(serde_json::json!({ "value": 1 })))),
);
assert!(matches!(err, Error::AssertionFailed(_)));
}
#[test]
fn no_details_is_a_plain_protocol_error() {
let err = parse_protocol_error(payload("boom"), None);
assert!(matches!(err, Error::ProtocolError(_)));
}
#[test]
fn empty_expect_details_is_a_protocol_error_not_an_assertion() {
let err = parse_protocol_error(
payload("Target page, context or browser has been closed"),
Some(serde_json::json!({})),
);
assert!(matches!(err, Error::ProtocolError(_)));
}
#[test]
fn unparseable_expect_details_degrade_to_a_protocol_error() {
let err = parse_protocol_error(payload("boom"), Some(serde_json::json!({ "timedOut": 1 })));
assert!(matches!(err, Error::ProtocolError(_)));
}
#[test]
fn no_details_still_detects_browser_install_errors() {
let err = parse_protocol_error(payload("Executable doesn't exist for chromium"), None);
assert!(matches!(err, Error::BrowserNotInstalled { .. }));
}
#[test]
fn each_install_error_phrase_is_detected_independently() {
assert!(is_browser_installation_error(
"Looks like Playwright was just installed"
));
assert!(is_browser_installation_error(
"Executable doesn't exist at /x"
));
assert!(is_browser_installation_error(
"the browser is not installed"
));
assert!(is_browser_installation_error(
"Please run playwright install"
));
assert!(!is_browser_installation_error("an unrelated failure"));
}
#[test]
fn browser_name_extracted_per_engine() {
assert_eq!(extract_browser_name("launching firefox failed"), "firefox");
assert_eq!(extract_browser_name("webkit missing"), "webkit");
assert_eq!(extract_browser_name("chromium gone"), "chromium");
assert_eq!(extract_browser_name("something generic"), "browsers");
}
#[test]
fn legacy_body_reports_a_plain_assertion_holding_or_failing() {
let matched = serde_json::json!({ "matches": true });
let missed = serde_json::json!({ "matches": false });
assert_eq!(legacy_expect_verdict(&matched, false), Some(true));
assert_eq!(legacy_expect_verdict(&missed, false), Some(false));
}
#[test]
fn legacy_body_applies_is_not_client_side() {
let matched = serde_json::json!({ "matches": true });
let missed = serde_json::json!({ "matches": false });
assert_eq!(legacy_expect_verdict(&matched, true), Some(false));
assert_eq!(legacy_expect_verdict(&missed, true), Some(true));
}
#[test]
fn modern_body_carries_no_verdict_to_read() {
for body in [
serde_json::Value::Null,
serde_json::json!({}),
serde_json::json!({ "other": 1 }),
] {
assert_eq!(legacy_expect_verdict(&body, false), None);
assert_eq!(legacy_expect_verdict(&body, true), None);
}
}
#[test]
fn non_boolean_matches_is_not_a_verdict() {
for body in [
serde_json::json!({ "matches": "true" }),
serde_json::json!({ "matches": 1 }),
serde_json::json!({ "matches": serde_json::Value::Null }),
] {
assert_eq!(legacy_expect_verdict(&body, false), None);
}
}
}