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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +47 -0
- data/MIT.LICENSE +2 -2
- data/README.md +56 -35
- data/app/assets/javascripts/teaspoon/hook.coffee +1 -1
- data/app/assets/javascripts/teaspoon/teaspoon.coffee +13 -1
- data/app/controllers/teaspoon/suite_controller.rb +18 -13
- data/app/views/teaspoon/suite/_boot.html.erb +1 -1
- data/app/views/teaspoon/suite/index.html.erb +1 -1
- data/lib/generators/teaspoon/install/install_generator.rb +34 -34
- data/lib/generators/teaspoon/install/templates/MISSING_FRAMEWORK +1 -1
- data/lib/generators/teaspoon/install/templates/POST_INSTALL +1 -1
- data/lib/generators/teaspoon/install/templates/env_comments.rb.tt +9 -7
- data/lib/tasks/teaspoon/info.rake +1 -1
- data/lib/teaspoon/command_line.rb +76 -76
- data/lib/teaspoon/configuration.rb +4 -4
- data/lib/teaspoon/console.rb +41 -40
- data/lib/teaspoon/coverage.rb +29 -25
- data/lib/teaspoon/deprecated.rb +1 -1
- data/lib/teaspoon/driver.rb +1 -1
- data/lib/teaspoon/driver/browserstack.rb +111 -0
- data/lib/teaspoon/driver/phantomjs.rb +25 -26
- data/lib/teaspoon/driver/phantomjs/runner.js +31 -3
- data/lib/teaspoon/driver/selenium.rb +13 -9
- data/lib/teaspoon/engine.rb +32 -27
- data/lib/teaspoon/environment.rb +28 -28
- data/lib/teaspoon/exceptions.rb +7 -7
- data/lib/teaspoon/exporter.rb +23 -23
- data/lib/teaspoon/formatter/base.rb +64 -46
- data/lib/teaspoon/formatter/clean.rb +2 -2
- data/lib/teaspoon/formatter/documentation.rb +40 -40
- data/lib/teaspoon/formatter/dot.rb +12 -12
- data/lib/teaspoon/formatter/json.rb +21 -21
- data/lib/teaspoon/formatter/junit.rb +52 -51
- data/lib/teaspoon/formatter/modules/report_module.rb +34 -32
- data/lib/teaspoon/formatter/pride.rb +21 -21
- data/lib/teaspoon/formatter/rspec_html.rb +31 -31
- data/lib/teaspoon/formatter/snowday.rb +6 -5
- data/lib/teaspoon/formatter/swayze_or_oprah.rb +91 -91
- data/lib/teaspoon/formatter/tap.rb +29 -29
- data/lib/teaspoon/formatter/tap_y.rb +57 -57
- data/lib/teaspoon/formatter/teamcity.rb +63 -63
- data/lib/teaspoon/framework/base.rb +1 -1
- data/lib/teaspoon/instrumentation.rb +18 -18
- data/lib/teaspoon/registry.rb +9 -9
- data/lib/teaspoon/registry/has_default.rb +2 -2
- data/lib/teaspoon/runner.rb +37 -37
- data/lib/teaspoon/server.rb +30 -29
- data/lib/teaspoon/suite.rb +68 -57
- data/lib/teaspoon/utility.rb +6 -0
- data/lib/teaspoon/version.rb +1 -1
- metadata +22 -15
- data/lib/teaspoon/driver/capybara_webkit.rb +0 -40
data/lib/teaspoon/deprecated.rb
CHANGED
@@ -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/
|
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
|
data/lib/teaspoon/driver.rb
CHANGED
@@ -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(:
|
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
|
-
|
35
|
-
|
34
|
+
def run(*args, &block)
|
35
|
+
IO.popen([executable, *args].join(" ")) { |io| io.each(&block) }
|
36
36
|
|
37
|
-
|
38
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
51
|
+
def escape_quotes(string)
|
52
|
+
%{"#{string.gsub('"', '\"')}"}
|
53
|
+
end
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
93
|
-
|
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(
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
data/lib/teaspoon/engine.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
87
|
+
def self.using_phantomjs?
|
88
|
+
Teaspoon::Driver.matches?(Teaspoon.configuration.driver, :phantomjs)
|
89
|
+
end
|
85
90
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
data/lib/teaspoon/environment.rb
CHANGED
@@ -26,41 +26,41 @@ module Teaspoon
|
|
26
26
|
|
27
27
|
private
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
def self.find_env(override = nil)
|
30
|
+
override ||= ENV["TEASPOON_ENV"]
|
31
|
+
env_files = override && !override.empty? ? [override] : standard_environments
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
40
|
-
|
39
|
+
raise Teaspoon::EnvironmentNotFound.new(searched: env_files.join(", "))
|
40
|
+
end
|
41
41
|
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
def self.standard_environments
|
43
|
+
["spec/teaspoon_env.rb", "test/teaspoon_env.rb", "teaspoon_env.rb"]
|
44
|
+
end
|
45
45
|
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
def self.require_env(file)
|
47
|
+
::Kernel.load(file)
|
48
|
+
end
|
49
49
|
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
def self.rails_loaded?
|
51
|
+
!!defined?(Rails)
|
52
|
+
end
|
53
53
|
|
54
|
-
|
55
|
-
|
54
|
+
def self.load_rails
|
55
|
+
rails_env = ENV["TEASPOON_RAILS_ENV"] || File.expand_path("config/environment.rb", Teaspoon.root)
|
56
56
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
data/lib/teaspoon/exceptions.rb
CHANGED
@@ -2,13 +2,13 @@ module Teaspoon
|
|
2
2
|
class Error < StandardError
|
3
3
|
protected
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
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
|