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 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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Isoautomate
2
+ VERSION = "0.1.0"
3
+ end
@@ -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: []