Added JavaScript Specs and Client-side Templates

- New rake task `rake jst:compile` to precompile
  JavaScript templates into functions.

  Add handlebars templates to app/views/jst and
  they'll get compiled to public/javascripts/jst

- New rake task `rake jasmine` and `jasmine:ci` to
  run JavaScript specs.

  Add specs to spec/coffeescripts and they'll get
  compiled into spec/javascripts

- Added Guard gem `$ guard` that watches
  coffeescript and handlebars files and compiles
  them when changes are made.

- Created Handlebars Ruby class that precompiles
  the templates into JavaScript functions

- Added JS Template constructor to abstract
  our tempting API



Change-Id: Ie993d0fc50d49b161ed94dbc066c4475cefdc427
Reviewed-on: https://gerrit.instructure.com/5813
Tested-by: Hudson <hudson@instructure.com>
Reviewed-by: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
Ryan Florence 2011-09-26 13:25:00 -06:00
parent e85e98cb25
commit 0f9d86d0d8
21 changed files with 2436 additions and 1 deletions

2
.gitignore vendored
View File

@ -50,4 +50,4 @@ public/assets/*
doc/api/*
doc/plugins/*
public/doc/api/*
.yardoc/*
.yardoc/*

View File

@ -70,6 +70,7 @@ group :test do
gem 'selenium-webdriver', '2.5.0'
gem 'webrat', '0.7.2'
gem 'yard', '0.7.2'
gem 'jasmine', '1.1.0'
end
group :development do
@ -77,12 +78,16 @@ group :development do
gem 'ruby_parser', '2.0.6'
gem 'sexp_processor', '3.0.5'
gem 'ya2yaml', '0.30'
gem 'guard', '0.7.0'
gem 'guard-coffeescript', '0.1.0'
gem 'execjs', '1.2.8'
end
group :redis do
gem 'redis-store', '1.0.0.rc1'
end
# The closure-compiler gem has an undocumented
# gem dependency on windows with ruby < 1.9. I'm
# working to get this fixed in the gem itself, but

16
Guardfile Normal file
View File

@ -0,0 +1,16 @@
guard 'coffeescript', :output => 'public/javascripts/compiled' do
watch('^app/coffeescripts/(.*)\.coffee')
end
guard 'coffeescript', :output => 'spec/javascripts' do
watch('^spec/coffeescripts/(.*)\.coffee')
end
guard 'jst', :output => 'public/javascripts/jst' do
watch('app/views/jst/(.*)\.handlebars')
end
#guard 'livereload' do
# watch('^spec/javascripts/.+\.js$')
# watch('^public/javascripts/compiled/.+\.js$')
#end

View File

@ -0,0 +1,46 @@
# A client-side templating wrapper. Templates are compiled with the rake task
# `$ rake jst:compile` or automatically using the guard gem `$ guard`.
# Don't call the templating object directly (like Handlebars), use this class.
class @Template
# If called w/o `new`, it will return the HTML string immediately.
# i.e. `Template(test, {foo: 'bar'})` => '<div>bar</div>'
#
# If called with `new` it will return an instance.
#
# Arguments:
# @name (string):
# The id of template. Examples [template path] => [id]:
# `app/views/jst/foo.handlebars` becomes `foo`
# `app/views/jst/foo/bar/baz.handlebars` becomes `foo/bar/baz`
#
# @locals (object: optional):
# Object literal of key:value pairs for use as local vars in the template.
#
constructor: (@name, @locals) ->
if this instanceof Template isnt true
return new Template(name, locals).toHTML()
# Generates an HTML string from the template.
#
# Arguments:
# locals (object: optional) - locals to use in the template, if omitted the
# instance locals property will be used.
#
# Returns:
# String - and HTML string
toHTML: (locals = @locals) ->
Handlebars.templates[@name](locals)
# Creates an element rendered with the template.
#
# Arguments:
# locals (object: optional):
# locals to use in the template, if omitted the instance locals property
# will be used.
#
# Returns:
# jQuery Element Collection
toElement: (locals) ->
html = @toHTML locals
jQuery('<div/>').html(html)

View File

@ -10,6 +10,7 @@ javascripts:
- public/javascripts/firebugx.js
- public/javascripts/datejs_to_iso_string_patch.js
- public/javascripts/date.js
- public/javascripts/handlebars.vm.js
- public/javascripts/jquery-1.5.2.js
- public/javascripts/jquery-ui-1.8.js
- public/javascripts/i18n.js

15
guard/jst.rb Normal file
View File

@ -0,0 +1,15 @@
require 'guard'
require 'guard/guard'
require 'lib/handlebars/handlebars'
module Guard
class JST < Guard
# Compiles templates from app/views/jst to public/javascripts/jst
def run_on_change(paths)
paths.each do |path|
puts "Running #{path}"
Handlebars.compile_file path, 'app/views/jst', @options[:output]
end
end
end
end

View File

@ -0,0 +1,54 @@
require 'fileutils'
# Precompiles handlebars templates into JavaScript function strings
class Handlebars
@@header = '!function() { var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};'
@@footer = '}()'
class << self
# Recursively compiles a source directory of .handlebars templates into a
# destination directory. Immitates the node.js bin script at
# https://github.com/wycats/handlebars.js/blob/master/bin/handlebars
#
# Arguments:
# root_path (string) - The root directory to find templates to compile
# compiled_path (string) - The destination directory in which to save the
# compiled templates.
def compile(root_path, compiled_path)
files = Dir["#{root_path}/**/**.handlebars"]
files.each { |file| compile_file file, root_path, compiled_path }
end
# Compiles a single file into a destination directory.
#
# Arguments:
# file (string) - The file to compile.
# root_path - See `compile`
# compiled_path - See `compile`
def compile_file(file, root_path, compiled_path)
require 'execjs'
id = file.gsub(root_path, '').gsub(/.handlebars$/, '')
path = "#{compiled_path}/#{id}.js"
dir = File.dirname(path)
template = context.call "Handlebars.precompile", File.read(file)
js = "#{@@header}\ntemplates['#{id}'] = template(#{template}); #{@@footer}"
FileUtils.mkdir_p(dir) unless File.exists?(dir)
File.open(path, 'w') { |file| file.write(js) }
end
protected
# Returns the JavaScript context
def context
@context ||= self.set_context
end
# Compiles and caches the handlebars JavaScript
def set_context
handlebars_source = File.read(File.dirname(__FILE__) + '/vendor/handlebars.js')
@context = ExecJS.compile handlebars_source
end
end
end

1573
lib/handlebars/vendor/handlebars.js vendored Normal file

File diff suppressed because it is too large Load Diff

8
lib/tasks/jasmine.rake Normal file
View File

@ -0,0 +1,8 @@
begin
require 'jasmine'
load 'jasmine/tasks/jasmine.rake'
rescue LoadError
task :jasmine do
abort "Jasmine is not available. In order to run jasmine, you must: (sudo) gem install jasmine"
end
end

9
lib/tasks/jst.rake Normal file
View File

@ -0,0 +1,9 @@
require 'lib/handlebars/handlebars'
namespace :jst do
desc 'precompile handlebars templates from app/views/jst to public/javascripts/jst'
task :compile do
Handlebars.compile 'app/views/jst', 'public/javascripts/jst'
end
end

View File

@ -0,0 +1,23 @@
(function() {
this.Template = (function() {
function Template(name, locals) {
this.name = name;
this.locals = locals;
if (this instanceof Template !== true) {
return new Template(name, locals).toHTML();
}
}
Template.prototype.toHTML = function(locals) {
if (locals == null) {
locals = this.locals;
}
return Handlebars.templates[this.name](locals);
};
Template.prototype.toElement = function(locals) {
var html;
html = this.toHTML(locals);
return jQuery('<div/>').html(html);
};
return Template;
})();
}).call(this);

View File

@ -0,0 +1,212 @@
// lib/handlebars/base.js
var Handlebars = {};
Handlebars.VERSION = "1.0.beta.2";
Handlebars.helpers = {};
Handlebars.partials = {};
Handlebars.registerHelper = function(name, fn, inverse) {
if(inverse) { fn.not = inverse; }
this.helpers[name] = fn;
};
Handlebars.registerPartial = function(name, str) {
this.partials[name] = str;
};
Handlebars.registerHelper('helperMissing', function(arg) {
if(arguments.length === 2) {
return undefined;
} else {
throw new Error("Could not find property '" + arg + "'");
}
});
Handlebars.registerHelper('blockHelperMissing', function(context, options) {
var inverse = options.inverse || function() {}, fn = options.fn;
var ret = "";
var type = Object.prototype.toString.call(context);
if(type === "[object Function]") {
context = context();
}
if(context === true) {
return fn(this);
} else if(context === false || context == null) {
return inverse(this);
} else if(type === "[object Array]") {
if(context.length > 0) {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn(context[i]);
}
} else {
ret = inverse(this);
}
return ret;
} else {
return fn(context);
}
});
Handlebars.registerHelper('each', function(context, options) {
var fn = options.fn, inverse = options.inverse;
var ret = "";
if(context && context.length > 0) {
for(var i=0, j=context.length; i<j; i++) {
ret = ret + fn(context[i]);
}
} else {
ret = inverse(this);
}
return ret;
});
Handlebars.registerHelper('if', function(context, options) {
if(!context || Handlebars.Utils.isEmpty(context)) {
return options.inverse(this);
} else {
return options.fn(this);
}
});
Handlebars.registerHelper('unless', function(context, options) {
var fn = options.fn, inverse = options.inverse;
options.fn = inverse;
options.inverse = fn;
return Handlebars.helpers['if'].call(this, context, options);
});
Handlebars.registerHelper('with', function(context, options) {
return options.fn(context);
});
;
// lib/handlebars/utils.js
Handlebars.Exception = function(message) {
var tmp = Error.prototype.constructor.apply(this, arguments);
for (var p in tmp) {
if (tmp.hasOwnProperty(p)) { this[p] = tmp[p]; }
}
};
Handlebars.Exception.prototype = new Error;
// Build out our basic SafeString type
Handlebars.SafeString = function(string) {
this.string = string;
};
Handlebars.SafeString.prototype.toString = function() {
return this.string.toString();
};
(function() {
var escape = {
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#x27;",
"`": "&#x60;"
};
var badChars = /&(?!\w+;)|[<>"'`]/g;
var possible = /[&<>"'`]/;
var escapeChar = function(chr) {
return escape[chr] || "&amp;";
};
Handlebars.Utils = {
escapeExpression: function(string) {
// don't escape SafeStrings, since they're already safe
if (string instanceof Handlebars.SafeString) {
return string.toString();
} else if (string == null || string === false) {
return "";
}
if(!possible.test(string)) { return string; }
return string.replace(badChars, escapeChar);
},
isEmpty: function(value) {
if (typeof value === "undefined") {
return true;
} else if (value === null) {
return true;
} else if (value === false) {
return true;
} else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) {
return true;
} else {
return false;
}
}
};
})();;
// lib/handlebars/vm.js
Handlebars.VM = {
template: function(templateSpec) {
// Just add water
var container = {
escapeExpression: Handlebars.Utils.escapeExpression,
invokePartial: Handlebars.VM.invokePartial,
programs: [],
program: function(i, fn, data) {
var programWrapper = this.programs[i];
if(data) {
return Handlebars.VM.program(fn, data);
} else if(programWrapper) {
return programWrapper;
} else {
programWrapper = this.programs[i] = Handlebars.VM.program(fn);
return programWrapper;
}
},
programWithDepth: Handlebars.VM.programWithDepth,
noop: Handlebars.VM.noop
};
return function(context, options) {
options = options || {};
return templateSpec.call(container, Handlebars, context, options.helpers, options.partials, options.data);
};
},
programWithDepth: function(fn, data, $depth) {
var args = Array.prototype.slice.call(arguments, 2);
return function(context, options) {
options = options || {};
return fn.apply(this, [context, options.data || data].concat(args));
};
},
program: function(fn, data) {
return function(context, options) {
options = options || {};
return fn(context, options.data || data);
};
},
noop: function() { return ""; },
invokePartial: function(partial, name, context, helpers, partials) {
if(partial === undefined) {
throw new Handlebars.Exception("The partial " + name + " could not be found");
} else if(partial instanceof Function) {
return partial(context, {helpers: helpers, partials: partials});
} else if (!Handlebars.compile) {
throw new Handlebars.Exception("The partial " + name + " could not be compiled when running in vm mode");
} else {
partials[name] = Handlebars.compile(partial);
return partials[name](context, {helpers: helpers, partials: partials});
}
}
};
Handlebars.template = Handlebars.VM.template;
;

View File

@ -0,0 +1,15 @@
!function() { var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
templates['/CustomList/content'] = template(function (Handlebars,depth0,helpers,partials,data) {
helpers = helpers || Handlebars.helpers;
var buffer = "", stack1, self=this, functionType="function", helperMissing=helpers.helperMissing, undef=void 0, escapeExpression=this.escapeExpression;
stack1 = helpers.howdy || depth0.howdy;
if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); }
else if(stack1=== undef) { stack1 = helperMissing.call(depth0, "howdy", { hash: {} }); }
buffer += escapeExpression(stack1) + " ";
stack1 = helpers.dowdy || depth0.dowdy;
if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, { hash: {} }); }
else if(stack1=== undef) { stack1 = helperMissing.call(depth0, "dowdy", { hash: {} }); }
buffer += escapeExpression(stack1) + " fasdfasfd \n";
return buffer;}); }()

View File

@ -0,0 +1,17 @@
describe "Template", ->
it 'should create an HTML string from a template', ->
# templates names derived from path in app/views/jst/
# here the file app/views/jst/test.handlebars becomes 'test'
template = new Template 'test'
html = template.toHTML(foo: 'bar')
expect(html).toEqual('bar')
it 'should create a collection of DOM elements', ->
template = new Template 'test'
element = template.toElement(foo: 'bar')
expect(element.html()).toEqual('bar')
it 'should return the HTML string when called w/o new', ->
html = Template('test', {foo: 'bar'})
expect(html).toEqual('bar')

View File

@ -0,0 +1,27 @@
(function() {
describe("Template", function() {
it('should create an HTML string from a template', function() {
var html, template;
template = new Template('test');
html = template.toHTML({
foo: 'bar'
});
return expect(html).toEqual('bar');
});
it('should create a collection of DOM elements', function() {
var element, template;
template = new Template('test');
element = template.toElement({
foo: 'bar'
});
return expect(element.html()).toEqual('bar');
});
return it('should return the HTML string when called w/o new', function() {
var html;
html = Template('test', {
foo: 'bar'
});
return expect(html).toEqual('bar');
});
});
}).call(this);

View File

@ -0,0 +1 @@
<div id="fixture"></div>

View File

@ -0,0 +1,28 @@
// A precompiled Handlebars template
(function() {
var template = Handlebars.template,
templates = Handlebars.templates = Handlebars.templates || {};
templates['test'] = template(function(Handlebars, depth0, helpers, partials, data) {
helpers = helpers || Handlebars.helpers;
var stack1,
self = this,
functionType = "function",
helperMissing = helpers.helperMissing,
undef = void 0,
escapeExpression = this.escapeExpression;
stack1 = helpers.foo || depth0.foo;
if (typeof stack1 === functionType) {
stack1 = stack1.call(depth0, {
hash: {}
});
}
else if (stack1 === undef) {
stack1 = helperMissing.call(depth0, "foo", {
hash: {}
});
}
return escapeExpression(stack1);
});
})()

View File

@ -0,0 +1,254 @@
var readFixtures = function() {
return jasmine.getFixtures().proxyCallTo_('read', arguments);
};
var loadFixtures = function() {
jasmine.getFixtures().proxyCallTo_('load', arguments);
};
var setFixtures = function(html) {
jasmine.getFixtures().set(html);
};
var sandbox = function(attributes) {
return jasmine.getFixtures().sandbox(attributes);
};
var spyOnEvent = function(selector, eventName) {
jasmine.JQuery.events.spyOn(selector, eventName);
}
jasmine.getFixtures = function() {
return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures();
};
jasmine.Fixtures = function() {
this.containerId = 'jasmine-fixtures';
this.fixturesCache_ = {};
this.fixturesPath = 'spec/javascripts/fixtures';
};
jasmine.Fixtures.prototype.set = function(html) {
this.cleanUp();
this.createContainer_(html);
};
jasmine.Fixtures.prototype.load = function() {
this.cleanUp();
this.createContainer_(this.read.apply(this, arguments));
};
jasmine.Fixtures.prototype.read = function() {
var htmlChunks = [];
var fixtureUrls = arguments;
for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) {
htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex]));
}
return htmlChunks.join('');
};
jasmine.Fixtures.prototype.clearCache = function() {
this.fixturesCache_ = {};
};
jasmine.Fixtures.prototype.cleanUp = function() {
$('#' + this.containerId).remove();
};
jasmine.Fixtures.prototype.sandbox = function(attributes) {
var attributesToSet = attributes || {};
return $('<div id="sandbox" />').attr(attributesToSet);
};
jasmine.Fixtures.prototype.createContainer_ = function(html) {
var container = $('<div id="' + this.containerId + '" />');
container.html(html);
$('body').append(container);
};
jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) {
if (typeof this.fixturesCache_[url] == 'undefined') {
this.loadFixtureIntoCache_(url);
}
return this.fixturesCache_[url];
};
jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) {
var self = this;
var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl;
$.ajax({
async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded
cache: false,
dataType: 'html',
url: url,
success: function(data) {
self.fixturesCache_[relativeUrl] = data;
}
});
};
jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) {
return this[methodName].apply(this, passedArguments);
};
jasmine.JQuery = function() {};
jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
return $('<div/>').append(html).html();
};
jasmine.JQuery.elementToString = function(element) {
return $('<div />').append(element.clone()).html();
};
jasmine.JQuery.matchersClass = {};
(function(namespace) {
var data = {
spiedEvents: {},
handlers: []
};
namespace.events = {
spyOn: function(selector, eventName) {
var handler = function(e) {
data.spiedEvents[[selector, eventName]] = e;
};
$(selector).bind(eventName, handler);
data.handlers.push(handler);
},
wasTriggered: function(selector, eventName) {
return !!(data.spiedEvents[[selector, eventName]]);
},
cleanUp: function() {
data.spiedEvents = {};
data.handlers = [];
}
}
})(jasmine.JQuery);
(function(){
var jQueryMatchers = {
toHaveClass: function(className) {
return this.actual.hasClass(className);
},
toBeVisible: function() {
return this.actual.is(':visible');
},
toBeHidden: function() {
return this.actual.is(':hidden');
},
toBeSelected: function() {
return this.actual.is(':selected');
},
toBeChecked: function() {
return this.actual.is(':checked');
},
toBeEmpty: function() {
return this.actual.is(':empty');
},
toExist: function() {
return this.actual.size() > 0;
},
toHaveAttr: function(attributeName, expectedAttributeValue) {
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
},
toHaveId: function(id) {
return this.actual.attr('id') == id;
},
toHaveHtml: function(html) {
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
},
toHaveText: function(text) {
if (text && jQuery.isFunction(text.test)) {
return text.test(this.actual.text());
} else {
return this.actual.text() == text;
}
},
toHaveValue: function(value) {
return this.actual.val() == value;
},
toHaveData: function(key, expectedValue) {
return hasProperty(this.actual.data(key), expectedValue);
},
toBe: function(selector) {
return this.actual.is(selector);
},
toContain: function(selector) {
return this.actual.find(selector).size() > 0;
},
toBeDisabled: function(selector){
return this.actual.attr("disabled") == true;
}
};
var hasProperty = function(actualValue, expectedValue) {
if (expectedValue === undefined) {
return actualValue !== undefined;
}
return actualValue == expectedValue;
};
var bindMatcher = function(methodName) {
var builtInMatcher = jasmine.Matchers.prototype[methodName];
jasmine.JQuery.matchersClass[methodName] = function() {
if (this.actual instanceof jQuery) {
var result = jQueryMatchers[methodName].apply(this, arguments);
this.actual = jasmine.JQuery.elementToString(this.actual);
return result;
}
if (builtInMatcher) {
return builtInMatcher.apply(this, arguments);
}
return false;
};
};
for(var methodName in jQueryMatchers) {
bindMatcher(methodName);
}
})();
beforeEach(function() {
this.addMatchers(jasmine.JQuery.matchersClass);
this.addMatchers({
toHaveBeenTriggeredOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been triggered on" + selector,
"Expected event " + this.actual + " not to have been triggered on" + selector
];
};
return jasmine.JQuery.events.wasTriggered(selector, this.actual);
}
})
});
afterEach(function() {
jasmine.getFixtures().cleanUp();
jasmine.JQuery.events.cleanUp();
});

View File

@ -0,0 +1,76 @@
# src_files
#
# Return an array of filepaths relative to src_dir to include before jasmine specs.
# Default: []
#
# EXAMPLE:
#
# src_files:
# - lib/source1.js
# - lib/source2.js
# - dist/**/*.js
#
src_files:
- public/javascripts/**/*.js
# stylesheets
#
# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
# Default: []
#
# EXAMPLE:
#
# stylesheets:
# - css/style.css
# - stylesheets/*.css
#
stylesheets:
- stylesheets/**/*.css
# helpers
#
# Return an array of filepaths relative to spec_dir to include before jasmine specs.
# Default: ["helpers/**/*.js"]
#
# EXAMPLE:
#
# helpers:
# - helpers/**/*.js
#
helpers:
- helpers/**/*.js
# spec_files
#
# Return an array of filepaths relative to spec_dir to include.
# Default: ["**/*[sS]pec.js"]
#
# EXAMPLE:
#
# spec_files:
# - **/*[sS]pec.js
#
spec_files:
- '**/*[sS]pec.js'
# src_dir
#
# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
# Default: project root
#
# EXAMPLE:
#
# src_dir: public
#
src_dir:
# spec_dir
#
# Spec directory path. Your spec_files must be returned relative to this path.
# Default: spec/javascripts
#
# EXAMPLE:
#
# spec_dir: spec/javascripts
#
spec_dir: spec/javascripts

View File

@ -0,0 +1,23 @@
module Jasmine
class Config
# Add your overrides or custom config code here
end
end
# Note - this is necessary for rspec2, which has removed the backtrace
module Jasmine
class SpecBuilder
def declare_spec(parent, spec)
me = self
example_name = spec["name"]
@spec_ids << spec["id"]
backtrace = @example_locations[parent.description + " " + example_name]
parent.it example_name, {} do
me.report_spec(spec["id"])
end
end
end
end

View File

@ -0,0 +1,32 @@
$:.unshift(ENV['JASMINE_GEM_PATH']) if ENV['JASMINE_GEM_PATH'] # for gem testing purposes
require 'rubygems'
require 'jasmine'
jasmine_config_overrides = File.expand_path(File.join(File.dirname(__FILE__), 'jasmine_config.rb'))
require jasmine_config_overrides if File.exist?(jasmine_config_overrides)
if Jasmine::rspec2?
require 'rspec'
else
require 'spec'
end
jasmine_config = Jasmine::Config.new
spec_builder = Jasmine::SpecBuilder.new(jasmine_config)
should_stop = false
if Jasmine::rspec2?
RSpec.configuration.after(:suite) do
spec_builder.stop if should_stop
end
else
Spec::Runner.configure do |config|
config.after(:suite) do
spec_builder.stop if should_stop
end
end
end
spec_builder.start
should_stop = true
spec_builder.declare_suites