kookaburra 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +1 -3
- data/README.markdown +259 -181
- data/Rakefile +11 -2
- data/kookaburra.gemspec +1 -0
- data/lib/kookaburra.rb +24 -9
- data/lib/kookaburra/api_client.rb +1 -1
- data/lib/kookaburra/api_driver.rb +1 -1
- data/lib/kookaburra/assertion.rb +1 -1
- data/lib/kookaburra/configuration.rb +25 -9
- data/lib/kookaburra/configuration/proxy.rb +55 -0
- data/lib/kookaburra/dependency_accessor.rb +7 -0
- data/lib/kookaburra/exceptions.rb +11 -3
- data/lib/kookaburra/mental_model.rb +5 -5
- data/lib/kookaburra/rack_app_server.rb +3 -12
- data/lib/kookaburra/test_helpers.rb +53 -12
- data/lib/kookaburra/ui_driver/ui_component.rb +9 -2
- data/lib/kookaburra/version.rb +1 -1
- data/spec/integration/test_a_rack_application_spec.rb +3 -2
- data/spec/integration/test_multiple_applications_spec.rb +96 -0
- data/spec/kookaburra/api_client_spec.rb +6 -5
- data/spec/kookaburra/api_driver_spec.rb +18 -0
- data/spec/kookaburra/configuration/proxy_spec.rb +39 -0
- data/spec/kookaburra/configuration_spec.rb +54 -2
- data/spec/kookaburra/mental_model_spec.rb +22 -20
- data/spec/kookaburra/test_helpers_spec.rb +60 -24
- data/spec/kookaburra/ui_driver/scoped_browser_spec.rb +3 -2
- data/spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb +2 -1
- data/spec/kookaburra/ui_driver/ui_component_spec.rb +48 -14
- data/spec/kookaburra/ui_driver_spec.rb +4 -3
- data/spec/kookaburra_spec.rb +19 -14
- data/spec/spec_helper.rb +79 -0
- data/spec/support/shared_examples/it_can_have_ui_components.rb +2 -2
- data/spec/support/shared_examples/it_can_make_assertions.rb +3 -3
- data/spec/support/shared_examples/it_has_a_dependency_accessor.rb +3 -4
- metadata +25 -2
data/Rakefile
CHANGED
@@ -3,8 +3,17 @@
|
|
3
3
|
require 'bundler/gem_tasks'
|
4
4
|
require 'rspec/core/rake_task'
|
5
5
|
|
6
|
-
task :default => :spec
|
7
|
-
|
6
|
+
task :default => [:spec, 'spec:slow']
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new do |t|
|
9
|
+
t.rspec_opts = "-t ~slow"
|
10
|
+
end
|
11
|
+
|
12
|
+
namespace :spec do
|
13
|
+
RSpec::Core::RakeTask.new(:slow) do |t|
|
14
|
+
t.rspec_opts = "-t slow"
|
15
|
+
end
|
16
|
+
end
|
8
17
|
|
9
18
|
desc 'Runs reek to detect code smells'
|
10
19
|
task :reek do
|
data/kookaburra.gemspec
CHANGED
data/lib/kookaburra.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
require 'kookaburra/exceptions'
|
2
3
|
require 'kookaburra/mental_model'
|
3
4
|
require 'kookaburra/api_driver'
|
@@ -17,21 +18,31 @@ require 'kookaburra/configuration'
|
|
17
18
|
#
|
18
19
|
# @see Kookaburra::TestHelpers
|
19
20
|
class Kookaburra
|
21
|
+
extend Forwardable
|
22
|
+
|
20
23
|
class << self
|
21
24
|
# Stores the configuration object that is used by default when creating new
|
22
25
|
# instances of Kookaburra
|
23
26
|
#
|
24
|
-
# @return [Kookaburra::Configuration]
|
27
|
+
# @return [Kookaburra::Configuration]
|
25
28
|
def configuration
|
26
29
|
@configuration ||= Configuration.new
|
27
30
|
end
|
28
31
|
|
29
|
-
# Yields
|
32
|
+
# Yields a new configuration so that it can be modified
|
33
|
+
#
|
34
|
+
# The new configuration object will now be used by default when creating
|
35
|
+
# instances of Kookaburra.
|
30
36
|
#
|
31
37
|
# @yield [Kookaburra::Configuration]
|
32
38
|
def configure(&blk)
|
39
|
+
self.configuration = Configuration.new
|
33
40
|
blk.call(configuration)
|
34
41
|
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
attr_writer :configuration
|
35
46
|
end
|
36
47
|
|
37
48
|
# Returns a new Kookaburra instance that wires together your application's
|
@@ -39,10 +50,7 @@ class Kookaburra
|
|
39
50
|
#
|
40
51
|
# @param [Kookaburra::Configuration] configuration (Kookaburra.configuration)
|
41
52
|
def initialize(configuration = Kookaburra.configuration)
|
42
|
-
|
43
|
-
@configuration.mental_model = MentalModel.new
|
44
|
-
@api_driver_class = configuration.api_driver_class
|
45
|
-
@ui_driver_class = configuration.ui_driver_class
|
53
|
+
self.configuration = configuration
|
46
54
|
end
|
47
55
|
|
48
56
|
# Returns an instance of your APIDriver class configured to share test
|
@@ -50,7 +58,7 @@ class Kookaburra
|
|
50
58
|
#
|
51
59
|
# @return [Kookaburra::APIDriver]
|
52
60
|
def api
|
53
|
-
@api ||=
|
61
|
+
@api ||= api_driver_class.new(configuration)
|
54
62
|
end
|
55
63
|
|
56
64
|
# Returns an instance of your UIDriver class configured to share test fixture
|
@@ -59,7 +67,7 @@ class Kookaburra
|
|
59
67
|
#
|
60
68
|
# @return [Kookaburra::UIDriver]
|
61
69
|
def ui
|
62
|
-
@ui ||=
|
70
|
+
@ui ||= ui_driver_class.new(configuration)
|
63
71
|
end
|
64
72
|
|
65
73
|
# Returns a deep-dup of the specified {MentalModel::Collection}.
|
@@ -75,6 +83,13 @@ class Kookaburra
|
|
75
83
|
#
|
76
84
|
# @return [Kookaburra::MentalModel::Collection]
|
77
85
|
def get_data(collection_name)
|
78
|
-
|
86
|
+
mental_model.send(collection_name).dup
|
79
87
|
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
attr_accessor :configuration
|
92
|
+
|
93
|
+
def_delegators :configuration, :api_driver_class, :ui_driver_class,
|
94
|
+
:mental_model
|
80
95
|
end
|
@@ -7,7 +7,7 @@ class Kookaburra
|
|
7
7
|
#
|
8
8
|
# You will create a subclass of {APIClient} in your testing
|
9
9
|
# implementation to be used with you subclass of
|
10
|
-
# {Kookaburra::APIDriver}. While the {
|
10
|
+
# {Kookaburra::APIDriver}. While the {APIDriver} implements the
|
11
11
|
# "business domain" DSL for setting up your application state, the
|
12
12
|
# {APIClient} maps discreet operations to your application's web
|
13
13
|
# service API and can (optionally) handle encoding input data and
|
@@ -36,7 +36,7 @@ class Kookaburra
|
|
36
36
|
extend Forwardable
|
37
37
|
|
38
38
|
# It is unlikely that you would call #initialize yourself; your APIDriver
|
39
|
-
# object is instantiated for you by {Kookaburra#
|
39
|
+
# object is instantiated for you by {Kookaburra#api}.
|
40
40
|
#
|
41
41
|
# @param [Kookaburra::Configuration] configuration
|
42
42
|
def initialize(configuration)
|
data/lib/kookaburra/assertion.rb
CHANGED
@@ -18,7 +18,7 @@ class Kookaburra
|
|
18
18
|
#
|
19
19
|
# @raise [Kookaburra::AssertionFailed] raised if test evaluates to false
|
20
20
|
def assert(test, message = "You might want to provide a better message, eh?")
|
21
|
-
test or raise AssertionFailed, message
|
21
|
+
(!!test == true) or raise AssertionFailed, message
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'ostruct'
|
2
2
|
require 'delegate'
|
3
3
|
require 'kookaburra/dependency_accessor'
|
4
|
+
require 'kookaburra/mental_model'
|
5
|
+
require 'kookaburra/configuration/proxy'
|
4
6
|
|
5
7
|
class Kookaburra
|
6
8
|
# Provides access to the configuration data used throughout Kookaburra
|
@@ -38,15 +40,6 @@ class Kookaburra
|
|
38
40
|
# without it having been set
|
39
41
|
dependency_accessor :app_host
|
40
42
|
|
41
|
-
# This is the {Kookaburra::MentalModel} that is shared between your
|
42
|
-
# APIDriver and your UIDriver. This attribute is managed by {Kookaburra},
|
43
|
-
# so you shouldn't need to change it yourself.
|
44
|
-
#
|
45
|
-
# @attribute [rw] mental_model
|
46
|
-
# @raise [Kookaburra::ConfigurationError] if you try to read this attribute
|
47
|
-
# without it having been set
|
48
|
-
dependency_accessor :mental_model
|
49
|
-
|
50
43
|
# This is the logger to which Kookaburra will send various messages
|
51
44
|
# about its operation. This would generally be used to allow
|
52
45
|
# UIDriver subclasses to provide detailed failure information.
|
@@ -84,5 +77,28 @@ class Kookaburra
|
|
84
77
|
def app_host_uri
|
85
78
|
URI.parse(app_host)
|
86
79
|
end
|
80
|
+
|
81
|
+
# This is the {Kookaburra::MentalModel} that is shared between your
|
82
|
+
# APIDriver and your UIDriver. This attribute is managed by {Kookaburra},
|
83
|
+
# so you shouldn't need to change it yourself.
|
84
|
+
def mental_model
|
85
|
+
@mental_model ||= MentalModel.new
|
86
|
+
end
|
87
|
+
|
88
|
+
attr_writer :mental_model
|
89
|
+
|
90
|
+
def application(name, &block)
|
91
|
+
proxy = Proxy.new(name: name, basis: self)
|
92
|
+
block.call(proxy) if block_given?
|
93
|
+
applications[name] = Kookaburra.new(proxy)
|
94
|
+
end
|
95
|
+
|
96
|
+
def applications
|
97
|
+
@applications ||= {}
|
98
|
+
end
|
99
|
+
|
100
|
+
def has_named_applications?
|
101
|
+
!applications.empty?
|
102
|
+
end
|
87
103
|
end
|
88
104
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
class Kookaburra
|
4
|
+
class Configuration
|
5
|
+
# Used to manage configuration for a named application
|
6
|
+
#
|
7
|
+
# `Configuration::Proxy` objects delegate to their basis object to retrieve
|
8
|
+
# default values. This allows a base configuration to be specified and
|
9
|
+
# inherited by each application configuration.
|
10
|
+
#
|
11
|
+
# @see Kookaburra::Configuration#application
|
12
|
+
# @private
|
13
|
+
class Proxy < DelegateClass(Configuration)
|
14
|
+
attr_reader :name
|
15
|
+
|
16
|
+
# Builds a new Proxy
|
17
|
+
#
|
18
|
+
# @param [String] name An (arbitrary) identifier for this configuration
|
19
|
+
# @param [Kookaburra::Configuration] The object that will be used as the source for default values
|
20
|
+
def initialize(name:, basis:)
|
21
|
+
self.name = name
|
22
|
+
self.basis = basis
|
23
|
+
__setobj__(basis)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_writer :name
|
29
|
+
attr_accessor :basis
|
30
|
+
|
31
|
+
def method_missing(method_name, *args, &block)
|
32
|
+
define_proxy_override(method_name, args.first) \
|
33
|
+
|| super
|
34
|
+
end
|
35
|
+
|
36
|
+
def define_proxy_override(writer_name, value)
|
37
|
+
return false unless valid_attr_writer?(writer_name)
|
38
|
+
reader_name = reader_name_for writer_name
|
39
|
+
(class << self; self; end).class_eval do
|
40
|
+
define_method(reader_name) { value }
|
41
|
+
end
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid_attr_writer?(writer_name)
|
46
|
+
basis.respond_to?(writer_name) \
|
47
|
+
&& /=$/ =~ writer_name.to_s
|
48
|
+
end
|
49
|
+
|
50
|
+
def reader_name_for(writer_name)
|
51
|
+
writer_name.to_s.sub(/=$/, '')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -12,13 +12,20 @@ class Kookaburra
|
|
12
12
|
private
|
13
13
|
|
14
14
|
def define_dependency_accessor(name)
|
15
|
+
define_attr_reader(name)
|
16
|
+
define_attr_writer(name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def define_attr_reader(name)
|
15
20
|
define_method(name) do
|
16
21
|
class_name = self.class.name
|
17
22
|
class_name.sub!(/^$/, 'an Anonymous Class!!!')
|
18
23
|
instance_variable_get("@#{name}") or raise "No %s object was set on %s initialization." \
|
19
24
|
% [name, class_name]
|
20
25
|
end
|
26
|
+
end
|
21
27
|
|
28
|
+
def define_attr_writer(name)
|
22
29
|
define_method("#{name}=") do |value|
|
23
30
|
instance_variable_set("@#{name}", value)
|
24
31
|
end
|
@@ -1,14 +1,22 @@
|
|
1
1
|
class Kookaburra
|
2
|
+
# Raised when accessing unknown mental model data
|
2
3
|
# @private
|
3
4
|
class UnknownKeyError < ArgumentError; end
|
5
|
+
|
6
|
+
# Raised when there is a problem with the Kookaburra runtime configuration
|
4
7
|
# @private
|
5
8
|
class ConfigurationError < StandardError; end
|
9
|
+
|
10
|
+
# Raised when an API or UI request results in an enexpected HTTP response
|
6
11
|
# @private
|
7
12
|
class UnexpectedResponse < RuntimeError; end
|
13
|
+
|
14
|
+
# Raised when an assertion fails inside the drivers.
|
8
15
|
# @private
|
9
16
|
class AssertionFailed < RuntimeError; end
|
17
|
+
|
18
|
+
# Raised when calling `api` or `ui` test helpers and named applications are
|
19
|
+
# configured
|
10
20
|
# @private
|
11
|
-
class
|
12
|
-
# @private
|
13
|
-
class NullBrowserError < ConfigurationError; end
|
21
|
+
class AmbiguousDriverError < StandardError; end
|
14
22
|
end
|
@@ -28,7 +28,7 @@ class Kookaburra
|
|
28
28
|
# MentalModel instances respond to everything.
|
29
29
|
#
|
30
30
|
# @see #method_missing
|
31
|
-
def respond_to?
|
31
|
+
def respond_to?(_)
|
32
32
|
true
|
33
33
|
end
|
34
34
|
|
@@ -53,12 +53,12 @@ class Kookaburra
|
|
53
53
|
# @param [String] name The name of the collection. Used to provide
|
54
54
|
# helpful error messages when unknown keys are accessed.
|
55
55
|
# @param [Hash] init_data Preloads specific data into the collection
|
56
|
-
def initialize(name, init_data =
|
56
|
+
def initialize(name, init_data = {})
|
57
57
|
self.name = name
|
58
58
|
data = Hash.new do |hash, key|
|
59
59
|
raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
|
60
60
|
end
|
61
|
-
data.merge!(init_data)
|
61
|
+
data.merge!(init_data)
|
62
62
|
super(data)
|
63
63
|
end
|
64
64
|
|
@@ -132,8 +132,8 @@ class Kookaburra
|
|
132
132
|
#
|
133
133
|
# @return [Hash] the key/value pairs still remaining after the deletion
|
134
134
|
def delete_if(&block)
|
135
|
-
move =
|
136
|
-
super { |
|
135
|
+
move = ->(key, value) { deleted[key] = value; true }
|
136
|
+
super { |key, value| block.call(key, value) && move.call(key, value) }
|
137
137
|
end
|
138
138
|
|
139
139
|
def dup
|
@@ -49,16 +49,11 @@ class Kookaburra::RackAppServer
|
|
49
49
|
# will then monitor that port and only return once the app server is
|
50
50
|
# responding (or after the timeout period specified on {#initialize}).
|
51
51
|
def boot
|
52
|
-
|
53
|
-
thread_app_server
|
54
|
-
else
|
55
|
-
fork_app_server
|
56
|
-
end
|
52
|
+
fork_app_server
|
57
53
|
wait_for_app_to_respond
|
58
54
|
end
|
59
55
|
|
60
56
|
def shutdown
|
61
|
-
return if defined?(JRUBY_VERSION)
|
62
57
|
Process.kill(9, rack_server_pid)
|
63
58
|
Process.wait
|
64
59
|
end
|
@@ -68,10 +63,6 @@ class Kookaburra::RackAppServer
|
|
68
63
|
attr_accessor :rack_app_initializer, :rack_server_pid, :startup_timeout
|
69
64
|
attr_writer :port
|
70
65
|
|
71
|
-
def thread_app_server
|
72
|
-
Thread.new { start_server }
|
73
|
-
end
|
74
|
-
|
75
66
|
def fork_app_server
|
76
67
|
self.rack_server_pid = fork do
|
77
68
|
start_server
|
@@ -98,8 +89,8 @@ class Kookaburra::RackAppServer
|
|
98
89
|
end
|
99
90
|
|
100
91
|
def running?
|
101
|
-
|
102
|
-
|
92
|
+
case (Net::HTTP.start('localhost', port) { |http| http.get('/__identify__') })
|
93
|
+
when Net::HTTPSuccess, Net::HTTPRedirection
|
103
94
|
true
|
104
95
|
else
|
105
96
|
false
|
@@ -82,21 +82,62 @@ class Kookaburra
|
|
82
82
|
module TestHelpers
|
83
83
|
extend Forwardable
|
84
84
|
|
85
|
-
#
|
86
|
-
#
|
87
|
-
#
|
85
|
+
# @method api
|
86
|
+
# Delegates to main {Kookaburra} instance
|
87
|
+
#
|
88
|
+
# @note This method will only be available when no named applications have
|
89
|
+
# been defined. (See {Kookaburra::Configuration#application}.)
|
90
|
+
#
|
91
|
+
# @raise [Kookaburra::AmbiguousDriverError] if named applications have been
|
92
|
+
# configured.
|
93
|
+
def_delegator :kookaburra, :api
|
94
|
+
|
95
|
+
# @method ui
|
96
|
+
# Delegates to main {Kookaburra} instance
|
88
97
|
#
|
89
|
-
# @
|
90
|
-
|
91
|
-
|
98
|
+
# @note This method will only be available when no named applications have
|
99
|
+
# been defined. (See {Kookaburra::Configuration#application}.)
|
100
|
+
#
|
101
|
+
# @raise [Kookaburra::AmbiguousDriverError] if named applications have been
|
102
|
+
# configured.
|
103
|
+
def_delegator :kookaburra, :ui
|
104
|
+
|
105
|
+
# Delegates to main {Kookaburra} instance
|
106
|
+
def get_data(*args)
|
107
|
+
main_kookaburra.get_data(*args)
|
92
108
|
end
|
93
109
|
|
94
|
-
#
|
95
|
-
#
|
96
|
-
|
110
|
+
# Will return the {Kookaburra} instance for any configured applications
|
111
|
+
#
|
112
|
+
# After an application is configured via
|
113
|
+
# {Kookaburra::Configuration#application}, the TestHelpers will respond to
|
114
|
+
# the name of that application by returning the associated Kookaburra
|
115
|
+
# instance. (Messages that do not match the name of a configured application
|
116
|
+
# will be handled with the usual #method_missing semantics.)
|
117
|
+
#
|
118
|
+
# @see [Kookaburra::Configuration#application]
|
119
|
+
def method_missing(method_name, *args, &block)
|
120
|
+
Kookaburra.configuration.applications[method_name.to_sym] \
|
121
|
+
|| super
|
122
|
+
end
|
97
123
|
|
98
|
-
# @
|
99
|
-
|
100
|
-
|
124
|
+
# @see [Kookaburra::TestHelpers#method_missing]
|
125
|
+
def respond_to?(method_name)
|
126
|
+
Kookaburra.configuration.applications.has_key?(method_name) \
|
127
|
+
|| super
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def kookaburra
|
133
|
+
if Kookaburra.configuration.has_named_applications?
|
134
|
+
raise AmbiguousDriverError
|
135
|
+
end
|
136
|
+
main_kookaburra
|
137
|
+
end
|
138
|
+
|
139
|
+
def main_kookaburra
|
140
|
+
@kookaburra ||= Kookaburra.new
|
141
|
+
end
|
101
142
|
end
|
102
143
|
end
|
@@ -106,15 +106,22 @@ class Kookaburra
|
|
106
106
|
end
|
107
107
|
|
108
108
|
# Is the component's element found on the page and is it considered
|
109
|
-
# "visible" by the browser driver
|
109
|
+
# "visible" by the browser driver?
|
110
110
|
def visible?
|
111
|
-
visible = browser.has_css?(component_locator, :
|
111
|
+
visible = browser.has_css?(component_locator, visible: true)
|
112
112
|
unless visible
|
113
113
|
detect_server_error!
|
114
114
|
end
|
115
115
|
visible
|
116
116
|
end
|
117
117
|
|
118
|
+
# The opposite of {#visible?}
|
119
|
+
#
|
120
|
+
# @note This does not check for a server error.
|
121
|
+
def not_visible?
|
122
|
+
browser.has_no_css?(component_locator, visible: true)
|
123
|
+
end
|
124
|
+
|
118
125
|
# Returns the full URL by appending {#component_path} to the value of the
|
119
126
|
# {Kookaburra::Configuration#app_host} from the initialized configuration.
|
120
127
|
def url(*args)
|