├── .tool-versions ├── spec ├── suite1 │ ├── spec │ │ └── javascripts │ │ │ ├── bar_spec.js │ │ │ ├── foo_spec.js │ │ │ ├── libs │ │ │ └── lucid_spec.js │ │ │ ├── models │ │ │ └── game_spec.js │ │ │ ├── templates │ │ │ ├── escape.html │ │ │ ├── script_tags.html │ │ │ ├── one_template.html │ │ │ └── another_template.html │ │ │ ├── helpers │ │ │ ├── spec_helper.js │ │ │ └── spec_helper.coffee │ │ │ ├── invalid_coffee_spec.coffee │ │ │ ├── coffeescript_spec.coffee │ │ │ ├── slow_spec.coffee │ │ │ ├── testing_spec.js │ │ │ ├── failing_spec.js │ │ │ ├── with_helper_spec.js │ │ │ ├── transactions_spec.js │ │ │ └── templates_spec.js │ └── public │ │ ├── styles.css │ │ └── jquery.js ├── suite3 │ ├── public │ │ └── foo.js │ ├── templates │ │ └── foo.html │ └── spec │ │ └── javascripts │ │ ├── templates │ │ └── foo.html │ │ ├── helpers │ │ └── spec_helper.js │ │ ├── failing_spec.js │ │ └── awesome_spec.js ├── suite2 │ ├── public_html │ │ └── foo.js │ ├── templates │ │ └── foo.html │ ├── spec │ │ ├── failing_spec.js │ │ └── awesome_spec.js │ └── config │ │ └── evergreen.rb ├── runner_spec.rb ├── spec_spec.rb ├── suite_spec.rb ├── helper_spec.rb ├── template_spec.rb ├── evergreen_spec.rb ├── spec_helper.rb └── meta_spec.rb ├── example ├── public │ └── implementation.js └── spec │ └── javascripts │ └── foo_spec.js ├── lib ├── evergreen │ ├── version.rb │ ├── resources │ │ ├── elabs.png │ │ ├── list.js │ │ ├── run.js │ │ ├── json2.js │ │ ├── evergreen.css │ │ └── jquery.js │ ├── views │ │ ├── _spec_error.erb │ │ ├── list.erb │ │ ├── layout.erb │ │ └── run.erb │ ├── tasks.rb │ ├── rails.rb │ ├── server.rb │ ├── utils │ │ └── timeout.rb │ ├── template.rb │ ├── helper.rb │ ├── cli.rb │ ├── spec.rb │ ├── suite.rb │ ├── application.rb │ └── runner.rb ├── tasks │ └── evergreen.rake └── evergreen.rb ├── .gitignore ├── .gitmodules ├── config └── routes.rb ├── Rakefile ├── Gemfile ├── .travis.yml ├── bin └── evergreen ├── .circleci └── config.yml ├── evergreen.gemspec └── README.rdoc /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.0.5 2 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/bar_spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/foo_spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/libs/lucid_spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/models/game_spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/implementation.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /spec/suite3/public/foo.js: -------------------------------------------------------------------------------- 1 | var something = "The Foo"; 2 | -------------------------------------------------------------------------------- /spec/suite2/public_html/foo.js: -------------------------------------------------------------------------------- 1 | var something = "The Foo"; 2 | -------------------------------------------------------------------------------- /spec/suite1/public/styles.css: -------------------------------------------------------------------------------- 1 | #from-template { 2 | width: 300px; 3 | } -------------------------------------------------------------------------------- /spec/suite2/templates/foo.html: -------------------------------------------------------------------------------- 1 |
The foo template
2 | -------------------------------------------------------------------------------- /spec/suite3/templates/foo.html: -------------------------------------------------------------------------------- 1 |
The foo template
2 | -------------------------------------------------------------------------------- /lib/evergreen/version.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | VERSION = '1.3.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/templates/escape.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/suite3/spec/javascripts/templates/foo.html: -------------------------------------------------------------------------------- 1 |
The foo template
2 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/helpers/spec_helper.js: -------------------------------------------------------------------------------- 1 | var SpecHelper = { spec: 'helper' }; 2 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/invalid_coffee_spec.coffee: -------------------------------------------------------------------------------- 1 | this => is &! not ( valid coffee script 2 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/helpers/spec_helper.coffee: -------------------------------------------------------------------------------- 1 | window.CoffeeSpecHelper = { coffee: 'script' } 2 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/templates/script_tags.html: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rvmrc 2 | .bundle 3 | Gemfile.lock 4 | Gruntfile.js 5 | .rbenv-version 6 | node_modules 7 | coverage/* 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/jasmine"] 2 | path = lib/jasmine 3 | url = http://github.com/pivotal/jasmine.git 4 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | mount Evergreen::Application, :at => '/evergreen' 3 | end 4 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/templates/one_template.html: -------------------------------------------------------------------------------- 1 |

This is from the template

2 | -------------------------------------------------------------------------------- /lib/evergreen/resources/elabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abepetrillo/evergreen/HEAD/lib/evergreen/resources/elabs.png -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/templates/another_template.html: -------------------------------------------------------------------------------- 1 |

This is from another template

2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /spec/suite2/spec/failing_spec.js: -------------------------------------------------------------------------------- 1 | describe('failing', function() { 2 | it('fails', function() { 3 | expect('llama').toEqual('monkey'); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development, :test do 6 | gem "cuprite" 7 | gem 'puma' 8 | gem 'pry' 9 | end 10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/suite2/config/evergreen.rb: -------------------------------------------------------------------------------- 1 | Evergreen.configure do |config| 2 | config.public_dir = 'public_html' 3 | config.template_dir = 'templates' 4 | config.spec_dir = 'spec' 5 | end 6 | -------------------------------------------------------------------------------- /lib/evergreen/views/_spec_error.erb: -------------------------------------------------------------------------------- 1 | describe("failure", function() { 2 | it("should not fail", function() { 3 | throw(<%= "#{error.class}: #{error.message}".to_json %>); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/coffeescript_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'coffeescript', -> 2 | 3 | it "should pass", -> 4 | expect('foo').toEqual('foo') 5 | 6 | it "should also pass", -> 7 | expect('bar').toEqual('bar') 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 3.0.5 4 | before_script: 5 | - 'git submodule update --init' 6 | - 'sh -e /etc/init.d/xvfb start' 7 | before_install: 8 | - gem install bundler 9 | env: 10 | - DISPLAY=':99.0' 11 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/slow_spec.coffee: -------------------------------------------------------------------------------- 1 | describe 'slow specs', -> 2 | 3 | it "should wait for results to show", -> 4 | runs -> 5 | expect('foo').toEqual('foo') 6 | waits 1000 7 | runs -> 8 | expect('bar').toEqual('baz') 9 | -------------------------------------------------------------------------------- /spec/suite3/spec/javascripts/helpers/spec_helper.js: -------------------------------------------------------------------------------- 1 | Evergreen.noConflict(); 2 | 3 | require = function(file){ 4 | //console.log('custom require code') 5 | } 6 | 7 | template = function(file){ 8 | //console.log('custom template code') 9 | } 10 | -------------------------------------------------------------------------------- /lib/tasks/evergreen.rake: -------------------------------------------------------------------------------- 1 | # Rails 3.0/3.1 Rake tasks 2 | namespace :spec do 3 | desc "Run JavaScript specs via Evergreen" 4 | task :javascripts => :environment do 5 | result = Evergreen::Runner.new.run 6 | Kernel.exit(1) unless result 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/testing_spec.js: -------------------------------------------------------------------------------- 1 | describe('testing', function() { 2 | 3 | it("should pass", function() { 4 | expect('foo').toEqual('foo'); 5 | }); 6 | 7 | it("should also pass", function() { 8 | expect('bar').toEqual('bar'); 9 | }); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /lib/evergreen/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'evergreen' 2 | 3 | # Rails 2.3 Rake tasks 4 | namespace :spec do 5 | desc "Run JavaScript specs via Evergreen" 6 | task :javascripts => :environment do 7 | result = Evergreen::Runner.new.run 8 | Kernel.exit(1) unless result 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/failing_spec.js: -------------------------------------------------------------------------------- 1 | describe('failing spec', function() { 2 | 3 | it("should pass", function() { 4 | expect('foo').toEqual('foo'); 5 | }); 6 | 7 | it("should fail", function() { 8 | expect('bar').toEqual('noooooo'); 9 | }); 10 | 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/with_helper_spec.js: -------------------------------------------------------------------------------- 1 | describe('the spec helper', function() { 2 | it("should load js file", function() { 3 | expect(SpecHelper.spec).toEqual('helper'); 4 | }); 5 | 6 | it("should load coffee file", function() { 7 | expect(CoffeeSpecHelper.coffee).toEqual('script'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /spec/suite2/spec/awesome_spec.js: -------------------------------------------------------------------------------- 1 | require('/foo.js'); 2 | 3 | describe('awesome', function() { 4 | template('foo.html'); 5 | 6 | it('requires public files', function() { 7 | expect(something).toEqual('The Foo'); 8 | }); 9 | it('loads templates', function() { 10 | expect(document.getElementById('foo').innerHTML).toEqual('The foo template'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/evergreen/rails.rb: -------------------------------------------------------------------------------- 1 | require 'evergreen' 2 | require 'rails' 3 | 4 | module Evergreen 5 | if defined?(Rails::Engine) 6 | class Railtie < Rails::Engine 7 | initializer 'evergreen.config' do 8 | Evergreen.application = Rails.application 9 | Evergreen.root = Rails.root 10 | Evergreen.mounted_at = "/evergreen" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/suite3/spec/javascripts/failing_spec.js: -------------------------------------------------------------------------------- 1 | require('/foo.js'); 2 | 3 | describe('awesome', function() { 4 | template('foo.html'); 5 | 6 | it('requires public files', function() { 7 | expect(something).toEqual('The Foo'); 8 | }); 9 | it('loads templates', function() { 10 | expect(document.getElementById('foo').innerHTML).toEqual('The foo template'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /spec/suite3/spec/javascripts/awesome_spec.js: -------------------------------------------------------------------------------- 1 | Evergreen.require('/foo.js'); 2 | 3 | describe('awesome', function() { 4 | Evergreen.template('foo.html'); 5 | 6 | it('requires public files', function() { 7 | expect(something).toEqual('The Foo'); 8 | }); 9 | it('loads templates', function() { 10 | expect(document.getElementById('foo').innerHTML).toEqual('The foo template'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /lib/evergreen/server.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class Server 3 | attr_reader :suite 4 | 5 | def serve 6 | server.boot 7 | Launchy.open("http://#{server.host}:#{server.port}/#{Evergreen.mounted_at.to_s}") 8 | trap('SIGINT') { puts 'Shutting down...' ; exit 0 } 9 | sleep 10 | end 11 | 12 | protected 13 | 14 | def server 15 | @server ||= Capybara::Server.new(Evergreen.application) 16 | end 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/transactions_spec.js: -------------------------------------------------------------------------------- 1 | require('/jquery.js'); 2 | 3 | describe('transactions', function() { 4 | 5 | it("should add stuff in one test...", function() { 6 | $('#test').append('

New Stuff

'); 7 | expect($('#test h1#added').length).toEqual(1); 8 | }); 9 | 10 | it("... should have been removed before the next starts", function() { 11 | expect($('#test h1#added').length).toEqual(0); 12 | }); 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /bin/evergreen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib') 4 | 5 | require 'evergreen' 6 | 7 | begin 8 | # The dup is to keep ARGV intact, so that tools like ruby-debug can respawn. 9 | success = Evergreen::Cli.execute(ARGV.dup) 10 | Kernel.exit(success ? 0 : 1) 11 | rescue SystemExit => e 12 | Kernel.exit(e.status) 13 | rescue Exception => e 14 | STDERR.puts("#{e.message} (#{e.class})") 15 | STDERR.puts(e.backtrace.join("\n")) 16 | Kernel.exit(1) 17 | end 18 | 19 | -------------------------------------------------------------------------------- /lib/evergreen/utils/timeout.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class << self 3 | 4 | ## 5 | # Provides timeout similar to standard library Timeout, but avoids threads 6 | # 7 | def timeout(seconds = 1, error_message = nil, &block) 8 | start_time = Time.now 9 | 10 | result = nil 11 | 12 | until result 13 | return result if result = yield 14 | 15 | delay = seconds - (Time.now - start_time) 16 | if delay <= 0 17 | raise TimeoutError, error_message || "timed out" 18 | end 19 | 20 | sleep(0.05) 21 | end 22 | end 23 | 24 | end 25 | end -------------------------------------------------------------------------------- /lib/evergreen/views/list.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

Evergreen

5 | 6 |
7 | All 8 | Run 9 |
10 | 11 | 19 |
20 | -------------------------------------------------------------------------------- /lib/evergreen/template.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class Template 3 | attr_reader :name, :suite 4 | 5 | def initialize(suite, name) 6 | @suite = suite 7 | @name = name 8 | end 9 | 10 | def root 11 | suite.root 12 | end 13 | 14 | def full_path 15 | File.join(root, Evergreen.template_dir, name) 16 | end 17 | 18 | def read 19 | File.read(full_path) 20 | end 21 | alias_method :contents, :read 22 | 23 | def escaped_contents 24 | contents.to_json.gsub("", %{}) 25 | end 26 | 27 | def exist? 28 | File.exist?(full_path) 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/evergreen/helper.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class Helper 3 | 4 | attr_reader :name, :suite 5 | 6 | def initialize(suite, name) 7 | @suite = suite 8 | @name = name 9 | end 10 | 11 | def root 12 | suite.root 13 | end 14 | 15 | def full_path 16 | File.join(root, Evergreen.helper_dir, name) 17 | end 18 | 19 | def read 20 | if full_path =~ /\.coffee$/ 21 | require 'coffee-script' 22 | CoffeeScript.compile(File.read(full_path)) 23 | else 24 | File.read(full_path) 25 | end 26 | end 27 | alias_method :contents, :read 28 | 29 | def exist? 30 | File.exist?(full_path) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/evergreen/views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Evergreen 6 | 7 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/evergreen/cli.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | #Translates the arguments passed in from the command line 3 | class Cli 4 | def self.execute(argv) 5 | new.execute(argv) 6 | end 7 | 8 | def execute(argv) 9 | command = argv.shift 10 | Evergreen.root = File.expand_path(argv.shift || '.', Dir.pwd) 11 | 12 | # detect Rails apps 13 | if File.exist?(File.join(Evergreen.root, 'config/environment.rb')) 14 | require File.join(Evergreen.root, 'config/environment.rb') 15 | require 'evergreen/rails' if defined?(Rails) 16 | end 17 | 18 | case command 19 | when "serve" 20 | Evergreen::Server.new.serve 21 | return true 22 | when "run" 23 | return Evergreen::Runner.new.run 24 | else 25 | puts "no such command '#{command}'" 26 | return false 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/runner_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Runner do 4 | let(:suite) { Evergreen::Suite.new } 5 | let(:runner) { Evergreen::Runner.new(buffer) } 6 | let(:buffer) { StringIO.new } 7 | 8 | describe '#run' do 9 | before { runner.run } 10 | 11 | describe 'the buffer' do 12 | subject { buffer.rewind; buffer.read } 13 | it { is_expected.to include("Expected 'bar' to equal 'noooooo'") } 14 | it { is_expected.to include("18 examples, 3 failures") } 15 | end 16 | end 17 | 18 | describe '#run_spec' do 19 | let(:spec) { suite.get_spec('failing_spec.js') } 20 | before { runner.spec_runner(spec).run } 21 | 22 | describe 'the buffer' do 23 | subject { buffer.rewind; buffer.read } 24 | 25 | it { is_expected.to include('.F') } 26 | it { is_expected.to include("Expected 'bar' to equal 'noooooo'") } 27 | it { is_expected.to include("2 examples, 1 failures") } 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/evergreen/spec.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Evergreen 4 | class Spec 5 | attr_reader :name, :suite 6 | 7 | def initialize(suite, name) 8 | @suite = suite 9 | @name = name 10 | end 11 | 12 | def root 13 | suite.root 14 | end 15 | 16 | def full_path 17 | File.join(root, Evergreen.spec_dir, name) 18 | end 19 | 20 | def read 21 | if full_path =~ /\.coffee$/ 22 | require 'coffee-script' 23 | CoffeeScript.compile(File.read(full_path)) 24 | else 25 | File.read(full_path) 26 | end 27 | end 28 | alias_method :contents, :read 29 | 30 | def url 31 | "#{suite.mounted_at}/run/#{name}" 32 | end 33 | 34 | def passed? 35 | runner.passed? 36 | end 37 | 38 | def failure_messages 39 | runner.failure_messages 40 | end 41 | 42 | def exist? 43 | File.exist?(full_path) 44 | end 45 | 46 | protected 47 | 48 | def runner 49 | @runner ||= suite.runner.spec_runner(self) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/spec_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Spec do 4 | let(:suite) { Evergreen::Suite.new } 5 | subject { Evergreen::Spec.new(suite, 'testing_spec.js') } 6 | 7 | it 'has the correct details' do 8 | expect(subject.name).to eq 'testing_spec.js' 9 | expect(subject.root).to eq File.expand_path('suite1', File.dirname(__FILE__)) 10 | expect(subject.full_path).to eq File.expand_path("spec/javascripts/testing_spec.js", Evergreen.root) 11 | expect(subject.url).to eq "/run/testing_spec.js" 12 | expect(subject.contents).to include "describe\('testing'" 13 | end 14 | 15 | context "with coffeescript" do 16 | subject { Evergreen::Spec.new(suite, 'coffeescript_spec.coffee') } 17 | it 'contains coffeescript' do 18 | expect(subject.contents).to include "describe\('coffeescript', function" 19 | end 20 | end 21 | 22 | context "with existing spec file" do 23 | it { is_expected.to exist } 24 | end 25 | 26 | context "with missing spec file" do 27 | subject { Evergreen::Spec.new(suite, 'does_not_exist.js') } 28 | it { is_expected.not_to exist } 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /example/spec/javascripts/foo_spec.js: -------------------------------------------------------------------------------- 1 | require('/implementation.js') 2 | 3 | describe('with no tokens', function () { 4 | it("should return an empty string if an empty string is given", function() { 5 | expect(foo).toEqual('foo'); 6 | }) 7 | 8 | it("should return a string unchanged", function() { 9 | expect(foo).toEqual('foo'); 10 | }) 11 | }) 12 | 13 | describe('with one token', function () { 14 | it("should replace the token with an empty string if no value is passed in", function() { 15 | expect(foo).toEqual('foo'); 16 | }) 17 | 18 | it("should replace the token with a given value", function() { 19 | }) 20 | 21 | it("should not replace partial token matches", function() { 22 | }) 23 | 24 | it("should work when calling replace twice on the same string template", function() { 25 | }) 26 | }) 27 | 28 | describe('with two tokens (OMG!?)', function () { 29 | it("should replace all tokens with their values", function() { 30 | }) 31 | 32 | it("should not do anything about tokens not present in the string template", function() { 33 | }) 34 | 35 | it("should replace tokens without value with the empty string", function() { 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /lib/evergreen/suite.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class Suite 3 | attr_reader :driver 4 | 5 | def initialize 6 | paths = [ 7 | File.expand_path("config/evergreen.rb", root), 8 | File.expand_path(".evergreen", root), 9 | "#{ENV["HOME"]}/.evergreen" 10 | ] 11 | paths.each { |path| load(path) if File.exist?(path) } 12 | end 13 | 14 | def root 15 | Evergreen.root 16 | end 17 | 18 | def mounted_at 19 | Evergreen.mounted_at 20 | end 21 | 22 | def get_spec(name) 23 | Spec.new(self, name) 24 | end 25 | 26 | def specs 27 | Dir.glob(File.join(root, Evergreen.spec_dir, '**/*_spec.{js,coffee}')).map do |path| 28 | Spec.new(self, path.gsub(File.join(root, Evergreen.spec_dir, ''), '')) 29 | end 30 | end 31 | 32 | def templates 33 | Dir.glob(File.join(root, Evergreen.template_dir, '**/*')).map do |path| 34 | Template.new(self, File.basename(path)) 35 | end 36 | end 37 | 38 | def helpers 39 | Dir.glob(File.join(root, Evergreen.helper_dir, '*')).map do |path| 40 | Helper.new(self, File.basename(path)) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/suite_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Suite do 4 | subject { Evergreen::Suite.new } 5 | 6 | describe '#get_spec' do 7 | subject { Evergreen::Suite.new.get_spec('testing_spec.js') } 8 | it 'has the correct name' do 9 | expect(subject.name).to eq 'testing_spec.js' 10 | end 11 | 12 | it 'should have the correct root' do 13 | expect(subject.root).to eq File.expand_path('suite1', File.dirname(__FILE__)) 14 | end 15 | end 16 | 17 | describe '#specs' do 18 | it "should find all specs recursively in the given root directory" do 19 | expect(subject.specs.map(&:name)).to include('testing_spec.js', 'foo_spec.js', 'bar_spec.js', 'libs/lucid_spec.js', 'models/game_spec.js') 20 | end 21 | end 22 | 23 | describe '#templates' do 24 | it "should find all specs in the given root directory" do 25 | expect(subject.templates.map(&:name)).to include('one_template.html', 'another_template.html') 26 | end 27 | end 28 | 29 | describe '#spec_helpers' do 30 | it "should find all spec helpers in the given helpers directory" do 31 | expect(subject.helpers.map(&:name)).to include('spec_helper.js', 'spec_helper.coffee') 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Helper do 4 | let(:suite) { Evergreen::Suite.new } 5 | subject { Evergreen::Helper.new(suite, 'spec_helper.js') } 6 | 7 | 8 | it 'has the corrent details' do 9 | expect(subject.name).to eq 'spec_helper.js' 10 | expect(subject.root).to eq File.expand_path('suite1', File.dirname(__FILE__)) 11 | expect(subject.full_path).to eq File.expand_path("spec/javascripts/helpers/spec_helper.js", Evergreen.root) 12 | expect(subject.contents).to eq "var SpecHelper = { spec: 'helper' };\n" 13 | end 14 | 15 | context "with coffeescript" do 16 | subject { Evergreen::Helper.new(suite, 'spec_helper.coffee') } 17 | it 'load the coffeeScript helper' do 18 | expect(subject.contents).to include 'window.CoffeeSpecHelper' 19 | end 20 | end 21 | 22 | describe '.exists' do 23 | context 'with existing spec file' do 24 | it 'returns true' do 25 | expect(subject.exist?).to eq true 26 | end 27 | end 28 | 29 | context "with missing spec file" do 30 | subject { Evergreen::Helper.new(suite, 'does_not_exist.js') } 31 | it 'returns false' do 32 | expect(subject.exist?).to eq false 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/evergreen/application.rb: -------------------------------------------------------------------------------- 1 | module Evergreen 2 | class Application < Sinatra::Base 3 | set :static, false 4 | set :root, File.expand_path('.', File.dirname(__FILE__)) 5 | 6 | helpers do 7 | def url(path) 8 | Evergreen.mounted_at.to_s + path.to_s 9 | end 10 | 11 | def render_spec(spec) 12 | spec.read if spec 13 | rescue StandardError => error 14 | erb :_spec_error, :locals => { :error => error } 15 | end 16 | end 17 | 18 | get '/' do 19 | @suite = Evergreen::Suite.new 20 | erb :list 21 | end 22 | 23 | get '/run/all' do 24 | @suite = Evergreen::Suite.new 25 | erb :run 26 | end 27 | 28 | get '/run/*' do |name| 29 | @suite = Evergreen::Suite.new 30 | @spec = @suite.get_spec(name) 31 | erb :run 32 | end 33 | 34 | get "/jasmine/*" do |path| 35 | send_file File.expand_path(File.join('../jasmine/lib/jasmine-core', path), File.dirname(__FILE__)) 36 | end 37 | 38 | get "/resources/*" do |path| 39 | send_file File.expand_path(File.join('resources', path), File.dirname(__FILE__)) 40 | end 41 | 42 | get '/*' do |path| 43 | send_file File.join(Evergreen.root, Evergreen.public_dir, path) 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/evergreen.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'sinatra/base' 3 | require 'capybara' 4 | require 'launchy' 5 | require 'evergreen/version' 6 | require 'evergreen/application' 7 | require 'json' 8 | require 'evergreen/utils/timeout' 9 | 10 | module Evergreen 11 | autoload :Cli, 'evergreen/cli' 12 | autoload :Server, 'evergreen/server' 13 | autoload :Runner, 'evergreen/runner' 14 | autoload :Suite, 'evergreen/suite' 15 | autoload :Spec, 'evergreen/spec' 16 | autoload :Template, 'evergreen/template' 17 | autoload :Helper, 'evergreen/helper' 18 | 19 | class << self 20 | attr_accessor :driver, :root, :application, :public_dir, :spec_dir, :template_dir, :helper_dir, :mounted_at, :spec_timeout 21 | 22 | def configure 23 | yield self 24 | end 25 | 26 | def use_defaults! 27 | configure do |config| 28 | config.application = Evergreen::Application 29 | config.driver = :selenium 30 | config.public_dir = 'public' 31 | config.spec_dir = 'spec/javascripts' 32 | config.template_dir = 'spec/javascripts/templates' 33 | config.helper_dir = 'spec/javascripts/helpers' 34 | config.mounted_at = "" 35 | config.spec_timeout = 300 36 | end 37 | end 38 | end 39 | end 40 | 41 | Evergreen.use_defaults! 42 | -------------------------------------------------------------------------------- /spec/template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Template do 4 | let(:suite) { Evergreen::Suite.new } 5 | subject { Evergreen::Template.new(suite, 'one_template.html') } 6 | 7 | it 'has the correct details' do 8 | expect(subject.name).to eq 'one_template.html' 9 | expect(subject.root).to eq File.expand_path('suite1', File.dirname(__FILE__)) 10 | expect(subject.full_path).to eq File.expand_path("spec/javascripts/templates/one_template.html", Evergreen.root) 11 | expect(subject.contents).to include '

This is from the template

' 12 | end 13 | 14 | describe '.exist?' do 15 | context "with existing spec file" do 16 | it 'returns true' do 17 | expect(subject.exist?).to eq true 18 | end 19 | end 20 | 21 | context "with missing spec file" do 22 | subject { Evergreen::Template.new(suite, 'does_not_exist.html') } 23 | it 'returns false' do 24 | expect(subject.exist?).to eq false 25 | end 26 | end 27 | end 28 | 29 | end 30 | 31 | describe Evergreen::Template, "escaping" do 32 | let(:suite) { Evergreen::Suite.new } 33 | subject { Evergreen::Template.new(suite, 'escape.html') } 34 | 35 | it "escapes contents" do 36 | expect(subject.escaped_contents.strip).to eq %{"var foo = 0;\\n"} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. 6 | # See: https://circleci.com/docs/2.0/orb-intro/ 7 | orbs: 8 | ruby: circleci/ruby@1.4.0 9 | 10 | # Define a job to be invoked later in a workflow. 11 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 12 | jobs: 13 | build: 14 | docker: 15 | - image: cimg/ruby:3.0.6-browsers 16 | executor: ruby/default 17 | steps: 18 | - checkout 19 | - run: 20 | name: setup 21 | command: bundle install 22 | - run: 23 | name: Load submodules 24 | command: git submodule update --init 25 | - run: 26 | name: Run tests 27 | command: bundle exec rspec 28 | 29 | # Invoke jobs via workflows 30 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 31 | workflows: 32 | run_specs: # This is the name of the workflow, feel free to change it to better match your workflow. 33 | # Inside the workflow, you define the jobs you want to run. 34 | jobs: 35 | - build 36 | -------------------------------------------------------------------------------- /evergreen.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib/', __FILE__) 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'evergreen/version' 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "evergreen" 9 | s.rubyforge_project = "evergreen" 10 | s.version = Evergreen::VERSION 11 | 12 | s.authors = ["Jonas Nicklas", "Abe Petrillo"] 13 | s.email = ["jonas.nicklas@gmail.com", "abe.petrillo@gmail.com"] 14 | s.description = "Run Jasmine JavaScript unit tests, integrate them into Ruby applications." 15 | s.license = "MIT" 16 | 17 | s.files = Dir.glob("{bin,lib,spec,config}/**/*") + %w(README.rdoc) 18 | s.extra_rdoc_files = ["README.rdoc"] 19 | s.executables = ['evergreen'] 20 | 21 | s.homepage = "http://github.com/abepetrillo/evergreen" 22 | s.rdoc_options = ["--main", "README.rdoc"] 23 | s.require_paths = ["lib"] 24 | s.rubygems_version = "1.3.6" 25 | s.summary = "Run Jasmine JavaScript unit tests, integrate them into Ruby applications." 26 | 27 | s.add_runtime_dependency("capybara", [">= 2.1.0"]) 28 | s.add_runtime_dependency("launchy") 29 | s.add_runtime_dependency("sinatra", [">= 1.1"]) 30 | s.add_runtime_dependency("json_pure") 31 | s.add_runtime_dependency("coffee-script") 32 | 33 | s.add_development_dependency('rspec', ['~>3.2']) 34 | s.add_development_dependency('rake') 35 | s.add_development_dependency('coveralls_reborn') 36 | end 37 | -------------------------------------------------------------------------------- /lib/evergreen/resources/list.js: -------------------------------------------------------------------------------- 1 | var Evergreen = {}; 2 | 3 | Evergreen.Spec = function(element) { 4 | var self = this; 5 | this.element = $(element); 6 | this.runLink = this.element.find('.run'); 7 | this.runLink.click(function() { 8 | self.run() 9 | return false; 10 | }); 11 | } 12 | 13 | Evergreen.Spec.prototype.run = function() { 14 | var self = this 15 | this.iframe = $('').attr('src', this.runLink.attr('href')).appendTo(this.element) 16 | this.iframe.css({ position: 'absolute', left: '-20000px' }); 17 | this.runLink.addClass('running').text('Running…'); 18 | $(this.iframe).load(function() { 19 | var context = self.iframe.get(0).contentWindow; 20 | var evergreen = context.Evergreen; 21 | if(evergreen.done) { 22 | self.done(evergreen.results); 23 | } else { 24 | evergreen.onDone = function() { 25 | self.done(evergreen.results); 26 | } 27 | } 28 | }); 29 | } 30 | 31 | Evergreen.Spec.prototype.done = function(results) { 32 | var failed = [] 33 | $.each(results, function() { 34 | if(!this.passed) { failed.push(this); } 35 | }); 36 | 37 | this.runLink.removeClass('running'); 38 | 39 | if(failed.length) { 40 | this.runLink.addClass('fail').removeClass('pass').text('Fail') 41 | } else { 42 | this.runLink.addClass('pass').removeClass('fail').text('Pass') 43 | } 44 | this.iframe.remove(); 45 | } 46 | 47 | $(function() { 48 | $('#specs li, #all').each(function() { 49 | new Evergreen.Spec(this) 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /spec/evergreen_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Application do 4 | include Capybara::DSL 5 | 6 | it "should show a successful test run" do 7 | visit("/") 8 | click_link("testing_spec.js") 9 | expect(page).to have_content "2 specs, 0 failures" 10 | end 11 | 12 | it "should show a successful test run for a coffeescript spec" do 13 | visit("/") 14 | click_link("coffeescript_spec.coffee") 15 | expect(page).to have_content("2 specs, 0 failures") 16 | end 17 | 18 | it "should show errors for a failing spec" do 19 | visit("/") 20 | click_link("failing_spec.js") 21 | expect(page).to have_content("2 specs, 1 failure") 22 | expect(page).to have_content("Expected 'bar' to equal 'noooooo'.") 23 | end 24 | 25 | it "should run all specs" do 26 | visit("/") 27 | click_link("All") 28 | expect(page).to have_content("18 specs, 3 failures") 29 | expect(page).to have_content("Expected 'bar' to equal 'noooooo'.") 30 | end 31 | 32 | it "should run a spec inline" do 33 | visit("/") 34 | within('li', :text => 'testing_spec.js') do 35 | click_link("Run") 36 | expect(page).to have_content('Pass') 37 | end 38 | end 39 | 40 | it "should run a failing spec inline" do 41 | visit("/") 42 | within('li', :text => 'failing_spec.js') do 43 | click_link("Run") 44 | begin 45 | expect(page).to have_content('Fail') 46 | rescue # why you make me sad, Capybara webkit??? 47 | expect(page).to have_content('Fail') 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'evergreen' 5 | require 'rspec' 6 | 7 | require 'capybara/dsl' 8 | require "capybara/cuprite" 9 | 10 | require 'pry' 11 | 12 | require 'coveralls' 13 | Coveralls.wear! 14 | 15 | TEST_DRIVER = :cuprite 16 | 17 | Evergreen.root = File.expand_path('suite1', File.dirname(__FILE__)) 18 | 19 | Capybara.app = Evergreen::Application 20 | Capybara.default_driver = TEST_DRIVER 21 | 22 | Capybara.javascript_driver = :cuprite 23 | Capybara.register_driver(:cuprite) do |app| 24 | Capybara::Cuprite::Driver.new(app, window_size: [1200, 800]) 25 | end 26 | 27 | module EvergreenMatchers 28 | class PassSpec # :nodoc: 29 | 30 | def description 31 | 'Successfull if the runner manages to pass all the JS specs' 32 | end 33 | 34 | def matches?(actual) 35 | @actual = actual 36 | @runner = Evergreen::Runner.new(StringIO.new).spec_runner(@actual) 37 | @runner.passed? 38 | end 39 | 40 | def failure_message 41 | "expected #{@actual.name} to pass, but it failed with:\n\n#{@runner.failure_messages}" 42 | end 43 | 44 | def failure_message_when_negated 45 | "expected #{@actual.name} not to pass, but it did" 46 | end 47 | end 48 | 49 | def pass 50 | PassSpec.new 51 | end 52 | end 53 | 54 | RSpec.configure do |config| 55 | config.include EvergreenMatchers 56 | config.before do 57 | Capybara.reset_sessions! 58 | Evergreen.use_defaults! 59 | Evergreen.root = File.expand_path('suite1', File.dirname(__FILE__)) 60 | Evergreen.driver = TEST_DRIVER 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/suite1/spec/javascripts/templates_spec.js: -------------------------------------------------------------------------------- 1 | require('/jquery.js'); 2 | stylesheet('/styles.css'); 3 | 4 | describe('templates', function() { 5 | 6 | describe('with template', function() { 7 | template('one_template.html'); 8 | 9 | it("should append the template to the test div", function() { 10 | expect($('#test h1#from-template').length).toEqual(1); 11 | }); 12 | 13 | it("should change stuff in one test...", function() { 14 | expect($('#test h1#from-template').length).toEqual(1); 15 | 16 | $('#test h1#from-template').attr('id', 'changed'); 17 | 18 | expect($('#test h1#changed').length).toEqual(1); 19 | expect($('#test h1#from-template').length).toEqual(0); 20 | }); 21 | 22 | it("... should have been removed before the next starts", function() { 23 | expect($('#test h1#changed').length).toEqual(0); 24 | expect($('#test h1#from-template').length).toEqual(1); 25 | }); 26 | }); 27 | 28 | describe('with another template', function() { 29 | template('another_template.html'); 30 | 31 | it("should append the template to the test div", function() { 32 | expect($('#test h1#another-template').length).toEqual(1); 33 | }); 34 | }); 35 | 36 | describe('with template with script tags', function() { 37 | template('script_tags.html'); 38 | 39 | it("should append the template to the test div", function() { 40 | expect($('#test h1#script-tags').length).toEqual(1); 41 | }); 42 | }); 43 | 44 | }); 45 | 46 | describe('stylesheet', function() { 47 | template('one_template.html'); 48 | 49 | it("should style the template", function() { 50 | expect($('#from-template').css('width')).toEqual('300px'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /lib/evergreen/views/run.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 |
24 |

Evergreen

25 | ">Back to list 26 |
27 | 28 |
29 | 30 | 52 | -------------------------------------------------------------------------------- /spec/meta_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Evergreen::Runner do 4 | let(:suite) { Evergreen::Suite.new } 5 | subject { Evergreen::Spec.new(suite, template) } 6 | 7 | context "with standard setup" do 8 | before { Evergreen.root = File.expand_path('suite1', File.dirname(__FILE__)) } 9 | 10 | context "with transactions spec" do 11 | let(:template) { 'transactions_spec.js' } 12 | it { is_expected.to pass } 13 | end 14 | 15 | context "with spec helper" do 16 | let(:template) { 'with_helper_spec.js' } 17 | it { is_expected.to pass } 18 | end 19 | 20 | context "with template spec" do 21 | let(:template) { 'templates_spec.js' } 22 | it { is_expected.to pass } 23 | end 24 | 25 | context "invalid coffee" do 26 | let(:template) { 'invalid_coffee_spec.coffee' } 27 | it { is_expected.not_to pass } 28 | end 29 | 30 | context "with slow failing spec" do 31 | let(:template) { 'slow_spec.coffee' } 32 | it { is_expected.not_to pass } 33 | end 34 | end 35 | 36 | context "with modified setup" do 37 | before { Evergreen.root = File.expand_path('suite2', File.dirname(__FILE__)) } 38 | 39 | context "with awesome spec" do 40 | let(:template) { 'awesome_spec.js' } 41 | it { is_expected.to pass } 42 | end 43 | 44 | context "with failing spec" do 45 | let(:template) { 'failing_spec.js' } 46 | it { is_expected.not_to pass } 47 | end 48 | end 49 | 50 | context 'when noConflict is called via JS' do 51 | before { Evergreen.root = File.expand_path('suite3', File.dirname(__FILE__)) } 52 | let(:template) { 'awesome_spec.js' } 53 | it 'does not over-ride existing methods in window' do 54 | expect(subject).to pass 55 | end 56 | 57 | context 'and not using the Evergreen namespace' do 58 | let(:template) { 'failing_spec.js' } 59 | it 'fails' do 60 | expect(subject).to_not pass 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/evergreen/resources/run.js: -------------------------------------------------------------------------------- 1 | if(!this.JSON){this.JSON={};} 2 | 3 | var Evergreen = { 4 | potentialConflicts: {} 5 | }; 6 | 7 | Evergreen.dots = "" 8 | 9 | Evergreen.ReflectiveReporter = function() { 10 | this.reportRunnerStarting = function(runner) { 11 | Evergreen.results = []; 12 | }; 13 | this.reportSpecResults = function(spec) { 14 | var results = spec.results(); 15 | var item = results.getItems()[0] || {}; 16 | Evergreen.results.push({ 17 | name: spec.getFullName(), 18 | passed: results.failedCount === 0, 19 | message: item.message, 20 | trace: item.trace 21 | }); 22 | Evergreen.dots += (results.failedCount === 0) ? "." : "F"; 23 | }; 24 | this.reportRunnerResults = function(runner) { 25 | Evergreen.done = true; 26 | if(Evergreen.onDone) { Evergreen.onDone() } 27 | }; 28 | }; 29 | 30 | Evergreen.templates = {}; 31 | 32 | Evergreen.getResults = function() { 33 | return JSON.stringify(Evergreen.results); 34 | }; 35 | 36 | beforeEach(function() { 37 | document.getElementById('test').innerHTML = ""; 38 | }); 39 | 40 | Evergreen.template = function(name) { 41 | beforeEach(function() { 42 | document.getElementById('test').innerHTML = Evergreen.templates[name] 43 | }); 44 | }; 45 | 46 | Evergreen.require = function(file) { 47 | document.write(''); 48 | }; 49 | 50 | Evergreen.stylesheet = function(file) { 51 | document.write(''); 52 | }; 53 | 54 | Evergreen.defineGlobalMethods = function(){ 55 | this.potentialConflicts['require'] = window['require'] 56 | this.potentialConflicts['template'] = window['template'] 57 | this.potentialConflicts['stylesheet'] = window['stylesheet'] 58 | 59 | window.require = Evergreen.require 60 | window.template = Evergreen.template 61 | window.stylesheet = Evergreen.stylesheet 62 | } 63 | 64 | 65 | 66 | //Tells Evergreen to namespace functions instead of potentially over-riding existing ones 67 | Evergreen.noConflict = function() { 68 | window.require = this.potentialConflicts.require 69 | window.template = this.potentialConflicts.template 70 | window.stylesheet = this.potentialConflicts.stylesheet 71 | } 72 | 73 | Evergreen.defineGlobalMethods() 74 | -------------------------------------------------------------------------------- /lib/evergreen/resources/json2.js: -------------------------------------------------------------------------------- 1 | var JSON;if(!JSON){JSON={};} 2 | (function(){"use strict";function f(n){return n<10?'0'+n:n;} 3 | if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+'-'+ 4 | f(this.getUTCMonth()+1)+'-'+ 5 | f(this.getUTCDate())+'T'+ 6 | f(this.getUTCHours())+':'+ 7 | f(this.getUTCMinutes())+':'+ 8 | f(this.getUTCSeconds())+'Z':null;};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf();};} 9 | var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4);})+'"':'"'+string+'"';} 10 | function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==='object'&&typeof value.toJSON==='function'){value=value.toJSON(key);} 11 | if(typeof rep==='function'){value=rep.call(holder,key,value);} 12 | switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';} 13 | gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;i 'evergreen/rails' 67 | 68 | Start your rails application and navigate to /evergreen. You should now see a 69 | list of all spec files, click on one to run it. 70 | 71 | There's a rake task provided for you that you can use to run your specs: 72 | 73 | rake spec:javascripts 74 | 75 | == Integrating with Rails 2 76 | 77 | Add the following line to your Rakefile: 78 | 79 | require 'evergreen/tasks' 80 | 81 | This will give you the `spec:javascripts` rake task. Note that mounting is not 82 | possible under Rails 2 and that `require 'evergreen/rails'` will fail. 83 | 84 | == Configuration 85 | 86 | By default, Evergreen uses Selenium to run your specs and assumes a certain 87 | directory structure. If this standard is fine for you, then you don't need to 88 | do anything else. If you need to configure Evergreen to suit your needs, 89 | Evergreen will automatically look for and load the following files: 90 | 91 | config/evergreen.rb 92 | .evergreen 93 | ~/.evergreen 94 | 95 | The content of these files could look like this: 96 | 97 | require 'capybara-webkit' 98 | 99 | Evergreen.configure do |config| 100 | config.driver = :webkit 101 | config.public_dir = 'public_html' 102 | config.template_dir = 'templates' 103 | config.spec_dir = 'spec' 104 | end 105 | 106 | == Transactions 107 | 108 | One problem often faced when writing unit tests for client side code is that 109 | changes to the page are not reverted for the next example, so that successive 110 | examples become dependent on each other. Evergreen adds a special div to your 111 | page with an id of test. This div is automatically emptied before each example. 112 | You should avoid appending markup to the page body and instead append it to 113 | this test div: 114 | 115 | describe('transactions', function() { 116 | it("should add stuff in one test...", function() { 117 | $('#test').append('

New Stuff

'); 118 | expect($('#test h1#added').length).toEqual(1); 119 | }); 120 | 121 | it("... should have been removed before the next starts", function() { 122 | expect($('#test h1#added').length).toEqual(0); 123 | }); 124 | }); 125 | 126 | == Templates 127 | 128 | Even more powerful than that, Evergreen allows you to create HTML templates to 129 | go along with your specs. Put the templates in their own folder like this: 130 | 131 | spec/javascripts/templates/one_template.html 132 | spec/javascripts/templates/another_template.html 133 | 134 | You can then load the template into the test div, by calling the template 135 | function in your specs: 136 | 137 | describe('transactions', function() { 138 | template('one_template.html') 139 | 140 | it("should load the template in this test", function() { 141 | ... 142 | }); 143 | }); 144 | 145 | == Spec Helper 146 | 147 | If you add a spec_helper file like so: 148 | 149 | spec/javascripts/helpers/spec_helper.js 150 | 151 | It will automatically be loaded. This is a great place for adding custom 152 | matchers and the like. 153 | 154 | == CoffeeScript 155 | 156 | Evergreen supports specs written in 157 | {CoffeeScript}[http://github.com/jashkenas/coffee-script]. Just name your spec 158 | file _spec.coffee and it will automatically be translated for you. 159 | 160 | Note that since CoffeeScript files are not compiled by Sprockets (as in Rails), 161 | the double-extension .js.coffee is not supported. 162 | 163 | You can also add a CoffeeScript spec helper, but remember that CoffeeScript 164 | encloses individual files in a closure, if you need something you define in the 165 | spec helper to be available in your spec files, attach it to the window object: 166 | 167 | # spec/javascripts/helpers/spec_helper.coffee 168 | 169 | MyThing: "foo" # local to spec helper 170 | window.MyThing: "foo" # global 171 | 172 | == Development 173 | 174 | If you plan to work on Evergreen, you need to checkout the Jasmine gem, which 175 | is added as a git submodule. Run the following command: 176 | 177 | git submodule update --init 178 | 179 | If you're using a version of Evergreen from git with bundler, you need to tell 180 | bundler to use submodules, this can be achieved with the following command: 181 | 182 | gem 'evergreen', :submodules => true, :git => 'git://github.com/abepetrillo/evergreen.git' 183 | 184 | == License: 185 | 186 | (The MIT License) 187 | 188 | Copyright (c) 2009 Jonas Nicklas 189 | 190 | Permission is hereby granted, free of charge, to any person obtaining 191 | a copy of this software and associated documentation files (the 192 | 'Software'), to deal in the Software without restriction, including 193 | without limitation the rights to use, copy, modify, merge, publish, 194 | distribute, sublicense, and/or sell copies of the Software, and to 195 | permit persons to whom the Software is furnished to do so, subject to 196 | the following conditions: 197 | 198 | The above copyright notice and this permission notice shall be 199 | included in all copies or substantial portions of the Software. 200 | 201 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 202 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 203 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 204 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 205 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 206 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 207 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 208 | -------------------------------------------------------------------------------- /spec/suite1/public/jquery.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery JavaScript Library v1.4.1 3 | * http://jquery.com/ 4 | * 5 | * Copyright 2010, John Resig 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://jquery.org/license 8 | * 9 | * Includes Sizzle.js 10 | * http://sizzlejs.com/ 11 | * Copyright 2010, The Dojo Foundation 12 | * Released under the MIT, BSD, and GPL Licenses. 13 | * 14 | * Date: Mon Jan 25 19:43:33 2010 -0500 15 | */ 16 | (function(z,v){function la(){if(!c.isReady){try{r.documentElement.doScroll("left")}catch(a){setTimeout(la,1);return}c.ready()}}function Ma(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var n in b)X(a,n,b[n],f,e,d);return a}if(d!==v){f=!i&&f&&c.isFunction(d);for(n=0;n-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete x[o]}i=c(a.target).closest(f, 18 | a.currentTarget);m=0;for(s=i.length;m)[^>]*$|^#([\w-]+)$/,Qa=/^.[^:#\[\.,]*$/,Ra=/\S/,Sa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Ta=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,O=navigator.userAgent, 21 | va=false,P=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,Q=Array.prototype.slice,wa=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Pa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:r;if(a=Ta.exec(a))if(c.isPlainObject(b)){a=[r.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ra([d[1]], 22 | [f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=r.getElementById(d[2])){if(b.id!==d[2])return S.find(a);this.length=1;this[0]=b}this.context=r;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=r;a=r.getElementsByTagName(a)}else return!b||b.jquery?(b||S).find(a):c(b).find(a);else if(c.isFunction(a))return S.ready(a);if(a.selector!==v){this.selector=a.selector;this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a, 23 | this)},selector:"",jquery:"1.4.1",length:0,size:function(){return this.length},toArray:function(){return Q.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length=0;ba.apply(this,a);return this},each:function(a,b){return c.each(this, 24 | a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(r,c);else P&&P.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(Q.apply(this,arguments),"slice",Q.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice}; 25 | c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,n;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
a";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support= 34 | {leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:r.createElement("select").appendChild(r.createElement("option")).selected,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null}; 35 | b.type="text/javascript";try{b.appendChild(r.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b,a.firstChild);if(z[f]){c.support.scriptEval=true;delete z[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function n(){c.support.noCloneEvent=false;d.detachEvent("onclick",n)});d.cloneNode(true).fireEvent("onclick")}d=r.createElement("div");d.innerHTML="";a=r.createDocumentFragment();a.appendChild(d.firstChild); 36 | c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var n=r.createElement("div");n.style.width=n.style.paddingLeft="1px";r.body.appendChild(n);c.boxModel=c.support.boxModel=n.offsetWidth===2;r.body.removeChild(n).style.display="none"});a=function(n){var o=r.createElement("div");n="on"+n;var m=n in o;if(!m){o.setAttribute(n,"return;");m=typeof o[n]==="function"}return m};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props= 37 | {"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ua=0,xa={},Va={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var f=a[G],e=c.cache;if(!b&&!f)return null;f||(f=++Ua);if(typeof b==="object"){a[G]=f;e=e[f]=c.extend(true, 38 | {},b)}else e=e[f]?e[f]:typeof d==="undefined"?Va:(e[f]={});if(d!==v){a[G]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[G]}catch(i){a.removeAttribute&&a.removeAttribute(G)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this, 39 | a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===v){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===v&&this.length)f=c.data(this[0],a);return f===v&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d); 40 | return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===v)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]|| 41 | a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var ya=/[\n\t]/g,ca=/\s+/,Wa=/\r/g,Xa=/href|src|style/,Ya=/(button|input)/i,Za=/(button|input|object|select|textarea)/i,$a=/^(a|area)$/i,za=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(o){var m= 42 | c(this);m.addClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===v){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value|| 45 | {}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i=0;else if(c.nodeName(this,"select")){var x=c.makeArray(s);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),x)>=0});if(!x.length)this.selectedIndex=-1}else this.value=s}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return v;if(f&&b in c.attrFn)return c(a)[b](d); 47 | f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==v;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Xa.test(b);if(b in a&&f&&!i){if(e){b==="type"&&Ya.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Za.test(a.nodeName)||$a.test(a.nodeName)&&a.href?0:v;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText= 48 | ""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?v:a}return c.style(a,b,d)}});var ab=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==z&&!a.frameElement)a=z;if(!d.guid)d.guid=c.guid++;if(f!==v){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j= 49 | function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):v};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var n,o=0;n=b[o++];){var m=n.split(".");n=m.shift();if(o>1){d=c.proxy(d);if(f!==v)d.data=f}d.type=m.slice(0).sort().join(".");var s=e[n],x=this.special[n]||{};if(!s){s=e[n]={};if(!x.setup||x.setup.call(a,f,m,d)===false)if(a.addEventListener)a.addEventListener(n,i,false);else a.attachEvent&&a.attachEvent("on"+n,i)}if(x.add)if((m=x.add.call(a, 50 | d,f,m,s))&&c.isFunction(m)){m.guid=m.guid||d.guid;m.data=m.data||d.data;m.type=m.type||d.type;d=m}s[d.guid]=d;this.global[n]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===v||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/);for(var n=0;i=b[n++];){var o=i.split(".");i=o.shift();var m=!o.length,s=c.map(o.slice(0).sort(),ab);s=new RegExp("(^|\\.)"+ 51 | s.join("\\.(?:.*\\.)?")+"(\\.|$)");var x=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var A in f[i])if(m||s.test(f[i][A].type))delete f[i][A];x.remove&&x.remove.call(a,o,j);for(e in f[i])break;if(!e){if(!x.teardown||x.teardown.call(a,o)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(A=c.data(a,"handle"))A.elem=null;c.removeData(a, 52 | "events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return v;a.result=v;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d, 53 | b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(i){}if(!a.isPropagationStopped()&&f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){d=a.target;var j;if(!(c.nodeName(d,"a")&&e==="click")&&!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){try{if(d[e]){if(j=d["on"+e])d["on"+e]=null;this.triggered=true;d[e]()}}catch(n){}if(j)d["on"+e]=j;this.triggered=false}}},handle:function(a){var b, 54 | d;a=arguments[0]=c.event.fix(a||z.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==v){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), 55 | fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||r;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=r.documentElement;d=r.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop|| 56 | d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==v)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;b.liveProxy=a;c.event.add(this,b.live,na,b)},remove:function(a){if(a.length){var b= 57 | 0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],na)}},special:{}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true}; 58 | c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y};var Aa=function(a){for(var b= 59 | a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ba=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ba:Aa,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ba:Aa)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!== 60 | "form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return ma("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return ma("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this, 61 | "keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var da=/textarea|input|select/i;function Ca(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ea(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Ca(d);if(a.type!=="focusout"|| 62 | d.type!=="radio")c.data(d,"_change_data",e);if(!(f===v||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}}c.event.special.change={filters:{focusout:ea,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ea.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ea.call(this,a)},beforeactivate:function(a){a= 63 | a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Ca(a))}},setup:function(a,b,d){for(var f in T)c.event.add(this,f+".specialChange."+d.guid,T[f]);return da.test(this.nodeName)},remove:function(a,b){for(var d in T)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),T[d]);return da.test(this.nodeName)}};var T=c.event.special.change.filters}r.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this, 64 | f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){e=f;f=v}var j=b==="one"?c.proxy(e,function(n){c(this).unbind(n,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a, 65 | b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d0){y=t;break}}t=t[g]}l[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,i=Object.prototype.toString,j=false,n=true;[0,0].sort(function(){n=false;return 0});var o=function(g,h,k,l){k=k||[];var q=h=h||r;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g|| 70 | typeof g!=="string")return k;for(var p=[],u,t,y,R,H=true,M=w(h),I=g;(f.exec(""),u=f.exec(I))!==null;){I=u[3];p.push(u[1]);if(u[2]){R=u[3];break}}if(p.length>1&&s.exec(g))if(p.length===2&&m.relative[p[0]])t=fa(p[0]+p[1],h);else for(t=m.relative[p[0]]?[h]:o(p.shift(),h);p.length;){g=p.shift();if(m.relative[g])g+=p.shift();t=fa(g,t)}else{if(!l&&p.length>1&&h.nodeType===9&&!M&&m.match.ID.test(p[0])&&!m.match.ID.test(p[p.length-1])){u=o.find(p.shift(),h,M);h=u.expr?o.filter(u.expr,u.set)[0]:u.set[0]}if(h){u= 71 | l?{expr:p.pop(),set:A(l)}:o.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=u.expr?o.filter(u.expr,u.set):u.set;if(p.length>0)y=A(t);else H=false;for(;p.length;){var D=p.pop();u=D;if(m.relative[D])u=p.pop();else D="";if(u==null)u=h;m.relative[D](y,u,M)}}else y=[]}y||(y=t);y||o.error(D||g);if(i.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))k.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&& 72 | y[g].nodeType===1&&k.push(t[g]);else k.push.apply(k,y);else A(y,k);if(R){o(R,q,k,l);o.uniqueSort(k)}return k};o.uniqueSort=function(g){if(C){j=n;g.sort(C);if(j)for(var h=1;h":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var l=0,q=g.length;l=0))k||l.push(u);else if(k)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&& 79 | "2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,l,q,p){h=g[1].replace(/\\/g,"");if(!p&&m.attrMap[h])g[1]=m.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,l,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=o(g[3],null,null,h);else{g=o.filter(g[3],h,k,true^q);k||l.push.apply(l,g);return false}else if(m.match.POS.test(g[0])||m.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true); 80 | return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!o(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"=== 81 | g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,k,l){return h===l.length-1},even:function(g,h){return h%2=== 82 | 0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return hk[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,l){var q=h[1],p=m.filters[q];if(p)return p(g,k,h,l);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=h[3];k=0;for(l=h.length;k= 84 | 0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=m.attrHandle[k]?m.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var l=h[2];h=h[4];return g==null?l==="!=":l==="="?k===h:l==="*="?k.indexOf(h)>=0:l==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:l==="!="?k!==h:l==="^="? 85 | k.indexOf(h)===0:l==="$="?k.substr(k.length-h.length)===h:l==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,l){var q=m.setFilters[h[2]];if(q)return q(g,k,h,l)}}},s=m.match.POS;for(var x in m.match){m.match[x]=new RegExp(m.match[x].source+/(?![^\[]*\])(?![^\(]*\))/.source);m.leftMatch[x]=new RegExp(/(^(?:.|\r|\n)*?)/.source+m.match[x].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var A=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g}; 86 | try{Array.prototype.slice.call(r.documentElement.childNodes,0)}catch(B){A=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,l=g.length;k";var k=r.documentElement;k.insertBefore(g,k.firstChild);if(r.getElementById(h)){m.find.ID=function(l,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(l[1]))?q.id===l[1]||typeof q.getAttributeNode!=="undefined"&&q.getAttributeNode("id").nodeValue===l[1]?[q]:v:[]};m.filter.ID=function(l,q){var p=typeof l.getAttributeNode!=="undefined"&&l.getAttributeNode("id"); 89 | return l.nodeType===1&&p&&p.nodeValue===q}}k.removeChild(g);k=g=null})();(function(){var g=r.createElement("div");g.appendChild(r.createComment(""));if(g.getElementsByTagName("*").length>0)m.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var l=0;k[l];l++)k[l].nodeType===1&&h.push(k[l]);k=h}return k};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")m.attrHandle.href=function(h){return h.getAttribute("href", 90 | 2)};g=null})();r.querySelectorAll&&function(){var g=o,h=r.createElement("div");h.innerHTML="

";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){o=function(l,q,p,u){q=q||r;if(!u&&q.nodeType===9&&!w(q))try{return A(q.querySelectorAll(l),p)}catch(t){}return g(l,q,p,u)};for(var k in g)o[k]=g[k];h=null}}();(function(){var g=r.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== 91 | 0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){m.order.splice(1,0,"CLASS");m.find.CLASS=function(h,k,l){if(typeof k.getElementsByClassName!=="undefined"&&!l)return k.getElementsByClassName(h[1])};g=null}}})();var E=r.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},w=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},fa=function(g,h){var k=[], 92 | l="",q;for(h=h.nodeType?[h]:h;q=m.match.PSEUDO.exec(g);){l+=q[0];g=g.replace(m.match.PSEUDO,"")}g=m.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var i=d;i0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var n=a.length;e 95 | -1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var o=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(m,s){for(;s&&s.ownerDocument&&s!==b;){if(o?o.index(s)>-1:c(s).is(a))return s;s=s.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(), 96 | a);return this.pushStack(pa(a[0])||pa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")}, 97 | nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);bb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e): 98 | e;if((this.length>1||db.test(f))&&cb.test(a))e=e.reverse();return this.pushStack(e,a,Q.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===v||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!== 99 | b&&d.push(a);return d}});var Fa=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ga=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/"},F={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"], 100 | col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==v)return this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(a));return c.getText(this)}, 101 | wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length? 102 | d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments, 103 | false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&& 104 | !c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Fa,"").replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){qa(this,b);qa(this.find("*"),b.find("*"))}return b},html:function(a){if(a===v)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Fa,""):null;else if(typeof a==="string"&&!/