Permalink
Browse files

Fixed: browser now creates new window for each new document.

  • Loading branch information...
1 parent 36a44cd commit 01eaf25eff500d83410dd1df70522c0580828ef8 @assaf committed Dec 20, 2010
Showing with 139 additions and 130 deletions.
  1. +5 −0 CHANGELOG
  2. +65 −62 lib/zombie/browser.coffee
  3. +16 −18 lib/zombie/eventloop.coffee
  4. +24 −28 lib/zombie/history.coffee
  5. +22 −21 lib/zombie/storage.coffee
  6. +1 −1 spec/cookie-spec.coffee
  7. +6 −0 spec/history-spec.coffee
View
5 CHANGELOG
@@ -4,6 +4,11 @@ Version 0.6.1 Pending
(host and port) and path, and returns wrapper to access specific cookie
context.
+ Fixed: browser now creates new window for each new document.
+
+ 178 tests.
+ 1.8 sec to complete.
+
Version 0.6.0 2010-12-20
First release that I could use to test an existing project.
View
127 lib/zombie/browser.coffee
@@ -8,78 +8,28 @@ require "./forms"
# The browser maintains state for cookies and localStorage.
class Browser
constructor: ->
- # Cookies and storage.
cookies = require("./cookies").use(this)
storage = require("./storage").use(this)
- # ### browser.cookies(host, path) => Cookies
- #
- # Returns all the cookies for this host/path. Path defaults to "/".
- this.cookies = (host, path)-> cookies.access(host, path)
- # ### brower.localStorage(host) => Storage
- #
- # Returns local Storage based on the document origin (hostname/port). This
- # is the same storage area you can access from any document of that origin.
- this.localStorage = (host)-> storage.local(host)
- # ### brower.sessionStorage(host) => Storage
- #
- # Returns session Storage based on the document origin (hostname/port). This
- # is the same storage area you can access from any document of that origin.
- this.sessionStorage = (host)-> storage.session(host)
-
- # Event loop.
eventloop = require("./eventloop").use(this)
- # ### browser.clock
- #
- # The current clock time. Initialized to current system time when creating
- # a new browser, but doesn't advance except by setting it explicitly or
- # firing timeout/interval events.
- @clock = new Date().getTime()
- # ### browser.now => Date
- #
- # Date object with current time, based on browser clock.
- @__defineGetter__ "now", -> new Date(clock)
-
- # History, page loading, XHR.
history = require("./history").use(this)
xhr = require("./xhr").use(this)
- # ### browser.location => Location
- #
- # Return the location of the current document (same as window.location.href).
- @__defineGetter__ "location", -> window.location.href
- # ### browser.location = url
- #
- # Changes document location, loads new document if necessary (same as
- # setting window.location).
- @__defineSetter__ "location", (url)-> window.location = url
-
- # Window management.
- createWindow = ->
+ window = null
+ # Open new browser window.
+ this.open = ->
window = jsdom.createWindow(jsdom.dom.level3.html)
window.__defineGetter__ "browser", => this
cookies.extend window
storage.extend window
eventloop.extend window
history.extend window
xhr.extend window
- window
-
- # ### browser.window => Window
- #
- # Returns the main window.
- @__defineGetter__ "window", -> window
- # ### browser.document => Document
- #
- # Retursn the main window's document. Only valid after opening a document
- # (Browser.open).
- @__defineGetter__ "document", -> window.document
-
-
- window = createWindow()
-
- # TODO: Fix
- window.Image = ->
+ # TODO: Fix
+ window.Image = ->
+ return window
+ # Always start with an open window.
+ @open()
# Events
@@ -107,7 +57,7 @@ class Browser
if !callback
callback = terminate
terminate = null
- window.wait terminate, (err) => callback err, this
+ eventloop.wait window, terminate, (err) => callback err, this
return
# ### browser.fire name, target, calback?
@@ -125,6 +75,17 @@ class Browser
target.dispatchEvent event
@wait callback if callback
+ # ### browser.clock
+ #
+ # The current clock time. Initialized to current system time when creating
+ # a new browser, but doesn't advance except by setting it explicitly or
+ # firing timeout/interval events.
+ @clock = new Date().getTime()
+ # ### browser.now => Date
+ #
+ # Date object with current time, based on browser clock.
+ @__defineGetter__ "now", -> new Date(clock)
+
# Accessors
# ---------
@@ -166,14 +127,23 @@ class Browser
else
return window.document.outerHTML
+ # ### browser.window => Window
+ #
+ # Returns the main window.
+ @__defineGetter__ "window", -> window
+ # ### browser.document => Document
+ #
+ # Retursn the main window's document. Only valid after opening a document
+ # (Browser.open).
+ @__defineGetter__ "document", -> window?.document
# ### browser.body => Element
#
# Returns the body Element of the current document.
@__defineGetter__ "body", -> window.document?.find("body")[0]
- # Actions
- # -------
+ # Navigation
+ # ----------
# ### browser.visit url, callback
#
@@ -182,11 +152,25 @@ class Browser
#
# If it fails to download, calls the callback with the error.
this.visit = (url, callback)->
- window.location = url
+ history._assign url
window.addEventListener "error", (err)-> callback err
window.document.addEventListener "DOMContentLoaded", => @wait callback
return
+ # ### browser.location => Location
+ #
+ # Return the location of the current document (same as window.location.href).
+ @__defineGetter__ "location", -> window.location.href
+ # ### browser.location = url
+ #
+ # Changes document location, loads new document if necessary (same as
+ # setting window.location).
+ @__defineSetter__ "location", (url)-> window.location = url
+
+
+ # Forms
+ # -----
+
# ### browser.clickLink selector, callback
#
# Clicks on a link. Clicking on a link can trigger other events, load new #
@@ -353,6 +337,25 @@ class Browser
throw new Error("No BUTTON '#{name}'")
+ # Cookies and storage
+ # -------------------
+
+ # ### browser.cookies(host, path) => Cookies
+ #
+ # Returns all the cookies for this host/path. Path defaults to "/".
+ this.cookies = (host, path)-> cookies.access(host, path)
+ # ### brower.localStorage(host) => Storage
+ #
+ # Returns local Storage based on the document origin (hostname/port). This
+ # is the same storage area you can access from any document of that origin.
+ this.localStorage = (host)-> storage.local(host)
+ # ### brower.sessionStorage(host) => Storage
+ #
+ # Returns session Storage based on the document origin (hostname/port). This
+ # is the same storage area you can access from any document of that origin.
+ this.sessionStorage = (host)-> storage.session(host)
+
+
# Debugging
# ---------
View
34 lib/zombie/eventloop.coffee
@@ -3,7 +3,7 @@ URL = require("url")
# Handles the Window event loop, timers and pending requests.
class EventLoop
- constructor: (browser, window)->
+ constructor: (browser)->
timers = {}
lastHandle = 0
@@ -14,10 +14,10 @@ class EventLoop
timer =
when: browser.clock + delay
timeout: true
- fire: ->
+ fire: =>
try
if typeof fn == "function"
- fn.apply(window)
+ fn.apply this
else
eval fn
finally
@@ -33,10 +33,10 @@ class EventLoop
timer =
when: browser.clock + delay
interval: true
- fire: ->
+ fire: =>
try
if typeof fn == "function"
- fn.apply(window)
+ fn.apply this
else
eval fn
finally
@@ -82,7 +82,7 @@ class EventLoop
#
# Events include timeout, interval and XHR onreadystatechange. DOM events
# are handled synchronously.
- this.wait = (terminate, callback, intervals)->
+ this.wait = (window, terminate, callback, intervals)->
if !callback
intervals = callback
callback = terminate
@@ -102,7 +102,7 @@ class EventLoop
earliest.fire()
if event
try
- event.call(window)
+ event()
if typeof terminate is "number"
--terminate
if terminate <= 0
@@ -112,11 +112,11 @@ class EventLoop
if terminate.call(window) == false
process.nextTick -> callback null, window
return
- @wait terminate, callback, intervals
+ @wait window, terminate, callback, intervals
catch err
callback err, window
else if requests > 0
- waiting.push => @wait terminate, callback, intervals
+ waiting.push => @wait window, terminate, callback, intervals
else
callback null, window
@@ -142,14 +142,12 @@ class EventLoop
process.nextTick -> wait()
waiting = []
+ this.extend = (window)=>
+ for fn in ["setTimeout", "setInterval", "clearTimeout", "clearInterval"]
+ window[fn] = this[fn]
+ window.queue = this.queue
+ window.wait = (terminate, callback)=> this.wait(window, terminate, callback)
+ window.request = this.request
exports.use = (browser)->
- # Add event loop to window.
- extend = (window)->
- eventLoop = new EventLoop(browser, window)
- for fn in ["setTimeout", "setInterval", "clearTimeout", "clearInterval"]
- window[fn] = -> eventLoop[fn].apply(window, arguments)
- window.queue = eventLoop.queue
- window.wait = eventLoop.wait
- window.request = eventLoop.request
- return extend: extend
+ return new EventLoop(browser)
View
52 lib/zombie/history.coffee
@@ -8,7 +8,7 @@ qs = require("querystring")
#
# Represents window.history.
class History
- constructor: (browser, window)->
+ constructor: (browser)->
stack = []
index = -1
history = @
@@ -25,14 +25,14 @@ class History
if new_index != index && entry = stack[new_index]
index = new_index
if entry.pop
- if window.document
+ if browser.document
# Created with pushState/replaceState, send popstate event
- evt = window.document.createEvent("HTMLEvents")
+ evt = browser.document.createEvent("HTMLEvents")
evt.initEvent "popstate", false, false
evt.state = entry.state
- window.dispatchEvent evt
+ browser.window.dispatchEvent evt
# Do not load different page unless we're on a different host
- @_loadPage() if window.location.host != entry.host
+ @_loadPage() if @_location.host != entry.host
else
pageChanged old
return
@@ -94,18 +94,16 @@ class History
# Make a request to external resource. We use this to fetch pages and
# submit forms, see _loadPage and _submit.
resource = (url, method = "GET", data, enctype)=>
- if window.document && window.document.readyState == "loading"
- # Redirecting, so reuse existing window.
- document = window.document
- else
- # Create new DOM Level 3 document, add features (load external
- # resources, etc) and associate it with current document. From this
- # point on the browser sees a new document, client register event
- # handler for DOMContentLoaded/error.
- aug = jsdom.browserAugmentation(jsdom.dom.level3.html)
- document = new aug.HTMLDocument(url: URL.format(url), deferClose: true)
- jsdom.applyDocumentFeatures document
- window.document = document
+ window = browser.window
+ window = browser.open() if browser.window.document
+ # Create new DOM Level 3 document, add features (load external
+ # resources, etc) and associate it with current document. From this
+ # point on the browser sees a new document, client register event
+ # handler for DOMContentLoaded/error.
+ aug = jsdom.browserAugmentation(jsdom.dom.level3.html)
+ document = new aug.HTMLDocument(url: URL.format(url), deferClose: true)
+ jsdom.applyDocumentFeatures document
+ window.document = document
# Make the actual request: called again when dealing with a redirect.
makeRequest = (url, method, data)=>
@@ -172,9 +170,15 @@ class History
@_loadPage()
else if old.hash != url.hash
# Hash changed. Do not reload page, but do send hashchange
- evt = window.document.createEvent("HTMLEvents")
+ evt = browser.document.createEvent("HTMLEvents")
evt.initEvent "hashchange", true, false
- window.dispatchEvent evt
+ browser.window.dispatchEvent evt
+
+ # Add Location/History to window.
+ this.extend = (window)->
+ window.__defineGetter__ "history", => this
+ window.__defineGetter__ "location", => @_location
+ window.__defineSetter__ "location", (url)=> @_assign url
# ## window.location
@@ -208,12 +212,4 @@ jsdom.dom.level3.core.HTMLDocument.prototype.__defineGetter__ "location", -> @pa
exports.use = (browser)->
- # Add Location/History to window: creates new history and adds
- # location/history accessors.
- extend = (window)->
- history = new History(browser, window)
- window.__defineGetter__ "history", -> history
- window.__defineSetter__ "history", (history)-> # runInNewContext needs this
- window.__defineGetter__ "location", => history._location
- window.__defineSetter__ "location", (url)=> history._assign url
- return extend: extend
+ return new History(browser)
View
43 lib/zombie/storage.coffee
@@ -86,25 +86,26 @@ Storage.prototype.__proto__ = events.Event.prototype
core.SECURITY_ERR = 18
-# Creates local/session storage and returns an object with three functions:
-# - local(host) to access local storage by host
-# - session(host) to access session storage by host
-# - extend(window) to add local/session storage support to window
+# Combined local/session storage.
+class Storages
+ constructor: (browser)->
+ localAreas = {}
+ sessionAreas = {}
+ # Return local Storage based on the document origin (hostname/port).
+ this.local = (host)->
+ area = localAreas[host] ?= new StorageArea()
+ new Storage(area)
+ # Return session Storage based on the document origin (hostname/port).
+ this.session = (host)->
+ area = sessionAreas[host] ?= new StorageArea()
+ new Storage(area)
+ # Extend window with local/session storage support.
+ this.extend = (window)->
+ window.__defineGetter__ "sessionStorage", ->
+ @document._sessionStorage ||= browser.sessionStorage(@location.host)
+ window.__defineGetter__ "localStorage", ->
+ @document._localStorage ||= browser.localStorage(@location.host)
+
+
exports.use = (browser)->
- localAreas = {}
- sessionAreas = {}
- # Return local Storage based on the document origin (hostname, port).
- local = (host)->
- area = localAreas[host] ?= new StorageArea()
- new Storage(area)
- # Return session Storage based on the document origin (hostname, port).
- session = (host)->
- area = sessionAreas[host] ?= new StorageArea()
- new Storage(area)
- # Extend window with local/session storage support.
- extend = (window)->
- window.__defineGetter__ "sessionStorage", ->
- @document._sessionStorage ||= browser.sessionStorage(@location.host)
- window.__defineGetter__ "localStorage", ->
- @document._localStorage ||= browser.localStorage(@location.host)
- return local: local, session: session, extend: extend
+ return new Storages(browser)
View
2 spec/cookie-spec.coffee
@@ -5,7 +5,7 @@ brains.get "/cookies", (req, res)->
res.cookie "_name", "value"
res.cookie "_expires1", "3s", "Expires": new Date(Date.now() + 3000)
res.cookie "_expires2", "5s", "Max-Age": 5000
- res.cookie "_expires3", "0s", "Expires": new Date()
+ res.cookie "_expires3", "0s", "Expires": new Date(Date.now() - 100)
res.cookie "_expires4", "0s", "Max-Age": 0
res.cookie "_path1", "yummy", "Path": "/cookies"
res.cookie "_path2", "yummy", "Path": "/cookies/sub"
View
6 spec/history-spec.coffee
@@ -108,28 +108,34 @@ vows.describe("History").addBatch(
"assign":
zombie.wants "http://localhost:3003/"
topic: (browser)->
+ @window = browser.window
browser.window.location.assign "http://localhost:3003/boo"
browser.document.addEventListener "DOMContentLoaded", => @callback null, browser
"should add page to history": (browser)-> assert.length browser.window.history, 2
"should change location URL": (browser)-> assert.equal browser.location, "http://localhost:3003/boo"
"should load document": (browser)-> assert.match browser.html(), /Eeek!/
+ "should load document in new window": (browser)-> assert.ok browser.window != @window
"replace":
zombie.wants "http://localhost:3003/"
topic: (browser)->
+ @window = browser.window
browser.window.location.replace "http://localhost:3003/boo"
browser.window.document.addEventListener "DOMContentLoaded", => @callback null, browser
"should not add page to history": (browser)-> assert.length browser.window.history, 1
"should change location URL": (browser)-> assert.equal browser.location, "http://localhost:3003/boo"
"should load document": (browser)-> assert.match browser.html(), /Eeek!/
+ "should load document in new window": (browser)-> assert.ok browser.window != @window
"reload":
zombie.wants "http://localhost:3003/"
topic: (browser)->
+ @window = browser.window
browser.window.document.innerHTML = "Wolf"
browser.window.location.reload()
browser.window.document.addEventListener "DOMContentLoaded", => @callback null, browser
"should not add page to history": (browser)-> assert.length browser.window.history, 1
"should not change location URL": (browser)-> assert.equal browser.location, "http://localhost:3003/"
"should reload document": (browser)-> assert.match browser.html(), /Tap, Tap/
+ "should reload document in new window": (browser)-> assert.ok browser.window != @window
"redirect":
zombie.wants "http://localhost:3003/redirect"

0 comments on commit 01eaf25

Please sign in to comment.