Compare commits

...

No commits in common. "gh-pages" and "master" have entirely different histories.

105 changed files with 8775 additions and 667 deletions

24
.gitignore vendored
View File

@ -1,6 +1,22 @@
.sass-cache/ *.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
.tmp
node_modules/
.tmp-sprockets/
_site/ _site/
coverage/ .sass-cache/
tmp/
pkg/

15
Gemfile
View File

@ -1,10 +1,7 @@
source :rubygems source 'https://rubygems.org'
# Specify your gem's dependencies in flowerbox.gemspec
gemspec
gem 'guard-flowerbox', :path => '../guard-flowerbox'
gem 'redcarpet', '~> 1'
gem 'jekyll'
gem 'compass'
gem 'foreman'
gem 'guard'
gem 'haml'
gem 'guard-haml'
gem 'guard-compass'

View File

@ -1,11 +1,18 @@
# A sample Guardfile # A sample Guardfile
# More info at https://github.com/guard/guard#readme # More info at https://github.com/guard/guard#readme
group :rspec do
guard 'compass', :configuration_file => 'config/compass.rb' do guard 'rspec', :version => 2 do
watch(%r{^sass/(.*)\.s[ac]ss$}) watch(%r{^spec/.+_spec\.rb$})
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
watch('spec/spec_helper.rb') { "spec" }
end
end end
guard 'haml', :input => '_layouts/haml', :output => '_layouts' do group :wip do
watch(/^.+(\.html\.haml)/) guard 'cucumber', :cli => '-p wip' do
watch(%r{^features/.+\.feature$})
watch(%r{^features/support/.+$}) { 'features' }
watch(%r{^features/step_definitions/.*$}) { 'features' }
end
end end

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2012 John Bintz
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,3 +0,0 @@
jekyll: jekyll --server
guard: bundle exec guard

29
README.md Normal file
View File

@ -0,0 +1,29 @@
# Flowerbox
TODO: Write a gem description
## Installation
Add this line to your application's Gemfile:
gem 'flowerbox'
And then execute:
$ bundle
Or install it yourself as:
$ gem install flowerbox
## Usage
TODO: Write usage instructions here
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Added some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request

2
Rakefile Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"

View File

@ -1,4 +0,0 @@
markdown: redcarpet
auto: true
pygments: true

View File

@ -1,14 +0,0 @@
<html>
<head>
<title>Flowerbox</title>
<link href='http://fonts.googleapis.com/css?family=Pontano+Sans' rel='stylesheet' type='text/css' />
<link href='http://fonts.googleapis.com/css?family=Gorditas' rel='stylesheet' type='text/css' />
<link href='stylesheets/screen.css' rel='stylesheet' type='text/css' />
<link href='stylesheets/syntax.css' rel='stylesheet' type='text/css' />
</head>
<body>
<div id='container'>
{{ content }}
</div>
</body>
</html>

View File

@ -1,11 +0,0 @@
%html
%head
%title Flowerbox
%link{:href => 'http://fonts.googleapis.com/css?family=Pontano+Sans', :rel => :stylesheet, :type => 'text/css'}/
%link{:href => 'http://fonts.googleapis.com/css?family=Gorditas', :rel => :stylesheet, :type => 'text/css'}/
%link{:rel => :stylesheet, :href => 'stylesheets/screen.css', :type => 'text/css'}/
%link{:rel => :stylesheet, :href => 'stylesheets/syntax.css', :type => 'text/css'}/
%body
#container
{{ content }}

95
bin/flowerbox Executable file
View File

@ -0,0 +1,95 @@
#!/usr/bin/env ruby
if !ENV['BUNDLE_GEMFILE'] && File.file?('Gemfile')
require 'bundler/setup'
$: << File.expand_path('../../lib', __FILE__)
end
require 'flowerbox'
require 'thor'
class Flowerbox::CLI < Thor
include Thor::Actions
default_task :help
def help(*args)
if !args.first
puts "Flowerbox is a multi-environment, multi-runner JavaScript testing tool."
puts "It supports Jasmine and Cucumber.js, and it runs tests on Node.js and Selenium-driven browsers."
puts
end
super
end
desc "test [DIR]", "Run the specs found in spec dir, loading spec_helper.rb for configuration details"
method_options :pwd => :string, [:quiet, '-q'] => false, :env_options => nil, :runners => :string, :runner => :string, :verbose_server => false
def test(dir = "spec/javascripts", *files)
Dir.chdir(pwd) do
result = Flowerbox.run(dir, options.dup.merge(:files => files))
Flowerbox.cleanup!
exit result
end
end
desc "debug DIR", "Start the Flowerbox server to help debug loading issues."
method_options :pwd => :string, :env_options => nil, :runners => :string, :runner => :string, :verbose_server => false
def debug(dir = "spec/javascripts")
Dir.chdir(pwd) do
Flowerbox.debug(dir, options.dup)
end
end
desc "transplant DIR", "Convert an existing JavaScript testing project to Flowerbox"
long_desc <<-TXT
`flowerbox transplant` converts an existing JavaScript testing project type
to Flowerbox. Currently, you can transplant the following types of projects:
* Pivotal Labs Jasmine gem-style
\x5 (also covers jasmine-headless-webkit)
These types of projects live in `spec/javascripts` (or the specified directory)
and have the file `support/jasmine.yml` that defines what files are loaded
at what parts in the test load process. `jasmine.yml` is converted to a
Flowerbox `spec_helper.rb` file and placed into `spec/javascripts`.
Flowerbox will ask before overwriting existing files.
TXT
def transplant(dir)
Flowerbox.transplant(dir)
end
desc "clean", "Clean the Sprockets cache"
def clean
FileUtils.rm_rf(Flowerbox::CACHE_DIR)
puts "Sprockets cache cleaned."
end
desc "plant TYPE [DIR]", "Start a new Flowerbox project of TYPE, potentially specifying a different DIR to install"
def plant(type, dir = nil)
env = Flowerbox::TestEnvironment.for(type)
self.class.source_root(Flowerbox.path.join(env.plant_source))
directory('.', dir || env.plant_target)
end
def method_missing(method, *args)
if File.directory?(method.to_s)
test(method.to_s, *args)
else
super
end
end
no_tasks do
def pwd
options[:pwd] || Dir.pwd
end
end
end
Flowerbox::CLI.start

View File

@ -1,3 +0,0 @@
css_dir = 'stylesheets'
sass_dir = 'sass'

2
config/cucumber.yml Normal file
View File

@ -0,0 +1,2 @@
default: -f pretty
wip: --tags @wip -f pretty

View File

@ -0,0 +1,52 @@
Feature: Basic Run
Background:
Given I have the file "spec/javascripts/cat_spec.js.coffee" with the content:
"""
#= require cat
describe 'Cat', ->
describe '#meow', ->
it 'should meow', ->
cat = new Cat()
expect(cat.meow()).toEqual('meow')
"""
Given I have the file "lib/cat.js.coffee" with the content:
"""
class @Cat
meow: -> "meow"
"""
Scenario: Use the Node runner using Jasmine
Given I have the file "spec/javascripts/spec_helper.rb" with the content:
"""
Flowerbox.configure do |c|
c.test_with :jasmine
c.run_with :node
c.spec_patterns << "**/*_spec.*"
c.asset_paths << "lib"
c.reporters << :json
end
"""
When I run Flowerbox with "spec/javascripts"
Then I should have 1 test and 0 failures
@wip
Scenario: Use the Selenium runner using Jasmine
Given I have the file "spec/javascripts/spec_helper.rb" with the content:
"""
Flowerbox.configure do |c|
c.test_with :jasmine
c.run_with :chrome
c.spec_patterns << "**/*_spec.*"
c.asset_paths << "lib"
c.reporters << :json
end
"""
When I run Flowerbox with "spec/javascripts"
Then I should have 1 test and 0 failures

View File

@ -0,0 +1,7 @@
Given /^I have the file "([^"]*)" with the content:$/ do |file, string|
target = File.join(@root, file)
FileUtils.mkdir_p File.dirname(target)
File.open(target, 'wb') { |fh| fh.print string }
end

View File

@ -0,0 +1,9 @@
require 'json'
Then /^I should have (\d+) tests? and (\d+) failures?$/ do |tests, failures|
results = JSON.parse(@output).last.last
results['total'].should == tests.to_i
results['failures'].should == failures.to_i
end

View File

@ -0,0 +1,8 @@
When /^I run Flowerbox with "([^"]*)"$/ do |arguments|
command = %{bundle exec bin/flowerbox test #{arguments} -q --pwd #{@root} 2>&1}
@output = %x{#{command}}
raise StandardError.new("Flowerbox failed: #{@output}") if $?.exitstatus != 0
end

6
features/support/env.rb Normal file
View File

@ -0,0 +1,6 @@
Before do
@root = "tmp/features"
FileUtils.rm_rf @root
end

38
flowerbox.gemspec Normal file
View File

@ -0,0 +1,38 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/flowerbox/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["John Bintz"]
gem.email = ["john@coswellproductions.com"]
gem.description = %q{No-nonsense JavaScript testing solution.}
gem.summary = %q{No-nonsense JavaScript testing solution.}
gem.homepage = ""
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
gem.files = `git ls-files`.split("\n")
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.name = "flowerbox"
gem.require_paths = ["lib"]
gem.version = Flowerbox::VERSION
gem.add_development_dependency 'cucumber'
gem.add_development_dependency 'rspec'
gem.add_development_dependency 'simplecov'
gem.add_development_dependency 'mocha'
gem.add_development_dependency 'fakefs'
gem.add_development_dependency 'guard'
gem.add_development_dependency 'guard-flowerbox'
gem.add_development_dependency 'guard-rspec'
gem.add_development_dependency 'guard-cucumber'
gem.add_dependency 'jasmine-core'
gem.add_dependency 'thor'
gem.add_dependency 'selenium-webdriver'
gem.add_dependency 'rainbow'
gem.add_dependency 'sprockets'
gem.add_dependency 'sprockets-vendor_gems'
gem.add_dependency 'thin'
gem.add_dependency 'em-websocket'
gem.add_dependency 'coffee-script'
end

237
index.md
View File

@ -1,237 +0,0 @@
---
title: Flowerbox -- a multi-environment, multi-runner, super-easy JavaScript testing framework framework.
layout: default
---
# Flowerbox
## A multi-environment, multi-runner, super-easy JavaScript testing framework framework.
---
## Super-fast unit testing getting started super guide!
### (you need Firefox at the very least)
{% highlight sh %}
gem install flowerbox
flowerbox plant jasmine
flowerbox spec/javascripts
{% endhighlight %}
## ...and what about integration testing?
### (whoa nelly, pure JS Cucumber?)
{% highlight sh %}
flowerbox plant cucumber
flowerbox js-features
{% endhighlight %}
## Hey, I'm already using `jasmine-gem` or `jasmine-headless-webkit`
### (you're smart!)
{% highlight sh %}
flowerbox transplant spec/javascripts
flowerbox spec/javascripts
{% endhighlight %}
## I use Sprockets and need to configure asset paths and templates and stuff!
### (...may the force be with you...)
{% highlight ruby %}
# in spec/javascripts/spec_helper.rb
require 'haml-sprockets'
Flowerbox.configure do |c|
c.asset_paths << "app/assets/javascripts"
c.asset_paths << "vendor/assets/javascripts"
c.additional_files << "applications.js.coffee"
# ... other stuff ...
end
{% endhighlight %}
## Firefox is so 2008. Can I run my tests on Chrome?
### (we like Selenium here!)
{% highlight ruby %}
Flowerbox.configure do |c|
c.run_with :chrome
end
{% endhighlight %}
## Chrome is so 2010. Can I run my tests on node.js?
### (evented hipness! jsdom will give you a fake DOM to work with.)
{% highlight ruby %}
# make sure node and npm are in your path
# jsdom and ws get installed in your project under node_modules
Flowerbox.configure do |c|
c.run_with :node
end
{% endhighlight %}
{% highlight coffeescript %}
# make sure you bind your global things to this
class @MyCoolClass extends @MyBaseClass
cool: -> "yeah man"
{% endhighlight %}
## Where'd headless support go?!
No, I knew you'd ask. Read the long bit of text below. _tl;dr_: Selenium with Chrome is
pretty much the same setup, just as fast, and gives you stack traces and other debugging assistance.
---
# Plant some Jasmine
---
# Plant a Cucumber or two
## Make a feature like in big boy Cucumber
### (that's 'cause it *is* big boy Cucumber, just in JS form)
{% highlight gherkin %}
# js-features/features/my_cool_thing.feature
Feature: My Cool Thing
Scenario: Show a cool thing
Given I have a cool thing
And I have a cool thing viewer
When I render the viewer
Then I should see the cool thing in the viewer
@wip # <= tags work, too!
Scenario: Maybe do something else
Given something
When yadda
Then BAM
{% endhighlight %}
## Write some steps
### (Flowerbox gives you a donut-full of syntactic sugar that wraps around Cucumber.js. Be sure to drink lots of water!)
{% highlight coffeescript %}
# js-features/steps/given/i_have_a_cool_thing.js.coffee
Flowerbox.Given /^I have a cool thing$/, ->
@data =
one: 'two'
three: 'four'
@coolThing = new CoolThing(@data)
{% endhighlight %}
{% highlight coffeescript %}
# js-features/steps/given/i_have_a_cool_thing_viewer.js.coffee
Flowerbox.Given /^I have a cool thing viewer$/, ->
@coolThingViewer = new CoolThingViewer(@coolThing)
{% endhighlight %}
{% highlight coffeescript %}
# js-features/steps/when/i_render_the_viewer.js.coffee
Flowerbox.When /^I render the viewer$/, ->
@coolThingViewer.render()
{% endhighlight %}
{% highlight coffeescript %}
# js-features/steps/then/i_should_see_the_cool_thing.js.coffee
Flowerbox.Then /^I should see the cool thing in the viewer$/, ->
@expect(
@coolThingViewer.$('input[name="one"]').val()
).toEqual(@data.one)
@expect(
@coolThingViewer.$('input[name="three"]').val()
).toEqual(@data.three)
{% endhighlight %}
## Get some hooks up in here!
### (and other fun environment stuff)
{% highlight coffeescript %}
# js-features/support/env.js.coffee
#
#= require ../steps
Flowerbox.World ->
@Before (callback) ->
@something = 'is available everywhere'
callback()
@After (callback) ->
Put = 'some stuff back'
callback()
{% endhighlight %}
## Auto-generate missing steps
### (see [cucumber-step_writer](http://github.com/johnbintz/cucumber-step_writer) for my philosophy)
{% highlight ruby %}
# js-features/spec_helper.rb
Flowerbox.configure do |c|
c.report_with :verbose
c.reporters.add(
:step_writer,
:target => 'js-features/step_definitions',
:on_finish => lambda { |dir| system %{open #{dir}} }
)
end
{% endhighlight %}
---
# Flowerbox! Yea...wha?
So I (John) have been in this JavaScript testing game for a while, and, after about
three years of doing JS testing for Web apps, decided to finally take all the knowledge I had
and make something that was dead-simple to use.
## You already did once...
`jasmine-headless-webkit` was my first attempt, but it's a C++/Qt mess. Downloading all of
Qt is a total pain. Also, Qt4's JavaScript engine, JavaScriptCore, doesn't have stack trace
support, so it got pretty useless for me for bigger, messy projects (looking at you, Backbone).
Qt5 has V8, but whatever, I just decided to ditch the whole
compiling software thing and replace it with running tests in Selenium and/or node.js.
## Selenium faster than headless? But CI servers--
Sure, spinning up that instance of Firefox or Chrome is slower than starting up a small
QtWebKit widget, but there's a lot less that can go wrong when using a full-blown
browser. This also means your CI server config stays leaner. You may already have Qt installed
for capybara-webkit, but one less thing depending on Qt just makes your life (and mine) easier.
Also, when running Flowerbox with Guard, your browser stays open between test runs, so you only
pay the startup penalty once.
And, if you write everything correctly, you can just run your tests on node.js and not even have
to worry about a browser.
## But you said fake browsers are teh bad--
I changed my mind. As long as I'm not testing for browser-only things, like CSS interaction
or position elements, running tests on node.js with a browser-like DOM provided by jsdom
really is good enough. I use full-blown Ruby Cucumber if I need to test more complex browser
things.
## Cucumber.js? Full-stack testing is the only way!
Yeah, and it's slow. I'll carve out what I can in Cucumber.js while I work on Ruby Cucumber
features. Makes the process of getting those complex integration tests written a lot faster.
Also, I've changed my views on unit testing as a process. Unit testing for me is now about
two things:
* Code design through a liberal use of stubs and mocks.
* Really quickly running through blg blobs of inputs/outputs (parsers and such).
Treating unit testing this way taught me how to better organize and design my code, and I
only use it for really testing code when I need to test those big blobs of things.
This is my opinion, YMMV, Flowerbox lets you do what you want. Never use Cucumber.js support
if you don't want to. Write your integration bits in Jasmine. I don't mind.

View File

@ -0,0 +1,65 @@
Flowerbox =
debug: false
ping: ->
Flowerbox.contact('ping')
messageQueue: []
pulling: false
contact: (url, data...) ->
Flowerbox.started = true
if !Flowerbox.debug
message = [ url, data ]
Flowerbox.messageQueue.push(message)
if Flowerbox.socket && !Flowerbox.pulling
Flowerbox.queuePuller()
started: false
done: false
queuePuller: ->
Flowerbox.pulling = true
if Flowerbox.messageQueue.length > 0
message = Flowerbox.messageQueue.shift()
Flowerbox.socket.onmessage = (data) ->
if message[0] == 'results'
if __$instrument?
Flowerbox.socket.send JSON.stringify(['instrument', __$instrument]) {}, ->
Flowerbox.done = true
else
Flowerbox.done = true
Flowerbox.queuePuller()
Flowerbox.socket.send JSON.stringify(message)
else
setTimeout(Flowerbox.queuePuller, 10) if !Flowerbox.done
fail: ->
class Flowerbox.Exception
constructor: (@source, @name, @stack) ->
toJSON: ->
{ status: Flowerbox.Result.FAILURE, source: @source, name: @name, trace: { stack: @stack } }
class Flowerbox.Result
@SUCCESS = 'success'
@PENDING = 'pending'
@UNDEFINED = 'undefined'
@FAILURE = 'failure'
@SKIPPED = 'skipped'
constructor: (data) ->
for key, value of data
this[key] = value
this.status ||= Flowerbox.Result.SKIPPED
this.failures ||= []
toJSON: => this

View File

@ -0,0 +1,119 @@
#= require flowerbox/cucumber/reporter
Flowerbox.Cucumber ||= {}
Flowerbox.Cucumber.features = ->
Flowerbox.Cucumber.Features ||= []
Flowerbox.Cucumber.Features.join("\n")
Flowerbox.World = (code = null) ->
if code
Flowerbox.World.Code ||= []
Flowerbox.World.Code.push(code)
else
->
for code in (Flowerbox.World.Code || [])
code.apply(this)
Flowerbox.Matchers =
toEqual: (expected) ->
@message = "Expected #{@actual} #{@notMessage} equal #{expected}"
result = null
if @actual? && expected?
switch (typeof @actual)
when 'object'
result = true
for key, value of @actual
result = false if expected[key] != value
for key, value of expected
result = false if @actual[key] != value
if result == null
result = (@actual == expected)
result
Flowerbox.World ->
@assert = (what, message = 'failed') ->
throw new Error(message) if !what
@expect = (what) -> new Flowerbox.Matcher(what)
@addMatchers = (data) -> Flowerbox.Matcher.addMatchers(data)
Flowerbox.Matcher.matchers = {}
@addMatchers(Flowerbox.Matchers)
class Flowerbox.Matcher
@addMatchers: (data) ->
for method, code of data
Flowerbox.Matcher.matchers[method] = code
constructor: (@actual, @_negated = false) ->
@not = this.negated() if !@_negated
@notMessage = if @_negated then "to not" else "to"
generateMethod = (method) ->
(args...) -> @fail() if !@maybeNegate(Flowerbox.Matcher.matchers[method].apply(this, args))
for method, code of Flowerbox.Matcher.matchers
this[method] = generateMethod(method)
negated: ->
new Flowerbox.Matcher(@actual, true)
fail: ->
throw new Error(@message)
maybeNegate: (result) ->
result = !result if @_negated
result
Flowerbox.Step = (type, match, code) ->
Flowerbox.World.Code ||= []
Flowerbox.World.Code.push (args..., callback) ->
this[type] match, (args..., callback) =>
_pending = false
@pending = -> _pending = true
result = code.apply(this, args)
if result? and result.__prototype__ == Error
callback.fail(result)
else
if _pending then callback.pending("pending") else callback()
null
Flowerbox.Step.files ||= {}
Flowerbox.Step.matchFile = (name) ->
for key, data of Flowerbox.Step.files
[ regexp, filename ] = data
if name.match(regexp)
return filename
null
stepGenerator = (type) ->
Flowerbox[type] = (match, code) ->
if !Flowerbox.Step.files[match.toString()]
nextLine = false
if stack = (new Error()).stack
for line in stack.split('\n')
if nextLine
result = if line.match(/__F__/)
line.replace(/^.*__F__\/([^:]+:\d+).*$/, '$1')
else
line.replace(/^.*(\(| )([^:]+:\d+).*$/, '$2')
Flowerbox.Step.files[match.toString()] = [ match, result ]
break
if line.match(/(Given|When|Then)/)
nextLine = true
Flowerbox.Step(type, match, code)
stepGenerator(type) for type in [ 'Given', 'When', 'Then' ]

View File

@ -0,0 +1,67 @@
Flowerbox = Flowerbox || {}
Flowerbox.Cucumber = Flowerbox.Cucumber || {}
class Flowerbox.Cucumber.Reporter
nameParts: ->
[ @feature.getName(), @scenario.getName(), "#{this.type()} #{@step.getName()}" ]
type: ->
type = "Given"
if @step.isOutcomeStep()
type = "Then"
else if @step.isEventStep()
type = "When"
type
hear: (event, callback) ->
switch event.getName()
when 'BeforeFeatures'
@time = (new Date()).getTime()
Flowerbox.contact("starting")
when 'AfterFeatures'
Flowerbox.contact("results", (new Date()).getTime() - @time)
when 'BeforeFeature'
@feature = event.getPayloadItem('feature')
when 'BeforeScenario'
@scenario = event.getPayloadItem('scenario')
when 'BeforeStep'
@step = event.getPayloadItem('step')
Flowerbox.contact("start_test", @step.getName())
when 'StepResult'
stepResult = event.getPayloadItem('stepResult')
file = Flowerbox.Step.matchFile(@step.getName()) || "#{Flowerbox.UNKNOWN}:0"
result = new Flowerbox.Result(step_type: this.type(), source: 'cucumber', original_name: @step.getName(), name: this.nameParts(), file: file)
if stepResult.isSuccessful()
result.status = Flowerbox.Result.SUCCESS
else if stepResult.isPending()
result.status = Flowerbox.Result.PENDING
else if stepResult.isUndefined()
result.status = Flowerbox.Result.UNDEFINED
result.hasDataTable = @step.hasDataTable()
result.hasDocString = @step.hasDocString()
else if stepResult.isFailed()
result.status = Flowerbox.Result.FAILURE
error = stepResult.getFailureException()
stack = (error.stack || "message\n#{file}:0").split("\n")
failure = { runner: Flowerbox.environment, message: error.message, stack: stack }
result.failures.push(failure)
Flowerbox.contact("finish_test", result.toJSON())
callback()

View File

@ -0,0 +1,4 @@
window.onerror = (message, file, line) ->
Flowerbox.contact("log", message)
Flowerbox.contact("log", " #{file}:#{line}")

View File

@ -0,0 +1,57 @@
#= require flowerbox/jasmine/reporter
# because why not?
@context = @describe
getSplitName = (parts) ->
parts.push(String(@description).replace(/[\n\r]/g, ' '))
parts
jasmine.Suite.prototype.getSuiteSplitName = ->
this.getSplitName(if @parentSuite then @parentSuite.getSuiteSplitName() else [])
jasmine.Spec.prototype.getSpecSplitName = ->
this.getSplitName(@suite.getSuiteSplitName())
jasmine.Suite.prototype.getSplitName = getSplitName
jasmine.Spec.prototype.getSplitName = getSplitName
jasmine.Spec.prototype.addMatcherResult = (result) ->
for method in jasmine.Spec.beforeAddMatcherResult()
method.apply(result, [ this ])
@results_.addResult(result)
jasmine.Spec.beforeAddMatcherResult = ->
@_beforeAddMatcherResult ||= []
jasmine.Spec.beforeAddMatcherResult().push (spec) ->
@splitName = spec.getSpecSplitName()
Flowerbox.only = (envs..., code) ->
for env in envs
if Flowerbox.environment == env
describe("only in #{envs.join(', ')}", code)
jasmine.WaitsBlock.prototype._execute = jasmine.WaitsBlock.prototype.execute
jasmine.WaitsForBlock.prototype._execute = jasmine.WaitsForBlock.prototype.execute
pauseAndRun = (onComplete) ->
jasmine.getEnv().reporter.reportSpecWaiting()
this._execute ->
jasmine.getEnv().reporter.reportSpecRunning()
onComplete()
jasmine.WaitsBlock.prototype.execute = pauseAndRun
jasmine.WaitsForBlock.prototype.execute = pauseAndRun
for method in [ "reportSpecWaiting", "reportSpecRunning" ]
generator = (method) ->
(args...) ->
for reporter in @subReporters_
if reporter[method]?
reporter[method](args...)
jasmine.MultiReporter.prototype[method] = generator(method)

View File

@ -0,0 +1,5 @@
jasmine.Spec.beforeAddMatcherResult().push ->
if !@passed_
@trace = { stack: new Error().stack }
Flowerbox.fail = -> process.exit(1)

View File

@ -0,0 +1,53 @@
class jasmine.FlowerboxReporter
buildResult: (spec, overrides = {}) ->
data =
status: Flowerbox.Result.SUCCESS
source: 'jasmine'
name: spec.getSpecSplitName()
file: "#{Flowerbox.UNKNOWN}:0"
for key, value of overrides
data[key] = value
new Flowerbox.Result(data)
reportRunnerStarting: (runner) ->
@time = (new Date()).getTime()
Flowerbox.contact("starting")
reportSpecStarting: (spec) ->
Flowerbox.contact("start_test", spec.getSpecSplitName().join(" "))
if spec.description == 'encountered a declaration exception'
Flowerbox.contact("finish_test", this.buildResult(spec, status: Flowerbox.Result.FAILURE))
reportSpecWaiting: ->
@paused = true
Flowerbox.contact('pause_timer')
reportSpecRunning: ->
@paused = false
Flowerbox.contact('unpause_timer')
reportSpecResults: (spec) ->
this.reportSpecRunning() if @paused
result = this.buildResult(spec)
for item in spec.results().items_
if !item.passed_
result.status = Flowerbox.Result.FAILURE
stack = item.trace.stack || []
if stack.constructor != Array
stack = stack.split("\n")
failure = { runner: Flowerbox.environment, message: item.message, stack: stack }
result.failures.push(failure)
Flowerbox.contact("finish_test", result)
reportRunnerResults: (runner) ->
Flowerbox.contact("results", (new Date().getTime()) - @time)

View File

@ -0,0 +1,17 @@
jasmine.Spec.beforeAddMatcherResult().push ->
if !@passed_
@trace = { stack: [] }
try
lol.wut
catch e
if e.stack
file = switch Flowerbox.environment.toLowerCase()
when 'firefox'
e.stack.split("\n")[3]
when 'chrome'
e.stack.split("\n")[4]
else
"#{Flowerbox.UNKNOWN}:0"
@trace = { stack: [ @message, file ] }

109
lib/flowerbox.rb Normal file
View File

@ -0,0 +1,109 @@
require "flowerbox/version"
require 'rainbow'
require 'forwardable'
module Flowerbox
require 'flowerbox/core_ext/module'
require 'flowerbox/runner'
require 'flowerbox/run/base'
require 'flowerbox/run/test'
require 'flowerbox/run/debug'
require 'flowerbox/test_environment'
require 'flowerbox/result'
require 'flowerbox/result_set'
require 'flowerbox/gathered_result'
require 'flowerbox/reporter'
require 'flowerbox/configuration'
if defined?(Rails::Engine)
require 'flowerbox/rails/engine'
end
CACHE_DIR = 'tmp/sprockets'
class << self
attr_writer :quiet
attr_accessor :server
def reset!
@configuration = nil
end
def configuration
@configuration ||= Flowerbox::Configuration.new
end
def path
Pathname(File.expand_path('../..', __FILE__))
end
def configure(&block)
configuration.configure(&block)
end
def bare_coffeescript
@bare_coffeescript ||= true
end
def debug(dir, options = {})
Flowerbox::Run::Debug.execute(dir, options)
end
def run(dir, options = {})
Flowerbox.quiet = options[:quiet]
Flowerbox::Run::Test.execute(dir, options)
end
def quiet?
@quiet == true
end
def notify(msg)
if !Flowerbox.quiet?
puts msg
end
end
def browsers
@browsers ||= {}
end
def cleanup!
browsers.values.each do |browser|
begin
browser.close
rescue Errno::ECONNREFUSED => e
puts "Browser already closed.".foreground(:yellow)
rescue ::Selenium::WebDriver::Error::UnknownError => e
puts "Unknown browser error, pushing past it to close...".foreground(:yellow)
end
end
@browsers = {}
end
def transplant(dir)
Flowerbox::TestEnvironment.transplantable_environments.each do |env|
break if env.transplant(dir)
end
end
def cache_dir
File.join(CACHE_DIR, Flowerbox.test_environment.name)
end
def method_missing(method, *args)
if configuration.respond_to?(method)
configuration.send(method, *args)
else
super
end
end
end
end

View File

@ -0,0 +1,66 @@
require 'flowerbox/reporter_list'
require 'flowerbox/instrumented_files_list'
module Flowerbox
class Configuration
attr_writer :reporters, :backtrace_filter, :instrument_js
attr_accessor :port
attr_accessor :test_environment, :runner_environment, :bare_coffeescript
def spec_patterns
@spec_patterns ||= []
end
def asset_paths
@asset_paths ||= []
end
def reporters
@reporters ||= Flowerbox::ReporterList.new
end
def additional_files
@additional_files ||= []
end
def backtrace_filter
@backtrace_filter ||= []
end
def instrument_files
@instrument_files ||= Flowerbox::InstrumentedFilesList.new
end
def instrument_js?
!instrument_files.empty?
end
def test_with(what)
self.test_environment = Flowerbox::TestEnvironment.for(what)
end
def run_with(*whats)
self.runner_environment = whats.flatten.collect { |what| Flowerbox::Runner.for(what.to_s) }
end
def report_with(*whats)
self.reporters.clear!
whats.each { |what| self.reporters << what }
end
def configure
yield self
if spec_patterns.empty?
spec_patterns << "**/*_spec*"
spec_patterns << "*/*_spec*"
end
if reporters.empty?
reporters << :progress
end
end
end
end

View File

@ -0,0 +1,14 @@
module Flowerbox
module CoreExt
module Module
def find_constant(string)
const_get(constants.find { |f| f.to_s.downcase == string.to_s.downcase.gsub('_', '') })
end
def for(env)
find_constant(env).new
end
end
end
end

31
lib/flowerbox/failure.rb Normal file
View File

@ -0,0 +1,31 @@
module Flowerbox
class Failure < BaseResult
def pending?
message == "pending"
end
def skipped?
message == "skipped"
end
def undefined?
message[%r{^Step not defined}]
end
def print_progress
case message
when "pending"
print "P".foreground(:yellow)
when "skipped"
print "-".foreground(:cyan)
else
if undefined?
print "U".foreground(:yellow)
else
print "F".foreground(:red)
end
end
end
end
end

View File

@ -0,0 +1,30 @@
module Flowerbox
class GatheredResult
attr_reader :name
def initialize(name)
@name = name
end
def <<(result)
results << result
end
def results
@results ||= []
end
def successes
results.find_all(&:success?)
end
def failures
results.reject(&:success?)
end
def success?
@results.all?(&:success?)
end
end
end

View File

@ -0,0 +1,22 @@
require 'forwardable'
module Flowerbox
class InstrumentedFilesList
extend Forwardable
def_delegators :@files_list, :empty?
def initialize
@files_list = []
end
def <<(pattern)
@files_list << pattern
end
def include?(filename)
@files_list.any? { |pattern| filename[pattern] }
end
end
end

37
lib/flowerbox/rack.rb Normal file
View File

@ -0,0 +1,37 @@
module Flowerbox
class Rack
attr_accessor :runner, :sprockets
def initialize(runner, sprockets)
@runner, @sprockets = runner, sprockets
end
def call(env)
dup._call(env)
end
def _call(env)
if sprockets_file = env['PATH_INFO'][%r{/__F__(.*)$}, 1]
result = sprockets.call(env.merge('QUERY_STRING' => 'body=1', 'PATH_INFO' => sprockets_file))
result[1]['Cache-Control'] = 'max-age: 0; must-revalidate; no-store'
result[1].delete('ETag')
result
else
begin
template = runner.template
[ 200, { 'Content-type' => 'text/html' }, [ template ] ]
rescue => e
$stderr.puts
$stderr.puts e.message
$stderr.puts e.backtrace.join("\n")
$stderr.puts
[ 500, {}, [] ]
end
end
end
end
end

View File

@ -0,0 +1,15 @@
require 'rails/engine'
module Flowerbox
module Rails
class Engine < ::Rails::Engine
rake_tasks do
require 'flowerbox/task'
Flowerbox::Task.create("flowerbox:spec", :dir => "spec/javascripts")
Flowerbox::Task.create("flowerbox:integration", :dir => "js-features")
end
end
end
end

14
lib/flowerbox/reporter.rb Normal file
View File

@ -0,0 +1,14 @@
module Flowerbox
module Reporter
extend Flowerbox::CoreExt::Module
require 'flowerbox/reporter/file_display'
def self.for(env)
require "flowerbox/reporter/#{env}"
find_constant(env).new
end
end
end

View File

@ -0,0 +1,67 @@
require 'forwardable'
module Flowerbox
module Reporter
class Base
extend Forwardable
def_delegators :$stdout, :puts, :print
attr_accessor :options
def post_report_data
@post_report_data ||= {}
end
def report(gathered_results, data = {})
gathered_results.each do |result|
self.send("report_#{result.type}", result)
end
report_numeric_results(gathered_results, data)
post_report_data.each do |type, data|
puts
self.send("post_report_#{type}", data)
end
end
def report_success(result); end
def report_skipped(result) ; end
def report_failure(result) ; end
def report_pending(result) ; end
def report_undefined(result) ; end
def report_progress(result); end
def post_report_success(data) ; end
def post_report_skipped(data) ; end
def post_report_pending(data) ; end
def post_report_undefined(data)
self.puts "Some steps were not defined. Define them using the following code:".foreground(:yellow)
self.puts
data.each do |code|
self.puts code.foreground(:yellow)
end
end
def post_report_failed(data) ; end
def report_numeric_results(gathered_results, data = {}) ; end
def start(message) ; end
def log(message) ; end
protected
def numeric_results_for(gathered_results)
{
:total => gathered_results.length,
:failures => gathered_results.find_all(&:failure?).length,
:pending => gathered_results.find_all(&:pending?).length
}
end
end
end
end

View File

@ -0,0 +1,48 @@
require 'flowerbox/reporter/base'
module Flowerbox::Reporter
class ConsoleBase < Base
include FileDisplay
def start(message)
self.puts message
end
def report_success(result) ; end
def report_skipped(result) ; end
def report_pending(result)
puts
puts result.name.join(" - ").foreground(:yellow)
puts " Pending (finish defining the test)".foreground(:yellow) + ' ' + path_for(result)
end
def report_undefined(result)
(post_report_data[:undefined] ||= []) << result.test_environment.obtain_test_definition_for(result)
end
def report_failure(result)
puts
puts result.name.join(" - ").foreground(:red)
result.failures.each do |failure|
puts " " + failure.message.foreground(:red) + " [" + failure.runners.join(',') + "] " + path_for(failure)
puts failure.filtered_stack.join("\n").foreground(:red) if failure.exception?
end
end
def report_numeric_results(gathered_results, data = {})
results = numeric_results_for(gathered_results)
output = "#{results[:total]} total, #{results[:failures]} failed, #{results[:pending]} pending, #{data[:time].to_f / 1000} (#{data[:realtime]}.0) secs."
color = :green
color = :yellow if results[:pending] > 0
color = :red if results[:failures] > 0
puts
puts output.foreground(color)
puts
end
end
end

View File

@ -0,0 +1,13 @@
module Flowerbox::Reporter
module FileDisplay
private
def path_for(result)
if result.line.gsub(%r{[^0-9]}, '').to_i == 0
''
else
result.translated_file_and_line.foreground(:cyan)
end
end
end
end

View File

@ -0,0 +1,21 @@
require 'flowerbox/reporter/base'
require 'json'
module Flowerbox::Reporter
class JSON < Base
def output
@output ||= []
end
def log(message)
@output << [ :log, message ]
end
def report_numeric_results(gathered_results, data = {})
output << [ :results, numeric_results_for(gathered_results) ]
self.puts ::JSON.dump(output)
end
end
end

View File

@ -0,0 +1,31 @@
require 'flowerbox/reporter/console_base'
module Flowerbox::Reporter
class Progress < ConsoleBase
def report_progress(result)
print self.send("progress_#{result.type}")
$stdout.flush
end
def progress_skipped
"-".foreground(:blue)
end
def progress_success
".".foreground(:green)
end
def progress_failure
"F".foreground(:red)
end
def progress_pending
"P".foreground(:yellow)
end
def progress_undefined
"U".foreground(:yellow)
end
end
end

View File

@ -0,0 +1,28 @@
require 'flowerbox/reporter/base'
module Flowerbox::Reporter
class StepWriter < Base
def report_undefined(result)
target = File.join(options[:target], result.step_type.downcase, result.original_name.strip.downcase.gsub(%r{[^\w]+}, '_')) + '.js'
if result.test_environment.primarily_coffeescript?
target << '.coffee'
end
if !File.file?(target)
self.puts "Writing #{target}..."
File.open(target, 'wb') { |fh| fh.print result.test_environment.obtain_test_definition_for(result) }
post_report_data[:undefined] = true
end
end
def post_report_undefined(data)
if post_report_data[:undefined] && options[:on_finish]
options[:on_finish].call(options[:target])
end
end
end
end

View File

@ -0,0 +1,59 @@
require 'flowerbox/reporter/console_base'
module Flowerbox::Reporter
class Verbose < ConsoleBase
def initialize
@name_stack = []
end
def indent(text, count)
" " * count + text
end
def report_progress(result)
if result.name
result.name.each_with_index do |name, index|
if @name_stack[index] != name
@name_stack = []
if name != result.name.last
message = name
else
message = self.send("progress_#{result.type}", result)
if !(file_display = path_for(result)).empty?
message << " # #{file_display}"
end
end
puts indent(message, index)
end
end
@name_stack = result.name.dup
else
puts result.data
end
end
def progress_skipped(result)
result.name.last.foreground(:blue)
end
def progress_success(result)
result.name.last.foreground(:green)
end
def progress_failure(result)
result.name.last.foreground(:red)
end
def progress_pending(result)
"#{result.name.last} (Pending)".foreground(:yellow)
end
def progress_undefined(result)
"#{result.name.last} (Undefined)".foreground(:yellow)
end
end
end

View File

@ -0,0 +1,46 @@
require 'forwardable'
module Flowerbox
class ReporterList
include Enumerable
extend Forwardable
def_delegators :@reporters, :empty?
def each(&block)
@reporters.each(&block)
end
def initialize
clear!
end
def clear!
@reporters = []
end
def <<(reporter)
@reporters << Flowerbox::Reporter.for(reporter)
end
def add(reporter, options)
reporter_obj = Flowerbox::Reporter.for(reporter)
reporter_obj.options = options
@reporters << reporter_obj
end
def start(message)
on_each_reporter(:start, message)
end
def log(message)
on_each_reporter(:log, message)
end
private
def on_each_reporter(method, message)
@reporters.each { |reporter| reporter.send(method, message) }
end
end
end

30
lib/flowerbox/result.rb Normal file
View File

@ -0,0 +1,30 @@
module Flowerbox
module Result
extend Flowerbox::CoreExt::Module
module Cucumber
extend Flowerbox::CoreExt::Module
end
module Jasmine
extend Flowerbox::CoreExt::Module
end
require 'flowerbox/result/base'
require 'flowerbox/result/exception'
require 'flowerbox/result/failure'
require 'flowerbox/result/pending'
require 'flowerbox/result/failure_message'
require 'flowerbox/result/file_info'
class << self
def for(test_env, type)
require "flowerbox/result/#{test_env}/#{type}"
test_group = find_constant(test_env)
test_group.find_constant(type)
end
end
end
end

View File

@ -0,0 +1,74 @@
require 'forwardable'
module Flowerbox
module Result
class Base
require 'flowerbox/result/file_info'
include Flowerbox::Result::FileInfo
extend Forwardable
attr_reader :data
def_delegators :data, :[]
def type
@type ||= self.class.name.split("::").last.downcase.to_sym
end
def initialize(data)
@data = data
end
def name
data['name']
end
def message
data['message']
end
def runners
data['runners']
end
def file
data['file']
end
def ==(other)
name == other.name
end
def <=>(other)
self.name.length <=> other.name.length
end
def runners
@runners ||= []
end
def success?
false
end
def failure?
!success?
end
def pending?
false
end
def <<(other)
runners
@runners += other.runners
end
def test_environment
Flowerbox.test_environment
end
end
end
end

View File

View File

@ -0,0 +1,7 @@
module Flowerbox::Result
module Cucumber
class Failure < Flowerbox::Result::Failure
end
end
end

View File

@ -0,0 +1,8 @@
module Flowerbox::Result
module Cucumber
class Pending < Flowerbox::Result::Pending
end
end
end

View File

@ -0,0 +1,7 @@
module Flowerbox::Result
module Cucumber
class Skipped < Flowerbox::Result::Pending
end
end
end

View File

@ -0,0 +1,10 @@
module Flowerbox::Result
module Cucumber
class Success < Flowerbox::Result::Base
def success?
true
end
end
end
end

View File

@ -0,0 +1,14 @@
module Flowerbox::Result
module Cucumber
class Undefined < Flowerbox::Result::Pending
def step_type
@data['step_type']
end
def original_name
@data['original_name']
end
end
end
end

View File

@ -0,0 +1,6 @@
module Flowerbox::Result
class Exception < Base
end
end

View File

@ -0,0 +1,20 @@
module Flowerbox::Result
class Failure < Flowerbox::Result::Base
def failures
@failures ||= @data['failures'].collect { |fail| FailureMessage.new(fail) }
end
def <<(other)
super
other.failures.each do |failure|
if existing_failure = failures.find { |f| f.message == failure.message }
existing_failure.runners << failure.runner
else
failures << failure
end
end
end
end
end

View File

@ -0,0 +1,56 @@
module Flowerbox::Result
class FailureMessage
include Flowerbox::Result::FileInfo
attr_reader :data
def initialize(data)
@data = data
end
def message
@data['message']
end
def file
file = first_local_stack
if file['__F__']
file[%r{^.*__F__/([^:]+:\d+)}, 1]
else
file[%r{\(([^:]+\:\d+)}, 1]
end
end
def runner
@data['runner']
end
def runners
@runners ||= [ runner ]
end
def stack
@data['stack'] || []
end
def filtered_stack
filtered_stack = stack.reject { |line| Flowerbox.backtrace_filter.any? { |filter| line[filter] } }
filtered_stack.shift if exception?
filter_coffeescript_lines(filtered_stack)
end
def first_local_stack
@first_local_stack ||= (stack[1..-1] || []).find do |line|
!system_files.any? { |file| line[%r{\(#{file}}] }
end || stack[1] || ''
end
def exception?
(stack[0] || '')[%r{^.+Error: }]
end
end
end

View File

@ -0,0 +1,52 @@
module Flowerbox::Result::FileInfo
UNKNOWN = '__unknown__'
def translated_file
return @translated_file if @translated_file
if filename == UNKNOWN
@translated_file = UNKNOWN
else
@translated_file = Flowerbox.test_environment.actual_path_for(filename)
end
@translated_file
end
def file_translated?
translated_file[%r{\.coffee$}]
end
def filename
file.to_s.split(":").first || UNKNOWN
end
def line_number
return @line_number if @line_number
@line_number = file.to_s.split(":")[1]
@line_number = "~#{coffeescript_line_to_js_line(@line_number)}" if file_translated?
@line_number ||= "0"
end
alias :line :line_number
def translated_file_and_line
"#{translated_file.gsub(%r{^#{Dir.pwd}/}, '')}:#{line_number}"
end
def system_files
Flowerbox.test_environment.system_files
end
def filter_coffeescript_lines(lines)
[ lines ].flatten.collect { |line|
line.gsub(%r{\.coffee:(\d+)}) { |_| ".coffee:~#{coffeescript_line_to_js_line($1)}" }
}
end
def coffeescript_line_to_js_line(number)
(number.to_i * 0.67 + 1).to_i
end
end

View File

@ -0,0 +1,7 @@
module Flowerbox::Result
module Jasmine
class Failure < Flowerbox::Result::Failure
end
end
end

View File

@ -0,0 +1,8 @@
module Flowerbox::Result
module Jasmine
class Success < Flowerbox::Result::Base
def success? ; true ; end
end
end
end

View File

@ -0,0 +1,14 @@
module Flowerbox::Result
class Pending < Base
def pending?
true
end
def failure?
false
end
def failures; [] ; end
end
end

102
lib/flowerbox/result_set.rb Normal file
View File

@ -0,0 +1,102 @@
module Flowerbox
class ResultSet
attr_reader :results, :options
attr_accessor :time
def self.from_results(results, options)
results = results.collect do |result|
result['runner'] = options[:runner]
if name = result['name']
Flowerbox::Result.for(result['source'], result['status']).new(result)
else
Flowerbox::Result::Exception.new(result)
end
end.flatten
new(results, options)
end
def self.for(runner)
new(results, :runner => runner)
end
def reporters
Flowerbox.reporters
end
def initialize(results = [], options = {})
@results, @options = results, options
end
def <<(other)
other.results.each do |other_result|
if existing_result = results.find { |result| result == other_result }
existing_result << other_result
else
results << other_result
end
end
end
def exitstatus
if results.any?(&:failure?)
1
elsif results.any?(&:pending?)
2
else
0
end
end
def print(data = {})
reporters.each { |reporter| reporter.report(flattened_gathered_results, data) }
end
def total_tests
results.length
end
def total_failures
results.reject(&:success?).length
end
def print_progress
@results.each do |result|
reporters.each do |reporter|
begin
reporter.report_progress(result)
rescue => e
puts e.message
puts e.backtrace.join("\n")
raise e
end
end
end
end
def gathered_results
return @gathered_results if @gathered_results
@gathered_results = []
results.each do |result|
if !(gathered_result = @gathered_results.find { |g| g.name == result.name })
gathered_result = GatheredResult.new(result.name)
@gathered_results << gathered_result
end
gathered_result << result
end
@gathered_results
end
def flattened_gathered_results
gathered_results.collect(&:results).flatten
end
end
end

79
lib/flowerbox/run/base.rb Normal file
View File

@ -0,0 +1,79 @@
module Flowerbox::Run
class Base
attr_reader :dir, :options
def self.execute(dir, options)
new(dir, options).execute
end
def initialize(dir, options)
@dir, @options = dir, options
end
def execute
raise StandardError.new("override in subclass")
end
def prep!
Flowerbox.reset!
load File.join(dir, 'spec_helper.rb')
require 'coffee_script'
require 'tilt/coffee'
Tilt::CoffeeScriptTemplate.default_bare = Flowerbox.bare_coffeescript
if runners = options[:runners] || options[:runner]
Flowerbox.run_with(runners.split(','))
end
Flowerbox.test_environment.run = self
Flowerbox.test_environment.set_additional_options(options[:env_options])
end
def sprockets
require 'flowerbox/sprockets_handler'
Flowerbox::SprocketsHandler.new(:asset_paths => asset_paths)
end
def asset_paths
[
Flowerbox.path.join("lib/assets/javascripts"),
Flowerbox.path.join("vendor/assets/javascripts"),
@dir,
Flowerbox.asset_paths
].flatten
end
def spec_files
return @spec_files if @spec_files
@spec_files = []
Flowerbox.spec_patterns.each do |pattern|
Dir[File.join(dir, pattern)].each do |file|
if !only || only.find { |match| file[%r{^#{match}}] }
@spec_files << File.expand_path(file)
end
end
end
@spec_files
end
def only
return @only if @only
@only = options[:files] || []
@only = nil if only.empty?
@only
end
def system_files
%w{flowerbox json2}
end
end
end

View File

@ -0,0 +1,43 @@
module Flowerbox::Run
class Debug < Base
def execute
prep!
env = Flowerbox.runner_environment.first
env.setup(sprockets, spec_files, options)
Flowerbox.reporters.clear!
puts "Flowerbox debug server running test prepared for #{env.console_name} on #{env.server.address}"
env.server.start
trap('INT') do
env.server.stop
end
@restart = false
trap('QUIT') do
puts "Restarting Flowerbox server..."
@restart = true
env.server.stop
end
while env.server.alive?
sleep 0.25
end
if @restart
debug(dir, options)
else
puts "Flowerbox finished."
end
end
def options
@options.dup.merge(:debug => true)
end
end
end

35
lib/flowerbox/run/test.rb Normal file
View File

@ -0,0 +1,35 @@
module Flowerbox::Run
class Test < Base
require 'flowerbox/runner/base'
def execute
prep!
result_set = Flowerbox::ResultSet.new
time = 0
realtime = Time.now.to_i
failed = false
runner_envs = Flowerbox.runner_environment.collect do |env|
env.ensure_configured!
result_set << env.run(sprockets, spec_files, options)
failed = true if !env.started?
time += env.time
env
end
result_set.print(:time => time, :realtime => Time.now.to_i - realtime)
runner_envs.each(&:cleanup)
failed ? 1 : result_set.exitstatus
rescue Flowerbox::Runner::Base::RunnerDiedError
255
end
end
end

12
lib/flowerbox/runner.rb Normal file
View File

@ -0,0 +1,12 @@
module Flowerbox
module Runner
extend Flowerbox::CoreExt::Module
def self.find_constant(string)
require "flowerbox/runner/#{string}"
super
end
end
end

View File

@ -0,0 +1,247 @@
require 'thread'
module Flowerbox
module Runner
class Base
attr_reader :sprockets, :spec_files, :options, :time
attr_accessor :results
MAX_COUNT = 50
class RunnerDiedError < StandardError ; end
def self.mutex
@mutex ||= Mutex.new
end
def initialize
@results = ResultSet.new
@started = false
end
def started?
@started
end
def starting(*args)
@started = true
end
def reporters
Flowerbox.reporters
end
def load(file)
sprockets.find_asset(file.first, :bundle => false).body
end
def instrument(data)
results = []
data.flatten.first.each do |filename, lines|
results += lines.reject { |line| line == nil }
end
visited = results.find_all { |result| result == 1 }.length
Flowerbox.notify "Coverage: %d/%d lines (%0.2f%% of instrumented code)" % [ visited, results.length, (visited.to_f / results.length.to_f) * 100 ]
end
def ensure_alive
while @count < MAX_COUNT && !finished?
@count += 1 if @timer_running
sleep 0.1
end
if !finished?
Flowerbox.notify "Something died hard. Here are the tests that did get run before Flowerbox died.".foreground(:red)
Flowerbox.notify tests.flatten.join("\n").foreground(:red)
server.stop
Flowerbox.server = nil
raise RunnerDiedError.new
end
end
def setup(sprockets, spec_files, options)
@sprockets, @spec_files, @options = sprockets, spec_files, options
Flowerbox.test_environment.runner = self
Flowerbox.test_environment.inject_into(sprockets)
Flowerbox.additional_files.each { |file| sprockets.add(file) }
end
def run(*args)
self.class.mutex.synchronize do
setup(*args)
@count = 0
@timer_running = true
reporters.start("Flowerbox running your #{Flowerbox.test_environment.name} tests on #{console_name}...")
attempts = 3
require 'flowerbox/server'
while true
begin
server.start
break
rescue Flowerbox::Server::ServerDiedError
attempts -= 1
raise RunnerDiedError.new if attempts == 0
Flowerbox.server = nil
ensure
@sprockets.reset!
end
end
yield
server.stop
end
@results
rescue => e
case e
when ExecJS::RuntimeError, ExecJS::ProgramError, Sprockets::FileNotFound, Sprockets::CircularDependencyError
handle_coffeescript_compilation_error(e)
else
raise e
end
end
def configured?
true
end
def configure ; end
def pause_timer(*args)
@timer_running = false
@count = 0
end
def unpause_timer(*args)
@timer_running = true
end
def debug?
options[:debug] == true
end
def ensure_configured!
if !configured?
Flowerbox.notify "#{console_name} is not configured for this project, configuring now..."
configure
end
end
def type
self.class.name.to_s.split('::').last.downcase.to_sym
end
def start_test_environment
Flowerbox.test_environment.start
rescue ExecJS::ProgramError => e
handle_coffeescript_compilation_error(e)
end
def time=(time)
@results.time = time
end
def server
require 'flowerbox/rack'
if Flowerbox.server
Flowerbox.server.runner = self
Flowerbox.server.app.runner = self
Flowerbox.server.app.sprockets = sprockets
return Flowerbox.server
end
require 'flowerbox/server'
server_options = { :app => Flowerbox::Rack.new(self, sprockets) }
server_options[:logging] = true if options[:verbose_server]
server_options[:port] = Flowerbox.port
Flowerbox.server = Flowerbox::Server.new(self, server_options)
end
def log(message)
reporters.log(message)
end
def tests
@tests ||= []
end
def start_test(new_tests)
tests << new_tests
Flowerbox.notify(new_tests.flatten) if options[:verbose_server]
@count = 0
end
def failures
@failures ||= []
end
def finish_test(test_results)
results = result_set_from_test_results(test_results)
results.print_progress
@count = 0
@results << results
end
def total_count
@tests.length
end
def failure_count
@failures.length
end
def time
@time ||= 0
end
def results(time)
@time = time.first
@finished = true
end
def finished?
@finished
end
def ping
@count = 0
end
def handle_coffeescript_compilation_error(exception)
Flowerbox.notify(exception.message.foreground(:red))
@finished = true
raise RunnerDiedError.new(exception.message)
end
private
def result_set_from_test_results(test_results)
ResultSet.from_results(test_results, options.merge(:runner => name))
end
end
end
end

View File

@ -0,0 +1,22 @@
require 'flowerbox/runner/selenium'
class Flowerbox::Runner::Chrome < Flowerbox::Runner::Selenium
def name
"Chrome"
end
def console_name
"C".foreground('#4f97d1') +
"h".foreground('#ec5244') +
"r".foreground('#fdd901') +
"o".foreground('#4f97d1') +
"m".foreground('#5cb15b') +
"e".foreground('#ec5244')
end
def browser
Flowerbox.browsers[:chrome] ||= ::Selenium::WebDriver.for(:chrome)
end
end

View File

@ -0,0 +1,16 @@
require 'flowerbox/runner/selenium'
class Flowerbox::Runner::Firefox < Flowerbox::Runner::Selenium
def name
"Firefox"
end
def console_name
"Firefox".foreground('#d0450b')
end
def browser
Flowerbox.browsers[:firefox] ||= ::Selenium::WebDriver.for(:firefox)
end
end

View File

@ -0,0 +1,132 @@
require 'tempfile'
require 'json'
require 'flowerbox/runner/base'
module Flowerbox
module Runner
class Node < Base
def name
"node.js"
end
def console_name
"n".foreground(:white) +
"o".foreground('#8cc84b') +
"de".foreground(:white) +
".js".foreground('#8cc84b')
end
def type
:node
end
def configured?
File.directory?(File.join(Dir.pwd, 'node_modules/jsdom')) &&
File.directory?(File.join(Dir.pwd, 'node_modules/ws'))
end
def cleanup ; end
def configure
Flowerbox.notify %x{bash -c "mkdir -p node_modules && npm link jsdom && npm link ws"}
end
def run(sprockets, spec_files, options)
super do
begin
file = File.join(Dir.pwd, ".node-tmp.#{Time.now.to_i}.js")
File.open(file, 'wb') { |fh| fh.print template }
system %{node #{file}}
if $?.exitstatus == 0
count = 20
while !finished? && count > 0
sleep 0.1
count -= 1
end
end
ensure
File.unlink(file) if file
end
end
end
def template
env = start_test_environment
<<-JS
// whoa node
var fs = require('fs'),
vm = require('vm'),
http = require('http'),
jsdom = require('jsdom'),
ws = require('ws')
// expand the sandbox a bit
var context = function() {};
for (method in global) { context[method] = global[method]; }
jsdom.env(
"<html><head><title></title></head><body></body></html>", [], function(errors, window) {
context.window = window;
context.WebSocket = ws;
var gotFlowerbox = false;
console._log = console.log;
var socket = new ws('ws://localhost:#{server.port + 1}/');
socket.onopen = function() {
var files = #{sprockets.files.to_json};
var fileLoader = function() {
if (files.length > 0) {
var file = files.shift();
socket.onmessage = function(data) {
vm.runInNewContext(data.data, context, file);
for (thing in window) {
if (!context[thing]) { context[thing] = window[thing] }
}
if (!gotFlowerbox && context.Flowerbox) {
context.Flowerbox.environment = 'node';
context.Flowerbox.UNKNOWN = '#{Flowerbox::Result::FileInfo::UNKNOWN}';
context.Flowerbox.socket = socket;
gotFlowerbox = true;
}
fileLoader();
};
socket.send(JSON.stringify(['load', file]));
} else {
socket.onmessage = null;
#{env}
var waitForFinish;
waitForFinish = function() {
if (!context.Flowerbox.started || !context.Flowerbox.done) {
process.nextTick(waitForFinish);
} else {
process.exit(0);
}
};
waitForFinish();
}
};
fileLoader();
};
});
JS
end
end
end
end

View File

@ -0,0 +1,91 @@
require 'selenium-webdriver'
require 'flowerbox/runner/base'
module Flowerbox
module Runner
class Selenium < Base
def name
raise StandardError.new("Override me")
end
def type
:selenium
end
def cleanup
end
def browser
raise StandardError.new("Define a browser")
end
def run(sprockets, spec_files, options)
super do
navigate = Proc.new { browser.navigate.to "http://localhost:#{server.port}/?#{Time.now.to_f}" }
begin
navigate.call
rescue ::Selenium::WebDriver::Error::UnknownError => e
puts "Browser communication error, reopening all browsers...".foreground(:yellow)
Flowerbox.cleanup!
navigate.call
end
ensure_alive
end
end
def log(msg)
puts msg
end
def page_title
"Flowerbox - #{Flowerbox.test_environment.name} Runner"
end
def template
env = start_test_environment
<<-HTML
<html>
<head>
<title>#{page_title}</title>
</head>
<body>
<h1>#{page_title}</h1>
<pre id="queue"></pre>
<script type="text/javascript">
console._log = console.log;
console.log = function(msg) {
console._log(msg);
Flowerbox.contact("log", msg);
}
</script>
#{template_files.join("\n")}
<script type="text/javascript">
Flowerbox.environment = '#{name}';
Flowerbox.UNKNOWN = '#{Flowerbox::Result::FileInfo::UNKNOWN}';
var context = this;
window.addEventListener('DOMContentLoaded', function() {
Flowerbox.socket = new WebSocket('ws://localhost:#{server.port + 1}/');
Flowerbox.socket.onopen = function() {
#{env}
};
}, false);
</script>
</body>
</html>
HTML
end
def template_files
sprockets.files.collect { |file| %{<script type="text/javascript" src="/__F__/#{sprockets.logical_path_for(file)}?#{Time.now.to_f}"></script>} }
end
end
end
end

176
lib/flowerbox/server.rb Normal file
View File

@ -0,0 +1,176 @@
require 'rack'
require 'net/http'
require 'socket'
require 'rack/builder'
require 'thin'
require 'em-websocket'
require 'json'
require 'forwardable'
module Flowerbox
class Server
attr_reader :options
attr_accessor :runner
class MissingRackApp < StandardError ; end
class ServerDiedError < StandardError ; end
extend Forwardable
def_delegators :@server_thread, :alive?
def initialize(runner, options = {})
@runner = runner
@options = { :logging => false }.merge(options || {})
end
def app
options[:app] || raise(MissingRackApp.new)
end
def rack_app
Thin::Logging.silent = !options[:logging]
rack_app = app
if options[:logging]
rack_app = ::Rack::Builder.new do
use ::Rack::CommonLogger, STDOUT
run app
end
end
rack_app
end
def websocket_app(ws)
ws.onmessage { |message|
command, data = JSON.parse(message)
begin
output = runner.send(command, [ data ].flatten)
if command == 'load'
ws.send(output)
else
ws.send("ok")
end
rescue => e
$stderr.puts e.message
exit 1
end
}
end
def start
@server_thread = Thread.new do
begin
server_options = { :Port => port, :Host => interface }
EventMachine.run do
EventMachine::WebSocket.start(:host => interface, :port => port + 1, &method(:websocket_app))
::Rack::Handler::Thin.run(rack_app, server_options) do |server|
Thread.current[:server] = server
trap('QUIT') { server.stop }
end
end
rescue => e
@server_thread[:exception] = e
raise e
end
end
while !@server_thread[:server] && alive?
sleep 0.1
end
if @server_thread[:exception]
raise @server_thread[:exception]
else
raise ServerDiedError.new if !alive?
end
end
def stop
if @server_thread
@server_thread[:server].stop rescue nil
EventMachine.stop_event_loop
@server_thread.kill
wait_for_server_to_stop
end
end
def interface
options[:interface] || '0.0.0.0'
end
def port
return @port if @port
if options[:port]
return @port = options[:port]
end
attempts = 20
begin
try_server_to_something(nil, Proc.new { |port| @port = port }, (random_port / 2).floor * 2)
attempts -= 1
end while !@port and attempts != 0
raise StandardError.new("can't start server") if attempts == 0
@port
end
def address
@address ||= "http://#{interface}:#{port}/"
end
private
def wait_for_server_to_start
started = false
while !started do
try_server_to_something(Proc.new { started = true })
end
end
def wait_for_server_to_stop
while alive? do
try_server_to_something(nil, Proc.new { return })
end
end
def try_server_to_something(success, failure = nil, current_port = port)
begin
connect_interface = '127.0.0.1' if interface == '0.0.0.0'
socket = TCPSocket.new(connect_interface, current_port)
socket.close
success.call(current_port) if success
rescue => e
case e
when Errno::ECONNREFUSED, Errno::ECONNRESET
failure.call(current_port) if failure
else
raise e
end
end
sleep 0.1
end
def random_port
25000 + Kernel.rand(1000)
end
end
end

View File

@ -0,0 +1,78 @@
require 'sprockets'
require 'sprockets/engines'
require 'forwardable'
require 'sprockets-vendor_gems'
require 'fileutils'
module Flowerbox
class SprocketsHandler
extend Forwardable
class LogicalPathNotFoundError < StandardError ; end
attr_reader :files, :options
def_delegators :environment, :append_path, :register_engine, :[], :call, :find_asset, :paths
def self.gem_asset_paths
@gem_asset_paths ||= Sprockets.find_gem_vendor_paths
end
def initialize(options)
@options = options
require 'flowerbox/unique_asset_list'
@files = Flowerbox::UniqueAssetList.new(self)
end
def add(asset)
assets_for(asset).each { |dependent_asset| files.add(dependent_asset) }
end
def assets_for(asset)
find_asset(asset, :bundle => true).to_a
end
def environment
return @environment if @environment
@environment = Sprockets::Environment.new
if Flowerbox.instrument_js?
require 'flowerbox/tilt/instrument_js'
@environment.register_postprocessor 'application/javascript', Flowerbox::Tilt::InstrumentJS
end
default_asset_paths.each { |path| @environment.append_path(path) }
@environment
end
def default_asset_paths
self.class.gem_asset_paths + options[:asset_paths]
end
def cache
Sprockets::Cache::FileStore.new(Flowerbox.cache_dir)
end
def logical_path_for(asset)
asset_path = asset.pathname.to_s
paths.each do |path|
if result = asset_path[%r{^#{path}/(.*)}, 1]
return result
end
end
raise LogicalPathNotFoundError.new("Could not find logical path for #{asset_path}")
end
def reset!
@environment.send(:expire_index!)
end
end
end

12
lib/flowerbox/success.rb Normal file
View File

@ -0,0 +1,12 @@
module Flowerbox
class Success < BaseResult
def print_progress
print ".".foreground(:green)
end
def success?
true
end
end
end

27
lib/flowerbox/task.rb Normal file
View File

@ -0,0 +1,27 @@
module Flowerbox
class Task
include Rake::DSL if defined?(Rake::DSL)
def self.create(*args)
new(*args).add
end
attr_reader :name, :options
def initialize(name = "flowerbox", options = nil)
@name = name
@options = options || {}
@options = { :dir => 'spec/javascripts' }.merge(@options)
end
def add
desc "Run Flowerbox for the tests in #{options[:dir]}"
task(name) do
raise StandardError.new("Flowerbox tests failed") if Flowerbox.run(@options[:dir], @options) != 0
Flowerbox.cleanup!
end
end
end
end

View File

@ -0,0 +1,20 @@
module Flowerbox
module TestEnvironment
extend Flowerbox::CoreExt::Module
def self.for(env)
require "flowerbox/test_environment/#{env}"
find_constant(env).new
end
def self.transplantable_environments
Dir[File.expand_path('../test_environment/*.rb', __FILE__)].each do |file|
require file
end
constants.collect { |k| const_get(k) }.find_all(&:transplantable?)
end
end
end

View File

@ -0,0 +1,55 @@
require 'yaml'
module Flowerbox
module TestEnvironment
class Base
attr_accessor :runner, :run
def name
self.class.name.split("::").last
end
def reporters
@reporters ||= []
end
def self.transplantable?
respond_to?(:transplant)
end
def set_additional_options(opts = nil)
@options = {}
if opts
case opts
when String
@options = Hash[YAML.load(opts).collect { |k, v| [ k.to_sym, v ] }]
when Hash
@options = opts
end
@options[:tags] = [ @options[:tags] ].flatten(1) if @options[:tags]
end
end
def inject_into(sprockets)
@sprockets = sprockets
system_files.each { |file| @sprockets.add(file) }
end
def system_files
run.system_files + global_system_files + runner_system_files
end
def start
runner.spec_files.each { |file| @sprockets.add(file) }
end
def actual_path_for(file)
@sprockets.find_asset(file.gsub(%r{\?.*$}, ''), :bundle => false).pathname.to_s
end
end
end
end

View File

@ -0,0 +1,112 @@
require 'flowerbox/test_environment/base'
module Flowerbox
module TestEnvironment
class Cucumber < Base
def initialize
@step_language = nil
end
def prefer_step_language(language)
@step_language = language
end
def inject_into(sprockets)
super
require 'flowerbox/tilt/feature_template'
@sprockets.register_engine('.feature', Flowerbox::Tilt::FeatureTemplate)
end
def global_system_files
%w{cucumber.js flowerbox/cucumber}
end
def runner_system_files
[ "flowerbox/cucumber/#{@runner.type}" ]
end
def start
super
<<-JS
context.Cucumber = context.require('./cucumber');
options = {}
#{maybe_tags}
context.cucumber = context.Cucumber(context.Flowerbox.Cucumber.features(), context.Flowerbox.World(), options);
context.cucumber.attachListener(new context.Flowerbox.Cucumber.Reporter());
context.cucumber.start(function() {});
JS
end
def maybe_tags
"options.tags = #{@options[:tags].to_json};" if @options[:tags]
end
def obtain_test_definition_for(result)
matcher = result.original_name
args = []
matcher.gsub!(%r{"[^"]+"}) do |_, match|
args << "arg#{args.length + 1}"
'"([^"]+)"'
end
matcher.gsub!(%r{ \d+ }) do |_, match|
args << "arg#{args.length + 1}"
" (\d+) "
end
messages = []
if result['hasDataTable']
args << "table"
messages << "table is a Cucumber AST data table"
end
if result['hasDocString']
args << "string"
messages << "string is a doc string"
end
args_string = args.join(', ')
output = []
if primarily_coffeescript?
output << %{Flowerbox.#{result.step_type} /^#{matcher}$/, #{"(#{args_string}) " if !args_string.empty?}->}
output += messages.collect { |msg| " # #{msg}" }
output << %{ @pending() # add your code here}
else
output << "Flowerbox.#{result.step_type}(/^#{matcher}$/, function(#{args_string}) {"
output += messages.collect { |msg| " // #{msg}" }
output << %{ this.pending(); // add your code here}
output << "});"
end
output.collect { |line| "#{line}\n" }.join
end
def primarily_coffeescript?
return true if @step_language == :coffeescript
coffee_count = @runner.spec_files.inject(0) { |s, n| s += 1 if n[%r{.coffee$}]; s }
js_count = @runner.spec_files.inject(0) { |s, n| s += 1 if n[%r{.js$}]; s }
coffee_count > js_count
end
def plant_source
"skel/cucumber"
end
def plant_target
"js-features"
end
end
end
end

View File

@ -0,0 +1,84 @@
require 'flowerbox/test_environment/base'
require 'jasmine-core'
require 'yaml'
module Flowerbox
module TestEnvironment
class Jasmine < Base
def self.transplant(dir)
if File.file?(jasmine_yml = File.join(dir, 'support/jasmine.yml'))
puts "Transplanting #{jasmine_yml} into Flowerbox..."
config = [
%{f.test_with :jasmine},
%{f.run_with :firefox},
%{f.report_with :verbose}
]
YAML.load_file(jasmine_yml).each do |key, value|
case key
when 'src_dir'
[ value ].flatten.each do |dir|
config << %{f.asset_paths << "#{dir}"}
end
when 'src_files'
[ value ].flatten.each do |file|
config << %{f.additional_files << "#{file}"}
end
when 'helpers'
[ value ].flatten.each do |pattern|
Dir[File.join(dir, pattern)].each do |file|
config << %{f.additional_files << "#{file.gsub("#{dir}/", '')}"}
end
end
when 'spec_files'
[ value ].flatten.each do |pattern|
config << %{f.spec_patterns << "#{pattern}"}
end
end
end
File.open(target = File.join(dir, 'spec_helper.rb'), 'wb') do |fh|
fh.puts "Flowerbox.configure do |f|"
config.each do |line|
fh.puts " #{line}"
end
fh.puts "end"
end
puts "#{target} created. Run your tests with:"
puts " flowerbox test #{dir}"
end
end
def inject_into(sprockets)
sprockets.append_path(::Jasmine::Core.path)
super
end
def global_system_files
%w{jasmine.js flowerbox/jasmine.js}
end
def runner_system_files
[ "flowerbox/jasmine/#{@runner.type}.js" ]
end
def start
super
<<-JS
if (typeof context != 'undefined' && typeof jasmine == 'undefined') {
jasmine = context.jasmine;
}
jasmine.getEnv().addReporter(new jasmine.FlowerboxReporter());
jasmine.getEnv().execute();
JS
end
end
end
end

View File

@ -0,0 +1,24 @@
require 'tilt'
module Flowerbox
module Tilt
class FeatureTemplate < ::Tilt::Template
self.default_mime_type = 'application/javascript'
def prepare; end
def evaluate(scope, locals, &block)
<<-JS
Flowerbox.Cucumber.Features = Flowerbox.Cucumber.Features || [];
Flowerbox.Cucumber.Features.push("#{escaped_data}");
JS
end
def escaped_data
data.gsub("\n", "\\n").gsub('"', '\\"')
end
end
end
end

View File

@ -0,0 +1,50 @@
require 'tilt'
module Flowerbox
module Tilt
class InstrumentJS < ::Tilt::Template
def prepare ; end
def evaluate(scope, locals, &block)
if Flowerbox.instrument_files.include?(file)
block_comment = false
lines = data.lines.to_a
output = [
'if (typeof __$instrument == "undefined") { __$instrument = {} }',
"__$instrument['#{file}'] = [];",
"__$instrument['#{file}'][#{lines.length - 1}] = null;"
]
prior_comma_end = comma_end = false
lines_instrumented = []
code_output = []
lines.each_with_index do |line, index|
line.rstrip!
instrument = "__$instrument['#{file}'][#{index}] = 1;"
if line[%r{; *$}]
line = line + instrument
lines_instrumented << index
end
code_output << line
end
lines_instrumented.each do |line|
output << "__$instrument['#{file}'][#{line}] = 0;"
end
(output + code_output).join("\n")
else
data
end
end
end
end
end

View File

@ -0,0 +1,34 @@
module Flowerbox
class UniqueAssetList < ::Array
attr_reader :sprockets
def initialize(sprockets)
super([])
@sprockets = sprockets
@included = {}
end
def add(files)
[ files ].flatten.each do |file|
self << file if !included?(file)
end
end
def <<(file)
super(file)
@included[file.pathname.to_s] = true
end
def to_json
collect { |file| sprockets.logical_path_for(file) }
end
def included?(file)
@included[file.pathname.to_s]
end
end
end

3
lib/flowerbox/version.rb Normal file
View File

@ -0,0 +1,3 @@
module Flowerbox
VERSION = "0.0.1"
end

View File

@ -1,5 +0,0 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@ -1,3 +0,0 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View File

@ -1,82 +0,0 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
@import "compass/reset";
@import "compass/css3";
$base_color: #d9b741;
$header_color: #000;
$header_blend_color: #3a60e5;
$code_border_color: $header_blend_color;
body {
background-color: $base_color;
}
#container {
width: 550px;
margin: 0 auto;
}
h1, h2 {
font-weight: bold;
}
h1 {
font-size: 1.75em;
margin: 0.35em 0;
color: mix($header_color, $header_blend_color, 90);
}
h2 {
font-size: 1.45em;
margin: 0.25em 0;
color: mix($header_color, $header_blend_color, 70);
}
h3 {
font-size: 1.1em;
margin: 0.2em 0;
color: mix($header_color, $header_blend_color, 50);
}
h1, h2, h3 {
text-align: center;
font-family: 'Gorditas', 'Helvetica Neue', 'Helvetica';
}
p, li {
font-family: 'Pontano Sans', 'Helvetica Neue', 'Helvetica';
line-height: 120%;
}
code, pre {
font-family: 'Courier New';
}
.highlight {
font-size: 0.9em;
padding: 1em;
border: solid $code_border_color 1px;
margin: 1em 0;
}
em {
font-style: italic;
}
p {
margin: 0.7em 0;
}
li {
list-style-type: disc;
}
ul {
padding-left: 2em;
}

View File

@ -0,0 +1,7 @@
Feature: My First Feature
Scenario: Do Something
Given I have a flowerbox
When I plant a "cucumber" seed
Then I should get the following when I pick:
| cucumber |

View File

@ -0,0 +1,10 @@
Flowerbox.configure do |f|
f.test_with :cucumber
f.run_with :firefox
f.additional_files << "support/env.js.coffee"
f.spec_patterns << "features/**/*.feature"
f.test_environment.prefer_step_language :coffeescript
end

View File

@ -0,0 +1,8 @@
Flowerbox.Given /^I have a flowerbox$/, ->
@flowerbox =
plantSeed: (type) ->
@types ||= []
@types.push(type)
pick: ->
@types

View File

@ -0,0 +1,5 @@
Flowerbox.Then /^I should get the following when I pick:$/, (table) ->
# table is a Cucumber AST data table
data = (row[0] for row in table.raw())
@expect(@flowerbox.pick()).toEqual(data)

View File

@ -0,0 +1,3 @@
Flowerbox.When /^I plant a "([^"]+)" seed$/, (type) ->
@flowerbox.plantSeed(type)

View File

@ -0,0 +1 @@
#= require_tree ../step_definitions

View File

@ -0,0 +1,22 @@
require 'spec_helper'
require 'flowerbox/reporter/json'
describe Flowerbox::Reporter::JSON do
let(:json) { described_class.new }
describe '#report_numeric_results' do
let(:results) { 'results' }
let(:numeric_results_for) { 'numeric results for' }
before do
json.expects(:numeric_results_for).with(results).returns(numeric_results_for)
json.expects(:puts)
end
it 'should add numeric results' do
json.report_numeric_results(results)
json.output.should == [ [ :results, numeric_results_for ] ]
end
end
end

View File

@ -0,0 +1,131 @@
require 'spec_helper'
require 'socket'
require 'thread'
require 'flowerbox/server'
describe Flowerbox::Server do
let(:server) { described_class.new(runner, options) }
let(:options) { nil }
let(:runner) { nil }
subject { server }
describe '#initialize' do
let(:options) { { :port => port, :interface => interface } }
let(:port) { 'port' }
let(:interface) { 'interface' }
its(:port) { should == port }
its(:interface) { should == interface }
end
describe '#app' do
subject { server.app }
context 'no app defined' do
before do
server.stubs(:options).returns({})
end
it 'should raise an error' do
expect { server.app }.to raise_error(Flowerbox::Server::MissingRackApp)
end
end
context 'app defined' do
let(:app) { 'app' }
before do
server.stubs(:options).returns(:app => app)
end
it { should == app }
end
end
describe '#start' do
let(:port) { 12345 }
let(:interface) { '127.0.0.1' }
before do
server.stubs(:port).returns(port)
server.stubs(:interface).returns(interface)
server.stubs(:app).returns(lambda { |env| [ 200, {}, [] ] })
end
it 'should start a Rack server' do
server.start
TCPSocket.new(server.interface, server.port)
end
end
describe '#interface' do
subject { server.interface }
it { should == '0.0.0.0' }
end
describe '#port' do
let(:interface) { '127.0.0.1' }
let(:base) { 25000 }
let(:initial) { base + @offset }
before do
server.stubs(:interface).returns(interface)
@offset = 0
ok = true
begin
[ 0, 1 ].each do |index|
begin
TCPSocket.new(interface, base + @offset + index)
@offset += 1
ok = false
rescue Errno::ECONNREFUSED => e
end
end
end while !ok
end
subject { server.port }
context 'no running service' do
before do
Kernel.stubs(:rand).returns(@offset)
end
it { should == initial }
end
context 'running service' do
before do
@server = Thread.new do
TCPServer.new(interface, initial)
end
server.stubs(:random_port).returns(initial, initial + 2)
count = 10
while count > 0
begin
TCPSocket.new(interface, initial)
break
rescue Errno::ECONNREFUSED
count -= 1
sleep 0.1
end
end
end
it { should == initial + 2 }
after do
@server.kill
end
end
end
end

View File

@ -0,0 +1,96 @@
require 'spec_helper'
require 'flowerbox/sprockets_handler'
describe Flowerbox::SprocketsHandler do
let(:sprockets_handler) { described_class.new(options) }
let(:options) { { :asset_paths => asset_paths } }
let(:asset_paths) { [ File.expand_path('asset path') ] }
describe '#add' do
let(:asset) { 'asset' }
let(:dependent_asset) { 'dependent' }
let(:pathname_path) { 'pathname path' }
let(:files) { stub }
before do
sprockets_handler.expects(:assets_for).with(asset).returns([ dependent_asset ])
sprockets_handler.stubs(:files).returns(files)
files.expects(:add).with(dependent_asset)
end
it 'should add the asset to the list of ones to work with' do
sprockets_handler.add(asset)
end
end
describe '#environment' do
let(:cache) { stub }
before do
sprockets_handler.stubs(:cache).returns(cache)
sprockets_handler.stubs(:default_asset_paths).returns([])
end
subject { sprockets_handler.environment }
it { should be_a_kind_of(Sprockets::Environment) }
end
describe '#default_asset_paths' do
let(:gem) { 'gem' }
let(:asset) { 'asset' }
before do
described_class.stubs(:gem_asset_paths).returns([ gem ])
sprockets_handler.stubs(:options).returns(:asset_paths => [ asset ])
end
subject { sprockets_handler.default_asset_paths }
it { should == [ gem, asset ] }
end
describe '#assets_for' do
subject { sprockets_handler.assets_for(asset) }
let(:asset) { 'asset' }
let(:found_asset) { 'found asset' }
let(:other_asset) { 'other asset' }
before do
sprockets_handler.stubs(:find_asset).returns(found_asset)
found_asset.stubs(:to_a).returns(other_asset)
end
it { should == other_asset }
end
describe '#logical_path_for' do
subject { sprockets_handler.logical_path_for(asset) }
let(:path) { 'path' }
let(:result) { 'result' }
let(:asset) { stub(:pathname => Pathname("#{path}/#{result}")) }
before do
sprockets_handler.stubs(:paths).returns(paths)
end
context 'found' do
let(:paths) { [ path ] }
it { should == result }
end
context 'not found' do
let(:paths) { [] }
it 'should raise an exception' do
expect { subject }.to raise_error(Flowerbox::SprocketsHandler::LogicalPathNotFoundError)
end
end
end
end

View File

@ -0,0 +1,47 @@
require 'spec_helper'
require 'flowerbox/unique_asset_list'
describe Flowerbox::UniqueAssetList do
let(:unique_asset_list) { described_class.new(sprockets) }
let(:sprockets) { stub }
describe "#add" do
let(:first) { stub(:pathname => Pathname.new('one')) }
let(:second) { stub(:pathname => Pathname.new('one')) }
let(:third) { stub(:pathname => Pathname.new('two')) }
it 'should not add assets already added' do
unique_asset_list.add(first)
unique_asset_list.add([ second, third ])
unique_asset_list.should == [ first, third ]
end
end
describe '#to_json' do
subject { unique_asset_list.to_json }
let(:file) { 'file' }
let(:path) { 'path' }
before do
unique_asset_list.replace([ file ])
sprockets.expects(:logical_path_for).with(file).returns(path)
end
it { should == [ path ] }
end
describe '#<<' do
let(:asset) { stub(:pathname => Pathname(path)) }
let(:path) { 'path' }
it 'should add the asset and mark it included' do
unique_asset_list << asset
unique_asset_list.should == [ asset ]
unique_asset_list.should be_included(asset)
end
end
end

View File

@ -0,0 +1,10 @@
Flowerbox.configure do |c|
c.test_with :jasmine
c.run_with :node
c.spec_patterns << "*_spec.*"
c.spec_patterns << "**/*_spec.*"
c.test_environment.reporters << "SimpleNodeReporter"
end

View File

@ -0,0 +1,6 @@
describe("cats", function() {
it("should hiss", function() {
expect("hiss").toEqual("hiss");
});
});

11
spec/spec_helper.rb Normal file
View File

@ -0,0 +1,11 @@
require 'simplecov'
SimpleCov.start
require 'mocha'
require 'fakefs/spec_helpers'
require 'flowerbox'
RSpec.configure do |c|
c.mock_with :mocha
end

View File

@ -1,5 +0,0 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@ -1,3 +0,0 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

Some files were not shown because too many files have changed in this diff Show More