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: implement Element.waitForSelector #187

Merged
merged 2 commits into from Jan 1, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/puppeteer/aria_query_handler.rb
Expand Up @@ -36,19 +36,21 @@ def query_one(element, selector)
end
end

def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
# addHandlerToWorld
binding_function = Puppeteer::DOMWorld::BindingFunction.new(
name: 'ariaQuerySelector',
proc: -> (sel) { query_one(dom_world.send(:document), sel) },
proc: -> (sel) { query_one(root || dom_world.send(:document), sel) },
)
dom_world.send(:wait_for_selector_in_page,
'(_, selector) => globalThis.ariaQuerySelector(selector)',
selector,
visible: visible,
hidden: hidden,
timeout: timeout,
binding_function: binding_function)
binding_function: binding_function,
root: root,
)
end

def query_all(element, selector)
Expand Down
4 changes: 2 additions & 2 deletions lib/puppeteer/custom_query_handler.rb
Expand Up @@ -21,12 +21,12 @@ def query_one(element, selector)
nil
end

def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil, root: nil)
unless @query_one
raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
end

dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout)
dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
end

def query_all(element, selector)
Expand Down
20 changes: 12 additions & 8 deletions lib/puppeteer/dom_world.rb
Expand Up @@ -420,10 +420,10 @@ def type_text(selector, text, delay: nil)
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
# @param timeout [Integer]
def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, root: nil)
# call wait_for_selector_in_page with custom query selector.
query_selector_manager = Puppeteer::QueryHandlerManager.instance
query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout, root: root)
end

private def binding_identifier(name, context)
Expand Down Expand Up @@ -497,10 +497,11 @@ def add_binding_to_context(context, binding_function)
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
# @param timeout [Integer]
private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, binding_function: nil)
private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, root: nil, binding_function: nil)
option_wait_for_visible = visible || false
option_wait_for_hidden = hidden || false
option_timeout = timeout || @timeout_settings.timeout
option_root = root

polling =
if option_wait_for_visible || option_wait_for_hidden
Expand All @@ -511,11 +512,11 @@ def add_binding_to_context(context, binding_function)
title = "selector #{selector}#{option_wait_for_hidden ? 'to be hidden' : ''}"

selector_predicate = make_predicate_string(
predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
predicate_query_handler: query_one,
async: true,
predicate_body: <<~JAVASCRIPT
const node = await predicateQueryHandler(document, selector)
const node = await predicateQueryHandler(root, selector)
return checkWaitForOptions(node, waitForVisible, waitForHidden);
JAVASCRIPT
)
Expand All @@ -527,6 +528,7 @@ def add_binding_to_context(context, binding_function)
polling: polling,
timeout: option_timeout,
args: [selector, option_wait_for_visible, option_wait_for_hidden],
root: option_root,
binding_function: binding_function,
)
handle = wait_task.await_promise
Expand All @@ -541,10 +543,11 @@ def add_binding_to_context(context, binding_function)
# @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
# @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
# @param timeout [Integer]
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil, root: nil)
option_wait_for_visible = visible || false
option_wait_for_hidden = hidden || false
option_timeout = timeout || @timeout_settings.timeout
option_root = root

polling =
if option_wait_for_visible || option_wait_for_hidden
Expand All @@ -555,9 +558,9 @@ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"

xpath_predicate = make_predicate_string(
predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
predicate_arg_def: '(root, selector, waitForVisible, waitForHidden)',
predicate_body: <<~JAVASCRIPT
const node = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
const node = document.evaluate(selector, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
return checkWaitForOptions(node, waitForVisible, waitForHidden);
JAVASCRIPT
)
Expand All @@ -569,6 +572,7 @@ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
polling: polling,
timeout: option_timeout,
args: [xpath, option_wait_for_visible, option_wait_for_hidden],
root: option_root,
)
handle = wait_task.await_promise
unless handle.as_element
Expand Down
54 changes: 54 additions & 0 deletions lib/puppeteer/element_handle.rb
Expand Up @@ -20,6 +20,60 @@ def initialize(context:, client:, remote_object:, page:, frame_manager:)
@disposed = false
end

def inspect
values = %i[context remote_object page disposed].map do |sym|
value = instance_variable_get(:"@#{sym}")
"@#{sym}=#{value}"
end
"#<Puppeteer::ElementHandle #{values.join(' ')}>"
end

#
# Wait for the `selector` to appear within the element. If at the moment of calling the
# method the `selector` already exists, the method will return immediately. If
# the `selector` doesn't appear after the `timeout` milliseconds of waiting, the
# function will throw.
#
# This method does not work across navigations or if the element is detached from DOM.
#
# @param selector - A
# {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors | selector}
# of an element to wait for
# @param options - Optional waiting parameters
# @returns Promise which resolves when element specified by selector string
# is added to DOM. Resolves to `null` if waiting for hidden: `true` and
# selector is not found in DOM.
# @remarks
# The optional parameters in `options` are:
#
# - `visible`: wait for the selected element to be present in DOM and to be
# visible, i.e. to not have `display: none` or `visibility: hidden` CSS
# properties. Defaults to `false`.
#
# - `hidden`: wait for the selected element to not be found in the DOM or to be hidden,
# i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to
# `false`.
#
# - `timeout`: maximum time to wait in milliseconds. Defaults to `30000`
# (30 seconds). Pass `0` to disable timeout. The default value can be changed
# by using the {@link Page.setDefaultTimeout} method.
def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
frame = @context.frame

secondary_world = frame.secondary_world
adopted_root = secondary_world.execution_context.adopt_element_handle(self)
handle = secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
adopted_root.dispose
return nil unless handle

main_world = frame.main_world
result = main_world.execution_context.adopt_element_handle(handle)
handle.dispose
result
end

define_async_method :async_wait_for_selector

def as_element
self
end
Expand Down
8 changes: 8 additions & 0 deletions lib/puppeteer/js_handle.rb
Expand Up @@ -36,6 +36,14 @@ def initialize(context:, client:, remote_object:)

attr_reader :context, :remote_object

def inspect
values = %i[context remote_object disposed].map do |sym|
value = instance_variable_get(:"@#{sym}")
"@#{sym}=#{value}"
end
"#<Puppeteer::JSHandle #{values.join(' ')}>"
end

# @return [Puppeteer::ExecutionContext]
def execution_context
@context
Expand Down
4 changes: 2 additions & 2 deletions lib/puppeteer/query_handler_manager.rb
Expand Up @@ -26,8 +26,8 @@ def query_one(element_handle)
@query_handler.query_one(element_handle, @selector)
end

def wait_for(dom_world, visible:, hidden:, timeout:)
@query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout)
def wait_for(dom_world, visible:, hidden:, timeout:, root:)
@query_handler.wait_for(dom_world, @selector, visible: visible, hidden: hidden, timeout: timeout, root: root)
end

def query_all(element_handle)
Expand Down
17 changes: 10 additions & 7 deletions lib/puppeteer/wait_task.rb
Expand Up @@ -9,7 +9,7 @@ def initialize(title:, timeout:)
end
end

def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil)
def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: [], binding_function: nil, root: nil)
if polling.is_a?(String)
if polling != 'raf' && polling != 'mutation'
raise ArgumentError.new("Unknown polling option: #{polling}")
Expand All @@ -25,6 +25,7 @@ def initialize(dom_world:, predicate_body:, title:, polling:, timeout:, args: []
@dom_world = dom_world
@polling = polling
@timeout = timeout
@root = root
@predicate_body = "return (#{predicate_body})(...args);"
@args = args
@binding_function = binding_function
Expand Down Expand Up @@ -68,6 +69,7 @@ def rerun
begin
success = context.evaluate_handle(
WAIT_FOR_PREDICATE_PAGE_FUNCTION,
@root,
@predicate_body,
@polling,
@timeout,
Expand Down Expand Up @@ -121,8 +123,9 @@ def rerun
private define_async_method :async_rerun

WAIT_FOR_PREDICATE_PAGE_FUNCTION = <<~JAVASCRIPT
async function _(predicateBody, polling, timeout, ...args) {
async function _(root, predicateBody, polling, timeout, ...args) {
const predicate = new Function('...args', predicateBody);
root = root || document
let timedOut = false;
if (timeout)
setTimeout(() => (timedOut = true), timeout);
Expand All @@ -136,7 +139,7 @@ def rerun
* @return {!Promise<*>}
*/
async function pollMutation() {
const success = await predicate(...args);
const success = await predicate(root, ...args);
if (success) return Promise.resolve(success);
let fulfill;
const result = new Promise((x) => (fulfill = x));
Expand All @@ -145,13 +148,13 @@ def rerun
observer.disconnect();
fulfill();
}
const success = await predicate(...args);
const success = await predicate(root, ...args);
if (success) {
observer.disconnect();
fulfill(success);
}
});
observer.observe(document, {
observer.observe(root, {
childList: true,
subtree: true,
attributes: true,
Expand All @@ -168,7 +171,7 @@ def rerun
fulfill();
return;
}
const success = await predicate(...args);
const success = await predicate(root, ...args);
if (success) fulfill(success);
else requestAnimationFrame(onRaf);
}
Expand All @@ -183,7 +186,7 @@ def rerun
fulfill();
return;
}
const success = await predicate(...args);
const success = await predicate(root, ...args);
if (success) fulfill(success);
else setTimeout(onTimeout, pollInterval);
}
Expand Down
19 changes: 16 additions & 3 deletions spec/integration/aria_query_handler_spec.rb
Expand Up @@ -93,6 +93,19 @@
Timeout.timeout(1) { page.wait_for_selector('aria/[role="button"]') }
end

it 'should return the element handle' do
page.evaluate(<<~JAVASCRIPT)
() => (document.body.innerHTML = `<div></div>`)
JAVASCRIPT
element = page.query_selector('div')
inner_element = element.wait_for_selector('aria/test') do
element.evaluate(<<~JAVASCRIPT)
el => el.innerHTML="<p><button>test</button></p>"
JAVASCRIPT
end
expect(inner_element.evaluate('el => el.outerHTML')).to eq('<button>test</button>')
end

it 'should persist query handler bindings across reloads' do
page.goto(server_empty_page)
page.evaluate(add_element, 'button')
Expand Down Expand Up @@ -372,9 +385,9 @@
# });

it 'should return the element handle' do
promise = page.async_wait_for_selector('aria/zombo')
page.content = "<div aria-label='zombo'>anything</div>"
result = await promise
result = page.wait_for_selector('aria/zombo') do
page.content = "<div aria-label='zombo'>anything</div>"
end
expect(result).to be_a(Puppeteer::ElementHandle)
expect(page.evaluate('(x) => x.textContent', result)).to eq('anything')
end
Expand Down
45 changes: 45 additions & 0 deletions spec/integration/element_handle_spec.rb
Expand Up @@ -279,6 +279,26 @@
end
end

describe '#wait_for_selector' do
it 'should wait correctly with waitForSelector on an element' do
element = page.wait_for_selector('.foo') do
# Set the page content after the waitFor has been started.
page.content = '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
end

inner_element = element.wait_for_selector('.bar') do
element.evaluate(<<~JAVASCRIPT)
(el) => {
el.innerHTML = '<div class="bar">bar1</div>';
}
JAVASCRIPT
end
expect(inner_element).not_to be_nil
text = inner_element.evaluate('el => el.innerText')
expect(text).to eq('bar1')
end
end

describe '#hover' do
it 'should work', sinatra: true do
page.goto("#{server_prefix}/input/scrollable.html")
Expand Down Expand Up @@ -423,7 +443,32 @@

# expect(element).toBeDefined();
# });
# it('should wait correctly with waitForSelector on an element', async () => {
# const { page, puppeteer } = getTestState();
# puppeteer.registerCustomQueryHandler('getByClass', {
# queryOne: (element, selector) => element.querySelector(`.${selector}`),
# });
# const waitFor = page.waitForSelector('getByClass/foo');

# // Set the page content after the waitFor has been started.
# await page.setContent(
# '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>'
# );
# let element = await waitFor;
# expect(element).toBeDefined();

# const innerWaitFor = element.waitForSelector('getByClass/bar');

# await element.evaluate((el) => {
# el.innerHTML = '<div class="bar">bar1</div>';
# });

# element = await innerWaitFor;
# expect(element).toBeDefined();
# expect(
# await element.evaluate((el: HTMLElement) => el.innerText)
# ).toStrictEqual('bar1');
# });
# it('should wait correctly with waitFor', async () => {
# /* page.waitFor is deprecated so we silence the warning to avoid test noise */
# sinon.stub(console, 'warn').callsFake(() => {});
Expand Down