Skip to content

Commit

Permalink
Fixes to eventloop and using resource queue for evaluating internal
Browse files Browse the repository at this point in the history
scripts to have all scripts evaluate in document order.
  • Loading branch information
assaf committed Jan 4, 2011
1 parent f6f7b2b commit 849d955
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 129 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Expand Up @@ -4,14 +4,17 @@ zombie.js-changelog(7) -- Changelog

### Version 0.8.8 2011-01-04

Fixed script execution order: now in document order even when mixing
internal and external scripts.

Fixed image submit (José Valim).

Ensure checkboxes are properly serialized (José Valim).

It should send first select option if none was chosen (José Valim).

232 Tests
3.4 sec to complete
231 Tests
3.3 sec to complete


### Version 0.8.7 2011-01-04
Expand Down
2 changes: 1 addition & 1 deletion doc/API.md
Expand Up @@ -379,7 +379,7 @@ events. The terminator can be a number, in which case that many events
are processed. It can be a function, which is called after each event;
processing stops when the function returns the value `false`.

### Event: 'drain'
### Event: 'done'
`function (browser) { }`

Emitted whenever the event queue goes back to empty.
Expand Down
2 changes: 1 addition & 1 deletion spec/browser-spec.coffee
Expand Up @@ -106,7 +106,7 @@ vows.describe("Browser").addBatch(
topic: ->
brains.ready =>
browser = new zombie.Browser
browser.on "drain", (browser)=> @callback null, browser
browser.on "done", (browser)=> @callback null, browser
browser.window.location = "http://localhost:3003/"
browser.wait()
"should fire done event": (browser)-> assert.ok browser.visit
Expand Down
99 changes: 50 additions & 49 deletions spec/script-spec.coffee
@@ -1,55 +1,25 @@
require "./helpers"
{ vows: vows, assert: assert, zombie: zombie, brains: brains } = require("vows")


brains.get "/scripted", (req, res)-> res.send """
<html>
<head>
<title>Whatever</title>
<script src="/jquery.js"></script>
</head>
<body>Hello World</body>
<script>
document.title = "Nice";
$(function() { $("title").text("Awesome") })
</script>
</html>
brains.get "/script/context", (req, res)-> res.send """
<script>var foo = 1;</script>
<script>foo = foo + 1;</script>
<script>document.title = foo;</script>
"""

brains.get "/living", (req, res)-> res.send """
brains.get "/script/order", (req, res)-> res.send """
<html>
<head>
<script src="/jquery.js"></script>
<script src="/sammy.js"></script>
<script src="/app.js"></script>
<title>Zero</title>
<script src="/script/order.js"></script>
</head>
<body>
<div id="main">
<a href="/dead">Kill</a>
<form action="#/dead" method="post">
<label>Email <input type="text" name="email"></label>
<label>Password <input type="password" name="password"></label>
<button>Sign Me Up</button>
</form>
</div>
<div class="now">Walking Aimlessly</div>
<script>
document.title = document.title + "Two";</script>
</body>
</html>
"""
brains.get "/app.js", (req, res)-> res.send """
Sammy("#main", function(app) {
app.get("#/", function(context) {
document.title = "The Living";
});
app.get("#/dead", function(context) {
context.swap("The Living Dead");
});
app.post("#/dead", function(context) {
document.title = "Signed up";
});
});
$(function() { Sammy("#main").run("#/") });
"""
brains.get "/script/order.js", (req, res)-> res.send "document.title = document.title + 'One'";

brains.get "/dead", (req, res)-> res.send """
<html>
Expand Down Expand Up @@ -94,18 +64,51 @@ brains.get "/script/append", (req, res)-> res.send """
</html>
"""

brains.get "/context", (req, res)-> res.send """
<script>var foo = 1;</script>
<script>foo = foo + 1;</script>
<script>document.title = foo;</script>
brains.get "/living", (req, res)-> res.send """
<html>
<head>
<script src="/jquery.js"></script>
<script src="/sammy.js"></script>
<script src="/app.js"></script>
</head>
<body>
<div id="main">
<a href="/dead">Kill</a>
<form action="#/dead" method="post">
<label>Email <input type="text" name="email"></label>
<label>Password <input type="password" name="password"></label>
<button>Sign Me Up</button>
</form>
</div>
<div class="now">Walking Aimlessly</div>
</body>
</html>
"""
brains.get "/app.js", (req, res)-> res.send """
Sammy("#main", function(app) {
app.get("#/", function(context) {
document.title = "The Living";
});
app.get("#/dead", function(context) {
context.swap("The Living Dead");
});
app.post("#/dead", function(context) {
document.title = "Signed up";
});
});
$(function() { Sammy("#main").run("#/") });
"""


vows.describe("Scripts").addBatch(
"script context":
zombie.wants "http://localhost:3003/context"
zombie.wants "http://localhost:3003/script/context"
"should be shared by all scripts": (browser)-> assert.equal browser.text("title"), "2"

"script order":
zombie.wants "http://localhost:3003/script/order"
"should run scripts in order regardless of source": (browser)-> assert.equal browser.text("title"), "ZeroOneTwo"

"adding script using document.write":
zombie.wants "http://localhost:3003/script/write"
"should run script": (browser)-> assert.equal browser.document.title, "Script document.write"
Expand All @@ -116,10 +119,8 @@ vows.describe("Scripts").addBatch(
"run without scripts":
topic: ->
browser = new zombie.Browser(runScripts: false)
browser.wants "http://localhost:3003/scripted", @callback
"should load document from server": (browser)-> assert.match browser.html(), /<body>Hello World/
"should not load external scripts": (browser)-> assert.isUndefined browser.window.jQuery
"should not run scripts": (browser)-> assert.equal browser.document.title, "Whatever"
browser.wants "http://localhost:3003/script/order", @callback
"should not run scripts": (browser)-> assert.equal browser.document.title, "Zero"

"run app":
zombie.wants "http://localhost:3003/living"
Expand Down
19 changes: 16 additions & 3 deletions src/zombie/browser.coffee
Expand Up @@ -83,7 +83,7 @@ class Browser extends require("events").EventEmitter
# Events
# ------

# ### browser.wait(callback)
# ### browser.wait(callback?)
# ### browser.wait(terminator, callback)
#
# Process all events from the queue. This method returns immediately, events
Expand All @@ -100,13 +100,26 @@ class Browser extends require("events").EventEmitter
# * function -- called after each event, stop processing when function
# returns false
#
# You can call this method with no arguments and simply listen to the `done`
# and `error` events.
#
# Events include timeout, interval and XHR `onreadystatechange`. DOM events
# are handled synchronously.
this.wait = (terminate, callback)->
if !callback
[callback, terminate] = [terminate, null]
eventloop.wait window, terminate, (error) =>
callback error, this if callback
if callback
onerror = (error)=>
@removeListener "error", onerror
@removeListener "done", ondone
callback error
ondone = (error)=>
@removeListener "error", onerror
@removeListener "done", ondone
callback null, this
@on "error", onerror
@on "done", ondone
eventloop.wait window, terminate
return

# ### browser.fire(name, target, calback?)
Expand Down
104 changes: 47 additions & 57 deletions src/zombie/eventloop.coffee
Expand Up @@ -54,11 +54,27 @@ class EventLoop
# Implements window.clearInterval using event queue
this.clearInterval = (handle)-> delete timers[handle] if timers[handle]?.interval

# Size of processing queue (number of ongoing tasks).
processing = 0
# Requests on wait that cannot be handled yet: there's no event in the
# queue, but we anticipate one (in-progress XHR request).
waiting = []
# Queue of events.
queue = []
# Called when done processing a request, and if we're done processing all
# requests, wake up any waiting callbacks.
wakeUp = ->
if --processing == 0
process.nextTick waiter while waiter = waiting.pop()

# ### perform(fn)
#
# Run the function as part of the event queue (calls to `wait` will wait for
# this function to complete). Function can be anything and is called
# synchronous with a `done` function; when it's done processing, it lets the
# event loop know by calling the done function.
this.perform = (fn)->
++processing
fn wakeUp
return

# ### wait(window, terminate, callback, intervals)
#
Expand All @@ -67,22 +83,9 @@ class EventLoop
# the callback with null, window; if any event fails, it calls the callback
# with the exception.
#
# With one argument, that argument is the callback. With two arguments, the
# first argument is a terminator and the last argument is the callback. The
# terminator is one of:
#
# * null -- process all events
# * number -- process that number of events
# * function -- called after each event, stop processing when function
# returns false
#
# Events include timeout, interval and XHR onreadystatechange. DOM events
# are handled synchronously.
this.wait = (window, terminate, callback, intervals)->
if !callback
intervals = callback
callback = terminate
terminate = null
process.nextTick =>
earliest = null
for handle, timer of timers
Expand All @@ -96,24 +99,26 @@ class EventLoop
if event
try
event()
done = false
if typeof terminate is "number"
--terminate
if terminate <= 0
process.nextTick -> callback null, window
return
done = true if terminate <= 0
else if typeof terminate is "function"
if terminate.call(window) == false
process.nextTick -> callback null, window
return
@wait window, terminate, callback, intervals
done = true if terminate.call(window) == false
if done
process.nextTick ->
browser.emit "done", browser
callback null, window if callback
else
@wait window, terminate, callback, intervals
catch err
browser.emit "error", err
callback err, window
else if queue.length > 0
callback err, window if callback
else if processing > 0
waiting.push => @wait window, terminate, callback, intervals
else
browser.emit "drain", browser
callback null, window
browser.emit "done", browser
callback null, window if callback

# Used internally for the duration of an internal request (loading
# resource, XHR). Also collects request/response for debugging.
Expand All @@ -122,49 +127,34 @@ class EventLoop
# next. After storing the request, that function is called with a single
# argument, a done callback. It must call the done callback when it
# completes processing, passing error and response arguments.
#
# See also `processing`.
this.request = (request, fn)->
url = request.url.toString()
browser.log -> "#{request.method} #{url}"
pending = browser.record request
this.queue (done)->
fn (err, response)->
if err
browser.log -> "Error loading #{url}: #{err}"
pending.error = err
else
browser.log -> "#{request.method} #{url} => #{response.status}"
pending.response = response
done()

queue = []
# ### queue(event)
#
# Queue an event to be processed by wait(). Event is a function call in the
# context of the window.
this.queue = (fn)->
queue.push fn
fn ->
queue.splice queue.indexOf(fn), 1
if queue.length == 0
for wait in waiting
process.nextTick -> wait()
waiting = []
++processing
fn (err, response)->
if err
browser.log -> "Error loading #{url}: #{err}"
pending.error = err
else
browser.log -> "#{request.method} #{url} => #{response.status}"
pending.response = response
wakeUp()

this.extend = (window)=>
for fn in ["setTimeout", "setInterval", "clearTimeout", "clearInterval"]
window[fn] = this[fn]
window.queue = this.queue
window.perform = this.perform
window.wait = (terminate, callback)=> this.wait(window, terminate, callback)
window.request = this.request

this.dump = ()->
dump = [ "The time: #{browser.clock}",
"Timers: #{timers.length}",
"Queue: #{queue.length}",
"Waiting: #{waiting.length}",
"Requests:"]
dump.push " #{request}" for request in requests
dump
[ "The time: #{browser.clock}",
"Timers: #{timers.length}",
"Processing: #{processing}",
"Waiting: #{waiting.length}" ]

exports.use = (browser)->
return new EventLoop(browser)
23 changes: 10 additions & 13 deletions src/zombie/jsdom_patches.coffee
Expand Up @@ -86,19 +86,16 @@ core.CharacterData.prototype.__defineGetter__ "_nodeValue", -> @_text
# when its text contents is changed. Safari and Firefox support that.
core.Document.prototype._elementBuilders["script"] = (doc, s)->
script = new core.HTMLScriptElement(doc, s)
script.addEventListener "DOMCharacterDataModified", (event)->
code = event.target.nodeValue
if code.trim().length > 0
src = @sourceLocation || {}
filename = src.file || @ownerDocument.URL
if src
filename += ':' + src.line + ':' + src.col
filename += '<script>'
eval = (text, filename)->
if text == @text && @ownerDocument.implementation.hasFeature("ProcessExternalResources", "script")
core.languageProcessors[@language](this, text, filename)
process.nextTick =>
core.resourceLoader.enqueue(this, eval, filename)(null, code)
if doc.implementation.hasFeature("ProcessExternalResources", "script")
script.addEventListener "DOMCharacterDataModified", (event)->
code = event.target.nodeValue
if code.trim().length > 0
filename = @ownerDocument.URL
@ownerDocument.parentWindow.perform (done)=>
loaded = (code, filename)=>
core.languageProcessors[@language](this, code, filename) if code == @text
done()
core.resourceLoader.enqueue(this, loaded, filename)(null, code)
return script


Expand Down

0 comments on commit 849d955

Please sign in to comment.