lib/puppeteer/browser.rb in puppeteer-ruby-0.42.0 vs lib/puppeteer/browser.rb in puppeteer-ruby-0.43.0

- old
+ new

@@ -5,73 +5,85 @@ include Puppeteer::DebugPrint include Puppeteer::EventCallbackable include Puppeteer::IfPresent using Puppeteer::DefineAsyncMethod + # @param product [String|nil] 'chrome' or 'firefox' # @param {!Puppeteer.Connection} connection # @param {!Array<string>} contextIds # @param {boolean} ignoreHTTPSErrors # @param {?Puppeteer.Viewport} defaultViewport # @param process [Puppeteer::BrowserRunner::BrowserProcess|NilClass] # @param {function()=} closeCallback - def self.create(connection:, + def self.create(product:, + connection:, context_ids:, ignore_https_errors:, default_viewport:, process:, close_callback:, target_filter_callback:, is_page_target_callback:) browser = Puppeteer::Browser.new( + product: product, connection: connection, context_ids: context_ids, ignore_https_errors: ignore_https_errors, default_viewport: default_viewport, process: process, close_callback: close_callback, target_filter_callback: target_filter_callback, is_page_target_callback: is_page_target_callback, ) - connection.send_message('Target.setDiscoverTargets', discover: true) + browser.send(:attach) browser end + # @param product [String|nil] 'chrome' or 'firefox' # @param {!Puppeteer.Connection} connection # @param {!Array<string>} contextIds # @param {boolean} ignoreHTTPSErrors # @param {?Puppeteer.Viewport} defaultViewport # @param {?Puppeteer.ChildProcess} process # @param {(function():Promise)=} closeCallback - def initialize(connection:, + def initialize(product:, + connection:, context_ids:, ignore_https_errors:, default_viewport:, process:, close_callback:, target_filter_callback:, is_page_target_callback:) + @product = product || 'chrome' @ignore_https_errors = ignore_https_errors @default_viewport = default_viewport @process = process @connection = connection @close_callback = close_callback @target_filter_callback = target_filter_callback || method(:default_target_filter_callback) @is_page_target_callback = is_page_target_callback || method(:default_is_page_target_callback) - @default_context = Puppeteer::BrowserContext.new(@connection, self, nil) @contexts = {} + context_ids.each do |context_id| @contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self, context_id) end - @targets = {} - @wait_for_creating_targets = {} - @connection.on_event(ConnectionEmittedEvents::Disconnected) do - emit_event(BrowserEmittedEvents::Disconnected) + + if @product == 'firefox' + @target_manager = Puppeteer::FirefoxTargetManager.new( + connection: connection, + target_factory: method(:create_target), + target_filter_callback: @target_filter_callback, + ) + else + @target_manager = Puppeteer::ChromeTargetManager.new( + connection: connection, + target_factory: method(:create_target), + target_filter_callback: @target_filter_callback, + ) end - @connection.on_event('Target.targetCreated', &method(:handle_target_created)) - @connection.on_event('Target.targetDestroyed', &method(:handle_target_destroyed)) - @connection.on_event('Target.targetInfoChanged', &method(:handle_target_info_changed)) end private def default_target_filter_callback(target_info) true end @@ -98,15 +110,49 @@ end super(event_name.to_s, &block) end + private def attach + @connection_event_listeners ||= [] + @connection_event_listeners << @connection.add_event_listener(ConnectionEmittedEvents::Disconnected) do + emit_event(BrowserEmittedEvents::Disconnected) + end + @target_manager_event_listeners ||= [] + @target_manager.add_event_listener( + TargetManagerEmittedEvents::TargetAvailable, + &method(:handle_attached_to_target) + ) + @target_manager.add_event_listener( + TargetManagerEmittedEvents::TargetGone, + &method(:handle_detached_from_target) + ) + @target_manager.add_event_listener( + TargetManagerEmittedEvents::TargetChanged, + &method(:handle_target_changed) + ) + @target_manager.add_event_listener( + TargetManagerEmittedEvents::TargetDiscovered, + &method(:handle_target_discovered) + ) + @target_manager.init + end + + private def detach + @connection.remove_event_listener(*@connection_event_listeners) + @target_manager.remove_event_listener(*@target_manager_event_listeners) + end + # @return [Puppeteer::BrowserRunner::BrowserProcess] def process @process end + private def target_manager + @target_manager + end + # @return [Puppeteer::BrowserContext] def create_incognito_browser_context result = @connection.send_message('Target.createBrowserContext') browser_context_id = result['browserContextId'] @contexts[browser_context_id] = Puppeteer::BrowserContext.new(@connection, self, browser_context_id) @@ -121,137 +167,124 @@ @default_context end # @param context_id [String] def dispose_context(context_id) + return unless context_id @connection.send_message('Target.disposeBrowserContext', browserContextId: context_id) @contexts.delete(context_id) end - class TargetAlreadyExistError < StandardError - def initialize - super('Target should not exist before targetCreated') - end - end + class MissingBrowserContextError < StandardError ; end - # @param {!Protocol.Target.targetCreatedPayload} event - def handle_target_created(event) - target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo']) + # @param target_info [Puppeteer::Target::TargetInfo] + # @param session [CDPSession|nil] + def create_target(target_info, session) browser_context_id = target_info.browser_context_id context = if browser_context_id && @contexts.has_key?(browser_context_id) @contexts[browser_context_id] else @default_context end - if @targets[target_info.target_id] - raise TargetAlreadyExistError.new + unless context + raise MissingBrowserContextError.new('Missing browser context') end - return unless @target_filter_callback.call(target_info) - - target = Puppeteer::Target.new( + Puppeteer::Target.new( target_info: target_info, + session: session, browser_context: context, + target_manager: @target_manager, session_factory: -> { @connection.create_session(target_info) }, ignore_https_errors: @ignore_https_errors, default_viewport: @default_viewport, is_page_target_callback: @is_page_target_callback, ) - @targets[target_info.target_id] = target - if_present(@wait_for_creating_targets.delete(target_info.target_id)) do |promise| - promise.fulfill(target) - end - if await target.initialized_promise + end + + private def handle_attached_to_target(target) + if target.initialized_promise.value! emit_event(BrowserEmittedEvents::TargetCreated, target) - context.emit_event(BrowserContextEmittedEvents::TargetCreated, target) + target.browser_context.emit_event(BrowserContextEmittedEvents::TargetCreated, target) end end - # @param {{targetId: string}} event - def handle_target_destroyed(event) - target_id = event['targetId'] - target = @targets[target_id] + private def handle_detached_from_target(target) target.ignore_initialize_callback_promise - @targets.delete(target_id) - if_present(@wait_for_creating_targets.delete(target_id)) do |promise| - promise.reject('target destroyed') - end target.closed_callback - if await target.initialized_promise + if target.initialized_promise.value! emit_event(BrowserEmittedEvents::TargetDestroyed, target) target.browser_context.emit_event(BrowserContextEmittedEvents::TargetDestroyed, target) end end - class TargetNotExistError < StandardError - def initialize - super('target should exist before targetInfoChanged') - end - end - - # @param {!Protocol.Target.targetInfoChangedPayload} event - def handle_target_info_changed(event) - target_info = Puppeteer::Target::TargetInfo.new(event['targetInfo']) - target = @targets[target_info.target_id] or raise TargetNotExistError.new + private def handle_target_changed(target, target_info) previous_url = target.url was_initialized = target.initialized? target.handle_target_info_changed(target_info) if was_initialized && previous_url != target.url emit_event(BrowserEmittedEvents::TargetChanged, target) target.browser_context.emit_event(BrowserContextEmittedEvents::TargetChanged, target) end end + private def handle_target_discovered(target_info) + emit_event('targetdiscovered', target_info) + end + # @return [String] def ws_endpoint @connection.url end def new_page @default_context.new_page end + class MissingTargetError < StandardError ; end + class CreatePageError < StandardError ; end + # @param {?string} contextId # @return {!Promise<!Puppeteer.Page>} def create_page_in_context(context_id) - create_target_params = { url: 'about:blank' } - if context_id - create_target_params[:browserContextId] = context_id - end + create_target_params = { + url: 'about:blank', + browserContextId: context_id, + }.compact result = @connection.send_message('Target.createTarget', **create_target_params) target_id = result['targetId'] - target = @targets[target_id] + target = @target_manager.available_targets[target_id] unless target - # Target.targetCreated is often notified before the response of Target.createdTarget. - # https://github.com/YusukeIwaki/puppeteer-ruby/issues/91 - # D, [2021-04-07T03:00:10.125241 #187] DEBUG -- : SEND >> {"method":"Target.createTarget","params":{"url":"about:blank","browserContextId":"56A86FC3391B50180CF9A6450A0D8C21"},"id":3} - # D, [2021-04-07T03:00:10.142396 #187] DEBUG -- : RECV << {"id"=>3, "result"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632"}} - # D, [2021-04-07T03:00:10.145360 #187] DEBUG -- : RECV << {"method"=>"Target.targetCreated", "params"=>{"targetInfo"=>{"targetId"=>"A518447C415A1A3E1A8979454A155632", "type"=>"page", "title"=>"", "url"=>"", "attached"=>false, "canAccessOpener"=>false, "browserContextId"=>"56A86FC3391B50180CF9A6450A0D8C21"}}} - # This is just a workaround logic... - @wait_for_creating_targets[target_id] = resolvable_future - target = await @wait_for_creating_targets[target_id] + raise MissingTargetError.new("Missing target for page (id = #{target_id})") end - await target.initialized_promise - await target.page + unless target.initialized_promise.value! + raise CreatePageError.new("Failed to create target for page (id = #{target_id})") + end + page = target.page + unless page + raise CreatePageError.new("Failed to create a page for context (id = #{context_id})") + end + page end - # @return {!Array<!Target>} + # All active targets inside the Browser. In case of multiple browser contexts, returns + # an array with all the targets in all browser contexts. def targets - @targets.values.select { |target| target.initialized? } + @target_manager.available_targets.values.select { |target| target.initialized? } end - # @return {!Target} + # The target associated with the browser. def target - targets.find { |target| target.type == 'browser' } + targets.find { |target| target.type == 'browser' } or raise 'Browser target is not found' end # used only in Target#opener private def find_target_by_id(target_id) - @targets[target_id] + @target_manager.available_targets[target_id] end # @param predicate [Proc(Puppeteer::Target -> Boolean)] # @return [Puppeteer::Target] def wait_for_target(predicate:, timeout: nil) @@ -291,42 +324,43 @@ browser_contexts.flat_map(&:pages) end # @return [String] def version - get_version.product + Version.fetch(@connection).product end # @return [String] def user_agent - get_version.user_agent + Version.fetch(@connection).user_agent end def close @close_callback.call disconnect end def disconnect + @target_manager.dispose @connection.dispose end def connected? !@connection.closed? end class Version + def self.fetch(connection) + new(connection.send_message('Browser.getVersion')) + end + def initialize(hash) @protocol_version = hash['protocolVersion'] @product = hash['product'] @revision = hash['revision'] @user_agent = hash['userAgent'] @js_version = hash['jsVersion'] end attr_reader :protocol_version, :product, :revision, :user_agent, :js_version - end - - private def get_version - Version.new(@connection.send_message('Browser.getVersion')) end end