kookaburra 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +1 -3
  4. data/README.markdown +259 -181
  5. data/Rakefile +11 -2
  6. data/kookaburra.gemspec +1 -0
  7. data/lib/kookaburra.rb +24 -9
  8. data/lib/kookaburra/api_client.rb +1 -1
  9. data/lib/kookaburra/api_driver.rb +1 -1
  10. data/lib/kookaburra/assertion.rb +1 -1
  11. data/lib/kookaburra/configuration.rb +25 -9
  12. data/lib/kookaburra/configuration/proxy.rb +55 -0
  13. data/lib/kookaburra/dependency_accessor.rb +7 -0
  14. data/lib/kookaburra/exceptions.rb +11 -3
  15. data/lib/kookaburra/mental_model.rb +5 -5
  16. data/lib/kookaburra/rack_app_server.rb +3 -12
  17. data/lib/kookaburra/test_helpers.rb +53 -12
  18. data/lib/kookaburra/ui_driver/ui_component.rb +9 -2
  19. data/lib/kookaburra/version.rb +1 -1
  20. data/spec/integration/test_a_rack_application_spec.rb +3 -2
  21. data/spec/integration/test_multiple_applications_spec.rb +96 -0
  22. data/spec/kookaburra/api_client_spec.rb +6 -5
  23. data/spec/kookaburra/api_driver_spec.rb +18 -0
  24. data/spec/kookaburra/configuration/proxy_spec.rb +39 -0
  25. data/spec/kookaburra/configuration_spec.rb +54 -2
  26. data/spec/kookaburra/mental_model_spec.rb +22 -20
  27. data/spec/kookaburra/test_helpers_spec.rb +60 -24
  28. data/spec/kookaburra/ui_driver/scoped_browser_spec.rb +3 -2
  29. data/spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb +2 -1
  30. data/spec/kookaburra/ui_driver/ui_component_spec.rb +48 -14
  31. data/spec/kookaburra/ui_driver_spec.rb +4 -3
  32. data/spec/kookaburra_spec.rb +19 -14
  33. data/spec/spec_helper.rb +79 -0
  34. data/spec/support/shared_examples/it_can_have_ui_components.rb +2 -2
  35. data/spec/support/shared_examples/it_can_make_assertions.rb +3 -3
  36. data/spec/support/shared_examples/it_has_a_dependency_accessor.rb +3 -4
  37. 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
- RSpec::Core::RakeTask.new
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
@@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "uuid"
33
33
  spec.add_development_dependency "find_a_port"
34
34
  spec.add_development_dependency "json"
35
+ spec.add_development_dependency "codeclimate-test-reporter"
35
36
  end
@@ -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] return value is memoized
27
+ # @return [Kookaburra::Configuration]
25
28
  def configuration
26
29
  @configuration ||= Configuration.new
27
30
  end
28
31
 
29
- # Yields the current configuration so that it can be modified
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
- @configuration = configuration
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 ||= @api_driver_class.new(@configuration)
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 ||= @ui_driver_class.new(@configuration)
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
- @configuration.mental_model.send(collection_name).dup
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 {GivenDriver} implements 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#given}.
39
+ # object is instantiated for you by {Kookaburra#api}.
40
40
  #
41
41
  # @param [Kookaburra::Configuration] configuration
42
42
  def initialize(configuration)
@@ -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 ComponentNotFound < RuntimeError; end
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 = nil)
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) unless init_data.nil?
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 = lambda { |k,v| deleted[k] = v; true }
136
- super { |k,v| block.call(k,v) && move.call(k,v) }
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
- if defined?(JRUBY_VERSION)
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
- res = Net::HTTP.start('localhost', port) { |http| http.get('/__identify__') }
102
- if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
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
- # The {Kookaburra} instance to be used by your tests. It gets configured
86
- # using the options set in {Kookaburra.configuration}, and the result is
87
- # memoized.
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
- # @return [Kookaburra]
90
- def k
91
- @k ||= Kookaburra.new
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
- # @method api
95
- # Delegates to {#k}
96
- def_delegator :k, :api
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
- # @method ui
99
- # Delegates to {#k}
100
- def_delegator :k, :ui
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, :visible)
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)