Permalink
Browse files

The `wait` method now takes two optional parameters: options that det…

…ermine how

long to wait and a callback. Options include `duration`, `function` and
`element`, the later completing when the element is found in the document.

The `wait` method's wait function now receives two parameters: the active window
and how long the wait is for the next event (0 to Infinity).

The `visit` method now passes applicable options to `wait` (`duration`,
`function`, etc).

The `maxWait` option is now called `waitDuration` and `waitFor` is no longer
supported.
  • Loading branch information...
1 parent 2176d02 commit 97f5c4f5ca6c723dc745decac53a7a8bf8fd4570 @assaf committed Oct 19, 2012
Showing with 119 additions and 77 deletions.
  1. +14 −1 CHANGELOG.md
  2. +67 −37 lib/zombie/browser.coffee
  3. +37 −38 lib/zombie/eventloop.coffee
  4. +1 −1 test/google_map_test.coffee
View
@@ -31,8 +31,21 @@ Added `browser.activeElement` (removed `browser.focus`) and support for
Browser is now an `EventEmitter` so you can hook into it and instrument it.
+The `wait` method now takes two optional parameters: options that determine how
+long to wait and a callback. Options include `duration`, `function` and
+`element`, the later completing when the element is found in the document.
+
+The `wait` method's wait function now receives two parameters: the active window
+and how long the wait is for the next event (0 to Infinity).
+
+The `visit` method now passes applicable options to `wait` (`duration`,
+`function`, etc).
+
+The `maxWait` option is now called `waitDuration` and `waitFor` is no longer
+supported.
+
566 tests
- 11.0 sec to complete
+ 9.2 sec to complete
## Version 1.4.1 2012-08-22
@@ -15,6 +15,7 @@ FS = require("fs")
Interact = require("./interact")
JSDOM = require("jsdom")
Mime = require("mime")
+ms = require("ms")
Q = require("q")
Path = require("path")
Resources = require("./resources")
@@ -24,13 +25,13 @@ XHR = require("./xhr")
# Browser options you can set when creating new browser, or on browser instance.
-BROWSER_OPTIONS = ["debug", "headers", "htmlParser", "loadCSS", "maxWait",
+BROWSER_OPTIONS = ["debug", "headers", "htmlParser", "loadCSS", "waitDuration",
"proxy", "referer", "runScripts", "silent", "site", "userAgent",
- "waitFor", "maxRedirects"]
+ "maxRedirects"]
# Global options you can set on Browser and will be inherited by each new browser.
-GLOBAL_OPTIONS = ["debug", "headers", "htmlParser", "loadCSS", "maxWait", "proxy",
- "runScripts", "silent", "site", "userAgent", "waitFor", "maxRedirects"]
+GLOBAL_OPTIONS = ["debug", "headers", "htmlParser", "loadCSS", "waitDuration", "proxy",
+ "runScripts", "silent", "site", "userAgent", "maxRedirects"]
PACKAGE = JSON.parse(require("fs").readFileSync(__dirname + "/../../package.json"))
@@ -186,8 +187,8 @@ class Browser extends EventEmitter
# True to load external stylesheets.
@loadCSS: true
- # Maximum time to wait (visit, wait, etc).
- @maxWait: "5s"
+ # Default time to wait (visit, wait, etc).
+ @waitDuration: "5s"
# Proxy URL.
#
@@ -209,9 +210,6 @@ class Browser extends EventEmitter
# You can use visit with a path, and it will make a request relative to this host/URL.
@site: undefined
- # Tells `wait` and any function that uses `wait` how long to wait for, executing timers. Defaults to 0.5 seconds.
- @waitFor: "0.5s"
-
# Tells the browser how many redirects to follow before aborting a request. Defaults to 5
@maxRedirects: 5
@@ -270,39 +268,72 @@ class Browser extends EventEmitter
# Waits for the browser to complete loading resources and processing JavaScript events.
#
- # You can pass a callback as the last argument. Without a callback, this method returns a promise.
+ # Accepts two parameters, both optional:
+ # options - Options that determine how long to wait (see below)
+ # callback - Called with error or null and browser
+ #
+ # To determine how long to wait:
+ # duration - Do not wait more than this duration (milliseconds or string of
+ # the form "5s"). Defaults to "5s" (see Browser.waitDuration).
+ # element - Stop when this element(s) appear in the DOM.
+ # function - Stop when function returns true; this function is called with
+ # the active window and expected time to the next event (0 to
+ # Infinity).
#
- # With `duration` as the first argument, this method waits for the specified time (in milliseconds) and any
- # resource/JavaScript to complete processing. Duration can also be a function, called after each event to determine
- # whether or not to proceed.
+ # As a convenience you can also pass the duration directly.
#
- # Without duration, Zombie makes best judgement by waiting up to 5 seconds for the page to load resources (scripts,
- # XHR requests, iframes), process DOM events, and fire timeouts events.
- wait: (duration, callback)->
+ # Without a callback, this method returns a promise.
+ wait: (options, callback)->
unless @window
- callback new Error("No window open")
+ process.nextTick ->
+ callback new Error("No window open")
return
- if arguments.length < 2 && typeof duration == "function"
- [callback, duration] = [duration, null]
- if typeof duration == "function"
- [completion, duration] = [duration, null]
- duration ||= @maxWait
-
- deferred = Q.defer()
- if callback
- deferred.promise.then(callback, callback)
- last = @errors[@errors.length - 1]
+ if arguments.length == 1 && typeof(options) == "function"
+ [callback, options] = [options, null]
- @_eventLoop.wait duration, completion, (error, timedOut)=>
- newest = @errors[@errors.length - 1]
- unless error || last == newest
- error = newest
- if error
+ if callback && typeof(callback) != "function"
+ throw new Error("Second argument expected to be a callback function or null")
+ # Support all sort of shortcuts for options. Unofficial.
+ if typeof(options) == "number"
+ waitDuration = options
+ else if typeof(options) == "string"
+ waitDuration = options
+ else if typeof(options) == "function"
+ waitDuration = @waitDuration
+ waitFunction = options
+ else if options
+ waitDuration = options.duration || @waitDuration
+ if options.element
+ waitFunction = (window)->
+ return !!window.document.querySelector(options.element)
+ else
+ waitFunction = options.function
+ else
+ waitDuration = @waitDuration
+
+ unless callback
+ deferred = Q.defer()
+
+ # Catch errors sent to browser but not propagated to event-loop wait
+ lastError = null
+ catchError = (error)->
+ lastError ||= error
+ @addListener "error", catchError
+
+ @_eventLoop.wait waitDuration, waitFunction, (error, timedOut)=>
+ error ||= lastError
+ @removeListener "error", catchError
+ if callback
+ callback(error, this)
+ else if error
deferred.reject(error)
else
deferred.resolve()
- return deferred.promise unless callback
+
+ if deferred
+ return deferred.promise
+
# Fire a DOM event. You can use this to simulate a DOM event, e.g. clicking a link. These events will bubble up and
# can be cancelled. Like `wait` this method either takes a callback or returns a promise.
@@ -481,8 +512,6 @@ class Browser extends EventEmitter
visit: (url, options, callback)->
if typeof options == "function" && !callback
[callback, options] = [options, null]
- if typeof options != "object"
- [duration, options] = [options, null]
deferred = Q.defer()
reset_options = @withOptions(options)
@@ -494,7 +523,7 @@ class Browser extends EventEmitter
@window.location = url
else
this.open(url: url)
- @wait duration, (error)=>
+ @wait options, (error)=>
reset_options()
if error
deferred.reject(error)
@@ -749,7 +778,8 @@ class Browser extends EventEmitter
unless field && field.tagName == "INPUT" && field.type == "radio"
throw new Error("No radio INPUT matching '#{selector}'")
field.click()
- @wait callback if callback
+ if callback
+ @wait(callback)
return this
_findOption: (selector, value)->
@@ -67,55 +67,54 @@ class EventLoop
# Duration is specifies in milliseconds or string form (e.g. "15s").
#
# Completion function is called with the currently active window (may change
- # during page navigation or form submission) and returns true to stop waiting,
- # any other value to continue processing events.
- wait: (duration, completion, callback)->
- # Determines how long we're going to wait
- duration = ms(duration.toString())
- waitFor = ms(@browser.waitFor.toString())
- timeout = Date.now() + waitFor
-
- if @listeners.length == 0
- # Someone's paying attention, start processing events
- process.nextTick =>
- if @active
- @active._eventQueue.resume()
- @run()
+ # during page navigation or form submission) and how long until the next
+ # event, and returns true to stop waiting, any other value to continue
+ # processing events.
+ wait: (waitDuration, waitFunction, callback)->
+ # Don't wait longer than duration
+ waitDuration = ms(waitDuration.toString()) || @browser.waitDuration
+ timeoutTimer = global.setTimeout(->
+ done(null, true)
+ , waitDuration)
+ timeout = Date.now() + waitDuration
+
+ # Remove this listener and pass control to callback
+ done = (error, timedOut)=>
+ clearTimeout(timeoutTimer)
+ @listeners = @listeners.filter((l)-> l != listener)
+ if @listeners.length == 0
+ @emit("done")
+ callback(error, !!timedOut)
# Receive tick, done and error events
- listener = (event, value)=>
+ listener = (event)=>
switch event
when "tick"
- # Event processed, are we ready to complete?
- if completion
- try
- completed = completion(@active)
+ try
+ next = arguments[1]
+ if next >= timeout
+ # No events in the queue, no point waiting
+ done(null, true)
+ else if waitFunction
+ waitFor = Math.max(next - Date.now(), 0)
+ # Event processed, are we ready to complete?
+ completed = waitFunction(@active, waitFor)
if completed
done(null, false)
- return
- catch error
- done(error)
-
- # Should we keep waiting for next timer?
- if value >= waitFor + Date.now()
- done(null, true)
+ catch error
+ done(error)
when "done"
done()
when "error"
- done(value)
+ done(error = arguments[1])
@listeners.push(listener)
- timer = global.setTimeout(->
- done(null, true)
- , duration)
-
- # Cleanup listeners and times before calling callback
- done = (error, timedOut)=>
- clearTimeout(timer)
- @listeners = @listeners.filter((l)-> l != listener)
- if @listeners.length == 0
- @emit("done")
- callback(error, !!timedOut)
+ # Someone (us) just started paying attention, start processing events
+ if @listeners.length == 1
+ process.nextTick =>
+ if @active
+ @active._eventQueue.resume()
+ @run()
return
@@ -29,7 +29,7 @@ describe "Google map", ->
before (done)->
@browser = new Browser()
- @browser.visit "http://localhost:3003/browser/map", done
+ @browser.visit("http://localhost:3003/browser/map", element: ".gmnoprint", done)
it "should load map", ->
assert @browser.window.map

0 comments on commit 97f5c4f

Please sign in to comment.