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.
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -1
- data/README.markdown +114 -135
- data/VERSION +1 -1
- data/kookaburra.gemspec +8 -10
- data/lib/kookaburra.rb +22 -31
- data/lib/kookaburra/api_driver.rb +95 -11
- data/lib/kookaburra/given_driver.rb +32 -16
- data/lib/kookaburra/json_api_driver.rb +43 -70
- data/lib/kookaburra/{test_data.rb → mental_model.rb} +15 -9
- data/lib/kookaburra/null_browser.rb +4 -0
- data/lib/kookaburra/test_helpers.rb +2 -4
- data/lib/kookaburra/ui_driver.rb +15 -11
- data/lib/kookaburra/ui_driver/ui_component.rb +22 -5
- data/spec/integration/test_a_rack_application_spec.rb +171 -137
- data/spec/kookaburra/api_driver_spec.rb +126 -0
- data/spec/kookaburra/json_api_driver_spec.rb +85 -30
- data/spec/kookaburra/{test_data_spec.rb → mental_model_spec.rb} +6 -6
- data/spec/kookaburra/ui_driver_spec.rb +5 -3
- data/spec/kookaburra_spec.rb +9 -41
- metadata +9 -11
- data/lib/kookaburra/rack_driver.rb +0 -109
- data/lib/kookaburra/utils/active_record_shared_connection.rb +0 -14
- data/spec/kookaburra/rack_driver_spec.rb +0 -42
@@ -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')
|
data/lib/kookaburra/ui_driver.rb
CHANGED
@@ -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
|
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
|
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 [
|
60
|
-
# instance.
|
61
|
-
# @option options [Kookaburra::
|
62
|
-
# @option options [
|
63
|
-
#
|
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
|
-
@
|
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]
|
74
|
-
dependency_accessor :
|
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 [
|
74
|
-
#
|
75
|
-
# @option options [
|
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
|
-
#
|
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
|
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
|
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
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
138
|
+
EOF
|
139
|
+
end
|
140
|
+
content << <<-EOF
|
254
141
|
<ul>
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
280
|
-
|
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?('
|
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
|
-
:
|
290
|
-
:browser => Capybara::Session.new(:
|
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
|
|