isoautomate 0.1.0
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/LICENSE +21 -0
- data/README.md +103 -0
- data/ext/sdk-ruby.png +0 -0
- data/isoautomate.gemspec +31 -0
- data/lib/isoautomate/client.rb +911 -0
- data/lib/isoautomate/config.rb +15 -0
- data/lib/isoautomate/version.rb +3 -0
- data/lib/isoautomate.rb +11 -0
- metadata +119 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 73ae7755ecf1b7f8ecb33fd0bfee1bbb66dbf3140716e3391c67fa33f5a96c00
|
|
4
|
+
data.tar.gz: d7a51abd998faf6c248a204feb8a3f8b43677a3bc836d1057478b36972e8abd7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ac945e07957d11158ea4a3e5b4f8c69d6dcaf5b81baca0a3dd3dccba4ded90067e79834928f1ddb043889fe04902054434619b50defe894732f03427cfe661ec
|
|
7
|
+
data.tar.gz: e9a01ab37c8143c8e62b240de1e1d4c76e7821bef63046d10b07fcc0ba2a71db74009e2538812fe6c1b139aaadb5620812b48b23b631e088d68d387adb1d6b1d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 isoAutomate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1 align="center">isoAutomate Ruby SDK</h1>
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Enterprise-Grade Browser Orchestration for Ruby</b>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<a href="https://opensource.org/licenses/MIT">
|
|
8
|
+
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://isoautomate.com/docs">
|
|
11
|
+
<img src="https://img.shields.io/badge/Docs-isoautomate.com-blue.svg" alt="Documentation">
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Introduction
|
|
18
|
+
|
|
19
|
+
The **isoAutomate Ruby SDK** provides a high-level, idiomatic Ruby client for controlling remote browsers via **isoFleet**. It brings the full power of 120+ SeleniumBase-style actions to the Ruby ecosystem, optimized for high-performance automation.
|
|
20
|
+
|
|
21
|
+
- **Idiomatic Ruby**: Pure Ruby implementation using modern syntax.
|
|
22
|
+
- **Stealth First**: Native support for OS-level GUI interactions and bot-bypass.
|
|
23
|
+
- **120+ Actions**: Complete parity with Python, Go, and Java SDKs.
|
|
24
|
+
- **Automated Assertions**: Failure screenshots are automatically captured and saved locally.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Add this line to your application's `Gemfile`:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
gem 'isoautomate'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
And then execute:
|
|
37
|
+
```bash
|
|
38
|
+
$ bundle install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or install it directly via:
|
|
42
|
+
```bash
|
|
43
|
+
$ gem install isoautomate
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
The SDK automatically detects configuration from your environment or a .env file:
|
|
48
|
+
```ini
|
|
49
|
+
REDIS_HOST=localhost
|
|
50
|
+
REDIS_PORT=6379
|
|
51
|
+
REDIS_PASSWORD=your_password
|
|
52
|
+
REDIS_DB=0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage Example
|
|
56
|
+
```Ruby
|
|
57
|
+
require 'isoautomate'
|
|
58
|
+
|
|
59
|
+
# The client uses Redis to communicate with your browser workers
|
|
60
|
+
client = IsoAutomate::Client.new
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
# 1. Acquire a browser session
|
|
64
|
+
client.acquire("chrome", record: true)
|
|
65
|
+
|
|
66
|
+
# 2. Perform actions
|
|
67
|
+
client.open_url("[https://example.com](https://example.com)")
|
|
68
|
+
client.type("#search", "isoAutomate")
|
|
69
|
+
client.click(".submit-btn")
|
|
70
|
+
|
|
71
|
+
# 3. Use built-in assertions (saves screenshot on failure)
|
|
72
|
+
client.assert_text("Results", "h1")
|
|
73
|
+
|
|
74
|
+
puts "Session Video: #{client.video_url}"
|
|
75
|
+
ensure
|
|
76
|
+
# Always release the browser back to the pool
|
|
77
|
+
client.release
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Core Capabilities
|
|
82
|
+
**MFA Handling**
|
|
83
|
+
Generate and enter TOTP codes instantly.
|
|
84
|
+
```Ruby
|
|
85
|
+
code = client.get_mfa_code("YOUR_SECRET_KEY")
|
|
86
|
+
client.type("#otp-input", code)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Stealth & OS-Level Control**
|
|
90
|
+
Bypass detection using real OS-level events.
|
|
91
|
+
```Ruby
|
|
92
|
+
client.gui_click_element("#captcha-checkbox")
|
|
93
|
+
client.gui_write("Typing like a human...")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Session Persistance**
|
|
97
|
+
Maintain logins by saving and loading cookie states.
|
|
98
|
+
```Ruby
|
|
99
|
+
client.save_cookies("session.json")
|
|
100
|
+
client.load_cookies("session.json")
|
|
101
|
+
```
|
|
102
|
+
## License
|
|
103
|
+
MIT License - Copyright (c) 2026 isoAutomate
|
data/ext/sdk-ruby.png
ADDED
|
Binary file
|
data/isoautomate.gemspec
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require "isoautomate/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "isoautomate"
|
|
7
|
+
spec.version = Isoautomate::VERSION
|
|
8
|
+
spec.authors = ["isoAutomate Team"]
|
|
9
|
+
spec.email = ["support@isoautomate.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Official Ruby SDK for the isoAutomate Sovereign Browser Infrastructure."
|
|
12
|
+
spec.description = "Provides connectivity to the isoFleet engine via Redis for distributed browser automation."
|
|
13
|
+
spec.homepage = "https://github.com/isoautomate/isoautomate-ruby"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
|
|
16
|
+
# Prevent pushing this gem to RubyGems.org by mistake - remove if you intend to publish publicly
|
|
17
|
+
# spec.metadata["allowed_push_host"] = "TODO: Set to 'https://rubygems.org'"
|
|
18
|
+
|
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
21
|
+
end
|
|
22
|
+
spec.bindir = "exe"
|
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
spec.add_dependency "redis", ">= 4.0"
|
|
27
|
+
spec.add_dependency "dotenv", ">= 2.7"
|
|
28
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
31
|
+
end
|
|
@@ -0,0 +1,911 @@
|
|
|
1
|
+
require 'redis'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'logger'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'dotenv'
|
|
8
|
+
|
|
9
|
+
require 'isoautomate/config'
|
|
10
|
+
require 'isoautomate/exceptions'
|
|
11
|
+
require 'isoautomate/utils'
|
|
12
|
+
|
|
13
|
+
module Isoautomate
|
|
14
|
+
class Client
|
|
15
|
+
# ----------------------------------------------------------------
|
|
16
|
+
# SETUP & CONFIGURATION
|
|
17
|
+
# ----------------------------------------------------------------
|
|
18
|
+
attr_reader :session, :video_url, :record_url
|
|
19
|
+
|
|
20
|
+
def initialize(redis_url: nil, redis_host: nil, redis_port: nil, redis_password: nil, redis_db: nil, redis_ssl: false, env_file: nil)
|
|
21
|
+
# Load Env
|
|
22
|
+
if env_file && File.exist?(env_file)
|
|
23
|
+
Dotenv.load(env_file)
|
|
24
|
+
else
|
|
25
|
+
Dotenv.load
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Logger
|
|
29
|
+
@logger = Logger.new(STDOUT)
|
|
30
|
+
@logger.level = Logger::INFO
|
|
31
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
|
32
|
+
"[#{severity}] #{msg}\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Redis Config
|
|
36
|
+
env_url = ENV["REDIS_URL"]
|
|
37
|
+
env_host = ENV["REDIS_HOST"]
|
|
38
|
+
env_port = ENV["REDIS_PORT"]
|
|
39
|
+
env_pass = ENV["REDIS_PASSWORD"]
|
|
40
|
+
env_db = ENV["REDIS_DB"]
|
|
41
|
+
env_ssl = ENV["REDIS_SSL"].to_s.downcase == "true"
|
|
42
|
+
|
|
43
|
+
@redis_url = redis_url || env_url
|
|
44
|
+
@host = redis_host || env_host
|
|
45
|
+
@port = redis_port || env_port
|
|
46
|
+
@password = redis_password || env_pass
|
|
47
|
+
@db = redis_db || (env_db || 0).to_i
|
|
48
|
+
@ssl = redis_ssl || env_ssl
|
|
49
|
+
|
|
50
|
+
if @redis_url.nil? && @host.nil?
|
|
51
|
+
raise BrowserError, "Missing Redis Configuration."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Connect to Redis
|
|
55
|
+
begin
|
|
56
|
+
if @redis_url
|
|
57
|
+
@r = Redis.new(url: @redis_url)
|
|
58
|
+
else
|
|
59
|
+
actual_port = @port ? @port.to_i : 6379
|
|
60
|
+
@r = Redis.new(
|
|
61
|
+
host: @host,
|
|
62
|
+
port: actual_port,
|
|
63
|
+
password: @password,
|
|
64
|
+
db: @db,
|
|
65
|
+
ssl: @ssl
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
# Test connection
|
|
69
|
+
@r.ping
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
raise BrowserError, "Failed to initialize Redis connection: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@session = nil
|
|
75
|
+
@video_url = nil
|
|
76
|
+
@record_url = nil
|
|
77
|
+
@session_data = {}
|
|
78
|
+
@init_sent = false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- Redis Wrappers (Using the Utils Module) ---
|
|
82
|
+
def _r_rpush(key, *values)
|
|
83
|
+
Isoautomate::Utils.redis_retry { @r.rpush(key, values) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def _r_get(key)
|
|
87
|
+
Isoautomate::Utils.redis_retry { @r.get(key) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def _r_delete(key)
|
|
91
|
+
Isoautomate::Utils.redis_retry { @r.del(key) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- Lifecycle (Ruby equivalent of Context Manager) ---
|
|
95
|
+
# Python's __enter__ returns self.
|
|
96
|
+
# In Ruby, users will likely use: client = Client.new; client.acquire; ... client.release
|
|
97
|
+
def open
|
|
98
|
+
# Placeholder if block syntax is added later
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def close
|
|
103
|
+
return unless @session
|
|
104
|
+
begin
|
|
105
|
+
@logger.info("[SDK] Auto-releasing session #{@session['browser_id'][0..5]}...")
|
|
106
|
+
release
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
@logger.error("[SDK] Release failed during cleanup: #{e.message}")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ----------------------------------------------------------------
|
|
113
|
+
# ACQUIRE & RELEASE
|
|
114
|
+
# ----------------------------------------------------------------
|
|
115
|
+
def acquire(browser_type: "chrome", video: false, profile: nil, record: false)
|
|
116
|
+
profile_id = nil
|
|
117
|
+
|
|
118
|
+
# Profile Handling
|
|
119
|
+
if profile == true
|
|
120
|
+
profile_store = File.join(Dir.pwd, ".iso_profiles")
|
|
121
|
+
FileUtils.mkdir_p(profile_store)
|
|
122
|
+
id_file = File.join(profile_store, "default_profile.id")
|
|
123
|
+
|
|
124
|
+
if File.exist?(id_file)
|
|
125
|
+
profile_id = File.read(id_file).strip
|
|
126
|
+
else
|
|
127
|
+
profile_id = "user_#{SecureRandom.hex(4)}"
|
|
128
|
+
File.write(id_file, profile_id)
|
|
129
|
+
end
|
|
130
|
+
elsif profile.is_a?(String)
|
|
131
|
+
profile_id = profile
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@init_sent = false
|
|
135
|
+
|
|
136
|
+
# Lua Script (Identical to Python)
|
|
137
|
+
lua_script = <<~LUA
|
|
138
|
+
local workers = redis.call('SMEMBERS', KEYS[1])
|
|
139
|
+
for i = #workers, 2, -1 do
|
|
140
|
+
local j = math.random(i)
|
|
141
|
+
workers[i], workers[j] = workers[j], workers[i]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
for _, worker in ipairs(workers) do
|
|
145
|
+
local free_key = ARGV[1] .. worker .. ':' .. ARGV[2] .. ':free'
|
|
146
|
+
local bid = redis.call('SPOP', free_key)
|
|
147
|
+
if bid then
|
|
148
|
+
local busy_key = ARGV[1] .. worker .. ':' .. ARGV[2] .. ':busy'
|
|
149
|
+
redis.call('SADD', busy_key, bid)
|
|
150
|
+
return {worker, bid}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
return nil
|
|
154
|
+
LUA
|
|
155
|
+
|
|
156
|
+
begin
|
|
157
|
+
result = @r.eval(lua_script, keys: [Isoautomate::WORKERS_SET], argv: [Isoautomate::REDIS_PREFIX, browser_type])
|
|
158
|
+
rescue StandardError => e
|
|
159
|
+
raise BrowserError, "Redis Lua Error: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if result
|
|
163
|
+
worker_name = result[0]
|
|
164
|
+
bid = result[1]
|
|
165
|
+
|
|
166
|
+
@session = {
|
|
167
|
+
"browser_id" => bid,
|
|
168
|
+
"worker" => worker_name,
|
|
169
|
+
"browser_type" => browser_type,
|
|
170
|
+
"video" => video,
|
|
171
|
+
"profile_id" => profile_id,
|
|
172
|
+
"record" => record
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if profile_id || video || record
|
|
176
|
+
@logger.info("[SDK] Initializing persistent environment on #{worker_name}...")
|
|
177
|
+
_send("get_title")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
return { "status" => "ok", "browser_id" => bid, "worker" => worker_name }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
raise BrowserError, "No browsers available for type: '#{browser_type}'. Check workers."
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def release
|
|
187
|
+
return { "status" => "error", "error" => "not_acquired" } unless @session
|
|
188
|
+
|
|
189
|
+
begin
|
|
190
|
+
if @session["video"]
|
|
191
|
+
@logger.info("[SDK] Stopping video...")
|
|
192
|
+
res = _send("stop_video", {}, timeout: 120)
|
|
193
|
+
if res["video_url"]
|
|
194
|
+
@video_url = res["video_url"]
|
|
195
|
+
@logger.info("[SDK] Session Video: #{@video_url}")
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
if @session["record"]
|
|
200
|
+
@logger.info("[SDK] Finalizing session record (RRWeb)...")
|
|
201
|
+
res_r = _send("stop_record", {}, timeout: 60)
|
|
202
|
+
if res_r["record_url"]
|
|
203
|
+
@record_url = res_r["record_url"]
|
|
204
|
+
@logger.info("[SDK] Session Record URL: #{@record_url}")
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@logger.info("[SDK] Sending release command...")
|
|
209
|
+
res = _send("release_browser")
|
|
210
|
+
@session_data = res
|
|
211
|
+
return res
|
|
212
|
+
rescue StandardError => e
|
|
213
|
+
@logger.error("[SDK ERROR] Error inside release: #{e.message}")
|
|
214
|
+
return { "status" => "error", "error" => e.message }
|
|
215
|
+
ensure
|
|
216
|
+
@session = nil
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def _send(action, args = {}, timeout: 60)
|
|
221
|
+
raise BrowserError, "Cannot perform action '#{action}': Browser session not acquired." unless @session
|
|
222
|
+
|
|
223
|
+
task_id = SecureRandom.hex
|
|
224
|
+
result_key = "#{Isoautomate::REDIS_PREFIX}result:#{task_id}"
|
|
225
|
+
queue = "#{Isoautomate::REDIS_PREFIX}#{@session['worker']}:tasks"
|
|
226
|
+
|
|
227
|
+
payload = {
|
|
228
|
+
"task_id" => task_id,
|
|
229
|
+
"browser_id" => @session["browser_id"],
|
|
230
|
+
"worker_name" => @session["worker"],
|
|
231
|
+
"action" => action,
|
|
232
|
+
"args" => args,
|
|
233
|
+
"result_key" => result_key
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
unless @init_sent
|
|
237
|
+
payload["video"] = true if @session["video"]
|
|
238
|
+
payload["record"] = true if @session["record"]
|
|
239
|
+
if @session["profile_id"]
|
|
240
|
+
payload["profile_id"] = @session["profile_id"]
|
|
241
|
+
payload["browser_type"] = @session["browser_type"]
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
_r_rpush(queue, payload.to_json)
|
|
246
|
+
|
|
247
|
+
begin
|
|
248
|
+
# Blocking Pop (Instant RPC)
|
|
249
|
+
# Redis gem blpop returns [key, value] or nil
|
|
250
|
+
resp = @r.blpop(result_key, timeout: timeout)
|
|
251
|
+
if resp
|
|
252
|
+
@init_sent = true
|
|
253
|
+
return JSON.parse(resp[1])
|
|
254
|
+
else
|
|
255
|
+
return { "status" => "error", "error" => "Timeout waiting for worker" }
|
|
256
|
+
end
|
|
257
|
+
rescue StandardError => e
|
|
258
|
+
return { "status" => "error", "error" => "Redis RPC Error: #{e.message}" }
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# --- Assertions Handler ---
|
|
263
|
+
def _handle_assertion(action, args)
|
|
264
|
+
args[:screenshot] = true unless args.key?(:screenshot)
|
|
265
|
+
res = _send(action, args)
|
|
266
|
+
|
|
267
|
+
if res["status"] == "fail"
|
|
268
|
+
if res["screenshot_base64"]
|
|
269
|
+
begin
|
|
270
|
+
FileUtils.mkdir_p(Isoautomate::ASSERTION_FOLDER)
|
|
271
|
+
selector_clean = args.fetch(:selector, "unknown").gsub(/[#.\s]/, "_")[0..19]
|
|
272
|
+
timestamp = Time.now.strftime("%H%M%S")
|
|
273
|
+
filename = "FAIL_#{action}_#{selector_clean}_#{timestamp}.png"
|
|
274
|
+
path = File.join(Isoautomate::ASSERTION_FOLDER, filename)
|
|
275
|
+
|
|
276
|
+
File.open(path, "wb") do |f|
|
|
277
|
+
f.write(Base64.decode64(res["screenshot_base64"]))
|
|
278
|
+
end
|
|
279
|
+
@logger.warn("[Assertion Fail] Screenshot saved: #{path}")
|
|
280
|
+
rescue StandardError
|
|
281
|
+
# ignore save errors
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
error_msg = res.fetch("error", "Unknown assertion error")
|
|
285
|
+
raise error_msg # Raising RuntimeError like AssertionError
|
|
286
|
+
end
|
|
287
|
+
true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# --- Helper: Save Base64 File ---
|
|
291
|
+
def _save_base64_file(res, key_name, output_path)
|
|
292
|
+
if res["status"] == "ok" && res.key?(key_name)
|
|
293
|
+
begin
|
|
294
|
+
dirname = File.dirname(output_path)
|
|
295
|
+
FileUtils.mkdir_p(dirname) unless dirname == "."
|
|
296
|
+
File.open(output_path, "wb") do |f|
|
|
297
|
+
f.write(Base64.decode64(res[key_name]))
|
|
298
|
+
end
|
|
299
|
+
return { "status" => "ok", "path" => File.expand_path(output_path) }
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
return { "status" => "error", "error" => "Failed to save local file: #{e.message}" }
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
res
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# ==================================================
|
|
308
|
+
# 1. NAVIGATION & LIFECYCLE
|
|
309
|
+
# ==================================================
|
|
310
|
+
def open_url(url)
|
|
311
|
+
_send("open_url", { "url" => url })
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def reload(ignore_cache: true, script: nil)
|
|
315
|
+
_send("reload", { "ignore_cache" => ignore_cache, "script_to_evaluate_on_load" => script })
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def refresh
|
|
319
|
+
_send("refresh")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def go_back
|
|
323
|
+
_send("go_back")
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def go_forward
|
|
327
|
+
_send("go_forward")
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def internalize_links
|
|
331
|
+
_send("internalize_links")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def get_navigation_history
|
|
335
|
+
_send("get_navigation_history")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# ==================================================
|
|
339
|
+
# 2. MOUSE INTERACTION
|
|
340
|
+
# ==================================================
|
|
341
|
+
def click(selector, timeout: nil)
|
|
342
|
+
_send("click", { "selector" => selector, "timeout" => timeout })
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def click_if_visible(selector)
|
|
346
|
+
_send("click_if_visible", { "selector" => selector })
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def click_visible_elements(selector, limit: 0)
|
|
350
|
+
_send("click_visible_elements", { "selector" => selector, "limit" => limit })
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def click_nth_element(selector, number: 1)
|
|
354
|
+
_send("click_nth_element", { "selector" => selector, "number" => number })
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def click_nth_visible_element(selector, number: 1)
|
|
358
|
+
_send("click_nth_visible_element", { "selector" => selector, "number" => number })
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def click_link(text)
|
|
362
|
+
_send("click_link", { "text" => text })
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def click_active_element
|
|
366
|
+
_send("click_active_element")
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def mouse_click(selector)
|
|
370
|
+
_send("mouse_click", { "selector" => selector })
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def nested_click(parent_selector, selector)
|
|
374
|
+
_send("nested_click", { "parent_selector" => parent_selector, "selector" => selector })
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def click_with_offset(selector, x, y, center: false)
|
|
378
|
+
_send("click_with_offset", { "selector" => selector, "x" => x, "y" => y, "center" => center })
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# ==================================================
|
|
382
|
+
# 3. KEYBOARD & INPUT
|
|
383
|
+
# ==================================================
|
|
384
|
+
def type(selector, text, timeout: nil)
|
|
385
|
+
_send("type", { "selector" => selector, "text" => text, "timeout" => timeout })
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def press_keys(selector, text)
|
|
389
|
+
_send("press_keys", { "selector" => selector, "text" => text })
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def send_keys(selector, text)
|
|
393
|
+
_send("send_keys", { "selector" => selector, "text" => text })
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def set_value(selector, text)
|
|
397
|
+
_send("set_value", { "selector" => selector, "text" => text })
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def clear(selector)
|
|
401
|
+
_send("clear", { "selector" => selector })
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def clear_input(selector)
|
|
405
|
+
_send("clear_input", { "selector" => selector })
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def submit(selector)
|
|
409
|
+
_send("submit", { "selector" => selector })
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def focus(selector)
|
|
413
|
+
_send("focus", { "selector" => selector })
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# ==================================================
|
|
417
|
+
# 4. GUI ACTIONS (PyAutoGUI / Profiled)
|
|
418
|
+
# ==================================================
|
|
419
|
+
def gui_click_element(selector, timeframe: 0.25)
|
|
420
|
+
_send("gui_click_element", { "selector" => selector, "timeframe" => timeframe })
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def gui_click_x_y(x, y, timeframe: 0.25)
|
|
424
|
+
_send("gui_click_x_y", { "x" => x, "y" => y, "timeframe" => timeframe })
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def gui_click_captcha
|
|
428
|
+
_send("gui_click_captcha")
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def solve_captcha
|
|
432
|
+
_send("solve_captcha")
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def gui_drag_and_drop(drag_selector, drop_selector, timeframe: 0.35)
|
|
436
|
+
_send("gui_drag_and_drop", { "drag_selector" => drag_selector, "drop_selector" => drop_selector, "timeframe" => timeframe })
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def gui_hover_element(selector)
|
|
440
|
+
_send("gui_hover_element", { "selector" => selector })
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def gui_write(text)
|
|
444
|
+
_send("gui_write", { "text" => text })
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def gui_press_keys(keys_list)
|
|
448
|
+
_send("gui_press_keys", { "keys" => keys_list })
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# ==================================================
|
|
452
|
+
# 5. SELECTS & DROPDOWNS
|
|
453
|
+
# ==================================================
|
|
454
|
+
def select_option_by_text(selector, text)
|
|
455
|
+
_send("select_option_by_text", { "selector" => selector, "text" => text })
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def select_option_by_value(selector, value)
|
|
459
|
+
_send("select_option_by_value", { "selector" => selector, "value" => value })
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def select_option_by_index(selector, index)
|
|
463
|
+
_send("select_option_by_index", { "selector" => selector, "index" => index })
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# ==================================================
|
|
467
|
+
# 6. WINDOW & TAB MANAGEMENT
|
|
468
|
+
# ==================================================
|
|
469
|
+
def open_new_tab(url)
|
|
470
|
+
_send("open_new_tab", { "url" => url })
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def open_new_window(url)
|
|
474
|
+
_send("open_new_window", { "url" => url })
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def switch_to_tab(index: -1)
|
|
478
|
+
_send("switch_to_tab", { "index" => index })
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def switch_to_window(index: -1)
|
|
482
|
+
_send("switch_to_window", { "index" => index })
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def close_active_tab
|
|
486
|
+
_send("close_active_tab")
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def maximize
|
|
490
|
+
_send("maximize")
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def minimize
|
|
494
|
+
_send("minimize")
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def medimize
|
|
498
|
+
_send("medimize")
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def tile_windows
|
|
502
|
+
_send("tile_windows")
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
# ==================================================
|
|
506
|
+
# 7. DATA EXTRACTION (GETTERS)
|
|
507
|
+
# ==================================================
|
|
508
|
+
def get_text(selector: "body")
|
|
509
|
+
_send("get_text", { "selector" => selector })
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def get_title
|
|
513
|
+
_send("get_title")
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def get_current_url
|
|
517
|
+
_send("get_current_url")
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def get_page_source
|
|
521
|
+
_send("get_page_source")
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
def get_html(selector: nil)
|
|
525
|
+
_send("get_html", { "selector" => selector })
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def get_attribute(selector, attribute)
|
|
529
|
+
_send("get_attribute", { "selector" => selector, "attribute" => attribute })
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def get_element_attributes(selector)
|
|
533
|
+
_send("get_element_attributes", { "selector" => selector })
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def get_user_agent
|
|
537
|
+
_send("get_user_agent")
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def get_cookie_string
|
|
541
|
+
_send("get_cookie_string")
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def get_element_rect(selector)
|
|
545
|
+
_send("get_element_rect", { "selector" => selector })
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def get_window_rect
|
|
549
|
+
_send("get_window_rect")
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def get_screen_rect
|
|
553
|
+
_send("get_screen_rect")
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def is_element_visible(selector)
|
|
557
|
+
_send("is_element_visible", { "selector" => selector })
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def is_text_visible(text)
|
|
561
|
+
_send("is_text_visible", { "text" => text })
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def is_checked(selector)
|
|
565
|
+
_send("is_checked", { "selector" => selector })
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def is_selected(selector)
|
|
569
|
+
_send("is_selected", { "selector" => selector })
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def is_online
|
|
573
|
+
_send("is_online")
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# ==================================================
|
|
577
|
+
# 8. COOKIES & STORAGE
|
|
578
|
+
# ==================================================
|
|
579
|
+
def get_all_cookies
|
|
580
|
+
_send("get_all_cookies")
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def add_cookie(cookie_dict)
|
|
584
|
+
_send("add_cookie", { "cookie" => cookie_dict })
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def delete_cookie(name)
|
|
588
|
+
_send("delete_cookie", { "name" => name })
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def save_cookies(name: "cookies.txt")
|
|
592
|
+
res = _send("save_cookies")
|
|
593
|
+
if res["status"] == "ok" && res["cookies"]
|
|
594
|
+
begin
|
|
595
|
+
File.open(name, "w") { |f| f.write(JSON.pretty_generate(res["cookies"])) }
|
|
596
|
+
return { "status" => "ok", "path" => File.expand_path(name) }
|
|
597
|
+
rescue StandardError => e
|
|
598
|
+
return { "status" => "error", "error" => "Failed to write local file: #{e.message}" }
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
res
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def load_cookies(name: "cookies.txt", cookies_list: nil)
|
|
605
|
+
final_cookies = cookies_list
|
|
606
|
+
if final_cookies.nil? && name
|
|
607
|
+
begin
|
|
608
|
+
if File.exist?(name)
|
|
609
|
+
final_cookies = JSON.parse(File.read(name))
|
|
610
|
+
else
|
|
611
|
+
return { "status" => "error", "error" => "Local cookie file not found: #{name}" }
|
|
612
|
+
end
|
|
613
|
+
rescue StandardError => e
|
|
614
|
+
return { "status" => "error", "error" => "Failed to read local file: #{e.message}" }
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
_send("load_cookies", { "name" => name, "cookies" => final_cookies })
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def clear_cookies
|
|
621
|
+
_send("clear_cookies")
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def get_local_storage_item(key)
|
|
625
|
+
_send("get_local_storage_item", { "key" => key })
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def set_local_storage_item(key, value)
|
|
629
|
+
_send("set_local_storage_item", { "key" => key, "value" => value })
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def get_session_storage_item(key)
|
|
633
|
+
_send("get_session_storage_item", { "key" => key })
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def set_session_storage_item(key, value)
|
|
637
|
+
_send("set_session_storage_item", { "key" => key, "value" => value })
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
# ==================================================
|
|
641
|
+
# 9. VISUALS & HIGHLIGHTS
|
|
642
|
+
# ==================================================
|
|
643
|
+
def highlight(selector)
|
|
644
|
+
_send("highlight", { "selector" => selector })
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def highlight_overlay(selector)
|
|
648
|
+
_send("highlight_overlay", { "selector" => selector })
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
def remove_element(selector)
|
|
652
|
+
_send("remove_element", { "selector" => selector })
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def flash(selector, duration: 1)
|
|
656
|
+
_send("flash", { "selector" => selector, "duration" => duration })
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
# ==================================================
|
|
660
|
+
# 10. ADVANCED (MFA, Permissions, Scripting)
|
|
661
|
+
# ==================================================
|
|
662
|
+
def get_mfa_code(totp_key)
|
|
663
|
+
_send("get_mfa_code", { "totp_key" => totp_key })
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
def enter_mfa_code(selector, totp_key)
|
|
667
|
+
_send("enter_mfa_code", { "selector" => selector, "totp_key" => totp_key })
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
def grant_permissions(permissions)
|
|
671
|
+
_send("grant_permissions", { "permissions" => permissions })
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def execute_script(script)
|
|
675
|
+
_send("execute_script", { "script" => script })
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def evaluate(expression)
|
|
679
|
+
_send("evaluate", { "expression" => expression })
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# ==================================================
|
|
683
|
+
# 11. ASSERTIONS
|
|
684
|
+
# ==================================================
|
|
685
|
+
def assert_text(text, selector: "html", screenshot: true)
|
|
686
|
+
_handle_assertion("assert_text", { "text" => text, "selector" => selector, "screenshot" => screenshot })
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def assert_exact_text(text, selector: "html", screenshot: true)
|
|
690
|
+
_handle_assertion("assert_exact_text", { "text" => text, "selector" => selector, "screenshot" => screenshot })
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def assert_element(selector, screenshot: true)
|
|
694
|
+
_handle_assertion("assert_element", { "selector" => selector, "screenshot" => screenshot })
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def assert_element_present(selector, screenshot: true)
|
|
698
|
+
_handle_assertion("assert_element_present", { "selector" => selector, "screenshot" => screenshot })
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def assert_element_absent(selector, screenshot: true)
|
|
702
|
+
_handle_assertion("assert_element_absent", { "selector" => selector, "screenshot" => screenshot })
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def assert_element_not_visible(selector, screenshot: true)
|
|
706
|
+
_handle_assertion("assert_element_not_visible", { "selector" => selector, "screenshot" => screenshot })
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def assert_text_not_visible(text, selector: "html", screenshot: true)
|
|
710
|
+
_handle_assertion("assert_text_not_visible", { "text" => text, "selector" => selector, "screenshot" => screenshot })
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def assert_title(title, screenshot: true)
|
|
714
|
+
_handle_assertion("assert_title", { "title" => title, "screenshot" => screenshot })
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def assert_url(url_substring, screenshot: true)
|
|
718
|
+
_handle_assertion("assert_url", { "url" => url_substring, "screenshot" => screenshot })
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def assert_attribute(selector, attribute, value, screenshot: true)
|
|
722
|
+
_handle_assertion("assert_attribute", { "selector" => selector, "attribute" => attribute, "value" => value, "screenshot" => screenshot })
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# ==================================================
|
|
726
|
+
# 12. SCROLLING & WAITING
|
|
727
|
+
# ==================================================
|
|
728
|
+
def scroll_into_view(selector)
|
|
729
|
+
_send("scroll_into_view", { "selector" => selector })
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def scroll_to_bottom
|
|
733
|
+
_send("scroll_to_bottom")
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def scroll_to_top
|
|
737
|
+
_send("scroll_to_top")
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def scroll_down(amount: 25)
|
|
741
|
+
_send("scroll_down", { "amount" => amount })
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def scroll_up(amount: 25)
|
|
745
|
+
_send("scroll_up", { "amount" => amount })
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def scroll_to_y(y)
|
|
749
|
+
_send("scroll_to_y", { "y" => y })
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def sleep(seconds)
|
|
753
|
+
_send("sleep", { "seconds" => seconds })
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def wait_for_element(selector, timeout: nil)
|
|
757
|
+
_send("wait_for_element", { "selector" => selector, "timeout" => timeout })
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def wait_for_text(text, selector: "html", timeout: nil)
|
|
761
|
+
_send("wait_for_text", { "text" => text, "selector" => selector, "timeout" => timeout })
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def wait_for_element_present(selector, timeout: nil)
|
|
765
|
+
_send("wait_for_element_present", { "selector" => selector, "timeout" => timeout })
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def wait_for_element_absent(selector, timeout: nil)
|
|
769
|
+
_send("wait_for_element_absent", { "selector" => selector, "timeout" => timeout })
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def wait_for_element_not_visible(selector, timeout: nil)
|
|
773
|
+
_send("wait_for_element_not_visible", { "selector" => selector, "timeout" => timeout })
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# ==================================================
|
|
777
|
+
# 13. SCREENSHOTS & FILES
|
|
778
|
+
# ==================================================
|
|
779
|
+
def screenshot(filename: nil, selector: nil)
|
|
780
|
+
if filename.nil?
|
|
781
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
782
|
+
unique_id = SecureRandom.hex(2)
|
|
783
|
+
filename = File.join(Isoautomate::SCREENSHOT_FOLDER, "#{timestamp}_#{unique_id}.png")
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
res = _send("save_screenshot", { "name" => "temp.png", "selector" => selector })
|
|
787
|
+
_save_base64_file(res, "image_base64", filename)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def save_as_pdf(filename: nil)
|
|
791
|
+
filename = "doc_#{Time.now.to_i}.pdf" if filename.nil?
|
|
792
|
+
res = _send("save_as_pdf")
|
|
793
|
+
_save_base64_file(res, "pdf_base64", filename)
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def save_page_source(name: "source.html")
|
|
797
|
+
res = _send("save_page_source")
|
|
798
|
+
if res["status"] == "ok" && res["source_base64"]
|
|
799
|
+
begin
|
|
800
|
+
data = Base64.decode64(res["source_base64"])
|
|
801
|
+
File.open(name, "w:UTF-8") { |f| f.write(data) }
|
|
802
|
+
return { "status" => "ok", "path" => File.expand_path(name) }
|
|
803
|
+
rescue StandardError => e
|
|
804
|
+
return { "status" => "error", "error" => e.message }
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
res
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def upload_file(selector, local_file_path)
|
|
811
|
+
return { "status" => "error", "error" => "Local file not found: #{local_file_path}" } unless File.exist?(local_file_path)
|
|
812
|
+
|
|
813
|
+
file_data = Base64.strict_encode64(File.read(local_file_path))
|
|
814
|
+
filename = File.basename(local_file_path)
|
|
815
|
+
_send("upload_file", { "selector" => selector, "file_name" => filename, "file_data" => file_data })
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# ==================================================
|
|
819
|
+
# 14. NETWORK CONTROL
|
|
820
|
+
# ==================================================
|
|
821
|
+
def block_urls(patterns)
|
|
822
|
+
_send("block_urls", { "patterns" => patterns })
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def wait_for_network_idle
|
|
826
|
+
_send("wait_for_network_idle")
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def get_performance_metrics
|
|
830
|
+
_send("get_performance_metrics")
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# ==================================================
|
|
834
|
+
# 15. IFRAME SWITCHING
|
|
835
|
+
# ==================================================
|
|
836
|
+
def switch_to_frame(selector)
|
|
837
|
+
_send("switch_to_frame", { "selector" => selector })
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def switch_to_default_content
|
|
841
|
+
_send("switch_to_default_content")
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def switch_to_parent_frame
|
|
845
|
+
_send("switch_to_parent_frame")
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
# ==================================================
|
|
849
|
+
# 16. ALERTS & DIALOGS
|
|
850
|
+
# ==================================================
|
|
851
|
+
def accept_alert
|
|
852
|
+
_send("accept_alert")
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def dismiss_alert
|
|
856
|
+
_send("dismiss_alert")
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
def get_alert_text
|
|
860
|
+
_send("get_alert_text")
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
# ==================================================
|
|
864
|
+
# 17. ADVANCED MOUSE (DOM LEVEL)
|
|
865
|
+
# ==================================================
|
|
866
|
+
def double_click(selector)
|
|
867
|
+
_send("double_click", { "selector" => selector })
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def right_click(selector)
|
|
871
|
+
_send("right_click", { "selector" => selector })
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def hover(selector)
|
|
875
|
+
_send("hover", { "selector" => selector })
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def drag_and_drop(drag_selector, drop_selector)
|
|
879
|
+
_send("drag_and_drop", { "drag_selector" => drag_selector, "drop_selector" => drop_selector })
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# ==================================================
|
|
883
|
+
# 18. VIEWPORT SIZE
|
|
884
|
+
# ==================================================
|
|
885
|
+
def set_window_size(width, height)
|
|
886
|
+
_send("set_window_size", { "width" => width, "height" => height })
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def set_window_rect(x, y, width, height)
|
|
890
|
+
_send("set_window_rect", { "x" => x, "y" => y, "width" => width, "height" => height })
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# ==================================================
|
|
894
|
+
# 19. SESSION STATE (Import/Export)
|
|
895
|
+
# ==================================================
|
|
896
|
+
def export_session
|
|
897
|
+
_send("get_storage_state")
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def import_session(state_dict)
|
|
901
|
+
_send("set_storage_state", { "state" => state_dict })
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
# ==================================================
|
|
905
|
+
# 20. GOD MODE (Raw CDP Access)
|
|
906
|
+
# ==================================================
|
|
907
|
+
def execute_cdp_cmd(cmd, params = {})
|
|
908
|
+
_send("execute_cdp_cmd", { "cmd" => cmd, "params" => params })
|
|
909
|
+
end
|
|
910
|
+
end
|
|
911
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Isoautomate
|
|
2
|
+
# Redis Keys
|
|
3
|
+
REDIS_PREFIX = "ISOAUTOMATE:"
|
|
4
|
+
WORKERS_SET = "#{REDIS_PREFIX}workers"
|
|
5
|
+
|
|
6
|
+
# File System Paths
|
|
7
|
+
SCREENSHOT_FOLDER = "screenshots"
|
|
8
|
+
ASSERTION_FOLDER = File.join(SCREENSHOT_FOLDER, "failures")
|
|
9
|
+
|
|
10
|
+
# Defaults (Left nil to force explicit or ENV configuration)
|
|
11
|
+
DEFAULT_REDIS_HOST = nil
|
|
12
|
+
DEFAULT_REDIS_PORT = nil
|
|
13
|
+
DEFAULT_REDIS_PASSWORD = nil
|
|
14
|
+
DEFAULT_REDIS_DB = 0
|
|
15
|
+
end
|
data/lib/isoautomate.rb
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
require "dotenv/load"
|
|
2
|
+
require "isoautomate/version"
|
|
3
|
+
require "isoautomate/config"
|
|
4
|
+
require "isoautomate/exceptions"
|
|
5
|
+
require "isoautomate/utils"
|
|
6
|
+
# client is required last as it depends on the above
|
|
7
|
+
require "isoautomate/client"
|
|
8
|
+
|
|
9
|
+
module Isoautomate
|
|
10
|
+
# Module level logger could go here if needed
|
|
11
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: isoautomate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- isoAutomate Team
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: redis
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '4.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dotenv
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.7'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.7'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: bundler
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '10.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '10.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rspec
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '3.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.0'
|
|
82
|
+
description: Provides connectivity to the isoFleet engine via Redis for distributed
|
|
83
|
+
browser automation.
|
|
84
|
+
email:
|
|
85
|
+
- support@isoautomate.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- ext/sdk-ruby.png
|
|
93
|
+
- isoautomate.gemspec
|
|
94
|
+
- lib/isoautomate.rb
|
|
95
|
+
- lib/isoautomate/client.rb
|
|
96
|
+
- lib/isoautomate/config.rb
|
|
97
|
+
- lib/isoautomate/version.rb
|
|
98
|
+
homepage: https://github.com/isoautomate/isoautomate-ruby
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata: {}
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements: []
|
|
116
|
+
rubygems_version: 4.0.3
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: Official Ruby SDK for the isoAutomate Sovereign Browser Infrastructure.
|
|
119
|
+
test_files: []
|