kookaburra 0.18.3 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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