Skip to content

Commit

Permalink
Merge pull request #187 from YusukeIwaki/porting/7825
Browse files Browse the repository at this point in the history
feat: implement Element.waitForSelector
  • Loading branch information
Yusuke Iwaki committed Jan 1, 2022
2 parents 6f1e631 + 75c827f commit 8954694
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 25 deletions.
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

0 comments on commit 8954694

Please sign in to comment.