mirror of
https://github.com/actionbook/actionbook.git
synced 2026-09-19 03:28:06 +08:00
e00c48fd28
* test: add hover command cases * test: add hover text error coverage * feat: implement browser hover command Add the `browser hover <selector>` command. Resolves the selector to a DOM node via the shared element module, then dispatches mouseenter, mouseover, and mousemove events via Runtime.callFunctionOn for reliable hover semantics in headless Chrome. Wired up across CLI parsing, action enum, router, and output formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add focus command cases * feat: implement browser focus command Add the `browser focus <selector>` command. Resolves the selector via the shared element module, uses DOM.focus to set focus on the target, and compares pre/post active element by JS reference identity (===) to correctly report focus_changed even for anonymous elements. Wired up across CLI parsing, action enum, router, and output formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add press command cases * feat: implement browser press command Add the `browser press <key-or-chord>` command. Parses single keys and chord combinations (e.g. Control+A, Shift+Tab), validates modifier segments, and dispatches keyDown/keyUp via CDP Input.dispatchKeyEvent with the appropriate modifiers bitmask. Wired up across CLI parsing, action enum, router, and output formatting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add drag command cases * feat: implement browser drag command Adds drag-and-drop via CDP Input.dispatchMouseEvent sequence: mousePressed at source → mouseMoved along path → mouseReleased at destination. Destination accepts either a selector or x,y coordinates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: drag dispatch via viewport resize + CDP Input events CDP Input.dispatchMouseEvent requires elements to be within the visible viewport. The drag fixture positions elements at top:780px which exceeds the default headless Chrome viewport (~469px). Fix by temporarily enlarging the viewport via Emulation.setDeviceMetricsOverride before the drag sequence, then clearing the override after completion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resize viewport before resolving drag coordinates Move Emulation.setDeviceMetricsOverride before element resolution so coordinates are captured under the final viewport. Avoids stale coordinates on responsive pages where a post-resolution resize could shift element positions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: preserve viewport width during drag, only expand height if needed Read the current viewport dimensions and only override if the height is too short for the drag endpoints. Width is preserved to avoid flipping responsive breakpoints or triggering resize handlers. The override is skipped entirely when the viewport is already tall enough. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add upload command cases * feat: implement browser upload command Adds `browser upload <selector> <file...> --session --tab` which sets files on a file input via CDP DOM.setFileInputFiles. Validates that all paths are absolute, resolves the target node, and returns a value_summary with file list and count. All 10 e2e tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: require at least one file path in browser upload Adds `#[arg(required = true, num_args = 1..)]` to the `files` field so Clap rejects invocations with zero files at parse time, matching the spec's `<file...>` contract. Adds a unit test for the missing-files case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add eval command cases * fix: update browser eval to return typed value, type, and preview per spec - Return `data.value` as typed JSON (number/bool/string) instead of always stringifying - Add `data.type` (JS type from CDP) and `data.preview` (string repr) - Return null context for SESSION_NOT_FOUND - Extract exception description for better EVAL_FAILED error messages - Update cloud eval tests to assert typed values All 11 eval tests pass (8 interaction + 3 cloud). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add eval context coverage * fix: include tab URL and title in browser eval context Fetches the current page URL and title after eval execution and returns them in the response context, matching the spec contract for tab-scoped commands. Tests now assert context.url and context.title are present. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add mouse-move command cases * feat: implement browser mouse-move command Adds `browser mouse-move <x,y> --session --tab` which dispatches a CDP Input.dispatchMouseEvent (mouseMoved) at the given coordinates. Parses and validates x,y format, tracks url/focus changes, returns context with tab URL and title. All 8 e2e tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add cursor-position command cases * test: add cursor-position text status check * feat: implement browser cursor-position command Adds cursor-position command that returns the last known cursor position for a tab. Position is stored in SessionRegistry when mouse-move is executed, using the existing session_id\0tab_id composite key pattern. Wired in all 6 registration points: mod.rs, action.rs, cli.rs (enum + to_action + command_name + context), router.rs, output.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: track cursor position in click, drag, and hover commands cursor-position was only updated by mouse-move. Now click, drag, and hover also store the final pointer coordinates in SessionRegistry so that cursor-position reflects the true position after any mouse action. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add regression test for click -> cursor-position tracking Verifies that cursor-position reflects coordinates from a click action, not just mouse-move. Covers the contract gap identified in review. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add scroll command cases * test: add scroll changed field checks * feat: implement browser scroll commands Adds scroll command with three subcommand forms: - Directional: scroll up|down|left|right <pixels> - Edge: scroll top|bottom - Into-view: scroll into-view <selector> [--align start|center|end|nearest] All forms support --container for scrolling within a specific element. Tracks scroll_changed, url_changed, focus_changed in response data. Wired in all 6 registration points. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add scroll xpath selector coverage * fix: use resolved CDP nodes for scroll selectors instead of querySelector Refactored scroll to resolve selectors via element::resolve_node + DOM.resolveNode → objectId, then use Runtime.callFunctionOn for all scroll operations. This correctly supports CSS, XPath, and @eN selectors per the API contract, instead of raw querySelector which only handles CSS. Added DOM.getDocument init call to ensure XPath DOM.requestNode works. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
5621 lines
137 KiB
Rust
5621 lines
137 KiB
Rust
//! Browser interaction E2E tests: browser click, type, fill, select.
|
|
//!
|
|
//! This file groups interaction commands together, similar to navigation.rs.
|
|
//! The current coverage here is for `browser click`, `browser type`,
|
|
//! `browser fill`, and `browser select`,
|
|
//! per api-reference.md §11.
|
|
|
|
use crate::harness::{
|
|
SessionGuard, assert_failure, assert_success, headless, headless_json, parse_json, skip,
|
|
stdout_str,
|
|
};
|
|
|
|
const TEST_URL: &str = "https://example.com";
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
fn assert_meta(v: &serde_json::Value) {
|
|
assert!(
|
|
v["meta"]["duration_ms"].is_number(),
|
|
"meta.duration_ms must be a number"
|
|
);
|
|
assert!(
|
|
v["meta"]["warnings"].is_array(),
|
|
"meta.warnings must be an array"
|
|
);
|
|
assert!(
|
|
v["meta"]["pagination"].is_null(),
|
|
"meta.pagination must be null"
|
|
);
|
|
assert!(
|
|
v["meta"]["truncated"].is_boolean(),
|
|
"meta.truncated must be a boolean"
|
|
);
|
|
}
|
|
|
|
fn assert_error_envelope(v: &serde_json::Value, expected_code: &str) {
|
|
assert_eq!(v["ok"], false, "ok must be false on error");
|
|
assert!(v["data"].is_null(), "data must be null on failure");
|
|
assert_eq!(v["error"]["code"], expected_code);
|
|
assert!(
|
|
v["error"]["message"].is_string(),
|
|
"error.message must be a string"
|
|
);
|
|
assert!(
|
|
v["error"]["retryable"].is_boolean(),
|
|
"error.retryable must be a boolean"
|
|
);
|
|
assert!(
|
|
v["error"]["details"].is_object() || v["error"]["details"].is_null(),
|
|
"error.details must be object or null"
|
|
);
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_click_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: Option<&str>,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.click");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "click");
|
|
assert!(data["target"].is_object(), "data.target must be an object");
|
|
if let Some(selector) = expected_selector {
|
|
assert_eq!(data["target"]["selector"], selector);
|
|
}
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_type_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
expected_text_length: u64,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.type");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "type");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert_eq!(data["value_summary"]["text_length"], expected_text_length);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_fill_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
expected_text_length: u64,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.fill");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "fill");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert_eq!(data["value_summary"]["text_length"], expected_text_length);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_select_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
expected_value: &str,
|
|
expected_by_text: bool,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.select");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "select");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert_eq!(data["value_summary"]["value"], expected_value);
|
|
assert_eq!(data["value_summary"]["by_text"], expected_by_text);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_hover_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.hover");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "hover");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_focus_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.focus");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "focus");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_press_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_keys: &str,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.press");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "press");
|
|
assert_eq!(data["keys"], expected_keys);
|
|
assert!(
|
|
data.get("target").is_none() || data["target"].is_null(),
|
|
"press should not require a target in the response when keys are used"
|
|
);
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_drag_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_source: &str,
|
|
expected_destination_selector: Option<&str>,
|
|
expected_destination_coordinates: Option<&str>,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.drag");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "drag");
|
|
assert_eq!(data["target"]["selector"], expected_source);
|
|
if let Some(selector) = expected_destination_selector {
|
|
assert_eq!(data["destination"]["selector"], selector);
|
|
}
|
|
if let Some(coords) = expected_destination_coordinates {
|
|
assert_eq!(data["destination"]["coordinates"], coords);
|
|
}
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_mouse_move_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_coordinates: &str,
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.mouse-move");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "mouse-move");
|
|
assert_eq!(data["target"]["coordinates"], expected_coordinates);
|
|
assert!(
|
|
data["changed"]["url_changed"].is_boolean(),
|
|
"data.changed.url_changed must be a boolean"
|
|
);
|
|
assert!(
|
|
data["changed"]["focus_changed"].is_boolean(),
|
|
"data.changed.focus_changed must be a boolean"
|
|
);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn assert_upload_success(
|
|
v: &serde_json::Value,
|
|
session_id: &str,
|
|
tab_id: &str,
|
|
expected_selector: &str,
|
|
expected_files: &[String],
|
|
) {
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.upload");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], session_id);
|
|
assert_eq!(v["context"]["tab_id"], tab_id);
|
|
|
|
let data = &v["data"];
|
|
assert_eq!(data["action"], "upload");
|
|
assert_eq!(data["target"]["selector"], expected_selector);
|
|
assert_eq!(data["value_summary"]["count"], expected_files.len());
|
|
|
|
let files = data["value_summary"]["files"]
|
|
.as_array()
|
|
.expect("value_summary.files must be an array");
|
|
let actual: Vec<String> = files
|
|
.iter()
|
|
.map(|v| v.as_str().unwrap_or("").to_string())
|
|
.collect();
|
|
assert_eq!(actual, expected_files);
|
|
|
|
assert_meta(v);
|
|
}
|
|
|
|
fn start_session(url: &str) -> (String, String) {
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"start",
|
|
"--mode",
|
|
"local",
|
|
"--headless",
|
|
"--open-url",
|
|
url,
|
|
],
|
|
30,
|
|
);
|
|
assert_success(&out, "start session");
|
|
let v = parse_json(&out);
|
|
let sid = v["data"]["session"]["session_id"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_string();
|
|
let tid = v["data"]["tab"]["tab_id"].as_str().unwrap().to_string();
|
|
(sid, tid)
|
|
}
|
|
|
|
fn close_session(session_id: &str) {
|
|
let out = headless(&["browser", "close", "--session", session_id], 30);
|
|
assert_success(&out, &format!("close {session_id}"));
|
|
}
|
|
|
|
fn eval_value(session_id: &str, tab_id: &str, expression: &str) -> String {
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
expression,
|
|
"--session",
|
|
session_id,
|
|
"--tab",
|
|
tab_id,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "eval");
|
|
let v = parse_json(&out);
|
|
v["data"]["value"].as_str().unwrap_or("").to_string()
|
|
}
|
|
|
|
fn install_click_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-click-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_clicks = 0;
|
|
window.__ab_dblclicks = 0;
|
|
window.__ab_middle_clicks = 0;
|
|
window.__ab_right_clicks = 0;
|
|
window.__ab_last_click_button = -1;
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-click-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-click-btn, #ab-link, #ab-right-target, #ab-middle-target {
|
|
position: fixed;
|
|
left: 40px;
|
|
width: 180px;
|
|
height: 36px;
|
|
z-index: 2147483647;
|
|
}
|
|
#ab-click-btn { top: 40px; }
|
|
#ab-link {
|
|
top: 100px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #ffedd5;
|
|
color: #111827;
|
|
}
|
|
#ab-right-target {
|
|
top: 160px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #e5e7eb;
|
|
}
|
|
#ab-middle-target {
|
|
top: 220px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #dbeafe;
|
|
}
|
|
</style>
|
|
<button id="ab-click-btn" type="button">Click target</button>
|
|
<a id="ab-link" href="https://example.org/#ab-click-target">Open link</a>
|
|
<div id="ab-right-target" tabindex="0">Right click target</div>
|
|
<div id="ab-middle-target" tabindex="0">Middle click target</div>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const btn = document.getElementById('ab-click-btn');
|
|
btn.addEventListener('click', (event) => {
|
|
window.__ab_clicks += 1;
|
|
window.__ab_last_click_button = event.button;
|
|
document.body.setAttribute('data-clicks', String(window.__ab_clicks));
|
|
document.body.setAttribute('data-last-click-button', String(window.__ab_last_click_button));
|
|
});
|
|
btn.addEventListener('dblclick', () => {
|
|
window.__ab_dblclicks += 1;
|
|
document.body.setAttribute('data-dblclicks', String(window.__ab_dblclicks));
|
|
});
|
|
|
|
const rightTarget = document.getElementById('ab-right-target');
|
|
rightTarget.addEventListener('contextmenu', (event) => {
|
|
event.preventDefault();
|
|
window.__ab_right_clicks += 1;
|
|
document.body.setAttribute('data-right-clicks', String(window.__ab_right_clicks));
|
|
});
|
|
|
|
const middleTarget = document.getElementById('ab-middle-target');
|
|
middleTarget.addEventListener('auxclick', (event) => {
|
|
if (event.button === 1) {
|
|
window.__ab_middle_clicks += 1;
|
|
document.body.setAttribute('data-middle-clicks', String(window.__ab_middle_clicks));
|
|
}
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "fixture should install successfully");
|
|
}
|
|
|
|
fn install_type_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-type-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_type_keydown_count = 0;
|
|
window.__ab_type_input_count = 0;
|
|
window.__ab_type_keyup_count = 0;
|
|
window.__ab_type_events = [];
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-type-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-type-input {
|
|
position: fixed;
|
|
top: 280px;
|
|
left: 40px;
|
|
width: 240px;
|
|
height: 36px;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<input id="ab-type-input" type="text" value="seed-" />
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const input = document.getElementById('ab-type-input');
|
|
input.addEventListener('keydown', (event) => {
|
|
window.__ab_type_keydown_count += 1;
|
|
window.__ab_type_events.push('keydown:' + event.key);
|
|
});
|
|
input.addEventListener('input', () => {
|
|
window.__ab_type_input_count += 1;
|
|
window.__ab_type_events.push('input:' + input.value);
|
|
});
|
|
input.addEventListener('keyup', (event) => {
|
|
window.__ab_type_keyup_count += 1;
|
|
window.__ab_type_events.push('keyup:' + event.key);
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "type fixture should install successfully");
|
|
}
|
|
|
|
fn install_fill_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-fill-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_fill_keydown_count = 0;
|
|
window.__ab_fill_input_count = 0;
|
|
window.__ab_fill_keyup_count = 0;
|
|
window.__ab_fill_change_count = 0;
|
|
window.__ab_fill_events = [];
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-fill-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-fill-input, #ab-fill-textarea {
|
|
position: fixed;
|
|
left: 40px;
|
|
width: 240px;
|
|
height: 36px;
|
|
z-index: 2147483647;
|
|
}
|
|
#ab-fill-input {
|
|
top: 340px;
|
|
}
|
|
#ab-fill-textarea {
|
|
top: 390px;
|
|
height: 72px;
|
|
}
|
|
</style>
|
|
<input id="ab-fill-input" type="text" value="seed-" />
|
|
<textarea id="ab-fill-textarea">seed-area</textarea>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const attachFillListeners = (el, label) => {
|
|
el.addEventListener('keydown', (event) => {
|
|
window.__ab_fill_keydown_count += 1;
|
|
window.__ab_fill_events.push(label + ':keydown:' + event.key);
|
|
});
|
|
el.addEventListener('input', () => {
|
|
window.__ab_fill_input_count += 1;
|
|
window.__ab_fill_events.push(label + ':input:' + el.value);
|
|
});
|
|
el.addEventListener('keyup', (event) => {
|
|
window.__ab_fill_keyup_count += 1;
|
|
window.__ab_fill_events.push(label + ':keyup:' + event.key);
|
|
});
|
|
el.addEventListener('change', () => {
|
|
window.__ab_fill_change_count += 1;
|
|
window.__ab_fill_events.push(label + ':change:' + el.value);
|
|
});
|
|
};
|
|
|
|
attachFillListeners(document.getElementById('ab-fill-input'), 'input');
|
|
attachFillListeners(document.getElementById('ab-fill-textarea'), 'textarea');
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "fill fixture should install successfully");
|
|
}
|
|
|
|
fn install_select_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-select-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_select_input_count = 0;
|
|
window.__ab_select_change_count = 0;
|
|
window.__ab_select_events = [];
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-select-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-select {
|
|
position: fixed;
|
|
top: 480px;
|
|
left: 40px;
|
|
width: 240px;
|
|
height: 36px;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<select id="ab-select">
|
|
<option value="apple" selected>Apple</option>
|
|
<option value="banana">Banana</option>
|
|
<option value="citrus">Citrus Fruit</option>
|
|
</select>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const select = document.getElementById('ab-select');
|
|
select.addEventListener('input', () => {
|
|
window.__ab_select_input_count += 1;
|
|
window.__ab_select_events.push('input:' + select.value);
|
|
});
|
|
select.addEventListener('change', () => {
|
|
window.__ab_select_change_count += 1;
|
|
window.__ab_select_events.push('change:' + select.value);
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "select fixture should install successfully");
|
|
}
|
|
|
|
fn install_hover_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-hover-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_hover_enter_count = 0;
|
|
window.__ab_hover_over_count = 0;
|
|
window.__ab_hover_move_count = 0;
|
|
window.__ab_hover_last_target = '';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-hover-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-hover-target {
|
|
position: fixed;
|
|
top: 540px;
|
|
left: 40px;
|
|
width: 220px;
|
|
height: 44px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: #d1fae5;
|
|
color: #111827;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<div id="ab-hover-target">Hover target</div>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const target = document.getElementById('ab-hover-target');
|
|
target.addEventListener('mouseenter', () => {
|
|
window.__ab_hover_enter_count += 1;
|
|
window.__ab_hover_last_target = target.id;
|
|
document.body.setAttribute('data-hover-enter', String(window.__ab_hover_enter_count));
|
|
});
|
|
target.addEventListener('mouseover', () => {
|
|
window.__ab_hover_over_count += 1;
|
|
window.__ab_hover_last_target = target.id;
|
|
document.body.setAttribute('data-hover-over', String(window.__ab_hover_over_count));
|
|
});
|
|
target.addEventListener('mousemove', () => {
|
|
window.__ab_hover_move_count += 1;
|
|
window.__ab_hover_last_target = target.id;
|
|
document.body.setAttribute('data-hover-move', String(window.__ab_hover_move_count));
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "hover fixture should install successfully");
|
|
}
|
|
|
|
fn install_focus_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-focus-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_focus_target_count = 0;
|
|
window.__ab_focus_other_count = 0;
|
|
window.__ab_blur_other_count = 0;
|
|
window.__ab_last_focused = '';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-focus-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-focus-other, #ab-focus-target {
|
|
position: fixed;
|
|
left: 40px;
|
|
width: 220px;
|
|
height: 40px;
|
|
z-index: 2147483647;
|
|
}
|
|
#ab-focus-other {
|
|
top: 600px;
|
|
}
|
|
#ab-focus-target {
|
|
top: 650px;
|
|
}
|
|
</style>
|
|
<input id="ab-focus-other" type="text" value="other" />
|
|
<button id="ab-focus-target" type="button">Focus target</button>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const other = document.getElementById('ab-focus-other');
|
|
const target = document.getElementById('ab-focus-target');
|
|
|
|
other.addEventListener('focus', () => {
|
|
window.__ab_focus_other_count += 1;
|
|
window.__ab_last_focused = other.id;
|
|
});
|
|
other.addEventListener('blur', () => {
|
|
window.__ab_blur_other_count += 1;
|
|
});
|
|
target.addEventListener('focus', () => {
|
|
window.__ab_focus_target_count += 1;
|
|
window.__ab_last_focused = target.id;
|
|
});
|
|
|
|
other.focus();
|
|
return document.activeElement && document.activeElement.id;
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(
|
|
value, "ab-focus-other",
|
|
"focus fixture should install successfully"
|
|
);
|
|
}
|
|
|
|
fn install_press_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-press-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_press_keydown_count = 0;
|
|
window.__ab_press_keyup_count = 0;
|
|
window.__ab_press_events = [];
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-press-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-press-input {
|
|
position: fixed;
|
|
top: 710px;
|
|
left: 40px;
|
|
width: 260px;
|
|
height: 40px;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<input id="ab-press-input" type="text" value="seed" />
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const input = document.getElementById('ab-press-input');
|
|
input.addEventListener('keydown', (event) => {
|
|
window.__ab_press_keydown_count += 1;
|
|
window.__ab_press_events.push(
|
|
'keydown:' + event.key + ':' + event.ctrlKey + ':' + event.shiftKey
|
|
);
|
|
});
|
|
input.addEventListener('keyup', (event) => {
|
|
window.__ab_press_keyup_count += 1;
|
|
window.__ab_press_events.push(
|
|
'keyup:' + event.key + ':' + event.ctrlKey + ':' + event.shiftKey
|
|
);
|
|
});
|
|
|
|
input.focus();
|
|
return document.activeElement && document.activeElement.id;
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(
|
|
value, "ab-press-input",
|
|
"press fixture should install successfully"
|
|
);
|
|
}
|
|
|
|
fn install_drag_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-drag-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_drag_mousemove_count = 0;
|
|
window.__ab_drag_drop_count = 0;
|
|
window.__ab_drag_last_drop = '';
|
|
window.__ab_drag_state = 'idle';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-drag-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-drag-source, #ab-drag-target {
|
|
position: fixed;
|
|
top: 780px;
|
|
width: 110px;
|
|
height: 48px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2147483647;
|
|
user-select: none;
|
|
}
|
|
#ab-drag-source {
|
|
left: 40px;
|
|
background: #bfdbfe;
|
|
}
|
|
#ab-drag-target {
|
|
left: 260px;
|
|
background: #bbf7d0;
|
|
}
|
|
</style>
|
|
<div id="ab-drag-source">Drag source</div>
|
|
<div id="ab-drag-target">Drop target</div>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const source = document.getElementById('ab-drag-source');
|
|
const target = document.getElementById('ab-drag-target');
|
|
|
|
const finishDrop = (label) => {
|
|
if (window.__ab_drag_state !== 'dragging') return;
|
|
window.__ab_drag_drop_count += 1;
|
|
window.__ab_drag_last_drop = label;
|
|
window.__ab_drag_state = 'idle';
|
|
};
|
|
|
|
source.addEventListener('mousedown', () => {
|
|
window.__ab_drag_state = 'dragging';
|
|
});
|
|
|
|
document.addEventListener('mousemove', () => {
|
|
if (window.__ab_drag_state === 'dragging') {
|
|
window.__ab_drag_mousemove_count += 1;
|
|
}
|
|
}, true);
|
|
|
|
target.addEventListener('mouseup', () => {
|
|
finishDrop('ab-drag-target');
|
|
});
|
|
|
|
document.addEventListener('mouseup', (event) => {
|
|
if (window.__ab_drag_state !== 'dragging') return;
|
|
const el = document.elementFromPoint(event.clientX, event.clientY);
|
|
if (el && el.id === 'ab-drag-target') {
|
|
finishDrop('ab-drag-target');
|
|
} else {
|
|
window.__ab_drag_state = 'idle';
|
|
}
|
|
}, true);
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "drag fixture should install successfully");
|
|
}
|
|
|
|
fn install_upload_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-upload-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_upload_change_count = 0;
|
|
window.__ab_upload_last_count = 0;
|
|
window.__ab_upload_last_names = '';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-upload-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-upload-input {
|
|
position: fixed;
|
|
top: 840px;
|
|
left: 40px;
|
|
width: 260px;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<input id="ab-upload-input" type="file" multiple />
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const input = document.getElementById('ab-upload-input');
|
|
input.addEventListener('change', () => {
|
|
window.__ab_upload_change_count += 1;
|
|
window.__ab_upload_last_count = input.files.length;
|
|
window.__ab_upload_last_names = Array.from(input.files).map(file => file.name).join(',');
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "upload fixture should install successfully");
|
|
}
|
|
|
|
fn install_mouse_move_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-mouse-move-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.__ab_mouse_move_count = 0;
|
|
window.__ab_mouse_move_last_target = '';
|
|
window.__ab_mouse_move_last_coords = '';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-mouse-move-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
#ab-mouse-move-target {
|
|
position: fixed;
|
|
top: 120px;
|
|
left: 90px;
|
|
width: 180px;
|
|
height: 60px;
|
|
background: #fde68a;
|
|
z-index: 2147483647;
|
|
}
|
|
</style>
|
|
<div id="ab-mouse-move-target">Move target</div>
|
|
`;
|
|
document.body.appendChild(root);
|
|
|
|
const target = document.getElementById('ab-mouse-move-target');
|
|
target.addEventListener('mousemove', (event) => {
|
|
window.__ab_mouse_move_count += 1;
|
|
window.__ab_mouse_move_last_target = target.id;
|
|
window.__ab_mouse_move_last_coords = `${event.clientX},${event.clientY}`;
|
|
});
|
|
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(
|
|
value, "ok",
|
|
"mouse-move fixture should install successfully"
|
|
);
|
|
}
|
|
|
|
fn install_scroll_fixture(session_id: &str, tab_id: &str) {
|
|
let expression = r#"
|
|
(() => {
|
|
const existing = document.getElementById('ab-scroll-fixture');
|
|
if (existing) existing.remove();
|
|
|
|
window.scrollTo(0, 0);
|
|
document.body.innerHTML = '';
|
|
document.documentElement.style.height = '';
|
|
document.body.style.height = '';
|
|
document.body.style.margin = '0';
|
|
|
|
const root = document.createElement('div');
|
|
root.id = 'ab-scroll-fixture';
|
|
root.innerHTML = `
|
|
<style>
|
|
body {
|
|
min-height: 2600px;
|
|
}
|
|
#ab-scroll-target {
|
|
margin-top: 1400px;
|
|
height: 60px;
|
|
background: #bfdbfe;
|
|
}
|
|
#ab-scroll-container {
|
|
position: fixed;
|
|
top: 120px;
|
|
left: 40px;
|
|
width: 260px;
|
|
height: 120px;
|
|
overflow: auto;
|
|
border: 1px solid #111827;
|
|
background: #f8fafc;
|
|
z-index: 2147483647;
|
|
}
|
|
#ab-scroll-container-inner {
|
|
width: 600px;
|
|
height: 700px;
|
|
position: relative;
|
|
}
|
|
#ab-scroll-container-target {
|
|
position: absolute;
|
|
top: 560px;
|
|
left: 20px;
|
|
width: 180px;
|
|
height: 40px;
|
|
background: #bbf7d0;
|
|
}
|
|
</style>
|
|
<div id="ab-scroll-container">
|
|
<div id="ab-scroll-container-inner">
|
|
<div id="ab-scroll-container-target">Container target</div>
|
|
</div>
|
|
</div>
|
|
<div id="ab-scroll-target">Page target</div>
|
|
`;
|
|
document.body.appendChild(root);
|
|
return 'ok';
|
|
})()
|
|
"#;
|
|
|
|
let value = eval_value(session_id, tab_id, expression);
|
|
assert_eq!(value, "ok", "scroll fixture should install successfully");
|
|
}
|
|
|
|
fn create_upload_files(names: &[&str]) -> (tempfile::TempDir, Vec<String>) {
|
|
let dir = tempfile::tempdir().expect("create upload temp dir");
|
|
let mut paths = Vec::new();
|
|
for name in names {
|
|
let path = dir.path().join(name);
|
|
std::fs::write(&path, format!("fixture for {name}\n")).expect("write upload fixture file");
|
|
paths.push(path.to_string_lossy().to_string());
|
|
}
|
|
(dir, paths)
|
|
}
|
|
|
|
fn list_tabs(session_id: &str) -> serde_json::Value {
|
|
let out = headless_json(&["browser", "list-tabs", "--session", session_id], 15);
|
|
assert_success(&out, "list-tabs");
|
|
parse_json(&out)
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 1: click — basic success path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn click_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-click-btn"));
|
|
assert_eq!(eval_value(&sid, &tid, "String(window.__ab_clicks)"), "1");
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_last_click_button)"),
|
|
"0"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.click"),
|
|
"must contain ok browser.click"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-click-btn"),
|
|
"must contain target line with selector"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_coordinates_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"60,60",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click coordinates json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, None);
|
|
assert_eq!(eval_value(&sid, &tid, "String(window.__ab_clicks)"), "1");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_coordinates_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"60,60",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click coordinates text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.click"),
|
|
"must contain ok browser.click"
|
|
);
|
|
assert!(
|
|
text.contains("target: 60,60"),
|
|
"must contain target line with coordinates"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 2: click — option flags
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn click_count_two_triggers_double_click() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--count",
|
|
"2",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click count=2");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-click-btn"));
|
|
assert_eq!(eval_value(&sid, &tid, "String(window.__ab_dblclicks)"), "1");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_right_button_dispatches_contextmenu() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-right-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--button",
|
|
"right",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click button=right");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-right-target"));
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_right_clicks)"),
|
|
"1"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_middle_button_dispatches_auxclick() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-middle-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--button",
|
|
"middle",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click button=middle");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-middle-target"));
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_middle_clicks)"),
|
|
"1"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_new_tab_opens_link_in_new_tab() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-link",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--new-tab",
|
|
],
|
|
30,
|
|
);
|
|
assert_success(&out, "click new-tab");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-link"));
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert!(
|
|
eval_value(&sid, &tid, "location.href").contains("example.com"),
|
|
"current tab should stay on the original page when --new-tab is used"
|
|
);
|
|
|
|
let tabs = list_tabs(&sid);
|
|
assert!(
|
|
tabs["data"]["total_tabs"].as_u64().unwrap_or(0) >= 2,
|
|
"new-tab click should create another tab"
|
|
);
|
|
let tabs = tabs["data"]["tabs"].as_array().expect("tabs array");
|
|
let any_new_tab = tabs
|
|
.iter()
|
|
.any(|tab| tab["url"].as_str().unwrap_or("").contains("example.org"));
|
|
assert!(any_new_tab, "one tab should load the clicked link URL");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_new_tab_coordinates_without_href_does_not_open_new_tab() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"60,60",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--new-tab",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "click new-tab coordinates without href");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, None);
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(eval_value(&sid, &tid, "String(window.__ab_clicks)"), "1");
|
|
|
|
let tabs = list_tabs(&sid);
|
|
assert_eq!(tabs["data"]["total_tabs"], serde_json::json!(1));
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 3: click — navigation semantics
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn click_navigation_updates_context_url() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_click_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-link",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
30,
|
|
);
|
|
assert_success(&out, "click navigation");
|
|
let v = parse_json(&out);
|
|
|
|
assert_click_success(&v, &sid, &tid, Some("#ab-link"));
|
|
assert_eq!(v["data"]["changed"]["url_changed"], true);
|
|
assert!(
|
|
v["context"]["url"]
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.contains("example.org"),
|
|
"context.url must update to the post-navigation URL"
|
|
);
|
|
assert!(
|
|
v["context"]["title"].is_string(),
|
|
"context.title should be returned after navigation when known"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 4: click — error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn click_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "click nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.click");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn click_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "click nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn click_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "click nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.click");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#ab-click-btn",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "click nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "click missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.click");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "click missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn click_invalid_coordinates_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
// These cases are intentionally coordinate-like but malformed, so they
|
|
// should be rejected as invalid coordinates rather than treated as selectors.
|
|
for target in ["10,", ",10", "10,abc", ",,,"] {
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let out = headless_json(
|
|
&["browser", "click", target, "--session", &sid, "--tab", &tid],
|
|
10,
|
|
);
|
|
assert_failure(&out, &format!("click invalid coordinates json: {target}"));
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.click");
|
|
assert_error_envelope(&v, "INVALID_ARGUMENT");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session and tab are valid"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 5: type — basic success path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn type_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_type_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "type json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_type_success(&v, &sid, &tid, "#ab-type-input", 3);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-type-input').value"),
|
|
"seed-abc"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_type_keydown_count)"),
|
|
"3"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_type_input_count)"),
|
|
"3"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_type_keyup_count)"),
|
|
"3"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"document.activeElement && document.activeElement.id"
|
|
),
|
|
"ab-type-input"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn type_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_type_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "type text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.type"),
|
|
"must contain ok browser.type"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-type-input"),
|
|
"must contain target line with selector"
|
|
);
|
|
assert!(
|
|
text.contains("text_length: 3"),
|
|
"must contain text_length: 3"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn type_with_spaces_and_punctuation_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_type_fixture(&sid, &tid);
|
|
let typed_text = "Hello, world!";
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
typed_text,
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "type with spaces json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_type_success(&v, &sid, &tid, "#ab-type-input", typed_text.len() as u64);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-type-input').value"),
|
|
format!("seed-{typed_text}")
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 6: type — error paths
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn type_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "type nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.type");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn type_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "type nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn type_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "type nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.type");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn type_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#ab-type-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "type nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn type_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#definitely-missing-element",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "type missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.type");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn type_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"type",
|
|
"#definitely-missing-element",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "type missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 7: fill — basic success path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn fill_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_fill_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "fill json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_fill_success(&v, &sid, &tid, "#ab-fill-input", 3);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-fill-input').value"),
|
|
"abc"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_input_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_keydown_count)"),
|
|
"0"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_keyup_count)"),
|
|
"0"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_fill_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "fill text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.fill"),
|
|
"must contain ok browser.fill"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-fill-input"),
|
|
"must contain target line with selector"
|
|
);
|
|
assert!(
|
|
text.contains("text_length: 3"),
|
|
"must contain text_length: 3"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_textarea_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_fill_fixture(&sid, &tid);
|
|
let fill_text = "textarea value";
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-textarea",
|
|
fill_text,
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "fill textarea json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_fill_success(&v, &sid, &tid, "#ab-fill-textarea", fill_text.len() as u64);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"document.querySelector('#ab-fill-textarea').value"
|
|
),
|
|
fill_text
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_input_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_keydown_count)"),
|
|
"0"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_keyup_count)"),
|
|
"0"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_replaces_existing_value_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_fill_fixture(&sid, &tid);
|
|
let fill_text = "Hello, world!";
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
fill_text,
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "fill replaces value json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_fill_success(&v, &sid, &tid, "#ab-fill-input", fill_text.len() as u64);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-fill-input').value"),
|
|
fill_text
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_fill_input_count)"),
|
|
"1"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 8: fill — error paths
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn fill_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "fill nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.fill");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "fill nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "fill nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.fill");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#ab-fill-input",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "fill nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#definitely-missing-element",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "fill missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.fill");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn fill_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"fill",
|
|
"#definitely-missing-element",
|
|
"abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "fill missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 9: select — basic success path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn select_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_select_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "select json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_select_success(&v, &sid, &tid, "#ab-select", "banana", false);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-select').value"),
|
|
"banana"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"document.querySelector('#ab-select').selectedOptions[0].textContent.trim()"
|
|
),
|
|
"Banana"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn select_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_select_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "select text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.select"),
|
|
"must contain ok browser.select"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-select"),
|
|
"must contain target line with selector"
|
|
);
|
|
assert!(
|
|
text.contains("value: banana"),
|
|
"must contain selected value"
|
|
);
|
|
assert!(
|
|
text.contains("by_text: false"),
|
|
"must contain by_text: false"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn select_by_text_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_select_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"Citrus Fruit",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--by-text",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "select by-text json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_select_success(&v, &sid, &tid, "#ab-select", "Citrus Fruit", true);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "document.querySelector('#ab-select').value"),
|
|
"citrus"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"document.querySelector('#ab-select').selectedOptions[0].textContent.trim()"
|
|
),
|
|
"Citrus Fruit"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 10: select — error paths
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn select_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "select nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.select");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn select_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "select nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn select_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "select nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.select");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn select_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#ab-select",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "select nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn select_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#definitely-missing-element",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "select missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.select");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn select_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"select",
|
|
"#definitely-missing-element",
|
|
"banana",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "select missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 13: hover — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn hover_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_hover_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "hover json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_hover_success(&v, &sid, &tid, "#ab-hover-target");
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_hover_enter_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_hover_over_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_hover_move_count !== 0)"),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_hover_last_target"),
|
|
"ab-hover-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_hover_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "hover text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.hover"),
|
|
"must contain ok browser.hover"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-hover-target"),
|
|
"must contain target line with selector"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "hover nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.hover");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "hover nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "hover nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.hover");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#ab-hover-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "hover nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "hover missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.hover");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn hover_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"hover",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "hover missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 14: focus — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn focus_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_focus_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "focus json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_focus_success(&v, &sid, &tid, "#ab-focus-target");
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], true);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_focus_target_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_focus_other_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_blur_other_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"document.activeElement && document.activeElement.id"
|
|
),
|
|
"ab-focus-target"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_last_focused"),
|
|
"ab-focus-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_focus_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "focus text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.focus"),
|
|
"must contain ok browser.focus"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-focus-target"),
|
|
"must contain target line with selector"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "focus nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.focus");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "focus nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "focus nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.focus");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#ab-focus-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "focus nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "focus missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.focus");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn focus_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"focus",
|
|
"#definitely-missing-element",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "focus missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 15: press — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn press_json_single_key() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_press_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "press json single key");
|
|
let v = parse_json(&out);
|
|
|
|
assert_press_success(&v, &sid, &tid, "Enter");
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_press_keydown_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_press_keyup_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"window.__ab_press_events.includes('keydown:Enter:false:false') ? 'yes' : 'no'"
|
|
),
|
|
"yes"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"window.__ab_press_events.includes('keyup:Enter:false:false') ? 'yes' : 'no'"
|
|
),
|
|
"yes"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn press_text_chord() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_press_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Control+A",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "press text chord");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.press"),
|
|
"must contain ok browser.press"
|
|
);
|
|
assert!(
|
|
text.contains("keys: Control+A"),
|
|
"must contain keys line with the chord"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"window.__ab_press_events.includes('keydown:a:true:false') ? 'yes' : 'no'"
|
|
),
|
|
"yes"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"window.__ab_press_events.includes('keyup:a:true:false') ? 'yes' : 'no'"
|
|
),
|
|
"yes"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn press_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.press");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn press_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn press_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.press");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn press_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Enter",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn press_invalid_chord_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Control+",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press invalid chord json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.press");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "INVALID_ARGUMENT");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn press_invalid_chord_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"press",
|
|
"Control+",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "press invalid chord text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error INVALID_ARGUMENT:"),
|
|
"text must contain error INVALID_ARGUMENT: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 16: drag — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn drag_json_to_selector() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_drag_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "drag json to selector");
|
|
let v = parse_json(&out);
|
|
|
|
assert_drag_success(
|
|
&v,
|
|
&sid,
|
|
&tid,
|
|
"#ab-drag-source",
|
|
Some("#ab-drag-target"),
|
|
None,
|
|
);
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_drag_mousemove_count !== 0)"),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_drag_drop_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_drag_last_drop"),
|
|
"ab-drag-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_text_to_coordinates() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_drag_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"315,804",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "drag text to coordinates");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.drag"),
|
|
"must contain ok browser.drag"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-drag-source"),
|
|
"must contain source target line"
|
|
);
|
|
assert!(
|
|
text.contains("destination: 315,804"),
|
|
"must contain destination line with coordinates"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_drag_drop_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_drag_last_drop"),
|
|
"ab-drag-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.drag");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.drag");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_missing_source_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#definitely-missing-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "drag missing source json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.drag");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-source"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_missing_source_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#definitely-missing-source",
|
|
"#ab-drag-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "drag missing source text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_invalid_destination_coordinates_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_drag_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"315,abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag invalid destination coordinates json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.drag");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "INVALID_ARGUMENT");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn drag_invalid_destination_coordinates_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_drag_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"drag",
|
|
"#ab-drag-source",
|
|
"315,abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "drag invalid destination coordinates text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error INVALID_ARGUMENT:"),
|
|
"text must contain error INVALID_ARGUMENT: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 17: upload — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn upload_json_single_file() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (_tmp, files) = create_upload_files(&["upload-a.txt"]);
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_upload_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
&files[0],
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "upload json single file");
|
|
let v = parse_json(&out);
|
|
|
|
assert_upload_success(&v, &sid, &tid, "#ab-upload-input", &files);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_upload_change_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_upload_last_count)"),
|
|
"1"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_upload_last_names"),
|
|
"upload-a.txt"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_text_multiple_files() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (_tmp, files) = create_upload_files(&["upload-a.txt", "upload-b.txt"]);
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_upload_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
&files[0],
|
|
&files[1],
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "upload text multiple files");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.upload"),
|
|
"must contain ok browser.upload"
|
|
);
|
|
assert!(
|
|
text.contains("target: #ab-upload-input"),
|
|
"must contain target line with selector"
|
|
);
|
|
assert!(text.contains("count: 2"), "must contain file count line");
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_upload_last_count)"),
|
|
"2"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_upload_last_names"),
|
|
"upload-a.txt,upload-b.txt"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"/tmp/example.txt",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.upload");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"/tmp/example.txt",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"/tmp/example.txt",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.upload");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"/tmp/example.txt",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_missing_selector_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (_tmp, files) = create_upload_files(&["upload-a.txt"]);
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#definitely-missing-element",
|
|
&files[0],
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "upload missing selector json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.upload");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-element"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_missing_selector_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (_tmp, files) = create_upload_files(&["upload-a.txt"]);
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#definitely-missing-element",
|
|
&files[0],
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_failure(&out, "upload missing selector text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_relative_path_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_upload_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"relative.txt",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload relative path json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.upload");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "INVALID_ARGUMENT");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn upload_relative_path_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_upload_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"upload",
|
|
"#ab-upload-input",
|
|
"relative.txt",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "upload relative path text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error INVALID_ARGUMENT:"),
|
|
"text must contain error INVALID_ARGUMENT: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 18: eval — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn eval_json_number() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&["browser", "eval", "2 + 2", "--session", &sid, "--tab", &tid],
|
|
10,
|
|
);
|
|
assert_success(&out, "eval json number");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.eval");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert!(
|
|
v["context"]["url"]
|
|
.as_str()
|
|
.is_some_and(|url| url.starts_with(TEST_URL)),
|
|
"context.url should include the current page URL"
|
|
);
|
|
assert_eq!(v["context"]["title"], "Example Domain");
|
|
assert_eq!(v["data"]["value"], serde_json::json!(4));
|
|
assert_eq!(v["data"]["type"], "number");
|
|
assert_eq!(v["data"]["preview"], "4");
|
|
assert_meta(&v);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&["browser", "eval", "2 + 2", "--session", &sid, "--tab", &tid],
|
|
10,
|
|
);
|
|
assert_success(&out, "eval text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}] {TEST_URL}")),
|
|
"header must include the current page URL: got {text}"
|
|
);
|
|
assert!(
|
|
text.trim_end().ends_with('4'),
|
|
"eval text should end with the evaluated value: {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"2 + 2",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.eval");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"2 + 2",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"2 + 2",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.eval");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"2 + 2",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_exception_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"(() => { throw new Error('boom-eval'); })()",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval exception json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.eval");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "EVAL_FAILED");
|
|
assert!(
|
|
v["error"]["message"]
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.contains("boom-eval")
|
|
|| v["error"]["message"]
|
|
.as_str()
|
|
.unwrap_or("")
|
|
.contains("Error"),
|
|
"eval exception should surface an expression error message"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn eval_exception_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"eval",
|
|
"(() => { throw new Error('boom-eval'); })()",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "eval exception text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error EVAL_FAILED:"),
|
|
"text must contain error EVAL_FAILED: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 19: mouse-move — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn mouse_move_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_mouse_move_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "mouse-move json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_mouse_move_success(&v, &sid, &tid, "120,140");
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "String(window.__ab_mouse_move_count !== 0)"),
|
|
"true"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_mouse_move_last_target"),
|
|
"ab-mouse-move-target"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_mouse_move_last_coords"),
|
|
"120,140"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_mouse_move_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "mouse-move text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.mouse-move"),
|
|
"must contain ok browser.mouse-move"
|
|
);
|
|
assert!(
|
|
text.contains("target: 120,140"),
|
|
"must contain target line with coordinates"
|
|
);
|
|
assert_eq!(
|
|
eval_value(&sid, &tid, "window.__ab_mouse_move_last_target"),
|
|
"ab-mouse-move-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.mouse-move");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.mouse-move");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_invalid_coordinates_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move invalid coordinates json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.mouse-move");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "INVALID_ARGUMENT");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_move_invalid_coordinates_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,abc",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "mouse-move invalid coordinates text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error INVALID_ARGUMENT:"),
|
|
"text must contain error INVALID_ARGUMENT: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 20: cursor-position — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn cursor_position_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let move_out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&move_out, "cursor-position setup mouse-move");
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_success(&out, "cursor-position json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.cursor-position");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_eq!(v["data"]["x"], 120);
|
|
assert_eq!(v["data"]["y"], 140);
|
|
assert_meta(&v);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let move_out = headless(
|
|
&[
|
|
"browser",
|
|
"mouse-move",
|
|
"120,140",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&move_out, "cursor-position setup mouse-move");
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_success(&out, "cursor-position text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.cursor-position"),
|
|
"must contain ok browser.cursor-position"
|
|
);
|
|
assert!(text.contains("x: 120"), "must contain x line: {text}");
|
|
assert!(text.contains("y: 140"), "must contain y line: {text}");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "cursor-position nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.cursor-position");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "cursor-position nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "cursor-position nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.cursor-position");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn cursor_position_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "cursor-position nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 20b: cursor-position regression — non-mouse-move actions update position
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn cursor_position_after_click_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
// Click at coordinates — this should update cursor position
|
|
let click_out = headless_json(
|
|
&[
|
|
"browser",
|
|
"click",
|
|
"200,250",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&click_out, "click setup for cursor-position");
|
|
|
|
// Now cursor-position should reflect the click coordinates
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"cursor-position",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.cursor-position");
|
|
assert_eq!(v["data"]["x"], 200, "x should match click coordinate");
|
|
assert_eq!(v["data"]["y"], 250, "y should match click coordinate");
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
// ========================================================================
|
|
// Group 21: scroll — command wiring, success path, and error path
|
|
// ========================================================================
|
|
|
|
#[test]
|
|
fn scroll_json_down_page() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "scroll json down page");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_eq!(v["data"]["action"], "scroll");
|
|
assert_eq!(v["data"]["direction"], "down");
|
|
assert_eq!(v["data"]["pixels"], 180);
|
|
assert_eq!(v["data"]["changed"]["scroll_changed"], true);
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(eval_value(&sid, &tid, "String(window.scrollY)"), "180");
|
|
assert_meta(&v);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_text_bottom_container() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"bottom",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--container",
|
|
"#ab-scroll-container",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "scroll text bottom container");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("ok browser.scroll"),
|
|
"must contain ok browser.scroll"
|
|
);
|
|
assert!(
|
|
text.contains("direction: bottom"),
|
|
"must contain direction line"
|
|
);
|
|
assert!(
|
|
text.contains("container: #ab-scroll-container"),
|
|
"must contain container line"
|
|
);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"(() => { const el = document.getElementById('ab-scroll-container'); return String(el.scrollTop === el.scrollHeight - el.clientHeight); })()"
|
|
),
|
|
"true"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_into_view_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"into-view",
|
|
"#ab-scroll-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--align",
|
|
"center",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "scroll into-view json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_eq!(v["data"]["action"], "scroll");
|
|
assert_eq!(v["data"]["target"]["selector"], "#ab-scroll-target");
|
|
assert_eq!(v["data"]["align"], "center");
|
|
assert_eq!(v["data"]["changed"]["scroll_changed"], true);
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"(() => { const rect = document.getElementById('ab-scroll-target').getBoundingClientRect(); return String(rect.top >= 0 && rect.bottom <= window.innerHeight); })()"
|
|
),
|
|
"true"
|
|
);
|
|
assert_meta(&v);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_into_view_xpath_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let xpath = "//div[@id='ab-scroll-target']";
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"into-view",
|
|
xpath,
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--align",
|
|
"center",
|
|
],
|
|
15,
|
|
);
|
|
assert_success(&out, "scroll into-view xpath json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["ok"], true);
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert!(v["error"].is_null(), "error must be null on success");
|
|
assert!(v["context"].is_object(), "context must be present");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_eq!(v["data"]["action"], "scroll");
|
|
assert_eq!(v["data"]["target"]["selector"], xpath);
|
|
assert_eq!(v["data"]["align"], "center");
|
|
assert_eq!(v["data"]["changed"]["scroll_changed"], true);
|
|
assert_eq!(v["data"]["changed"]["url_changed"], false);
|
|
assert_eq!(v["data"]["changed"]["focus_changed"], false);
|
|
assert_eq!(
|
|
eval_value(
|
|
&sid,
|
|
&tid,
|
|
"(() => { const rect = document.getElementById('ab-scroll-target').getBoundingClientRect(); return String(rect.top >= 0 && rect.bottom <= window.innerHeight); })()"
|
|
),
|
|
"true"
|
|
);
|
|
assert_meta(&v);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_session_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll nonexistent session json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert_error_envelope(&v, "SESSION_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_null(),
|
|
"context must be null when session not found"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_session_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
"nonexistent-sid",
|
|
"--tab",
|
|
"any-tab",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll nonexistent session text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error SESSION_NOT_FOUND:"),
|
|
"text must contain error SESSION_NOT_FOUND: got {text}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_tab_not_found_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll nonexistent tab json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert_error_envelope(&v, "TAB_NOT_FOUND");
|
|
assert!(
|
|
v["context"].is_object(),
|
|
"context must be present when session found"
|
|
);
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_tab_not_found_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, _tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"down",
|
|
"180",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
"nonexistent-tab-id",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll nonexistent tab text");
|
|
let text = stdout_str(&out);
|
|
assert!(
|
|
text.contains("error TAB_NOT_FOUND:"),
|
|
"text must contain error TAB_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_missing_container_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"bottom",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--container",
|
|
"#definitely-missing-container",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll missing container json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-container"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_missing_container_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"bottom",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
"--container",
|
|
"#definitely-missing-container",
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll missing container text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_into_view_missing_target_json() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let out = headless_json(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"into-view",
|
|
"#definitely-missing-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll into-view missing target json");
|
|
let v = parse_json(&out);
|
|
|
|
assert_eq!(v["command"], "browser.scroll");
|
|
assert!(v["context"].is_object(), "context must be present on error");
|
|
assert_eq!(v["context"]["session_id"], sid);
|
|
assert_eq!(v["context"]["tab_id"], tid);
|
|
assert_error_envelope(&v, "ELEMENT_NOT_FOUND");
|
|
assert_eq!(
|
|
v["error"]["details"]["selector"],
|
|
"#definitely-missing-target"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_into_view_missing_target_text() {
|
|
if skip() {
|
|
return;
|
|
}
|
|
let (sid, tid) = start_session(TEST_URL);
|
|
let _guard = SessionGuard::new(&sid);
|
|
install_scroll_fixture(&sid, &tid);
|
|
|
|
let out = headless(
|
|
&[
|
|
"browser",
|
|
"scroll",
|
|
"into-view",
|
|
"#definitely-missing-target",
|
|
"--session",
|
|
&sid,
|
|
"--tab",
|
|
&tid,
|
|
],
|
|
10,
|
|
);
|
|
assert_failure(&out, "scroll into-view missing target text");
|
|
let text = stdout_str(&out);
|
|
|
|
assert!(
|
|
text.contains(&format!("[{sid} {tid}]")),
|
|
"header must contain [session_id tab_id]: got {text}"
|
|
);
|
|
assert!(
|
|
text.contains("error ELEMENT_NOT_FOUND:"),
|
|
"text must contain error ELEMENT_NOT_FOUND: got {text}"
|
|
);
|
|
|
|
close_session(&sid);
|
|
}
|