1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! Tests for the `browser_find` JS builder. Each test pins the shape
//! of the JS we send into the page — assigning a stable
//! `data-opencrabs-match` attribute and returning an array of
//! `{selector, text, tag, visible}` objects — so the selectors the
//! model passes back to browser_click are deterministic.
//!
//! We can't run the JS (that requires a real page / V8), so these
//! tests verify we emit the right script shape for each mode and
//! that user-supplied patterns are correctly escaped.
#![cfg(feature = "browser")]
use crate::brain::tools::browser::build_find_js;
#[test]
fn css_mode_uses_deep_query_selector_all() {
let js = build_find_js("css", "button.primary", 20);
// `__ocQueryAll` instead of `document.querySelectorAll`: css search
// reaches inside open shadow roots, and the limit is applied by the
// helper rather than a trailing `.slice`.
assert!(js.contains(r#"__ocQueryAll("button.primary", 20)"#));
}
#[test]
fn xpath_mode_uses_document_evaluate() {
let js = build_find_js("xpath", "//button[@type='submit']", 5);
assert!(js.contains("document.evaluate("));
assert!(js.contains("XPathResult.ORDERED_NODE_SNAPSHOT_TYPE"));
assert!(js.contains("//button[@type='submit']"));
assert!(js.contains("i < 5"));
}
#[test]
fn text_mode_walks_dom_for_substring() {
let js = build_find_js("text", "Sign in", 10);
// The composed-tree walk replaces the flat
// `createTreeWalker(document.body, ...)`, so text search sees
// elements rendered inside open shadow roots.
assert!(js.contains("for (const node of __ocWalk())"));
assert!(js.contains("out.length >= 10"));
// Pattern gets lowercased server-side for case-insensitive match
assert!(js.contains(r#""Sign in".toLowerCase()"#));
}
#[test]
fn aria_mode_uses_attribute_selector() {
let js = build_find_js("aria", "Close dialog", 3);
assert!(js.contains(r#"[aria-label*="Close dialog" i]"#));
}
#[test]
fn unknown_mode_defaults_to_css() {
let js = build_find_js("nonsense", ".btn", 5);
assert!(
js.contains(r#"__ocQueryAll(".btn", 5)"#),
"unknown mode must fall back to CSS selector path"
);
}
#[test]
fn escapes_double_quotes_in_pattern() {
// A pattern containing a double quote would otherwise close the JS
// string literal early and inject arbitrary code into the page
// context. This guard is a security boundary.
let js = build_find_js("css", "div[data-foo=\"bar\"]", 5);
// The inner double quotes must be backslash-escaped so the
// outer querySelectorAll(" … ") stays balanced.
assert!(js.contains(r#"div[data-foo=\"bar\"]"#));
// And no raw unescaped `"bar"` substring sneaks through breaking
// the outer literal.
assert!(
!js.contains(r#"__ocQueryAll("div[data-foo="bar"]", 5)"#),
"unescaped inner double-quotes would break the JS string literal"
);
}
#[test]
fn escapes_backslashes() {
// Backslash is the JS escape introducer — leaving one raw lets the
// user's pattern bend subsequent chars into escape sequences.
let js = build_find_js("css", r"div\.class", 5);
assert!(js.contains(r"div\\.class"));
}
#[test]
fn clears_previous_match_attributes_before_re_enumerating() {
// Every call starts by stripping any `data-opencrabs-match`
// attributes left over from the previous call. Without this,
// stale indices from call N would coexist with fresh indices
// from call N+1 on different elements and the returned selector
// would be ambiguous.
// The clear is DEEP (`__ocClearStamps`): once we stamp inside a
// shadow root, a flat `document.querySelectorAll` cleanup would
// leave those stamps rotting and a stale
// `[data-opencrabs-match="3"]` could resolve to a node from a
// previous page state.
let js = build_find_js("css", ".btn", 5);
assert!(js.contains("__ocClearStamps();"));
assert!(js.contains("removeAttribute('data-opencrabs-match')"));
}
#[test]
fn xpath_mode_stays_light_dom_by_spec() {
// XPath has no notion of a shadow boundary, so `document.evaluate`
// cannot cross one however we call it. The mode must therefore keep
// the flat evaluate and the tool description must say so.
let js = build_find_js("xpath", "//button", 5);
assert!(js.contains("document.evaluate("));
// Scope the negative to the node-collection expression: the shared
// wrapper DEFINES the deep helpers for every mode, so asserting
// against the whole script would only prove the preamble exists.
let collector = js
.split("const nodes =")
.nth(1)
.and_then(|rest| rest.split("const out = [];").next())
.expect("wrapper splices the collector in as `const nodes = ...`");
assert!(!collector.contains("__ocQueryAll("));
assert!(!collector.contains("__ocWalk()"));
}
#[test]
fn returns_object_with_stable_selector_per_match() {
// The payload shape the model sees must be:
// { selector, text, tag, visible, shadow }
// Selector uses the attribute we just assigned so it's unique
// and survives subsequent DOM churn (within the same turn).
let js = build_find_js("css", "button", 5);
assert!(js.contains(r#"selector: '[data-opencrabs-match="' + i + '"]'"#));
assert!(js.contains("text:"));
assert!(js.contains("tag:"));
assert!(js.contains("visible:"));
// `shadow` is additive — it tells the model the element sits behind
// a shadow boundary without changing the selector shape.
assert!(js.contains("shadow: __ocInShadow(el)"));
}