unmagic-browser 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/CHANGELOG.md +51 -0
- data/LICENSE +21 -0
- data/README.md +372 -0
- data/lib/unmagic/browser/configuration.rb +55 -0
- data/lib/unmagic/browser/console/banner.rb +82 -0
- data/lib/unmagic/browser/console/graphics.rb +145 -0
- data/lib/unmagic/browser/console/helpers.rb +325 -0
- data/lib/unmagic/browser/console/prompt.rb +152 -0
- data/lib/unmagic/browser/console/renderer.rb +205 -0
- data/lib/unmagic/browser/console/terminal.rb +108 -0
- data/lib/unmagic/browser/console/values.rb +78 -0
- data/lib/unmagic/browser/console.rb +161 -0
- data/lib/unmagic/browser/driver/base.rb +146 -0
- data/lib/unmagic/browser/driver/cloudflare/transport.rb +67 -0
- data/lib/unmagic/browser/driver/cloudflare.rb +214 -0
- data/lib/unmagic/browser/driver/connection.rb +75 -0
- data/lib/unmagic/browser/driver/local.rb +87 -0
- data/lib/unmagic/browser/driver/remote.rb +45 -0
- data/lib/unmagic/browser/driver.rb +75 -0
- data/lib/unmagic/browser/element.rb +257 -0
- data/lib/unmagic/browser/emulation.rb +248 -0
- data/lib/unmagic/browser/error.rb +44 -0
- data/lib/unmagic/browser/failures.rb +76 -0
- data/lib/unmagic/browser/locator.rb +256 -0
- data/lib/unmagic/browser/markdown.rb +54 -0
- data/lib/unmagic/browser/session.rb +588 -0
- data/lib/unmagic/browser/version.rb +8 -0
- data/lib/unmagic/browser.rb +214 -0
- metadata +105 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The class is declared before its parts are required, because they reopen it.
|
|
4
|
+
# Its documentation lives at the bottom of this file, with the implementation.
|
|
5
|
+
module Unmagic
|
|
6
|
+
class Browser; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative "browser/version"
|
|
10
|
+
require_relative "browser/error"
|
|
11
|
+
require_relative "browser/failures"
|
|
12
|
+
require_relative "browser/configuration"
|
|
13
|
+
require_relative "browser/emulation"
|
|
14
|
+
require_relative "browser/markdown"
|
|
15
|
+
require_relative "browser/locator"
|
|
16
|
+
require_relative "browser/element"
|
|
17
|
+
require_relative "browser/session"
|
|
18
|
+
require_relative "browser/driver"
|
|
19
|
+
require_relative "browser/driver/connection"
|
|
20
|
+
require_relative "browser/driver/base"
|
|
21
|
+
require_relative "browser/driver/local"
|
|
22
|
+
require_relative "browser/driver/remote"
|
|
23
|
+
require_relative "browser/driver/cloudflare"
|
|
24
|
+
|
|
25
|
+
module Unmagic
|
|
26
|
+
# A real browser you can drive from Ruby — for one-off page grabs *and*
|
|
27
|
+
# multi-step automation.
|
|
28
|
+
#
|
|
29
|
+
# The same API runs against local Chrome, any remote Puppeteer endpoint, and
|
|
30
|
+
# Cloudflare Browser Rendering. Which one is a construction-time decision;
|
|
31
|
+
# nothing else in your code changes.
|
|
32
|
+
#
|
|
33
|
+
# There are two ways in. Call {#markdown}, {#html}, {#screenshot} or {#pdf}
|
|
34
|
+
# straight on the browser for "just get me this page", and each takes the
|
|
35
|
+
# cheapest path its driver offers. Call {#open} for anything with more than
|
|
36
|
+
# one step, and you get a {Session} — a live page with Capybara-flavoured
|
|
37
|
+
# verbs that auto-wait.
|
|
38
|
+
#
|
|
39
|
+
# @example One-off
|
|
40
|
+
# Unmagic::Browser.new.markdown("https://example.com")
|
|
41
|
+
#
|
|
42
|
+
# @example Multi-step
|
|
43
|
+
# Unmagic::Browser.open("https://news.ycombinator.com/login") do |session|
|
|
44
|
+
# session.fill_in "acct", with: "pg"
|
|
45
|
+
# session.click_button "login"
|
|
46
|
+
# session.wait_for text: "logout"
|
|
47
|
+
# end
|
|
48
|
+
class Browser
|
|
49
|
+
# Environment names that mean "someone is sitting at this machine", where a
|
|
50
|
+
# local Chrome you can watch beats a remote one you can't.
|
|
51
|
+
DEVELOPMENT = ["development", "dev", "test"].freeze
|
|
52
|
+
|
|
53
|
+
class << self
|
|
54
|
+
# Set the gem's global defaults.
|
|
55
|
+
#
|
|
56
|
+
# @yieldparam config [Configuration]
|
|
57
|
+
# @return [Configuration]
|
|
58
|
+
#
|
|
59
|
+
# @example
|
|
60
|
+
# Unmagic::Browser.configure do |config|
|
|
61
|
+
# config.default_driver = :cloudflare
|
|
62
|
+
# end
|
|
63
|
+
def configure
|
|
64
|
+
yield(configuration) if block_given?
|
|
65
|
+
configuration
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Configuration] the current global defaults
|
|
69
|
+
def configuration
|
|
70
|
+
@configuration ||= Configuration.new
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Throw the configuration away. Mostly for tests.
|
|
74
|
+
#
|
|
75
|
+
# @return [void]
|
|
76
|
+
def reset!
|
|
77
|
+
@configuration = Configuration.new
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Shorthand for `Unmagic::Browser.new.open(url)`.
|
|
81
|
+
#
|
|
82
|
+
# @param url [String] the page to open
|
|
83
|
+
# @param options [Hash] see {Browser#open}
|
|
84
|
+
# @yieldparam session [Session]
|
|
85
|
+
# @return [Session, Object] the session, or the block's value
|
|
86
|
+
def open(url, **options, &block)
|
|
87
|
+
new.open(url, **options, &block)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether this process looks like somebody's machine rather than a
|
|
91
|
+
# server. Checked only when no driver was asked for.
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def development?
|
|
95
|
+
return Rails.env.development? || Rails.env.test? if defined?(Rails) && Rails.respond_to?(:env) && Rails.env
|
|
96
|
+
|
|
97
|
+
environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || ENV["APP_ENV"]
|
|
98
|
+
environment.nil? || DEVELOPMENT.include?(environment)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The driver doing the real work. The mirror of the `driver:` option: you
|
|
103
|
+
# pick one when you construct the browser, and this hands the instance back
|
|
104
|
+
# for anything the curated API doesn't wrap.
|
|
105
|
+
#
|
|
106
|
+
# @return [Driver::Base]
|
|
107
|
+
attr_reader :driver
|
|
108
|
+
|
|
109
|
+
# @param driver [Symbol, Driver::Base, nil] `:local`, `:remote`, `:cloudflare`, or an
|
|
110
|
+
# instance. Defaults to `:local` in development and to
|
|
111
|
+
# {Configuration#default_driver} everywhere else.
|
|
112
|
+
# @param timeout [Numeric, nil] seconds a locator waits, defaulting to {Configuration#timeout}
|
|
113
|
+
# @param navigation_timeout [Numeric, nil] seconds a navigation is given,
|
|
114
|
+
# defaulting to {Configuration#navigation_timeout}
|
|
115
|
+
def initialize(driver: nil, timeout: nil, navigation_timeout: nil)
|
|
116
|
+
@driver = Driver.build(driver || default_driver)
|
|
117
|
+
@timeout = timeout
|
|
118
|
+
@navigation_timeout = navigation_timeout
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @param url [String] the page to render
|
|
122
|
+
# @return [String] the page as Markdown
|
|
123
|
+
def markdown(url)
|
|
124
|
+
@driver.markdown(url)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @param url [String] the page to render
|
|
128
|
+
# @return [String] the page's HTML, after JavaScript has run
|
|
129
|
+
def html(url)
|
|
130
|
+
@driver.html(url)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @param url [String] the page to render
|
|
134
|
+
# @param full_page [Boolean] capture the whole scrollable page
|
|
135
|
+
# @param selector [String, nil] capture just the first element matching this CSS
|
|
136
|
+
# @return [String] PNG bytes
|
|
137
|
+
def screenshot(url, full_page: false, selector: nil)
|
|
138
|
+
@driver.screenshot(url, full_page: full_page, selector: selector)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @param url [String] the page to render
|
|
142
|
+
# @return [String] PDF bytes
|
|
143
|
+
def pdf(url)
|
|
144
|
+
@driver.pdf(url)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Open a page you can drive step by step.
|
|
148
|
+
#
|
|
149
|
+
# Works like `File.open`: with a block you get a session already on that
|
|
150
|
+
# page, cleaned up when the block ends. Without one, the session is yours to
|
|
151
|
+
# keep — and yours to {Session#close}, because it holds a real browser (and
|
|
152
|
+
# on Cloudflare, one of a limited number of concurrent slots) until you do.
|
|
153
|
+
#
|
|
154
|
+
# @param url [String, nil] the page to start on, or nil for a blank one
|
|
155
|
+
# @param emulate [Emulation, Hash, nil] how the page should render
|
|
156
|
+
# @param timeout [Numeric, nil] seconds a locator waits in this session
|
|
157
|
+
# @param navigation_timeout [Numeric, nil] seconds a navigation is given in this session
|
|
158
|
+
# @yieldparam session [Session]
|
|
159
|
+
# @return [Session, Object] the session, or the block's value
|
|
160
|
+
def open(url = nil, emulate: nil, timeout: nil, navigation_timeout: nil)
|
|
161
|
+
session = start_session(emulate: emulate, timeout: timeout, navigation_timeout: navigation_timeout)
|
|
162
|
+
begin
|
|
163
|
+
session.visit(url) if url
|
|
164
|
+
rescue StandardError
|
|
165
|
+
session.close
|
|
166
|
+
raise
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return session unless block_given?
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
yield(session)
|
|
173
|
+
ensure
|
|
174
|
+
session.close
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Release the shared browser one-off grabs run on. Sessions are unaffected —
|
|
179
|
+
# each holds its own.
|
|
180
|
+
#
|
|
181
|
+
# @return [void]
|
|
182
|
+
def close
|
|
183
|
+
@driver.close
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @return [String] the browser and the driver behind it
|
|
187
|
+
def inspect
|
|
188
|
+
"#<#{self.class.name} driver=#{@driver.name}>"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def start_session(emulate:, timeout:, navigation_timeout:)
|
|
194
|
+
connection = @driver.open_connection
|
|
195
|
+
begin
|
|
196
|
+
Session.new(
|
|
197
|
+
browser: self,
|
|
198
|
+
connection: connection,
|
|
199
|
+
page: connection.new_page,
|
|
200
|
+
emulation: emulate,
|
|
201
|
+
timeout: timeout || @timeout,
|
|
202
|
+
navigation_timeout: navigation_timeout || @navigation_timeout,
|
|
203
|
+
)
|
|
204
|
+
rescue StandardError
|
|
205
|
+
connection.close
|
|
206
|
+
raise
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def default_driver
|
|
211
|
+
self.class.development? ? :local : self.class.configuration.default_driver
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: unmagic-browser
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Keith Pitt
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: puppeteer-ruby
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.53'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.53'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: reverse_markdown
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
description: Drive a real browser from Ruby for one-off page grabs and multi-step
|
|
42
|
+
automation. Runs against local Chrome, any remote Puppeteer endpoint, or Cloudflare
|
|
43
|
+
Browser Rendering — behind one Capybara-flavoured API with smart locators, auto-waiting,
|
|
44
|
+
and sessions that outlive a single call.
|
|
45
|
+
email:
|
|
46
|
+
- keith@unreasonable-magic.com
|
|
47
|
+
executables: []
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- lib/unmagic/browser.rb
|
|
55
|
+
- lib/unmagic/browser/configuration.rb
|
|
56
|
+
- lib/unmagic/browser/console.rb
|
|
57
|
+
- lib/unmagic/browser/console/banner.rb
|
|
58
|
+
- lib/unmagic/browser/console/graphics.rb
|
|
59
|
+
- lib/unmagic/browser/console/helpers.rb
|
|
60
|
+
- lib/unmagic/browser/console/prompt.rb
|
|
61
|
+
- lib/unmagic/browser/console/renderer.rb
|
|
62
|
+
- lib/unmagic/browser/console/terminal.rb
|
|
63
|
+
- lib/unmagic/browser/console/values.rb
|
|
64
|
+
- lib/unmagic/browser/driver.rb
|
|
65
|
+
- lib/unmagic/browser/driver/base.rb
|
|
66
|
+
- lib/unmagic/browser/driver/cloudflare.rb
|
|
67
|
+
- lib/unmagic/browser/driver/cloudflare/transport.rb
|
|
68
|
+
- lib/unmagic/browser/driver/connection.rb
|
|
69
|
+
- lib/unmagic/browser/driver/local.rb
|
|
70
|
+
- lib/unmagic/browser/driver/remote.rb
|
|
71
|
+
- lib/unmagic/browser/element.rb
|
|
72
|
+
- lib/unmagic/browser/emulation.rb
|
|
73
|
+
- lib/unmagic/browser/error.rb
|
|
74
|
+
- lib/unmagic/browser/failures.rb
|
|
75
|
+
- lib/unmagic/browser/locator.rb
|
|
76
|
+
- lib/unmagic/browser/markdown.rb
|
|
77
|
+
- lib/unmagic/browser/session.rb
|
|
78
|
+
- lib/unmagic/browser/version.rb
|
|
79
|
+
homepage: https://github.com/unreasonable-magic/unmagic-browser
|
|
80
|
+
licenses:
|
|
81
|
+
- MIT
|
|
82
|
+
metadata:
|
|
83
|
+
homepage_uri: https://github.com/unreasonable-magic/unmagic-browser
|
|
84
|
+
source_code_uri: https://github.com/unreasonable-magic/unmagic-browser
|
|
85
|
+
changelog_uri: https://github.com/unreasonable-magic/unmagic-browser/blob/main/CHANGELOG.md
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: 3.3.7
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.5.22
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: A real browser you can drive from Ruby, locally or on Cloudflare
|
|
105
|
+
test_files: []
|