lib/puppeteer/page.rb in puppeteer-ruby-0.35.1 vs lib/puppeteer/page.rb in puppeteer-ruby-0.36.0

- old
+ new

@@ -1,14 +1,16 @@ require 'base64' require 'json' require "stringio" +require_relative './page/metrics' require_relative './page/pdf_options' require_relative './page/screenshot_options' require_relative './page/screenshot_task_queue' class Puppeteer::Page + include Puppeteer::DebugPrint include Puppeteer::EventCallbackable include Puppeteer::IfPresent using Puppeteer::DefineAsyncMethod # @param {!Puppeteer.CDPSession} client @@ -44,10 +46,12 @@ @coverage = Puppeteer::Coverage.new(client) @javascript_enabled = true @screenshot_task_queue = ScreenshotTaskQueue.new @workers = {} + @user_drag_interception_enabled = false + @client.on_event('Target.attachedToTarget') do |event| if event['targetInfo']['type'] != 'worker' # If we don't detach from service workers, they will never die. @client.async_send_message('Target.detachFromTarget', sessionId: event['sessionId']) next @@ -100,21 +104,25 @@ emit_event(PageEmittedEvents::Load) end @client.on('Runtime.consoleAPICalled') do |event| handle_console_api(event) end - # client.on('Runtime.bindingCalled', event => this._onBindingCalled(event)); + @client.on('Runtime.bindingCalled') do |event| + handle_binding_called(event) + end @client.on_event('Page.javascriptDialogOpening') do |event| handle_dialog_opening(event) end @client.on_event('Runtime.exceptionThrown') do |exception| handle_exception(exception['exceptionDetails']) end @client.on_event('Inspector.targetCrashed') do |event| handle_target_crashed end - # client.on('Performance.metrics', event => this._emitMetrics(event)); + @client.on_event('Performance.metrics') do |event| + emit_event(PageEmittedEvents::Metrics, MetricsEvent.new(event)) + end @client.on_event('Log.entryAdded') do |event| handle_log_entry_added(event) end @client.on_event('Page.fileChooserOpened') do |event| handle_file_chooser(event) @@ -132,10 +140,15 @@ @client.async_send_message('Performance.enable'), @client.async_send_message('Log.enable'), ) end + def drag_interception_enabled? + @user_drag_interception_enabled + end + alias_method :drag_interception_enabled, :drag_interception_enabled? + # @param event_name [Symbol] def on(event_name, &block) unless PageEmittedEvents.values.include?(event_name.to_s) raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{PageEmittedEvents.values.to_a.join(", ")}") end @@ -264,14 +277,24 @@ # @param value [Bool] def request_interception=(value) @frame_manager.network_manager.request_interception = value end + def drag_interception_enabled=(enabled) + @user_drag_interception_enabled = enabled + @client.send_message('Input.setInterceptDrags', enabled: enabled) + end + def offline_mode=(enabled) @frame_manager.network_manager.offline_mode = enabled end + # @param network_condition [Puppeteer::NetworkCondition|nil] + def emulate_network_conditions(network_condition) + @frame_manager.network_manager.emulate_network_conditions(network_condition) + end + # @param {number} timeout def default_navigation_timeout=(timeout) @timeout_settings.default_navigation_timeout = timeout end @@ -390,10 +413,55 @@ # @param content [String?] def add_style_tag(url: nil, path: nil, content: nil) main_frame.add_style_tag(url: url, path: path, content: content) end + # @param name [String] + # @param puppeteer_function [Proc] + def expose_function(name, puppeteer_function) + if @page_bindings[name] + raise ArgumentError.new("Failed to add page binding with name `#{name}` already exists!") + end + @page_bindings[name] = puppeteer_function + + add_page_binding = <<~JAVASCRIPT + function (type, bindingName) { + /* Cast window to any here as we're about to add properties to it + * via win[bindingName] which TypeScript doesn't like. + */ + const win = window; + const binding = win[bindingName]; + + win[bindingName] = (...args) => { + const me = window[bindingName]; + let callbacks = me.callbacks; + if (!callbacks) { + callbacks = new Map(); + me.callbacks = callbacks; + } + const seq = (me.lastSeq || 0) + 1; + me.lastSeq = seq; + const promise = new Promise((resolve, reject) => + callbacks.set(seq, { resolve, reject }) + ); + binding(JSON.stringify({ type, name: bindingName, seq, args })); + return promise; + }; + } + JAVASCRIPT + + source = JavaScriptFunction.new(add_page_binding, ['exposedFun', name]).source + @client.send_message('Runtime.addBinding', name: name) + @client.send_message('Page.addScriptToEvaluateOnNewDocument', source: source) + + promises = @frame_manager.frames.map do |frame| + frame.async_evaluate("() => #{source}") + end + await_all(*promises) + + nil + end # /** # * @param {string} name # * @param {Function} puppeteerFunction # */ # async exposeFunction(name, puppeteerFunction) { @@ -438,41 +506,15 @@ # @param user_agent [String] def user_agent=(user_agent) @frame_manager.network_manager.user_agent = user_agent end - # /** - # * @return {!Promise<!Metrics>} - # */ - # async metrics() { - # const response = await this._client.send('Performance.getMetrics'); - # return this._buildMetricsObject(response.metrics); - # } + def metrics + response = @client.send_message('Performance.getMetrics') + Metrics.new(response['metrics']) + end - # /** - # * @param {!Protocol.Performance.metricsPayload} event - # */ - # _emitMetrics(event) { - # this.emit(PageEmittedEvents::Metrics, { - # title: event.title, - # metrics: this._buildMetricsObject(event.metrics) - # }); - # } - - # /** - # * @param {?Array<!Protocol.Performance.Metric>} metrics - # * @return {!Metrics} - # */ - # _buildMetricsObject(metrics) { - # const result = {}; - # for (const metric of metrics || []) { - # if (supportedMetrics.has(metric.name)) - # result[metric.name] = metric.value; - # } - # return result; - # } - class PageError < StandardError ; end private def handle_exception(exception_details) message = Puppeteer::ExceptionDetails.new(exception_details).message err = PageError.new(message) @@ -504,61 +546,56 @@ Puppeteer::JSHandle.create(context: context, remote_object: remote_object) end add_console_message(event['type'], values, event['stackTrace']) end - # /** - # * @param {!Protocol.Runtime.bindingCalledPayload} event - # */ - # async _onBindingCalled(event) { - # const {name, seq, args} = JSON.parse(event.payload); - # let expression = null; - # try { - # const result = await this._pageBindings.get(name)(...args); - # expression = helper.evaluationString(deliverResult, name, seq, result); - # } catch (error) { - # if (error instanceof Error) - # expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); - # else - # expression = helper.evaluationString(deliverErrorValue, name, seq, error); - # } - # this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError); + def handle_binding_called(event) + execution_context_id = event['executionContextId'] + payload = + begin + JSON.parse(event['payload']) + rescue + # The binding was either called by something in the page or it was + # called before our wrapper was initialized. + return + end + name = payload['name'] + seq = payload['seq'] + args = payload['args'] - # /** - # * @param {string} name - # * @param {number} seq - # * @param {*} result - # */ - # function deliverResult(name, seq, result) { - # window[name]['callbacks'].get(seq).resolve(result); - # window[name]['callbacks'].delete(seq); - # } + if payload['type'] != 'exposedFun' || !@page_bindings[name] + return + end - # /** - # * @param {string} name - # * @param {number} seq - # * @param {string} message - # * @param {string} stack - # */ - # function deliverError(name, seq, message, stack) { - # const error = new Error(message); - # error.stack = stack; - # window[name]['callbacks'].get(seq).reject(error); - # window[name]['callbacks'].delete(seq); - # } + expression = + begin + result = @page_bindings[name].call(*args) - # /** - # * @param {string} name - # * @param {number} seq - # * @param {*} value - # */ - # function deliverErrorValue(name, seq, value) { - # window[name]['callbacks'].get(seq).reject(value); - # window[name]['callbacks'].delete(seq); - # } - # } + deliver_result = <<~JAVASCRIPT + function (name, seq, result) { + window[name].callbacks.get(seq).resolve(result); + window[name].callbacks.delete(seq); + } + JAVASCRIPT + JavaScriptFunction.new(deliver_result, [name, seq, result]).source + rescue => err + deliver_error = <<~JAVASCRIPT + function (name, seq, message) { + const error = new Error(message); + window[name].callbacks.get(seq).reject(error); + window[name].callbacks.delete(seq); + } + JAVASCRIPT + JavaScriptFunction.new(deliver_error, [name, seq, err.message]).source + end + + @client.async_send_message('Runtime.evaluate', expression: expression, contextId: execution_context_id).rescue do |error| + debug_puts(error) + end + end + private def add_console_message(type, args, stack_trace) text_tokens = args.map { |arg| arg.remote_object.value } call_frame = stack_trace['callFrames']&.first location = @@ -629,14 +666,13 @@ # @param timeout [number|nil] # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2' # @return [Puppeteer::Response] def reload(timeout: nil, wait_until: nil) - await_all( - async_wait_for_navigation(timeout: timeout, wait_until: wait_until), - @client.async_send_message('Page.reload'), - ).first + wait_for_navigation(timeout: timeout, wait_until: wait_until) do + @client.send_message('Page.reload') + end end def wait_for_navigation(timeout: nil, wait_until: nil) main_frame.send(:wait_for_navigation, timeout: timeout, wait_until: wait_until) end @@ -758,14 +794,13 @@ private def go(delta, timeout: nil, wait_until: nil) history = @client.send_message('Page.getNavigationHistory') entries = history['entries'] index = history['currentIndex'] + delta if_present(entries[index]) do |entry| - await_all( - async_wait_for_navigation(timeout: timeout, wait_until: wait_until), - @client.async_send_message('Page.navigateToHistoryEntry', entryId: entry['id']), - ) + wait_for_navigation(timeout: timeout, wait_until: wait_until) do + @client.send_message('Page.navigateToHistoryEntry', entryId: entry['id']) + end end end # Brings page to front (activates tab). def bring_to_front @@ -795,9 +830,18 @@ media_type_str = media_type.to_s unless ['screen', 'print', ''].include?(media_type_str) raise ArgumentError.new("Unsupported media type: #{media_type}") end @client.send_message('Emulation.setEmulatedMedia', media: media_type_str) + end + + # @param factor [Number|nil] Factor at which the CPU will be throttled (2x, 2.5x. 3x, ...). Passing `nil` disables cpu throttling. + def emulate_cpu_throttling(factor) + if factor.nil? || factor >= 1 + @client.send_message('Emulation.setCPUThrottlingRate', rate: factor || 1) + else + raise ArgumentError.new('Throttling rate should be greater or equal to 1') + end end # @param features [Array] def emulate_media_features(features) if features.nil?