kookaburra 0.22.3 → 0.23.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/README.markdown +18 -11
- data/VERSION +1 -1
- data/kookaburra.gemspec +5 -2
- data/lib/kookaburra.rb +9 -6
- data/lib/kookaburra/configuration.rb +11 -0
- data/lib/kookaburra/mental_model.rb +10 -18
- data/lib/kookaburra/mental_model_matcher.rb +130 -0
- data/lib/kookaburra/test_helpers.rb +22 -0
- data/spec/integration/test_a_rack_application_spec.rb +15 -4
- data/spec/kookaburra/configuration_spec.rb +20 -0
- data/spec/kookaburra/mental_model_matcher_spec.rb +234 -0
- data/spec/kookaburra/mental_model_spec.rb +33 -2
- data/spec/kookaburra/test_helpers_spec.rb +81 -0
- data/spec/kookaburra_spec.rb +6 -12
- metadata +8 -5
data/README.markdown
CHANGED
|
@@ -165,29 +165,34 @@ your own UIComponents. This method exists to verify preconditions and provide
|
|
|
165
165
|
more informative error messages; it is not intended to be used for test
|
|
166
166
|
verifications.
|
|
167
167
|
|
|
168
|
+
`Kookaburra::TestHelpers` provides a convenient way to make assertions about the
|
|
169
|
+
mental model. If you are using Test::Unit, see
|
|
170
|
+
`Kookaburra::TestHelpers#assert_mental_model_of`; for RSpec, see
|
|
171
|
+
`Kookaburra::TestHelpers#match_mental_model_of`.
|
|
172
|
+
|
|
168
173
|
Given the Cucumber scenario above, here is how the test implementation layer
|
|
169
174
|
might look:
|
|
170
175
|
|
|
171
176
|
# step_definitions/various_steps.rb
|
|
172
177
|
|
|
173
178
|
Given "I have an existing account" do
|
|
174
|
-
given.existing_account
|
|
179
|
+
given.existing_account
|
|
175
180
|
end
|
|
176
181
|
|
|
177
182
|
Given "I have previously specified default payment options" do
|
|
178
|
-
given.
|
|
183
|
+
given.default_payment_options_specified
|
|
179
184
|
end
|
|
180
185
|
|
|
181
186
|
Given "I have previously specified default shipping options" do
|
|
182
|
-
given.
|
|
187
|
+
given.default_shipping_options_specified
|
|
183
188
|
end
|
|
184
189
|
|
|
185
190
|
Given "I have an item in my shopping cart" do
|
|
186
|
-
given.an_item_in_my_shopping_cart
|
|
191
|
+
given.an_item_in_my_shopping_cart
|
|
187
192
|
end
|
|
188
193
|
|
|
189
194
|
When "I sign in to my account" do
|
|
190
|
-
ui.sign_in
|
|
195
|
+
ui.sign_in
|
|
191
196
|
end
|
|
192
197
|
|
|
193
198
|
When "I choose to check out" do
|
|
@@ -199,11 +204,13 @@ might look:
|
|
|
199
204
|
end
|
|
200
205
|
|
|
201
206
|
Then "I see that my default payment options will be used" do
|
|
202
|
-
ui.order_summary.payment_options.should
|
|
207
|
+
ui.order_summary.payment_options.should match_mental_model_of(:default_payment_options)
|
|
208
|
+
# Or if you prefer Test::Unit style assertions...
|
|
209
|
+
# assert_mental_model_matches(:default_payment_options, ui.order_summary.payment_options)
|
|
203
210
|
end
|
|
204
211
|
|
|
205
212
|
Then "I see that my default shipping options will be used" do
|
|
206
|
-
ui.order_summary.shipping_options.should
|
|
213
|
+
ui.order_summary.shipping_options.should match_mental_model_of(:default_shipping_options)
|
|
207
214
|
end
|
|
208
215
|
|
|
209
216
|
The step definitions contain neither explicitly shared state (instance
|
|
@@ -281,17 +288,17 @@ share a `MentalModel` instance, which is available to both of them via their
|
|
|
281
288
|
|
|
282
289
|
The `MentalModel` instance will return a `MentalModel::Collection` for any method
|
|
283
290
|
called on the object. The `MentalModel::Collection` object behaves like a `Hash`
|
|
284
|
-
for the most part
|
|
291
|
+
for the most part; however, it will raise a `Kookaburra::UnknownKeyError` if you
|
|
285
292
|
try to access a key that has not yet been assigned a value.
|
|
286
293
|
|
|
287
294
|
Deletions (via `#delete` or `#delete_if`) will actually remove the key/value
|
|
288
|
-
pair from the collection, but add it to a
|
|
295
|
+
pair from the collection, but add it to a sub-collection (available at
|
|
289
296
|
`MentalModel::Collection#deleted`). This reflects the fact that the user's
|
|
290
297
|
mental model of the dataset would also include any intentional exceptions -
|
|
291
|
-
the user will, for example,
|
|
298
|
+
the user will, for example, want to verify that an item they deleted does
|
|
292
299
|
not appear to be available in the system.
|
|
293
300
|
|
|
294
|
-
Here's
|
|
301
|
+
Here's an example of MentalModel behavior:
|
|
295
302
|
|
|
296
303
|
mental_model = MentalModel.new
|
|
297
304
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.23.0
|
data/kookaburra.gemspec
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
Gem::Specification.new do |s|
|
|
7
7
|
s.name = "kookaburra"
|
|
8
|
-
s.version = "0.
|
|
8
|
+
s.version = "0.23.0"
|
|
9
9
|
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
11
|
s.authors = ["John Wilger", "Sam Livingston-Gray", "Ravi Gadad"]
|
|
12
|
-
s.date = "2012-
|
|
12
|
+
s.date = "2012-04-02"
|
|
13
13
|
s.description = "Cucumber + Capybara = Kookaburra? It made sense at the time."
|
|
14
14
|
s.email = "johnwilger@gmail.com"
|
|
15
15
|
s.extra_rdoc_files = [
|
|
@@ -37,6 +37,7 @@ Gem::Specification.new do |s|
|
|
|
37
37
|
"lib/kookaburra/given_driver.rb",
|
|
38
38
|
"lib/kookaburra/json_api_driver.rb",
|
|
39
39
|
"lib/kookaburra/mental_model.rb",
|
|
40
|
+
"lib/kookaburra/mental_model_matcher.rb",
|
|
40
41
|
"lib/kookaburra/test_helpers.rb",
|
|
41
42
|
"lib/kookaburra/ui_driver.rb",
|
|
42
43
|
"lib/kookaburra/ui_driver/ui_component.rb",
|
|
@@ -45,7 +46,9 @@ Gem::Specification.new do |s|
|
|
|
45
46
|
"spec/kookaburra/api_driver_spec.rb",
|
|
46
47
|
"spec/kookaburra/configuration_spec.rb",
|
|
47
48
|
"spec/kookaburra/json_api_driver_spec.rb",
|
|
49
|
+
"spec/kookaburra/mental_model_matcher_spec.rb",
|
|
48
50
|
"spec/kookaburra/mental_model_spec.rb",
|
|
51
|
+
"spec/kookaburra/test_helpers_spec.rb",
|
|
49
52
|
"spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb",
|
|
50
53
|
"spec/kookaburra/ui_driver/ui_component_spec.rb",
|
|
51
54
|
"spec/kookaburra/ui_driver_spec.rb",
|
data/lib/kookaburra.rb
CHANGED
|
@@ -62,10 +62,7 @@ class Kookaburra
|
|
|
62
62
|
@ui ||= @ui_driver_class.new(@configuration)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
# Returns a
|
|
66
|
-
# However, this is neither a deep copy nor a deep freeze, so it is possible
|
|
67
|
-
# that you could modify data outside of your GivenDriver or UIDriver. Just
|
|
68
|
-
# don't do that. Trust me.
|
|
65
|
+
# Returns a deep-dup of the specified {MentalModel::Collection}.
|
|
69
66
|
#
|
|
70
67
|
# This access is provided so that you can reference the current mental model
|
|
71
68
|
# within your test implementation and make assertions about the state
|
|
@@ -74,10 +71,16 @@ class Kookaburra
|
|
|
74
71
|
# @example
|
|
75
72
|
# given.a_widget(:foo)
|
|
76
73
|
# ui.create_a_new_widget(:bar)
|
|
77
|
-
# ui.widget_list.widgets.should == k.get_data(:widgets).
|
|
74
|
+
# ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:foo, :bar)
|
|
78
75
|
#
|
|
79
76
|
# @return [Kookaburra::MentalModel::Collection]
|
|
80
77
|
def get_data(collection_name)
|
|
81
|
-
@configuration.mental_model.send(collection_name).dup
|
|
78
|
+
@configuration.mental_model.send(collection_name).dup
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def __mental_model__
|
|
84
|
+
@configuration.mental_model
|
|
82
85
|
end
|
|
83
86
|
end
|
|
@@ -66,5 +66,16 @@ class Kookaburra
|
|
|
66
66
|
@server_error_detection
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
|
+
|
|
70
|
+
# The parsed version of the {#app_host}
|
|
71
|
+
#
|
|
72
|
+
# This is useful if, for example, you are testing a multi-host application
|
|
73
|
+
# and need to change the hostname that will be accessed but want to keep the
|
|
74
|
+
# originally-specified port
|
|
75
|
+
#
|
|
76
|
+
# @return [URI] A URI object created from the {#app_host} string
|
|
77
|
+
def app_host_uri
|
|
78
|
+
URI.parse(app_host)
|
|
79
|
+
end
|
|
69
80
|
end
|
|
70
81
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'delegate'
|
|
2
2
|
require 'kookaburra/exceptions'
|
|
3
|
+
require 'active_support/core_ext/hash'
|
|
3
4
|
|
|
4
5
|
class Kookaburra
|
|
5
6
|
# Each instance of {Kookaburra} has its own instance of MentalModel. This object
|
|
@@ -50,11 +51,13 @@ class Kookaburra
|
|
|
50
51
|
class Collection < SimpleDelegator
|
|
51
52
|
# @param [String] name The name of the collection. Used to provide
|
|
52
53
|
# helpful error messages when unknown keys are accessed.
|
|
53
|
-
|
|
54
|
+
# @param [Hash] init_data Preloads specific data into the collection
|
|
55
|
+
def initialize(name, init_data = nil)
|
|
54
56
|
@name = name
|
|
55
57
|
data = Hash.new do |hash, key|
|
|
56
58
|
raise UnknownKeyError, "Can't find mental_model.#{@name}[#{key.inspect}]. Did you forget to set it?"
|
|
57
59
|
end
|
|
60
|
+
data.merge!(init_data) unless init_data.nil?
|
|
58
61
|
super(data)
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -66,23 +69,6 @@ class Kookaburra
|
|
|
66
69
|
self.object_id == other.object_id
|
|
67
70
|
end
|
|
68
71
|
|
|
69
|
-
# Returns the values of multiple keys from the collection.
|
|
70
|
-
#
|
|
71
|
-
# Unlike the `Hash#slice` implementation provided by ActiveSupport, this
|
|
72
|
-
# method returns an array of the values rather than a Hash.
|
|
73
|
-
#
|
|
74
|
-
# @param keys a list of keys to fetch from the collection.
|
|
75
|
-
#
|
|
76
|
-
# @return [Array] the values matching the specified index keys
|
|
77
|
-
#
|
|
78
|
-
# @raise [Kookaburra::UnknownKeyError] if any of the specified keys have
|
|
79
|
-
# not been set
|
|
80
|
-
def slice(*keys)
|
|
81
|
-
results = keys.map do |key|
|
|
82
|
-
self[key]
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
72
|
# Deletes a key/value pair from the collection, and persists the deleted pair
|
|
87
73
|
# in a subcollection.
|
|
88
74
|
#
|
|
@@ -121,6 +107,12 @@ class Kookaburra
|
|
|
121
107
|
move = lambda { |k,v| deleted[k] = v; true }
|
|
122
108
|
super { |k,v| block.call(k,v) && move.call(k,v) }
|
|
123
109
|
end
|
|
110
|
+
|
|
111
|
+
def dup
|
|
112
|
+
new_data = {}.merge(self)
|
|
113
|
+
new_data = Marshal.load(Marshal.dump(new_data))
|
|
114
|
+
self.class.new(@name, new_data)
|
|
115
|
+
end
|
|
124
116
|
end
|
|
125
117
|
end
|
|
126
118
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
require 'kookaburra/mental_model'
|
|
2
|
+
|
|
3
|
+
class Kookaburra
|
|
4
|
+
class MentalModel
|
|
5
|
+
# This is a custom matcher that matches the RSpec matcher API.
|
|
6
|
+
#
|
|
7
|
+
# @see Kookaburra::TestHelpers#match_mental_model_of
|
|
8
|
+
# @see Kookaburra::TestHelpers#assert_mental_model_of
|
|
9
|
+
class Matcher
|
|
10
|
+
def initialize(mental_model, collection_key)
|
|
11
|
+
@collection_key = collection_key
|
|
12
|
+
|
|
13
|
+
mental_model.send(collection_key).tap do |collection|
|
|
14
|
+
@expected = collection
|
|
15
|
+
@unexpected = collection.deleted
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Specifies that result should be limited to the given keys from the
|
|
20
|
+
# mental model.
|
|
21
|
+
#
|
|
22
|
+
# Useful if you are looking at a filtered result. That is, your mental
|
|
23
|
+
# model contains elements { A, B, C }, but you only expect to see element
|
|
24
|
+
# A.
|
|
25
|
+
#
|
|
26
|
+
# @param [Array] collection_keys The keys used in your mental model to
|
|
27
|
+
# reference the data
|
|
28
|
+
# @return [self]
|
|
29
|
+
def only(*collection_keys)
|
|
30
|
+
keepers = @expected.slice(*collection_keys)
|
|
31
|
+
tossers = @expected.except(*collection_keys)
|
|
32
|
+
|
|
33
|
+
@expected = keepers
|
|
34
|
+
@unexpected.merge! tossers
|
|
35
|
+
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Reads better than {#only} with no args
|
|
40
|
+
#
|
|
41
|
+
# @return [self]
|
|
42
|
+
def expecting_nothing
|
|
43
|
+
only
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The result contains everything that was expected to be found and nothing
|
|
47
|
+
# that was unexpected.
|
|
48
|
+
#
|
|
49
|
+
# (Part of the RSpec protocol for custom matchers.)
|
|
50
|
+
#
|
|
51
|
+
# @param [Array] actual This is the data observed that you are attempting
|
|
52
|
+
# to match against the mental model.
|
|
53
|
+
# @return Boolean
|
|
54
|
+
def matches?(actual)
|
|
55
|
+
@actual = actual
|
|
56
|
+
expected_items_not_found.empty? && unexpected_items_found.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Message to be printed when observed reality does not conform to
|
|
60
|
+
# mental model.
|
|
61
|
+
#
|
|
62
|
+
# (Part of the RSpec protocol for custom matchers.)
|
|
63
|
+
#
|
|
64
|
+
# @return String
|
|
65
|
+
def failure_message_for_should
|
|
66
|
+
message = "expected #{@collection_key} to match the user's mental model, but:\n"
|
|
67
|
+
if expected_items_not_found.present?
|
|
68
|
+
message += "expected to be present: #{pp_array(expected_items)}\n"
|
|
69
|
+
message += "the missing elements were: #{pp_array(expected_items_not_found)}\n"
|
|
70
|
+
end
|
|
71
|
+
if unexpected_items_found.present?
|
|
72
|
+
message += "expected to not be present: #{pp_array(unexpected_items)}\n"
|
|
73
|
+
message += "the unexpected extra elements: #{pp_array(unexpected_items_found)}\n"
|
|
74
|
+
end
|
|
75
|
+
message
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Message to be printed when observed reality does conform to mental
|
|
79
|
+
# model, but you did not expect it to. (To be honest, we can't think of
|
|
80
|
+
# why you would want this, but it is included for the sake of RSpec
|
|
81
|
+
# compatibility.)
|
|
82
|
+
#
|
|
83
|
+
# (Part of the RSpec protocol for custom matchers.)
|
|
84
|
+
#
|
|
85
|
+
# @return String
|
|
86
|
+
def failure_message_for_should_not
|
|
87
|
+
"expected #{@collection_key} not to match the user's mental model"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# (Part of the RSpec protocol for custom matchers.)
|
|
91
|
+
#
|
|
92
|
+
# @return String
|
|
93
|
+
def description
|
|
94
|
+
"match the user's mental model of #{@collection_key}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def expected_items; @expected.values; end
|
|
100
|
+
def unexpected_items; @unexpected.values; end
|
|
101
|
+
|
|
102
|
+
def expected_items_not_found
|
|
103
|
+
difference_between_arrays(expected_items, @actual)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def unexpected_items_found
|
|
107
|
+
unexpected_items_not_found = difference_between_arrays(unexpected_items, @actual)
|
|
108
|
+
difference_between_arrays(unexpected_items, unexpected_items_not_found)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# (Swiped from RSpec's array matcher)
|
|
112
|
+
# Returns the difference of arrays, accounting for duplicates.
|
|
113
|
+
# e.g., difference_between_arrays([1, 2, 3, 3], [1, 2, 3]) # => [3]
|
|
114
|
+
def difference_between_arrays(array_1, array_2)
|
|
115
|
+
difference = array_1.dup
|
|
116
|
+
array_2.each do |element|
|
|
117
|
+
if index = difference.index(element)
|
|
118
|
+
difference.delete_at(index)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
difference
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pp_array(array)
|
|
125
|
+
array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
|
|
126
|
+
array.inspect
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'kookaburra'
|
|
2
|
+
require 'kookaburra/mental_model_matcher'
|
|
2
3
|
require 'active_support/core_ext/module/delegation'
|
|
3
4
|
|
|
4
5
|
class Kookaburra
|
|
@@ -96,5 +97,26 @@ class Kookaburra
|
|
|
96
97
|
# @method ui
|
|
97
98
|
# Delegates to {#k}
|
|
98
99
|
delegate :ui, :to => :k
|
|
100
|
+
|
|
101
|
+
# RSpec-style custom matcher that compares a given array with
|
|
102
|
+
# the current state of one named collection in the mental model
|
|
103
|
+
#
|
|
104
|
+
# @see Kookaburra::MentalModel::Matcher
|
|
105
|
+
def match_mental_model_of(collection_key)
|
|
106
|
+
MentalModel::Matcher.new(k.send(:__mental_model__), collection_key)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Custom assertion for Test::Unit-style tests
|
|
110
|
+
# (really, anything that uses #assert(predicate, message = nil))
|
|
111
|
+
#
|
|
112
|
+
# @see Kookaburra::MentalModel::Matcher
|
|
113
|
+
def assert_mental_model_matches(collection_key, actual, message = nil)
|
|
114
|
+
matcher = match_mental_model_of(collection_key)
|
|
115
|
+
result = matcher.matches?(actual)
|
|
116
|
+
return if !!result # don't even bother
|
|
117
|
+
|
|
118
|
+
message ||= matcher.failure_message_for_should
|
|
119
|
+
assert result, message
|
|
120
|
+
end
|
|
99
121
|
end
|
|
100
122
|
end
|
|
@@ -294,7 +294,7 @@ describe "testing a Rack application with Kookaburra" do
|
|
|
294
294
|
|
|
295
295
|
def delete_widget(name)
|
|
296
296
|
assert widget_list.visible?, "Widget list is not visible!"
|
|
297
|
-
widget_list.choose_to_delete_widget(mental_model.widgets
|
|
297
|
+
widget_list.choose_to_delete_widget(mental_model.widgets.delete(name))
|
|
298
298
|
end
|
|
299
299
|
end
|
|
300
300
|
|
|
@@ -336,13 +336,24 @@ describe "testing a Rack application with Kookaburra" do
|
|
|
336
336
|
|
|
337
337
|
ui.sign_in(:bob)
|
|
338
338
|
ui.view_widget_list
|
|
339
|
-
|
|
339
|
+
|
|
340
|
+
# The following two lines are two different ways to shave the yak, but
|
|
341
|
+
# the second one does more to match against the full state of the mental
|
|
342
|
+
# model, provides better failure messages, and is shorter.
|
|
343
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b)
|
|
344
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
|
340
345
|
|
|
341
346
|
ui.create_new_widget(:widget_c, :name => 'Bar')
|
|
342
|
-
|
|
347
|
+
|
|
348
|
+
# As above, these are equivalent, but the second line is preferred.
|
|
349
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b, :widget_c)
|
|
350
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
|
343
351
|
|
|
344
352
|
ui.delete_widget(:widget_b)
|
|
345
|
-
|
|
353
|
+
|
|
354
|
+
# As above, these are equivalent, but the second line is preferred.
|
|
355
|
+
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_c)
|
|
356
|
+
ui.widget_list.widgets.should match_mental_model_of(:widgets)
|
|
346
357
|
end
|
|
347
358
|
end
|
|
348
359
|
end
|
|
@@ -15,4 +15,24 @@ describe Kookaburra::Configuration do
|
|
|
15
15
|
subject.server_error_detection.should == block
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
describe '#app_host_uri' do
|
|
20
|
+
it 'returns a URI version of the #app_host attribute via URI.parse' do
|
|
21
|
+
URI.should_receive(:parse) \
|
|
22
|
+
.with('http://example.com') \
|
|
23
|
+
.and_return(:a_parsed_uri)
|
|
24
|
+
subject.app_host = 'http://example.com'
|
|
25
|
+
subject.app_host_uri.should == :a_parsed_uri
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'changes if #app_host changes' do
|
|
29
|
+
URI.stub!(:parse) do |url|
|
|
30
|
+
url.to_sym
|
|
31
|
+
end
|
|
32
|
+
subject.app_host = 'http://example.com'
|
|
33
|
+
subject.app_host_uri.should == 'http://example.com'.to_sym
|
|
34
|
+
subject.app_host = 'http://foo.example.com'
|
|
35
|
+
subject.app_host_uri.should == 'http://foo.example.com'.to_sym
|
|
36
|
+
end
|
|
37
|
+
end
|
|
18
38
|
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
require 'kookaburra/mental_model_matcher'
|
|
2
|
+
|
|
3
|
+
# Makes the specs themselves a bit less verbose. You should probably read the
|
|
4
|
+
# specs first, though.
|
|
5
|
+
module MentalModelMatcherMacros
|
|
6
|
+
def self.included(receiver)
|
|
7
|
+
receiver.extend ClassMethods
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def pp_array(array)
|
|
12
|
+
array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
|
|
13
|
+
array.inspect
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def it_matches
|
|
17
|
+
it "matches" do
|
|
18
|
+
matcher.matches?(target).should be_true
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def it_doesnt_match
|
|
23
|
+
it "doesn't match" do
|
|
24
|
+
matcher.matches?(target).should be_false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def from_line
|
|
29
|
+
'(line %d)' % caller(2).first.split(':').last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def it_complains_about_missing(missing, options)
|
|
33
|
+
expected = options[:expected]
|
|
34
|
+
it "complains about a mismatch #{from_line}" do
|
|
35
|
+
failure_msg.should include("expected widgets to match the user's mental model, but:")
|
|
36
|
+
end
|
|
37
|
+
it "says what it expected to be present #{from_line}" do
|
|
38
|
+
failure_msg.should include("expected to be present: #{pp_array(expected)}")
|
|
39
|
+
end
|
|
40
|
+
it "complains about missing items #{from_line}" do
|
|
41
|
+
failure_msg.should include("the missing elements were: #{pp_array(missing)}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def it_complains_about_extra(extra, options)
|
|
46
|
+
unexpected = options[:unexpected]
|
|
47
|
+
it "complains about a mismatch #{from_line}" do
|
|
48
|
+
failure_msg.should include("expected widgets to match the user's mental model, but:")
|
|
49
|
+
end
|
|
50
|
+
it "says what it expected not to find #{from_line}" do
|
|
51
|
+
failure_msg.should include("expected to not be present: #{pp_array(unexpected)}")
|
|
52
|
+
end
|
|
53
|
+
it "complains about extra items #{from_line}" do
|
|
54
|
+
failure_msg.should include("the unexpected extra elements: #{pp_array(extra)}")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def matcher_for(collection_key)
|
|
60
|
+
Kookaburra::MentalModel::Matcher.new(mm, collection_key).tap do |m|
|
|
61
|
+
m.matches?(target)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pp_array(array)
|
|
66
|
+
self.class.pp_array(array)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe Kookaburra::MentalModel::Matcher do
|
|
71
|
+
include MentalModelMatcherMacros
|
|
72
|
+
|
|
73
|
+
let(:mm) { Kookaburra::MentalModel.new }
|
|
74
|
+
let(:matcher) { matcher_for(:widgets) }
|
|
75
|
+
let(:failure_msg) { matcher.failure_message_for_should }
|
|
76
|
+
|
|
77
|
+
def self.foo; 'FOO' ; end
|
|
78
|
+
def self.bar; 'BAR' ; end
|
|
79
|
+
def self.yak; 'YAK' ; end
|
|
80
|
+
let(:foo) { self.class.foo }
|
|
81
|
+
let(:bar) { self.class.bar }
|
|
82
|
+
let(:yak) { self.class.yak }
|
|
83
|
+
|
|
84
|
+
context "when mental model is [foo];" do
|
|
85
|
+
before(:each) do
|
|
86
|
+
mm.widgets[:foo] = foo
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context "for [] (foo missing)" do
|
|
90
|
+
let(:target) { [] }
|
|
91
|
+
it_doesnt_match
|
|
92
|
+
it_complains_about_missing [foo], :expected => [foo]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
context "for [foo] (OK: exact match)" do
|
|
96
|
+
let(:target) { [foo] }
|
|
97
|
+
it_matches
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
context "for [foo, bar] (OK: bar ignored)" do
|
|
101
|
+
let(:target) { [foo, bar] }
|
|
102
|
+
it_matches
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
context "when mental model is [];" do
|
|
107
|
+
context "for [] (OK)" do
|
|
108
|
+
let(:target) { [] }
|
|
109
|
+
it_matches
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
context "for [foo] (OK: foo ignored)" do
|
|
113
|
+
let(:target) { [foo] }
|
|
114
|
+
it_matches
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context "when mental model is [foo, bar];" do
|
|
119
|
+
before(:each) do
|
|
120
|
+
mm.widgets[:foo] = foo
|
|
121
|
+
mm.widgets[:bar] = bar
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context "for [] (foo, bar missing)" do
|
|
125
|
+
let(:target) { [] }
|
|
126
|
+
it_doesnt_match
|
|
127
|
+
it_complains_about_missing [foo, bar], :expected => [foo, bar]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context "for [foo] (bar missing)" do
|
|
131
|
+
let(:target) { [foo] }
|
|
132
|
+
it_doesnt_match
|
|
133
|
+
it_complains_about_missing [bar], :expected => [foo, bar]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
context "for [foo, bar] (OK: exact match)" do
|
|
137
|
+
let(:target) { [foo, bar] }
|
|
138
|
+
it_matches
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context "for [foo, bar, yak] (OK: foo, bar expected; yak ignored)" do
|
|
142
|
+
let(:target) { [foo, bar, yak] }
|
|
143
|
+
it_matches
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
context "when mental model is [foo], not expecting [bar];" do
|
|
148
|
+
before(:each) do
|
|
149
|
+
mm.widgets[:foo] = foo
|
|
150
|
+
mm.widgets[:bar] = bar
|
|
151
|
+
mm.widgets.delete(:bar)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context "for [] (foo missing)" do
|
|
155
|
+
let(:target) { [] }
|
|
156
|
+
it_doesnt_match
|
|
157
|
+
it_complains_about_missing [foo], :expected => [foo]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context "for [bar] (foo missing, bar deleted)" do
|
|
161
|
+
let(:target) { [bar] }
|
|
162
|
+
it_doesnt_match
|
|
163
|
+
it_complains_about_missing [foo], :expected => [foo]
|
|
164
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
context "for [foo, bar] (bar deleted)" do
|
|
168
|
+
let(:target) { [foo, bar] }
|
|
169
|
+
it_doesnt_match
|
|
170
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
context "for [foo] (OK: foo expected, bar not found)" do
|
|
174
|
+
let(:target) { [foo] }
|
|
175
|
+
it_matches
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
context "for [foo, yak] (OK: foo expected; yak ignored)" do
|
|
179
|
+
let(:target) { [foo, yak] }
|
|
180
|
+
it_matches
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe "postfix scoping methods" do
|
|
185
|
+
context "when mental model is [foo, bar];" do
|
|
186
|
+
before(:each) do
|
|
187
|
+
mm.widgets[:foo] = foo
|
|
188
|
+
mm.widgets[:bar] = bar
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
context "but scoped to .only(:foo)" do
|
|
192
|
+
let(:matcher) { matcher_for(:widgets).only(:foo) }
|
|
193
|
+
|
|
194
|
+
context "for [foo] (OK)" do
|
|
195
|
+
let(:target) { [foo] }
|
|
196
|
+
it_matches
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
context "for [foo, bar] (not expecting [bar])" do
|
|
200
|
+
let(:target) { [foo, bar] }
|
|
201
|
+
it_doesnt_match
|
|
202
|
+
it_complains_about_extra [bar], :unexpected => [bar]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
context "when mental model is [foo];" do
|
|
208
|
+
before(:each) do
|
|
209
|
+
mm.widgets[:foo] = foo
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context "but scoped with .expecting_nothing" do
|
|
213
|
+
let(:matcher) { matcher_for(:widgets).expecting_nothing }
|
|
214
|
+
|
|
215
|
+
context "for [] (OK)" do
|
|
216
|
+
let(:target) { [] }
|
|
217
|
+
it_matches
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
context "for [foo] (unexpected foo)" do
|
|
221
|
+
let(:target) { [foo] }
|
|
222
|
+
it_doesnt_match
|
|
223
|
+
it_complains_about_extra [foo], :unexpected => [foo]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
context "for [foo, bar] (unexpected [foo]; bar ignored)" do
|
|
227
|
+
let(:target) { [foo, bar] }
|
|
228
|
+
it_doesnt_match
|
|
229
|
+
it_complains_about_extra [foo], :unexpected => [foo]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -14,12 +14,21 @@ describe Kookaburra::MentalModel do
|
|
|
14
14
|
describe Kookaburra::MentalModel::Collection do
|
|
15
15
|
let(:collection) { Kookaburra::MentalModel::Collection.new('widgets') }
|
|
16
16
|
|
|
17
|
-
describe '#
|
|
17
|
+
describe '#values_at' do
|
|
18
18
|
it 'returns an array of items matching the specified keys' do
|
|
19
19
|
collection[:foo] = 'foo'
|
|
20
20
|
collection[:bar] = 'bar'
|
|
21
21
|
collection[:baz] = 'baz'
|
|
22
|
-
collection.
|
|
22
|
+
collection.values_at(:foo, :baz).should == %w(foo baz)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe '#slice' do
|
|
27
|
+
it 'returns a hash of items matching the specified keys' do
|
|
28
|
+
collection[:foo] = 'foo'
|
|
29
|
+
collection[:bar] = 'bar'
|
|
30
|
+
collection[:baz] = 'baz'
|
|
31
|
+
collection.slice(:foo, :baz).should == { :foo => 'foo', :baz => 'baz' }
|
|
23
32
|
end
|
|
24
33
|
end
|
|
25
34
|
|
|
@@ -81,5 +90,27 @@ describe Kookaburra::MentalModel do
|
|
|
81
90
|
lambda { collection[:foo] }.should \
|
|
82
91
|
raise_error(Kookaburra::UnknownKeyError, "Can't find mental_model.widgets[:foo]. Did you forget to set it?")
|
|
83
92
|
end
|
|
93
|
+
|
|
94
|
+
describe '#dup' do
|
|
95
|
+
it 'returns a different object' do
|
|
96
|
+
new_collection = collection.dup
|
|
97
|
+
new_collection.__id__.should_not === collection.__id__
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns an object with equal values to the original' do
|
|
101
|
+
collection[:foo] = :bar
|
|
102
|
+
collection[:baz] = :bam
|
|
103
|
+
new_collection = collection.dup
|
|
104
|
+
new_collection[:foo].should == :bar
|
|
105
|
+
new_collection[:baz].should == :bam
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'is a deep copy' do
|
|
109
|
+
collection[:foo] = {:bar => 'baz'}
|
|
110
|
+
new_collection = collection.dup
|
|
111
|
+
new_collection[:foo][:bar].should == 'baz'
|
|
112
|
+
new_collection[:foo][:bar].__id__.should_not === collection[:foo][:bar].__id__
|
|
113
|
+
end
|
|
114
|
+
end
|
|
84
115
|
end
|
|
85
116
|
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'kookaburra/test_helpers'
|
|
2
|
+
|
|
3
|
+
describe Kookaburra::TestHelpers do
|
|
4
|
+
include Kookaburra::TestHelpers
|
|
5
|
+
|
|
6
|
+
before(:all) do
|
|
7
|
+
Kookaburra.configure do |c|
|
|
8
|
+
c.given_driver_class = Kookaburra::GivenDriver
|
|
9
|
+
c.ui_driver_class = Kookaburra::UIDriver
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after(:all) do
|
|
14
|
+
Kookaburra.configure do |c|
|
|
15
|
+
c.given_driver_class = nil
|
|
16
|
+
c.ui_driver_class = nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe "#k" do
|
|
21
|
+
it "returns an instance of Kookaburra" do
|
|
22
|
+
k.should be_kind_of(Kookaburra)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "memoizes" do
|
|
26
|
+
a = k; b = k
|
|
27
|
+
a.should equal(b)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "methods delegated to #k" do
|
|
32
|
+
it "includes #given" do
|
|
33
|
+
k.should_receive(:given)
|
|
34
|
+
given
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "includes #ui" do
|
|
38
|
+
k.should_receive(:ui)
|
|
39
|
+
ui
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "methods related to the mental model" do
|
|
44
|
+
before(:each) do
|
|
45
|
+
mm = k.send(:__mental_model__)
|
|
46
|
+
mm.widgets[:foo] = 'FOO'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
describe "#match_mental_model_of" do
|
|
50
|
+
it "does a positive match" do
|
|
51
|
+
['FOO'].should match_mental_model_of(:widgets)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "does a negative match" do
|
|
55
|
+
['BAR'].should_not match_mental_model_of(:widgets)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "#assert_mental_model_matches" do
|
|
60
|
+
it "does a positive assertion" do
|
|
61
|
+
actual = ['FOO']
|
|
62
|
+
actual.should match_mental_model_of(:widgets) # Sanity check
|
|
63
|
+
self.should_receive(:assert).never
|
|
64
|
+
self.assert_mental_model_matches(:widgets, actual)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "does a negative assertion" do
|
|
68
|
+
actual = ['BAR']
|
|
69
|
+
self.should_receive(:assert).with(false, kind_of(String))
|
|
70
|
+
self.assert_mental_model_matches(:widgets, actual)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "does a negative assertion with a custom message" do
|
|
74
|
+
actual = ['YAK']
|
|
75
|
+
psa = 'Put the razor down and step away!'
|
|
76
|
+
self.should_receive(:assert).with(false, psa)
|
|
77
|
+
self.assert_mental_model_matches(:widgets, actual, psa)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/spec/kookaburra_spec.rb
CHANGED
|
@@ -30,18 +30,12 @@ describe Kookaburra do
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
describe '#get_data' do
|
|
33
|
-
it 'returns a
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
it 'does not return the same object that is the test data collection' do
|
|
40
|
-
k.get_data(:foos).should_not === k.get_data(:foos)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
it 'returns a frozen object' do
|
|
44
|
-
k.get_data(:foos).should be_frozen
|
|
33
|
+
it 'returns a dup of the specified MentalModel::Collection' do
|
|
34
|
+
collection = stub('MentalModel::Collection')
|
|
35
|
+
collection.should_receive(:dup) \
|
|
36
|
+
.and_return(:mental_model_collection_dup)
|
|
37
|
+
configuration.stub!(:mental_model => stub(:foos => collection))
|
|
38
|
+
k.get_data(:foos).should == :mental_model_collection_dup
|
|
45
39
|
end
|
|
46
40
|
end
|
|
47
41
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kookaburra
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
hash:
|
|
4
|
+
hash: 67
|
|
5
5
|
prerelease:
|
|
6
6
|
segments:
|
|
7
7
|
- 0
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
version: 0.
|
|
8
|
+
- 23
|
|
9
|
+
- 0
|
|
10
|
+
version: 0.23.0
|
|
11
11
|
platform: ruby
|
|
12
12
|
authors:
|
|
13
13
|
- John Wilger
|
|
@@ -17,7 +17,7 @@ autorequire:
|
|
|
17
17
|
bindir: bin
|
|
18
18
|
cert_chain: []
|
|
19
19
|
|
|
20
|
-
date: 2012-
|
|
20
|
+
date: 2012-04-02 00:00:00 Z
|
|
21
21
|
dependencies:
|
|
22
22
|
- !ruby/object:Gem::Dependency
|
|
23
23
|
prerelease: false
|
|
@@ -205,6 +205,7 @@ files:
|
|
|
205
205
|
- lib/kookaburra/given_driver.rb
|
|
206
206
|
- lib/kookaburra/json_api_driver.rb
|
|
207
207
|
- lib/kookaburra/mental_model.rb
|
|
208
|
+
- lib/kookaburra/mental_model_matcher.rb
|
|
208
209
|
- lib/kookaburra/test_helpers.rb
|
|
209
210
|
- lib/kookaburra/ui_driver.rb
|
|
210
211
|
- lib/kookaburra/ui_driver/ui_component.rb
|
|
@@ -213,7 +214,9 @@ files:
|
|
|
213
214
|
- spec/kookaburra/api_driver_spec.rb
|
|
214
215
|
- spec/kookaburra/configuration_spec.rb
|
|
215
216
|
- spec/kookaburra/json_api_driver_spec.rb
|
|
217
|
+
- spec/kookaburra/mental_model_matcher_spec.rb
|
|
216
218
|
- spec/kookaburra/mental_model_spec.rb
|
|
219
|
+
- spec/kookaburra/test_helpers_spec.rb
|
|
217
220
|
- spec/kookaburra/ui_driver/ui_component/address_bar_spec.rb
|
|
218
221
|
- spec/kookaburra/ui_driver/ui_component_spec.rb
|
|
219
222
|
- spec/kookaburra/ui_driver_spec.rb
|