headless_browser_tool 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.claude/settings.json +21 -0
- data/.rubocop.yml +56 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +298 -0
- data/LICENSE.md +7 -0
- data/README.md +522 -0
- data/Rakefile +12 -0
- data/config.ru +8 -0
- data/exe/hbt +7 -0
- data/lib/headless_browser_tool/browser.rb +374 -0
- data/lib/headless_browser_tool/browser_adapter.rb +320 -0
- data/lib/headless_browser_tool/cli.rb +34 -0
- data/lib/headless_browser_tool/directory_setup.rb +25 -0
- data/lib/headless_browser_tool/logger.rb +31 -0
- data/lib/headless_browser_tool/server.rb +150 -0
- data/lib/headless_browser_tool/session_manager.rb +199 -0
- data/lib/headless_browser_tool/session_middleware.rb +158 -0
- data/lib/headless_browser_tool/session_persistence.rb +146 -0
- data/lib/headless_browser_tool/stdio_server.rb +73 -0
- data/lib/headless_browser_tool/strict_session_middleware.rb +88 -0
- data/lib/headless_browser_tool/tools/attach_file_tool.rb +40 -0
- data/lib/headless_browser_tool/tools/auto_narrate_tool.rb +155 -0
- data/lib/headless_browser_tool/tools/base_tool.rb +39 -0
- data/lib/headless_browser_tool/tools/check_tool.rb +35 -0
- data/lib/headless_browser_tool/tools/choose_tool.rb +56 -0
- data/lib/headless_browser_tool/tools/click_button_tool.rb +49 -0
- data/lib/headless_browser_tool/tools/click_link_tool.rb +48 -0
- data/lib/headless_browser_tool/tools/click_tool.rb +45 -0
- data/lib/headless_browser_tool/tools/close_window_tool.rb +31 -0
- data/lib/headless_browser_tool/tools/double_click_tool.rb +37 -0
- data/lib/headless_browser_tool/tools/drag_tool.rb +46 -0
- data/lib/headless_browser_tool/tools/evaluate_script_tool.rb +20 -0
- data/lib/headless_browser_tool/tools/execute_script_tool.rb +29 -0
- data/lib/headless_browser_tool/tools/fill_in_tool.rb +66 -0
- data/lib/headless_browser_tool/tools/find_all_tool.rb +42 -0
- data/lib/headless_browser_tool/tools/find_element_tool.rb +21 -0
- data/lib/headless_browser_tool/tools/find_elements_containing_text_tool.rb +259 -0
- data/lib/headless_browser_tool/tools/get_attribute_tool.rb +21 -0
- data/lib/headless_browser_tool/tools/get_current_path_tool.rb +16 -0
- data/lib/headless_browser_tool/tools/get_current_url_tool.rb +16 -0
- data/lib/headless_browser_tool/tools/get_narration_history_tool.rb +35 -0
- data/lib/headless_browser_tool/tools/get_page_context_tool.rb +188 -0
- data/lib/headless_browser_tool/tools/get_page_source_tool.rb +16 -0
- data/lib/headless_browser_tool/tools/get_page_title_tool.rb +16 -0
- data/lib/headless_browser_tool/tools/get_session_info_tool.rb +37 -0
- data/lib/headless_browser_tool/tools/get_text_tool.rb +20 -0
- data/lib/headless_browser_tool/tools/get_value_tool.rb +20 -0
- data/lib/headless_browser_tool/tools/get_window_handles_tool.rb +29 -0
- data/lib/headless_browser_tool/tools/go_back_tool.rb +29 -0
- data/lib/headless_browser_tool/tools/go_forward_tool.rb +29 -0
- data/lib/headless_browser_tool/tools/has_element_tool.rb +21 -0
- data/lib/headless_browser_tool/tools/has_text_tool.rb +21 -0
- data/lib/headless_browser_tool/tools/hover_tool.rb +38 -0
- data/lib/headless_browser_tool/tools/is_visible_tool.rb +20 -0
- data/lib/headless_browser_tool/tools/maximize_window_tool.rb +34 -0
- data/lib/headless_browser_tool/tools/open_new_window_tool.rb +25 -0
- data/lib/headless_browser_tool/tools/refresh_tool.rb +32 -0
- data/lib/headless_browser_tool/tools/resize_window_tool.rb +43 -0
- data/lib/headless_browser_tool/tools/right_click_tool.rb +37 -0
- data/lib/headless_browser_tool/tools/save_page_tool.rb +32 -0
- data/lib/headless_browser_tool/tools/screenshot_tool.rb +199 -0
- data/lib/headless_browser_tool/tools/search_page_tool.rb +224 -0
- data/lib/headless_browser_tool/tools/search_source_tool.rb +148 -0
- data/lib/headless_browser_tool/tools/select_tool.rb +44 -0
- data/lib/headless_browser_tool/tools/switch_to_window_tool.rb +30 -0
- data/lib/headless_browser_tool/tools/uncheck_tool.rb +35 -0
- data/lib/headless_browser_tool/tools/visit_tool.rb +27 -0
- data/lib/headless_browser_tool/tools/visual_diff_tool.rb +177 -0
- data/lib/headless_browser_tool/tools.rb +104 -0
- data/lib/headless_browser_tool/version.rb +5 -0
- data/lib/headless_browser_tool.rb +8 -0
- metadata +256 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetCurrentUrlTool < BaseTool
|
8
|
+
tool_name "get_current_url"
|
9
|
+
description "Get the current page URL"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
browser.get_current_url
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetNarrationHistoryTool < BaseTool
|
8
|
+
tool_name "get_narration_history"
|
9
|
+
description "Get the history of narrated events since auto_narrate was enabled"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
history = browser.evaluate_script("window.getAINarration ? window.getAINarration() : []")
|
13
|
+
|
14
|
+
if history.nil? || history.empty?
|
15
|
+
"No narration history available. Enable auto_narrate first."
|
16
|
+
else
|
17
|
+
format_history(history)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def format_history(history)
|
24
|
+
output = ["🎬 Browser Event History:"]
|
25
|
+
|
26
|
+
history.last(20).each do |event|
|
27
|
+
time = Time.parse(event["timestamp"]).strftime("%H:%M:%S")
|
28
|
+
output << "[#{time}] #{event["message"]}"
|
29
|
+
end
|
30
|
+
|
31
|
+
output.join("\n")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetPageContextTool < BaseTool
|
8
|
+
tool_name "get_page_context"
|
9
|
+
description "Get a structured understanding of the current page"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
context_script = <<~JS
|
13
|
+
(() => {
|
14
|
+
// Helper to generate unique selector for elements
|
15
|
+
const getUniqueSelector = (elem) => {
|
16
|
+
if (elem.id) {
|
17
|
+
return '#' + CSS.escape(elem.id);
|
18
|
+
}
|
19
|
+
|
20
|
+
if (elem.className && typeof elem.className === 'string') {
|
21
|
+
const classes = elem.className.trim().split(/\\s+/)
|
22
|
+
.filter(c => c.length > 0)
|
23
|
+
.map(c => '.' + CSS.escape(c))
|
24
|
+
.join('');
|
25
|
+
if (classes && document.querySelectorAll(elem.tagName + classes).length === 1) {
|
26
|
+
return elem.tagName.toLowerCase() + classes;
|
27
|
+
}
|
28
|
+
}
|
29
|
+
|
30
|
+
// Build path from root
|
31
|
+
const path = [];
|
32
|
+
let current = elem;
|
33
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
34
|
+
let selector = current.tagName.toLowerCase();
|
35
|
+
if (current.id) {
|
36
|
+
selector = '#' + CSS.escape(current.id);
|
37
|
+
path.unshift(selector);
|
38
|
+
break;
|
39
|
+
} else {
|
40
|
+
let sibling = current;
|
41
|
+
let nth = 1;
|
42
|
+
while (sibling.previousElementSibling) {
|
43
|
+
sibling = sibling.previousElementSibling;
|
44
|
+
if (sibling.tagName === current.tagName) nth++;
|
45
|
+
}
|
46
|
+
if (nth > 1) selector += ':nth-of-type(' + nth + ')';
|
47
|
+
}
|
48
|
+
path.unshift(selector);
|
49
|
+
current = current.parentElement;
|
50
|
+
}
|
51
|
+
return path.join(' > ');
|
52
|
+
};
|
53
|
+
|
54
|
+
// Identify page type
|
55
|
+
const identifyPageType = () => {
|
56
|
+
const url = window.location.href;
|
57
|
+
const title = document.title.toLowerCase();
|
58
|
+
const h1 = document.querySelector('h1')?.textContent.toLowerCase() || '';
|
59
|
+
|
60
|
+
if (url.includes('login') || title.includes('login') || h1.includes('login')) return 'login';
|
61
|
+
if (url.includes('search') || title.includes('search')) return 'search_results';
|
62
|
+
if (url.includes('cart') || title.includes('cart')) return 'shopping_cart';
|
63
|
+
if (url.includes('checkout')) return 'checkout';
|
64
|
+
if (document.querySelector('form[method="post"]')) return 'form_page';
|
65
|
+
if (document.querySelectorAll('article').length > 3) return 'article_list';
|
66
|
+
if (document.querySelector('article')) return 'article';
|
67
|
+
return 'general';
|
68
|
+
};
|
69
|
+
|
70
|
+
// Get main navigation
|
71
|
+
const getNavigation = () => {
|
72
|
+
const nav = document.querySelector('nav') || document.querySelector('[role="navigation"]');
|
73
|
+
if (!nav) return [];
|
74
|
+
|
75
|
+
return Array.from(nav.querySelectorAll('a')).slice(0, 10).map((a, index) => ({
|
76
|
+
text: a.textContent.trim(),
|
77
|
+
href: a.href,
|
78
|
+
selector: getUniqueSelector(a)
|
79
|
+
}));
|
80
|
+
};
|
81
|
+
|
82
|
+
// Get actionable elements
|
83
|
+
const getActions = () => {
|
84
|
+
const actions = [];
|
85
|
+
|
86
|
+
// Primary buttons
|
87
|
+
document.querySelectorAll('button[type="submit"], button.primary, button.btn-primary').forEach(btn => {
|
88
|
+
if (btn.offsetWidth > 0) {
|
89
|
+
actions.push({
|
90
|
+
type: 'primary_button',
|
91
|
+
text: btn.textContent.trim(),
|
92
|
+
selector: btn.className ? `.${btn.className.split(' ')[0]}` : 'button'
|
93
|
+
});
|
94
|
+
}
|
95
|
+
});
|
96
|
+
|
97
|
+
// Forms
|
98
|
+
document.querySelectorAll('form').forEach((form, index) => {
|
99
|
+
const formInputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), textarea, select'))
|
100
|
+
.map(input => ({
|
101
|
+
name: input.name || input.id,
|
102
|
+
type: input.type || 'text',
|
103
|
+
required: input.required,
|
104
|
+
value: input.value,
|
105
|
+
selector: getUniqueSelector(input)
|
106
|
+
}));
|
107
|
+
|
108
|
+
if (formInputs.length > 0) {
|
109
|
+
actions.push({
|
110
|
+
type: 'form',
|
111
|
+
action: form.action,
|
112
|
+
method: form.method,
|
113
|
+
selector: getUniqueSelector(form),
|
114
|
+
inputs: formInputs
|
115
|
+
});
|
116
|
+
}
|
117
|
+
});
|
118
|
+
|
119
|
+
return actions;
|
120
|
+
};
|
121
|
+
|
122
|
+
// Get key content areas
|
123
|
+
const getContentAreas = () => {
|
124
|
+
const areas = {};
|
125
|
+
|
126
|
+
// Main content
|
127
|
+
const main = document.querySelector('main') || document.querySelector('[role="main"]') || document.querySelector('#content');
|
128
|
+
if (main) {
|
129
|
+
areas.main = main.textContent.trim().substring(0, 200) + '...';
|
130
|
+
}
|
131
|
+
|
132
|
+
// Headings structure
|
133
|
+
areas.headings = Array.from(document.querySelectorAll('h1, h2, h3')).slice(0, 10).map(h => ({
|
134
|
+
level: h.tagName,
|
135
|
+
text: h.textContent.trim(),
|
136
|
+
selector: getUniqueSelector(h)
|
137
|
+
}));
|
138
|
+
|
139
|
+
// Errors or alerts
|
140
|
+
const alerts = document.querySelectorAll('[role="alert"], .error, .alert, .warning, .success');
|
141
|
+
if (alerts.length > 0) {
|
142
|
+
areas.alerts = Array.from(alerts).map(a => ({
|
143
|
+
text: a.textContent.trim(),
|
144
|
+
selector: getUniqueSelector(a)
|
145
|
+
}));
|
146
|
+
}
|
147
|
+
|
148
|
+
return areas;
|
149
|
+
};
|
150
|
+
|
151
|
+
// Get data attributes that might be useful
|
152
|
+
const getDataAttributes = () => {
|
153
|
+
const elements = document.querySelectorAll('[data-testid], [data-test], [data-cy]');
|
154
|
+
return Array.from(elements).slice(0, 20).map(el => ({
|
155
|
+
testId: el.dataset.testid || el.dataset.test || el.dataset.cy,
|
156
|
+
tag: el.tagName.toLowerCase(),
|
157
|
+
text: el.textContent.trim().substring(0, 50),
|
158
|
+
selector: getUniqueSelector(el)
|
159
|
+
}));
|
160
|
+
};
|
161
|
+
|
162
|
+
return {
|
163
|
+
url: window.location.href,
|
164
|
+
title: document.title,
|
165
|
+
pageType: identifyPageType(),
|
166
|
+
navigation: getNavigation(),
|
167
|
+
actions: getActions(),
|
168
|
+
contentAreas: getContentAreas(),
|
169
|
+
testIds: getDataAttributes(),
|
170
|
+
metrics: {
|
171
|
+
loadTime: performance.timing.loadEventEnd - performance.timing.navigationStart,
|
172
|
+
domElements: document.querySelectorAll('*').length,
|
173
|
+
images: document.images.length,
|
174
|
+
scripts: document.scripts.length
|
175
|
+
}
|
176
|
+
};
|
177
|
+
})();
|
178
|
+
JS
|
179
|
+
|
180
|
+
context = browser.evaluate_script(context_script)
|
181
|
+
return { error: "Unable to get page context" } if context.nil?
|
182
|
+
|
183
|
+
# Return raw context data instead of formatted string
|
184
|
+
context
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetPageSourceTool < BaseTool
|
8
|
+
tool_name "get_page_source"
|
9
|
+
description "Get full HTML source of current page"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
browser.get_page_source
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetPageTitleTool < BaseTool
|
8
|
+
tool_name "get_page_title"
|
9
|
+
description "Get the page title from <title> tag"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
browser.get_page_title
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetSessionInfoTool < BaseTool
|
8
|
+
tool_name "get_session_info"
|
9
|
+
description "Get information about the current browser session"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
if HeadlessBrowserTool::Server.single_session_mode
|
13
|
+
{
|
14
|
+
mode: "single_session",
|
15
|
+
session_id: "shared",
|
16
|
+
message: "Server is running in single session mode"
|
17
|
+
}
|
18
|
+
else
|
19
|
+
session_id = Thread.current[:hbt_session_id] || "default"
|
20
|
+
session_info = HeadlessBrowserTool::Server.session_manager.session_info
|
21
|
+
|
22
|
+
current_session = session_info[:session_data][session_id]
|
23
|
+
|
24
|
+
{
|
25
|
+
mode: "multi_session",
|
26
|
+
session_id: session_id,
|
27
|
+
created_at: current_session&.dig(:created_at),
|
28
|
+
last_activity: current_session&.dig(:last_activity),
|
29
|
+
idle_time: current_session&.dig(:idle_time),
|
30
|
+
active_sessions: session_info[:active_sessions],
|
31
|
+
total_sessions: session_info[:session_count]
|
32
|
+
}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetTextTool < BaseTool
|
8
|
+
tool_name "get_text"
|
9
|
+
description "Get the visible text content of an element"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:selector).filled(:string).description("CSS selector of the element")
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(selector:)
|
16
|
+
browser.get_text(selector)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetValueTool < BaseTool
|
8
|
+
tool_name "get_value"
|
9
|
+
description "Get the value of an input field"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:selector).filled(:string).description("CSS selector of the input field")
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(selector:)
|
16
|
+
browser.get_value(selector)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GetWindowHandlesTool < BaseTool
|
8
|
+
tool_name "get_window_handles"
|
9
|
+
description "Get array of all window handles"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
handles = browser.get_window_handles
|
13
|
+
current_handle = browser.current_window_handle
|
14
|
+
|
15
|
+
{
|
16
|
+
current_window: current_handle,
|
17
|
+
windows: handles.map.with_index do |handle, index|
|
18
|
+
{
|
19
|
+
handle: handle,
|
20
|
+
index: index,
|
21
|
+
is_current: handle == current_handle
|
22
|
+
}
|
23
|
+
end,
|
24
|
+
total_windows: handles.size
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GoBackTool < BaseTool
|
8
|
+
tool_name "go_back"
|
9
|
+
description "Navigate back in browser history"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
url_before = browser.current_url
|
13
|
+
browser.title
|
14
|
+
|
15
|
+
browser.go_back
|
16
|
+
|
17
|
+
{
|
18
|
+
navigation: {
|
19
|
+
from: url_before,
|
20
|
+
to: browser.current_url,
|
21
|
+
title: browser.title,
|
22
|
+
navigated: browser.current_url != url_before
|
23
|
+
},
|
24
|
+
status: "navigated_back"
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class GoForwardTool < BaseTool
|
8
|
+
tool_name "go_forward"
|
9
|
+
description "Navigate forward in browser history"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
url_before = browser.current_url
|
13
|
+
browser.title
|
14
|
+
|
15
|
+
browser.go_forward
|
16
|
+
|
17
|
+
{
|
18
|
+
navigation: {
|
19
|
+
from: url_before,
|
20
|
+
to: browser.current_url,
|
21
|
+
title: browser.title,
|
22
|
+
navigated: browser.current_url != url_before
|
23
|
+
},
|
24
|
+
status: "navigated_forward"
|
25
|
+
}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class HasElementTool < BaseTool
|
8
|
+
tool_name "has_element"
|
9
|
+
description "Check if an element exists on the page"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:selector).filled(:string).description("CSS selector of the element")
|
13
|
+
optional(:wait_seconds).filled(:integer).description("Optional timeout in seconds")
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(selector:, wait_seconds: nil)
|
17
|
+
browser.has_element?(selector, wait_seconds)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class HasTextTool < BaseTool
|
8
|
+
tool_name "has_text"
|
9
|
+
description "Check if text appears on the page"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:text).filled(:string).description("Text to search for")
|
13
|
+
optional(:wait_seconds).filled(:integer).description("Optional timeout in seconds")
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(text:, wait_seconds: nil)
|
17
|
+
browser.has_text?(text, wait_seconds)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class HoverTool < BaseTool
|
8
|
+
tool_name "hover"
|
9
|
+
description "Hover over an element by CSS selector"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:selector).filled(:string).description("CSS selector of the element to hover over")
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(selector:)
|
16
|
+
element = browser.find(selector)
|
17
|
+
element_info = {
|
18
|
+
tag_name: element.tag_name,
|
19
|
+
text: element.text.strip,
|
20
|
+
visible: element.visible?,
|
21
|
+
attributes: {
|
22
|
+
id: element[:id],
|
23
|
+
class: element[:class],
|
24
|
+
title: element[:title]
|
25
|
+
}.compact
|
26
|
+
}
|
27
|
+
|
28
|
+
browser.hover(selector)
|
29
|
+
|
30
|
+
{
|
31
|
+
selector: selector,
|
32
|
+
element: element_info,
|
33
|
+
status: "hovering"
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class IsVisibleTool < BaseTool
|
8
|
+
tool_name "is_visible"
|
9
|
+
description "Check if element is visible on page"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:selector).filled(:string).description("CSS selector of the element to check")
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(selector:)
|
16
|
+
browser.is_visible?(selector)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class MaximizeWindowTool < BaseTool
|
8
|
+
tool_name "maximize_window"
|
9
|
+
description "Maximize the browser window"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
# Get window size before maximizing
|
13
|
+
size_before = browser.current_window_size
|
14
|
+
|
15
|
+
browser.maximize_window
|
16
|
+
|
17
|
+
# Get window size after maximizing
|
18
|
+
size_after = browser.current_window_size
|
19
|
+
|
20
|
+
{
|
21
|
+
size_before: {
|
22
|
+
width: size_before[0],
|
23
|
+
height: size_before[1]
|
24
|
+
},
|
25
|
+
size_after: {
|
26
|
+
width: size_after[0],
|
27
|
+
height: size_after[1]
|
28
|
+
},
|
29
|
+
status: "maximized"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class OpenNewWindowTool < BaseTool
|
8
|
+
tool_name "open_new_window"
|
9
|
+
description "Open a new browser window/tab"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
initial_windows = browser.windows
|
13
|
+
window_handle = browser.open_new_window
|
14
|
+
|
15
|
+
{
|
16
|
+
window_handle: window_handle,
|
17
|
+
total_windows: browser.windows.count,
|
18
|
+
previous_windows: initial_windows,
|
19
|
+
current_window: browser.current_window,
|
20
|
+
status: "opened"
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class RefreshTool < BaseTool
|
8
|
+
tool_name "refresh"
|
9
|
+
description "Reload the current page"
|
10
|
+
|
11
|
+
def execute
|
12
|
+
url_before = browser.current_url
|
13
|
+
title_before = browser.title
|
14
|
+
|
15
|
+
browser.refresh
|
16
|
+
|
17
|
+
# Brief wait for refresh to complete
|
18
|
+
sleep 0.1
|
19
|
+
|
20
|
+
{
|
21
|
+
url: browser.current_url,
|
22
|
+
title: browser.title,
|
23
|
+
changed: {
|
24
|
+
url: url_before != browser.current_url,
|
25
|
+
title: title_before != browser.title
|
26
|
+
},
|
27
|
+
status: "success"
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_tool"
|
4
|
+
|
5
|
+
module HeadlessBrowserTool
|
6
|
+
module Tools
|
7
|
+
class ResizeWindowTool < BaseTool
|
8
|
+
tool_name "resize_window"
|
9
|
+
description "Resize the browser window"
|
10
|
+
|
11
|
+
arguments do
|
12
|
+
required(:width).filled(:integer).description("Window width in pixels")
|
13
|
+
required(:height).filled(:integer).description("Window height in pixels")
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(width:, height:)
|
17
|
+
# Get window size before resizing
|
18
|
+
size_before = browser.current_window_size
|
19
|
+
|
20
|
+
browser.resize_window(width, height)
|
21
|
+
|
22
|
+
# Get actual window size after resizing
|
23
|
+
size_after = browser.current_window_size
|
24
|
+
|
25
|
+
{
|
26
|
+
requested_size: {
|
27
|
+
width: width,
|
28
|
+
height: height
|
29
|
+
},
|
30
|
+
size_before: {
|
31
|
+
width: size_before[0],
|
32
|
+
height: size_before[1]
|
33
|
+
},
|
34
|
+
size_after: {
|
35
|
+
width: size_after[0],
|
36
|
+
height: size_after[1]
|
37
|
+
},
|
38
|
+
status: "resized"
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|