teaspoon 1.1.3 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +47 -0
  3. data/MIT.LICENSE +2 -2
  4. data/README.md +56 -35
  5. data/app/assets/javascripts/teaspoon/hook.coffee +1 -1
  6. data/app/assets/javascripts/teaspoon/teaspoon.coffee +13 -1
  7. data/app/controllers/teaspoon/suite_controller.rb +18 -13
  8. data/app/views/teaspoon/suite/_boot.html.erb +1 -1
  9. data/app/views/teaspoon/suite/index.html.erb +1 -1
  10. data/lib/generators/teaspoon/install/install_generator.rb +34 -34
  11. data/lib/generators/teaspoon/install/templates/MISSING_FRAMEWORK +1 -1
  12. data/lib/generators/teaspoon/install/templates/POST_INSTALL +1 -1
  13. data/lib/generators/teaspoon/install/templates/env_comments.rb.tt +9 -7
  14. data/lib/tasks/teaspoon/info.rake +1 -1
  15. data/lib/teaspoon/command_line.rb +76 -76
  16. data/lib/teaspoon/configuration.rb +4 -4
  17. data/lib/teaspoon/console.rb +41 -40
  18. data/lib/teaspoon/coverage.rb +29 -25
  19. data/lib/teaspoon/deprecated.rb +1 -1
  20. data/lib/teaspoon/driver.rb +1 -1
  21. data/lib/teaspoon/driver/browserstack.rb +111 -0
  22. data/lib/teaspoon/driver/phantomjs.rb +25 -26
  23. data/lib/teaspoon/driver/phantomjs/runner.js +31 -3
  24. data/lib/teaspoon/driver/selenium.rb +13 -9
  25. data/lib/teaspoon/engine.rb +32 -27
  26. data/lib/teaspoon/environment.rb +28 -28
  27. data/lib/teaspoon/exceptions.rb +7 -7
  28. data/lib/teaspoon/exporter.rb +23 -23
  29. data/lib/teaspoon/formatter/base.rb +64 -46
  30. data/lib/teaspoon/formatter/clean.rb +2 -2
  31. data/lib/teaspoon/formatter/documentation.rb +40 -40
  32. data/lib/teaspoon/formatter/dot.rb +12 -12
  33. data/lib/teaspoon/formatter/json.rb +21 -21
  34. data/lib/teaspoon/formatter/junit.rb +52 -51
  35. data/lib/teaspoon/formatter/modules/report_module.rb +34 -32
  36. data/lib/teaspoon/formatter/pride.rb +21 -21
  37. data/lib/teaspoon/formatter/rspec_html.rb +31 -31
  38. data/lib/teaspoon/formatter/snowday.rb +6 -5
  39. data/lib/teaspoon/formatter/swayze_or_oprah.rb +91 -91
  40. data/lib/teaspoon/formatter/tap.rb +29 -29
  41. data/lib/teaspoon/formatter/tap_y.rb +57 -57
  42. data/lib/teaspoon/formatter/teamcity.rb +63 -63
  43. data/lib/teaspoon/framework/base.rb +1 -1
  44. data/lib/teaspoon/instrumentation.rb +18 -18
  45. data/lib/teaspoon/registry.rb +9 -9
  46. data/lib/teaspoon/registry/has_default.rb +2 -2
  47. data/lib/teaspoon/runner.rb +37 -37
  48. data/lib/teaspoon/server.rb +30 -29
  49. data/lib/teaspoon/suite.rb +68 -57
  50. data/lib/teaspoon/utility.rb +6 -0
  51. data/lib/teaspoon/version.rb +1 -1
  52. metadata +22 -15
  53. data/lib/teaspoon/driver/capybara_webkit.rb +0 -40
@@ -79,7 +79,7 @@ teaspoon coverage directive has changed and is now more flexible, define coverag
79
79
  end
80
80
 
81
81
  def no_coverage(*)
82
- Teaspoon.dep("suite.no_coverage has been removed in Teaspoon 1.0. Please use coverage.ignore instead. https://github.com/modeset/teaspoon/blob/master/CHANGELOG.md")
82
+ Teaspoon.dep("suite.no_coverage has been removed in Teaspoon 1.0. Please use coverage.ignore instead. https://github.com/jejacks0n/teaspoon/blob/master/CHANGELOG.md")
83
83
  []
84
84
  end
85
85
  alias_method :no_coverage=, :no_coverage
@@ -12,4 +12,4 @@ end
12
12
 
13
13
  Teaspoon::Driver.register(:phantomjs, "Teaspoon::Driver::Phantomjs", "teaspoon/driver/phantomjs", default: true)
14
14
  Teaspoon::Driver.register(:selenium, "Teaspoon::Driver::Selenium", "teaspoon/driver/selenium")
15
- Teaspoon::Driver.register(:capybara_webkit, "Teaspoon::Driver::CapybaraWebkit", "teaspoon/driver/capybara_webkit")
15
+ Teaspoon::Driver.register(:browserstack, "Teaspoon::Driver::BrowserStack", "teaspoon/driver/browserstack")
@@ -0,0 +1,111 @@
1
+ # :nocov:
2
+ begin
3
+ require "selenium-webdriver"
4
+ rescue LoadError
5
+ Teaspoon.abort("Could not find Selenium Webdriver. Install the selenium-webdriver gem.")
6
+ end
7
+ # :nocov:
8
+
9
+ require "teaspoon/driver/base"
10
+
11
+ # Need to have BrowserStackLocal binary (https://www.browserstack.com/local-testing#command-line)
12
+ # running in the background to use this driver.
13
+ module Teaspoon
14
+ module Driver
15
+ class BrowserStack < Base
16
+ MAX_PARALLEL = 10
17
+
18
+ def initialize(options = nil)
19
+ options ||= {}
20
+ case options
21
+ when Hash then @options = options.symbolize_keys
22
+ when String then @options = JSON.parse(options).symbolize_keys
23
+ else raise Teaspoon::DriverOptionsError.new(types: "hash or json string")
24
+ end
25
+
26
+ unless @options[:capabilities] && @options[:capabilities].is_a?(Array)
27
+ raise Teaspoon::DriverOptionsError.new(types: "capabilities array." \
28
+ "Options must have a key 'capabilities' of type array")
29
+ end
30
+ @options[:capabilities].each(&:symbolize_keys!)
31
+ rescue JSON::ParserError
32
+ raise Teaspoon::DriverOptionsError.new(types: "hash or json string")
33
+ end
34
+
35
+ def run_specs(runner, url)
36
+ parallelize do
37
+ driver = Thread.current[:driver]
38
+ driver.navigate.to(url)
39
+ ::Selenium::WebDriver::Wait.new(driver_options).until do
40
+ done = driver.execute_script("return window.Teaspoon && window.Teaspoon.finished")
41
+ driver.execute_script("return window.Teaspoon && window.Teaspoon.getMessages() || []").each do |line|
42
+ runner.process("#{line}\n")
43
+ end
44
+ done
45
+ end
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def parallelize
52
+ threads = []
53
+ left_capabilities = capabilities
54
+ until left_capabilities.empty?
55
+ left_capabilities.pop(max_parallel).each do |desired_capability|
56
+ desired_capability[:"browserstack.local"] = true
57
+ desired_capability[:project] = driver_options[:project] if driver_options[:project]
58
+ desired_capability[:build] = driver_options[:build] if driver_options[:build]
59
+ threads << Thread.new do
60
+ driver = ::Selenium::WebDriver.for(:remote, url: hub_url, desired_capabilities: desired_capability)
61
+ Thread.current[:driver] = driver
62
+ capability = driver.capabilities
63
+
64
+ Thread.current[:name] = "Session on #{capability[:platform].to_s.strip}," \
65
+ "#{capability[:browser_name].to_s.strip} #{capability[:version].to_s.strip}"
66
+
67
+ yield
68
+ Thread.current[:driver].quit
69
+ STDOUT.print("#{Thread.current[:name]} Completed\n") unless Teaspoon.configuration.suppress_log
70
+ end
71
+ end
72
+ threads.each(&:join)
73
+ threads = []
74
+ end
75
+ end
76
+
77
+ def capabilities
78
+ driver_options[:capabilities]
79
+ end
80
+
81
+ def hub_url
82
+ "https://#{username}:#{access_key}@hub.browserstack.com/wd/hub"
83
+ end
84
+
85
+ def username
86
+ driver_options[:username] || ENV["BROWSERSTACK_USERNAME"]
87
+ end
88
+
89
+ def access_key
90
+ driver_options[:access_key] || ENV["BROWSERSTACK_ACCESS_KEY"]
91
+ end
92
+
93
+ def max_parallel
94
+ parallel = MAX_PARALLEL
95
+ begin
96
+ parallel = driver_options[:max_parallel].to_i if driver_options[:max_parallel].to_i > 0
97
+ rescue
98
+ end
99
+ parallel
100
+ end
101
+
102
+ def driver_options
103
+ @driver_options ||= HashWithIndifferentAccess.new(
104
+ timeout: Teaspoon.configuration.driver_timeout.to_i,
105
+ interval: 0.01,
106
+ message: "Timed out"
107
+ ).merge(@options)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -31,38 +31,37 @@ module Teaspoon
31
31
 
32
32
  protected
33
33
 
34
- def run(*args, &block)
35
- IO.popen([executable, *args].join(" ")) { |io| io.each(&block) }
34
+ def run(*args, &block)
35
+ IO.popen([executable, *args].join(" ")) { |io| io.each(&block) }
36
36
 
37
- unless $?.success?
38
- raise Teaspoon::DependencyError.new("Failed to use phantomjs, which exited with status code: #{$?.exitstatus}")
37
+ unless $?.nil? || $?.success?
38
+ raise Teaspoon::DependencyError.new("Failed to use phantomjs, which exited with status code: #{$?.exitstatus}")
39
+ end
39
40
  end
40
- end
41
41
 
42
- def driver_options(url)
43
- [
44
- @options,
45
- escape_quotes(script),
46
- escape_quotes(url),
47
- Teaspoon.configuration.driver_timeout
48
- ].flatten.compact
49
- end
42
+ def driver_options(url)
43
+ [
44
+ @options,
45
+ escape_quotes(script),
46
+ escape_quotes(url),
47
+ Teaspoon.configuration.driver_timeout
48
+ ].flatten.compact
49
+ end
50
50
 
51
- def escape_quotes(string)
52
- %{"#{string.gsub('"', '\"')}"}
53
- end
51
+ def escape_quotes(string)
52
+ %{"#{string.gsub('"', '\"')}"}
53
+ end
54
54
 
55
- def executable
56
- return @executable if @executable
57
- @executable = which("phantomjs")
58
- @executable = ::Phantomjs.path if @executable.blank? && defined?(::Phantomjs)
59
- return @executable unless @executable.blank?
60
- raise Teaspoon::MissingDependencyError.new("Unable to locate phantomjs. Install it or use the phantomjs gem.")
61
- end
55
+ def executable
56
+ return @executable if @executable
57
+ @executable = defined?(::Phantomjs) ? ::Phantomjs.path : which("phantomjs")
58
+ return @executable unless @executable.blank?
59
+ raise Teaspoon::MissingDependencyError.new("Unable to locate phantomjs. Install it or use the phantomjs gem.")
60
+ end
62
61
 
63
- def script
64
- File.expand_path("../phantomjs/runner.js", __FILE__)
65
- end
62
+ def script
63
+ File.expand_path("../phantomjs/runner.js", __FILE__)
64
+ end
66
65
  end
67
66
  end
68
67
  end
@@ -59,9 +59,12 @@
59
59
  var _this = this;
60
60
  return {
61
61
  onError: function(message, trace) {
62
+ var started = _this.page.evaluate(function() {
63
+ return window.Teaspoon && window.Teaspoon.started;
64
+ });
62
65
  console.log(JSON.stringify({
63
66
  _teaspoon: true,
64
- type: "error",
67
+ type: started ? "error" : "exception",
65
68
  message: message,
66
69
  trace: trace
67
70
  }));
@@ -89,8 +92,33 @@
89
92
  return window.Teaspoon;
90
93
  });
91
94
  if (!(status === "success" && defined)) {
92
- _this.fail("Failed to load: " + _this.url);
93
- return;
95
+ if (status === "success") {
96
+ // Could not load the window.Teaspoon object from the JavaScript on the
97
+ // rendered page. This indicates that a problem occured. Lets therfore
98
+ // print the page as a failure description.
99
+ // Get plain text of the page, intend all lines (better readable)
100
+ var ind = " ";
101
+ var error = _this.page.plainText.replace(/(?:\n)/g, "\n" + ind);
102
+ // take only first 10 lines, as they usually provide a good entry
103
+ // point for debugging and we should not spam our console.
104
+ var erroroutput = error.split("\n").slice(0, 10);
105
+ if (erroroutput !== error) {
106
+ erroroutput.push("... (further lines have been removed)");
107
+ }
108
+ var fail = [
109
+ "Failed to get Teaspoon result object on page: " + _this.url,
110
+ "The title of this page was '" + _this.page.title + "'.",
111
+ "",
112
+ erroroutput.join("\n")
113
+ ];
114
+
115
+ _this.fail(fail.join(" \n" + ind));
116
+ }
117
+ else {
118
+ // Status is not 'success'
119
+ _this.fail("Failed to load: " + _this.url + ". Status: " + status);
120
+ return;
121
+ }
94
122
  }
95
123
  _this.waitForResults();
96
124
  }
@@ -23,7 +23,10 @@ module Teaspoon
23
23
  end
24
24
 
25
25
  def run_specs(runner, url)
26
- driver = ::Selenium::WebDriver.for(driver_options[:client_driver])
26
+ driver = ::Selenium::WebDriver.for(
27
+ driver_options[:client_driver],
28
+ **driver_options[:selenium_options].to_hash.to_options
29
+ )
27
30
  driver.navigate.to(url)
28
31
 
29
32
  ::Selenium::WebDriver::Wait.new(driver_options).until do
@@ -39,14 +42,15 @@ module Teaspoon
39
42
 
40
43
  protected
41
44
 
42
- def driver_options
43
- @driver_options ||= HashWithIndifferentAccess.new(
44
- client_driver: :firefox,
45
- timeout: Teaspoon.configuration.driver_timeout.to_i,
46
- interval: 0.01,
47
- message: "Timed out"
48
- ).merge(@options)
49
- end
45
+ def driver_options
46
+ @driver_options ||= HashWithIndifferentAccess.new(
47
+ client_driver: :firefox,
48
+ timeout: Teaspoon.configuration.driver_timeout.to_i,
49
+ interval: 0.01,
50
+ message: "Timed out",
51
+ selenium_options: {}
52
+ ).merge(@options)
53
+ end
50
54
  end
51
55
  end
52
56
  end
@@ -51,6 +51,9 @@ module Teaspoon
51
51
 
52
52
  def self.inject_instrumentation
53
53
  Sprockets::Environment.send(:include, Teaspoon::SprocketsInstrumentation)
54
+ Sprockets::CachedEnvironment.send(:include, Teaspoon::SprocketsInstrumentation)
55
+ rescue NameError
56
+ # Handle cached environment in Sprockets 2.x
54
57
  Sprockets::Index.send(:include, Teaspoon::SprocketsInstrumentation)
55
58
  end
56
59
 
@@ -58,7 +61,9 @@ module Teaspoon
58
61
  mount_at = Teaspoon.configuration.mount_at
59
62
 
60
63
  return if app.routes.recognize_path(mount_at)[:action] != "routing_error" rescue nil
61
- require Teaspoon::Engine.root.join("app/controllers/teaspoon/suite_controller")
64
+ ActiveSupport.on_load(:action_controller) do
65
+ require Teaspoon::Engine.root.join("app/controllers/teaspoon/suite_controller")
66
+ end
62
67
 
63
68
  app.routes.prepend { mount Teaspoon::Engine => mount_at, as: "teaspoon" }
64
69
  end
@@ -79,40 +84,40 @@ module Teaspoon
79
84
 
80
85
  private
81
86
 
82
- def self.using_phantomjs?
83
- Teaspoon::Driver.equal?(Teaspoon.configuration.driver, :phantomjs)
84
- end
87
+ def self.using_phantomjs?
88
+ Teaspoon::Driver.matches?(Teaspoon.configuration.driver, :phantomjs)
89
+ end
85
90
 
86
- def self.render_exceptions_with_javascript
87
- ActionDispatch::DebugExceptions.class_eval do
88
- def render_exception(_env, exception)
89
- message = "#{exception.class.name}: #{exception.message}"
90
- body = "<script>throw Error(#{[message, exception.backtrace].join("\n").inspect})</script>"
91
- [200, { "Content-Type" => "text/html;", "Content-Length" => body.bytesize.to_s }, [body]]
91
+ def self.render_exceptions_with_javascript
92
+ ActionDispatch::DebugExceptions.class_eval do
93
+ def render_exception(_env, exception)
94
+ message = "#{exception.class.name}: #{exception.message}"
95
+ body = "<script>throw Error(#{[message, exception.backtrace].join("\n").inspect})</script>"
96
+ [200, { "Content-Type" => "text/html;", "Content-Length" => body.bytesize.to_s }, [body]]
97
+ end
92
98
  end
93
99
  end
94
- end
95
100
  end
96
101
  end
97
102
  end
98
103
 
99
- begin
100
- require 'action_view'
101
- if ActionView::VERSION::STRING == "4.2.5" || ActionView::VERSION::MAJOR >= 5
102
- require 'action_view/helpers/asset_tag_helper'
103
- module ActionView::Helpers::AssetTagHelper
104
- def javascript_include_tag(*sources)
105
- options = sources.extract_options!.stringify_keys
106
- path_options = options.extract!('protocol', 'extname', 'host').symbolize_keys
107
- path_options[:debug] = options['allow_non_precompiled']
108
- sources.uniq.map { |source|
109
- tag_options = {
110
- "src" => path_to_javascript(source, path_options)
111
- }.merge!(options)
112
- content_tag(:script, "", tag_options)
113
- }.join("\n").html_safe
104
+ class Sprockets::Rails::HelperAssetResolvers::Environment
105
+ def raise_unless_precompiled_asset(path)
106
+ if Rails.env.test? or Rails.env.development?
107
+ # nothing, thank you
108
+ else
109
+ super
110
+ end
111
+ end
112
+ end
113
+
114
+ # Some Sprockets patches to work with Sprockets 2.x
115
+ unless Sprockets::Asset.public_method_defined?(:filename)
116
+ module Sprockets
117
+ class Asset
118
+ def filename
119
+ pathname.to_s
114
120
  end
115
121
  end
116
122
  end
117
- rescue
118
123
  end
@@ -26,41 +26,41 @@ module Teaspoon
26
26
 
27
27
  private
28
28
 
29
- def self.find_env(override = nil)
30
- override ||= ENV["TEASPOON_ENV"]
31
- env_files = override && !override.empty? ? [override] : standard_environments
29
+ def self.find_env(override = nil)
30
+ override ||= ENV["TEASPOON_ENV"]
31
+ env_files = override && !override.empty? ? [override] : standard_environments
32
32
 
33
- env_files.each do |filename|
34
- file = File.expand_path(filename, Dir.pwd)
35
- ENV["TEASPOON_ENV"] = file if override
36
- return file if File.exists?(file)
37
- end
33
+ env_files.each do |filename|
34
+ file = File.expand_path(filename, Teaspoon.root)
35
+ ENV["TEASPOON_ENV"] = file if override
36
+ return file if File.exist?(file)
37
+ end
38
38
 
39
- raise Teaspoon::EnvironmentNotFound.new(searched: env_files.join(", "))
40
- end
39
+ raise Teaspoon::EnvironmentNotFound.new(searched: env_files.join(", "))
40
+ end
41
41
 
42
- def self.standard_environments
43
- ["spec/teaspoon_env.rb", "test/teaspoon_env.rb", "teaspoon_env.rb"]
44
- end
42
+ def self.standard_environments
43
+ ["spec/teaspoon_env.rb", "test/teaspoon_env.rb", "teaspoon_env.rb"]
44
+ end
45
45
 
46
- def self.require_env(file)
47
- ::Kernel.load(file)
48
- end
46
+ def self.require_env(file)
47
+ ::Kernel.load(file)
48
+ end
49
49
 
50
- def self.rails_loaded?
51
- !!defined?(Rails)
52
- end
50
+ def self.rails_loaded?
51
+ !!defined?(Rails)
52
+ end
53
53
 
54
- def self.load_rails
55
- rails_env = ENV["TEASPOON_RAILS_ENV"] || File.expand_path("config/environment.rb", Dir.pwd)
54
+ def self.load_rails
55
+ rails_env = ENV["TEASPOON_RAILS_ENV"] || File.expand_path("config/environment.rb", Teaspoon.root)
56
56
 
57
- # Try to load rails, assume teaspoon_env will do it if the expected
58
- # environment isn't found.
59
- if File.exists?(rails_env)
60
- require rails_env
61
- else
62
- require_environment
57
+ # Try to load rails, assume teaspoon_env will do it if the expected
58
+ # environment isn't found.
59
+ if File.exist?(rails_env)
60
+ require rails_env
61
+ else
62
+ require_environment
63
+ end
63
64
  end
64
- end
65
65
  end
66
66
  end
@@ -2,13 +2,13 @@ module Teaspoon
2
2
  class Error < StandardError
3
3
  protected
4
4
 
5
- def build_message(msg_or_options)
6
- if msg_or_options.is_a?(String)
7
- msg_or_options
8
- else
9
- yield msg_or_options
5
+ def build_message(msg_or_options)
6
+ if msg_or_options.is_a?(String)
7
+ msg_or_options
8
+ else
9
+ yield msg_or_options
10
+ end
10
11
  end
11
- end
12
12
  end
13
13
 
14
14
  class Failure < Teaspoon::Error
@@ -29,7 +29,7 @@ module Teaspoon
29
29
  super(build_message(msg_or_options) do |options|
30
30
  msg = "Unknown framework: expected \"#{options[:name]}\" to be a registered framework. Available frameworks are #{options[:available]}."
31
31
  if options[:available].blank?
32
- msg += " Do you need to update your Gemfile to use the teaspoon-#{options[:name]} gem? If you are upgrading, please see https://github.com/modeset/teaspoon/blob/master/CHANGELOG.md"
32
+ msg + " Do you need to update your Gemfile to use the teaspoon-#{options[:name]} gem? If you are upgrading, please see https://github.com/jejacks0n/teaspoon/blob/master/CHANGELOG.md"
33
33
  end
34
34
  end)
35
35
  end