├── init.rb ├── rails └── init.rb ├── VERSION.yml ├── .gitignore ├── lib ├── start_celerity.rb ├── culerity │ ├── persistent_delivery.rb │ ├── remote_browser_proxy.rb │ ├── celerity_server.rb │ └── remote_object_proxy.rb ├── culerity.rb └── tasks │ └── rspec.rake ├── bin └── run_celerity_server.rb ├── spec ├── spec_helper.rb ├── remote_browser_proxy_spec.rb ├── culerity_spec.rb ├── remote_object_proxy_spec.rb └── celerity_server_spec.rb ├── features ├── step_definitions │ ├── culerity_setup_steps.rb │ ├── jruby_steps.rb │ ├── rails_setup_steps.rb │ └── common_steps.rb ├── support │ ├── matchers.rb │ ├── env.rb │ └── common.rb ├── fixtures │ ├── sample_feature │ └── jquery ├── running_cucumber_without_explicitly_running_external_services.feature └── installing_culerity.feature ├── rails_generators └── culerity │ ├── templates │ ├── features │ │ ├── support │ │ │ └── env.rb │ │ └── step_definitions │ │ │ └── culerity_steps.rb │ ├── config │ │ └── environments │ │ │ ├── culerity_continuousintegration.rb │ │ │ └── culerity.rb │ ├── public │ │ └── javascripts │ │ │ └── culerity.js │ └── lib │ │ └── tasks │ │ └── culerity.rake │ └── culerity_generator.rb ├── script ├── destroy ├── generate └── console ├── CHANGES.md ├── MIT-LICENSE ├── Rakefile ├── culerity.gemspec └── README.md /init.rb: -------------------------------------------------------------------------------- 1 | require 'rails/init' -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'culerity' -------------------------------------------------------------------------------- /VERSION.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :minor: 2 3 | :build: 4 | :patch: 15 5 | :major: 0 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | rdoc 3 | pkg 4 | tmp 5 | *.sw? 6 | *.gem 7 | .rvmrc 8 | -------------------------------------------------------------------------------- /lib/start_celerity.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require File.dirname(__FILE__) + '/culerity/celerity_server' 3 | Culerity::CelerityServer.new(STDIN, STDOUT) 4 | -------------------------------------------------------------------------------- /bin/run_celerity_server.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env jruby 2 | require File.dirname(__FILE__) << '/../lib/culerity/celerity_server' 3 | Culerity::CelerityServer.new(STDIN, STDOUT) 4 | 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if RUBY_PLATFORM != 'java' 2 | puts "You need JRuby to run these specs" 3 | exit -1 4 | end 5 | 6 | require File.dirname(__FILE__) + '/../lib/culerity' 7 | require File.dirname(__FILE__) + '/../lib/culerity/celerity_server' -------------------------------------------------------------------------------- /features/step_definitions/culerity_setup_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I setup load path to local code$/ do 2 | project_lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib") 3 | in_project_folder do 4 | force_local_lib_override(:target => 'features/step_definitions/culerity_steps.rb') 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /rails_generators/culerity/templates/features/support/env.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "culerity" 2 | require File.expand_path(File.dirname(__FILE__) + '/../../config/environment') 3 | require 'cucumber/rails/world' 4 | 5 | Cucumber::Rails::World.use_transactional_fixtures = false 6 | ActionController::Base.allow_rescue = false 7 | 8 | require 'cucumber/formatter/unicode' 9 | require 'cucumber/rails/rspec' -------------------------------------------------------------------------------- /features/support/matchers.rb: -------------------------------------------------------------------------------- 1 | module Matchers 2 | def contain(expected) 3 | simple_matcher("contain #{expected.inspect}") do |given, matcher| 4 | matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}" 5 | matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}" 6 | given.index expected 7 | end 8 | end 9 | end 10 | 11 | World(Matchers) 12 | -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/destroy' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit] 14 | RubiGen::Scripts::Destroy.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')) 3 | 4 | begin 5 | require 'rubigen' 6 | rescue LoadError 7 | require 'rubygems' 8 | require 'rubigen' 9 | end 10 | require 'rubigen/scripts/generate' 11 | 12 | ARGV.shift if ['--help', '-h'].include?(ARGV[0]) 13 | RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit] 14 | RubiGen::Scripts::Generate.new.run(ARGV) 15 | -------------------------------------------------------------------------------- /features/fixtures/sample_feature: -------------------------------------------------------------------------------- 1 | Feature: Check that default Rails index.html shows information 2 | In order to value 3 | As a role 4 | I want feature 5 | 6 | Scenario: Check javascript runs on static file 7 | Given I go to the homepage 8 | Then I should not see "Rails version" 9 | When I follow "About your application’s environment" 10 | And I wait for the AJAX call to finish 11 | Then I should see "No route matches" 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # File: script/console 3 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 4 | 5 | libs = " -r irb/completion" 6 | # Perhaps use a console_lib to store any extra methods I may want available in the cosole 7 | # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}" 8 | libs << " -r #{File.dirname(__FILE__) + '/../lib/culerity.rb'}" 9 | puts "Loading culerity gem" 10 | exec "#{irb} #{libs} --simple-prompt" -------------------------------------------------------------------------------- /features/step_definitions/jruby_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I have jruby installed$/ do 2 | @jruby_cmd = `which jruby`.strip 3 | raise "Need to setup @jruby_cmd to test jruby environment" if @jruby_cmd.blank? 4 | end 5 | 6 | Then /^the gem "([^\"]*)" is installed into jruby environment$/ do |gem_name| 7 | raise "Need to setup @jruby_cmd to test jruby environment" if @jruby_cmd.blank? 8 | gem_list = `#{@jruby_cmd} -S gem list #{gem_name}` 9 | gem_list.should =~ /#{gem_name}/ 10 | end 11 | 12 | -------------------------------------------------------------------------------- /rails_generators/culerity/templates/config/environments/culerity_continuousintegration.rb: -------------------------------------------------------------------------------- 1 | # Settings specified here will take precedence over those in config/environment.rb 2 | 3 | # The production environment is meant for finished, "live" apps. 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.action_controller.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | config.action_view.cache_template_loading = true 11 | 12 | config.action_mailer.delivery_method = :persistent 13 | 14 | config.after_initialize do 15 | require 'culerity/persistent_delivery' 16 | end 17 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + "/../../lib/culerity" 2 | 3 | gem 'cucumber' 4 | require 'cucumber' 5 | gem 'rspec' 6 | require 'spec' 7 | 8 | Before do 9 | @tmp_root = File.dirname(__FILE__) + "/../../tmp" 10 | @home_path = File.expand_path(File.join(@tmp_root, "home")) 11 | @lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib") 12 | FileUtils.rm_rf @tmp_root 13 | FileUtils.mkdir_p @home_path 14 | ENV['HOME'] = @home_path 15 | end 16 | 17 | require 'rubigen' 18 | require 'rubigen/helpers/generator_test_helper' 19 | include RubiGen::GeneratorTestHelper 20 | 21 | SOURCES = Dir[File.dirname(__FILE__) + "/../../generators"].map do |f| 22 | RubiGen::PathSource.new(:test, File.expand_path(f)) 23 | end 24 | 25 | -------------------------------------------------------------------------------- /rails_generators/culerity/templates/public/javascripts/culerity.js: -------------------------------------------------------------------------------- 1 | // this allows culerity to wait until all ajax requests have finished 2 | jQuery(function($) { 3 | var original_ajax = $.ajax; 4 | var count_down = function(callback) { 5 | return function() { 6 | try { 7 | if(callback) { 8 | callback.apply(this, arguments); 9 | }; 10 | } catch(e) { 11 | window.running_ajax_calls -= 1; 12 | throw(e); 13 | } 14 | window.running_ajax_calls -= 1; 15 | }; 16 | }; 17 | window.running_ajax_calls = 0; 18 | 19 | var ajax_with_count = function(options) { 20 | if(options.async == false) { 21 | return(original_ajax(options)); 22 | } else { 23 | window.running_ajax_calls += 1; 24 | options.success = count_down(options.success); 25 | options.error = count_down(options.error); 26 | return original_ajax(options); 27 | } 28 | }; 29 | 30 | $.ajax = ajax_with_count; 31 | }); -------------------------------------------------------------------------------- /rails_generators/culerity/culerity_generator.rb: -------------------------------------------------------------------------------- 1 | class CulerityGenerator < Rails::Generator::Base 2 | 3 | def manifest 4 | record do |m| 5 | m.directory 'features/step_definitions' 6 | m.file 'features/step_definitions/culerity_steps.rb', 'features/step_definitions/culerity_steps.rb' 7 | m.file 'features/support/env.rb', 'features/support/env.rb' 8 | m.file 'config/environments/culerity.rb', 'config/environments/culerity.rb' 9 | 10 | m.gsub_file 'config/database.yml', /cucumber:.*\n/, "cucumber: &CUCUMBER\n" 11 | 12 | m.gsub_file 'config/database.yml', /\z/, "\nculerity:\n <<: *CUCUMBER" 13 | 14 | m.file "lib/tasks/culerity.rake", "lib/tasks/culerity.rake" 15 | 16 | m.file 'public/javascripts/culerity.js', 'public/javascripts/culerity.js' 17 | end 18 | end 19 | 20 | protected 21 | 22 | def banner 23 | "Usage: #{$0} culerity" 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /rails_generators/culerity/templates/config/environments/culerity.rb: -------------------------------------------------------------------------------- 1 | config.cache_classes = true # set because of https://rspec.lighthouseapp.com/projects/16211/tickets/165 2 | 3 | # Log error messages when you accidentally call methods on nil. 4 | config.whiny_nils = true 5 | 6 | # Show full error reports and disable caching 7 | config.action_controller.consider_all_requests_local = true 8 | config.action_controller.perform_caching = false 9 | config.action_view.cache_template_loading = false 10 | 11 | # Disable request forgery protection in test environment 12 | config.action_controller.allow_forgery_protection = false 13 | 14 | # Tell Action Mailer not to deliver emails to the real world. 15 | # The :test delivery method accumulates sent emails in the 16 | # ActionMailer::Base.deliveries array. 17 | config.action_mailer.delivery_method = :persistent 18 | 19 | config.after_initialize do 20 | require 'culerity/persistent_delivery' 21 | end 22 | 23 | -------------------------------------------------------------------------------- /features/support/common.rb: -------------------------------------------------------------------------------- 1 | module CommonHelpers 2 | def in_tmp_folder(&block) 3 | FileUtils.chdir(@tmp_root, &block) 4 | end 5 | 6 | def in_project_folder(&block) 7 | project_folder = @active_project_folder || @tmp_root 8 | FileUtils.chdir(project_folder, &block) 9 | end 10 | 11 | def in_home_folder(&block) 12 | FileUtils.chdir(@home_path, &block) 13 | end 14 | 15 | def force_local_lib_override(options = {}) 16 | target_path = options[:target_path] || options[:target_file] || options[:target] || 'Rakefile' 17 | in_project_folder do 18 | contents = File.read(target_path) 19 | File.open(target_path, "w+") do |f| 20 | f << "$:.unshift('#{@lib_path}')\n" 21 | f << contents 22 | end 23 | end 24 | end 25 | 26 | def setup_active_project_folder project_name 27 | @active_project_folder = File.join(@tmp_root, project_name) 28 | @project_name = project_name 29 | end 30 | end 31 | 32 | World(CommonHelpers) -------------------------------------------------------------------------------- /lib/culerity/persistent_delivery.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Culerity 4 | module PersistentDelivery 5 | 6 | DELIVERIES_PATH = 7 | File.join(RAILS_ROOT, 'tmp', 'action_mailer_acceptance_deliveries.cache') 8 | 9 | def self.included(base) 10 | base.class_eval do 11 | def self.deliveries 12 | return [] unless File.exist?(DELIVERIES_PATH) 13 | File.open(DELIVERIES_PATH,'r') do |f| 14 | Marshal.load(f) 15 | end 16 | end 17 | 18 | def self.clear_deliveries 19 | FileUtils.rm_f DELIVERIES_PATH 20 | end 21 | end 22 | end 23 | 24 | def perform_delivery_persistent(mail) 25 | deliveries = self.class.deliveries << mail 26 | File.open(DELIVERIES_PATH,'w') do |f| 27 | f << Marshal.dump(deliveries) 28 | end 29 | end 30 | end 31 | end 32 | 33 | ActionMailer::Base.send :include, Culerity::PersistentDelivery if defined?(ActionMailer) 34 | 35 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.10 2 | * another speed improvement by only clearing cookies instead of closing browsers after each scenario (dewind) 3 | 4 | ## 0.2.9 5 | * fixed memory leaks within the java process by clearing proxies (dewind) 6 | * fixed syntax error when sending multiline lambdas (endor, langalex) 7 | 8 | ## 0.2.8 9 | 10 | * removed separate development and continuous integration environments and replaced them with a single one (thilo) 11 | * more webrat like step definitions (lupine) 12 | * improve on stability issues (mattmatt) 13 | 14 | ## 0.2.7 15 | 16 | * fixed RemoteBrowser#confirm called celerity remove_listener with invalid arguments 17 | * extended communication protocol to be able to send procs as arguments and blocks 18 | * default mail delivery method is now 'persistent', ActionMailer::Base.deliveries works again in features 19 | 20 | 21 | ## 0.2.5 22 | 23 | * added javascript helper to make 'I wait for the AJAX call to finish' work reliably (langalex) 24 | 25 | ## Before that 26 | 27 | Lots of important contributions from a bunch of people. check the commit logs. 28 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Alexander Lang 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'spec/rake/spectask' 3 | require 'rake/rdoctask' 4 | require 'cucumber/rake/task' 5 | 6 | begin 7 | require 'rubygems' unless ENV['NO_RUBYGEMS'] 8 | require 'jeweler' 9 | Jeweler::Tasks.new do |s| 10 | s.name = "culerity" 11 | s.summary = %Q{Culerity integrates Cucumber and Celerity in order to test your application's full stack.} 12 | s.email = "alex@upstream-berlin.com" 13 | s.homepage = "http://github.com/langalex/culerity" 14 | s.description = "Culerity integrates Cucumber and Celerity in order to test your application's full stack." 15 | s.authors = ["Alexander Lang"] 16 | 17 | s.add_development_dependency 'cucumber' 18 | s.add_development_dependency 'rspec' 19 | end 20 | rescue LoadError 21 | puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler' 22 | end 23 | 24 | desc "Run all unit specs" 25 | Spec::Rake::SpecTask.new(:spec) do |t| 26 | t.spec_files = FileList['spec/*_spec.rb'] 27 | end 28 | 29 | desc "Run all features" 30 | Cucumber::Rake::Task.new(:features) do |t| 31 | t.fork = true 32 | t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'pretty')] 33 | end 34 | 35 | Rake::RDocTask.new do |rdoc| 36 | rdoc.rdoc_dir = 'rdoc' 37 | rdoc.title = 'Culerity' 38 | rdoc.options << '--line-numbers' << '--inline-source' 39 | rdoc.rdoc_files.include('README*') 40 | rdoc.rdoc_files.include('lib/**/*.rb') 41 | end 42 | 43 | task :default => :spec 44 | -------------------------------------------------------------------------------- /features/running_cucumber_without_explicitly_running_external_services.feature: -------------------------------------------------------------------------------- 1 | Feature: Running cucumber without explicitly running external services 2 | In order to reduce learning cost of using culerity 3 | As a rails developer 4 | I want the headless browser and rails processes to launch and shutdown automatically 5 | 6 | Background: 7 | Given a Rails app 8 | And I run executable "script/generate" with arguments "cucumber --rspec --webrat" 9 | And I delete file "features/step_definitions/web_steps.rb" 10 | And I delete file "features/support/env.rb" 11 | And culerity is installed as a plugin 12 | And I invoke task "rake db:migrate" 13 | When I run executable "script/generate" with arguments "culerity" 14 | And I setup load path to local code 15 | And I setup the culerity javascript helpers 16 | And I add the JRUBY_INVOCATION check to "features/support/env.rb" 17 | And I add an rvm_verbose_flag=0-wielding .rvmrc to the home folder 18 | 19 | Scenario: Successfully run scenarios without requiring celerity or rails processes running 20 | When I add a feature file to test Rails index.html default file 21 | And I run executable "cucumber" with arguments "features/" 22 | Then file "tmp/culerity_rails_server.pid" is not created 23 | And I should see "1 scenario" 24 | And I should see "5 steps (5 passed)" 25 | And I should see "WARNING: Speed up execution by running 'rake culerity:rails:start'" 26 | -------------------------------------------------------------------------------- /rails_generators/culerity/templates/lib/tasks/culerity.rake: -------------------------------------------------------------------------------- 1 | namespace 'culerity' do 2 | namespace 'rails' do 3 | desc "Starts a rails server for cucumber/culerity tests" 4 | task :start do 5 | port = ENV['PORT'] || 3001 6 | environment = ENV["RAILS_ENV"] || 'culerity' 7 | pid_file = RAILS_ROOT + "/tmp/culerity_rails_server.pid" 8 | if File.exists?(pid_file) 9 | puts "culerity rails server already running; if not, delete tmp/culerity_rails_server.pid and try again" 10 | exit 1 11 | end 12 | rails_server = IO.popen("script/server -e #{environment} -p #{port}", 'r+') 13 | File.open(pid_file, "w") { |file| file << rails_server.pid } 14 | end 15 | 16 | desc "Stops the running rails server for cucumber/culerity tests" 17 | task :stop do 18 | pid_file = RAILS_ROOT + "/tmp/culerity_rails_server.pid" 19 | if File.exists?(pid_file) 20 | pid = File.read(pid_file).to_i 21 | Process.kill(6, pid) 22 | File.delete(pid_file) 23 | else 24 | puts "No culerity rails server running. Doing nothing." 25 | end 26 | end 27 | 28 | desc "Restarts the rails server for cucumber/culerity tests" 29 | task :restart => [:stop, :start] 30 | end 31 | 32 | desc "Install required gems into jruby" 33 | task :install do 34 | jgem_cmd = `which jruby`.strip 35 | raise "ERROR: You need to install jruby to use culerity and celerity." if jgem_cmd.blank? 36 | sh "#{jgem_cmd} -S gem install celerity" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/culerity.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/culerity/remote_object_proxy' 2 | require File.dirname(__FILE__) + '/culerity/remote_browser_proxy' 3 | 4 | Symbol.class_eval do 5 | def to_proc 6 | Proc.new{|object| object.send(self)} 7 | end 8 | end unless :symbol.respond_to?(:to_proc) 9 | 10 | module Culerity 11 | 12 | module ServerCommands 13 | def exit_server 14 | self << '["_exit_"]' 15 | Process.kill(6, self.pid.to_i) 16 | end 17 | 18 | def close_browsers 19 | self.puts '["_close_browsers_"]' 20 | end 21 | 22 | def clear_proxies 23 | self.puts '["_clear_proxies_"]' 24 | end 25 | end 26 | 27 | def self.culerity_root 28 | File.expand_path('../../', __FILE__) 29 | end 30 | 31 | def self.celerity_invocation 32 | %{#{culerity_root}/lib/start_celerity.rb} 33 | end 34 | 35 | def self.jruby_invocation 36 | @jruby_invocation ||= (ENV["JRUBY_INVOCATION"] || "jruby") 37 | end 38 | 39 | def self.jruby_invocation=(invocation) 40 | @jruby_invocation = invocation 41 | end 42 | 43 | def self.run_server 44 | IO.popen(%{RUBYOPT="" #{jruby_invocation} "#{celerity_invocation}"}, 'r+').extend(ServerCommands) 45 | end 46 | 47 | def self.run_rails(options = {}) 48 | if defined?(Rails) && !File.exists?("tmp/culerity_rails_server.pid") 49 | puts "WARNING: Speed up execution by running 'rake culerity:rails:start'" 50 | port = options[:port] || 3001 51 | environment = options[:environment] || 'culerity' 52 | rails_server = fork do 53 | $stdin.reopen "/dev/null" 54 | $stdout.reopen "/dev/null" 55 | $stderr.reopen "/dev/null" 56 | Dir.chdir(Rails.root) do 57 | exec "script/server -e #{environment} -p #{port}" 58 | end 59 | end 60 | sleep 5 61 | rails_server 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /features/installing_culerity.feature: -------------------------------------------------------------------------------- 1 | Feature: Installing culerity 2 | In order to not have to use f@#$ing selenium and receive hate into our lives 3 | As a self-respective Rails/JavaScript developer 4 | I want to install culerity into my Rails app 5 | 6 | Background: 7 | Given a Rails app 8 | And I run executable "script/generate" with arguments "cucumber --rspec --webrat" 9 | And I delete file "features/step_definitions/web_steps.rb" 10 | And I delete file "features/support/env.rb" 11 | And culerity is installed as a plugin 12 | And I invoke task "rake db:migrate" 13 | When I run executable "script/generate" with arguments "culerity" 14 | And I setup load path to local code 15 | And I setup the culerity javascript helpers 16 | And I add the JRUBY_INVOCATION check to "features/support/env.rb" 17 | And I add an rvm_verbose_flag=0-wielding .rvmrc to the home folder 18 | 19 | Scenario: Install culerity and test the rails start + stop tasks 20 | When I invoke task "rake culerity:rails:start" 21 | Then file "tmp/culerity_rails_server.pid" is created 22 | And I invoke task "rake culerity:rails:stop" 23 | Then file "tmp/culerity_rails_server.pid" is not created 24 | 25 | Scenario: Install culerity into a Rails app and check it works 26 | Then file "features/step_definitions/culerity_steps.rb" is created 27 | Then file "config/environments/culerity.rb" is created 28 | 29 | When I run executable "cucumber" with arguments "features/" 30 | Then I should see "0 scenarios" 31 | And I should see "0 steps" 32 | Given I invoke task "rake culerity:rails:start" 33 | When I add a feature file to test Rails index.html default file 34 | And I run executable "cucumber" with arguments "features/" 35 | Then I should see "1 scenario" 36 | And I should see "5 steps (5 passed)" 37 | And I should not see "WARNING: Speed up executing by running 'rake culerity:rails:start'" 38 | -------------------------------------------------------------------------------- /lib/culerity/remote_browser_proxy.rb: -------------------------------------------------------------------------------- 1 | module Culerity 2 | class RemoteBrowserProxy < RemoteObjectProxy 3 | def initialize(io, browser_options = {}) 4 | @io = io 5 | #sets the remote receiver to celerity for the new_browser message. 6 | @remote_object_id = "celerity".inspect 7 | #celerity server will create a new browser which shall receive the remote calls from now on. 8 | @remote_object_id = new_browser(browser_options).inspect 9 | end 10 | 11 | # 12 | # Calls the block until it returns true or +time_to_wait+ is reached. 13 | # +time_to_wait+ is 30 seconds by default 14 | # 15 | # Returns true upon success 16 | # Raises RuntimeError when +time_to_wait+ is reached. 17 | # 18 | def wait_until time_to_wait=30, &block 19 | time_limit = Time.now + time_to_wait 20 | until block.call 21 | if Time.now > time_limit 22 | raise "wait_until timeout after #{time_to_wait} seconds" 23 | end 24 | sleep 0.1 25 | end 26 | true 27 | end 28 | 29 | # 30 | # Calls the block until it doesn't return true or +time_to_wait+ is reached. 31 | # +time_to_wait+ is 30 seconds by default 32 | # 33 | # Returns true upon success 34 | # Raises RuntimeError when +time_to_wait+ is reached. 35 | # 36 | def wait_while time_to_wait=30, &block 37 | time_limit = Time.now + time_to_wait 38 | while block.call 39 | if Time.now > time_limit 40 | raise "wait_while timeout after #{time_to_wait} seconds" 41 | end 42 | sleep 0.1 43 | end 44 | true 45 | end 46 | 47 | 48 | # 49 | # Specify whether to accept or reject all confirm js dialogs 50 | # for the code in the block that's run. 51 | # 52 | def confirm(bool, &block) 53 | blk = "lambda { #{bool} }" 54 | 55 | self.send_remote(:add_listener, :confirm) { blk } 56 | block.call 57 | self.send_remote(:remove_listener, :confirm, lambda {blk}) 58 | end 59 | 60 | end 61 | 62 | 63 | end 64 | -------------------------------------------------------------------------------- /lib/tasks/rspec.rake: -------------------------------------------------------------------------------- 1 | gem 'test-unit', '1.2.3' if RUBY_VERSION.to_f >= 1.9 2 | 3 | # Don't load rspec if running "rake gems:*" 4 | unless ARGV.any? {|a| a =~ /^gems/} 5 | 6 | begin 7 | require 'spec/rake/spectask' 8 | rescue MissingSourceFile 9 | module Spec 10 | module Rake 11 | class SpecTask 12 | def initialize(name) 13 | task name do 14 | # if rspec-rails is a configured gem, this will output helpful material and exit ... 15 | require File.expand_path(File.dirname(__FILE__) + "/../../config/environment") 16 | 17 | # ... otherwise, do this: 18 | raise <<-MSG 19 | 20 | #{"*" * 80} 21 | * You are trying to run an rspec rake task defined in 22 | * #{__FILE__}, 23 | * but rspec can not be found in vendor/gems, vendor/plugins or system gems. 24 | #{"*" * 80} 25 | MSG 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | 33 | Rake.application.instance_variable_get('@tasks').delete('default') 34 | 35 | spec_prereq = File.exist?(File.join(RAILS_ROOT, 'config', 'database.yml')) ? "db:test:prepare" : :noop 36 | task :noop do 37 | end 38 | 39 | task :default => [:spec,:features] 40 | task :stats => "spec:statsetup" 41 | 42 | desc "Run all specs in spec directory (excluding plugin specs)" 43 | Spec::Rake::SpecTask.new(:spec => spec_prereq) do |t| 44 | t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""] 45 | t.spec_files = FileList['spec/**/*/*_spec.rb'] 46 | end 47 | 48 | desc "Run all specs in the spec directory with html output (excluding plugins specs)" 49 | Spec::Rake::SpecTask.new(:spec_html => spec_prereq) do |t| 50 | t.spec_opts = ['--colour', '--format html', '--loadby mtime', '--reverse'] 51 | t.spec_files = FileList['spec/**/*/*_spec.rb'] 52 | end 53 | 54 | namespace :spec do 55 | desc "Run all specs in spec directory with RCov (excluding plugin specs)" 56 | Spec::Rake::SpecTask.new(:rcov) do |t| 57 | t.spec_opts = ['--options', "\"#{RAILS_ROOT}/spec/spec.opts\""] 58 | t.spec_files = FileList['spec/**/*/*_spec.rb'] 59 | t.rcov = true 60 | t.rcov_opts = lambda do 61 | IO.readlines("#{RAILS_ROOT}/spec/rcov.opts").map {|l| l.chomp.split " "}.flatten 62 | end 63 | end 64 | end 65 | 66 | end -------------------------------------------------------------------------------- /features/step_definitions/rails_setup_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^a Rails app$/ do 2 | FileUtils.chdir(@tmp_root) do 3 | `rails my_project` 4 | end 5 | @active_project_folder = File.expand_path(File.join(@tmp_root, "my_project")) 6 | end 7 | 8 | Given /^culerity is installed as a plugin$/ do 9 | generators_folder = 'vendor/generators' 10 | plugin_folder = 'vendor/plugins/culerity' 11 | in_project_folder do 12 | FileUtils.mkdir_p(generators_folder) 13 | FileUtils.mkdir_p(plugin_folder) 14 | end 15 | `cp -rf #{File.dirname(__FILE__) + "/../../rails_generators/*"} #{File.join(@active_project_folder, generators_folder)}` 16 | `cp -rf #{File.dirname(__FILE__) + "/../../lib"} #{File.join(@active_project_folder, plugin_folder)}` 17 | `cp -rf #{File.dirname(__FILE__) + "/../../rails"} #{File.join(@active_project_folder, plugin_folder)}` 18 | `cp -rf #{File.dirname(__FILE__) + "/../../init.rb"} #{File.join(@active_project_folder, plugin_folder)}` 19 | `cp -rf #{File.dirname(__FILE__) + "/../../bin"} #{File.join(@active_project_folder, plugin_folder)}` 20 | end 21 | 22 | When /^I add a feature file to test Rails index.html default file$/ do 23 | sample_feature = File.expand_path(File.dirname(__FILE__) + "/../fixtures/sample_feature") 24 | in_project_folder do 25 | `cp -rf #{sample_feature} features/sample.feature` 26 | end 27 | end 28 | 29 | When /^(?:I )?add an rvm_verbose_flag=0-wielding \.rvmrc to the home folder$/ do 30 | in_home_folder do 31 | File.open('.rvmrc', 'w+') do |f| 32 | f.puts "rvm_verbose_flag=0" 33 | end 34 | end 35 | end 36 | 37 | When /^I setup the culerity javascript helpers$/ do 38 | `cp #{File.dirname(__FILE__) + "/../fixtures/jquery"} #{File.join(@active_project_folder, 'public', 'javascripts', 'jquery.js')}` 39 | in_project_folder do 40 | _index = File.read('public/index.html') 41 | File.open('public/index.html', 'w') do |f| 42 | f << _index.sub('', '') 43 | end 44 | end 45 | end 46 | 47 | After do 48 | in_project_folder do 49 | Given 'I invoke task "rake culerity:rails:stop"' 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/remote_browser_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe Culerity::RemoteBrowserProxy do 4 | before(:each) do 5 | @io = stub 'io', :gets => "[:return, \"browser0\"]", :<< => nil 6 | end 7 | 8 | it "should send the serialized method call to the output" do 9 | @io.should_receive(:<<).with("[[\"celerity\", \"new_browser\", {}]]\n").ordered 10 | @io.should_receive(:<<).with("[[\"browser0\", \"goto\", \"/homepage\"]]\n").ordered 11 | proxy = Culerity::RemoteBrowserProxy.new @io 12 | proxy.goto '/homepage' 13 | end 14 | 15 | it "should return the deserialized return value" do 16 | io = stub 'io', :gets => "[:return, :okay]\n", :<< => nil 17 | proxy = Culerity::RemoteBrowserProxy.new io 18 | proxy.goto.should == :okay 19 | end 20 | 21 | it "should send the browser options to the remote server" do 22 | io = stub 'io', :gets => "[:return, \"browser0\"]" 23 | io.should_receive(:<<).with('[["celerity", "new_browser", {:browser=>:firefox}]]' + "\n") 24 | proxy = Culerity::RemoteBrowserProxy.new io, {:browser => :firefox} 25 | end 26 | 27 | it "should timeout if wait_until takes too long" do 28 | proxy = Culerity::RemoteBrowserProxy.new @io 29 | lambda { 30 | proxy.wait_until(0.1) { false } 31 | }.should raise_error(RuntimeError) 32 | end 33 | 34 | it "should return successfully when wait_until returns true" do 35 | proxy = Culerity::RemoteBrowserProxy.new @io 36 | proxy.wait_until(0.1) { true }.should == true 37 | end 38 | 39 | it "should timeout if wait_while takes too long" do 40 | proxy = Culerity::RemoteBrowserProxy.new @io 41 | lambda { 42 | proxy.wait_while(0.1) { true } 43 | }.should raise_error(RuntimeError) 44 | end 45 | 46 | it "should return successfully when wait_while returns !true" do 47 | proxy = Culerity::RemoteBrowserProxy.new @io 48 | proxy.wait_while(0.1) { false }.should == true 49 | end 50 | 51 | it "should accept all javascript confirmation dialogs" do 52 | proxy = Culerity::RemoteBrowserProxy.new @io 53 | 54 | proxy.should_receive(:send_remote).with(:add_listener, :confirm).and_return(true) 55 | proxy.should_receive(:send_remote).with(:goto, "http://example.com").and_return(true) 56 | proxy.should_receive(:send_remote).with(:remove_listener, :confirm, an_instance_of(Proc)).and_return(true) 57 | 58 | proxy.confirm(true) do 59 | proxy.goto "http://example.com" 60 | end 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /lib/culerity/celerity_server.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'celerity' 3 | 4 | 5 | module Culerity 6 | class CelerityServer 7 | 8 | def initialize(_in, _out) 9 | @proxies = {} 10 | @browsers = [] 11 | 12 | while(true) 13 | call, block = eval _in.gets.to_s.strip 14 | return if call == "_exit_" 15 | next(close_browsers) if call == "_close_browsers_" 16 | next(clear_proxies) if call == "_clear_proxies_" 17 | 18 | unless call.nil? 19 | begin 20 | result = target(call.first).send call[1], *call[2..-1], &block 21 | _out << "[:return, #{proxify result}]\n" 22 | rescue => e 23 | _out << "[:exception, \"#{e.class.name}\", #{e.message.inspect}, #{prepend_js_stack_trace(e).inspect}]\n" 24 | end 25 | end 26 | 27 | end 28 | 29 | end 30 | 31 | private 32 | 33 | def clear_proxies 34 | @proxies = {} 35 | end 36 | 37 | def configure_browser(options) 38 | @browser_options = options 39 | end 40 | 41 | def new_browser(options, number = nil) 42 | number ||= @browsers.size 43 | @browsers[number] = Celerity::Browser.new(options || @browser_options || {}) 44 | "browser#{number}" 45 | end 46 | 47 | def close_browsers 48 | @browsers.each { |browser| browser.close } 49 | @browsers = [] 50 | @proxies = {} 51 | end 52 | 53 | def browser(number) 54 | unless @browsers[number] 55 | new_browser(nil, number) 56 | end 57 | @browsers[number] 58 | end 59 | 60 | def target(object_id) 61 | if object_id =~ /browser(\d+)/ 62 | browser($1.to_i) 63 | elsif object_id == 'celerity' 64 | self 65 | else 66 | @proxies[object_id] 67 | end 68 | end 69 | 70 | def proxify(result) 71 | if result.is_a?(Array) 72 | "[" + result.map {|x| proxify(x) }.join(", ") + "]" 73 | elsif [Symbol, String, TrueClass, FalseClass, Fixnum, Float, NilClass].include?(result.class) 74 | result.inspect 75 | else 76 | @proxies[result.object_id] = result 77 | "Culerity::RemoteObjectProxy.new(#{result.object_id}, @io)" 78 | end 79 | end 80 | 81 | def prepend_js_stack_trace(exception) 82 | def extract_js_strack_trace(e) 83 | if e.respond_to?(:getScriptStackTrace) 84 | e.getScriptStackTrace 85 | elsif e.respond_to?(:cause) && e.cause 86 | extract_js_strack_trace e.cause 87 | else 88 | "" 89 | end 90 | end 91 | extract_js_strack_trace(exception).split("\n") + exception.backtrace 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/culerity/remote_object_proxy.rb: -------------------------------------------------------------------------------- 1 | module Culerity 2 | 3 | class CulerityException < StandardError 4 | def initialize(message, backtrace) 5 | super message 6 | set_backtrace(backtrace) 7 | end 8 | end 9 | 10 | class RemoteObjectProxy 11 | def initialize(remote_object_id, io) 12 | @remote_object_id = remote_object_id 13 | @io = io 14 | end 15 | 16 | # 17 | # Commonly used to get the HTML id attribute 18 | # Use `object_id` to get the local objects' id. 19 | # 20 | def id 21 | send_remote(:id) 22 | end 23 | 24 | def inspect 25 | send_remote(:inspect) 26 | end 27 | 28 | def respond_to?(name) 29 | send_remote :respond_to?, name 30 | end 31 | 32 | def method_missing(name, *args, &block) 33 | send_remote(name, *args, &block) 34 | end 35 | 36 | # 37 | # Calls the passed method on the remote object with any arguments specified. 38 | # Behaves the same as Object#send. 39 | # 40 | # If you pass it a block then it will append the block as a "lambda { … }". 41 | # If your block returns a lambda string ("lambda { … }") then it will be passed 42 | # straight through, otherwise it will be wrapped in a lambda string before sending. 43 | # 44 | def send_remote(name, *args, &blk) 45 | input = [remote_object_id, %Q{"#{name}"}, *args.map{|a| arg_to_string(a)}] 46 | serialized_block = ", #{block_to_string(&blk)}" if block_given? 47 | @io << "[[#{input.join(", ")}]#{serialized_block}]\n" 48 | process_result @io.gets.to_s.strip 49 | end 50 | 51 | def exit 52 | @io << '["_exit_"]' 53 | end 54 | 55 | private 56 | 57 | def process_result(result) 58 | res = eval result 59 | if res.first == :return 60 | res[1] 61 | elsif res.first == :exception 62 | begin 63 | raise "local trace" 64 | rescue => ex 65 | raise CulerityException.new("#{res[1]}: #{res[2]}", res[3] + ex.backtrace) 66 | end 67 | end 68 | end 69 | 70 | # 71 | # Takes a block and either returns the result (if it returns "lambda { … }") 72 | # or builds the lambda string with the result of the block in it. 73 | # 74 | # Returns a string in the format "lambda { … }" 75 | # 76 | def block_to_string &block 77 | result = block.call.to_s.strip 78 | unless result.is_a?(String) && result[/^lambda\s*(\{|do).+(\}|end)/xm] 79 | result = "lambda { #{result} }" 80 | end 81 | result.gsub("\n", ";") 82 | end 83 | 84 | def arg_to_string(arg) 85 | if arg.is_a?(Proc) 86 | block_to_string(&arg) 87 | else 88 | arg.inspect 89 | end 90 | end 91 | 92 | def remote_object_id 93 | @remote_object_id 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /culerity.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{culerity} 8 | s.version = "0.2.15" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Alexander Lang"] 12 | s.date = %q{2011-01-21} 13 | s.default_executable = %q{run_celerity_server.rb} 14 | s.description = %q{Culerity integrates Cucumber and Celerity in order to test your application's full stack.} 15 | s.email = %q{alex@upstream-berlin.com} 16 | s.executables = ["run_celerity_server.rb"] 17 | s.extra_rdoc_files = [ 18 | "README.md" 19 | ] 20 | s.files = [ 21 | "CHANGES.md", 22 | "MIT-LICENSE", 23 | "README.md", 24 | "Rakefile", 25 | "VERSION.yml", 26 | "bin/run_celerity_server.rb", 27 | "culerity.gemspec", 28 | "features/fixtures/jquery", 29 | "features/fixtures/sample_feature", 30 | "features/installing_culerity.feature", 31 | "features/running_cucumber_without_explicitly_running_external_services.feature", 32 | "features/step_definitions/common_steps.rb", 33 | "features/step_definitions/culerity_setup_steps.rb", 34 | "features/step_definitions/jruby_steps.rb", 35 | "features/step_definitions/rails_setup_steps.rb", 36 | "features/support/common.rb", 37 | "features/support/env.rb", 38 | "features/support/matchers.rb", 39 | "init.rb", 40 | "lib/culerity.rb", 41 | "lib/culerity/celerity_server.rb", 42 | "lib/culerity/persistent_delivery.rb", 43 | "lib/culerity/remote_browser_proxy.rb", 44 | "lib/culerity/remote_object_proxy.rb", 45 | "lib/start_celerity.rb", 46 | "lib/tasks/rspec.rake", 47 | "rails/init.rb", 48 | "rails_generators/culerity/culerity_generator.rb", 49 | "rails_generators/culerity/templates/config/environments/culerity.rb", 50 | "rails_generators/culerity/templates/config/environments/culerity_continuousintegration.rb", 51 | "rails_generators/culerity/templates/features/step_definitions/culerity_steps.rb", 52 | "rails_generators/culerity/templates/features/support/env.rb", 53 | "rails_generators/culerity/templates/lib/tasks/culerity.rake", 54 | "rails_generators/culerity/templates/public/javascripts/culerity.js", 55 | "script/console", 56 | "script/destroy", 57 | "script/generate", 58 | "spec/celerity_server_spec.rb", 59 | "spec/culerity_spec.rb", 60 | "spec/remote_browser_proxy_spec.rb", 61 | "spec/remote_object_proxy_spec.rb", 62 | "spec/spec_helper.rb" 63 | ] 64 | s.homepage = %q{http://github.com/langalex/culerity} 65 | s.require_paths = ["lib"] 66 | s.rubygems_version = %q{1.3.7} 67 | s.summary = %q{Culerity integrates Cucumber and Celerity in order to test your application's full stack.} 68 | s.test_files = [ 69 | "spec/celerity_server_spec.rb", 70 | "spec/culerity_spec.rb", 71 | "spec/remote_browser_proxy_spec.rb", 72 | "spec/remote_object_proxy_spec.rb", 73 | "spec/spec_helper.rb" 74 | ] 75 | 76 | if s.respond_to? :specification_version then 77 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 78 | s.specification_version = 3 79 | 80 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 81 | s.add_development_dependency(%q, [">= 0"]) 82 | s.add_development_dependency(%q, [">= 0"]) 83 | else 84 | s.add_dependency(%q, [">= 0"]) 85 | s.add_dependency(%q, [">= 0"]) 86 | end 87 | else 88 | s.add_dependency(%q, [">= 0"]) 89 | s.add_dependency(%q, [">= 0"]) 90 | end 91 | end 92 | 93 | -------------------------------------------------------------------------------- /spec/culerity_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | 3 | describe Culerity do 4 | describe 'run_rails' do 5 | def stub_rails_root! 6 | unless defined?(::Rails) 7 | Kernel.const_set "Rails", stub() 8 | end 9 | Rails.stub!(:root).and_return(Dir.pwd) 10 | end 11 | 12 | before(:each) do 13 | Kernel.stub!(:sleep) 14 | IO.stub!(:popen) 15 | Culerity.stub!(:fork).and_yield.and_return(3200) 16 | Culerity.stub!(:exec) 17 | Culerity.stub!(:sleep) 18 | [$stdin, $stdout, $stderr].each{|io| io.stub(:reopen)} 19 | end 20 | 21 | it "should not run rails if we are not using rails" do 22 | Culerity.should_not_receive(:exec) 23 | Culerity.run_rails :port => 4000, :environment => 'culerity' 24 | end 25 | 26 | describe "when Rails is being used" do 27 | before(:each) do 28 | stub_rails_root! 29 | end 30 | 31 | it "should run rails with default values" do 32 | Culerity.should_receive(:exec).with("script/server -e culerity -p 3001") 33 | Culerity.run_rails 34 | end 35 | 36 | it "should run rails with the given values" do 37 | Culerity.should_receive(:exec).with("script/server -e culerity -p 4000") 38 | Culerity.run_rails :port => 4000, :environment => 'culerity' 39 | end 40 | 41 | it "should change into the rails root directory" do 42 | Dir.should_receive(:chdir).with(Dir.pwd) 43 | Culerity.run_rails :port => 4000, :environment => 'culerity' 44 | end 45 | 46 | it "should wait for the server to start up" do 47 | Culerity.should_receive(:sleep) 48 | Culerity.run_rails :port => 4000, :environment => 'culerity' 49 | end 50 | 51 | it "should reopen the i/o channels to /dev/null" do 52 | [$stdin, $stdout, $stderr].each{|io| io.should_receive(:reopen).with("/dev/null")} 53 | Culerity.run_rails :port => 4000, :environment => 'culerity' 54 | end 55 | end 56 | end 57 | 58 | describe "run_server" do 59 | before(:each) do 60 | IO.stub!(:popen) 61 | end 62 | 63 | after(:each) do 64 | Culerity.jruby_invocation = nil 65 | end 66 | 67 | it "knows where it is located" do 68 | Culerity.culerity_root.should == File.expand_path(File.dirname(__FILE__) + '/../') 69 | end 70 | 71 | it "has access to the Celerity invocation" do 72 | Culerity.stub!(:culerity_root).and_return('/path/to/culerity') 73 | 74 | Culerity.celerity_invocation.should == "/path/to/culerity/lib/start_celerity.rb" 75 | end 76 | 77 | describe "invoking JRuby" do 78 | it "knows how to invoke it" do 79 | Culerity.jruby_invocation.should == 'jruby' 80 | end 81 | 82 | it "allows for the invocation to be overridden directly" do 83 | Culerity.jruby_invocation = '/opt/local/bin/jruby' 84 | 85 | Culerity.jruby_invocation.should == '/opt/local/bin/jruby' 86 | end 87 | 88 | it "allows for the invocation to be overridden from an environment variable" do 89 | ENV['JRUBY_INVOCATION'] = 'rvm jruby ruby' 90 | 91 | Culerity.jruby_invocation.should == 'rvm jruby ruby' 92 | end 93 | end 94 | 95 | it "shells out and sparks up jruby with the correct invocation" do 96 | Culerity.stub!(:celerity_invocation).and_return('/path/to/start_celerity.rb') 97 | 98 | IO.should_receive(:popen).with('RUBYOPT="" jruby "/path/to/start_celerity.rb"', 'r+') 99 | 100 | Culerity.run_server 101 | end 102 | 103 | it "allows a more complex situation, e.g. using RVM + named gemset" do 104 | Culerity.stub!(:celerity_invocation).and_return('/path/to/start_celerity.rb') 105 | 106 | IO.should_receive(:popen).with('RUBYOPT="" rvm jruby@culerity ruby "/path/to/start_celerity.rb"', 'r+') 107 | 108 | Culerity.jruby_invocation = "rvm jruby@culerity ruby" 109 | Culerity.run_server 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | Culerity integrates Cucumber and Celerity in order to test your application's full stack. 4 | 5 | Culerity lets you: 6 | * run Celerity from within Cucumber which allows you to test the full stack of your Rails (or other web) application from Database to in browser JavaScript 7 | * run your application in any Ruby (like MRI 1.8.6) while Celerity runs in JRuby so you can still use gems/plugins that would not work with JRuby 8 | * reuse existing Webrat-Style step definitions 9 | 10 | ## Getting Started 11 | 12 | The following guide is written for a Rails application (tested with 2.3.5) but Culerity should work with any other Web Framework that is supported by Cucumber. 13 | 14 | First download JRuby and unpack it to some location, for example $HOME/jruby. Make sure that the jruby executable is in your path. You can do this by either setting your PATH accordingly... 15 | 16 | export PATH=$HOME/jruby/bin:$PATH 17 | 18 | ... or by creating a symlink from your bin directory: 19 | 20 | ln -s $HOME/jruby/bin/jruby /usr/bin/jruby 21 | 22 | You will need the celerity gem installed into JRuby: 23 | 24 | jruby -S gem install celerity 25 | 26 | Now install the Culerity gem: 27 | 28 | gem install culerity --source http://gemcutter.org 29 | 30 | Assuming you have a Rails application set up already you can run the RSpec, Cucumber and Culerity generators: 31 | 32 | cd RAILS_ROOT 33 | script/generate rspec 34 | script/generate cucumber 35 | script/generate culerity 36 | 37 | This creates the features folder and a file culerity_steps.rb into your application. This file contains step definitions for basic interactions like clicking links or filling out forms. Now is a good time to get rid of any webrat specific files generated by cucumber (i.e. features/step_definitions/webrat_steps.rb). 38 | 39 | After you have written a first feature you can run it just like you would run a standard cucumber feature. The only difference is that you have to start a web server (e.g. mongrel) with the test environment enabled beforehand. 40 | 41 | rake culerity:rails:start 42 | cucumber features/my_feature.feature 43 | 44 | The Rails instance uses a special environment culerity. 45 | 46 | When you have finished running culerity/cucumber you can turn off the Rails instance: 47 | 48 | NOTE: The default port for this server is 3001. You can change this in features/step_definitions/common_celerity.rb 49 | 50 | rake culerity:rails:stop 51 | 52 | ### RVM 53 | 54 | If you are using RVM there's an [integration guide for culerity](http://rvm.beginrescueend.com/integration/culerity/) on their website. 55 | 56 | ## How does it work 57 | 58 | While Celerity is based on Java and requires JRuby to run, with Culerity you can still run your tests in your own Ruby Environment. When you run your features a separate JRuby process for Celerity is spawned and all Celerity Commands are redirected to this other process. 59 | 60 | ## Troubleshooting 61 | 62 | I get a broken pipe error: 63 | 64 | * make sure JRuby is installed and in your path: running _jruby -v_ should not produce an error 65 | 66 | I get _Connection Refused_ errors 67 | 68 | * make sure you have started a server in the test environment that runs on port 3001 69 | 70 | My application can't find the data I create in my steps 71 | 72 | * make sure you have disabled transactional fixtures in your env.rb 73 | 74 | My database is not cleared automatically between scenarios 75 | 76 | * Rails can't clean the database because you had to disable transactional fixtures - which only work if your test process is the same as your web server process. Hence you have to clean your database manually. A quick way would be: 77 | 78 | Before do 79 | [User, .... all your models].each do |model| 80 | model.delete_all 81 | end 82 | end 83 | 84 | 85 | ## Links to Celerity documentation 86 | 87 | * [How to select elements](http://celerity.rubyforge.org/yard/Celerity/Container.html) 88 | * [FAQ](http://wiki.github.com/jarib/celerity/faq) 89 | * [Tutorial](http://wiki.github.com/jarib/celerity/getting-started) 90 | * [API docs](http://celerity.rubyforge.org/yard/) 91 | 92 | ## Links 93 | 94 | * [Cucumber](http://github.com/aslakhellesoy/cucumber/wikis) 95 | * [Celerity](http://celerity.rubyforge.org) 96 | * [JRuby](http://jruby.org) 97 | * [RSpec](http://rspec.info) 98 | * [HtmlUnit](http://htmlunit.sourceforge.net/) 99 | 100 | ## Contact 101 | 102 | Written 2009 by Alexander Lang, contact alex[at]upstream-berlin.com or , released under the MIT license 103 | -------------------------------------------------------------------------------- /spec/remote_object_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe Culerity::RemoteObjectProxy do 4 | describe "block_to_string method" do 5 | it "should return block result when result is lambda string" do 6 | proxy = Culerity::RemoteObjectProxy.new nil, nil 7 | block = lambda { "lambda { true}" } 8 | proxy.send(:block_to_string, &block).should == "lambda { true}" 9 | end 10 | 11 | it "should replace newlines in lambda string with semicolons so that the server can parse it as one command" do 12 | proxy = Culerity::RemoteObjectProxy.new nil, nil 13 | block = lambda { "lambda { \ntrue\n}" } 14 | proxy.send(:block_to_string, &block).should == "lambda { ;true;}" 15 | end 16 | 17 | it "should accept do end in lambda string instead of {}" do 18 | code = <<-CODE 19 | lambda do |page, message| 20 | true 21 | end 22 | CODE 23 | 24 | proxy = Culerity::RemoteObjectProxy.new nil, nil 25 | block = lambda { code } 26 | proxy.send(:block_to_string, &block).should == "lambda do |page, message|; true; end" 27 | end 28 | 29 | it "should return lambda string when block result isn't a lambda string" do 30 | proxy = Culerity::RemoteObjectProxy.new nil, nil 31 | [true, false, "blah", 5].each do |var| 32 | block = lambda { var } 33 | proxy.send(:block_to_string, &block).should == "lambda { #{var} }" 34 | end 35 | end 36 | end 37 | 38 | it "should send the serialized method call to the output" do 39 | io = stub 'io', :gets => '[:return]' 40 | io.should_receive(:<<).with(%Q{[[345, "goto", "/homepage"]]\n}) 41 | proxy = Culerity::RemoteObjectProxy.new 345, io 42 | proxy.goto '/homepage' 43 | end 44 | 45 | it "should send inspect as a serialized method call to the output" do 46 | io = stub 'io', :gets => '[:return, "inspect output"]' 47 | io.should_receive(:<<).with(%Q{[[345, "inspect"]]\n}) 48 | proxy = Culerity::RemoteObjectProxy.new 345, io 49 | proxy.inspect.should == "inspect output" 50 | end 51 | 52 | it "should send respond_to? as a serialized method call to the output" do 53 | io = stub 'io', :gets => '[:return, true]' 54 | io.should_receive(:<<).with(%Q{[[345, "respond_to?", :xyz]]\n}) 55 | proxy = Culerity::RemoteObjectProxy.new 345, io 56 | proxy.respond_to?(:xyz).should == true 57 | end 58 | 59 | it "should send the serialized method call with a proc argument to the output" do 60 | io = stub 'io', :gets => "[:return]" 61 | io.should_receive(:<<).with(%Q{[[345, "method", true, lambda { true }]]\n}) 62 | proxy = Culerity::RemoteObjectProxy.new 345, io 63 | 64 | proxy.send_remote(:method, true, lambda{true}) 65 | end 66 | 67 | it "should send the serialized method call and a block to the output" do 68 | io = stub 'io', :gets => "[:return]" 69 | io.should_receive(:<<).with(%Q{[[345, "method"], lambda { true }]\n}) 70 | proxy = Culerity::RemoteObjectProxy.new 345, io 71 | 72 | proxy.send_remote(:method) { "lambda { true }" } 73 | end 74 | 75 | it "should return the deserialized return value" do 76 | io = stub 'io', :gets => "[:return, :okay]\n", :<< => nil 77 | proxy = Culerity::RemoteObjectProxy.new 345, io 78 | proxy.goto.should == :okay 79 | end 80 | 81 | it "should raise the received exception" do 82 | io = stub 'io', :gets => %Q{[:exception, "RuntimeError", "test exception", []]}, :<< => nil 83 | proxy = Culerity::RemoteObjectProxy.new 345, io 84 | lambda { 85 | proxy.goto '/home' 86 | }.should raise_error(Culerity::CulerityException) 87 | end 88 | 89 | it "should include the full 'local' back trace in addition to the 'remote' backtrace" do 90 | io = stub 'io', :gets => %Q{[:exception, "RuntimeError", "test exception", ["Remote", "Backtrace"]]}, :<< => nil 91 | proxy = Culerity::RemoteObjectProxy.new 345, io 92 | begin 93 | proxy.goto '/home' 94 | rescue => ex 95 | puts ex.backtrace 96 | ex.backtrace[0].should == "Remote" 97 | ex.backtrace[1].should == "Backtrace" 98 | ex.backtrace.detect {|line| line =~ /lib\/culerity\/remote_object_proxy\.rb/}.should_not be_nil 99 | ex.backtrace.detect {|line| line =~ /spec\/remote_object_proxy_spec\.rb/}.should_not be_nil 100 | end 101 | end 102 | 103 | it "should send exit" do 104 | io = stub 'io', :gets => '[:return]' 105 | io.should_receive(:<<).with('["_exit_"]') 106 | proxy = Culerity::RemoteObjectProxy.new 345, io 107 | proxy.exit 108 | end 109 | end -------------------------------------------------------------------------------- /rails_generators/culerity/templates/features/step_definitions/culerity_steps.rb: -------------------------------------------------------------------------------- 1 | require 'culerity' 2 | 3 | Before do 4 | $rails_server_pid ||= Culerity::run_rails(:environment => 'culerity', :port => 3001) 5 | $server ||= Culerity::run_server 6 | unless $browser 7 | $browser = Culerity::RemoteBrowserProxy.new $server, {:browser => :firefox3, 8 | :javascript_exceptions => true, 9 | :resynchronize => true, 10 | :status_code_exceptions => true 11 | } 12 | $browser.log_level = :warning 13 | end 14 | @host = 'http://localhost:3001' 15 | end 16 | 17 | After do 18 | $server.clear_proxies 19 | $browser.clear_cookies 20 | end 21 | 22 | at_exit do 23 | $browser.exit if $browser 24 | $server.exit_server if $server 25 | Process.kill(6, $rails_server_pid) if $rails_server_pid 26 | end 27 | 28 | Given /^(?:|I )am on (.+)$/ do |page_name| 29 | $browser.goto @host + path_to(page_name) 30 | end 31 | 32 | When /I follow "([^\"]*)"/ do |link| 33 | _link = [ 34 | [:text, /^#{Regexp.escape(link)}$/ ], 35 | [:id, link], 36 | [:title, link], 37 | [:text, /#{Regexp.escape(link)}/ ], 38 | ].map{|args| $browser.link(*args)}.find{|__link| __link.exist?} 39 | raise "link \"#{link}\" not found" unless _link 40 | _link.click 41 | assert_successful_response 42 | end 43 | 44 | When /I press "([^\"]*)"/ do |button| 45 | $browser.button(:text, button).click 46 | assert_successful_response 47 | end 48 | 49 | When /I fill in "([^\"]*)" with "([^\"]*)"/ do |field, value| 50 | find_by_label_or_id(:text_field, field).set(value) 51 | end 52 | 53 | When /I fill in "([^\"]*)" for "([^\"]*)"/ do |value, field| 54 | find_by_label_or_id(:text_field, field).set(value) 55 | end 56 | 57 | When /I check "([^\"]*)"/ do |field| 58 | find_by_label_or_id(:check_box, field).set(true) 59 | end 60 | 61 | When /^I uncheck "([^\"]*)"$/ do |field| 62 | find_by_label_or_id(:check_box, field).set(false) 63 | end 64 | 65 | When /I select "([^\"]*)" from "([^\"]*)"/ do |value, field| 66 | find_by_label_or_id(:select_list, field).select value 67 | end 68 | 69 | When /I choose "([^\"]*)"/ do |field| 70 | find_by_label_or_id(:radio, field).set(true) 71 | end 72 | 73 | When /I go to (.+)/ do |path| 74 | $browser.goto @host + path_to(path) 75 | assert_successful_response 76 | end 77 | 78 | When /^I wait for the AJAX call to finish$/ do 79 | $browser.wait_while do 80 | begin 81 | count = $browser.execute_script("window.running_ajax_calls").to_i 82 | count.to_i > 0 83 | rescue => e 84 | if e.message.include?('HtmlunitCorejsJavascript::Undefined') 85 | raise "For 'I wait for the AJAX call to finish' to work please include culerity.js after including jQuery. If you don't use jQuery please rewrite culerity.js accordingly." 86 | else 87 | raise(e) 88 | end 89 | end 90 | end 91 | end 92 | 93 | Then /^(?:|I )should be on (.+)$/ do |page_name| 94 | current_path = URI.parse($browser.url) 95 | expected_path = URI.parse(path_to(page_name)) 96 | 97 | # If our expected path doesn't specify a query-string, ignore any query string 98 | # in the current path 99 | current_path, expected_path = if expected_path.query.nil? 100 | [ current_path.path, expected_path.path ] 101 | else 102 | [ current_path.select(:path, :query).compact.join('?'), path_to(page_name) ] 103 | end 104 | 105 | if defined?(Spec::Rails::Matchers) 106 | current_path.should == path_to(page_name) 107 | else 108 | assert_equal path_to(page_name), current_path 109 | end 110 | end 111 | 112 | Then /^the "([^\"]*)" field should contain "([^\"]*)"$/ do |field, value| 113 | f = find_by_label_or_id(:text_field, field) 114 | if defined?(Spec::Rails::Matchers) 115 | f.text.should =~ /#{Regexp::escape(value)}/ 116 | else 117 | assert_match(/#{Regexp::escape(value)}/, f.text) 118 | end 119 | end 120 | 121 | Then /^the "([^\"]*)" field should not contain "([^\"]*)"$/ do |field, value| 122 | f = find_by_label_or_id(:text_field, field) 123 | if defined?(Spec::Rails::Matchers) 124 | f.text.should_not =~ /#{Regexp::escape(value)}/ 125 | else 126 | assert_no_match(/#{Regexp::escape(value)}/, f.text) 127 | end 128 | end 129 | 130 | Then /^the "([^\"]*)" checkbox should be checked$/ do |label| 131 | f = find_by_label_or_id(:check_box, label) 132 | if defined?(Spec::Rails::Matchers) 133 | f.should be_checked 134 | else 135 | assert f.checked? 136 | end 137 | end 138 | 139 | Then /^the "([^\"]*)" checkbox should not be checked$/ do |label| 140 | f = find_by_label_or_id(:check_box, label) 141 | if defined?(Spec::Rails::Matchers) 142 | f.should_not be_checked 143 | else 144 | assert !f.checked? 145 | end 146 | end 147 | 148 | Then /I should see "([^\"]*)"/ do |text| 149 | $browser.text.include?(text).should be_true 150 | end 151 | 152 | Then /I should not see "([^\"]*)"/ do |text| 153 | $browser.text.include?(text).should_not be_true 154 | end 155 | 156 | def find_by_label_or_id(element, attribute) 157 | matchers = [[attribute, :id], [attribute, :name]] 158 | matchers << [$browser.label(:text, attribute).for, :id] if $browser.label(:text, attribute).exist? 159 | field = matchers.map{|_field, matcher| $browser.send(element, matcher, _field)}.find(&:exist?) || raise("#{element} not found using \"#{attribute}\"") 160 | end 161 | 162 | def assert_successful_response 163 | status = $browser.page.web_response.status_code 164 | if(status == 302 || status == 301) 165 | location = $browser.page.web_response.get_response_header_value('Location') 166 | puts "Being redirected to #{location}" 167 | $browser.goto location 168 | assert_successful_response 169 | elsif status != 200 170 | filename = "culerity-#{Time.now.to_i}.html" 171 | File.open(RAILS_ROOT + "/tmp/#{filename}", "w") do |f| 172 | f.write $browser.html 173 | end 174 | `open tmp/#{filename}` 175 | raise "Browser returned Response Code #{$browser.page.web_response.status_code}" 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /features/step_definitions/common_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^this project is active project folder/ do 2 | @active_project_folder = File.expand_path(File.dirname(__FILE__) + "/../..") 3 | end 4 | 5 | Given /^env variable \$([\w_]+) set to "(.*)"/ do |env_var, value| 6 | ENV[env_var] = value 7 | end 8 | 9 | Given /I delete (folder|file) "([^\"]*)"/ do |type, folder| 10 | in_project_folder { FileUtils.rm_rf folder } 11 | end 12 | 13 | When /^I invoke "(.*)" generator with arguments "(.*)"$/ do |generator, arguments| 14 | @stdout = StringIO.new 15 | in_project_folder do 16 | if Object.const_defined?("APP_ROOT") 17 | APP_ROOT.replace(FileUtils.pwd) 18 | else 19 | APP_ROOT = FileUtils.pwd 20 | end 21 | run_generator(generator, arguments.split(' '), SOURCES, :stdout => @stdout) 22 | end 23 | File.open(File.join(@tmp_root, "generator.out"), "w") do |f| 24 | @stdout.rewind 25 | f << @stdout.read 26 | end 27 | end 28 | 29 | When /^I run executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 30 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 31 | in_project_folder do 32 | system "#{executable} #{arguments} > #{@stdout} 2> #{@stdout}" 33 | end 34 | end 35 | 36 | When /^I run project executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 37 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 38 | in_project_folder do 39 | system "ruby #{executable} #{arguments} > #{@stdout} 2> #{@stdout}" 40 | end 41 | end 42 | 43 | When /^I run local executable "(.*)" with arguments "(.*)"/ do |executable, arguments| 44 | @stdout = File.expand_path(File.join(@tmp_root, "executable.out")) 45 | executable = File.expand_path(File.join(File.dirname(__FILE__), "/../../bin", executable)) 46 | in_project_folder do 47 | system "ruby #{executable} #{arguments} > #{@stdout} 2> #{@stdout}" 48 | end 49 | end 50 | 51 | When /^I invoke task "rake (.*)"/ do |task| 52 | @stdout = File.expand_path(File.join(@tmp_root, "rake.out")) 53 | @stderr = File.expand_path(File.join(@tmp_root, "rake.err")) 54 | in_project_folder do 55 | system "rake #{task} --trace > #{@stdout} 2> #{@stderr}" 56 | end 57 | File.read(@stderr).should_not =~ /rake aborted!/ 58 | end 59 | 60 | Then /^folder "(.*)" (is|is not) created/ do |folder, is| 61 | in_project_folder do 62 | File.exists?(folder).should(is == 'is' ? be_true : be_false) 63 | end 64 | end 65 | 66 | Then /^file "(.*)" (is|is not) created/ do |file, is| 67 | in_project_folder do 68 | File.exists?(file).should(is == 'is' ? be_true : be_false) 69 | end 70 | end 71 | 72 | Then /^file with name matching "(.*)" is created/ do |pattern| 73 | in_project_folder do 74 | Dir[pattern].should_not be_empty 75 | end 76 | end 77 | 78 | Then /^file "(.*)" contents (does|does not) match \/(.*)\// do |file, does, regex| 79 | in_project_folder do 80 | actual_output = File.read(file) 81 | (does == 'does') ? 82 | actual_output.should(match(/#{regex}/)) : 83 | actual_output.should_not(match(/#{regex}/)) 84 | end 85 | end 86 | 87 | Then /gem file "(.*)" and generated file "(.*)" should be the same/ do |gem_file, project_file| 88 | File.exists?(gem_file).should be_true 89 | File.exists?(project_file).should be_true 90 | gem_file_contents = File.read(File.dirname(__FILE__) + "/../../#{gem_file}") 91 | project_file_contents = File.read(File.join(@active_project_folder, project_file)) 92 | project_file_contents.should == gem_file_contents 93 | end 94 | 95 | Then /^(does|does not) invoke generator "(.*)"$/ do |does_invoke, generator| 96 | actual_output = File.read(@stdout) 97 | does_invoke == "does" ? 98 | actual_output.should(match(/dependency\s+#{generator}/)) : 99 | actual_output.should_not(match(/dependency\s+#{generator}/)) 100 | end 101 | 102 | Then /help options "(.*)" and "(.*)" are displayed/ do |opt1, opt2| 103 | actual_output = File.read(@stdout) 104 | actual_output.should match(/#{opt1}/) 105 | actual_output.should match(/#{opt2}/) 106 | end 107 | 108 | Then /^I should see "([^\"]*)"$/ do |text| 109 | actual_output = File.read(@stdout) 110 | actual_output.should contain(text) 111 | end 112 | 113 | Then /^I should not see "([^\"]*)"$/ do |text| 114 | actual_output = File.read(@stdout) 115 | actual_output.should_not contain(text) 116 | end 117 | 118 | Then /^I should see$/ do |text| 119 | actual_output = File.read(@stdout) 120 | actual_output.should contain(text) 121 | end 122 | 123 | Then /^I should not see$/ do |text| 124 | actual_output = File.read(@stdout) 125 | actual_output.should_not contain(text) 126 | end 127 | 128 | Then /^I should see exactly$/ do |text| 129 | actual_output = File.read(@stdout) 130 | actual_output.should == text 131 | end 132 | 133 | Then /^I should see all (\d+) tests pass/ do |expected_test_count| 134 | expected = %r{^#{expected_test_count} tests, \d+ assertions, 0 failures, 0 errors} 135 | actual_output = File.read(@stdout) 136 | actual_output.should match(expected) 137 | end 138 | 139 | Then /^I should see all (\d+) examples pass/ do |expected_test_count| 140 | expected = %r{^#{expected_test_count} examples?, 0 failures} 141 | actual_output = File.read(@stdout) 142 | actual_output.should match(expected) 143 | end 144 | 145 | Then /^yaml file "(.*)" contains (\{.*\})/ do |file, yaml| 146 | in_project_folder do 147 | yaml = eval yaml 148 | YAML.load(File.read(file)).should == yaml 149 | end 150 | end 151 | 152 | Then /^Rakefile can display tasks successfully/ do 153 | @stdout = File.expand_path(File.join(@tmp_root, "rakefile.out")) 154 | in_project_folder do 155 | system "rake -T > #{@stdout} 2> #{@stdout}" 156 | end 157 | actual_output = File.read(@stdout) 158 | actual_output.should match(/^rake\s+\w+\s+#\s.*/) 159 | end 160 | 161 | Then /^task "rake (.*)" is executed successfully/ do |task| 162 | @stdout.should_not be_nil 163 | actual_output = File.read(@stdout) 164 | actual_output.should_not match(/^Don't know how to build task '#{task}'/) 165 | actual_output.should_not match(/Error/i) 166 | end 167 | 168 | Then /^gem spec key "(.*)" contains \/(.*)\// do |key, regex| 169 | in_project_folder do 170 | gem_file = Dir["pkg/*.gem"].first 171 | gem_spec = Gem::Specification.from_yaml(`gem spec #{gem_file}`) 172 | spec_value = gem_spec.send(key.to_sym) 173 | spec_value.to_s.should match(/#{regex}/) 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/celerity_server_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/spec_helper' 2 | 3 | describe Culerity::CelerityServer do 4 | before(:each) do 5 | @browser = stub 'browser' 6 | Celerity::Browser.stub!(:new).and_return(@browser) 7 | end 8 | 9 | it "should pass the method call to the celerity browser" do 10 | @browser.should_receive(:goto).with('/homepage') 11 | _in = stub 'in' 12 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 13 | _out = stub 'out', :<< => nil 14 | Culerity::CelerityServer.new(_in, _out) 15 | end 16 | 17 | it "should pass procs" do 18 | @browser.should_receive(:remove_listener).with(:confirm, instance_of(Proc)) 19 | _in = stub 'in' 20 | _in.stub!(:gets).and_return("[[\"browser0\", \"remove_listener\", :confirm, lambda{true}]]\n", "[\"_exit_\"]\n") 21 | _out = stub 'out', :<< => nil 22 | Culerity::CelerityServer.new(_in, _out) 23 | end 24 | 25 | it "should pass blocks" do 26 | def @browser.add_listener(type, &block) 27 | @type = type 28 | @block = block 29 | end 30 | _in = stub 'in' 31 | _in.stub!(:gets).and_return("[[\"browser0\", \"add_listener\", :confirm], lambda{true}]\n", "[\"_exit_\"]\n") 32 | _out = stub 'out', :<< => nil 33 | Culerity::CelerityServer.new(_in, _out) 34 | @browser.instance_variable_get(:@type).should == :confirm 35 | @browser.instance_variable_get(:@block).call.should == true 36 | end 37 | 38 | it "should return the browser id when a new browser is requested" do 39 | _in = stub 'in' 40 | _in.stub!(:gets).and_return("[[\"celerity\", \"new_browser\", {}]]\n", "[\"_exit_\"]\n") 41 | _out = stub 'out' 42 | _out.should_receive(:<<).with("[:return, \"browser0\"]\n") 43 | Culerity::CelerityServer.new(_in, _out) 44 | end 45 | 46 | it "should create a new browser with the provided options" do 47 | _in = stub 'in' 48 | _in.stub!(:gets).and_return("[[\"celerity\", \"new_browser\", {:browser => :firefox}]]\n", "[\"_exit_\"]\n") 49 | Celerity::Browser.should_receive(:new).with(:browser => :firefox) 50 | Culerity::CelerityServer.new(_in, stub.as_null_object) 51 | end 52 | 53 | it "should create multiple browsers and return the appropriate id for each" do 54 | _in = stub 'in' 55 | _in.stub!(:gets).and_return("[[\"celerity\", \"new_browser\", {}]]\n", "[[\"celerity\", \"new_browser\", {}]]\n", "[\"_exit_\"]\n") 56 | Celerity::Browser.should_receive(:new).twice 57 | _out = stub 'out' 58 | _out.should_receive(:<<).with("[:return, \"browser0\"]\n").ordered 59 | _out.should_receive(:<<).with("[:return, \"browser1\"]\n").ordered 60 | Culerity::CelerityServer.new(_in, _out) 61 | 62 | end 63 | 64 | 65 | it "should send back the return value of the call" do 66 | @browser.stub!(:goto).and_return(true) 67 | _in = stub 'in' 68 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 69 | _out = stub 'out' 70 | _out.should_receive(:<<).with("[:return, true]\n") 71 | Culerity::CelerityServer.new(_in, _out) 72 | end 73 | 74 | it "should ignore empty inputs" do 75 | _in = stub 'in' 76 | _in.stub!(:gets).and_return("\n", "[\"_exit_\"]\n") 77 | _out = stub 'out' 78 | _out.should_not_receive(:<<) 79 | Culerity::CelerityServer.new(_in, _out) 80 | end 81 | 82 | it "should send back a symbol" do 83 | @browser.stub!(:goto).and_return(:ok) 84 | _in = stub 'in' 85 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\"]]\n", "[\"_exit_\"]\n") 86 | _out = stub 'out' 87 | _out.should_receive(:<<).with("[:return, :ok]\n") 88 | Culerity::CelerityServer.new(_in, _out) 89 | end 90 | 91 | it "should send back a proxy if the return value is not a string, number, nil, symbol or boolean" do 92 | @browser.stub!(:goto).and_return(stub('123', :object_id => 456)) 93 | _in = stub 'in' 94 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 95 | _out = stub 'out' 96 | _out.should_receive(:<<).with("[:return, Culerity::RemoteObjectProxy.new(456, @io)]\n") 97 | Culerity::CelerityServer.new(_in, _out) 98 | end 99 | 100 | it "should send back arrays" do 101 | @browser.stub!(:goto).and_return([true, false, "test", 1, 12.3, nil, stub('123', :object_id => 456), ["test2", 32.1, 5000, nil, false, true, stub('789', :object_id => 101)]]) 102 | _in = stub 'in' 103 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 104 | _out = stub 'out' 105 | _out.should_receive(:<<).with("[:return, [true, false, \"test\", 1, 12.3, nil, Culerity::RemoteObjectProxy.new(456, @io), [\"test2\", 32.1, 5000, nil, false, true, Culerity::RemoteObjectProxy.new(101, @io)]]]\n") 106 | Culerity::CelerityServer.new(_in, _out) 107 | end 108 | 109 | it "should pass the method call to a proxy" do 110 | proxy = stub('123', :object_id => 456) 111 | @browser.stub!(:goto).and_return(proxy) 112 | _in = stub 'in' 113 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[[456, \"goto_2\", \"1\"]]", "[\"_exit_\"]\n") 114 | _out = stub 'out', :<< => nil 115 | proxy.should_receive(:goto_2).with('1') 116 | Culerity::CelerityServer.new(_in, _out) 117 | end 118 | 119 | it "should pass multiple method calls" do 120 | @browser.should_receive(:goto).with('/homepage') 121 | @browser.should_receive(:goto).with('/page2') 122 | _in = stub 'in' 123 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[[\"browser0\", \"goto\", \"/page2\"]]\n", "[\"_exit_\"]\n") 124 | _out = stub 'out', :<< => nil 125 | Culerity::CelerityServer.new(_in, _out) 126 | end 127 | 128 | it "should return an exception" do 129 | @browser.stub!(:goto).and_raise(RuntimeError.new('test exception with "quotes"')) 130 | _in = stub 'in' 131 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 132 | _out = stub 'out' 133 | _out.should_receive(:<<).with(/^\[:exception, \"RuntimeError\", \"test exception with \\\"quotes\\\"\", \[.*\]\]\n$/) 134 | Culerity::CelerityServer.new(_in, _out) 135 | end 136 | 137 | it "should extract a js stack trace if available and prepend it on the regular backtrace" do 138 | exception = RuntimeError.new("the exception") 139 | exception.stub!(:cause => stub('ex2', :cause => stub('ex3', :getScriptStackTrace => "The\nStack\nTrace"))) 140 | 141 | @browser.stub!(:goto).and_raise(exception) 142 | _in = stub 'in' 143 | _in.stub!(:gets).and_return("[[\"browser0\", \"goto\", \"/homepage\"]]\n", "[\"_exit_\"]\n") 144 | _out = stub 'out' 145 | _out.should_receive(:<<).with(/^\[:exception, \"RuntimeError\", \"the exception\", \[\"The\", \"Stack\", \"Trace\", \".*\"\]\]\n$/) 146 | Culerity::CelerityServer.new(_in, _out) 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /features/fixtures/jquery: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery JavaScript Library v1.3.2 3 | * http://jquery.com/ 4 | * 5 | * Copyright (c) 2009 John Resig 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://docs.jquery.com/License 8 | * 9 | * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) 10 | * Revision: 6246 11 | */ 12 | (function(){ 13 | 14 | var 15 | // Will speed up references to window, and allows munging its name. 16 | window = this, 17 | // Will speed up references to undefined, and allows munging its name. 18 | undefined, 19 | // Map over jQuery in case of overwrite 20 | _jQuery = window.jQuery, 21 | // Map over the $ in case of overwrite 22 | _$ = window.$, 23 | 24 | jQuery = window.jQuery = window.$ = function( selector, context ) { 25 | // The jQuery object is actually just the init constructor 'enhanced' 26 | return new jQuery.fn.init( selector, context ); 27 | }, 28 | 29 | // A simple way to check for HTML strings or ID strings 30 | // (both of which we optimize for) 31 | quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, 32 | // Is it a simple selector 33 | isSimple = /^.[^:#\[\.,]*$/; 34 | 35 | jQuery.fn = jQuery.prototype = { 36 | init: function( selector, context ) { 37 | // Make sure that a selection was provided 38 | selector = selector || document; 39 | 40 | // Handle $(DOMElement) 41 | if ( selector.nodeType ) { 42 | this[0] = selector; 43 | this.length = 1; 44 | this.context = selector; 45 | return this; 46 | } 47 | // Handle HTML strings 48 | if ( typeof selector === "string" ) { 49 | // Are we dealing with HTML string or an ID? 50 | var match = quickExpr.exec( selector ); 51 | 52 | // Verify a match, and that no context was specified for #id 53 | if ( match && (match[1] || !context) ) { 54 | 55 | // HANDLE: $(html) -> $(array) 56 | if ( match[1] ) 57 | selector = jQuery.clean( [ match[1] ], context ); 58 | 59 | // HANDLE: $("#id") 60 | else { 61 | var elem = document.getElementById( match[3] ); 62 | 63 | // Handle the case where IE and Opera return items 64 | // by name instead of ID 65 | if ( elem && elem.id != match[3] ) 66 | return jQuery().find( selector ); 67 | 68 | // Otherwise, we inject the element directly into the jQuery object 69 | var ret = jQuery( elem || [] ); 70 | ret.context = document; 71 | ret.selector = selector; 72 | return ret; 73 | } 74 | 75 | // HANDLE: $(expr, [context]) 76 | // (which is just equivalent to: $(content).find(expr) 77 | } else 78 | return jQuery( context ).find( selector ); 79 | 80 | // HANDLE: $(function) 81 | // Shortcut for document ready 82 | } else if ( jQuery.isFunction( selector ) ) 83 | return jQuery( document ).ready( selector ); 84 | 85 | // Make sure that old selector state is passed along 86 | if ( selector.selector && selector.context ) { 87 | this.selector = selector.selector; 88 | this.context = selector.context; 89 | } 90 | 91 | return this.setArray(jQuery.isArray( selector ) ? 92 | selector : 93 | jQuery.makeArray(selector)); 94 | }, 95 | 96 | // Start with an empty selector 97 | selector: "", 98 | 99 | // The current version of jQuery being used 100 | jquery: "1.3.2", 101 | 102 | // The number of elements contained in the matched element set 103 | size: function() { 104 | return this.length; 105 | }, 106 | 107 | // Get the Nth element in the matched element set OR 108 | // Get the whole matched element set as a clean array 109 | get: function( num ) { 110 | return num === undefined ? 111 | 112 | // Return a 'clean' array 113 | Array.prototype.slice.call( this ) : 114 | 115 | // Return just the object 116 | this[ num ]; 117 | }, 118 | 119 | // Take an array of elements and push it onto the stack 120 | // (returning the new matched element set) 121 | pushStack: function( elems, name, selector ) { 122 | // Build a new jQuery matched element set 123 | var ret = jQuery( elems ); 124 | 125 | // Add the old object onto the stack (as a reference) 126 | ret.prevObject = this; 127 | 128 | ret.context = this.context; 129 | 130 | if ( name === "find" ) 131 | ret.selector = this.selector + (this.selector ? " " : "") + selector; 132 | else if ( name ) 133 | ret.selector = this.selector + "." + name + "(" + selector + ")"; 134 | 135 | // Return the newly-formed element set 136 | return ret; 137 | }, 138 | 139 | // Force the current matched set of elements to become 140 | // the specified array of elements (destroying the stack in the process) 141 | // You should use pushStack() in order to do this, but maintain the stack 142 | setArray: function( elems ) { 143 | // Resetting the length to 0, then using the native Array push 144 | // is a super-fast way to populate an object with array-like properties 145 | this.length = 0; 146 | Array.prototype.push.apply( this, elems ); 147 | 148 | return this; 149 | }, 150 | 151 | // Execute a callback for every element in the matched set. 152 | // (You can seed the arguments with an array of args, but this is 153 | // only used internally.) 154 | each: function( callback, args ) { 155 | return jQuery.each( this, callback, args ); 156 | }, 157 | 158 | // Determine the position of an element within 159 | // the matched set of elements 160 | index: function( elem ) { 161 | // Locate the position of the desired element 162 | return jQuery.inArray( 163 | // If it receives a jQuery object, the first element is used 164 | elem && elem.jquery ? elem[0] : elem 165 | , this ); 166 | }, 167 | 168 | attr: function( name, value, type ) { 169 | var options = name; 170 | 171 | // Look for the case where we're accessing a style value 172 | if ( typeof name === "string" ) 173 | if ( value === undefined ) 174 | return this[0] && jQuery[ type || "attr" ]( this[0], name ); 175 | 176 | else { 177 | options = {}; 178 | options[ name ] = value; 179 | } 180 | 181 | // Check to see if we're setting style values 182 | return this.each(function(i){ 183 | // Set all the styles 184 | for ( name in options ) 185 | jQuery.attr( 186 | type ? 187 | this.style : 188 | this, 189 | name, jQuery.prop( this, options[ name ], type, i, name ) 190 | ); 191 | }); 192 | }, 193 | 194 | css: function( key, value ) { 195 | // ignore negative width and height values 196 | if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) 197 | value = undefined; 198 | return this.attr( key, value, "curCSS" ); 199 | }, 200 | 201 | text: function( text ) { 202 | if ( typeof text !== "object" && text != null ) 203 | return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); 204 | 205 | var ret = ""; 206 | 207 | jQuery.each( text || this, function(){ 208 | jQuery.each( this.childNodes, function(){ 209 | if ( this.nodeType != 8 ) 210 | ret += this.nodeType != 1 ? 211 | this.nodeValue : 212 | jQuery.fn.text( [ this ] ); 213 | }); 214 | }); 215 | 216 | return ret; 217 | }, 218 | 219 | wrapAll: function( html ) { 220 | if ( this[0] ) { 221 | // The elements to wrap the target around 222 | var wrap = jQuery( html, this[0].ownerDocument ).clone(); 223 | 224 | if ( this[0].parentNode ) 225 | wrap.insertBefore( this[0] ); 226 | 227 | wrap.map(function(){ 228 | var elem = this; 229 | 230 | while ( elem.firstChild ) 231 | elem = elem.firstChild; 232 | 233 | return elem; 234 | }).append(this); 235 | } 236 | 237 | return this; 238 | }, 239 | 240 | wrapInner: function( html ) { 241 | return this.each(function(){ 242 | jQuery( this ).contents().wrapAll( html ); 243 | }); 244 | }, 245 | 246 | wrap: function( html ) { 247 | return this.each(function(){ 248 | jQuery( this ).wrapAll( html ); 249 | }); 250 | }, 251 | 252 | append: function() { 253 | return this.domManip(arguments, true, function(elem){ 254 | if (this.nodeType == 1) 255 | this.appendChild( elem ); 256 | }); 257 | }, 258 | 259 | prepend: function() { 260 | return this.domManip(arguments, true, function(elem){ 261 | if (this.nodeType == 1) 262 | this.insertBefore( elem, this.firstChild ); 263 | }); 264 | }, 265 | 266 | before: function() { 267 | return this.domManip(arguments, false, function(elem){ 268 | this.parentNode.insertBefore( elem, this ); 269 | }); 270 | }, 271 | 272 | after: function() { 273 | return this.domManip(arguments, false, function(elem){ 274 | this.parentNode.insertBefore( elem, this.nextSibling ); 275 | }); 276 | }, 277 | 278 | end: function() { 279 | return this.prevObject || jQuery( [] ); 280 | }, 281 | 282 | // For internal use only. 283 | // Behaves like an Array's method, not like a jQuery method. 284 | push: [].push, 285 | sort: [].sort, 286 | splice: [].splice, 287 | 288 | find: function( selector ) { 289 | if ( this.length === 1 ) { 290 | var ret = this.pushStack( [], "find", selector ); 291 | ret.length = 0; 292 | jQuery.find( selector, this[0], ret ); 293 | return ret; 294 | } else { 295 | return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ 296 | return jQuery.find( selector, elem ); 297 | })), "find", selector ); 298 | } 299 | }, 300 | 301 | clone: function( events ) { 302 | // Do the clone 303 | var ret = this.map(function(){ 304 | if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { 305 | // IE copies events bound via attachEvent when 306 | // using cloneNode. Calling detachEvent on the 307 | // clone will also remove the events from the orignal 308 | // In order to get around this, we use innerHTML. 309 | // Unfortunately, this means some modifications to 310 | // attributes in IE that are actually only stored 311 | // as properties will not be copied (such as the 312 | // the name attribute on an input). 313 | var html = this.outerHTML; 314 | if ( !html ) { 315 | var div = this.ownerDocument.createElement("div"); 316 | div.appendChild( this.cloneNode(true) ); 317 | html = div.innerHTML; 318 | } 319 | 320 | return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; 321 | } else 322 | return this.cloneNode(true); 323 | }); 324 | 325 | // Copy the events from the original to the clone 326 | if ( events === true ) { 327 | var orig = this.find("*").andSelf(), i = 0; 328 | 329 | ret.find("*").andSelf().each(function(){ 330 | if ( this.nodeName !== orig[i].nodeName ) 331 | return; 332 | 333 | var events = jQuery.data( orig[i], "events" ); 334 | 335 | for ( var type in events ) { 336 | for ( var handler in events[ type ] ) { 337 | jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); 338 | } 339 | } 340 | 341 | i++; 342 | }); 343 | } 344 | 345 | // Return the cloned set 346 | return ret; 347 | }, 348 | 349 | filter: function( selector ) { 350 | return this.pushStack( 351 | jQuery.isFunction( selector ) && 352 | jQuery.grep(this, function(elem, i){ 353 | return selector.call( elem, i ); 354 | }) || 355 | 356 | jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ 357 | return elem.nodeType === 1; 358 | }) ), "filter", selector ); 359 | }, 360 | 361 | closest: function( selector ) { 362 | var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, 363 | closer = 0; 364 | 365 | return this.map(function(){ 366 | var cur = this; 367 | while ( cur && cur.ownerDocument ) { 368 | if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { 369 | jQuery.data(cur, "closest", closer); 370 | return cur; 371 | } 372 | cur = cur.parentNode; 373 | closer++; 374 | } 375 | }); 376 | }, 377 | 378 | not: function( selector ) { 379 | if ( typeof selector === "string" ) 380 | // test special case where just one selector is passed in 381 | if ( isSimple.test( selector ) ) 382 | return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); 383 | else 384 | selector = jQuery.multiFilter( selector, this ); 385 | 386 | var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; 387 | return this.filter(function() { 388 | return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; 389 | }); 390 | }, 391 | 392 | add: function( selector ) { 393 | return this.pushStack( jQuery.unique( jQuery.merge( 394 | this.get(), 395 | typeof selector === "string" ? 396 | jQuery( selector ) : 397 | jQuery.makeArray( selector ) 398 | ))); 399 | }, 400 | 401 | is: function( selector ) { 402 | return !!selector && jQuery.multiFilter( selector, this ).length > 0; 403 | }, 404 | 405 | hasClass: function( selector ) { 406 | return !!selector && this.is( "." + selector ); 407 | }, 408 | 409 | val: function( value ) { 410 | if ( value === undefined ) { 411 | var elem = this[0]; 412 | 413 | if ( elem ) { 414 | if( jQuery.nodeName( elem, 'option' ) ) 415 | return (elem.attributes.value || {}).specified ? elem.value : elem.text; 416 | 417 | // We need to handle select boxes special 418 | if ( jQuery.nodeName( elem, "select" ) ) { 419 | var index = elem.selectedIndex, 420 | values = [], 421 | options = elem.options, 422 | one = elem.type == "select-one"; 423 | 424 | // Nothing was selected 425 | if ( index < 0 ) 426 | return null; 427 | 428 | // Loop through all the selected options 429 | for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { 430 | var option = options[ i ]; 431 | 432 | if ( option.selected ) { 433 | // Get the specifc value for the option 434 | value = jQuery(option).val(); 435 | 436 | // We don't need an array for one selects 437 | if ( one ) 438 | return value; 439 | 440 | // Multi-Selects return an array 441 | values.push( value ); 442 | } 443 | } 444 | 445 | return values; 446 | } 447 | 448 | // Everything else, we just grab the value 449 | return (elem.value || "").replace(/\r/g, ""); 450 | 451 | } 452 | 453 | return undefined; 454 | } 455 | 456 | if ( typeof value === "number" ) 457 | value += ''; 458 | 459 | return this.each(function(){ 460 | if ( this.nodeType != 1 ) 461 | return; 462 | 463 | if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) 464 | this.checked = (jQuery.inArray(this.value, value) >= 0 || 465 | jQuery.inArray(this.name, value) >= 0); 466 | 467 | else if ( jQuery.nodeName( this, "select" ) ) { 468 | var values = jQuery.makeArray(value); 469 | 470 | jQuery( "option", this ).each(function(){ 471 | this.selected = (jQuery.inArray( this.value, values ) >= 0 || 472 | jQuery.inArray( this.text, values ) >= 0); 473 | }); 474 | 475 | if ( !values.length ) 476 | this.selectedIndex = -1; 477 | 478 | } else 479 | this.value = value; 480 | }); 481 | }, 482 | 483 | html: function( value ) { 484 | return value === undefined ? 485 | (this[0] ? 486 | this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : 487 | null) : 488 | this.empty().append( value ); 489 | }, 490 | 491 | replaceWith: function( value ) { 492 | return this.after( value ).remove(); 493 | }, 494 | 495 | eq: function( i ) { 496 | return this.slice( i, +i + 1 ); 497 | }, 498 | 499 | slice: function() { 500 | return this.pushStack( Array.prototype.slice.apply( this, arguments ), 501 | "slice", Array.prototype.slice.call(arguments).join(",") ); 502 | }, 503 | 504 | map: function( callback ) { 505 | return this.pushStack( jQuery.map(this, function(elem, i){ 506 | return callback.call( elem, i, elem ); 507 | })); 508 | }, 509 | 510 | andSelf: function() { 511 | return this.add( this.prevObject ); 512 | }, 513 | 514 | domManip: function( args, table, callback ) { 515 | if ( this[0] ) { 516 | var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), 517 | scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), 518 | first = fragment.firstChild; 519 | 520 | if ( first ) 521 | for ( var i = 0, l = this.length; i < l; i++ ) 522 | callback.call( root(this[i], first), this.length > 1 || i > 0 ? 523 | fragment.cloneNode(true) : fragment ); 524 | 525 | if ( scripts ) 526 | jQuery.each( scripts, evalScript ); 527 | } 528 | 529 | return this; 530 | 531 | function root( elem, cur ) { 532 | return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? 533 | (elem.getElementsByTagName("tbody")[0] || 534 | elem.appendChild(elem.ownerDocument.createElement("tbody"))) : 535 | elem; 536 | } 537 | } 538 | }; 539 | 540 | // Give the init function the jQuery prototype for later instantiation 541 | jQuery.fn.init.prototype = jQuery.fn; 542 | 543 | function evalScript( i, elem ) { 544 | if ( elem.src ) 545 | jQuery.ajax({ 546 | url: elem.src, 547 | async: false, 548 | dataType: "script" 549 | }); 550 | 551 | else 552 | jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); 553 | 554 | if ( elem.parentNode ) 555 | elem.parentNode.removeChild( elem ); 556 | } 557 | 558 | function now(){ 559 | return +new Date; 560 | } 561 | 562 | jQuery.extend = jQuery.fn.extend = function() { 563 | // copy reference to target object 564 | var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; 565 | 566 | // Handle a deep copy situation 567 | if ( typeof target === "boolean" ) { 568 | deep = target; 569 | target = arguments[1] || {}; 570 | // skip the boolean and the target 571 | i = 2; 572 | } 573 | 574 | // Handle case when target is a string or something (possible in deep copy) 575 | if ( typeof target !== "object" && !jQuery.isFunction(target) ) 576 | target = {}; 577 | 578 | // extend jQuery itself if only one argument is passed 579 | if ( length == i ) { 580 | target = this; 581 | --i; 582 | } 583 | 584 | for ( ; i < length; i++ ) 585 | // Only deal with non-null/undefined values 586 | if ( (options = arguments[ i ]) != null ) 587 | // Extend the base object 588 | for ( var name in options ) { 589 | var src = target[ name ], copy = options[ name ]; 590 | 591 | // Prevent never-ending loop 592 | if ( target === copy ) 593 | continue; 594 | 595 | // Recurse if we're merging object values 596 | if ( deep && copy && typeof copy === "object" && !copy.nodeType ) 597 | target[ name ] = jQuery.extend( deep, 598 | // Never move original objects, clone them 599 | src || ( copy.length != null ? [ ] : { } ) 600 | , copy ); 601 | 602 | // Don't bring in undefined values 603 | else if ( copy !== undefined ) 604 | target[ name ] = copy; 605 | 606 | } 607 | 608 | // Return the modified object 609 | return target; 610 | }; 611 | 612 | // exclude the following css properties to add px 613 | var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, 614 | // cache defaultView 615 | defaultView = document.defaultView || {}, 616 | toString = Object.prototype.toString; 617 | 618 | jQuery.extend({ 619 | noConflict: function( deep ) { 620 | window.$ = _$; 621 | 622 | if ( deep ) 623 | window.jQuery = _jQuery; 624 | 625 | return jQuery; 626 | }, 627 | 628 | // See test/unit/core.js for details concerning isFunction. 629 | // Since version 1.3, DOM methods and functions like alert 630 | // aren't supported. They return false on IE (#2968). 631 | isFunction: function( obj ) { 632 | return toString.call(obj) === "[object Function]"; 633 | }, 634 | 635 | isArray: function( obj ) { 636 | return toString.call(obj) === "[object Array]"; 637 | }, 638 | 639 | // check if an element is in a (or is an) XML document 640 | isXMLDoc: function( elem ) { 641 | return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || 642 | !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); 643 | }, 644 | 645 | // Evalulates a script in a global context 646 | globalEval: function( data ) { 647 | if ( data && /\S/.test(data) ) { 648 | // Inspired by code by Andrea Giammarchi 649 | // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html 650 | var head = document.getElementsByTagName("head")[0] || document.documentElement, 651 | script = document.createElement("script"); 652 | 653 | script.type = "text/javascript"; 654 | if ( jQuery.support.scriptEval ) 655 | script.appendChild( document.createTextNode( data ) ); 656 | else 657 | script.text = data; 658 | 659 | // Use insertBefore instead of appendChild to circumvent an IE6 bug. 660 | // This arises when a base node is used (#2709). 661 | head.insertBefore( script, head.firstChild ); 662 | head.removeChild( script ); 663 | } 664 | }, 665 | 666 | nodeName: function( elem, name ) { 667 | return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); 668 | }, 669 | 670 | // args is for internal usage only 671 | each: function( object, callback, args ) { 672 | var name, i = 0, length = object.length; 673 | 674 | if ( args ) { 675 | if ( length === undefined ) { 676 | for ( name in object ) 677 | if ( callback.apply( object[ name ], args ) === false ) 678 | break; 679 | } else 680 | for ( ; i < length; ) 681 | if ( callback.apply( object[ i++ ], args ) === false ) 682 | break; 683 | 684 | // A special, fast, case for the most common use of each 685 | } else { 686 | if ( length === undefined ) { 687 | for ( name in object ) 688 | if ( callback.call( object[ name ], name, object[ name ] ) === false ) 689 | break; 690 | } else 691 | for ( var value = object[0]; 692 | i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} 693 | } 694 | 695 | return object; 696 | }, 697 | 698 | prop: function( elem, value, type, i, name ) { 699 | // Handle executable functions 700 | if ( jQuery.isFunction( value ) ) 701 | value = value.call( elem, i ); 702 | 703 | // Handle passing in a number to a CSS property 704 | return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? 705 | value + "px" : 706 | value; 707 | }, 708 | 709 | className: { 710 | // internal only, use addClass("class") 711 | add: function( elem, classNames ) { 712 | jQuery.each((classNames || "").split(/\s+/), function(i, className){ 713 | if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) 714 | elem.className += (elem.className ? " " : "") + className; 715 | }); 716 | }, 717 | 718 | // internal only, use removeClass("class") 719 | remove: function( elem, classNames ) { 720 | if (elem.nodeType == 1) 721 | elem.className = classNames !== undefined ? 722 | jQuery.grep(elem.className.split(/\s+/), function(className){ 723 | return !jQuery.className.has( classNames, className ); 724 | }).join(" ") : 725 | ""; 726 | }, 727 | 728 | // internal only, use hasClass("class") 729 | has: function( elem, className ) { 730 | return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; 731 | } 732 | }, 733 | 734 | // A method for quickly swapping in/out CSS properties to get correct calculations 735 | swap: function( elem, options, callback ) { 736 | var old = {}; 737 | // Remember the old values, and insert the new ones 738 | for ( var name in options ) { 739 | old[ name ] = elem.style[ name ]; 740 | elem.style[ name ] = options[ name ]; 741 | } 742 | 743 | callback.call( elem ); 744 | 745 | // Revert the old values 746 | for ( var name in options ) 747 | elem.style[ name ] = old[ name ]; 748 | }, 749 | 750 | css: function( elem, name, force, extra ) { 751 | if ( name == "width" || name == "height" ) { 752 | var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; 753 | 754 | function getWH() { 755 | val = name == "width" ? elem.offsetWidth : elem.offsetHeight; 756 | 757 | if ( extra === "border" ) 758 | return; 759 | 760 | jQuery.each( which, function() { 761 | if ( !extra ) 762 | val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; 763 | if ( extra === "margin" ) 764 | val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; 765 | else 766 | val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; 767 | }); 768 | } 769 | 770 | if ( elem.offsetWidth !== 0 ) 771 | getWH(); 772 | else 773 | jQuery.swap( elem, props, getWH ); 774 | 775 | return Math.max(0, Math.round(val)); 776 | } 777 | 778 | return jQuery.curCSS( elem, name, force ); 779 | }, 780 | 781 | curCSS: function( elem, name, force ) { 782 | var ret, style = elem.style; 783 | 784 | // We need to handle opacity special in IE 785 | if ( name == "opacity" && !jQuery.support.opacity ) { 786 | ret = jQuery.attr( style, "opacity" ); 787 | 788 | return ret == "" ? 789 | "1" : 790 | ret; 791 | } 792 | 793 | // Make sure we're using the right name for getting the float value 794 | if ( name.match( /float/i ) ) 795 | name = styleFloat; 796 | 797 | if ( !force && style && style[ name ] ) 798 | ret = style[ name ]; 799 | 800 | else if ( defaultView.getComputedStyle ) { 801 | 802 | // Only "float" is needed here 803 | if ( name.match( /float/i ) ) 804 | name = "float"; 805 | 806 | name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); 807 | 808 | var computedStyle = defaultView.getComputedStyle( elem, null ); 809 | 810 | if ( computedStyle ) 811 | ret = computedStyle.getPropertyValue( name ); 812 | 813 | // We should always get a number back from opacity 814 | if ( name == "opacity" && ret == "" ) 815 | ret = "1"; 816 | 817 | } else if ( elem.currentStyle ) { 818 | var camelCase = name.replace(/\-(\w)/g, function(all, letter){ 819 | return letter.toUpperCase(); 820 | }); 821 | 822 | ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; 823 | 824 | // From the awesome hack by Dean Edwards 825 | // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 826 | 827 | // If we're not dealing with a regular pixel number 828 | // but a number that has a weird ending, we need to convert it to pixels 829 | if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { 830 | // Remember the original values 831 | var left = style.left, rsLeft = elem.runtimeStyle.left; 832 | 833 | // Put in the new values to get a computed value out 834 | elem.runtimeStyle.left = elem.currentStyle.left; 835 | style.left = ret || 0; 836 | ret = style.pixelLeft + "px"; 837 | 838 | // Revert the changed values 839 | style.left = left; 840 | elem.runtimeStyle.left = rsLeft; 841 | } 842 | } 843 | 844 | return ret; 845 | }, 846 | 847 | clean: function( elems, context, fragment ) { 848 | context = context || document; 849 | 850 | // !context.createElement fails in IE with an error but returns typeof 'object' 851 | if ( typeof context.createElement === "undefined" ) 852 | context = context.ownerDocument || context[0] && context[0].ownerDocument || document; 853 | 854 | // If a single string is passed in and it's a single tag 855 | // just do a createElement and skip the rest 856 | if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { 857 | var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); 858 | if ( match ) 859 | return [ context.createElement( match[1] ) ]; 860 | } 861 | 862 | var ret = [], scripts = [], div = context.createElement("div"); 863 | 864 | jQuery.each(elems, function(i, elem){ 865 | if ( typeof elem === "number" ) 866 | elem += ''; 867 | 868 | if ( !elem ) 869 | return; 870 | 871 | // Convert html string into DOM nodes 872 | if ( typeof elem === "string" ) { 873 | // Fix "XHTML"-style tags in all browsers 874 | elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ 875 | return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? 876 | all : 877 | front + ">"; 878 | }); 879 | 880 | // Trim whitespace, otherwise indexOf won't work as expected 881 | var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); 882 | 883 | var wrap = 884 | // option or optgroup 885 | !tags.indexOf("", "" ] || 887 | 888 | !tags.indexOf("", "" ] || 890 | 891 | tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && 892 | [ 1, "", "
" ] || 893 | 894 | !tags.indexOf("", "" ] || 896 | 897 | // matched above 898 | (!tags.indexOf("", "" ] || 900 | 901 | !tags.indexOf("", "" ] || 903 | 904 | // IE can't serialize and