require 'base64'
require_relative '../locator_utils'
module Playwright
# @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_page.py
define_channel_owner :Page do
include Utils::Errors::TargetClosedErrorMethods
include LocatorUtils
attr_writer :owned_context
private def after_initialize
@browser_context = @parent
@timeout_settings = TimeoutSettings.new(@browser_context.send(:_timeout_settings))
@accessibility = AccessibilityImpl.new(@channel)
@keyboard = KeyboardImpl.new(@channel)
@mouse = MouseImpl.new(@channel)
@touchscreen = TouchscreenImpl.new(@channel)
if @initializer['viewportSize']
@viewport_size = {
width: @initializer['viewportSize']['width'],
height: @initializer['viewportSize']['height'],
}
end
@closed = false
@workers = Set.new
@bindings = {}
@routes = []
@main_frame = ChannelOwners::Frame.from(@initializer['mainFrame'])
@main_frame.send(:update_page_from_page, self)
@frames = Set.new
@frames << @main_frame
@opener = ChannelOwners::Page.from_nullable(@initializer['opener'])
@close_reason = nil
@channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) })
@closed_or_crashed_promise = Concurrent::Promises.resolvable_future
@channel.once('close', ->(_) { on_close })
@channel.on('crash', ->(_) { on_crash })
@channel.on('download', method(:on_download))
@channel.on('fileChooser', ->(params) {
chooser = FileChooserImpl.new(
page: self,
element_handle: ChannelOwners::ElementHandle.from(params['element']),
is_multiple: params['isMultiple'])
emit(Events::Page::FileChooser, chooser)
})
@channel.on('frameAttached', ->(params) {
on_frame_attached(ChannelOwners::Frame.from(params['frame']))
})
@channel.on('frameDetached', ->(params) {
on_frame_detached(ChannelOwners::Frame.from(params['frame']))
})
@channel.on('pageError', ->(params) {
emit(Events::Page::PageError, Error.parse(params['error']['error']))
})
@channel.on('route', ->(params) { on_route(ChannelOwners::Route.from(params['route'])) })
@channel.on('video', method(:on_video))
@channel.on('webSocket', ->(params) {
emit(Events::Page::WebSocket, ChannelOwners::WebSocket.from(params['webSocket']))
})
@channel.on('worker', ->(params) {
worker = ChannelOwners::Worker.from(params['worker'])
on_worker(worker)
})
set_event_to_subscription_mapping({
Events::Page::Console => "console",
Events::Page::Dialog => "dialog",
Events::Page::Request => "request",
Events::Page::Response => "response",
Events::Page::RequestFinished => "requestFinished",
Events::Page::RequestFailed => "requestFailed",
Events::Page::FileChooser => "fileChooser",
})
end
attr_reader \
:accessibility,
:keyboard,
:mouse,
:touchscreen,
:viewport_size,
:main_frame
private def on_frame_attached(frame)
frame.send(:update_page_from_page, self)
@frames << frame
emit(Events::Page::FrameAttached, frame)
end
private def on_frame_detached(frame)
@frames.delete(frame)
frame.detached = true
emit(Events::Page::FrameDetached, frame)
end
private def on_route(route)
route.send(:update_context, self)
# It is not desired to use PlaywrightApi.wrap directly.
# However it is a little difficult to define wrapper for `handler` parameter in generate_api.
# Just a workaround...
Concurrent::Promises.future(PlaywrightApi.wrap(route)) do |wrapped_route|
handled = @routes.any? do |handler_entry|
next false unless handler_entry.match?(route.request.url)
promise = Concurrent::Promises.resolvable_future
route.send(:set_handling_future, promise)
promise_handled = Concurrent::Promises.zip(
promise,
handler_entry.async_handle(wrapped_route)
).value!.first
promise_handled
end
@routes.reject!(&:expired?)
if @routes.count == 0
async_update_interception_patterns
end
unless handled
@browser_context.send(:on_route, route)
end
end.rescue do |err|
puts err, err.backtrace
end
end
private def on_binding(binding_call)
func = @bindings[binding_call.name]
if func
binding_call.call_async(func)
end
@browser_context.send(:on_binding, binding_call)
end
private def on_worker(worker)
worker.page = self
@workers << worker
emit(Events::Page::Worker, worker)
end
private def on_close
@closed = true
@browser_context.send(:remove_page, self)
@browser_context.send(:remove_background_page, self)
if @closed_or_crashed_promise.pending?
@closed_or_crashed_promise.fulfill(close_error_with_reason)
end
emit(Events::Page::Close)
end
private def on_crash
if @closed_or_crashed_promise.pending?
@closed_or_crashed_promise.fulfill(TargetClosedError.new)
end
emit(Events::Page::Crash)
end
private def on_download(params)
artifact = ChannelOwners::Artifact.from(params['artifact'])
download = DownloadImpl.new(
page: self,
url: params['url'],
suggested_filename: params['suggestedFilename'],
artifact: artifact,
)
emit(Events::Page::Download, download)
end
private def on_video(params)
artifact = ChannelOwners::Artifact.from(params['artifact'])
video.send(:set_artifact, artifact)
end
# @override
private def perform_event_emitter_callback(event, callback, args)
should_callback_async = [
Events::Page::Dialog,
Events::Page::Response,
].freeze
if should_callback_async.include?(event)
Concurrent::Promises.future { super }
else
super
end
end
def context
@browser_context
end
def opener
if @opener&.closed?
nil
else
@opener
end
end
private def emit_popup_event_from_browser_context
if @opener && !@opener.closed?
@opener.emit(Events::Page::Popup, self)
end
end
def frame(name: nil, url: nil)
if name
@frames.find { |f| f.name == name }
elsif url
matcher = UrlMatcher.new(url, base_url: @browser_context.send(:base_url))
@frames.find { |f| matcher.match?(f.url) }
else
raise ArgumentError.new('Either name or url matcher should be specified')
end
end
def frames
@frames.to_a
end
def set_default_navigation_timeout(timeout)
@timeout_settings.default_navigation_timeout = timeout
@channel.send_message_to_server('setDefaultNavigationTimeoutNoReply', timeout: timeout)
end
def set_default_timeout(timeout)
@timeout_settings.default_timeout = timeout
@channel.send_message_to_server('setDefaultTimeoutNoReply', timeout: timeout)
end
def query_selector(selector, strict: nil)
@main_frame.query_selector(selector, strict: strict)
end
def query_selector_all(selector)
@main_frame.query_selector_all(selector)
end
def wait_for_selector(selector, state: nil, strict: nil, timeout: nil)
@main_frame.wait_for_selector(selector, state: state, strict: strict, timeout: timeout)
end
def checked?(selector, strict: nil, timeout: nil)
@main_frame.checked?(selector, strict: strict, timeout: timeout)
end
def disabled?(selector, strict: nil, timeout: nil)
@main_frame.disabled?(selector, strict: strict, timeout: timeout)
end
def editable?(selector, strict: nil, timeout: nil)
@main_frame.editable?(selector, strict: strict, timeout: timeout)
end
def enabled?(selector, strict: nil, timeout: nil)
@main_frame.enabled?(selector, strict: strict, timeout: timeout)
end
def hidden?(selector, strict: nil, timeout: nil)
@main_frame.hidden?(selector, strict: strict, timeout: timeout)
end
def visible?(selector, strict: nil, timeout: nil)
@main_frame.visible?(selector, strict: strict, timeout: timeout)
end
def dispatch_event(selector, type, eventInit: nil, strict: nil, timeout: nil)
@main_frame.dispatch_event(selector, type, eventInit: eventInit, strict: strict, timeout: timeout)
end
def evaluate(pageFunction, arg: nil)
@main_frame.evaluate(pageFunction, arg: arg)
end
def evaluate_handle(pageFunction, arg: nil)
@main_frame.evaluate_handle(pageFunction, arg: arg)
end
def eval_on_selector(selector, pageFunction, arg: nil, strict: nil)
@main_frame.eval_on_selector(selector, pageFunction, arg: arg, strict: strict)
end
def eval_on_selector_all(selector, pageFunction, arg: nil)
@main_frame.eval_on_selector_all(selector, pageFunction, arg: arg)
end
def add_script_tag(content: nil, path: nil, type: nil, url: nil)
@main_frame.add_script_tag(content: content, path: path, type: type, url: url)
end
def add_style_tag(content: nil, path: nil, url: nil)
@main_frame.add_style_tag(content: content, path: path, url: url)
end
def expose_function(name, callback)
@channel.send_message_to_server('exposeBinding', name: name)
@bindings[name] = ->(_source, *args) { callback.call(*args) }
end
def expose_binding(name, callback, handle: nil)
params = {
name: name,
needsHandle: handle,
}.compact
@channel.send_message_to_server('exposeBinding', params)
@bindings[name] = callback
end
def set_extra_http_headers(headers)
serialized_headers = HttpHeaders.new(headers).as_serialized
@channel.send_message_to_server('setExtraHTTPHeaders', headers: serialized_headers)
end
def url
@main_frame.url
end
def content
@main_frame.content
end
def set_content(html, timeout: nil, waitUntil: nil)
@main_frame.set_content(html, timeout: timeout, waitUntil: waitUntil)
end
def goto(url, timeout: nil, waitUntil: nil, referer: nil)
@main_frame.goto(url, timeout: timeout, waitUntil: waitUntil, referer: referer)
end
def reload(timeout: nil, waitUntil: nil)
params = {
timeout: timeout,
waitUntil: waitUntil,
}.compact
resp = @channel.send_message_to_server('reload', params)
ChannelOwners::Response.from_nullable(resp)
end
def wait_for_load_state(state: nil, timeout: nil)
@main_frame.wait_for_load_state(state: state, timeout: timeout)
end
def wait_for_url(url, timeout: nil, waitUntil: nil)
@main_frame.wait_for_url(url, timeout: timeout, waitUntil: waitUntil)
end
def go_back(timeout: nil, waitUntil: nil)
params = { timeout: timeout, waitUntil: waitUntil }.compact
resp = @channel.send_message_to_server('goBack', params)
ChannelOwners::Response.from_nullable(resp)
end
def go_forward(timeout: nil, waitUntil: nil)
params = { timeout: timeout, waitUntil: waitUntil }.compact
resp = @channel.send_message_to_server('goForward', params)
ChannelOwners::Response.from_nullable(resp)
end
def emulate_media(colorScheme: nil, forcedColors: nil, media: nil, reducedMotion: nil)
params = {
colorScheme: no_override_if_null(colorScheme),
forcedColors: no_override_if_null(forcedColors),
media: no_override_if_null(media),
reducedMotion: no_override_if_null(reducedMotion),
}.compact
@channel.send_message_to_server('emulateMedia', params)
nil
end
private def no_override_if_null(target)
if target == 'null'
'no-override'
else
target
end
end
def set_viewport_size(viewportSize)
@viewport_size = viewportSize
@channel.send_message_to_server('setViewportSize', { viewportSize: viewportSize })
nil
end
def bring_to_front
@channel.send_message_to_server('bringToFront')
nil
end
def add_init_script(path: nil, script: nil)
source =
if path
JavaScript::SourceUrl.new(File.read(path), path).to_s
elsif script
script
else
raise ArgumentError.new('Either path or script parameter must be specified')
end
@channel.send_message_to_server('addInitScript', source: source)
nil
end
def route(url, handler, times: nil)
entry = RouteHandler.new(url, @browser_context.send(:base_url), handler, times)
@routes.unshift(entry)
update_interception_patterns
end
def unroute_all(behavior: nil)
@routes.clear
update_interception_patterns
end
def unroute(url, handler: nil)
@routes.reject! do |handler_entry|
handler_entry.same_value?(url: url, handler: handler)
end
update_interception_patterns
end
def route_from_har(har, notFound: nil, update: nil, url: nil, updateContent: nil, updateMode: nil)
if update
@browser_context.send(:record_into_har, har, self, notFound: notFound, url: url, updateContent: updateContent, updateMode: updateMode)
return
end
router = HarRouter.create(
@connection.local_utils,
har.to_s,
notFound || "abort",
url_match: url,
)
router.add_page_route(self)
end
private def async_update_interception_patterns
patterns = RouteHandler.prepare_interception_patterns(@routes)
@channel.async_send_message_to_server('setNetworkInterceptionPatterns', patterns: patterns)
end
private def update_interception_patterns
patterns = RouteHandler.prepare_interception_patterns(@routes)
@channel.send_message_to_server('setNetworkInterceptionPatterns', patterns: patterns)
end
def screenshot(
animations: nil,
caret: nil,
clip: nil,
fullPage: nil,
mask: nil,
maskColor: nil,
omitBackground: nil,
path: nil,
quality: nil,
scale: nil,
style: nil,
timeout: nil,
type: nil)
params = {
type: type,
quality: quality,
fullPage: fullPage,
clip: clip,
maskColor: maskColor,
omitBackground: omitBackground,
animations: animations,
caret: caret,
scale: scale,
style: style,
timeout: timeout,
}.compact
if mask.is_a?(Enumerable)
params[:mask] = mask.map do |locator|
locator.send(:to_protocol)
end
end
encoded_binary = @channel.send_message_to_server('screenshot', params)
decoded_binary = Base64.strict_decode64(encoded_binary)
if path
File.open(path, 'wb') do |f|
f.write(decoded_binary)
end
end
decoded_binary
end
def title
@main_frame.title
end
def close(runBeforeUnload: nil, reason: nil)
@close_reason = reason
if @owned_context
@owned_context.close
else
options = { runBeforeUnload: runBeforeUnload }.compact
@channel.send_message_to_server('close', options)
end
nil
rescue => err
raise if !target_closed_error?(err) || !runBeforeUnload
end
def closed?
@closed
end
def click(
selector,
button: nil,
clickCount: nil,
delay: nil,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.click(
selector,
button: button,
clickCount: clickCount,
delay: delay,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
)
end
def drag_and_drop(
source,
target,
force: nil,
noWaitAfter: nil,
sourcePosition: nil,
strict: nil,
targetPosition: nil,
timeout: nil,
trial: nil)
@main_frame.drag_and_drop(
source,
target,
force: force,
noWaitAfter: noWaitAfter,
sourcePosition: sourcePosition,
strict: strict,
targetPosition: targetPosition,
timeout: timeout,
trial: trial)
end
def dblclick(
selector,
button: nil,
delay: nil,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.dblclick(
selector,
button: button,
delay: delay,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
)
end
def tap_point(
selector,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.tap_point(
selector,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
)
end
def fill(
selector,
value,
force: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
@main_frame.fill(
selector,
value,
force: force,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout)
end
def locator(
selector,
has: nil,
hasNot: nil,
hasNotText: nil,
hasText: nil)
@main_frame.locator(
selector,
has: has,
hasNot: hasNot,
hasNotText: hasNotText,
hasText: hasText)
end
def frame_locator(selector)
@main_frame.frame_locator(selector)
end
def focus(selector, strict: nil, timeout: nil)
@main_frame.focus(selector, strict: strict, timeout: timeout)
end
def text_content(selector, strict: nil, timeout: nil)
@main_frame.text_content(selector, strict: strict, timeout: timeout)
end
def inner_text(selector, strict: nil, timeout: nil)
@main_frame.inner_text(selector, strict: strict, timeout: timeout)
end
def inner_html(selector, strict: nil, timeout: nil)
@main_frame.inner_html(selector, strict: strict, timeout: timeout)
end
def get_attribute(selector, name, strict: nil, timeout: nil)
@main_frame.get_attribute(selector, name, strict: strict, timeout: timeout)
end
def hover(
selector,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.hover(
selector,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
)
end
def select_option(
selector,
element: nil,
index: nil,
value: nil,
label: nil,
force: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
@main_frame.select_option(
selector,
element: element,
index: index,
value: value,
label: label,
force: force,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout,
)
end
def input_value(selector, strict: nil, timeout: nil)
@main_frame.input_value(selector, strict: strict, timeout: timeout)
end
def set_input_files(selector, files, noWaitAfter: nil, strict: nil,timeout: nil)
@main_frame.set_input_files(
selector,
files,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout)
end
def type(
selector,
text,
delay: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
@main_frame.type(
selector,
text,
delay: delay,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout)
end
def press(
selector,
key,
delay: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
@main_frame.press(
selector,
key,
delay: delay,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout)
end
def check(
selector,
force: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.check(
selector,
force: force,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial)
end
def uncheck(
selector,
force: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
@main_frame.uncheck(
selector,
force: force,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial)
end
def set_checked(selector, checked, **options)
if checked
check(selector, **options)
else
uncheck(selector, **options)
end
end
def wait_for_timeout(timeout)
@main_frame.wait_for_timeout(timeout)
end
def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
@main_frame.wait_for_function(pageFunction, arg: arg, polling: polling, timeout: timeout)
end
def workers
@workers.to_a
end
def request
@browser_context.request
end
def pause
@browser_context.send(:pause)
end
def pdf(
displayHeaderFooter: nil,
footerTemplate: nil,
format: nil,
headerTemplate: nil,
height: nil,
landscape: nil,
margin: nil,
pageRanges: nil,
path: nil,
preferCSSPageSize: nil,
printBackground: nil,
scale: nil,
width: nil,
tagged: nil,
outline: nil)
params = {
displayHeaderFooter: displayHeaderFooter,
footerTemplate: footerTemplate,
format: format,
headerTemplate: headerTemplate,
height: height,
landscape: landscape,
margin: margin,
pageRanges: pageRanges,
preferCSSPageSize: preferCSSPageSize,
printBackground: printBackground,
scale: scale,
width: width,
tagged: tagged,
outline: outline,
}.compact
encoded_binary = @channel.send_message_to_server('pdf', params)
decoded_binary = Base64.strict_decode64(encoded_binary)
if path
File.open(path, 'wb') do |f|
f.write(decoded_binary)
end
end
decoded_binary
end
def video
return nil unless @browser_context.send(:has_record_video_option?)
@video ||= Video.new(self)
end
def start_js_coverage(resetOnNavigation: nil, reportAnonymousScripts: nil)
params = {
resetOnNavigation: resetOnNavigation,
reportAnonymousScripts: reportAnonymousScripts,
}.compact
@channel.send_message_to_server('startJSCoverage', params)
end
def stop_js_coverage
@channel.send_message_to_server('stopJSCoverage')
end
def start_css_coverage(resetOnNavigation: nil, reportAnonymousScripts: nil)
params = {
resetOnNavigation: resetOnNavigation,
}.compact
@channel.send_message_to_server('startCSSCoverage', params)
end
def stop_css_coverage
@channel.send_message_to_server('stopCSSCoverage')
end
class CrashedError < StandardError
def initialize
super('Page crashed')
end
end
class FrameAlreadyDetachedError < StandardError
def initialize
super('Navigating frame was detached!')
end
end
private def close_error_with_reason
reason = @close_reason || @browser_context.send(:effective_close_reason)
TargetClosedError.new(message: reason)
end
def expect_event(event, predicate: nil, timeout: nil, &block)
waiter = Waiter.new(self, wait_name: "Page.expect_event(#{event})")
timeout_value = timeout || @timeout_settings.timeout
waiter.reject_on_timeout(timeout_value, "Timeout #{timeout_value}ms exceeded while waiting for event \"#{event}\"")
unless event == Events::Page::Crash
waiter.reject_on_event(self, Events::Page::Crash, CrashedError.new)
end
unless event == Events::Page::Close
waiter.reject_on_event(self, Events::Page::Close, -> { close_error_with_reason })
end
waiter.wait_for_event(self, event, predicate: predicate)
block&.call
waiter.result.value!
end
def expect_console_message(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::Console, predicate: predicate, timeout: timeout, &block)
end
def expect_download(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::Download, predicate: predicate, timeout: timeout, &block)
end
def expect_file_chooser(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::FileChooser, predicate: predicate, timeout: timeout, &block)
end
def expect_navigation(timeout: nil, url: nil, waitUntil: nil, &block)
@main_frame.expect_navigation(
timeout: timeout,
url: url,
waitUntil: waitUntil,
&block)
end
def expect_popup(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::Popup, predicate: predicate, timeout: timeout, &block)
end
def expect_request(urlOrPredicate, timeout: nil, &block)
predicate =
case urlOrPredicate
when String, Regexp
url_matcher = UrlMatcher.new(urlOrPredicate, base_url: @browser_context.send(:base_url))
-> (req){ url_matcher.match?(req.url) }
when Proc
urlOrPredicate
else
-> (_) { true }
end
expect_event(Events::Page::Request, predicate: predicate, timeout: timeout, &block)
end
def expect_request_finished(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::RequestFinished, predicate: predicate, timeout: timeout, &block)
end
def expect_response(urlOrPredicate, timeout: nil, &block)
predicate =
case urlOrPredicate
when String, Regexp
url_matcher = UrlMatcher.new(urlOrPredicate, base_url: @browser_context.send(:base_url))
-> (req){ url_matcher.match?(req.url) }
when Proc
urlOrPredicate
else
-> (_) { true }
end
expect_event(Events::Page::Response, predicate: predicate, timeout: timeout, &block)
end
def expect_websocket(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::WebSocket, predicate: predicate, timeout: timeout, &block)
end
def expect_worker(predicate: nil, timeout: nil, &block)
expect_event(Events::Page::Worker, predicate: predicate, timeout: timeout, &block)
end
# called from Frame with send(:timeout_settings)
private def timeout_settings
@timeout_settings
end
# called from BrowserContext#expose_binding
private def has_bindings?(name)
@bindings.key?(name)
end
# called from Worker#on_close
private def remove_worker(worker)
@workers.delete(worker)
end
# called from Video
private def remote_connection?
@connection.remote?
end
# Expose guid for library developers.
# Not intended to be used by users.
def guid
@guid
end
end
end