From 6649d8aac3b00c8ef7b2674ba82d53708cac25b5 Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Mon, 1 Dec 2014 20:52:34 -0800 Subject: [PATCH 1/6] ADDED wait() returns a lazy promise --- src/zombie/browser.coffee | 9 ++- src/zombie/eventloop.coffee | 131 +++++++++++++++++++++--------------- test/browser_object_test.js | 2 +- test/event_loop_test.js | 114 ++++++++++++++++++++++++++++--- test/history_test.js | 21 +++--- test/websocket_test.js | 2 +- 6 files changed, 201 insertions(+), 78 deletions(-) diff --git a/src/zombie/browser.coffee b/src/zombie/browser.coffee index d422ecaf3..c7a3c2437 100644 --- a/src/zombie/browser.coffee +++ b/src/zombie/browser.coffee @@ -507,7 +507,9 @@ class Browser extends EventEmitter if typeof options == "function" && !callback [callback, options] = [options, null] - resetOptions = @withOptions(options) + if options + resetOptions = @withOptions(options) + if site = @site site = "http://#{site}" unless /^(https?:|file:)/i.test(site) url = URL.resolve(site, URL.parse(URL.format(url))) @@ -516,7 +518,10 @@ class Browser extends EventEmitter @tabs.close(@window) @tabs.open(url: url, referer: @referer) - promise = @wait(options).finally(resetOptions) + if options + promise = @wait(options).finally(resetOptions) + else + promise = @wait(options) if callback promise.done(callback, callback) return diff --git a/src/zombie/eventloop.coffee b/src/zombie/eventloop.coffee index c7921a481..535ecbf92 100644 --- a/src/zombie/eventloop.coffee +++ b/src/zombie/eventloop.coffee @@ -22,6 +22,27 @@ ms = require("ms") { Promise } = require("bluebird") +# Returns a Bluebird promise that evaluates only when registering a callback +# (via then, catch, done, etc). +# +# Takes the same resolver as new Promise() +lazyPromise = (resolver)-> + deferred = new Promise.defer() + resolved = false + promise = deferred.promise + promise._setProxyHandlers = (args...)-> + Promise.prototype._setProxyHandlers.apply(promise, args) + if !resolved + resolved = true + resolver(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)) + promise._addCallbacks = (args...)-> + Promise.prototype._addCallbacks.apply(promise, args) + if !resolved + resolved = true + resolver(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)) + return promise + + # The browser event loop. # # All asynchronous events are processed by this one. The event loop monitors one @@ -81,63 +102,67 @@ class EventLoop extends EventEmitter waitDuration = ms(waitDuration.toString()) || @browser.waitDuration timeoutOn = Date.now() + waitDuration - # Someone (us) just started paying attention, start processing events - ++@waiting - if @waiting == 1 - setImmediate => - if @active - @run() - - timer = null - ontick = null - onerror = null - ondone = null - - promise = new Promise((resolve, reject)=> - timer = global.setTimeout(resolve, waitDuration) - - ontick = (next)=> - if next >= timeoutOn - # Next event too long in the future, or no events in queue - # (Infinity), no point in waiting - resolve() - else if completionFunction && @active.document.documentElement - try - waitFor = Math.max(next - Date.now(), 0) - # Event processed, are we ready to complete? - completed = completionFunction(@active, waitFor) - if completed - resolve() - catch error - reject(error) - return - @on("tick", ontick) - - ondone = resolve - @once("done", ondone) - + lazy = lazyPromise((resolve, reject)=> - # Don't wait if browser encounters an error (event loop errors also - # propagate to browser) - onerror = reject - @browser.once("error", onerror) - return - ) + # Someone (us) just started paying attention, start processing events + ++@waiting + if @waiting == 1 + setImmediate => + if @active + @run() - promise = promise.finally(=> - - clearInterval(timer) - @removeListener("tick", ontick) - @removeListener("done", ondone) - @browser.removeListener("error", onerror) + timer = null + ontick = null + onerror = null + ondone = null + + work = new Promise((resolve, reject)=> + timer = global.setTimeout(resolve, waitDuration) + + ontick = (next)=> + if next >= timeoutOn + # Next event too long in the future, or no events in queue + # (Infinity), no point in waiting + resolve() + else if completionFunction && @active.document.documentElement + try + waitFor = Math.max(next - Date.now(), 0) + # Event processed, are we ready to complete? + completed = completionFunction(@active, waitFor) + if completed + resolve() + catch error + reject(error) + return + @on("tick", ontick) + + ondone = resolve + @once("done", ondone) + + + # Don't wait if browser encounters an error (event loop errors also + # propagate to browser) + onerror = reject + @browser.once("error", onerror) + return + ) + + finalized = work.finally(=> + + clearInterval(timer) + @removeListener("tick", ontick) + @removeListener("done", ondone) + @browser.removeListener("error", onerror) + + --@waiting + if @waiting == 0 + @browser.emit("done") + return + ) - --@waiting - if @waiting == 0 - @browser.emit("done") - return + resolve(finalized) ) - - return promise + return lazy dump: ()-> diff --git a/test/browser_object_test.js b/test/browser_object_test.js index 63ad2a5a8..d1d993828 100644 --- a/test/browser_object_test.js +++ b/test/browser_object_test.js @@ -270,7 +270,7 @@ describe('Browser', function() { browser.once('done', done); done = true; browser.location = '/browser/scripted'; - browser.wait(); + browser.wait().done(); }); }); diff --git a/test/event_loop_test.js b/test/event_loop_test.js index 59e461d1b..f7aabb23a 100644 --- a/test/event_loop_test.js +++ b/test/event_loop_test.js @@ -30,7 +30,7 @@ describe('EventLoop', function() { }); }); - describe('no wait', function() { + describe('not waiting', function() { before(async function() { await browser.visit('/eventloop/timeout'); browser.window.setTimeout(function() { @@ -59,7 +59,7 @@ describe('EventLoop', function() { }); }); - describe('from timeout', function() { + describe('from within timeout', function() { before(async function() { await browser.visit('/eventloop/timeout'); browser.window.setTimeout(function() { @@ -124,7 +124,8 @@ describe('EventLoop', function() { }); }); - describe('outside wait', function() { + + describe('outside of wait', function() { before(async function() { await browser.visit('/eventloop/function'); browser.window.setTimeout(function() { this.document.title += '1'; }, 100); @@ -314,14 +315,12 @@ describe('EventLoop', function() { describe('requestAnimationFrame', function() { before(function() { - brains.get('/eventloop/requestAnimationFrame', function(req, res) { - res.send(` - - - - - `); - }); + brains.static('/eventloop/requestAnimationFrame', ` + + + + + `); }); describe('with wait', function() { @@ -359,6 +358,99 @@ describe('EventLoop', function() { }); + // No callback -> event loop not runninga + // + // Test that when you call wait() with no callback, and don't attach anything + // to the promise, event loop pauses. + describe('wait', function() { + before(function() { + brains.static('/eventloop/wait', ` + + + + + `); + }); + + // This function is run in the context of the window (browser.evaluate), so + // has access to the current document and event loop setTimeout. + // + // Asynchronously it will update the document title to say 'Bang'. + function runAsynchronously() { + const { document } = this; + this.setTimeout(function() { + document.title = 'Bang'; + }, 100); + } + + describe('with Node callback', function() { + before(function() { + return browser.visit('/eventloop/wait'); + }); + + before(function(done) { + browser.evaluate(runAsynchronously); + browser.wait(done); + }); + + it('should run asynchronous code', function() { + browser.assert.text('title', 'Bang'); + }); + }); + + describe('with promise callback', function() { + before(function() { + return browser.visit('/eventloop/wait'); + }); + + before(function(done) { + browser.evaluate(runAsynchronously); + browser.wait().then(done, done); + }); + + it('should run asynchronous code', function() { + browser.assert.text('title', 'Bang'); + }); + }); + + describe('with no callback/then', function() { + before(function() { + return browser.visit('/eventloop/wait'); + }); + + before(function(done) { + browser.evaluate(runAsynchronously); + browser.wait(); + setTimeout(done, 100); + }); + + it('should not run asynchronous code', function() { + browser.assert.text('title', ''); + }); + }); + + describe('composed promise', function() { + before(function() { + return browser.visit('/eventloop/wait'); + }); + + before(function() { + browser.evaluate(runAsynchronously); + const resolved = Promise.resolve(); + const composed = resolved.then(function() { + return browser.wait(); + }); + return composed; + }); + + it('should run asynchronous code', function() { + browser.assert.text('title', 'Bang'); + }); + }); + + }); + + describe('page load', function() { before(function() { brains.get('/eventloop/dcl', function(req, res) { diff --git a/test/history_test.js b/test/history_test.js index d92142b8c..f6780bb2f 100644 --- a/test/history_test.js +++ b/test/history_test.js @@ -129,7 +129,7 @@ describe('History', function() { done(); }); window.history.back(); - browser.wait(); + browser.wait().done(); }); it('should fire popstate event', function() { @@ -159,6 +159,7 @@ describe('History', function() { done(); }); browser.history.forward(); + browser.wait().done(); }); it('should fire popstate event', function() { @@ -200,7 +201,7 @@ describe('History', function() { done(); }); window.history.back(); - browser.wait(); + browser.wait().done(); }); it('should change location URL', function() { @@ -312,7 +313,7 @@ describe('History', function() { before(function(done) { browser.window.location.pathname = '/history/boo'; browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { @@ -331,7 +332,7 @@ describe('History', function() { before(function(done) { browser.window.location.href = '/history/boo'; browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { @@ -353,7 +354,7 @@ describe('History', function() { browser.window.addEventListener('hashchange', ()=> done()); browser.window.location.hash = 'boo'; // Get the event loop running - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { @@ -372,7 +373,7 @@ describe('History', function() { before(function(done) { browser.window.location.assign('/history/boo'); browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { @@ -391,7 +392,7 @@ describe('History', function() { before(function(done) { browser.window.location.replace('/history/boo'); browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should not add page to history', function() { @@ -411,7 +412,7 @@ describe('History', function() { browser.window.document.innerHTML = 'Wolf'; browser.reload(); browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should not add page to history', function() { @@ -459,7 +460,7 @@ describe('History', function() { before(function(done) { browser.window.location = 'http://example.com/history/boo'; browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { @@ -478,7 +479,7 @@ describe('History', function() { before(function(done) { browser.window.document.location = 'http://example.com/history/boo'; browser.once('loaded', ()=> done()); - browser.wait(); + browser.wait().done(); }); it('should add page to history', function() { diff --git a/test/websocket_test.js b/test/websocket_test.js index a282d754f..5ec72726b 100644 --- a/test/websocket_test.js +++ b/test/websocket_test.js @@ -58,7 +58,7 @@ describe('WebSockets', function() { done(); }); - browser.visit('/websockets'); + browser.visit('/websockets').done(); }); From c66a2aca08d0dee90199bd578726800242688252 Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Mon, 1 Dec 2014 21:24:05 -0800 Subject: [PATCH 2/6] Updated changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 697e768ee..fadf9d511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## Version 2.5.0 2014-12-01 + +CHANGED wait() returns a lazy promise + +Prior to this change, calling `wait()` without a callback would return a +promise, which will resolve by running the event loop for completion, even if +you don't provide any callbacks. + +This is not specifically a problem with `wait`, but with methods that end by +calling `wait`, like `clickLink` and `pressButton`. + +After this change, `wait()` will do nothing, unless you either supply a +callback, or use the promise by means of calling `then/catch/done` on it. + +You can achieve the old behavior by calling `browser.wait().done()`. + + 699 passing (12s) + 8 pending + + ## Version 2.4.0 2014-11-27 FIXED eliminated endless spinning of the event loop From b0df717d07d058a7756a6a7a658b4826c371cc01 Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Thu, 4 Dec 2014 10:33:30 -0800 Subject: [PATCH 3/6] Changed from inheritance to composition --- src/zombie/eventloop.coffee | 52 ++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/zombie/eventloop.coffee b/src/zombie/eventloop.coffee index 535ecbf92..09c0da022 100644 --- a/src/zombie/eventloop.coffee +++ b/src/zombie/eventloop.coffee @@ -22,25 +22,35 @@ ms = require("ms") { Promise } = require("bluebird") -# Returns a Bluebird promise that evaluates only when registering a callback -# (via then, catch, done, etc). -# -# Takes the same resolver as new Promise() -lazyPromise = (resolver)-> - deferred = new Promise.defer() - resolved = false - promise = deferred.promise - promise._setProxyHandlers = (args...)-> - Promise.prototype._setProxyHandlers.apply(promise, args) - if !resolved - resolved = true - resolver(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)) - promise._addCallbacks = (args...)-> - Promise.prototype._addCallbacks.apply(promise, args) - if !resolved - resolved = true - resolver(deferred.resolve.bind(deferred), deferred.reject.bind(deferred)) - return promise +class LazyPromise + constructor: (@_resolver)-> + @_resolved = false + @_promise = new Promise(=> + @_resolve = arguments[0] + @_reject = arguments[1] + ) + + then: (args...)-> + @_lazyResolve() + return @_promise.then(args...) + + catch: (args...)-> + @_lazyResolve() + return @_promise.catch(args...) + + finally: (args...)-> + @_lazyResolve() + return @_promise.finally(args...) + + done: (args...)-> + @_lazyResolve() + return @_promise.done(args...) + + _lazyResolve: -> + unless @_resolved + @_resolved = true + @_resolver(@_resolve, @_reject) + # The browser event loop. @@ -102,7 +112,7 @@ class EventLoop extends EventEmitter waitDuration = ms(waitDuration.toString()) || @browser.waitDuration timeoutOn = Date.now() + waitDuration - lazy = lazyPromise((resolve, reject)=> + lazy = new LazyPromise((resolve, reject)=> # Someone (us) just started paying attention, start processing events ++@waiting @@ -148,7 +158,7 @@ class EventLoop extends EventEmitter ) finalized = work.finally(=> - + clearInterval(timer) @removeListener("tick", ontick) @removeListener("done", ondone) From b7d002390fb6b209e7a969d63f74e598a1429b08 Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Thu, 4 Dec 2014 11:26:36 -0800 Subject: [PATCH 4/6] CHANGED passing an option object to browser.visit is deprecated --- CHANGELOG.md | 8 +- src/zombie/browser.coffee | 12 +-- src/zombie/index.coffee | 36 ++----- test/browser_object_test.js | 34 +++---- test/history_test.js | 6 +- test/storage_test.coffee | 126 ------------------------ test/storage_test.js | 187 ++++++++++++++++++++++++++++++++++++ test/window_test.js | 4 +- 8 files changed, 227 insertions(+), 186 deletions(-) delete mode 100644 test/storage_test.coffee create mode 100644 test/storage_test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fadf9d511..15de18848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Version 2.5.0 2014-12-01 +REMOVED Passing an options object to browser.visit is deprecated and will be +removed soon. Passing an options object to Browser.visit is still supported. + + CHANGED wait() returns a lazy promise Prior to this change, calling `wait()` without a callback would return a @@ -14,8 +18,8 @@ callback, or use the promise by means of calling `then/catch/done` on it. You can achieve the old behavior by calling `browser.wait().done()`. - 699 passing (12s) - 8 pending + 696 passing (12s) + 8 pending ## Version 2.4.0 2014-11-27 diff --git a/src/zombie/browser.coffee b/src/zombie/browser.coffee index c7a3c2437..977881707 100644 --- a/src/zombie/browser.coffee +++ b/src/zombie/browser.coffee @@ -212,11 +212,9 @@ class Browser extends EventEmitter return ifMissing - # Changes the browser options, and calls the function with a callback (reset). When you're done processing, call the - # reset function to bring options back to their previous values. - # - # See `visit` if you want to see this method in action. + # Deprecated withOptions: (options, fn)-> + console.log "visit with options is deprecated and will be removed soon" if options restore = {} for k,v of options @@ -497,12 +495,8 @@ class Browser extends EventEmitter # ---------- # ### browser.visit(url, callback?) - # ### browser.visit(url, options, callback) - # - # Loads document from the specified URL, processes events and calls the callback. If the second argument are options, - # uses these options for the duration of the request and resets the options afterwards. # - # The callback is called with error, the browser and status code. + # Loads document from the specified URL, processes events and calls the callback, or returns a promise. visit: (url, options, callback)-> if typeof options == "function" && !callback [callback, options] = [options, null] diff --git a/src/zombie/index.coffee b/src/zombie/index.coffee index 49593c617..b40feac7b 100644 --- a/src/zombie/index.coffee +++ b/src/zombie/index.coffee @@ -1,38 +1,28 @@ Assert = require("./assert") Resources = require("./resources") Browser = require("./browser") -Path = require("path") + + +Browser.Assert = Assert +Browser.Resources = Resources # ### zombie.visit(url, callback) -# ### zombie.visit(url, options, callback) +# ### zombie.visit(url, options? callback) # # Creates a new Browser, opens window to the URL and calls the callback when # done processing all events. # -# For example: -# zombie = require("zombie") -# -# vows.describe("Brains").addBatch( -# "seek": -# topic: -> -# zombie.browse "http://localhost:3000/brains", @callback -# "should find": (browser)-> -# assert.ok browser.html("#brains")[0] -# ).export(module); -# # * url -- URL of page to open -# * options -- Initialize the browser with these options # * callback -- Called with error, browser -visit = (url, options, callback)-> - if arguments.length == 2 +Browser.visit = (url, options, callback)-> + if arguments.length == 2 && typeof(options) == "function" [options, callback] = [null, options] browser = Browser.create(options) if callback - browser.visit url, options, (error)-> - callback(error, browser) + browser.visit(url, (error)-> callback(error, browser)) else - return browser.visit(url, options).then(-> browser); + return browser.visit(url).then(-> browser); # ### listen port, callback @@ -42,15 +32,9 @@ visit = (url, options, callback)-> # Ask Zombie to listen on the specified port for requests. The default # port is 8091, or you can specify a socket name. The callback is # invoked once Zombie is ready to accept new connections. -listen = (port, callback)-> +Browser.listen = (port, callback)-> require("./zombie/protocol").listen(port, callback) -Browser.listen = listen -Browser.visit = visit -Browser.Assert = Assert -Browser.Resources = Resources - - # Export the globals from browser.coffee module.exports = Browser diff --git a/test/browser_object_test.js b/test/browser_object_test.js index d1d993828..d5cc98ee1 100644 --- a/test/browser_object_test.js +++ b/test/browser_object_test.js @@ -77,43 +77,37 @@ describe('Browser', function() { describe('visit', function() { describe('successful', function() { - let callbackBrowser; + let browser; before(async function() { - callbackBrowser = await Browser.visit('/browser/scripted'); + browser = await Browser.visit('/browser/scripted'); }); - it('should pass browser to callback', function() { - assert(callbackBrowser instanceof Browser); - }); - it('should pass status code to callback', function() { - callbackBrowser.assert.success(); + it('should resolve to browser object', function() { + assert(browser.visit && browser.wait); }); it('should indicate success', function() { - assert(callbackBrowser.success); + browser.assert.success(); }); it('should reset browser errors', function() { - assert.equal(callbackBrowser.errors.length, 0); - }); - it('should have a resources object', function() { - assert(callbackBrowser.resources); + assert.equal(browser.errors.length, 0); }); }); describe('with error', function() { let error; + let browser; - before(async function() { - try { - await browser.visit('/browser/errored'); - assert(false, 'Should have errored'); - } catch (callbackError) { - error = callbackError; - } + before(function(done) { + Browser.visit('/browser/errored', function() { + error = arguments[0]; + browser = arguments[1]; + done(); + }); }); it('should call callback with error', function() { - assert.equal(error.constructor.name, 'TypeError'); + assert(error.message && error.stack); }); it('should indicate success', function() { browser.assert.success(); diff --git a/test/history_test.js b/test/history_test.js index f6780bb2f..29b85feff 100644 --- a/test/history_test.js +++ b/test/history_test.js @@ -506,8 +506,10 @@ describe('History', function() { }); describe('referer set', function() { - before(function() { - return browser.visit('/history/referer', { referer: 'http://braindepot' }); + before(async function() { + browser.referer = 'http://braindepot'; + await browser.visit('/history/referer'); + delete browser.referer; }); it('should be set from browser', function() { diff --git a/test/storage_test.coffee b/test/storage_test.coffee deleted file mode 100644 index 1467082ed..000000000 --- a/test/storage_test.coffee +++ /dev/null @@ -1,126 +0,0 @@ -{ assert, brains, Browser } = require("./helpers") - - -test = (scope)-> - describe "initial", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - done(error) - - it "should start with no keys", -> - assert.equal @storage.length, 0 - it "should handle key() with no key", -> - assert !@storage.key(1) - it "should handle getItem() with no item", -> - assert.equal @storage.getItem("nosuch"), null - it "should handle removeItem() with no item", -> - assert.doesNotThrow => - @storage.removeItem("nosuch") - it "should handle clear() with no items", -> - assert.doesNotThrow => - @storage.clear() - - - describe "add some items", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - @storage.setItem "is", "hungry" - @storage.setItem "wants", "brains" - done(error) - - it "should count all items in length", -> - assert.equal @storage.length, 2 - it "should make key available", -> - keys = [@storage.key(0), @storage.key(1)].sort() - assert.deepEqual keys, ["is", "wants"] - it "should make value available", -> - assert.equal @storage.getItem("is"), "hungry" - assert.equal @storage.getItem("wants"), "brains" - - - describe "change an item", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - @storage.setItem "is", "hungry" - @storage.setItem "wants", "brains" - @storage.setItem "is", "dead" - @keys = [@storage.key(0), @storage.key(1)].sort() - done(error) - - it "should leave length intact", -> - assert.equal @storage.length, 2 - it "should keep key position", -> - assert.deepEqual [@storage.key(0), @storage.key(1)].sort(), @keys - it "should change value", -> - assert.equal @storage.getItem("is"), "dead" - it "should not change other values", -> - assert.equal @storage.getItem("wants"), "brains" - - - describe "remove an item", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - @storage.setItem "is", "hungry" - @storage.setItem "wants", "brains" - @storage.removeItem "is" - done(error) - - it "should drop item from length", -> - assert.equal @storage.length, 1 - it "should forget key", -> - assert.equal @storage.key(0), "wants" - assert !@storage.key(1) - it "should forget value", -> - assert.equal @storage.getItem("is"), null - assert.equal @storage.getItem("wants"), "brains" - - - describe "clean all items", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - @storage.setItem "is", "hungry" - @storage.setItem "wants", "brains" - @storage.clear() - done(error) - - it "should reset length to zero", -> - assert.equal @storage.length, 0 - it "should forget all keys", -> - assert !@storage.key(0) - it "should forget all values", -> - assert.equal @storage.getItem("is"), null - assert.equal @storage.getItem("wants"), null - - - describe "store null", -> - before (done)-> - Browser.visit "/storage", (error, browser)=> - @storage = scope(browser.window) - @storage.setItem "null", null - done(error) - - it "should store that item", -> - assert.equal @storage.length, 1 - it "should return null for key", -> - assert.equal @storage.getItem("null"), null - - -describe "Storage", -> - before (done)-> - brains.get "/storage", (req, res)-> - res.send "" - brains.ready done - - describe "local storage", -> - test.call this, (window)-> - window.localStorage - - describe "session storage", -> - test.call this, (window)-> - window.sessionStorage - diff --git a/test/storage_test.js b/test/storage_test.js new file mode 100644 index 000000000..c62ea9f2a --- /dev/null +++ b/test/storage_test.js @@ -0,0 +1,187 @@ +const assert = require('assert'); +const Browser = require('../src/zombie'); +const { brains } = require('./helpers'); + + +describe('Window', function() { + let browser; + + before(function() { + browser = Browser.create(); + brains.static('/storage', ''); + return brains.ready(); + }); + + describe("local storage", function() { + function getStorage(window) { + return window.localStorage; + } + addTests.call(this, getStorage); + }); + + describe("session storage", function() { + function getStorage(window) { + return window.sessionStorage; + } + addTests.call(this, getStorage); + }); + + + function addTests(getStorage) { + + describe("initial", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + }); + + it("should start with no keys", function() { + assert.equal(storage.length, 0); + }); + it("should handle key() with no key", function() { + assert(!storage.key(1)); + }); + it("should handle getItem() with no item", function() { + assert.equal(storage.getItem("nosuch"), null); + }); + it("should handle removeItem() with no item", function() { + assert.doesNotThrow(function() { + storage.removeItem("nosuch") + }); + }); + it("should handle clear() with no items", function() { + assert.doesNotThrow(function() { + storage.clear() + }); + }); + }); + + + describe("add some items", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + storage.setItem("is", "hungry"); + storage.setItem("wants", "brains"); + }); + + it("should count all items in length", function() { + assert.equal(storage.length, 2); + }); + it("should make key available", function() { + const keys = [storage.key(0), storage.key(1)].sort(); + assert.deepEqual(keys, ["is", "wants"]); + }); + it("should make value available", function() { + assert.equal(storage.getItem("is"), "hungry"); + assert.equal(storage.getItem("wants"), "brains"); + }); + }); + + + describe("change an item", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + storage.setItem("is", "hungry"); + storage.setItem("wants", "brains"); + storage.setItem("is", "dead"); + }); + + it("should leave length intact", function() { + assert.equal(storage.length, 2); + }); + it("should keep key position", function() { + const keys = [storage.key(0), storage.key(1)].sort(); + assert.deepEqual([storage.key(0), storage.key(1)].sort(), keys); + }); + it("should change value", function() { + assert.equal(storage.getItem("is"), "dead"); + }); + it("should not change other values", function() { + assert.equal(storage.getItem("wants"), "brains"); + }); + }); + + + describe("remove an item", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + storage.setItem("is", "hungry"); + storage.setItem("wants", "brains"); + storage.removeItem("is"); + }); + + it("should drop item from length", function() { + assert.equal(storage.length, 1); + }); + it("should forget key", function() { + assert.equal(storage.key(0), "wants"); + assert(!storage.key(1)); + }); + it("should forget value", function() { + assert.equal(storage.getItem("is"), null); + assert.equal(storage.getItem("wants"), "brains"); + }); + }); + + + describe("clean all items", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + storage.setItem("is", "hungry"); + storage.setItem("wants", "brains"); + storage.clear(); + }); + + it("should reset length to zero", function() { + assert.equal(storage.length, 0); + }); + it("should forget all keys", function() { + assert(!storage.key(0)); + }); + it("should forget all values", function() { + assert.equal(storage.getItem("is"), null); + assert.equal(storage.getItem("wants"), null); + }); + }); + + + describe("store null", function() { + let storage; + + before(async function() { + await browser.visit("/storage"); + storage = getStorage(browser.window); + storage.setItem("null", null); + }); + + it("should store that item", function() { + assert.equal(storage.length, 1); + }); + it("should return null for key", function() { + assert.equal(storage.getItem("null"), null); + }); + }); + + + }; + + + after(function() { + browser.destroy(); + }); +}); + diff --git a/test/window_test.js b/test/window_test.js index 961a65912..9db677a61 100644 --- a/test/window_test.js +++ b/test/window_test.js @@ -292,11 +292,13 @@ describe('Window', function() { }); it('should support testing the refresh page', async function() { + browser.visit('/windows/refresh'); + function complete() { return browser.query('meta'); } - await browser.visit('/windows/refresh', { function: complete }); + await browser.wait({ function: complete }); browser.assert.url('http://example.com/windows/refresh'); // Check the refresh page. browser.assert.text('title', 'Refresh'); From b1eb315f596b094f68483e6eca493b63e699ddcd Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Fri, 5 Dec 2014 13:11:46 -0800 Subject: [PATCH 5/6] Cleaner, more complete implementation of lazy promises --- src/zombie/eventloop.coffee | 32 +------------------ src/zombie/lazy_promise.coffee | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 src/zombie/lazy_promise.coffee diff --git a/src/zombie/eventloop.coffee b/src/zombie/eventloop.coffee index 09c0da022..e64640915 100644 --- a/src/zombie/eventloop.coffee +++ b/src/zombie/eventloop.coffee @@ -20,37 +20,7 @@ Domain = require("domain") { EventEmitter } = require("events") ms = require("ms") { Promise } = require("bluebird") - - -class LazyPromise - constructor: (@_resolver)-> - @_resolved = false - @_promise = new Promise(=> - @_resolve = arguments[0] - @_reject = arguments[1] - ) - - then: (args...)-> - @_lazyResolve() - return @_promise.then(args...) - - catch: (args...)-> - @_lazyResolve() - return @_promise.catch(args...) - - finally: (args...)-> - @_lazyResolve() - return @_promise.finally(args...) - - done: (args...)-> - @_lazyResolve() - return @_promise.done(args...) - - _lazyResolve: -> - unless @_resolved - @_resolved = true - @_resolver(@_resolve, @_reject) - +LazyPromise = require("./lazy_promise") # The browser event loop. diff --git a/src/zombie/lazy_promise.coffee b/src/zombie/lazy_promise.coffee new file mode 100644 index 000000000..460382ce0 --- /dev/null +++ b/src/zombie/lazy_promise.coffee @@ -0,0 +1,58 @@ +# Lazy promises don't evaluate until you register the first fulfilled or +# rejected handler. + +{ Promise } = require("bluebird") + + +THEN_METHODS = [ 'call', 'catch', 'done', 'error', 'finally', 'get', 'reflect', 'return', 'tap', 'then', 'throw' ] +INSPECTION_METHODS = [ 'isFulfilled', 'isPending', 'isRejected', 'reason', 'value' ] + +LazyPromise = (resolver)-> + unless typeof(resolver) == 'function' || resolver instanceof Function + throw new Error("Must be called with resolver callback") + this._resolver = resolver + this._resolved = false + this._promise = new Promise(=> + this._resolve = arguments[0] + this._reject = arguments[1] + ) + return this + +LazyPromise.prototype._lazyResolve = -> + if !this._resolved + setImmediate =>THEN_METHODS = [ 'call', 'catch', 'done', 'error', 'finally', 'get', 'reflect', 'return', 'tap', 'then', 'throw' ] +INSPECTION_METHODS = [ 'isFulfilled', 'isPending', 'isRejected', 'reason', 'value' ] + +LazyPromise = (resolver)-> + unless typeof(resolver) == 'function' || resolver instanceof Function + throw new Error("Must be called with resolver callback") + this._resolver = resolver + this._resolved = false + this._promise = new Promise(=> + this._resolve = arguments[0] + this._reject = arguments[1] + ) + return this + +LazyPromise.prototype._lazyResolve = -> + if !this._resolved + setImmediate => + try + this._resolver(this._resolve, this._reject) + catch ex + this._reject(ex) + this._resolved = true + +THEN_METHODS.forEach (name)-> + method = Promise.prototype[name] + LazyPromise.prototype[name] = -> + this._lazyResolve() + return method.apply(this._promise, arguments) + +INSPECTION_METHODS.forEach (name)-> + method = Promise.prototype[name] + LazyPromise.prototype[name] = -> + return method.apply(this._promise, arguments) + + +module.exports = LazyPromise From 7badf3e0f28f7fc8b1fc75674f80acc48398e3be Mon Sep 17 00:00:00 2001 From: Assaf Arkin Date: Fri, 5 Dec 2014 14:08:32 -0800 Subject: [PATCH 6/6] Changed to use lazybird --- package.json | 1 + src/zombie/eventloop.coffee | 4 +-- src/zombie/lazy_promise.coffee | 58 ---------------------------------- 3 files changed, 3 insertions(+), 60 deletions(-) delete mode 100644 src/zombie/lazy_promise.coffee diff --git a/package.json b/package.json index 663cfea42..d905d3862 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "eventsource": "^0.1.4", "iconv-lite": "^0.4.0", "jsdom": "1.3.1", + "lazybird": "^1.0.0", "mime": "^1.2.11", "ms": "^0.7.0", "request": "^2.48.0", diff --git a/src/zombie/eventloop.coffee b/src/zombie/eventloop.coffee index e64640915..d2a44a63a 100644 --- a/src/zombie/eventloop.coffee +++ b/src/zombie/eventloop.coffee @@ -20,7 +20,7 @@ Domain = require("domain") { EventEmitter } = require("events") ms = require("ms") { Promise } = require("bluebird") -LazyPromise = require("./lazy_promise") +Lazybird = require("lazybird") # The browser event loop. @@ -82,7 +82,7 @@ class EventLoop extends EventEmitter waitDuration = ms(waitDuration.toString()) || @browser.waitDuration timeoutOn = Date.now() + waitDuration - lazy = new LazyPromise((resolve, reject)=> + lazy = new Lazybird((resolve, reject)=> # Someone (us) just started paying attention, start processing events ++@waiting diff --git a/src/zombie/lazy_promise.coffee b/src/zombie/lazy_promise.coffee deleted file mode 100644 index 460382ce0..000000000 --- a/src/zombie/lazy_promise.coffee +++ /dev/null @@ -1,58 +0,0 @@ -# Lazy promises don't evaluate until you register the first fulfilled or -# rejected handler. - -{ Promise } = require("bluebird") - - -THEN_METHODS = [ 'call', 'catch', 'done', 'error', 'finally', 'get', 'reflect', 'return', 'tap', 'then', 'throw' ] -INSPECTION_METHODS = [ 'isFulfilled', 'isPending', 'isRejected', 'reason', 'value' ] - -LazyPromise = (resolver)-> - unless typeof(resolver) == 'function' || resolver instanceof Function - throw new Error("Must be called with resolver callback") - this._resolver = resolver - this._resolved = false - this._promise = new Promise(=> - this._resolve = arguments[0] - this._reject = arguments[1] - ) - return this - -LazyPromise.prototype._lazyResolve = -> - if !this._resolved - setImmediate =>THEN_METHODS = [ 'call', 'catch', 'done', 'error', 'finally', 'get', 'reflect', 'return', 'tap', 'then', 'throw' ] -INSPECTION_METHODS = [ 'isFulfilled', 'isPending', 'isRejected', 'reason', 'value' ] - -LazyPromise = (resolver)-> - unless typeof(resolver) == 'function' || resolver instanceof Function - throw new Error("Must be called with resolver callback") - this._resolver = resolver - this._resolved = false - this._promise = new Promise(=> - this._resolve = arguments[0] - this._reject = arguments[1] - ) - return this - -LazyPromise.prototype._lazyResolve = -> - if !this._resolved - setImmediate => - try - this._resolver(this._resolve, this._reject) - catch ex - this._reject(ex) - this._resolved = true - -THEN_METHODS.forEach (name)-> - method = Promise.prototype[name] - LazyPromise.prototype[name] = -> - this._lazyResolve() - return method.apply(this._promise, arguments) - -INSPECTION_METHODS.forEach (name)-> - method = Promise.prototype[name] - LazyPromise.prototype[name] = -> - return method.apply(this._promise, arguments) - - -module.exports = LazyPromise