~michaelforrest/use-case-mapper/trunk

« back to all changes in this revision

Viewing changes to vendor/rails/actionpack/lib/action_controller/assertions/selector_assertions.rb

  • Committer: Michael Forrest
  • Date: 2010-10-15 16:28:50 UTC
  • Revision ID: michael.forrest@canonical.com-20101015162850-tj2vchanv0kr0dun
refrozeĀ gems

Show diffs side-by-side

added added

removed removed

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