Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Flatten mode #434

Merged
merged 2 commits into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `#wait` wait for file download to be completed
- `#set_behavior` where and whether to store file
- `Browser::Client#command` accepts :async parameter [#433]
- `Ferrum::Browser` introduce `:flatten` mode with one connection and sessions [#434]

### Changed
- `Ferrum::Page#screeshot` accepts :area option [#410]
Expand Down Expand Up @@ -533,9 +534,8 @@ to `Ferrum::Browser#default_context`
### Fixed

### Removed
- `Ferrum::EmptyTargetsError`
- the `hack` to handle `new window` which doesn't have events at all by `Ferrum::Page#session_id` with
`Target.attachToTarget` and `Target.detachFromTarget` usage
- `Ferrum::EmptyTargetsError` the hack to handle `new window` which doesn't have events at all by
`Ferrum::Page#session_id` with `Target.attachToTarget` and `Target.detachFromTarget` usage
- `Ferrum::Page#close_connection` - the logic is moved to `Ferrum::Page#close` directly
- the third argument (`new_window = false`) for `Ferrum::Page` initializer
- `Ferrum::Targets` class with the delegations to `Ferrum::Targets` instance in `Ferrum::Browser` instance:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ Ferrum::Browser.new(options)
* `:headless` (String | Boolean) - Set browser as headless or not, `true` by default. You can set `"new"` to support
[new headless mode](https://developer.chrome.com/articles/new-headless/).
* `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default.
* `:flatten` (Boolean) - Use one websocket connection to the browser and all the pages in flatten mode.
* `:window_size` (Array) - The dimensions of the browser window in which to
test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
* `:extensions` (Array[String | Hash]) - An array of paths to files or JS
Expand Down
3 changes: 3 additions & 0 deletions lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class Browser
# @option options [Boolean] :xvfb (false)
# Run browser in a virtual framebuffer.
#
# @option options [Boolean] :flatten (true)
# Use one websocket connection to the browser and all the pages in flatten mode.
#
# @option options [(Integer, Integer)] :window_size ([1024, 768])
# The dimensions of the browser window in which to test, expressed as a
# 2-element array, e.g. `[1024, 768]`.
Expand Down
3 changes: 2 additions & 1 deletion lib/ferrum/browser/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Options
:js_errors, :base_url, :slowmo, :pending_connection_errors,
:url, :env, :process_timeout, :browser_name, :browser_path,
:save_path, :proxy, :port, :host, :headless, :browser_options,
:ignore_default_browser_options, :xvfb
:ignore_default_browser_options, :xvfb, :flatten
attr_accessor :timeout, :ws_url, :default_user_agent

def initialize(options = nil)
Expand All @@ -27,6 +27,7 @@ def initialize(options = nil)
@window_size = @options.fetch(:window_size, WINDOW_SIZE)
@js_errors = @options.fetch(:js_errors, false)
@headless = @options.fetch(:headless, true)
@flatten = @options.fetch(:flatten, true)
@pending_connection_errors = @options.fetch(:pending_connection_errors, true)
@process_timeout = @options.fetch(:process_timeout, PROCESS_TIMEOUT)
@slowmo = @options[:slowmo].to_f
Expand Down
54 changes: 53 additions & 1 deletion lib/ferrum/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,59 @@
require "ferrum/client/web_socket"

module Ferrum
class SessionClient
attr_reader :client, :session_id

def self.event_name(event, session_id)
[event, session_id].compact.join("_")
end

def initialize(client, session_id)
@client = client
@session_id = session_id
end

def command(method, async: false, **params)
message = build_message(method, params)
@client.send_message(message, async: async)
end

def on(event, &block)
@client.on(event_name(event), &block)
end

def subscribed?(event)
@client.subscribed?(event_name(event))
end

def respond_to_missing?(name, include_private)
@client.respond_to?(name, include_private)
end

def method_missing(name, ...)
@client.send(name, ...)
end

def close
@client.subscriber.clear(session_id: session_id)
end

private

def build_message(method, params)
@client.build_message(method, params).merge(sessionId: session_id)
end

def event_name(event)
self.class.event_name(event, session_id)
end
end

class Client
extend Forwardable
delegate %i[timeout timeout=] => :options

attr_reader :options
attr_reader :options, :subscriber

def initialize(ws_url, options)
@command_id = 0
Expand Down Expand Up @@ -54,6 +102,10 @@ def subscribed?(event)
@subscriber.subscribed?(event)
end

def session(session_id)
SessionClient.new(self, session_id)
end

def close
@ws.close
# Give a thread some time to handle a tail of messages
Expand Down
12 changes: 9 additions & 3 deletions lib/ferrum/client/subscriber.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def close
@priority_thread&.kill
end

def clear(session_id:)
@on.delete_if { |k, _| k.match?(session_id) }
end

private

def start
Expand All @@ -58,9 +62,11 @@ def start
end

def call(message)
method, params = message.values_at("method", "params")
total = @on[method].size
@on[method].each_with_index do |block, index|
method, session_id, params = message.values_at("method", "sessionId", "params")
event = SessionClient.event_name(method, session_id)

total = @on[event].size
@on[event].each_with_index do |block, index|
# In case of multiple callbacks we provide current index and total
block.call(params, index, total)
end
Expand Down
6 changes: 3 additions & 3 deletions lib/ferrum/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def create_target
target
end

def add_target(params)
new_target = Target.new(@client, params)
def add_target(params:, session_id: nil)
new_target = Target.new(@client, session_id, params)
target = @targets.put_if_absent(new_target.id, new_target)
target ||= new_target # `put_if_absent` returns nil if added a new value or existing if there was one already
@pendings.put(target, @client.timeout) if @pendings.empty?
Expand All @@ -71,7 +71,7 @@ def delete_target(target_id)

def close_targets_connection
@targets.each_value do |target|
next unless target.attached?
next unless target.connected?

target.page.close_connection
end
Expand Down
20 changes: 19 additions & 1 deletion lib/ferrum/contexts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def initialize(client)
@contexts = Concurrent::Map.new
@client = client
subscribe
auto_attach
discover
end

Expand Down Expand Up @@ -67,12 +68,23 @@ def size
private

def subscribe
@client.on("Target.attachedToTarget") do |params|
info, session_id = params.values_at("targetInfo", "sessionId")
next unless info["type"] == "page"

context_id = info["browserContextId"]
@contexts[context_id]&.add_target(session_id: session_id, params: info)
if params["waitingForDebugger"]
@client.session(session_id).command("Runtime.runIfWaitingForDebugger", async: true)
end
end

@client.on("Target.targetCreated") do |params|
info = params["targetInfo"]
next unless info["type"] == "page"

context_id = info["browserContextId"]
@contexts[context_id]&.add_target(info)
@contexts[context_id]&.add_target(params: info)
end

@client.on("Target.targetInfoChanged") do |params|
Expand All @@ -97,5 +109,11 @@ def subscribe
def discover
@client.command("Target.setDiscoverTargets", discover: true)
end

def auto_attach
return unless @client.options.flatten

@client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
end
end
end
11 changes: 8 additions & 3 deletions lib/ferrum/target.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ class Target
# where we enhance page class and build page ourselves.
attr_writer :page

def initialize(client, params = nil)
attr_reader :session_id

def initialize(client, session_id = nil, params = nil)
@page = nil
@client = client
@session_id = session_id
@params = params
end

def update(params)
@params = params
@params.merge!(params)
end

def attached?
def connected?
!!@page
end

Expand Down Expand Up @@ -68,6 +71,8 @@ def maybe_sleep_if_new_window

def build_client
options = @client.options
return @client.session(session_id) if options.flatten

ws_url = options.ws_url.merge(path: "/devtools/page/#{id}").to_s
Client.new(ws_url, options)
end
Expand Down