teaspoon 1.1.3 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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