Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
robert-stuttaford committed Jan 10, 2012
0 parents commit 0406ae1
Show file tree
Hide file tree
Showing 76 changed files with 11,963 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.bundle
.DS_Store
.idea
.sass-cache/
closure-library
jasmine
bin
Gemfile.lock
18 changes: 18 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
source 'http://rubygems.org'

# google closure compiler and templates compiler
gem 'closure'

# sass/compass
gem 'compass'
gem 'sassy-buttons'

# so that jasmine tests are automatically compiled to js
gem 'guard-coffeescript'
gem 'rb-fsevent'

# cucumber and capybara, and a drb server for quicker cucumber execution
gem 'cucumber'
gem 'capybara'
gem 'capybara-webkit'
gem 'spork', '~> 0.9.0.rc'
4 changes: 4 additions & 0 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# A sample Guardfile
# More info at https://github.com/guard/guard#readme

guard 'coffeescript', :input => 'app'
78 changes: 78 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Closure-Script Boilerplate

Google Closure development with the [Closure-Script](https://github.com/dturnbull/closure-script) gem, [Jasmine](https://github.com/pivotal/jasmine) for BDD style unit testing, and [Cucumber](http://cukes.info/) with [Capybara](https://github.com/jnicklas/capybara) for functional/integration/acceptance testing. Also included are Sass/Compass with the HTML5 Boilerplate all sassed up, and Guard for Coffeescript compilation, so that Jasmine tests can be written with Coffeescript.

I use this on OSX Lion. Prerequisites: Ruby 1.9.2 (haven't tested 1.8 or JRuby) via `rvm` and `homebrew`.

## Installation

Clone me:

git clone https://github.com/robert-stuttaford/Closure-Script-Boilerplate.git
cd Closure-Script-Boilerplate

Get the Closure Library:

svn checkout http://closure-library.googlecode.com/svn/trunk closure-library

Get Jasmine:

git clone https://github.com/pivotal/jasmine.git

I actually have both of these in a separate frameworks folder and I've simply symlinked these into each project.

Install the QT library from Nokia, capybara-webkit requires this (see <https://github.com/thoughtbot/capybara-webkit/wiki/Installing-QT>):

brew install qt

Install gems:

bundle install --binstubs

Start the Rack server:

./serve

Visit `localhost:3000` to see the development dashboard.

If you're writing Jasmine tests, be sure to start guard:

guard

And if you're writing any CSS, be sure to start compass:

compass watch

## Development dashboard

This is a two column page with useful links down the left and an iframe for your app and Jasmine tests on the right.

The left column has a couple sections:

* A big refresh button, mapped to keyboard shortcut 'r'. This refreshes the iframe.
* Links to Jasmine specs:
* A link to run all the _spec.js files found inside `app/` (regardless of depth) at the same time.
* A dynamic list of all those _spec.js files, nicely formatted for readability. My own project uses short filenames, so I chose to allow more than one spec per line for compactness.
* Links to view the app itself:
* Development version (uncompiled)
* Compiled debug version, and the compile-on-demand debug version, which produces the app.debug.js used by the compiled version.
* Compiled production version, and the compile-on-demand production version, which produces the app.js used by the compiled version.
* Tools and reference:
* Externs generator: load up a 3rd-party javascript file, enter which objects you want externs for, and it'll produce the externs for you. Drop the contents into a file named (file).externs.js into the externs folder, and the compiler will use it. Credit goes to Guido Tapa on the Closure-Library Google Group list for this.
* Links to the local Closure demos, and the Closure Library and Templates API documentation on the web. These open in a new tab.

## Cucumber/Capybara testing

Start the `spark` daemon:

spork

Then run `cucumber --drb` to run your cucumber integration tests. See `features/app.feature` for a sample test. See <http://cheat.errtheblog.com/s/capybara> for a quick reference and <https://github.com/jnicklas/capybara> for the full story.

## Deployment

To take advantage of the image compression, install `optipng` and `jpegtrans`:

brew install optipng jpeg

Alter the deploy script to suit your own requirements. I target this script in my Jenkins CI build configuration.
61 changes: 61 additions & 0 deletions app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
goog.provide('app.App');

// require this so that the jasmine tests work
goog.require('app.model.util');
goog.require('app.services.ConfigService');
goog.require('app.ui.common.templates');
goog.require('app.ui.Widget');
goog.require('app.ui.Main');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.style');

/**
* The App application.
* @constructor
* @extends {goog.events.EventTarget}
*/
app.App = function() {
goog.events.EventTarget.call(this);

var div = document.createElement('div');
div.style.cssText = 'height:100%';
div.innerHTML = app.ui.common.templates.app();
document.body.appendChild(div);

/** @type {app.services.ConfigService} */
var configService = new app.services.ConfigService();
goog.events.listenOnce(configService, app.services.ConfigService.EventType.CONFIG_LOADED, this.startUp_, false, this);
configService.loadConfig();

/**
* The Main view
* @type {app.ui.Main}
* @private
*/
this.main_ = new app.ui.Main();
this.main_.decorate(goog.dom.getElement('main'));
};
goog.inherits(app.App, goog.events.EventTarget);

/**
* Starts App after loading the config
* @param {goog.events.Event=} opt_event Event.
* @private
*/
app.App.prototype.startUp_ = function(opt_event) {
/**
* The Widget
* @type {app.ui.Widget}
* @private
*/
this.widget_ = new app.ui.Widget();
this.main_.addChild(this.widget_, true);
};

/** Start the app */
app.App.start = function() {
app.App.app = new app.App();
};

goog.exportSymbol('start', app.App.start);
33 changes: 33 additions & 0 deletions app/compiler.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<%
args = %w{
--summary_detail_level 3
--ns app.App
}
compiled = %w{
--compilation_level ADVANCED_OPTIMIZATIONS
--warning_level VERBOSE
--language_in ECMASCRIPT5_STRICT
}
compiled += Dir.glob( expand_path('../externs/*.externs.js') ).map { |x| ['--externs',x] }
compiled.flatten!
args += case query_string
when 'build' then compiled + %w{
--js_output_file ../public/js/app.js
--create_source_map ../public/js/app.map
}
when 'debug' then compiled + %w{
--js_output_file ../public/js/app.debug.js
--debug true
--formatting PRETTY_PRINT
}
else;[];end
goog.soy_to_js %w{
--cssHandlingScheme goog
--shouldGenerateJsdoc
--shouldProvideRequireSoyNamespaces
--outputPathFormat {INPUT_DIRECTORY}{INPUT_FILE_NAME}.js
**/*.soy
}
@response = goog.compile(args).to_response
%>
9 changes: 9 additions & 0 deletions app/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
goog.provide('app.Config');

/**
* Public Configuration Store
* @type {Object}
*/
app.Config = {
CONFIG_VALUE: ''
};
48 changes: 48 additions & 0 deletions app/model/modelUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
goog.provide('app.model.util');

/** @enum {string} */
app.model.ID_RANGE = {
ALL: 'all',
NONE: 'none'
};

/**
* Expand a set of numerical ID ranges to full lists of distinct IDs
* @param {string} range
* @return {?string}
*/
app.model.util.expandIDs = function (range) {
if ( range == null || range.search( /^(all|none|[0-9,-]+)$/i ) == -1 ) {
return null;
}
if ( range.indexOf( '-' ) == -1 ) {
return range.toLowerCase();
}
/** @type {Array.<string>} */
var items = range.replace( / /g, '' ).split( ',' );

/** @type {Array} */
var newItems = goog.array.map(items,function (/** @type {string} */item) {
if ( item.indexOf( '-' ) === -1 ) {
return item;
}
/** @type {Array.<string>} */
var bounds = item.split( '-' );
/** @type {number} */
var lower = parseInt(bounds[ 0 ], 10);
/** @type {number} */
var upper = parseInt(bounds[ 1 ], 10);
if ( lower > upper ) {
return null;
}
lower--;
upper++;
/** @type {Array.<string>} */
var newSet = [];
while ( ++lower < upper ) {
newSet.push( lower );
}
return newSet.join( ',' );
});
return newItems.join( ',' );
};
52 changes: 52 additions & 0 deletions app/model/modelUtil_spec.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
describe 'Model Util', ->

describe 'expand ids', ->

expandIDs = null

beforeEach ->
expandIDs = app.model.util.expandIDs

it 'returns null if given null', ->
expect( expandIDs() ).toBe null

it 'returns null if not given "all", "none", or any combination of comma-delimited numbers and/or ranges of numbers', ->
expect( expandIDs('bad data') ).toBe null
expect( expandIDs('all,none') ).toBe null
expect( expandIDs('all,1,2,3') ).toBe null

it 'returns what it was given if there are no ranges present', ->
expect( expandIDs( '1,2,3' ) ).toBe '1,2,3'
expect( expandIDs( 'all' ) ).toBe 'all'
expect( expandIDs( 'none' ) ).toBe 'none'

it 'returns expanded list if given a range', ->
expect( expandIDs('1-4') ).toBe '1,2,3,4'
expect( expandIDs('10-14') ).toBe '10,11,12,13,14'

it 'returns all numbers, ranges expanded, if given any combination of comma-delimited numbers and/or ranges of numbers', ->
expect( expandIDs('1,2,3-6') ).toBe '1,2,3,4,5,6'
expect( expandIDs('1-3,4,5,6') ).toBe '1,2,3,4,5,6'
expect( expandIDs('1-3,4-6') ).toBe '1,2,3,4,5,6'

describe 'filter items by ids', ->

filterItemsByIDs = null

beforeEach ->
filterItemsByIDs = app.model.util.filterItemsByIDs

it 'returns empty collection if given nulls', ->
expect( goog.object.getCount filterItemsByIDs(null,null) ).toBe 0

it 'returns empty collection if given NONE', ->
expect( goog.object.getCount filterItemsByIDs(app.model.ID_RANGE.NONE,{}) ).toBe 0

it 'returns clone of passed in collection if given ALL', ->
expect( goog.object.getCount filterItemsByIDs(app.model.ID_RANGE.ALL,{1:'foo'}) ).toBe 1

it 'returns correct items in collection when passed ids', ->
items = {1:'foo',2:'bar'}
filteredItems = filterItemsByIDs('1',items)
expect( goog.object.getCount filteredItems ).toBe 1
expect( filteredItems[1] ).toBe items[1]
63 changes: 63 additions & 0 deletions app/model/modelUtil_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(function() {

describe('Model Util', function() {
describe('expand ids', function() {
var expandIDs;
expandIDs = null;
beforeEach(function() {
return expandIDs = app.model.util.expandIDs;
});
it('returns null if given null', function() {
return expect(expandIDs()).toBe(null);
});
it('returns null if not given "all", "none", or any combination of comma-delimited numbers and/or ranges of numbers', function() {
expect(expandIDs('bad data')).toBe(null);
expect(expandIDs('all,none')).toBe(null);
return expect(expandIDs('all,1,2,3')).toBe(null);
});
it('returns what it was given if there are no ranges present', function() {
expect(expandIDs('1,2,3')).toBe('1,2,3');
expect(expandIDs('all')).toBe('all');
return expect(expandIDs('none')).toBe('none');
});
it('returns expanded list if given a range', function() {
expect(expandIDs('1-4')).toBe('1,2,3,4');
return expect(expandIDs('10-14')).toBe('10,11,12,13,14');
});
return it('returns all numbers, ranges expanded, if given any combination of comma-delimited numbers and/or ranges of numbers', function() {
expect(expandIDs('1,2,3-6')).toBe('1,2,3,4,5,6');
expect(expandIDs('1-3,4,5,6')).toBe('1,2,3,4,5,6');
return expect(expandIDs('1-3,4-6')).toBe('1,2,3,4,5,6');
});
});
return describe('filter items by ids', function() {
var filterItemsByIDs;
filterItemsByIDs = null;
beforeEach(function() {
return filterItemsByIDs = app.model.util.filterItemsByIDs;
});
it('returns empty collection if given nulls', function() {
return expect(goog.object.getCount(filterItemsByIDs(null, null))).toBe(0);
});
it('returns empty collection if given NONE', function() {
return expect(goog.object.getCount(filterItemsByIDs(app.model.ID_RANGE.NONE, {}))).toBe(0);
});
it('returns clone of passed in collection if given ALL', function() {
return expect(goog.object.getCount(filterItemsByIDs(app.model.ID_RANGE.ALL, {
1: 'foo'
}))).toBe(1);
});
return it('returns correct items in collection when passed ids', function() {
var filteredItems, items;
items = {
1: 'foo',
2: 'bar'
};
filteredItems = filterItemsByIDs('1', items);
expect(goog.object.getCount(filteredItems)).toBe(1);
return expect(filteredItems[1]).toBe(items[1]);
});
});
});

}).call(this);
Loading

0 comments on commit 0406ae1

Please sign in to comment.