kookaburra 0.18.3 → 0.20.0

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.
@@ -1,6 +1,10 @@
1
1
  require 'basic_object'
2
2
 
3
3
  class Kookaburra
4
+ # If you don't specify a browser in your {Kookaburra#configuration} but you
5
+ # try to access one in your {Kookaburra::UIDriver}, you'll get this instead.
6
+ # It gives a slightly better error message than complaining about calling
7
+ # stuff on `nil`.
4
8
  class NullBrowser < BasicObject
5
9
  def method_missing(*args)
6
10
  raise NullBrowserError,
@@ -9,14 +9,13 @@ class Kookaburra
9
9
  # @example RSpec setup
10
10
  # # in 'spec/support/kookaburra_setup.rb'
11
11
  # require 'kookaburra/test_helpers'
12
- # require 'my_app/kookaburra/api_driver'
13
12
  # require 'my_app/kookaburra/given_driver'
14
13
  # require 'my_app/kookaburra/ui_driver'
15
14
  #
16
15
  # Kookaburra.configuration = {
17
16
  # :given_driver_class => MyApp::Kookaburra::GivenDriver,
18
- # :api_driver_class => MyApp::Kookaburra::APIDriver,
19
17
  # :ui_driver_class => MyApp::Kookaburra::UIDriver,
18
+ # :app_host => 'http://my_app.example.com:12345',
20
19
  # :browser => Capybara,
21
20
  # :server_error_detection => lambda { |browser|
22
21
  # browser.has_css?('h1', text: 'Internal Server Error')
@@ -47,14 +46,13 @@ class Kookaburra
47
46
  # @example Cucumber setup
48
47
  # # in 'features/support/kookaburra_setup.rb'
49
48
  # require 'kookaburra/test_helpers'
50
- # require 'my_app/kookaburra/api_driver'
51
49
  # require 'my_app/kookaburra/given_driver'
52
50
  # require 'my_app/kookaburra/ui_driver'
53
51
  #
54
52
  # Kookaburra.configuration = {
55
53
  # :given_driver_class => MyApp::Kookaburra::GivenDriver,
56
- # :api_driver_class => MyApp::Kookaburra::APIDriver,
57
54
  # :ui_driver_class => MyApp::Kookaburra::UIDriver,
55
+ # :app_host => 'http://my_app.example.com:12345',
58
56
  # :browser => Capybara,
59
57
  # :server_error_detection => lambda { |browser|
60
58
  # browser.has_css?('h1', text: 'Internal Server Error')
@@ -4,9 +4,9 @@ require 'kookaburra/ui_driver/ui_component'
4
4
  class Kookaburra
5
5
  # You UIDriver subclass is where you define the DSL for testing your
6
6
  # application via its user interface. Methods defined in your DSL should
7
- # represent business processes rather than user interface manipulations. A
7
+ # represent business actions rather than user interface manipulations. A
8
8
  # good test of this is whether the names of your methods would need to change
9
- # significantly if the business process needed to be implemented in a vastly
9
+ # significantly if the application needed to be implemented in a vastly
10
10
  # different manner (a text-only terminal app vs. a web app, for instance).
11
11
  #
12
12
  # @abstract Subclass and implement your UI testing DSL
@@ -48,7 +48,8 @@ class Kookaburra
48
48
  # this component.
49
49
  def ui_component(component_name, component_class)
50
50
  define_method(component_name) do
51
- component_class.new(:browser => @browser, :server_error_detection => @server_error_detection)
51
+ component_class.new(:browser => @browser, :server_error_detection => @server_error_detection,
52
+ :app_host => @app_host)
52
53
  end
53
54
  end
54
55
  end
@@ -56,21 +57,24 @@ class Kookaburra
56
57
  # It is unlikely that you would instantiate your UIDriver on your own; the
57
58
  # object is configured for you when you call {Kookaburra#ui}.
58
59
  #
59
- # @option options [Object] browser Most likely a `Capybara::Session`
60
- # instance.
61
- # @option options [Kookaburra::TestData] test_data
62
- # @option options [Proc] server_error_detection A lambda that is passed the
63
- # `browser` object and should return `true` if the page indicates a server
60
+ # @option options [Capybara::Session] :browser Most likely a
61
+ # `Capybara::Session` instance.
62
+ # @option options [Kookaburra::MentalModel] :mental_model
63
+ # @option options [String] :app_host The root URL of your running
64
+ # application (e.g. "http://my_app.example.com:12345")
65
+ # @option options [Proc] :server_error_detection A lambda that is passed the
66
+ # `:browser` object and should return `true` if the page indicates a server
64
67
  # error has occured
65
68
  def initialize(options = {})
66
69
  @browser = options[:browser]
67
- @test_data = options[:test_data]
70
+ @app_host = options[:app_host]
71
+ @mental_model = options[:mental_model]
68
72
  @server_error_detection = options[:server_error_detection]
69
73
  end
70
74
 
71
75
  protected
72
76
 
73
- # @attribute [r] test_data
74
- dependency_accessor :test_data
77
+ # @attribute [r] mental_model
78
+ dependency_accessor :mental_model
75
79
  end
76
80
  end
@@ -70,11 +70,17 @@ class Kookaburra
70
70
  #
71
71
  # @see Kookaburra::UIDriver.ui_component
72
72
  #
73
- # @option options [Object] :browser probably ought to be a
74
- # `Capybara::Session` instance
75
- # @option options [Proc] :server_error_detection
73
+ # @option options [Capybara::Session] :browser This is the browser driver
74
+ # that allows you to interact with the web application's interface.
75
+ # @option options [String] :app_host The root URL of your running
76
+ # application (e.g. "http://my_app.example.com:12345")
77
+ # @option options [Proc] :server_error_detection A proc that will receive
78
+ # the object passed in to the :browser option as an argument and must
79
+ # return `true` if the server responded with an unexpected error or
80
+ # `false` if it did not.
76
81
  def initialize(options = {})
77
82
  @browser = options[:browser]
83
+ @app_host = options[:app_host]
78
84
  @server_error_detection = options[:server_error_detection]
79
85
  end
80
86
 
@@ -94,7 +100,8 @@ class Kookaburra
94
100
  end
95
101
 
96
102
  # @private
97
- # Behaves as you might expect given #method_missing
103
+ # (Not really private, but YARD seemingly lacks RDoc's :nodoc tag, and the
104
+ # semantics here don't differ from Object#respond_to?)
98
105
  def respond_to?(name)
99
106
  super || browser.respond_to?(name)
100
107
  end
@@ -116,7 +123,7 @@ class Kookaburra
116
123
  # to make it so.
117
124
  def show(*args)
118
125
  return if visible?
119
- browser.visit component_path(*args)
126
+ browser.visit component_url(*args)
120
127
  assert visible?, "The #{self.class.name} component is not visible!"
121
128
  end
122
129
 
@@ -157,12 +164,22 @@ class Kookaburra
157
164
 
158
165
  # @abstract
159
166
  # @return [String] the URL path that should be loaded in order to reach this component
167
+ # @raise [Kookaburra::ConfigurationError] raised if you haven't provided
168
+ # an implementation
160
169
  def component_path
161
170
  raise ConfigurationError, "You must define #{self.class.name}#component_path."
162
171
  end
163
172
 
173
+ # Returns the full URL by appending {#component_path} to the value of the
174
+ # :app_host option passed to {#initialize}.
175
+ def component_url(*args)
176
+ "#{@app_host}#{component_path(*args)}"
177
+ end
178
+
164
179
  # @abstract
165
180
  # @return [String] the CSS3 selector that will find the element in the DOM
181
+ # @raise [Kookaburra::ConfigurationError] raised if you haven't provided
182
+ # an implementation
166
183
  def component_locator
167
184
  raise ConfigurationError, "You must define #{self.class.name}#component_locator."
168
185
  end
@@ -1,135 +1,22 @@
1
1
  require 'kookaburra'
2
+ require 'kookaburra/json_api_driver'
2
3
  require 'capybara'
4
+
5
+ # These are required for the Rack app used for testing
3
6
  require 'sinatra/base'
7
+ require 'active_support/json'
4
8
  require 'active_support/hash_with_indifferent_access'
5
9
 
6
10
  describe "testing a Rack application with Kookaburra" do
7
11
  describe "with an HTML interface" do
8
12
  describe "with a JSON API" do
9
- require 'kookaburra/json_api_driver'
10
- class MyAPIDriver < Kookaburra::JsonApiDriver
11
- def create_user(user_data)
12
- post '/users', user_data
13
- end
14
-
15
- def create_widget(widget_data)
16
- post '/widgets', widget_data
17
- end
18
- end
19
-
20
- class MyGivenDriver < Kookaburra::GivenDriver
21
- def a_user(name)
22
- user = {'email' => 'bob@example.com', 'password' => '12345'}
23
- result = api.create_user(user)
24
- test_data.users[name] = result
25
- end
26
-
27
- def a_widget(name, attributes = {})
28
- widget = {'name' => 'Foo'}.merge(attributes)
29
- result = api.create_widget(widget)
30
- test_data.widgets[name] = result
31
- end
32
- end
33
-
34
- class SignInScreen < Kookaburra::UIDriver::UIComponent
35
- def component_path
36
- '/session/new'
37
- end
38
-
39
- def component_locator
40
- '#sign_in_screen'
41
- end
42
-
43
- def sign_in(user_data)
44
- fill_in 'Email:', :with => user_data['email']
45
- fill_in 'Password:', :with => user_data['password']
46
- click_button 'Sign In'
47
- end
48
- end
49
-
50
- class WidgetList < Kookaburra::UIDriver::UIComponent
51
- def component_path
52
- '/widgets'
53
- end
54
-
55
- def component_locator
56
- '#widget_list'
57
- end
58
-
59
- def widgets
60
- all('.widget_summary').map do |el|
61
- extract_widget_data(el)
62
- end
63
- end
64
-
65
- def last_widget_created
66
- element = find('.last_widget.created')
67
- extract_widget_data(element)
68
- end
69
-
70
- def choose_to_create_new_widget
71
- click_on 'New Widget'
72
- end
73
-
74
- def choose_to_delete_widget(widget_data)
75
- find("#delete_#{widget_data['id']}").click_button('Delete')
76
- end
77
-
78
- private
79
-
80
- def extract_widget_data(element)
81
- {
82
- 'id' => element.find('.id').text,
83
- 'name' => element.find('.name').text
84
- }
85
- end
86
- end
87
-
88
- class WidgetForm < Kookaburra::UIDriver::UIComponent
89
- def component_locator
90
- '#widget_form'
91
- end
92
-
93
- def submit(widget_data)
94
- fill_in 'Name:', :with => widget_data['name']
95
- click_on 'Save'
96
- end
97
- end
98
-
99
- class MyUIDriver < Kookaburra::UIDriver
100
- ui_component :sign_in_screen, SignInScreen
101
- ui_component :widget_list, WidgetList
102
- ui_component :widget_form, WidgetForm
103
-
104
- def sign_in(name)
105
- sign_in_screen.show
106
- sign_in_screen.sign_in(test_data.users[name])
107
- end
108
-
109
- def create_new_widget(name, attributes = {})
110
- widget_list.show
111
- widget_list.choose_to_create_new_widget
112
- widget_form.submit('name' => 'My Widget')
113
- test_data.widgets[name] = widget_list.last_widget_created
114
- end
115
-
116
- def delete_widget(name)
117
- widget_list.show
118
- widget_list.choose_to_delete_widget(test_data.widgets[name])
119
- end
120
- end
121
-
122
13
  # This is the fixture Rack application against which the integration
123
14
  # test will run. It uses class variables to persist data, because
124
15
  # Sinatra will instantiate a new instance of TestRackApp for each
125
16
  # request.
126
- class TestRackApp < Sinatra::Base
17
+ class JsonApiApp < Sinatra::Base
127
18
  enable :sessions
128
-
129
- # we want error handling to behave as it would for a production
130
- # deployment rather than development
131
- set :raise_errors, false
132
- set :show_exceptions, false
19
+ disable :show_exceptions
133
20
 
134
21
  def parse_json_req_body
135
22
  request.body.rewind
@@ -241,20 +128,20 @@ describe "testing a Rack application with Kookaburra" do
241
128
  </head>
242
129
  <body>
243
130
  <div id="widget_list">
244
- EOF
245
- if last_widget_created
246
- content << <<-EOF
131
+ EOF
132
+ if last_widget_created
133
+ content << <<-EOF
247
134
  <div class="last_widget created">
248
135
  <span class="id">#{last_widget_created[:id]}</span>
249
136
  <span class="name">#{last_widget_created[:name]}</span>
250
137
  </div>
251
- EOF
252
- end
253
- content << <<-EOF
138
+ EOF
139
+ end
140
+ content << <<-EOF
254
141
  <ul>
255
- EOF
256
- @@widgets.each do |w|
257
- content << <<-EOF
142
+ EOF
143
+ @@widgets.each do |w|
144
+ content << <<-EOF
258
145
  <li class="widget_summary">
259
146
  <span class="id">#{w[:id]}</span>
260
147
  <span class="name">#{w[:name]}</span>
@@ -262,9 +149,9 @@ describe "testing a Rack application with Kookaburra" do
262
149
  <button type="submit" value="Delete" />
263
150
  </form>
264
151
  </li>
265
- EOF
266
- end
267
- content << <<-EOF
152
+ EOF
153
+ end
154
+ content << <<-EOF
268
155
  </ul>
269
156
  <a href="/widgets/new">New Widget</a>
270
157
  </div>
@@ -273,22 +160,169 @@ describe "testing a Rack application with Kookaburra" do
273
160
  EOF
274
161
  body content
275
162
  end
163
+
164
+ error do
165
+ e = request.env['sinatra.error']
166
+ body << <<-EOF
167
+ <html>
168
+ <head>
169
+ <title>Internal Server Error</title>
170
+ </head>
171
+ <body>
172
+ <pre>
173
+ #{e.to_s}\n#{e.backtrace.join("\n")}
174
+ </pre>
175
+ </body>
176
+ </html>
177
+ EOF
178
+ end
276
179
  end
277
180
 
181
+ class MyAPIDriver < Kookaburra::JsonApiDriver
182
+ def create_user(user_data)
183
+ post '/users', user_data
184
+ end
278
185
 
279
- it "runs the tests against the app" do
280
- my_app = TestRackApp.new
186
+ def create_widget(widget_data)
187
+ post '/widgets', widget_data
188
+ end
189
+ end
190
+
191
+ class MyGivenDriver < Kookaburra::GivenDriver
192
+ def api
193
+ MyAPIDriver.new(:app_host => initialization_options[:app_host])
194
+ end
195
+
196
+ def a_user(name)
197
+ user = {'email' => 'bob@example.com', 'password' => '12345'}
198
+ result = api.create_user(user)
199
+ mental_model.users[name] = result
200
+ end
201
+
202
+ def a_widget(name, attributes = {})
203
+ widget = {'name' => 'Foo'}.merge(attributes)
204
+ result = api.create_widget(widget)
205
+ mental_model.widgets[name] = result
206
+ end
207
+ end
208
+
209
+ class SignInScreen < Kookaburra::UIDriver::UIComponent
210
+ def component_path
211
+ '/session/new'
212
+ end
213
+
214
+ def component_locator
215
+ '#sign_in_screen'
216
+ end
217
+
218
+ def sign_in(user_data)
219
+ fill_in 'Email:', :with => user_data['email']
220
+ fill_in 'Password:', :with => user_data['password']
221
+ click_button 'Sign In'
222
+ end
223
+ end
224
+
225
+ class WidgetList < Kookaburra::UIDriver::UIComponent
226
+ def component_path
227
+ '/widgets'
228
+ end
229
+
230
+ def component_locator
231
+ '#widget_list'
232
+ end
281
233
 
234
+ def widgets
235
+ all('.widget_summary').map do |el|
236
+ extract_widget_data(el)
237
+ end
238
+ end
239
+
240
+ def last_widget_created
241
+ element = find('.last_widget.created')
242
+ extract_widget_data(element)
243
+ end
244
+
245
+ def choose_to_create_new_widget
246
+ click_on 'New Widget'
247
+ end
248
+
249
+ def choose_to_delete_widget(widget_data)
250
+ find("#delete_#{widget_data['id']}").click_button('Delete')
251
+ end
252
+
253
+ private
254
+
255
+ def extract_widget_data(element)
256
+ {
257
+ 'id' => element.find('.id').text,
258
+ 'name' => element.find('.name').text
259
+ }
260
+ end
261
+ end
262
+
263
+ class WidgetForm < Kookaburra::UIDriver::UIComponent
264
+ def component_locator
265
+ '#widget_form'
266
+ end
267
+
268
+ def submit(widget_data)
269
+ fill_in 'Name:', :with => widget_data['name']
270
+ click_on 'Save'
271
+ end
272
+ end
273
+
274
+ class MyUIDriver < Kookaburra::UIDriver
275
+ ui_component :sign_in_screen, SignInScreen
276
+ ui_component :widget_list, WidgetList
277
+ ui_component :widget_form, WidgetForm
278
+
279
+ def sign_in(name)
280
+ sign_in_screen.show
281
+ sign_in_screen.sign_in(mental_model.users[name])
282
+ end
283
+
284
+ def create_new_widget(name, attributes = {})
285
+ widget_list.show
286
+ widget_list.choose_to_create_new_widget
287
+ widget_form.submit('name' => 'My Widget')
288
+ mental_model.widgets[name] = widget_list.last_widget_created
289
+ end
290
+
291
+ def delete_widget(name)
292
+ widget_list.show
293
+ widget_list.choose_to_delete_widget(mental_model.widgets[name])
294
+ end
295
+ end
296
+
297
+ before(:all) do
298
+ @rack_server_port = 3339
299
+ @rack_server_pid = fork do
300
+ Rack::Server.start(
301
+ :app => JsonApiApp.new,
302
+ :server => :webrick,
303
+ :Host => '127.0.0.1',
304
+ :Port => @rack_server_port,
305
+ :environment => 'production'
306
+ )
307
+ end
308
+ sleep 1 # Give the server a chance to start up.
309
+ end
310
+
311
+ after(:all) do
312
+ Process.kill(9, @rack_server_pid)
313
+ Process.wait
314
+ end
315
+
316
+ it "runs the tests against the app" do
282
317
  server_error_detection = lambda { |browser|
283
- browser.has_css?('h1', :text => 'Internal Server Error')
318
+ browser.has_css?('head title', :text => 'Internal Server Error')
284
319
  }
285
320
 
286
321
  k = Kookaburra.new({
287
322
  :ui_driver_class => MyUIDriver,
288
323
  :given_driver_class => MyGivenDriver,
289
- :api_driver_class => MyAPIDriver,
290
- :browser => Capybara::Session.new(:rack_test, my_app),
291
- :rack_app => my_app,
324
+ :app_host => 'http://127.0.0.1:%d' % @rack_server_port,
325
+ :browser => Capybara::Session.new(:selenium),
292
326
  :server_error_detection => server_error_detection
293
327
  })
294
328