Skip to content

Commit

Permalink
small restructure of roots.watch and tests
Browse files Browse the repository at this point in the history
- integration tests through selenium and phantomjs/chrome
- watch command now returns a promise
  • Loading branch information
Jeff Escalante committed May 27, 2014
1 parent 2da59be commit 29c282e
Show file tree
Hide file tree
Showing 13 changed files with 117 additions and 99 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
node_modules
docs/_build
.*.sw*
phantomjsdriver.log
6 changes: 6 additions & 0 deletions contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ Roots is constantly evolving, and to ensure that things are secure and working f

Please also ensure that any new lines you add are _fully covered_ by tests. Of course, this does not mean there will be no bugs, but it certainly makes it less likely. To test the code coverage, you can run `npm run coverage`.

Since roots integrates directly with the browser for reloading and errors, we use selenium to test these pieces of functionality. While quite powerful, unfortunately installing and working with selenium is really a pain. Below are a couple tips for getting selenium running correctly on osx.

- Make sure you have v2.9 of chromedriver installed, 2.10 is bugged
- Make sure chromedriver in in your path (aka accessible from the command line)
- If chromedriver isn't working for you right after installing, try restarting

### Code Style

To keep a consistant coding style in the project, we're going with [Felix's Node.js Style Guide](http://nodeguide.com/style.html) for JS and [Polar Mobile's guide](https://github.com/polarmobile/coffeescript-style-guide) for CoffeeScript, but it should be noted that much of this project uses under_scores rather than camelCase for naming. Both of these are pretty standard guides. For documenting in the code, we're using [JSDoc](http://usejsdoc.org/).
Expand Down
4 changes: 3 additions & 1 deletion lib/api/compile.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ class Compile
.then(after_ext_hook)
.then(after_hook)
.then(purge_empty_folders)
.then(@roots.emit.bind(@roots, 'done'), @roots.emit.bind(@roots, 'error'))
.then @roots.emit.bind(@roots, 'done'), (err) ->
@roots.emit('error', err)
W.reject(err)

###*
* Calls any user-provided before hooks with the roots context.
Expand Down
16 changes: 7 additions & 9 deletions lib/api/watch.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,22 @@ _ = require 'lodash'
class Watcher

constructor: (@roots) ->
@watcher = chokidar.watch @roots.root,
ignoreInitial: true
ignored: ignore.bind(@)

###*
* Compile the project, once done, watch it for further changes.
*
* @return {Object} chokidar [https://github.com/paulmillr/chokidar] instance
* @return {Promise} promise that the project has compiled and is watched
###

exec: ->
watcher = chokidar.watch @roots.root,
ignoreInitial: true
ignored: ignore.bind(@)

@roots.compile().finally ->
watcher
@roots.compile().finally =>
@watcher
.on('error', (err) => @roots.emit('error', err))
.on('change', @roots.compile.bind(@roots))

return _.extend(@roots, { watcher: watcher })
.yield(@watcher)

###*
* Given a path, returns true or false depending on whether it should be
Expand Down
40 changes: 19 additions & 21 deletions lib/cli/watch.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,21 @@ Server = require '../local_server'

module.exports = (cli, args) ->
project = new Roots(args.path, { env: args.environment })
server = new Server(project, project.root)
port = process.env.port or args.port
app = new Server(project)
port = process.env.port or args.port
res = { project: project }

cli.emit('inline', 'compiling... '.grey)
server.start(port)

watcher = project.watch()

watcher.on('start', on_start.bind(null, cli, server))
watcher.on('error', on_error.bind(null, cli, server))
watcher.on('done', on_done.bind(null, cli, server))

watcher.once 'done', ->
if project.config.open_browser and not args.no_open
open("http://localhost:#{port}/")
project.on('start', -> on_start(cli, app, res.server))
project.on('done', -> on_done(cli, app, res.server))
project.on('error', (err) -> on_error(cli, app, res.server, err))

return { server: server, watcher: watcher }
project.watch()
.then (w) ->
res.watcher = w
res.server = app.start(port)
if project.config.open_browser and not args.no_open
open("http://localhost:#{port}/")
.yield(res)

###*
* Emit an error to the CLI and sends it to the server to display in-browser
Expand All @@ -47,9 +45,9 @@ module.exports = (cli, args) ->
* @param {*} err - the error that happened
###

on_error = (cli, server, err) ->
on_error = (cli, server, active, err) ->
cli.emit('err', Error(err).stack)
server.show_error(Error(err).stack)
if active then server.show_error(Error(err).stack)

###*
* When a change has been detected, notifies the cli and browser that a compile
Expand All @@ -60,9 +58,9 @@ on_error = (cli, server, err) ->
* @param {Object} server - server instance
###

on_start = (cli, server) ->
on_start = (cli, server, active) ->
cli.emit('inline', 'compiling... '.grey)
server.compiling()
if active then server.compiling()

###*
* When a compile has finished, notifies the CLI and reloads the browser.
Expand All @@ -72,6 +70,6 @@ on_start = (cli, server) ->
* @param {Object} server - server instance
###

on_done = (cli, server) ->
on_done = (cli, server, active) ->
cli.emit('data', 'done!'.green)
server.reload()
if active then server.reload()
4 changes: 2 additions & 2 deletions lib/fs_parser.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ class FSParser
.on 'data', (f) =>
if ignored.call(@, f.path) then return
if f.parentDir.length then @ast.dirs.push(f.fullParentDir)
files.push(parse_file.call(@, f.fullPath))
file = parse_file.call(@, f.fullPath)
file.then((-> files.push(file)), deferred.reject)

return deferred.promise

Expand Down Expand Up @@ -133,7 +134,6 @@ class FSParser
* @param {Boolean} extract - if true, function is skipped
* @return {Boolean} promise for a boolean, passed as extract to next function
*
* @todo handle error if ext.fs doesn't return an object
* @todo handle error if ext.fs.detect doesn't exist
* @todo handle error if category not found
###
Expand Down
12 changes: 6 additions & 6 deletions lib/local_server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Server
* @param {String} dir - directory to serve
###

constructor: (@roots, @dir) ->
constructor: (@project) ->

###*
* Start the local server on the given port.
Expand All @@ -26,19 +26,19 @@ class Server
###

start: (port, cb) ->
opts = @roots.config.server or {}
opts = @project.config.server or {}
opts.log = false

if @roots.config.env == 'development'
if @project.config.env == 'development'
opts.write = content:
"<!-- roots development configuration -->
<script>var __livereload = #{@roots.config.live_reload};</script>
<script>var __livereload = #{@project.config.live_reload};</script>
<script src='/__roots__/main.js'></script>"
opts.cache_control = { '**': 'max-age=0, no-cache, no-store' }

app = charge(@roots.config.output_path(), opts)
app = charge(@project.config.output_path(), opts)

if @roots.config.env == 'development'
if @project.config.env == 'development'
app.stack.splice app.stack.length-2, 0,
route: '/__roots__'
handle: serve_static(path.resolve(__dirname, 'browser'))
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
"roots-util": "0.0.4",
"chai-fs": "0.0.3",
"mockery": "1.4.x",
"coffeelint": "1.3.x"
"coffeelint": "1.3.x",
"chai-webdriver": "^0.9.0",
"selenium-webdriver": "^2.41.0"
},
"scripts": {
"test": "npm run lint && mocha",
Expand Down
44 changes: 44 additions & 0 deletions test/browser.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Driver = require 'selenium-webdriver'
chaidriver = require 'chai-webdriver'
CLI = require '../lib/cli'
fs = require 'fs'

cli = new CLI(debug: true)
driver = new Driver.Builder().withCapabilities(Driver.Capabilities.phantomjs()).build()
chai.use(chaidriver(driver))
basic_path = path.join(base_path, 'compile/basic')

describe 'browser', ->

it 'should compile and serve the site on watch', (done) ->
cli.run("watch #{basic_path} --no-open").then (res) ->
driver.get('http://localhost:1111')
.then -> chai.expect('p').dom.to.have.text('hello worlds')
.then ->
sentinel = 0
deferred = W.defer()

res.project.once 'start', ->
sentinel++
# this happens too fast for selenium to reliably handle
# chai.expect('#roots-compile-loader').dom.to.be.visible()

res.project.once 'done', ->
setTimeout ->
chai.expect('p').dom.to.have.text('wow')
sentinel.should.equal(1)
fs.writeFileSync(path.join(basic_path, 'index.jade'), 'html\n body\n p= #$%#$T')
, 500

res.project.once 'error', ->
setTimeout ->
chai.expect('#roots-error').dom.to.be.visible()
fs.writeFileSync(path.join(basic_path, 'index.jade'), 'html\n body\n p hello worlds')
deferred.resolve()
, 500

fs.writeFileSync(path.join(basic_path, 'index.jade'), 'html\n body\n p wow')

return deferred.promise
.then -> res.server.close()
.then -> done()
27 changes: 13 additions & 14 deletions test/cli.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe 'cli', ->
describe 'watch', ->

before ->
@stub = sinon.stub(Roots.prototype, 'watch').returns(new EventEmitter)
@stub = sinon.stub(Roots.prototype, 'watch').returns(W.resolve())
mockery.registerMock('../../lib', Roots)

after ->
Expand All @@ -168,20 +168,19 @@ describe 'cli', ->

cli.on('inline', spy)
cli.on('data', spy)
cli.on('err', spy)

cwd = process.cwd()
process.chdir(path.join(__dirname, 'fixtures/compile/basic'))
{server, watcher} = cli.run('watch --no-open')
spy.should.have.been.calledOnce
watcher.emit('done')
spy.should.have.been.calledTwice
watcher.emit('start')
spy.should.have.been.calledThrice
# TODO: browser response needs testing here as well
process.chdir(cwd)
cli.removeListener('inline', spy)
cli.removeListener('data', spy)
done()
cli.run("watch #{path.join(__dirname, 'fixtures/compile/basic')} --no-open")
.then (obj) ->
obj.project.emit('start')
spy.should.have.been.calledOnce
obj.project.emit('done')
spy.should.have.been.calledTwice
obj.project.emit('error')
spy.should.have.been.calledThrice
cli.removeListener('inline', spy)
cli.removeListener('data', spy)
obj.server.close(done)

it 'should error when trying to compile invalid code'

Expand Down
54 changes: 12 additions & 42 deletions test/extensions.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -145,37 +145,22 @@ describe 'extension failures', ->
project = new Roots(path.join(@path, 'case2'))
(-> project.compile()).should.throw('The fs property must be a function')

it 'should bail when fs is a function but doesnt return an object', (done) ->
it 'should bail when fs is a function but doesnt return an object', ->
project = new Roots(path.join(@path, 'case3'))
project.compile().should.be.rejectedWith('fs function must return an object')

project.on 'error', (err) ->
err.toString().should.equal('Malformed Extension: fs function must return an object')
done()

project.compile()

it 'should bail when fs is used with no category', (done) ->
it 'should bail when fs is used with no category', ->
project = new Roots(path.join(@path, 'case4'))

project.on 'error', (err) ->
err.toString().should.equal('Malformed Extension: fs hooks defined with no category')
done()

project.compile()
project.compile().should.be.rejectedWith('fs hooks defined with no category')

# this should not throw
it 'should bail when compile_hooks is defined but not a function', ->
project = new Roots(path.join(@path, 'case5'))
(-> project.compile()).should.throw('The compile_hooks property must be a function')

it 'should bail when compile_hooks is a function but doesnt return an object', (done) ->
it 'should bail when compile_hooks is a function but doesnt return an object', ->
project = new Roots(path.join(@path, 'case6'))

project.on 'error', (err) ->
err.toString().should.equal('Malformed Extension: compile_hooks should return an object')
done()

project.compile()
project.compile().should.be.rejectedWith('compile_hooks should return an object')

it 'should bail when compile_hooks returned object keys are not functions'

Expand All @@ -184,37 +169,22 @@ describe 'extension failures', ->
project = new Roots(path.join(@path, 'case7'))
(-> project.compile()).should.throw('The category_hooks property must be a function')

it 'should bail when category_hooks is a function but doesnt return an object', (done) ->
it 'should bail when category_hooks is a function but doesnt return an object', ->
project = new Roots(path.join(@path, 'case8'))

project.on 'error', (err) ->
err.toString().should.equal('Malformed Extension: category_hooks should return an object')
done()

project.compile()
project.compile().should.be.rejectedWith('category_hooks should return an object')

# this should not throw
it 'should bail when project_hooks is defined but not a function', ->
project = new Roots(path.join(@path, 'case11'))
(-> project.compile()).should.throw('The project_hooks property must be a function')

it 'should bail when project_hooks is a function but doesnt return an object', (done) ->
it 'should bail when project_hooks is a function but doesnt return an object', ->
project = new Roots(path.join(@path, 'case12'))
project.compile().should.be.rejectedWith('project_hooks should return an object')

project.on 'error', (err) ->
err.toString().should.equal('Malformed Extension: project_hooks should return an object')
done()

project.compile()

it 'should bail if write hook returns anything other than an array, object, or boolean', (done) ->
it 'should bail if write hook returns anything other than an array, object, or boolean', ->
project = new Roots(path.join(@path, 'case9'))

project.on 'error', (err) ->
err.toString().should.equal('Malformed Write Hook Output: invalid return from write_hook')
done()

project.compile()
project.compile().should.be.rejectedWith('invalid return from write_hook')

it "should bail if an extension's constructor throws an error", ->
project = new Roots(path.join(@path, 'case10'))
Expand Down
3 changes: 0 additions & 3 deletions test/fixtures/compile/basic/index.jade
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
doctype html
html
head
title testing
body
p hello worlds
1 change: 1 addition & 0 deletions test/support/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ chai.use(chai_fs);

global.W = W;
global.sinon = sinon;
global.chai = chai;
global.Roots = Roots;
global.path = path;
global.fs = fs;
Expand Down

0 comments on commit 29c282e

Please sign in to comment.