require_relative '../locator_utils'
module Playwright
# @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_frame.py
define_channel_owner :Frame do
include LocatorUtils
private def after_initialize
if @initializer['parentFrame']
@parent_frame = ChannelOwners::Frame.from(@initializer['parentFrame'])
@parent_frame.send(:append_child_frame_from_child, self)
end
@name = @initializer['name']
@url = @initializer['url']
@detached = false
@child_frames = Set.new
@load_states = Set.new(@initializer['loadStates'])
@event_emitter = Object.new.extend(EventEmitter)
@channel.on('loadstate', ->(params) {
on_load_state(add: params['add'], remove: params['remove'])
})
@channel.on('navigated', method(:on_frame_navigated))
end
attr_reader :page, :parent_frame
attr_writer :detached
private def on_load_state(add:, remove:)
if add
@load_states << add
# Original JS version of Playwright emit event here.
# @event_emitter.emit('loadstate', add)
end
if remove
@load_states.delete(remove)
end
unless @parent_frame
if add == 'load'
@page&.emit(Events::Page::Load, @page)
elsif add == 'domcontentloaded'
@page&.emit(Events::Page::DOMContentLoaded, @page)
end
end
# emit to waitForLoadState(load) listeners explicitly after waitForEvent(load) listeners
if add
@event_emitter.emit('loadstate', add)
end
end
private def on_frame_navigated(event)
@url = event['url']
@name = event['name']
@event_emitter.emit('navigated', event)
unless event['error']
@page&.emit(Events::Page::FrameNavigated, self)
end
end
def goto(url, timeout: nil, waitUntil: nil, referer: nil)
params = {
url: url,
timeout: timeout,
waitUntil: waitUntil,
referer: referer
}.compact
resp = @channel.send_message_to_server('goto', params)
ChannelOwners::Response.from_nullable(resp)
end
class CrashedError < StandardError
def initialize
super('Navigation failed because page crashed!')
end
end
class FrameAlreadyDetachedError < StandardError
def initialize
super('Navigating frame was detached!')
end
end
private def setup_navigation_waiter(wait_name:, timeout_value:)
Waiter.new(page, wait_name: "frame.#{wait_name}").tap do |waiter|
waiter.reject_on_event(@page, Events::Page::Close, -> { @page.send(:close_error_with_reason) })
waiter.reject_on_event(@page, Events::Page::Crash, CrashedError.new)
waiter.reject_on_event(@page, Events::Page::FrameDetached, FrameAlreadyDetachedError.new, predicate: -> (frame) { frame == self })
waiter.reject_on_timeout(timeout_value, "Timeout #{timeout_value}ms exceeded.")
end
end
def expect_navigation(timeout: nil, url: nil, waitUntil: nil, &block)
option_wait_until = waitUntil || 'load'
option_timeout = timeout || @page.send(:timeout_settings).navigation_timeout
time_start = Time.now
waiter = setup_navigation_waiter(wait_name: :expect_navigation, timeout_value: option_timeout)
to_url = url ? " to \"#{url}\"" : ''
waiter.log("waiting for navigation#{to_url} until '#{option_wait_until}'")
predicate =
if url
matcher = UrlMatcher.new(url, base_url: @page.context.send(:base_url))
->(event) {
if event['error']
true
else
waiter.log(" navigated to \"#{event['url']}\"")
matcher.match?(event['url'])
end
}
else
->(_) { true }
end
waiter.wait_for_event(@event_emitter, 'navigated', predicate: predicate)
block&.call
event = waiter.result.value!
if event['error']
raise event['error']
end
unless @load_states.include?(option_wait_until)
elapsed_time = Time.now - time_start
if elapsed_time < option_timeout
wait_for_load_state(state: option_wait_until, timeout: option_timeout - elapsed_time)
end
end
request_json = event.dig('newDocument', 'request')
request = ChannelOwners::Request.from_nullable(request_json)
request&.response
end
def wait_for_url(url, timeout: nil, waitUntil: nil)
matcher = UrlMatcher.new(url, base_url: @page.context.send(:base_url))
if matcher.match?(@url)
wait_for_load_state(state: waitUntil, timeout: timeout)
else
expect_navigation(timeout: timeout, url: url, waitUntil: waitUntil)
end
end
def wait_for_load_state(state: nil, timeout: nil)
option_state = state || 'load'
option_timeout = timeout || @page.send(:timeout_settings).navigation_timeout
unless %w(load domcontentloaded networkidle commit).include?(option_state)
raise ArgumentError.new('state: expected one of (load|domcontentloaded|networkidle|commit)')
end
if @load_states.include?(option_state)
return
end
waiter = setup_navigation_waiter(wait_name: :wait_for_load_state, timeout_value: option_timeout)
predicate = ->(actual_state) {
waiter.log("\"#{actual_state}\" event fired")
actual_state == option_state
}
waiter.wait_for_event(@event_emitter, 'loadstate', predicate: predicate)
# Sometimes event is already fired durting setup.
if @load_states.include?(option_state)
waiter.force_fulfill(option_state)
return
end
waiter.result.value!
nil
end
def frame_element
resp = @channel.send_message_to_server('frameElement')
ChannelOwners::ElementHandle.from(resp)
end
def evaluate(pageFunction, arg: nil)
JavaScript::Expression.new(pageFunction, arg).evaluate(@channel)
end
def evaluate_handle(pageFunction, arg: nil)
JavaScript::Expression.new(pageFunction, arg).evaluate_handle(@channel)
end
def query_selector(selector, strict: nil)
params = {
selector: selector,
strict: strict,
}.compact
resp = @channel.send_message_to_server('querySelector', params)
ChannelOwners::ElementHandle.from_nullable(resp)
end
def query_selector_all(selector)
@channel.send_message_to_server('querySelectorAll', selector: selector).map do |el|
ChannelOwners::ElementHandle.from(el)
end
end
def wait_for_selector(selector, state: nil, strict: nil, timeout: nil)
params = { selector: selector, state: state, strict: strict, timeout: timeout }.compact
resp = @channel.send_message_to_server('waitForSelector', params)
ChannelOwners::ElementHandle.from_nullable(resp)
end
def checked?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isChecked', params)
end
def disabled?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isDisabled', params)
end
def editable?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isEditable', params)
end
def enabled?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isEnabled', params)
end
def hidden?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isHidden', params)
end
def visible?(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('isVisible', params)
end
def dispatch_event(selector, type, eventInit: nil, strict: nil, timeout: nil)
params = {
selector: selector,
type: type,
eventInit: JavaScript::ValueSerializer.new(eventInit).serialize,
strict: strict,
timeout: timeout,
}.compact
@channel.send_message_to_server('dispatchEvent', params)
nil
end
def eval_on_selector(selector, pageFunction, arg: nil, strict: nil)
JavaScript::Expression.new(pageFunction, arg).eval_on_selector(@channel, selector, strict: strict)
end
def eval_on_selector_all(selector, pageFunction, arg: nil)
JavaScript::Expression.new(pageFunction, arg).eval_on_selector_all(@channel, selector)
end
def content
@channel.send_message_to_server('content')
end
def set_content(html, timeout: nil, waitUntil: nil)
params = {
html: html,
timeout: timeout,
waitUntil: waitUntil,
}.compact
@channel.send_message_to_server('setContent', params)
nil
end
def name
@name || ''
end
def url
@url || ''
end
def child_frames
@child_frames.to_a
end
def detached?
@detached
end
def add_script_tag(content: nil, path: nil, type: nil, url: nil)
params = {
content: content,
type: type,
url: url,
}.compact
if path
params[:content] = JavaScript::SourceUrl.new(File.read(path), path).to_s
end
resp = @channel.send_message_to_server('addScriptTag', params)
ChannelOwners::ElementHandle.from(resp)
end
def add_style_tag(content: nil, path: nil, url: nil)
params = {
content: content,
url: url,
}.compact
if path
params[:content] = "#{File.read(path)}\n/*# sourceURL=#{path}*/"
end
resp = @channel.send_message_to_server('addStyleTag', params)
ChannelOwners::ElementHandle.from(resp)
end
def click(
selector,
button: nil,
clickCount: nil,
delay: nil,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
button: button,
clickCount: clickCount,
delay: delay,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('click', params)
nil
end
def drag_and_drop(
source,
target,
force: nil,
noWaitAfter: nil,
sourcePosition: nil,
strict: nil,
targetPosition: nil,
timeout: nil,
trial: nil)
params = {
source: source,
target: target,
force: force,
noWaitAfter: noWaitAfter,
sourcePosition: sourcePosition,
strict: strict,
targetPosition: targetPosition,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('dragAndDrop', params)
nil
end
def dblclick(
selector,
button: nil,
delay: nil,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
button: button,
delay: delay,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('dblclick', params)
nil
end
def tap_point(
selector,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('tap', params)
nil
end
def fill(
selector,
value,
force: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
params = {
selector: selector,
value: value,
force: force,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout,
}.compact
@channel.send_message_to_server('fill', params)
nil
end
def locator(
selector,
has: nil,
hasNot: nil,
hasNotText: nil,
hasText: nil)
LocatorImpl.new(
frame: self,
timeout_settings: @page.send(:timeout_settings),
selector: selector,
has: has,
hasNot: hasNot,
hasNotText: hasNotText,
hasText: hasText)
end
def frame_locator(selector)
FrameLocatorImpl.new(frame: self, timeout_settings: @page.send(:timeout_settings), frame_selector: selector)
end
def focus(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('focus', params)
nil
end
def text_content(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('textContent', params)
end
def inner_text(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('innerText', params)
end
def inner_html(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('innerHTML', params)
end
def get_attribute(selector, name, strict: nil, timeout: nil)
params = {
selector: selector,
name: name,
strict: strict,
timeout: timeout,
}.compact
@channel.send_message_to_server('getAttribute', params)
end
def hover(
selector,
force: nil,
modifiers: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
force: force,
modifiers: modifiers,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('hover', params)
nil
end
def select_option(
selector,
element: nil,
index: nil,
value: nil,
label: nil,
force: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
base_params = SelectOptionValues.new(
element: element,
index: index,
value: value,
label: label,
).as_params
params = base_params.merge({ selector: selector, force: force, noWaitAfter: noWaitAfter, strict: strict, timeout: timeout }.compact)
@channel.send_message_to_server('selectOption', params)
end
def input_value(selector, strict: nil, timeout: nil)
params = { selector: selector, strict: strict, timeout: timeout }.compact
@channel.send_message_to_server('inputValue', params)
end
def set_input_files(selector, files, noWaitAfter: nil, strict: nil, timeout: nil)
method_name, params = InputFiles.new(page.context, files).as_method_and_params
params.merge!({
selector: selector,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout,
}.compact)
@channel.send_message_to_server(method_name, params)
nil
end
def type(
selector,
text,
delay: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
params = {
selector: selector,
text: text,
delay: delay,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout,
}.compact
@channel.send_message_to_server('type', params)
nil
end
def press(
selector,
key,
delay: nil,
noWaitAfter: nil,
strict: nil,
timeout: nil)
params = {
selector: selector,
key: key,
delay: delay,
noWaitAfter: noWaitAfter,
strict: strict,
timeout: timeout,
}.compact
@channel.send_message_to_server('press', params)
nil
end
def check(
selector,
force: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
force: force,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('check', params)
nil
end
def uncheck(
selector,
force: nil,
noWaitAfter: nil,
position: nil,
strict: nil,
timeout: nil,
trial: nil)
params = {
selector: selector,
force: force,
noWaitAfter: noWaitAfter,
position: position,
strict: strict,
timeout: timeout,
trial: trial,
}.compact
@channel.send_message_to_server('uncheck', params)
nil
end
def set_checked(selector, checked, **options)
if checked
check(selector, **options)
else
uncheck(selector, **options)
end
end
def wait_for_timeout(timeout)
@channel.send_message_to_server('waitForTimeout', timeout: timeout)
nil
end
def wait_for_function(pageFunction, arg: nil, polling: nil, timeout: nil)
if polling.is_a?(String) && polling != 'raf'
raise ArgumentError.new("Unknown polling option: #{polling}")
end
expression = JavaScript::Expression.new(pageFunction, arg)
expression.wait_for_function(@channel, polling: polling, timeout: timeout)
end
def title
@channel.send_message_to_server('title')
end
def highlight(selector)
@channel.send_message_to_server('highlight', selector: selector)
end
# @param page [Page]
# @note This method should be used internally. Accessed via .send method, so keep private!
private def update_page_from_page(page)
@page = page
end
# @param child [Frame]
# @note This method should be used internally. Accessed via .send method, so keep private!
private def append_child_frame_from_child(frame)
@child_frames << frame
end
end
end