page-object icon indicating copy to clipboard operation
page-object copied to clipboard

Extend wait_for_ajax to check if the framework is initialized

Open Chuckv opened this issue 11 years ago • 7 comments
trafficstars

(givin back ;-) )

WIth so much client side rendering going on these days, you can get pages where the first thing you have to do is wait for ajax, right after the page loads, before you can do anything else. when this happens, you can sometimes try to interrogate the ajax framework before it is fully initialized, and the call will fail and crush the entire process of trying to wait. To get around this you need to first wait for the ajax framework to be there, then query it regarding outstanding requests

I suggest a small improvement to your wait_for_ajax method, which is to make sure that the ajax framework has actually been instantiated in the page before trying to poll it. This is an issue we ran into at climate.com where I'm working now, using Abe's Test Factory page object gem. we got around it by fist checking the type of the jQuery object

Our code (which yes uses fixed timeouts, we'll be changing that, and only supports jquery) looks like this

def wait_for_jquery
  #should wait until jquery reports it is no longer active
  i=0
  until @browser.execute_script("return typeof jQuery") === "function" || i>300
    sleep 0.1
    i+=1
  end
  if i >=300
    jquery_type = @browser.execute_script("return typeof jQuery")
    raise "jQuery was not initialized on page after 30 seconds. Type of JQuery after this time: '#{jquery_type}'"
  end
  Watir::Wait.until { @browser.execute_script("return jQuery.active == 0") }
end

I'd suggest you steal the first part of that and use something similar in your code.

Chuckv avatar Mar 21 '14 19:03 Chuckv

@Chuckv have you had instances where the framework was not initialized?

cheezy avatar Jun 01 '14 10:06 cheezy

sorry never saw cheezy's question on this. yeah I've had cases where I tried to check too early on the state of jquery.active and got an error. so by 'framework' in the above I meant jquery itself. not the page objects stuff. the above code first makes sure that the jQuery code has been activated and javascript has access to the jquery functions before it tries to make calls to jquery.active to determine if the page has open/pending ajax requests

Chuckv avatar Apr 15 '15 19:04 Chuckv

I am reopening this issue because I've seen a similar error with angular

Selenium::WebDriver::Error::UnknownError: unknown error: angular is not defined

AlexisKAndersen avatar Apr 24 '15 13:04 AlexisKAndersen

I am having issues with wait_for_ajax and jquery is not defined errors in my application. We use RequireJS to load all scripts, including jquery.

bojingo avatar Jun 29 '16 17:06 bojingo

So yeah, that was the sort of issue I was having on the client. I had pages that were basically set of empty containers, most of which got filled via AJAX calls. So I needed to use jQuery.active to tell if there were requests still pending, and wait for those to be completed before trying to interact with the page.

Hence the placing of this logic ahead of where I tried to call on a jquery function

i=0
  until @browser.execute_script("return typeof jQuery") === "function" || i>300
    sleep 0.1
    i+=1
  end
  if i >=300
    jquery_type = @browser.execute_script("return typeof jQuery")
    raise "jQuery was not initialized on page after 30 seconds. Type of JQuery after this time: '#{jquery_type}'"
  end

The 30 second wait there is likely excessive, I suspect I could have reduced it to around 5 seconds and it would have worked just fine.

Actually (and sorry I don't have this code with me) because we had two series of AJAX calls on some pages, (the second based on data returned by the first) I ended up having to put in logic that not only waited for jQuery.active to go to zero, but to stay there for about a half second, in order to stop the script from trying to proceed after the first set of ajax calls completed, but before the second set of calls was made.

Chuckv avatar Jun 29 '16 18:06 Chuckv

@Chuckv - actually, turns out your code didn't work because even a call to execute_script checks if jquery is loaded and results in Selenium::WebDriver::Error::UnknownError jquery is not defined.

As a hack solution, I put this in my base page object:

  def wait_for_ajax(timeout = 60, message = nil)
    wait_for_jquery(timeout / 2, message)
    super(timeout / 2, message)
  end

  def wait_for_jquery(timeout = 30, message = nil)
    end_time = ::Time.now + timeout
    until ::Time.now > end_time
      begin
        return if @browser.execute_script('return jQuery ? 1 : 0;') == 1
        puts 'jQuery not yet loaded, waiting half a second...'
      rescue Selenium::WebDriver::Error::UnknownError => jquery_error
        puts "jQuery not yet loaded, or failed when checking if it loaded: #{jquery_error}\nWaiting half a second..."
      end
      sleep 0.5
    end
    message = 'Timed out waiting for jquery to load' unless message
    raise message
  end

Note that the "jQuery not yet loaded" message never gets output because it always falls down into the rescue until RequireJS finally loads jQuery (which never takes more than 1 iteration, so my default 30 second wait is likely excessive too)

bojingo avatar Jun 30 '16 23:06 bojingo

@bojingo As you found, you can't try to use jquery itself to see if it's loaded.. that's why the trick of asking the browser to tell us the 'type' of jQuery.. if it's loaded it will come back with a type of function, if not loaded the type will be undefined. The JS 'typeof' operator is never going to error (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof if you are not familiar with it)

Here's a fast demo via IRB, neither the blank browser, nor google's main page use jquery, so it comes back as 'undefined'. the jQuery site itself of course uses jquery, so by the time I can enter the command manually it's loaded, and typeof returns 'function' when you ask it what jQuery is.

execute_script would be worthless if it required jquery since a lot of sites don't use it. I think your code above failed because you did not include the javascript typeof operator and had just return jQuery

$irb
irb(main):001:0> require 'watir-webdriver'
=> true
irb(main):002:0> b = Watir::Browser.new :chrome
=> #<Watir::Browser:0x59a6340dbe4e6bde url="data:," title="">
irb(main):003:0> puts b.execute_script("return typeof jQuery")
undefined
=> nil
irb(main):004:0> b.goto 'google.com'
=> "http://google.com"
irb(main):005:0> puts b.execute_script("return typeof jQuery")
undefined
=> nil
irb(main):006:0> b.goto 'jquery.com'
=> "http://jquery.com"
irb(main):007:0> puts b.execute_script("return typeof jQuery")
function
=> nil

Chuckv avatar Jul 07 '16 17:07 Chuckv