| [58] | 1 | require 'test/unit' |
|---|
| 2 | require 'test/unit/assertions' |
|---|
| 3 | require 'rexml/document' |
|---|
| 4 | require File.dirname(__FILE__) + "/vendor/html-scanner/html/document" |
|---|
| 5 | |
|---|
| 6 | module Test #:nodoc: |
|---|
| 7 | module Unit #:nodoc: |
|---|
| 8 | # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions |
|---|
| 9 | # can be used against. These collections are: |
|---|
| 10 | # |
|---|
| 11 | # * assigns: Instance variables assigned in the action that are available for the view. |
|---|
| 12 | # * session: Objects being saved in the session. |
|---|
| 13 | # * flash: The flash objects currently in the session. |
|---|
| 14 | # * cookies: Cookies being sent to the user on this request. |
|---|
| 15 | # |
|---|
| 16 | # These collections can be used just like any other hash: |
|---|
| 17 | # |
|---|
| 18 | # assert_not_nil assigns(:person) # makes sure that a @person instance variable was set |
|---|
| 19 | # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave" |
|---|
| 20 | # assert flash.empty? # makes sure that there's nothing in the flash |
|---|
| 21 | # |
|---|
| 22 | # For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To |
|---|
| 23 | # appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing. |
|---|
| 24 | # So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work. |
|---|
| 25 | # |
|---|
| 26 | # On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url. |
|---|
| 27 | # |
|---|
| 28 | # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another |
|---|
| 29 | # action call which can then be asserted against. |
|---|
| 30 | # |
|---|
| 31 | # == Manipulating the request collections |
|---|
| 32 | # |
|---|
| 33 | # The collections described above link to the response, so you can test if what the actions were expected to do happened. But |
|---|
| 34 | # sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions |
|---|
| 35 | # and cookies, though. For sessions, you just do: |
|---|
| 36 | # |
|---|
| 37 | # @request.session[:key] = "value" |
|---|
| 38 | # |
|---|
| 39 | # For cookies, you need to manually create the cookie, like this: |
|---|
| 40 | # |
|---|
| 41 | # @request.cookies["key"] = CGI::Cookie.new("key", "value") |
|---|
| 42 | # |
|---|
| 43 | # == Testing named routes |
|---|
| 44 | # |
|---|
| 45 | # If you're using named routes, they can be easily tested using the original named routes methods straight in the test case. |
|---|
| 46 | # Example: |
|---|
| 47 | # |
|---|
| 48 | # assert_redirected_to page_url(:title => 'foo') |
|---|
| 49 | module Assertions |
|---|
| 50 | # Asserts that the response is one of the following types: |
|---|
| 51 | # |
|---|
| 52 | # * <tt>:success</tt>: Status code was 200 |
|---|
| 53 | # * <tt>:redirect</tt>: Status code was in the 300-399 range |
|---|
| 54 | # * <tt>:missing</tt>: Status code was 404 |
|---|
| 55 | # * <tt>:error</tt>: Status code was in the 500-599 range |
|---|
| 56 | # |
|---|
| 57 | # You can also pass an explicit status code number as the type, like assert_response(501) |
|---|
| 58 | def assert_response(type, message = nil) |
|---|
| 59 | clean_backtrace do |
|---|
| 60 | if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?") |
|---|
| 61 | assert_block("") { true } # to count the assertion |
|---|
| 62 | elsif type.is_a?(Fixnum) && @response.response_code == type |
|---|
| 63 | assert_block("") { true } # to count the assertion |
|---|
| 64 | else |
|---|
| 65 | assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false } |
|---|
| 66 | end |
|---|
| 67 | end |
|---|
| 68 | end |
|---|
| 69 | |
|---|
| 70 | # Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, |
|---|
| 71 | # such that assert_redirected_to(:controller => "weblog") will also match the redirection of |
|---|
| 72 | # redirect_to(:controller => "weblog", :action => "show") and so on. |
|---|
| 73 | def assert_redirected_to(options = {}, message=nil) |
|---|
| 74 | clean_backtrace do |
|---|
| 75 | assert_response(:redirect, message) |
|---|
| 76 | |
|---|
| 77 | if options.is_a?(String) |
|---|
| 78 | msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url) |
|---|
| 79 | url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$} |
|---|
| 80 | eurl, epath, url, path = [options, @response.redirect_url].collect do |url| |
|---|
| 81 | u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url] |
|---|
| 82 | [u, (p[0..0] == '/') ? p : '/' + p] |
|---|
| 83 | end.flatten |
|---|
| 84 | |
|---|
| 85 | assert_equal(eurl, url, msg) if eurl && url |
|---|
| 86 | assert_equal(epath, path, msg) if epath && path |
|---|
| 87 | else |
|---|
| 88 | @response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) |
|---|
| 89 | msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)#{', difference: <?>' if @response_diff}", |
|---|
| 90 | @response.redirected_to || @response.redirect_url, @response_diff) |
|---|
| 91 | |
|---|
| 92 | assert_block(msg) do |
|---|
| 93 | if options.is_a?(Symbol) |
|---|
| 94 | @response.redirected_to == options |
|---|
| 95 | else |
|---|
| 96 | options.keys.all? do |k| |
|---|
| 97 | if k == :controller then options[k] == ActionController::Routing.controller_relative_to(@response.redirected_to[k], @controller.class.controller_path) |
|---|
| 98 | else options[k] == (@response.redirected_to[k].respond_to?(:to_param) ? @response.redirected_to[k].to_param : @response.redirected_to[k] unless @response.redirected_to[k].nil?) |
|---|
| 99 | end |
|---|
| 100 | end |
|---|
| 101 | end |
|---|
| 102 | end |
|---|
| 103 | end |
|---|
| 104 | end |
|---|
| 105 | end |
|---|
| 106 | |
|---|
| 107 | # Asserts that the request was rendered with the appropriate template file. |
|---|
| 108 | def assert_template(expected = nil, message=nil) |
|---|
| 109 | clean_backtrace do |
|---|
| 110 | rendered = expected ? @response.rendered_file(!expected.include?('/')) : @response.rendered_file |
|---|
| 111 | msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered) |
|---|
| 112 | assert_block(msg) do |
|---|
| 113 | if expected.nil? |
|---|
| 114 | !@response.rendered_with_file? |
|---|
| 115 | else |
|---|
| 116 | expected == rendered |
|---|
| 117 | end |
|---|
| 118 | end |
|---|
| 119 | end |
|---|
| 120 | end |
|---|
| 121 | |
|---|
| 122 | # Asserts that the routing of the given path was handled correctly and that the parsed options match. |
|---|
| 123 | def assert_recognizes(expected_options, path, extras={}, message=nil) |
|---|
| 124 | clean_backtrace do |
|---|
| 125 | path = "/#{path}" unless path[0..0] == '/' |
|---|
| 126 | # Load routes.rb if it hasn't been loaded. |
|---|
| 127 | ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? |
|---|
| 128 | |
|---|
| 129 | # Assume given controller |
|---|
| 130 | request = ActionController::TestRequest.new({}, {}, nil) |
|---|
| 131 | request.path = path |
|---|
| 132 | ActionController::Routing::Routes.recognize!(request) |
|---|
| 133 | |
|---|
| 134 | expected_options = expected_options.clone |
|---|
| 135 | extras.each_key { |key| expected_options.delete key } unless extras.nil? |
|---|
| 136 | |
|---|
| 137 | expected_options.stringify_keys! |
|---|
| 138 | msg = build_message(message, "The recognized options <?> did not match <?>", |
|---|
| 139 | request.path_parameters, expected_options) |
|---|
| 140 | assert_block(msg) { request.path_parameters == expected_options } |
|---|
| 141 | end |
|---|
| 142 | end |
|---|
| 143 | |
|---|
| 144 | # Asserts that the provided options can be used to generate the provided path. |
|---|
| 145 | def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) |
|---|
| 146 | clean_backtrace do |
|---|
| 147 | expected_path = "/#{expected_path}" unless expected_path[0] == ?/ |
|---|
| 148 | # Load routes.rb if it hasn't been loaded. |
|---|
| 149 | ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty? |
|---|
| 150 | |
|---|
| 151 | generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras) |
|---|
| 152 | found_extras = options.reject {|k, v| ! extra_keys.include? k} |
|---|
| 153 | |
|---|
| 154 | msg = build_message(message, "found extras <?>, not <?>", found_extras, extras) |
|---|
| 155 | assert_block(msg) { found_extras == extras } |
|---|
| 156 | |
|---|
| 157 | msg = build_message(message, "The generated path <?> did not match <?>", generated_path, |
|---|
| 158 | expected_path) |
|---|
| 159 | assert_block(msg) { expected_path == generated_path } |
|---|
| 160 | end |
|---|
| 161 | end |
|---|
| 162 | |
|---|
| 163 | # Asserts that path and options match both ways; in other words, the URL generated from |
|---|
| 164 | # options is the same as path, and also that the options recognized from path are the same as options |
|---|
| 165 | def assert_routing(path, options, defaults={}, extras={}, message=nil) |
|---|
| 166 | assert_recognizes(options, path, extras, message) |
|---|
| 167 | |
|---|
| 168 | controller, default_controller = options[:controller], defaults[:controller] |
|---|
| 169 | if controller && controller.include?(?/) && default_controller && default_controller.include?(?/) |
|---|
| 170 | options[:controller] = "/#{controller}" |
|---|
| 171 | end |
|---|
| 172 | |
|---|
| 173 | assert_generates(path, options, defaults, extras, message) |
|---|
| 174 | end |
|---|
| 175 | |
|---|
| 176 | # Asserts that there is a tag/node/element in the body of the response |
|---|
| 177 | # that meets all of the given conditions. The +conditions+ parameter must |
|---|
| 178 | # be a hash of any of the following keys (all are optional): |
|---|
| 179 | # |
|---|
| 180 | # * <tt>:tag</tt>: the node type must match the corresponding value |
|---|
| 181 | # * <tt>:attributes</tt>: a hash. The node's attributes must match the |
|---|
| 182 | # corresponding values in the hash. |
|---|
| 183 | # * <tt>:parent</tt>: a hash. The node's parent must match the |
|---|
| 184 | # corresponding hash. |
|---|
| 185 | # * <tt>:child</tt>: a hash. At least one of the node's immediate children |
|---|
| 186 | # must meet the criteria described by the hash. |
|---|
| 187 | # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must |
|---|
| 188 | # meet the criteria described by the hash. |
|---|
| 189 | # * <tt>:descendant</tt>: a hash. At least one of the node's descendants |
|---|
| 190 | # must meet the criteria described by the hash. |
|---|
| 191 | # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must |
|---|
| 192 | # meet the criteria described by the hash. |
|---|
| 193 | # * <tt>:after</tt>: a hash. The node must be after any sibling meeting |
|---|
| 194 | # the criteria described by the hash, and at least one sibling must match. |
|---|
| 195 | # * <tt>:before</tt>: a hash. The node must be before any sibling meeting |
|---|
| 196 | # the criteria described by the hash, and at least one sibling must match. |
|---|
| 197 | # * <tt>:children</tt>: a hash, for counting children of a node. Accepts |
|---|
| 198 | # the keys: |
|---|
| 199 | # * <tt>:count</tt>: either a number or a range which must equal (or |
|---|
| 200 | # include) the number of children that match. |
|---|
| 201 | # * <tt>:less_than</tt>: the number of matching children must be less |
|---|
| 202 | # than this number. |
|---|
| 203 | # * <tt>:greater_than</tt>: the number of matching children must be |
|---|
| 204 | # greater than this number. |
|---|
| 205 | # * <tt>:only</tt>: another hash consisting of the keys to use |
|---|
| 206 | # to match on the children, and only matching children will be |
|---|
| 207 | # counted. |
|---|
| 208 | # * <tt>:content</tt>: the textual content of the node must match the |
|---|
| 209 | # given value. This will not match HTML tags in the body of a |
|---|
| 210 | # tag--only text. |
|---|
| 211 | # |
|---|
| 212 | # Conditions are matched using the following algorithm: |
|---|
| 213 | # |
|---|
| 214 | # * if the condition is a string, it must be a substring of the value. |
|---|
| 215 | # * if the condition is a regexp, it must match the value. |
|---|
| 216 | # * if the condition is a number, the value must match number.to_s. |
|---|
| 217 | # * if the condition is +true+, the value must not be +nil+. |
|---|
| 218 | # * if the condition is +false+ or +nil+, the value must be +nil+. |
|---|
| 219 | # |
|---|
| 220 | # Usage: |
|---|
| 221 | # |
|---|
| 222 | # # assert that there is a "span" tag |
|---|
| 223 | # assert_tag :tag => "span" |
|---|
| 224 | # |
|---|
| 225 | # # assert that there is a "span" tag with id="x" |
|---|
| 226 | # assert_tag :tag => "span", :attributes => { :id => "x" } |
|---|
| 227 | # |
|---|
| 228 | # # assert that there is a "span" tag using the short-hand |
|---|
| 229 | # assert_tag :span |
|---|
| 230 | # |
|---|
| 231 | # # assert that there is a "span" tag with id="x" using the short-hand |
|---|
| 232 | # assert_tag :span, :attributes => { :id => "x" } |
|---|
| 233 | # |
|---|
| 234 | # # assert that there is a "span" inside of a "div" |
|---|
| 235 | # assert_tag :tag => "span", :parent => { :tag => "div" } |
|---|
| 236 | # |
|---|
| 237 | # # assert that there is a "span" somewhere inside a table |
|---|
| 238 | # assert_tag :tag => "span", :ancestor => { :tag => "table" } |
|---|
| 239 | # |
|---|
| 240 | # # assert that there is a "span" with at least one "em" child |
|---|
| 241 | # assert_tag :tag => "span", :child => { :tag => "em" } |
|---|
| 242 | # |
|---|
| 243 | # # assert that there is a "span" containing a (possibly nested) |
|---|
| 244 | # # "strong" tag. |
|---|
| 245 | # assert_tag :tag => "span", :descendant => { :tag => "strong" } |
|---|
| 246 | # |
|---|
| 247 | # # assert that there is a "span" containing between 2 and 4 "em" tags |
|---|
| 248 | # # as immediate children |
|---|
| 249 | # assert_tag :tag => "span", |
|---|
| 250 | # :children => { :count => 2..4, :only => { :tag => "em" } } |
|---|
| 251 | # |
|---|
| 252 | # # get funky: assert that there is a "div", with an "ul" ancestor |
|---|
| 253 | # # and an "li" parent (with "class" = "enum"), and containing a |
|---|
| 254 | # # "span" descendant that contains text matching /hello world/ |
|---|
| 255 | # assert_tag :tag => "div", |
|---|
| 256 | # :ancestor => { :tag => "ul" }, |
|---|
| 257 | # :parent => { :tag => "li", |
|---|
| 258 | # :attributes => { :class => "enum" } }, |
|---|
| 259 | # :descendant => { :tag => "span", |
|---|
| 260 | # :child => /hello world/ } |
|---|
| 261 | # |
|---|
| 262 | # <strong>Please note</strong: #assert_tag and #assert_no_tag only work |
|---|
| 263 | # with well-formed XHTML. They recognize a few tags as implicitly self-closing |
|---|
| 264 | # (like br and hr and such) but will not work correctly with tags |
|---|
| 265 | # that allow optional closing tags (p, li, td). <em>You must explicitly |
|---|
| 266 | # close all of your tags to use these assertions.</em> |
|---|
| 267 | def assert_tag(*opts) |
|---|
| 268 | clean_backtrace do |
|---|
| 269 | opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first |
|---|
| 270 | tag = find_tag(opts) |
|---|
| 271 | assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" |
|---|
| 272 | end |
|---|
| 273 | end |
|---|
| 274 | |
|---|
| 275 | # Identical to #assert_tag, but asserts that a matching tag does _not_ |
|---|
| 276 | # exist. (See #assert_tag for a full discussion of the syntax.) |
|---|
| 277 | def assert_no_tag(*opts) |
|---|
| 278 | clean_backtrace do |
|---|
| 279 | opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first |
|---|
| 280 | tag = find_tag(opts) |
|---|
| 281 | assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" |
|---|
| 282 | end |
|---|
| 283 | end |
|---|
| 284 | |
|---|
| 285 | # test 2 html strings to be equivalent, i.e. identical up to reordering of attributes |
|---|
| 286 | def assert_dom_equal(expected, actual, message="") |
|---|
| 287 | clean_backtrace do |
|---|
| 288 | expected_dom = HTML::Document.new(expected).root |
|---|
| 289 | actual_dom = HTML::Document.new(actual).root |
|---|
| 290 | full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s) |
|---|
| 291 | assert_block(full_message) { expected_dom == actual_dom } |
|---|
| 292 | end |
|---|
| 293 | end |
|---|
| 294 | |
|---|
| 295 | # negated form of +assert_dom_equivalent+ |
|---|
| 296 | def assert_dom_not_equal(expected, actual, message="") |
|---|
| 297 | clean_backtrace do |
|---|
| 298 | expected_dom = HTML::Document.new(expected).root |
|---|
| 299 | actual_dom = HTML::Document.new(actual).root |
|---|
| 300 | full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s) |
|---|
| 301 | assert_block(full_message) { expected_dom != actual_dom } |
|---|
| 302 | end |
|---|
| 303 | end |
|---|
| 304 | |
|---|
| 305 | # ensures that the passed record is valid by active record standards. returns the error messages if not |
|---|
| 306 | def assert_valid(record) |
|---|
| 307 | clean_backtrace do |
|---|
| 308 | assert record.valid?, record.errors.full_messages.join("\n") |
|---|
| 309 | end |
|---|
| 310 | end |
|---|
| 311 | |
|---|
| 312 | def clean_backtrace(&block) |
|---|
| 313 | yield |
|---|
| 314 | rescue AssertionFailedError => e |
|---|
| 315 | path = File.expand_path(__FILE__) |
|---|
| 316 | raise AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ } |
|---|
| 317 | end |
|---|
| 318 | end |
|---|
| 319 | end |
|---|
| 320 | end |
|---|