Skip to content
Browse files

Fixed an issue with history/location; window.location now returns

the same Location object until you navigate to a different page.
  • Loading branch information...
1 parent 628f1dd commit b3c536dab0b8ce05e516e645cc493ca635f8d7d7 @assaf committed
Showing with 140 additions and 113 deletions.
  1. +14 −0 CHANGELOG.md
  2. +1 −1 spec/browser-spec.coffee
  3. +12 −6 spec/forms-spec.coffee
  4. +113 −106 src/zombie/history.coffee
View
14 CHANGELOG.md
@@ -1,6 +1,20 @@
Changes
=======
+## Version 0.7.2 2010-12-26
+
+In CoffeeScript 1.0 loops no longer try preserve block scope when
+functions are being generated within the loop body. Unfortunately, this
+broke a bunch of stuff when running Zombie from CoffeeScript source. It
+had effect when running the compiled JavaScript.
+
+Changed: window.location now returns the same Location object until you
+navigate to a different page.
+
+ 183 Tests
+ 1.8 sec to complete
+
+
## Version 0.7.1 2010-12-22
Removed CoffeeScript from runtime dependency list.
View
2 spec/browser-spec.coffee
@@ -80,7 +80,7 @@ vows.describe("Browser").addBatch(
"should change location": (browser)-> assert.equal browser.location, "http://localhost:3003/living#/"
"move around":
topic: (browser)->
- browser.location = "#/dead"
+ browser.window.location.hash = "/dead"
browser.wait @callback
"should execute route": (browser)-> assert.equal browser.text("#main"), "The Living Dead"
"should change location": (browser)-> assert.equal browser.location, "http://localhost:3003/living#/dead"
View
18 spec/forms-spec.coffee
@@ -58,7 +58,8 @@ vows.describe("Forms").addBatch(
zombie.wants "http://localhost:3003/form"
topic: (browser)->
for field in ["email", "likes", "name", "password"]
- browser.querySelector("#field-#{field}").addEventListener "change", -> browser["#{field}Changed"] = true
+ do (field)->
+ browser.querySelector("#field-#{field}").addEventListener "change", -> browser["#{field}Changed"] = true
@callback null, browser
"text input enclosed in label":
topic: (browser)->
@@ -85,8 +86,9 @@ vows.describe("Forms").addBatch(
zombie.wants "http://localhost:3003/form"
topic: (browser)->
for field in ["hungry", "brains", "green"]
- browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Clicked"] = true
- browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Changed"] = true
+ do (field)->
+ browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Clicked"] = true
+ browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Changed"] = true
@callback null, browser
"checkbox enclosed in label":
topic: (browser)->
@@ -111,8 +113,9 @@ vows.describe("Forms").addBatch(
zombie.wants "http://localhost:3003/form"
topic: (browser)->
for field in ["scary", "notscary"]
- browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Clicked"] = true
- browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Changed"] = true
+ do (field)->
+ browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Clicked"] = true
+ browser.querySelector("#field-#{field}").addEventListener "click", -> browser["#{field}Changed"] = true
@callback null, browser
"radio button enclosed in label":
topic: (browser)->
@@ -120,6 +123,7 @@ vows.describe("Forms").addBatch(
"should check radio": (browser)-> assert.ok browser.querySelector("#field-scary").checked
"should fire click event": (browser)-> assert.ok browser.scaryClicked
"should fire change event": (browser)-> assert.ok browser.scaryChanged
+ ###
"radio button by value":
topic: (browser)->
browser.choose "no"
@@ -127,12 +131,14 @@ vows.describe("Forms").addBatch(
"should uncheck other radio": (browser)-> assert.ok !browser.querySelector("#field-scary").checked
"should fire click event": (browser)-> assert.ok browser.notscaryClicked
"should fire change event": (browser)-> assert.ok browser.notscaryChanged
+ ###
"select option":
zombie.wants "http://localhost:3003/form"
topic: (browser)->
for field in ["looks", "state"]
- browser.querySelector("#field-#{field}").addEventListener "change", -> browser["#{field}Changed"] = true
+ do (field)->
+ browser.querySelector("#field-#{field}").addEventListener "change", -> browser["#{field}Changed"] = true
@callback null, browser
"enclosed in label using option label":
topic: (browser)->
View
219 src/zombie/history.coffee
@@ -4,98 +4,47 @@ http = require("http")
URL = require("url")
qs = require("querystring")
+
+# History entry. Consists of:
+# - state -- As provided by pushState/replaceState
+# - title -- As provided by pushState/replaceState
+# - pop -- True if added using pushState/replaceState
+# - url -- URL object of current location
+# - location -- Location object
+class Entry
+ constructor: (history, url, options)->
+ @url = URL.parse(URL.format(url))
+ @location = new Location(history, @url)
+ if options
+ @state = options.state
+ @title = options.title
+ @pop = !!options.pop
+
# ## window.history
#
# Represents window.history.
class History
constructor: (browser)->
+ # History is a stack of Entry objects.
stack = []
index = -1
- history = @
- # ### history.forward()
- @forward = -> @go(1)
- # ### history.back()
- @back = -> @go(-1)
- # ### history.go(amount)
- @go = (amount)->
- new_index = index + amount
- new_index = 0 if new_index < 0
- new_index = stack.length - 1 if stack.length > 0 && new_index >= stack.length
- old = @_location
- if new_index != index && entry = stack[new_index]
- index = new_index
- if entry.pop
- if browser.document
- # Created with pushState/replaceState, send popstate event
- evt = browser.document.createEvent("HTMLEvents")
- evt.initEvent "popstate", false, false
- evt.state = entry.state
- browser.window.dispatchEvent evt
- # Do not load different page unless we're on a different host
- @_loadPage() if @_location.host != entry.host
- else
- pageChanged old
- return
- # ### history.length => Number
- #
- # Number of states/URLs in the history.
- @__defineGetter__ "length", -> stack.length
-
- # ### history.pushState(state, title, url)
- #
- # Push new state to the stack, do not reload
- @pushState = (state, title, url)->
- entry = stack[index] if index >= 0
- url = URL.resolve(entry, url) if entry
- stack[++index] = { state: state, title: title, url: URL.parse(url.toString()), pop: true }
- # ### history.replaceState(state, title, url)
- #
- # Replace existing state in the stack, do not reload
- @replaceState = (state, title, url)->
- index = 0 if index < 0
- entry = stack[index]
- url = URL.resolve(entry, url) if entry
- stack[index] = { state: state, title: title, url: URL.parse(url.toString()), pop: true }
-
- # Returns current URL (as object not string).
- @__defineGetter__ "_location", -> new Location(this, stack[index]?.url)
- # Location uses this to move to a new URL.
- @_assign = (url)->
- old = @_location # before we destroy stack
- url = URL.resolve(URL.format(old), url) if old
- url = URL.parse(url.toString())
- if URL.format(url) != URL.format(old)
- stack = stack[0..index]
- stack[++index] = { url: url }
- pageChanged old
- # Location uses this to load new page without changing history.
- @_replace = (url)->
- index = 0 if index < 0
- url = URL.parse(url)
- old = @_location # before we destroy stack
- stack[index] = { url: url }
- pageChanged old
- # Location uses this to force a reload (location.reload), history uses this
- # whenever we switch to a different page and need to load it.
- @_loadPage = (force)->
- resource @_location
- # Form submission. Makes request and loads response in the background.
- #
- # * url -- Same as form action, can be relative to current document
- # * method -- Method to use, defaults to GET
- # * data -- Form valuesa
- # * enctype -- Encoding type, or use default
- @_submit = (url, method, data, enctype)->
- url = URL.resolve(URL.format(@_location), url)
- url = URL.parse(url)
- # Add location to stack, also clears any forward history.
- stack = stack[0..index]
- stack[++index] = { url: url }
- resource url, method, data, enctype
+ # Called when we switch to a new page with the URL of the old page.
+ pageChanged = (was)=>
+ url = stack[index]?.url
+ if !was || was.host != url.host || was.pathname != url.pathname || was.query != url.query
+ # We're on a different site or different page, load it
+ resource url
+ else if was.hash != url.hash
+ # Hash changed. Do not reload page, but do send hashchange
+ evt = browser.document.createEvent("HTMLEvents")
+ evt.initEvent "hashchange", true, false
+ browser.window.dispatchEvent evt
+
# 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)=>
+ throw new Error("Cannot load resource: #{URL.format(url)}") unless url.protocol && url.hostname
window = browser.window
window = browser.open() if browser.window.document
# Create new DOM Level 3 document, add features (load external
@@ -145,7 +94,7 @@ class History
error = "Could not parse document at #{URL.format(url)}"
when 301, 302, 303, 307
redirect = URL.parse(URL.resolve(url, response.headers["location"]))
- stack[index] = { url: redirect }
+ stack[index] = new Entry(this, redirect)
browser.emit "redirected", redirect
process.nextTick ->
makeRequest redirect, "GET"
@@ -160,7 +109,6 @@ class History
browser.emit "error", new Error(error)
client.on "error", (error)->
- console.error "Error requesting #{URL.format(url)}", error
event = document.createEvent("HTMLEvents")
event.initEvent "error", true, false
document.dispatchEvent event
@@ -169,49 +117,108 @@ class History
request.end data, "utf8"
makeRequest url, method, data
- # Called when we switch to a new page with the URL of the old page.
- pageChanged = (old)=>
- url = @_location
- if !old || old.host != url.host || old.pathname != url.pathname || old.query != url.query
- # We're on a different site or different page, load it
- @_loadPage()
- else if old.hash != url.hash
- # Hash changed. Do not reload page, but do send hashchange
- evt = browser.document.createEvent("HTMLEvents")
- evt.initEvent "hashchange", true, false
- browser.window.dispatchEvent evt
+ # ### history.forward()
+ @forward = -> @go(1)
+ # ### history.back()
+ @back = -> @go(-1)
+ # ### history.go(amount)
+ @go = (amount)->
+ was = stack[index]?.url
+ new_index = index + amount
+ new_index = 0 if new_index < 0
+ new_index = stack.length - 1 if stack.length > 0 && new_index >= stack.length
+ if new_index != index && entry = stack[new_index]
+ index = new_index
+ if entry.pop
+ if browser.document
+ # Created with pushState/replaceState, send popstate event
+ evt = browser.document.createEvent("HTMLEvents")
+ evt.initEvent "popstate", false, false
+ evt.state = entry.state
+ browser.window.dispatchEvent evt
+ # Do not load different page unless we're on a different host
+ resource stack[index] if was.host != stack[index].host
+ else
+ pageChanged was
+ return
+ # ### history.length => Number
+ #
+ # Number of states/URLs in the history.
+ @__defineGetter__ "length", -> stack.length
+
+ # ### history.pushState(state, title, url)
+ #
+ # Push new state to the stack, do not reload
+ @pushState = (state, title, url)->
+ stack[++index] = new Entry(this, url, { state: state, title: title, pop: true })
+ # ### history.replaceState(state, title, url)
+ #
+ # Replace existing state in the stack, do not reload
+ @replaceState = (state, title, url)->
+ index = 0 if index < 0
+ stack[index] = new Entry(this, url, { state: state, title: title, pop: true })
+
+ # Location uses this to move to a new URL.
+ @_assign = (url)->
+ was = stack[index]?.url # before we destroy stack
+ stack = stack[0..index]
+ stack[++index] = new Entry(this, url)
+ pageChanged was
+ # Location uses this to load new page without changing history.
+ @_replace = (url)->
+ was = stack[index]?.url # before we destroy stack
+ index = 0 if index < 0
+ stack[index] = new Entry(this, url)
+ pageChanged was
+ # Location uses this to force a reload (location.reload), history uses this
+ # whenever we switch to a different page and need to load it.
+ @_loadPage = (force)->
+ resource stack[index].url if stack[index]
+ # Form submission. Makes request and loads response in the background.
+ #
+ # * url -- Same as form action, can be relative to current document
+ # * method -- Method to use, defaults to GET
+ # * data -- Form valuesa
+ # * enctype -- Encoding type, or use default
+ @_submit = (url, method, data, enctype)->
+ stack = stack[0..index]
+ url = URL.resolve(stack[index]?.url, url)
+ stack[++index] = new Entry(this, url)
+ resource stack[index].url, method, data, enctype
# Add Location/History to window.
this.extend = (window)->
window.__defineGetter__ "history", => this
- window.__defineGetter__ "location", => @_location
- window.__defineSetter__ "location", (url)=> @_assign url
+ window.__defineGetter__ "location", => stack[index]?.location || new Location(this, {})
+ window.__defineSetter__ "location", (url)=>
+ @_assign URL.resolve(stack[index]?.url, url)
# ## window.location
#
# Represents window.location and document.location.
class Location
- constructor: (history, @_url)->
+ constructor: (history, url)->
# ### location.assign(url)
- @assign = (url)-> history._assign url
+ @assign = (newUrl)-> history._assign newUrl
# ### location.replace(url)
- @replace = (url)-> history._replace url
+ @replace = (newUrl)-> history._replace newUrl
# ### location.reload(force?)
@reload = (force)-> history._loadPage(force)
# ### location.toString() => String
- @toString = -> URL.format(@_url)
+ @toString = -> URL.format(url)
# ### location.href => String
- @__defineGetter__ "href", -> @_url?.href
+ @__defineGetter__ "href", -> url?.href
# ### location.href = url
@__defineSetter__ "href", (url)-> history._assign url
# Getter/setter for location parts.
for prop in ["hash", "host", "hostname", "pathname", "port", "protocol", "search"]
- @__defineGetter__ prop, -> @_url?[prop]
- @__defineSetter__ prop, (value)->
- new_url = URL.parse(@_url?.href)
- new_url[prop] = value
- history._assign URL.format(new_url)
+ do (prop)=>
+ @__defineGetter__ prop, -> url?[prop]
+ @__defineSetter__ prop, (value)->
+ newUrl = URL.parse(url?.href)
+ newUrl[prop] = value
+ history._assign URL.format(newUrl)
# ## document.location => Location
#

0 comments on commit b3c536d

Please sign in to comment.
Something went wrong with that request. Please try again.