Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Scoping and filtering methods, and more helpful UnexpectedResponse #9

Closed
wants to merge 4 commits into from

3 participants

@ravigadad

1) Added an optional status code as an attribute of an UnexpectedResponse, since in most cases this exception is thrown by the APIDriver, and it's helpful to know why the request failed. Especially useful when the response was actually a 500 error instead of a 404, for example.

2) Added #where and #mapped_by methods to MentalModel::Matcher, for scoping based on a select block, and modification of collection elements using a map block.

ravigadad added some commits
@ravigadad ravigadad (rg) Add status code to UnexpectedResponse
When an APIDriver request results in an unexpected response, it's extremely helpful to know what the response
was (especially in the case of 500 errors).  Adding this to the exception.
b0ecaf5
@ravigadad ravigadad (rg) Add .where and .mapped_by to Matcher
Allow for filtering using a given block, with MentalModel::Matcher#where, and mapping using a given block, with
MentalModel::Matcher#mapped_by.
109c51a
@ravigadad ravigadad (rg) Assertion helper uses scopers/filters
Now we can use scopers (.where, .only) and filters (.mapped_by) with #assert-compatible testing frameworks as
well as with RSpec.
73bbc13
@ravigadad ravigadad (rg) New Matcher usage to fix timing issues
You can now use the MentalModel::Matcher in a new way - by matching against an object that returns the observed data,
rather than matching against an array of the observed data itself.  This enables us to, within the matcher itself,
retry failures for a period of time, to account for possible rendering delays (such as Javascript-rendered elements)
999bdee
@jwilger jwilger commented on the diff
lib/kookaburra/api_driver.rb
@@ -82,7 +82,7 @@ def check_response_status!(request_type, response, options)
verb, default_status = verb_map[request_type]
expected_status = options[:expected_response_status] || default_status
unless expected_status == response.status
- raise UnexpectedResponse, "#{verb} to #{response.url} responded with " \
+ raise UnexpectedResponse.new(response.status), "#{verb} to #{response.url} responded with " \
@jwilger Owner
jwilger added a note

I was under the impression that in raise Exception, message the "message" part was simply passed into Exception.new, so not sure what is happening to message on lines where this is raised now. Maybe I'm wrong?

Yeah, this is due to Ruby's wacky implementation of Kernel.raise. If you give it an exception class, the initializer is called with the message as the first argument, but the message is also stored on the exception. If you have an #initialize for your exception, you may not want the message as a param - in which case you explicitly create the instance of the Exception. The message is still stored on the exception somehow (possibly using a separate setter method), though according to what I read here: http://www.ruby-forum.com/topic/207448, the message and backtrace (optional third param to raise) are tucked away in inaccessible instance variables. When you call #message on the exception, the message is returned even if it wasn't part of the initialization, since #message uses #to_s which uses the internal variables. Magic! And yes, this has been raised as an issue, and debated: http://bugs.ruby-lang.org/issues/5898

(which isn't to say I agree with the issue as it was raised - it just seems to be another instance where the confusing dichotomy of Exception.new and Kernel.raise cause problems).

@jwilger Owner
jwilger added a note

That could work - in which case, I think we should use a different class of exception for the detect_server_error! exception. Unless we assume that a detected server error should always show up as a 500. I think UnexpectedResponse should always be expected to indicate the actual response.

At any rate, that sounds like a refactoring - the message is received the same way whether the exception constructor creates it or it's set by the raise call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jwilger
Owner

I will give a more detailed review later, just wanted to note the exception issue that caught my eye.

@ravigadad ravigadad commented on the diff
lib/kookaburra/ui_driver/ui_component.rb
@@ -149,7 +149,7 @@ def component_locator
# function returns true
def detect_server_error!
if @server_error_detection.try(:call, browser)
- raise UnexpectedResponse, "Your server error detection function detected a server error. Looks like your applications is busted. :-("
+ raise UnexpectedResponse.new, "Your server error detection function detected a server error. Looks like your application is busted. :-("

Note this change, related to your comment on the call to Kernel.raise in APIDriver. Here I had to pass an instance of the exception, rather than the class - otherwise, the new exception would have the message set as the status_code (as well as the return from #message, even though they're completely unrelated), because the message is sent in as the first argument to initialize, when raise is given a class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@ravigadad

Reorganized pull requests to get timing fix into projectdx/kookaburra master.

@ravigadad ravigadad closed this
@geeksam

Then why aren't there two objects?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 23, 2012
  1. @ravigadad

    (rg) Add status code to UnexpectedResponse

    ravigadad authored
    When an APIDriver request results in an unexpected response, it's extremely helpful to know what the response
    was (especially in the case of 500 errors).  Adding this to the exception.
  2. @ravigadad

    (rg) Add .where and .mapped_by to Matcher

    ravigadad authored
    Allow for filtering using a given block, with MentalModel::Matcher#where, and mapping using a given block, with
    MentalModel::Matcher#mapped_by.
  3. @ravigadad

    (rg) Assertion helper uses scopers/filters

    ravigadad authored
    Now we can use scopers (.where, .only) and filters (.mapped_by) with #assert-compatible testing frameworks as
    well as with RSpec.
  4. @ravigadad

    (rg) New Matcher usage to fix timing issues

    ravigadad authored
    You can now use the MentalModel::Matcher in a new way - by matching against an object that returns the observed data,
    rather than matching against an array of the observed data itself.  This enables us to, within the matcher itself,
    retry failures for a period of time, to account for possible rendering delays (such as Javascript-rendered elements)
This page is out of date. Refresh to see the latest.
View
2  lib/kookaburra/api_driver.rb
@@ -82,7 +82,7 @@ def check_response_status!(request_type, response, options)
verb, default_status = verb_map[request_type]
expected_status = options[:expected_response_status] || default_status
unless expected_status == response.status
- raise UnexpectedResponse, "#{verb} to #{response.url} responded with " \
+ raise UnexpectedResponse.new(response.status), "#{verb} to #{response.url} responded with " \
@jwilger Owner
jwilger added a note

I was under the impression that in raise Exception, message the "message" part was simply passed into Exception.new, so not sure what is happening to message on lines where this is raised now. Maybe I'm wrong?

Yeah, this is due to Ruby's wacky implementation of Kernel.raise. If you give it an exception class, the initializer is called with the message as the first argument, but the message is also stored on the exception. If you have an #initialize for your exception, you may not want the message as a param - in which case you explicitly create the instance of the Exception. The message is still stored on the exception somehow (possibly using a separate setter method), though according to what I read here: http://www.ruby-forum.com/topic/207448, the message and backtrace (optional third param to raise) are tucked away in inaccessible instance variables. When you call #message on the exception, the message is returned even if it wasn't part of the initialization, since #message uses #to_s which uses the internal variables. Magic! And yes, this has been raised as an issue, and debated: http://bugs.ruby-lang.org/issues/5898

(which isn't to say I agree with the issue as it was raised - it just seems to be another instance where the confusing dichotomy of Exception.new and Kernel.raise cause problems).

@jwilger Owner
jwilger added a note

That could work - in which case, I think we should use a different class of exception for the detect_server_error! exception. Unless we assume that a detected server error should always show up as a 500. I think UnexpectedResponse should always be expected to indicate the actual response.

At any rate, that sounds like a refactoring - the message is received the same way whether the exception constructor creates it or it's set by the raise call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ "#{response.status} status, not #{expected_status} as expected\n\n" \
+ response.body
end
View
8 lib/kookaburra/exceptions.rb
@@ -4,7 +4,13 @@ class UnknownKeyError < ArgumentError; end
# @private
class ConfigurationError < StandardError; end
# @private
- class UnexpectedResponse < RuntimeError; end
+ class UnexpectedResponse < RuntimeError
+ attr_reader :status_code
+
+ def initialize(status_code = nil)
+ @status_code = status_code
+ end
+ end
# @private
class AssertionFailed < RuntimeError; end
# @private
View
121 lib/kookaburra/mental_model_matcher.rb
@@ -1,4 +1,5 @@
require 'kookaburra/mental_model'
+require 'capybara/util/timeout'
class Kookaburra
class MentalModel
@@ -7,12 +8,24 @@ class MentalModel
# @see Kookaburra::TestHelpers#match_mental_model_of
# @see Kookaburra::TestHelpers#assert_mental_model_of
class Matcher
- def initialize(mental_model, collection_key)
+ # Creates a new matcher for the given MentalModel on the given
+ # collection_key.
+ #
+ # @param [MentalModel] mental_model The MentalModel instance to
+ # request the expected collection from.
+ # @param collection_key The name of the collection that contains
+ # the expected elements.
+ # @param [Integer] wait_for The number of seconds during which to
+ # continually retry failures, before reporting the failure. Not
+ # used when comparing against an array (see #matches?).
+ # @return [self]
+ def initialize(mental_model, collection_key, wait_for = 2)
@collection_key = collection_key
+ @wait_for = wait_for
mental_model.send(collection_key).tap do |collection|
- @expected = collection
- @unexpected = collection.deleted
+ @expected = collection.dup
+ @unexpected = collection.deleted.dup
end
end
@@ -23,8 +36,14 @@ def initialize(mental_model, collection_key)
# model contains elements { A, B, C }, but you only expect to see element
# A.
#
- # @param [Array] collection_keys The keys used in your mental model to
- # reference the data
+ # @example
+ # matcher.only?(:foo, :bar, :baz)
+ # @example With an array of keys
+ # keys = [:foo, :bar, :baz]
+ # matcher.only?(*keys)
+ #
+ # @param collection_keys The keys used in your mental model to reference
+ # the data - if you have an array, splat it.
# @return [self]
def only(*collection_keys)
keepers = @expected.slice(*collection_keys)
@@ -36,6 +55,35 @@ def only(*collection_keys)
self
end
+ # Specifies that members of the expected collection should be mapped by
+ # the given block before attempting to match.
+ #
+ # Useful if the result represents a modified version of what's on the
+ # mental model.
+ #
+ # @yield [val] map function, run once for each member of the collection
+ # @return [self]
+ def mapped_by(&block)
+ validate_block_arguments 'mapped_by', &block
+ @expected = Hash[@expected.map { |key, val| [key, block.call(val)] }]
+ @unexpected = Hash[@unexpected.map { |key, val| [key, block.call(val)] }]
+ self
+ end
+
+ # Specifies that result should be filtered by a given block.
+ #
+ # Useful if you are looking at a filtered result based on given criteria.
+ # That is, you only expect to see elements for which a given block
+ # returns true.
+ #
+ # @yield [val] function used to select specific results from collection
+ # @return [self]
+ def where(&block)
+ validate_block_arguments 'where', &block
+ valid_keys = @expected.select { |key, val| block.call(val) }.map { |key, val| key }
+ only *valid_keys
+ end
+
# Reads better than {#only} with no args
#
# @return [self]
@@ -43,17 +91,62 @@ def expecting_nothing
only
end
+ # Specifies the method to try calling on "actual," which is expected to
+ # return the observed result for comparison against the mental model.
+ #
+ # @param [Symbol] method The method to call on "actual"
+ # @return [self]
+ def using(collection_method)
+ @collection_method = collection_method
+ self
+ end
+
# The result contains everything that was expected to be found and nothing
# that was unexpected.
#
+ # The MentalModel::Matcher can be used with two types of objects.
+ #
+ # 1) If the matcher is called on an Array, a direct comparison will be
+ # done between the Array and the MentalModel collection (after any
+ # specified scoping or mapping methods have) been applied.
+ #
+ # 2) If the matcher is called on an object that responds to the
+ # collection_method (which defaults to the collection_key but can be
+ # overridden with "#using"), the result of actual#collection_method will
+ # be used for the comparison.
+ #
+ # When using method #2, if the match is unsuccessful, failure won't be
+ # reported immediately; instead, the comparison will be retried
+ # continuously for the number of seconds specified by wait_for (see
+ # "#initialize"). This is the recommended usage, and is useful for taking
+ # into account possible rendering delays.
+ #
# (Part of the RSpec protocol for custom matchers.)
#
- # @param [Array] actual This is the data observed that you are attempting
- # to match against the mental model.
+ # @param [Array, #collection_method] actual This is the data observed
+ # (or an object that returns the data observed when called with
+ # collection_method) that you you are attempting to match against the
+ # mental model.
# @return Boolean
def matches?(actual)
- @actual = actual
- expected_items_not_found.empty? && unexpected_items_found.empty?
+ @collection_method ||= @collection_key
+ @proc_for_actual = if actual.respond_to?(@collection_method.to_sym)
+ proc { actual.send(@collection_method.to_sym) }
+ else
+ @wait_for = 0
+ proc { actual }
+ end
+ if @wait_for > 0
+ Capybara.timeout(@wait_for) do
+ begin
+ check_expectations
+ rescue Capybara::TimeoutError
+ false
+ end
+ end
+ else
+ check_expectations
+ end
end
# Message to be printed when observed reality does not conform to
@@ -108,6 +201,11 @@ def unexpected_items_found
difference_between_arrays(unexpected_items, unexpected_items_not_found)
end
+ def check_expectations
+ @actual = @proc_for_actual.call
+ expected_items_not_found.empty? && unexpected_items_found.empty?
+ end
+
# (Swiped from RSpec's array matcher)
# Returns the difference of arrays, accounting for duplicates.
# e.g., difference_between_arrays([1, 2, 3, 3], [1, 2, 3]) # => [3]
@@ -125,6 +223,11 @@ def pp_array(array)
array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
array.inspect
end
+
+ def validate_block_arguments(method, &block)
+ raise "Must supply a block to ##{method}" unless block_given?
+ raise "Block supplied to ##{method} must take one argument (the value)" unless block.arity == 1
+ end
end
end
end
View
55 lib/kookaburra/test_helpers.rb
@@ -98,8 +98,40 @@ def k
# Delegates to {#k}
delegate :ui, :to => :k
- # RSpec-style custom matcher that compares a given array with
- # the current state of one named collection in the mental model
+ # RSpec-style custom matcher that compares an observed result with
+ # the current state of one named collection in the mental model.
+ #
+ # This matcher can be used in two different ways.
+ #
+ # 1) If the matcher is called on an Array, a direct comparison will be
+ # done between the Array and the MentalModel collection (after any
+ # specified scoping or mapping methods have) been applied.
+ #
+ # 2) If the matcher is called on an object that responds to the
+ # collection_method (which defaults to the collection_key but can be
+ # overridden with "#using"), the result of actual#collection_method will
+ # be used for the comparison.
+ #
+ # When using method #2, if the match is unsuccessful, failure won't be
+ # reported immediately; instead, the comparison will be retried
+ # continuously for the number of seconds specified by wait_for (see
+ # "#initialize"). This is the recommended usage, and is useful for taking
+ # into account possible rendering delays.
+ #
+ # @example Comparing against an array
+ # mental_model.widgets = { :foo => foo, :bar => bar }
+ # [foo, bar].should match_mental_model_of(:widgets)
+ # @example Comparing against an object that responds to the collection_key
+ # mental_model.widgets = { :foo => foo, :bar => bar }
+ # widget_index.widgets = [foo, bar]
+ # widget_index.should match_mental_model_of(:widgets)
+ # @example Comparing against an object that responds to an alternate key
+ # mental_model.widgets = { :foo => foo, :bar => bar }
+ # widget_index.visible_widgets = [foo, bar]
+ # widget_index.should match_mental_model_of(:widgets).using(:visible_widgets)
+ #
+ # @param [Symbol] collection_key The key of the collection on the
+ # mental model that represents the observed result we want.
#
# @see Kookaburra::MentalModel::Matcher
def match_mental_model_of(collection_key)
@@ -109,9 +141,26 @@ def match_mental_model_of(collection_key)
# Custom assertion for Test::Unit-style tests
# (really, anything that uses #assert(predicate, message = nil))
#
+ # This is essentially a wrapper of match_mental_model_of.
+ #
+ # @param [Symbol] collection_key The key of the collection on the
+ # mental model that represents the observed result we want.
+ # @param [Array, #collection_method] actual This is the data observed
+ # (or an object that returns the data observed when called with
+ # collection_method) that you you are attempting to match against the
+ # mental model.
+ # @param [message] message Message to return in case of failure; if not
+ # specified, will return message describing differences between expected
+ # and actual.
+ # @param [options] options Hash of scoping/filtering blocks to call on
+ # matcher (for limiting collection data to compare against) before
+ # attempting match. See the Matcher for details.
# @see Kookaburra::MentalModel::Matcher
- def assert_mental_model_matches(collection_key, actual, message = nil)
+ def assert_mental_model_matches(collection_key, actual, message = nil, options = {})
matcher = match_mental_model_of(collection_key)
+ options.each_pair do |key, val|
+ matcher = matcher.send(key.to_sym, &val)
+ end
result = matcher.matches?(actual)
return if !!result # don't even bother
View
2  lib/kookaburra/ui_driver/ui_component.rb
@@ -149,7 +149,7 @@ def component_locator
# function returns true
def detect_server_error!
if @server_error_detection.try(:call, browser)
- raise UnexpectedResponse, "Your server error detection function detected a server error. Looks like your applications is busted. :-("
+ raise UnexpectedResponse.new, "Your server error detection function detected a server error. Looks like your application is busted. :-("

Note this change, related to your comment on the call to Kernel.raise in APIDriver. Here I had to pass an instance of the exception, rather than the class - otherwise, the new exception would have the message set as the status_code (as well as the return from #message, even though they're completely unrelated), because the message is sent in as the first argument to initialize, when raise is given a class.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
end
end
end
View
54 spec/integration/test_a_rack_application_spec.rb
@@ -144,11 +144,19 @@ def parse_json_req_body
content << <<-EOF
<ul>
EOF
+ #
+ # Note that we're simulating a rendering delay for the "name" attribute
+ # of the widgets. They're being placed into a temporary data-attribute
+ # and then, 300ms later (see the setTimeout below), the value in that
+ # attribute is moved to the text content of the element, where it's to
+ # be expected. This is being done so we can test that the matcher won't
+ # fail immediately, and that it will retry for a period of time.
+ #
@@widgets.each do |w|
content << <<-EOF
<li class="widget_summary">
<span class="id">#{w[:id]}</span>
- <span class="name">#{w[:name]}</span>
+ <span class="name" data-name="#{w[:name]}"></span>
<form id="delete_#{w[:id]}" action="/widgets/#{w[:id]}" method="POST">
<button type="submit" value="Delete" />
</form>
@@ -159,6 +167,14 @@ def parse_json_req_body
</ul>
<a href="/widgets/new">New Widget</a>
</div>
+ <script>
+ setTimeout(function(){
+ nameElements = document.querySelectorAll('.widget_summary .name');
+ for (i = 0; i < nameElements.length; i++) {
+ nameElements[i].innerHTML = nameElements[i].getAttribute('data-name');
+ }
+ }, 300);
+ </script>
</body>
</html>
EOF
@@ -336,23 +352,43 @@ def delete_widget(name)
ui.sign_in(:bob)
ui.view_widget_list
- # The following two lines are two different ways to shave the yak, but
- # the second one does more to match against the full state of the mental
- # model, provides better failure messages, and is shorter.
+ # We're going to test presence of the expected elements in two ways here.
+ # They're both similar, but the first technique (match_mental_model_of)
+ # does more to match against the full state of the mental model, provides
+ # better failure messages, and is shorter. Also, the simple method of
+ # testing for equality of arrays will fail if there are any extraneous
+ # widgets in the list, while the MentalModelMatcher technique will take
+ # into account what is expected and what is explicitly not expected (through
+ # deletion); this is important in the case of a multi-client testing server.
+ # Finally, the array equality method will fail if the items are presented in
+ # a different order, which is usually irrelevant; this can be corrected for
+ # by using the unfortunately-named `=~` matcher.
+
+ # First we'll use the MentalModelMatcher; this is important because the
+ # MentalModelMatcher will actually wait until a given timeout (using
+ # Capybara's timeout) before failing, if items are not found. Since
+ # we're simulating a rendering delay in the widget index, a direct
+ # comparison of arrays would outright fail without artificially delaying
+ # within the test (e.g. using `sleep`).
+ ui.widget_list.should match_mental_model_of(:widgets)
+
+ # Now that we know the rendering is done (since match_mental_model_of
+ # waited until it found them), we can compare the arrays.
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b)
- ui.widget_list.widgets.should match_mental_model_of(:widgets)
ui.create_new_widget(:widget_c, :name => 'Bar')
- # As above, these are equivalent, but the second line is preferred.
+ # As above, these are mostly equivalent in this simple case, but the
+ # match_mental_model_of technique is preferred.
+ ui.widget_list.should match_mental_model_of(:widgets)
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_b, :widget_c)
- ui.widget_list.widgets.should match_mental_model_of(:widgets)
ui.delete_widget(:widget_b)
- # As above, these are equivalent, but the second line is preferred.
+ # As above, these are mostly equivalent in this simple case, but the
+ # match_mental_model_of technique is preferred.
+ ui.widget_list.should match_mental_model_of(:widgets)
ui.widget_list.widgets.should == k.get_data(:widgets).values_at(:widget_a, :widget_c)
- ui.widget_list.widgets.should match_mental_model_of(:widgets)
end
end
end
View
12 spec/kookaburra/api_driver_spec.rb
@@ -50,7 +50,8 @@
it 'raises an UnexpectedResponse if the response status is not the specified status' do
lambda { api.post('/foo', 'bar', :expected_response_status => 666) } \
.should raise_error(Kookaburra::UnexpectedResponse,
- "POST to /foo responded with 201 status, not 666 as expected\n\nfoo")
+ "POST to /foo responded with 201 status, not 666 as expected\n\nfoo") { |error|
+ error.status_code.should == 201 }
end
it 'raises an ArgumentError with a useful message if no request path is specified' do
@@ -79,7 +80,8 @@
it 'raises an UnexpectedResponse if the response status is not the specified status' do
lambda { api.put('/foo', 'bar', :expected_response_status => 666) } \
.should raise_error(Kookaburra::UnexpectedResponse,
- "PUT to /foo responded with 200 status, not 666 as expected\n\nfoo")
+ "PUT to /foo responded with 200 status, not 666 as expected\n\nfoo") { |error|
+ error.status_code.should == 200 }
end
it 'raises an ArgumentError with a useful message if no request path is specified' do
@@ -108,7 +110,8 @@
it 'raises an UnexpectedResponse if the response status is not the specified status' do
lambda { api.get('/foo', :expected_response_status => 666) } \
.should raise_error(Kookaburra::UnexpectedResponse,
- "GET to /foo responded with 200 status, not 666 as expected\n\nfoo")
+ "GET to /foo responded with 200 status, not 666 as expected\n\nfoo") { |error|
+ error.status_code.should == 200 }
end
it 'raises an ArgumentError with a useful message if no request path is specified' do
@@ -137,7 +140,8 @@
it 'raises an UnexpectedResponse if the response status is not the specified status' do
lambda { api.delete('/foo', :expected_response_status => 666) } \
.should raise_error(Kookaburra::UnexpectedResponse,
- "DELETE to /foo responded with 200 status, not 666 as expected\n\nfoo")
+ "DELETE to /foo responded with 200 status, not 666 as expected\n\nfoo") { |error|
+ error.status_code.should == 200 }
end
it 'raises an ArgumentError with a useful message if no request path is specified' do
View
101 spec/kookaburra/mental_model_matcher_spec.rb
@@ -60,9 +60,7 @@ def it_complains_about_extra(extra, options)
end
def matcher_for(collection_key)
- Kookaburra::MentalModel::Matcher.new(mm, collection_key).tap do |m|
- m.matches?(target)
- end
+ Kookaburra::MentalModel::Matcher.new(mm, collection_key)
end
def pp_array(array)
@@ -75,7 +73,7 @@ def pp_array(array)
let(:mm) { Kookaburra::MentalModel.new }
let(:matcher) { matcher_for(:widgets) }
- let(:failure_msg) { matcher.failure_message_for_should }
+ let(:failure_msg) { matcher.matches?(target); matcher.failure_message_for_should }
def self.foo; 'FOO' ; end
def self.bar; 'BAR' ; end
@@ -184,6 +182,52 @@ def self.yak; 'YAK' ; end
end
end
+ describe "postfix presentation methods" do
+ context "when mental model is two 3-element arrays" do
+ before(:each) do
+ mm.widgets[:the_foos] = ['f1', 'f2', 'f3']
+ mm.widgets[:the_bars] = ['b1', 'b2', 'b3']
+ end
+
+ context "but .mapped_by a block that selects only 2 of the elements" do
+ let(:matcher) { matcher_for(:widgets).mapped_by { |v| v[1,2] } }
+
+ context "for [['f2', 'f3'], ['b2', 'b3']] (OK)" do
+ let(:target) { [['f2', 'f3'], ['b2', 'b3']] }
+ it_matches
+ end
+
+ context "for [['f1', 'f2', 'f3'], ['b1', 'b2', 'b3']] (foos and bars missing)" do
+ let(:target) { [@foos, @bars] }
+ it_doesnt_match
+ it_complains_about_missing [['f2', 'f3'], ['b2', 'b3']], :expected => [['f2', 'f3'], ['b2', 'b3']]
+ end
+ end
+ end
+
+ context "when mental model is ['radish', 'pickle'];" do
+ before(:each) do
+ mm.widgets[:radish] = 'radish'
+ mm.widgets[:pickle] = 'pickle'
+ end
+
+ context "but .mapped_by a block that upcases the elements" do
+ let(:matcher) { matcher_for(:widgets).mapped_by { |v| v.upcase } }
+
+ context "for ['RADISH', 'PICKLE'] (OK)" do
+ let(:target) { ['RADISH', 'PICKLE'] }
+ it_matches
+ end
+
+ context "for ['radish', 'pickle'] (RADISH and PICKLE missing)" do
+ let(:target) { ['radish', 'pickle'] }
+ it_doesnt_match
+ it_complains_about_missing ['RADISH', 'PICKLE'], :expected => ['RADISH', 'PICKLE']
+ end
+ end
+ end
+ end
+
describe "postfix scoping methods" do
context "when mental model is [foo, bar];" do
before(:each) do
@@ -204,6 +248,36 @@ def self.yak; 'YAK' ; end
it_doesnt_match
it_complains_about_extra [bar], :unexpected => [bar]
end
+
+ it "doesn't modify the deleted collection" do
+ Kookaburra::MentalModel::Matcher.new(mm, :widgets).only(:foo)
+ mm.widgets.deleted.should be_empty
+ end
+ end
+
+ context "but scoped by .where with a block that doesn't like foo" do
+ let(:matcher) { matcher_for(:widgets).where { |v| v != foo } }
+
+ context "for [bar] (OK)" do
+ let(:target) { [bar] }
+ it_matches
+ end
+
+ context "for [foo, bar] (not expecting [foo])" do
+ let(:target) { [foo, bar] }
+ it_doesnt_match
+ it_complains_about_extra [foo], :unexpected => [foo]
+ end
+ end
+
+ context "but scoped by .where with an invalid block" do
+ let(:matcher) { matcher_for(:widgets).where { |a, b| true } }
+ let(:target) { [] }
+
+ it "raises an error" do
+ lambda { matcher.matches?(target) }.should raise_error(
+ "Block supplied to #where must take one argument (the value)")
+ end
end
end
@@ -234,4 +308,23 @@ def self.yak; 'YAK' ; end
end
end
end
+ describe "when target responds to collection_method" do
+ context "when mental model is [foo, bar];" do
+ before(:each) do
+ mm.widgets[:foo] = foo
+ mm.widgets[:bar] = bar
+ end
+
+ context "and target responds to collection_key" do
+ let(:target) { double(:widgets => [foo, bar]) }
+ it_matches
+ end
+
+ context "and target responds to specified collection_method" do
+ let(:matcher) { matcher_for(:widgets).using(:relevant_widgets) }
+ let(:target) { double(:relevant_widgets => [foo, bar]) }
+ it_matches
+ end
+ end
+ end
end
View
30 spec/kookaburra/test_helpers_spec.rb
@@ -44,28 +44,50 @@
before(:each) do
mm = k.send(:__mental_model__)
mm.widgets[:foo] = 'FOO'
+ mm.widgets[:bar] = 'BAR'
end
describe "#match_mental_model_of" do
it "does a positive match" do
- ['FOO'].should match_mental_model_of(:widgets)
+ ['FOO', 'BAR'].should match_mental_model_of(:widgets)
end
it "does a negative match" do
- ['BAR'].should_not match_mental_model_of(:widgets)
+ ['BAZ'].should_not match_mental_model_of(:widgets)
+ end
+
+ it "works with scoping and mapping methods" do
+ ['foo'].should match_mental_model_of(:widgets).mapped_by { |v|
+ v.downcase
+ }.where { |v|
+ v != 'bar'
+ }
end
end
describe "#assert_mental_model_matches" do
it "does a positive assertion" do
- actual = ['FOO']
+ actual = ['FOO', 'BAR']
actual.should match_mental_model_of(:widgets) # Sanity check
self.should_receive(:assert).never
self.assert_mental_model_matches(:widgets, actual)
end
+ it "works with scoping and mapping methods" do
+ mapper = Proc.new { |v| v.downcase }
+ filter = Proc.new { |v| v != 'bar' }
+ actual = ['foo']
+ actual.should match_mental_model_of(:widgets).
+ mapped_by(&mapper).
+ where(&filter) # Sanity check
+ self.should_receive(:assert).never
+ self.assert_mental_model_matches(:widgets, actual, nil,
+ :mapped_by => mapper,
+ :where => filter)
+ end
+
it "does a negative assertion" do
- actual = ['BAR']
+ actual = ['BAZ']
self.should_receive(:assert).with(false, kind_of(String))
self.assert_mental_model_matches(:widgets, actual)
end
View
3  spec/kookaburra/ui_driver/ui_component_spec.rb
@@ -89,7 +89,8 @@ def foo
component.stub!(:component_locator => '#my_component')
component.stub!(:component_locator => '#my_component')
lambda { component.visible? } \
- .should raise_error(Kookaburra::UnexpectedResponse)
+ .should raise_error(Kookaburra::UnexpectedResponse) { |error|
+ error.status_code.should be_nil }
end
end
Something went wrong with that request. Please try again.