root/trunk/actionpack/lib/action_controller/assertions/selector_assertions.rb
| Revision 8300, 23.9 kB (checked in by marcel, 1 year ago) |
|---|
| Line | |
|---|---|
| 1 | #-- |
| 2 | # Copyright (c) 2006 Assaf Arkin (http://labnotes.org) |
| 3 | # Under MIT and/or CC By license. |
| 4 | #++ |
| 5 | |
| 6 | require 'rexml/document' |
| 7 | require 'html/document' |
| 8 | |
| 9 | module ActionController |
| 10 | module Assertions |
| 11 | unless const_defined?(:NO_STRIP) |
| 12 | NO_STRIP = %w{pre script style textarea} |
| 13 | end |
| 14 | |
| 15 | # Adds the #assert_select method for use in Rails functional |
| 16 | # test cases, which can be used to make assertions on the response HTML of a controller |
| 17 | # action. You can also call #assert_select within another #assert_select to |
| 18 | # make assertions on elements selected by the enclosing assertion. |
| 19 | # |
| 20 | # Use #css_select to select elements without making an assertions, either |
| 21 | # from the response HTML or elements selected by the enclosing assertion. |
| 22 | # |
| 23 | # In addition to HTML responses, you can make the following assertions: |
| 24 | # * #assert_select_rjs -- Assertions on HTML content of RJS update and |
| 25 | # insertion operations. |
| 26 | # * #assert_select_encoded -- Assertions on HTML encoded inside XML, |
| 27 | # for example for dealing with feed item descriptions. |
| 28 | # * #assert_select_email -- Assertions on the HTML body of an e-mail. |
| 29 | # |
| 30 | # Also see HTML::Selector to learn how to use selectors. |
| 31 | module SelectorAssertions |
| 32 | # :call-seq: |
| 33 | # css_select(selector) => array |
| 34 | # css_select(element, selector) => array |
| 35 | # |
| 36 | # Select and return all matching elements. |
| 37 | # |
| 38 | # If called with a single argument, uses that argument as a selector |
| 39 | # to match all elements of the current page. Returns an empty array |
| 40 | # if no match is found. |
| 41 | # |
| 42 | # If called with two arguments, uses the first argument as the base |
| 43 | # element and the second argument as the selector. Attempts to match the |
| 44 | # base element and any of its children. Returns an empty array if no |
| 45 | # match is found. |
| 46 | # |
| 47 | # The selector may be a CSS selector expression (+String+), an expression |
| 48 | # with substitution values (+Array+) or an HTML::Selector object. |
| 49 | # |
| 50 | # ==== Examples |
| 51 | # # Selects all div tags |
| 52 | # divs = css_select("div") |
| 53 | # |
| 54 | # # Selects all paragraph tags and does something interesting |
| 55 | # pars = css_select("p") |
| 56 | # pars.each do |par| |
| 57 | # # Do something fun with paragraphs here... |
| 58 | # end |
| 59 | # |
| 60 | # # Selects all list items in unordered lists |
| 61 | # items = css_select("ul>li") |
| 62 | # |
| 63 | # # Selects all form tags and then all inputs inside the form |
| 64 | # forms = css_select("form") |
| 65 | # forms.each do |form| |
| 66 | # inputs = css_select(form, "input") |
| 67 | # ... |
| 68 | # end |
| 69 | # |
| 70 | def css_select(*args) |
| 71 | # See assert_select to understand what's going on here. |
| 72 | arg = args.shift |
| 73 | |
| 74 | if arg.is_a?(HTML::Node) |
| 75 | root = arg |
| 76 | arg = args.shift |
| 77 | elsif arg == nil |
| 78 | raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" |
| 79 | elsif @selected |
| 80 | matches = [] |
| 81 | |
| 82 | @selected.each do |selected| |
| 83 | subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) |
| 84 | subset.each do |match| |
| 85 | matches << match unless matches.any? { |m| m.equal?(match) } |
| 86 | end |
| 87 | end |
| 88 | |
| 89 | return matches |
| 90 | else |
| 91 | root = response_from_page_or_rjs |
| 92 | end |
| 93 | |
| 94 | case arg |
| 95 | when String |
| 96 | selector = HTML::Selector.new(arg, args) |
| 97 | when Array |
| 98 | selector = HTML::Selector.new(*arg) |
| 99 | when HTML::Selector |
| 100 | selector = arg |
| 101 | else raise ArgumentError, "Expecting a selector as the first argument" |
| 102 | end |
| 103 | |
| 104 | selector.select(root) |
| 105 | end |
| 106 | |
| 107 | # :call-seq: |
| 108 | # assert_select(selector, equality?, message?) |
| 109 | # assert_select(element, selector, equality?, message?) |
| 110 | # |
| 111 | # An assertion that selects elements and makes one or more equality tests. |
| 112 | # |
| 113 | # If the first argument is an element, selects all matching elements |
| 114 | # starting from (and including) that element and all its children in |
| 115 | # depth-first order. |
| 116 | # |
| 117 | # If no element if specified, calling #assert_select will select from the |
| 118 | # response HTML. Calling #assert_select inside an #assert_select block will |
| 119 | # run the assertion for each element selected by the enclosing assertion. |
| 120 | # |
| 121 | # ==== Example |
| 122 | # assert_select "ol>li" do |elements| |
| 123 | # elements.each do |element| |
| 124 | # assert_select element, "li" |
| 125 | # end |
| 126 | # end |
| 127 | # |
| 128 | # Or for short: |
| 129 | # assert_select "ol>li" do |
| 130 | # assert_select "li" |
| 131 | # end |
| 132 | # |
| 133 | # The selector may be a CSS selector expression (+String+), an expression |
| 134 | # with substitution values, or an HTML::Selector object. |
| 135 | # |
| 136 | # === Equality Tests |
| 137 | # |
| 138 | # The equality test may be one of the following: |
| 139 | # * <tt>true</tt> -- Assertion is true if at least one element selected. |
| 140 | # * <tt>false</tt> -- Assertion is true if no element selected. |
| 141 | # * <tt>String/Regexp</tt> -- Assertion is true if the text value of at least |
| 142 | # one element matches the string or regular expression. |
| 143 | # * <tt>Integer</tt> -- Assertion is true if exactly that number of |
| 144 | # elements are selected. |
| 145 | # * <tt>Range</tt> -- Assertion is true if the number of selected |
| 146 | # elements fit the range. |
| 147 | # If no equality test specified, the assertion is true if at least one |
| 148 | # element selected. |
| 149 | # |
| 150 | # To perform more than one equality tests, use a hash with the following keys: |
| 151 | # * <tt>:text</tt> -- Narrow the selection to elements that have this text |
| 152 | # value (string or regexp). |
| 153 | # * <tt>:html</tt> -- Narrow the selection to elements that have this HTML |
| 154 | # content (string or regexp). |
| 155 | # * <tt>:count</tt> -- Assertion is true if the number of selected elements |
| 156 | # is equal to this value. |
| 157 | # * <tt>:minimum</tt> -- Assertion is true if the number of selected |
| 158 | # elements is at least this value. |
| 159 | # * <tt>:maximum</tt> -- Assertion is true if the number of selected |
| 160 | # elements is at most this value. |
| 161 | # |
| 162 | # If the method is called with a block, once all equality tests are |
| 163 | # evaluated the block is called with an array of all matched elements. |
| 164 | # |
| 165 | # ==== Examples |
| 166 | # |
| 167 | # # At least one form element |
| 168 | # assert_select "form" |
| 169 | # |
| 170 | # # Form element includes four input fields |
| 171 | # assert_select "form input", 4 |
| 172 | # |
| 173 | # # Page title is "Welcome" |
| 174 | # assert_select "title", "Welcome" |
| 175 | # |
| 176 | # # Page title is "Welcome" and there is only one title element |
| 177 | # assert_select "title", {:count=>1, :text=>"Welcome"}, |
| 178 | # "Wrong title or more than one title element" |
| 179 | # |
| 180 | # # Page contains no forms |
| 181 | # assert_select "form", false, "This page must contain no forms" |
| 182 | # |
| 183 | # # Test the content and style |
| 184 | # assert_select "body div.header ul.menu" |
| 185 | # |
| 186 | # # Use substitution values |
| 187 | # assert_select "ol>li#?", /item-\d+/ |
| 188 | # |
| 189 | # # All input fields in the form have a name |
| 190 | # assert_select "form input" do |
| 191 | # assert_select "[name=?]", /.+/ # Not empty |
| 192 | # end |
| 193 | def assert_select(*args, &block) |
| 194 | # Start with optional element followed by mandatory selector. |
| 195 | arg = args.shift |
| 196 | |
| 197 | if arg.is_a?(HTML::Node) |
| 198 | # First argument is a node (tag or text, but also HTML root), |
| 199 | # so we know what we're selecting from. |
| 200 | root = arg |
| 201 | arg = args.shift |
| 202 | elsif arg == nil |
| 203 | # This usually happens when passing a node/element that |
| 204 | # happens to be nil. |
| 205 | raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" |
| 206 | elsif @selected |
| 207 | root = HTML::Node.new(nil) |
| 208 | root.children.concat @selected |
| 209 | else |
| 210 | # Otherwise just operate on the response document. |
| 211 | root = response_from_page_or_rjs |
| 212 | end |
| 213 | |
| 214 | # First or second argument is the selector: string and we pass |
| 215 | # all remaining arguments. Array and we pass the argument. Also |
| 216 | # accepts selector itself. |
| 217 | case arg |
| 218 | when String |
| 219 | selector = HTML::Selector.new(arg, args) |
| 220 | when Array |
| 221 | selector = HTML::Selector.new(*arg) |
| 222 | when HTML::Selector |
| 223 | selector = arg |
| 224 | else raise ArgumentError, "Expecting a selector as the first argument" |
| 225 | end |
| 226 | |
| 227 | # Next argument is used for equality tests. |
| 228 | equals = {} |
| 229 | case arg = args.shift |
| 230 | when Hash |
| 231 | equals = arg |
| 232 | when String, Regexp |
| 233 | equals[:text] = arg |
| 234 | when Integer |
| 235 | equals[:count] = arg |
| 236 | when Range |
| 237 | equals[:minimum] = arg.begin |
| 238 | equals[:maximum] = arg.end |
| 239 | when FalseClass |
| 240 | equals[:count] = 0 |
| 241 | when NilClass, TrueClass |
| 242 | equals[:minimum] = 1 |
| 243 | else raise ArgumentError, "I don't understand what you're trying to match" |
| 244 | end |
| 245 | |
| 246 | # By default we're looking for at least one match. |
| 247 | if equals[:count] |
| 248 | equals[:minimum] = equals[:maximum] = equals[:count] |
| 249 | else |
| 250 | equals[:minimum] = 1 unless equals[:minimum] |
| 251 | end |
| 252 | |
| 253 | # Last argument is the message we use if the assertion fails. |
| 254 | message = args.shift |
| 255 | #- message = "No match made with selector #{selector.inspect}" unless message |
| 256 | if args.shift |
| 257 | raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" |
| 258 | end |
| 259 | |
| 260 | matches = selector.select(root) |
| 261 | # If text/html, narrow down to those elements that match it. |
| 262 | content_mismatch = nil |
| 263 | if match_with = equals[:text] |
| 264 | matches.delete_if do |match| |
| 265 | text = "" |
| 266 | stack = match.children.reverse |
| 267 | while node = stack.pop |
| 268 | if node.tag? |
| 269 | stack.concat node.children.reverse |
| 270 | else |
| 271 | text << node.content |
| 272 | end |
| 273 | end |
| 274 | text.strip! unless NO_STRIP.include?(match.name) |
| 275 | unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) |
| 276 | content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, text) |
| 277 | true |
| 278 | end |
| 279 | end |
| 280 | elsif match_with = equals[:html] |
| 281 | matches.delete_if do |match| |
| 282 | html = match.children.map(&:to_s).join |
| 283 | html.strip! unless NO_STRIP.include?(match.name) |
| 284 | unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) |
| 285 | content_mismatch ||= build_message(message, "<?> expected but was\n<?>.", match_with, html) |
| 286 | true |
| 287 | end |
| 288 | end |
| 289 | end |
| 290 | # Expecting foo found bar element only if found zero, not if |
| 291 | # found one but expecting two. |
| 292 | message ||= content_mismatch if matches.empty? |
| 293 | # Test minimum/maximum occurrence. |
| 294 | min, max = equals[:minimum], equals[:maximum] |
| 295 | message = message || %(Expected #{count_description(min, max)} matching "#{selector.to_s}", found #{matches.size}.) |
| 296 | assert matches.size >= min, message if min |
| 297 | assert matches.size <= max, message if max |
| 298 | |
| 299 | # If a block is given call that block. Set @selected to allow |
| 300 | # nested assert_select, which can be nested several levels deep. |
| 301 | if block_given? && !matches.empty? |
| 302 | begin |
| 303 | in_scope, @selected = @selected, matches |
| 304 | yield matches |
| 305 | ensure |
| 306 | @selected = in_scope |
| 307 | end |
| 308 | end |
| 309 | |
| 310 | # Returns all matches elements. |
| 311 | matches |
| 312 | end |
| 313 | |
| 314 | def count_description(min, max) #:nodoc: |
| 315 | pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} |
| 316 | |
| 317 | if min && max && (max != min) |
| 318 | "between #{min} and #{max} elements" |
| 319 | elsif min && !(min == 1 && max == 1) |
| 320 | "at least #{min} #{pluralize['element', min]}" |
| 321 | elsif max |
| 322 | "at most #{max} #{pluralize['element', max]}" |
| 323 | end |
| 324 | end |
| 325 | |
| 326 | # :call-seq: |
| 327 | # assert_select_rjs(id?) { |elements| ... } |
| 328 | # assert_select_rjs(statement, id?) { |elements| ... } |
| 329 | # assert_select_rjs(:insert, position, id?) { |elements| ... } |
| 330 | # |
| 331 | # Selects content from the RJS response. |
| 332 | # |
| 333 | # === Narrowing down |
| 334 | # |
| 335 | # With no arguments, asserts that one or more elements are updated or |
| 336 | # inserted by RJS statements. |
| 337 | # |
| 338 | # Use the +id+ argument to narrow down the assertion to only statements |
| 339 | # that update or insert an element with that identifier. |
| 340 | # |
| 341 | # Use the first argument to narrow down assertions to only statements |
| 342 | # of that type. Possible values are <tt>:replace</tt>, <tt>:replace_html</tt>, |
| 343 | # <tt>:show</tt>, <tt>:hide</tt>, <tt>:toggle</tt>, <tt>:remove</tt> and |
| 344 | # <tt>:insert_html</tt>. |
| 345 | # |
| 346 | # Use the argument <tt>:insert</tt> followed by an insertion position to narrow |
| 347 | # down the assertion to only statements that insert elements in that |
| 348 | # position. Possible values are <tt>:top</tt>, <tt>:bottom</tt>, <tt>:before</tt> |
| 349 | # and <tt>:after</tt>. |
| 350 | # |
| 351 | # Using the <tt>:remove</tt> statement, you will be able to pass a block, but it will |
| 352 | # be ignored as there is no HTML passed for this statement. |
| 353 | # |
| 354 | # === Using blocks |
| 355 | # |
| 356 | # Without a block, #assert_select_rjs merely asserts that the response |
| 357 | # contains one or more RJS statements that replace or update content. |
| 358 | # |
| 359 | # With a block, #assert_select_rjs also selects all elements used in |
| 360 | # these statements and passes them to the block. Nested assertions are |
| 361 | # supported. |
| 362 | # |
| 363 | # Calling #assert_select_rjs with no arguments and using nested asserts |
| 364 | # asserts that the HTML content is returned by one or more RJS statements. |
| 365 | # Using #assert_select directly makes the same assertion on the content, |
| 366 | # but without distinguishing whether the content is returned in an HTML |
| 367 | # or JavaScript. |
| 368 | # |
| 369 | # ==== Examples |
| 370 | # |
| 371 | # # Replacing the element foo. |
| 372 | # # page.replace 'foo', ... |
| 373 | # assert_select_rjs :replace, "foo" |
| 374 | # |
| 375 | # # Replacing with the chained RJS proxy. |
| 376 | # # page[:foo].replace ... |
| 377 | # assert_select_rjs :chained_replace, 'foo' |
| 378 | # |
| 379 | # # Inserting into the element bar, top position. |
| 380 | # assert_select_rjs :insert, :top, "bar" |
| 381 | # |
| 382 | # # Remove the element bar |
| 383 | # assert_select_rjs :remove, "bar" |
| 384 | # |
| 385 | # # Changing the element foo, with an image. |
| 386 | # assert_select_rjs "foo" do |
| 387 | # assert_select "img[src=/images/logo.gif"" |
| 388 | # end |
| 389 | # |
| 390 | # # RJS inserts or updates a list with four items. |
| 391 | # assert_select_rjs do |
| 392 | # assert_select "ol>li", 4 |
| 393 | # end |
| 394 | # |
| 395 | # # The same, but shorter. |
| 396 | # assert_select "ol>li", 4 |
| 397 | def assert_select_rjs(*args, &block) |
| 398 | rjs_type = nil |
| 399 | arg = args.shift |
| 400 | |
| 401 | # If the first argument is a symbol, it's the type of RJS statement we're looking |
| 402 | # for (update, replace, insertion, etc). Otherwise, we're looking for just about |
| 403 | # any RJS statement. |
| 404 | if arg.is_a?(Symbol) |
| 405 | rjs_type = arg |
| 406 | |
| 407 | if rjs_type == :insert |
| 408 | arg = args.shift |
| 409 | insertion = "insert_#{arg}".to_sym |
| 410 | raise ArgumentError, "Unknown RJS insertion type #{arg}" unless RJS_STATEMENTS[insertion] |
| 411 | statement = "(#{RJS_STATEMENTS[insertion]})" |
| 412 | else |
| 413 | raise ArgumentError, "Unknown RJS statement type #{rjs_type}" unless RJS_STATEMENTS[rjs_type] |
| 414 | statement = "(#{RJS_STATEMENTS[rjs_type]})" |
| 415 | end |
| 416 | arg = args.shift |
| 417 | else |
| 418 | statement = "#{RJS_STATEMENTS[:any]}" |
| 419 | end |
| 420 | |
| 421 | # Next argument we're looking for is the element identifier. If missing, we pick |
| 422 | # any element. |
| 423 | if arg.is_a?(String) |
| 424 | id = Regexp.quote(arg) |
| 425 | arg = args.shift |
| 426 | else |
| 427 | id = "[^\"]*" |
| 428 | end |
| 429 | |
| 430 | pattern = |
| 431 | case rjs_type |
| 432 | when :chained_replace, :chained_replace_html |
| 433 | Regexp.new("\\$\\(\"#{id}\"\\)#{statement}\\(#{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) |
| 434 | when :remove, :show, :hide, :toggle |
| 435 | Regexp.new("#{statement}\\(\"#{id}\"\\)") |
| 436 | else |
| 437 | Regexp.new("#{statement}\\(\"#{id}\", #{RJS_PATTERN_HTML}\\)", Regexp::MULTILINE) |
| 438 | end |
| 439 | |
| 440 | # Duplicate the body since the next step involves destroying it. |
| 441 | matches = nil |
| 442 | case rjs_type |
| 443 | when :remove, :show, :hide, :toggle |
| 444 | matches = @response.body.match(pattern) |
| 445 | else |
| 446 | @response.body.gsub(pattern) do |match| |
| 447 | html = unescape_rjs($2) |
| 448 | matches ||= [] |
| 449 | matches.concat HTML::Document.new(html).root.children.select { |n| n.tag? } |
| 450 | "" |
| 451 | end |
| 452 | end |
| 453 | |
| 454 | if matches |
| 455 | assert_block("") { true } # to count the assertion |
| 456 | if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) |
| 457 | begin |
| 458 | in_scope, @selected = @selected, matches |
| 459 | yield matches |
| 460 | ensure |
| 461 | @selected = in_scope |
| 462 | end |
| 463 | end |
| 464 | matches |
| 465 | else |
| 466 | # RJS statement not found. |
| 467 | flunk args.shift || "No RJS statement that replaces or inserts HTML content." |
| 468 | end |
| 469 | end |
| 470 | |
| 471 | # :call-seq: |
| 472 | # assert_select_encoded(element?) { |elements| ... } |
| 473 | # |
| 474 | # Extracts the content of an element, treats it as encoded HTML and runs |
| 475 | # nested assertion on it. |
| 476 | # |
| 477 | # You typically call this method within another assertion to operate on |
| 478 | # all currently selected elements. You can also pass an element or array |
| 479 | # of elements. |
| 480 | # |
| 481 | # The content of each element is un-encoded, and wrapped in the root |
| 482 | # element +encoded+. It then calls the block with all un-encoded elements. |
| 483 | # |
| 484 | # ==== Examples |
| 485 | # # Selects all bold tags from within the title of an ATOM feed's entries (perhaps to nab a section name prefix) |
| 486 | # assert_select_feed :atom, 1.0 do |
| 487 | # # Select each entry item and then the title item |
| 488 | # assert_select "entry>title" do |
| 489 | # # Run assertions on the encoded title elements |
| 490 | # assert_select_encoded do |
| 491 | # assert_select "b" |
| 492 | # end |
| 493 | # end |
| 494 | # end |
| 495 | # |
| 496 | # |
| 497 | # # Selects all paragraph tags from within the description of an RSS feed |
| 498 | # assert_select_feed :rss, 2.0 do |
| 499 | # # Select description element of each feed item. |
| 500 | # assert_select "channel>item>description" do |
| 501 | # # Run assertions on the encoded elements. |
| 502 | # assert_select_encoded do |
| 503 | # assert_select "p" |
| 504 | # end |
| 505 | # end |
| 506 | # end |
| 507 | def assert_select_encoded(element = nil, &block) |
| 508 | case element |
| 509 | when Array |
| 510 | elements = element |
| 511 | when HTML::Node |
| 512 | elements = [element] |
| 513 | when nil |
| 514 | unless elements = @selected |
| 515 | raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" |
| 516 | end |
| 517 | else |
| 518 | raise ArgumentError, "Argument is optional, and may be node or array of nodes" |
| 519 | end |
| 520 | |
| 521 | fix_content = lambda do |node| |
| 522 | # Gets around a bug in the Rails 1.1 HTML parser. |
| 523 | node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { CGI.escapeHTML($1) } |
| 524 | end |
| 525 | |
| 526 | selected = elements.map do |element| |
| 527 | text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join |
| 528 | root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root |
| 529 | css_select(root, "encoded:root", &block)[0] |
| 530 | end |
| 531 | |
| 532 | begin |
| 533 | old_selected, @selected = @selected, selected |
| 534 | assert_select ":root", &block |
| 535 | ensure |
| 536 | @selected = old_selected |
| 537 | end |
| 538 | end |
| 539 | |
| 540 | # :call-seq: |
| 541 | # assert_select_email { } |
| 542 | # |
| 543 | # Extracts the body of an email and runs nested assertions on it. |
| 544 | # |
| 545 | # You must enable deliveries for this assertion to work, use: |
| 546 | # ActionMailer::Base.perform_deliveries = true |
| 547 | # |
| 548 | # ==== Examples |
| 549 | # |
| 550 | # assert_select_email do |
| 551 | # assert_select "h1", "Email alert" |
| 552 | # end |
| 553 | # |
| 554 | # assert_select_email do |
| 555 | # items = assert_select "ol>li" |
| 556 | # items.each do |
| 557 | # # Work with items here... |
| 558 | # end |
| 559 | # end |
| 560 | # |
| 561 | def assert_select_email(&block) |
| 562 | deliveries = ActionMailer::Base.deliveries |
| 563 | assert !deliveries.empty?, "No e-mail in delivery list" |
| 564 | |
| 565 | for delivery in deliveries |
| 566 | for part in delivery.parts |
| 567 | if part["Content-Type"].to_s =~ /^text\/html\W/ |
| 568 | root = HTML::Document.new(part.body).root |
| 569 | assert_select root, ":root", &block |
| 570 | end |
| 571 | end |
| 572 | end |
| 573 | end |
| 574 | |
| 575 | protected |
| 576 | unless const_defined?(:RJS_STATEMENTS) |
| 577 | RJS_STATEMENTS = { |
| 578 | :replace => /Element\.replace/, |
| 579 | :replace_html => /Element\.update/, |
| 580 | :chained_replace => /\.replace/, |
| 581 | :chained_replace_html => /\.update/, |
| 582 | :remove => /Element\.remove/, |
| 583 | :show => /Element\.show/, |
| 584 | :hide => /Element\.hide/, |
| 585 | :toggle => /Element\.toggle/ |
| 586 | } |
| 587 | RJS_INSERTIONS = [:top, :bottom, :before, :after] |
| 588 | RJS_INSERTIONS.each do |insertion| |
| 589 | RJS_STATEMENTS["insert_#{insertion}".to_sym] = Regexp.new(Regexp.quote("new Insertion.#{insertion.to_s.camelize}")) |
| 590 | end |
| 591 | RJS_STATEMENTS[:any] = Regexp.new("(#{RJS_STATEMENTS.values.join('|')})") |
| 592 | RJS_STATEMENTS[:insert_html] = Regexp.new(RJS_INSERTIONS.collect do | |