arbre2 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +75 -0
  7. data/Gemfile +2 -0
  8. data/Gemfile.lock +93 -0
  9. data/LICENSE +20 -0
  10. data/README.md +92 -0
  11. data/Rakefile +7 -0
  12. data/arbre.gemspec +28 -0
  13. data/lib/arbre/child_element_collection.rb +86 -0
  14. data/lib/arbre/container.rb +20 -0
  15. data/lib/arbre/context.rb +83 -0
  16. data/lib/arbre/element/building.rb +151 -0
  17. data/lib/arbre/element.rb +194 -0
  18. data/lib/arbre/element_collection.rb +93 -0
  19. data/lib/arbre/html/attributes.rb +91 -0
  20. data/lib/arbre/html/class_list.rb +53 -0
  21. data/lib/arbre/html/comment.rb +47 -0
  22. data/lib/arbre/html/document.rb +93 -0
  23. data/lib/arbre/html/html_tags.rb +67 -0
  24. data/lib/arbre/html/querying.rb +256 -0
  25. data/lib/arbre/html/tag.rb +317 -0
  26. data/lib/arbre/rails/layouts.rb +126 -0
  27. data/lib/arbre/rails/legacy_document.rb +29 -0
  28. data/lib/arbre/rails/rendering.rb +76 -0
  29. data/lib/arbre/rails/rspec/arbre_support.rb +61 -0
  30. data/lib/arbre/rails/rspec.rb +2 -0
  31. data/lib/arbre/rails/template_handler.rb +32 -0
  32. data/lib/arbre/rails.rb +35 -0
  33. data/lib/arbre/rspec/be_rendered_as_matcher.rb +103 -0
  34. data/lib/arbre/rspec/be_scripted_as_matcher.rb +68 -0
  35. data/lib/arbre/rspec/contain_script_matcher.rb +64 -0
  36. data/lib/arbre/rspec.rb +3 -0
  37. data/lib/arbre/text_node.rb +35 -0
  38. data/lib/arbre/version.rb +3 -0
  39. data/lib/arbre.rb +27 -0
  40. data/spec/arbre/integration/html_document_spec.rb +90 -0
  41. data/spec/arbre/integration/html_spec.rb +283 -0
  42. data/spec/arbre/integration/querying_spec.rb +187 -0
  43. data/spec/arbre/integration/rails_spec.rb +183 -0
  44. data/spec/arbre/rails/rspec/arbre_support_spec.rb +75 -0
  45. data/spec/arbre/rspec/be_rendered_as_matcher_spec.rb +80 -0
  46. data/spec/arbre/rspec/be_scripted_as_matcher_spec.rb +61 -0
  47. data/spec/arbre/rspec/contain_script_matcher_spec.rb +40 -0
  48. data/spec/arbre/support/arbre_example_group.rb +0 -0
  49. data/spec/arbre/unit/child_element_collection_spec.rb +146 -0
  50. data/spec/arbre/unit/container_spec.rb +23 -0
  51. data/spec/arbre/unit/context_spec.rb +95 -0
  52. data/spec/arbre/unit/element/building_spec.rb +300 -0
  53. data/spec/arbre/unit/element_collection_spec.rb +169 -0
  54. data/spec/arbre/unit/element_spec.rb +297 -0
  55. data/spec/arbre/unit/html/attributes_spec.rb +219 -0
  56. data/spec/arbre/unit/html/class_list_spec.rb +109 -0
  57. data/spec/arbre/unit/html/comment_spec.rb +42 -0
  58. data/spec/arbre/unit/html/querying_spec.rb +32 -0
  59. data/spec/arbre/unit/html/tag_spec.rb +300 -0
  60. data/spec/arbre/unit/rails/layouts_spec.rb +127 -0
  61. data/spec/arbre/unit/text_node_spec.rb +40 -0
  62. data/spec/rails/app/controllers/example_controller.rb +18 -0
  63. data/spec/rails/app/views/example/_arbre_partial.html.arb +7 -0
  64. data/spec/rails/app/views/example/_erb_partial.html.erb +1 -0
  65. data/spec/rails/app/views/example/arbre.html.arb +1 -0
  66. data/spec/rails/app/views/example/arbre_partial_result.html.arb +3 -0
  67. data/spec/rails/app/views/example/erb.html.erb +5 -0
  68. data/spec/rails/app/views/example/erb_partial_result.html.arb +3 -0
  69. data/spec/rails/app/views/example/partials.html.arb +11 -0
  70. data/spec/rails/app/views/layouts/empty.html.arb +1 -0
  71. data/spec/rails/app/views/layouts/with_title.html.arb +5 -0
  72. data/spec/rails/config/routes.rb +4 -0
  73. data/spec/rails_spec_helper.rb +13 -0
  74. data/spec/spec_helper.rb +20 -0
  75. data/spec/support/arbre_example_group.rb +19 -0
  76. metadata +254 -0
@@ -0,0 +1,126 @@
1
+ module Arbre
2
+ module Rails
3
+
4
+ # Provides a content-template / layout-template construct to be used with Arbre.
5
+ #
6
+ # The idea is that the 'content' view defines the type of document to use, through the {ContextMethods#document}
7
+ # method, and that the 'layout' view/views provide post-processing steps to the document, and subsequently render
8
+ # the document class.
9
+ #
10
+ # == Usage example
11
+ #
12
+ # *+app/views/session/new.html.arb+:*
13
+ #
14
+ # document LoginScreen do
15
+ # ...
16
+ # end
17
+ #
18
+ # *+app/views/layouts/application.html.arb+:*
19
+ #
20
+ # layout do
21
+ # self.title = "#{self.title} | Application Name"
22
+ # end
23
+ #
24
+ # In this case, the content template (+session/new.html.arb+) defines that the content document should be a +LoginScreen+
25
+ # class, and provides a block to customize the screen.
26
+ #
27
+ # The layout template (+layouts/application.html.arb+) provides some common steps that should be applied to all pages
28
+ # using this layout. This block is executed *after* the content customization block.
29
+ #
30
+ # == Content block
31
+ #
32
+ # The content block that is specified will be passed to the document class' +build+ method, meaning that it can be used
33
+ # to customize the document in place. However, if the block is not used by the class, it is "instance-exec'd" on the
34
+ # document instance, so that all documents can be customized in a content template.
35
+ #
36
+ # For example, imagine that class +LoginScreen < Arbre::Html::Document+ does not accept or call the block passed into
37
+ # its +build+ method. The following will still be possible:
38
+ #
39
+ # *+app/views/session/new.html.arb+:*
40
+ #
41
+ # document LoginScreen do
42
+ # after 'fieldset.password' do
43
+ # link forgot_password_path, 'forgot password?'
44
+ # end
45
+ # end
46
+ module Layouts
47
+
48
+ # Describes a content document and build options.
49
+ # @api internal
50
+ class ContentDocument < Struct.new(:klass, :build_arguments, :content_block); end
51
+
52
+ ######
53
+ # ControllerMethods concern
54
+
55
+ module ControllerMethods
56
+
57
+ protected
58
+
59
+ # See {ContextMethods#document} below.
60
+ # @api internal
61
+ def arbre_document(klass = nil, *build_arguments, &content_block)
62
+ @arbre_document = ContentDocument.new(klass, build_arguments, content_block) if klass
63
+ @arbre_document
64
+ end
65
+
66
+ end
67
+
68
+ ######
69
+ # ContextMethods
70
+
71
+ module ContextMethods
72
+
73
+ # Obtains and/or sets the current content document.
74
+ #
75
+ # @overload document
76
+ # Gets an object representing the content document to create. This is mostly for internal use.
77
+ #
78
+ # @overload document(klass, *build_arguments, &content_block)
79
+ # Specifies a document class and arguments to use. You may optionally customize it using a block.
80
+ #
81
+ # @param [Class] klass The document class to use.
82
+ # @param [Array] build_arguments Any arguments to pass to the document's +build+ method.
83
+ # @param [Proc] content_block A block to be executed as the content block. See {Layouts above}
84
+ # for more info.
85
+ def document(klass = nil, *build_arguments, &content_block)
86
+ controller.send :arbre_document, klass, *build_arguments, &content_block
87
+ end
88
+
89
+ # Provides a layout block to the current view and appends the current document
90
+ # to the current arbre element.
91
+ #
92
+ # @return [Arbre::Html::Document] The rendered document.
93
+ def layout(&layout_block)
94
+ doc = if document && document.content_block
95
+ # Use the specified document with the specified content block.
96
+
97
+ # Detect whether the block is called.
98
+ block_called = false
99
+ content_block = document.content_block
100
+ doc = append(document.klass, *document.build_arguments) do |*args|
101
+ block_called = true
102
+ instance_exec *args, &content_block
103
+ end
104
+
105
+ # Call the block anyway if it hasn't been called yet.
106
+ doc.instance_exec &document.content_block unless block_called
107
+ doc
108
+
109
+ elsif document
110
+ # Use the specified document.
111
+ append document.klass, *document.build_arguments
112
+ else
113
+ # Append an empty document.
114
+ append Arbre::Rails.legacy_document
115
+ end
116
+
117
+ # Run the layout block unless this is an AJAX request.
118
+ doc.instance_exec &layout_block if layout_block && !request.xhr?
119
+ doc
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,29 @@
1
+ module Arbre
2
+ module Rails
3
+
4
+ # This document adds "legacy" ActionView content to the right places.
5
+ class LegacyDocument < Html::Document
6
+
7
+ def build!
8
+ super
9
+
10
+ head do
11
+ text_node helpers.content_for(:head)
12
+ end
13
+ body do
14
+ text_node helpers.content_for(:layout)
15
+ end
16
+ end
17
+
18
+ def to_s
19
+ if request.xhr?
20
+ body.content
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ module Arbre
2
+ module Rails
3
+
4
+ # Template rendering strategies for Arbre.
5
+ #
6
+ # == Partials
7
+ #
8
+ # Simply use the method {#partial} instead of +render :partial => '...'+.
9
+ #
10
+ # *+edit.html.arb+:*
11
+ #
12
+ # fieldset do
13
+ # partial 'fieldset'
14
+ # end
15
+ #
16
+ # *+_fieldset.html.arb+:*
17
+ #
18
+ # # Note: `self' is the same `self' as in the template above!
19
+ # label :something
20
+ # text_field :something
21
+ #
22
+ # To pass another context than +self+:
23
+ #
24
+ # *+edit.html.arb+:*
25
+ #
26
+ # form do |form|
27
+ # fieldset do
28
+ # partial 'fieldset', context: form
29
+ # end
30
+ # end
31
+ #
32
+ # *+_fieldset.html.arb+:*
33
+ #
34
+ # # Note: `self' is now the form defined in the template above.
35
+ # label :something
36
+ # text_field :something
37
+ #
38
+ module Rendering
39
+
40
+ # Inserts a partial into the current flow.
41
+ #
42
+ # @param [Hash] locals
43
+ # Extra local variables for the partial.
44
+ # @option [Element] context
45
+ # The context which is used to render the partial. This can be any element, and
46
+ # is typically the calling element. The partial template may refer to this as
47
+ # +self+.
48
+ def partial(name, context: self, **locals)
49
+ render :partial => name, :locals => locals.merge(:arbre_context => context)
50
+ end
51
+
52
+ # Uses the given arguments to perform an ActionView render. If the result is an Arbre
53
+ # context, instead of treating it as a string, its children are added to the current
54
+ # element.
55
+ def render(*args, locals: {}, **options)
56
+ locals = locals.merge(:arbre_output_context => true)
57
+ result = helpers.render(*args, locals: locals, **options)
58
+
59
+ case result
60
+ when Arbre::Context
61
+ # Append all the context's children to the current element. However, watch out as
62
+ # the children collection is modified during this operation. We'll first create
63
+ # a copy.
64
+ current_element.children.concat result.children.to_a
65
+ when Arbre::Element
66
+ current_element.children << result
67
+ else
68
+ current_element.children << TextNode.from_string(result) if result.length > 0
69
+ end
70
+ result
71
+ end
72
+
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ module Arbre
2
+ module Rails
3
+ module RSpec
4
+
5
+ # Adds support for an Arbre context.
6
+ module ArbreSupport
7
+
8
+ # Simulates how ArbreTemplateHandler sets up a context, but using the context
9
+ # build block.
10
+ def arbre_context
11
+ @arbre_context ||= Arbre::Context.new(assigns, helpers)
12
+ end
13
+ alias_method :arbre, :arbre_context
14
+
15
+ # Override to provide default assigns (or define +let(:assigns) {...}+).
16
+ def assigns
17
+ @assigns ||= {}
18
+ end
19
+
20
+ # Override to provide default helpers (or define +let(:helpers) {...}+).
21
+ def helpers
22
+ @helpers ||= build_helpers
23
+ end
24
+
25
+ def build_helpers
26
+ helpers = ActionView::Base.new
27
+
28
+ # Simulate a default controller & request.
29
+ allow(helpers).to receive(:controller).and_return(controller)
30
+ allow(helpers).to receive(:request).and_return(request)
31
+
32
+ # Include all application controller's helpers.
33
+ if defined?(ApplicationController)
34
+ helpers.singleton_class.send :include, ApplicationController._helpers
35
+ end
36
+
37
+ # Stub asset_path
38
+ allow(helpers).to receive(:asset_path) { |asset| "/assets/#{asset}" }
39
+
40
+ helpers
41
+ end
42
+
43
+ def controller
44
+ @controller ||= ActionController::Base.new.tap do |controller|
45
+ controller.request = request
46
+ end
47
+ end
48
+
49
+ def request
50
+ @request ||= ActionDispatch::Request.new('rack.input' => '')
51
+ end
52
+
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+
59
+ RSpec.configure do |config|
60
+ config.include Arbre::Rails::RSpec::ArbreSupport, arbre: true
61
+ end
@@ -0,0 +1,2 @@
1
+ require 'arbre/rspec'
2
+ require 'arbre/rails/rspec/arbre_support'
@@ -0,0 +1,32 @@
1
+ module Arbre
2
+ module Rails
3
+
4
+ # Template handler capable of re-using an arbre context. If the method or local variable +arbre_context+
5
+ # yields an Arbre context, it is re-used. Note that this may very well be an element as well. The template
6
+ # source is executed on the found 'context' or on a new {Arbre::Context} if it was not found.
7
+ #
8
+ # @see Partials
9
+ class TemplateHandler
10
+
11
+ # Readable version:
12
+ #
13
+ # _arbre_reuse_context = defined?(arbre_context)
14
+ # _arbre_ctx = _arbre_reuse_context ? arbre_context : Arbre::Context.new(assigns, self)
15
+ # _arbre_ctx.instance_exec { <template source> }
16
+ #
17
+ # if _arbre_reuse_context
18
+ # ''
19
+ # elsif defined?(arbre_output_context)
20
+ # _arbre_ctx
21
+ # else
22
+ # _arbre_ctx.to_html
23
+ # end
24
+
25
+ def call(template)
26
+ "_arbre_reuse_context = defined?(arbre_context); _arbre_ctx = _arbre_reuse_context ? arbre_context : Arbre::Context.new(assigns, self); _arbre_ctx.instance_exec { #{template.source}\n}; _arbre_reuse_context ? '' : defined?(arbre_output_context) ? _arbre_ctx : _arbre_ctx.to_html"
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require 'arbre/rails/template_handler'
2
+ require 'arbre/rails/legacy_document'
3
+ require 'arbre/rails/layouts'
4
+ require 'arbre/rails/rendering'
5
+
6
+ module Arbre
7
+ module Rails
8
+ class << self
9
+ attr_accessor :legacy_document
10
+ def legacy_document
11
+ @legacy_document ||= Rails::LegacyDocument
12
+ end
13
+ end
14
+ end
15
+
16
+ class Railtie < ::Rails::Railtie
17
+
18
+ initializer "arbre.add_autoload_paths" do |app|
19
+ ActiveSupport::Dependencies.autoload_paths << "#{app.config.root}/app/views/arbre"
20
+ end
21
+
22
+ initializer "arbre.add_layout_support" do
23
+ ActionController::Base.send :include, Arbre::Rails::Layouts::ControllerMethods
24
+ Arbre::Context.send :include, Arbre::Rails::Layouts::ContextMethods
25
+ end
26
+
27
+ initializer "arbre.register_template_handler" do
28
+ ActionView::Template.register_template_handler :arb, Arbre::Rails::TemplateHandler.new
29
+ end
30
+
31
+ end
32
+
33
+ Element.send :include, Rails::Rendering
34
+ Element.send :include, Rails::Layouts
35
+ end
@@ -0,0 +1,103 @@
1
+ require 'erb'
2
+
3
+ module Arbre
4
+ module RSpec
5
+
6
+ # Used to match HTML snippets. HTML is canonized as much as possible, so that you don't have to worry about
7
+ # whitespace or attribute order. Use '(...)' as a wildcard. You may even place text in the wildcard for
8
+ # readability: '(... some wildcard ...)'.
9
+ #
10
+ # == Examples
11
+ #
12
+ # expect('<a href="test" target="_blank"/>').to \
13
+ # be_rendered_as('<a target="_blank" href="test" />') # => true
14
+ # expect('<div><span></span><sub></sub></div>').to \
15
+ # be_rendered_as('<div>(...)</div>') # => true
16
+ # expect('<div><span></span><sub></sub></div>').to \
17
+ # be_rendered_as('<div>(... a span here ...)<sub></sub></div>') # => true
18
+ class BeRenderedAsMatcher
19
+
20
+ def initialize(expected, escape: true)
21
+ @expected = expected
22
+ @escape = escape
23
+ end
24
+
25
+ attr_reader :expected
26
+
27
+ def description
28
+ "be rendered as #{expected}"
29
+ end
30
+
31
+ def matches?(actual)
32
+ @actual = actual
33
+
34
+ regexp = case expected
35
+ when Regexp then Regexp.new(canonize_html(expected.source))
36
+ else Regexp.new('^' + Regexp.escape(canonize_html(expected)).gsub(/\\\(\\\.\\\.\\\..*?(?:\\\.\\\.\\\.)?\\\)/, '.+') + '$')
37
+ end
38
+
39
+ html = actual.to_s
40
+ html = ERB::Util.html_escape(html) if @escape
41
+ canonize_html(html) =~ regexp
42
+ end
43
+
44
+ def canonize_html(html)
45
+ html = html.dup
46
+
47
+ html.gsub! /(\s*[\n\r]\s*)+/, ' '
48
+ html.gsub! /^\s+|\s+$/, ''
49
+ html.gsub! /\s*(\/?>|<)\s*/, '\1'
50
+
51
+ # Extract and order attributes.
52
+ html.gsub! %r|(<[-_:\w].*?>)| do |all|
53
+ all =~ %r|(<[-_:\w]+\s*)(.*?)(\s*/?>)|
54
+ _, pre, attributes, post = $~.to_a
55
+
56
+ has_wildcard = attributes =~ /\(\.\.\..*?(?:\.\.\.)?\)/ && attributes =~ /\w+/
57
+ attributes = attributes.gsub(/\(\.\.\..*?(?:\.\.\.)?\)/, '') if has_wildcard
58
+ attributes = attributes.scan(/(\(\.\.\..*?(?:\.\.\.)?\)|[-_:\w]+(?:=(?:".*?"|'.*?'|\S+))?)/).sort.join(' ')
59
+ if has_wildcard
60
+ attributes = "(...)#{attributes}(...)"
61
+ pre.chomp! ' '
62
+ end
63
+
64
+ "#{pre}#{attributes}#{post}"
65
+ end
66
+
67
+ # Extract and order classes and styles
68
+ html.gsub! %r[(class|style)="(.*?)"] do |all|
69
+ all =~ %r[(class|style)="(.*?)"]
70
+
71
+ attribute = $1
72
+ items = $2.strip.split(/(;\s*|\s+)/).reject(&:blank?).sort.join(' ')
73
+ %[#{attribute}="#{items}"]
74
+ end
75
+
76
+ html
77
+ end
78
+
79
+ def failure_message_for_should
80
+ <<-MSG.gsub(/^\s{10}/, '')
81
+ expected that element of type #{@actual.class} would be rendered differently:
82
+ expected: #{expected.is_a?(Regexp) ? '/' + canonize_html(expected.source) + '/' : canonize_html(expected)} (#{expected.class})
83
+ got: #{@actual.nil? ? 'nil' : canonize_html(ERB::Util.html_escape(@actual.to_s))}
84
+ MSG
85
+ end
86
+
87
+ def failure_message_for_should_not
88
+ <<-MSG.gsub(/^\s{10}/, '')
89
+ expected that element of type #{@actual.class} would not be rendered as #{canonize_html(expected)}, but it was:
90
+ got: #{@actual.nil? ? 'nil' : canonize_html(ERB::Util.html_escape(@actual.to_s))}
91
+ MSG
92
+ end
93
+
94
+ end
95
+
96
+ end
97
+ end
98
+
99
+ RSpec::Matchers.module_eval do
100
+ def be_rendered_as(expected, escape: true)
101
+ Arbre::RSpec::BeRenderedAsMatcher.new(expected, escape: escape)
102
+ end
103
+ end
@@ -0,0 +1,68 @@
1
+ module Arbre
2
+ module RSpec
3
+
4
+ # Used to match JS snippets in HTML content. The JS is canonized as much as possible, so that you don't have to
5
+ # worry about whitespace. Use '(...)' as a wildcard. You may even place text in the wildcard for
6
+ # readability: '(... some wildcard ...)'.
7
+ #
8
+ # == Examples
9
+ #
10
+ # expect(document.body.find_first('javascript')).to be_scripted_as('Flux.Application.initialize()')
11
+ # expect(document.body.find_first('javascript')).to be_scripted_as('Flux.Application.initialize((... args ...))')
12
+ class BeScriptedAsMatcher
13
+
14
+ def initialize(expected)
15
+ @expected = expected
16
+ end
17
+
18
+ attr_reader :expected
19
+
20
+ def description
21
+ "be scripted as #{expected}"
22
+ end
23
+
24
+ def matches?(actual)
25
+ @actual = actual
26
+
27
+ regexp = case expected
28
+ when Regexp then Regexp.new(canonize_js(expected.source))
29
+ else Regexp.new('^' + Regexp.escape(canonize_js(expected)).gsub('\(\.\.\.\)', '.+') + '$')
30
+ end
31
+
32
+ canonize_js(actual.content) =~ regexp
33
+ end
34
+
35
+ def canonize_js(js)
36
+ js = js.dup
37
+
38
+ js.gsub! %r|//\s+<!\[CDATA\[[\n\r]+|, ''
39
+ js.gsub! %r|[\n\r]+//\s+\]\]>|, ''
40
+
41
+ js.gsub! /(\s*[\n\r]\s*)+/, ' '
42
+ js.gsub! /^\s+|\s+$/, ''
43
+
44
+ js
45
+ end
46
+
47
+ def failure_message_for_should
48
+ <<-MSG.gsub(/^\s{10}/, '')
49
+ expected that element of type #{@actual.class} would be scripted differently:
50
+ expected: #{expected.is_a?(Regexp) ? '/' + canonize_js(expected.source) + '/' : canonize_js(expected)} (#{expected.class})
51
+ got: #{@actual.nil? ? 'nil' : canonize_js(@actual.content)}
52
+ MSG
53
+ end
54
+
55
+ def failure_message_for_should_not
56
+ "expected that element of type #{@actual.class} would be not scripted as #{expected}"
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+ RSpec::Matchers.module_eval do
65
+ def be_scripted_as(script)
66
+ Arbre::RSpec::BeScriptedAsMatcher.new(script)
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ module Arbre
2
+ module RSpec
3
+
4
+ # Used to match JS snippets in HTML content. The JS is canonized as much as possible, so that you don't have to
5
+ # worry about whitespace. Use '(...)' as a wildcard. You may even place text in the wildcard for
6
+ # readability: '(... some wildcard ...)'.
7
+ #
8
+ # == Examples
9
+ #
10
+ # expect(document.body).to contain_script('Flux.Application.initialize()')
11
+ # expect(document.body).to contain_script('Flux.Application.initialize((... args ...))')
12
+ class ContainScriptMatcher
13
+
14
+ def initialize(expected)
15
+ @expected = expected
16
+ end
17
+
18
+ attr_reader :expected
19
+
20
+ def description
21
+ "contain script #{expected}"
22
+ end
23
+
24
+ def matches?(actual)
25
+ @actual = actual
26
+ canonize_js(actual.to_s).include?(canonize_js(expected))
27
+ end
28
+
29
+ def canonize_js(js)
30
+ js = js.dup
31
+
32
+ js.gsub! %r|//\s+<!\[CDATA\[[\n\r]+|, ''
33
+ js.gsub! %r|[\n\r]+//\s+\]\]>|, ''
34
+
35
+ js.gsub! /(\s*[\n\r]\s*)+/, ' '
36
+ js.gsub! /^\s+|\s+$/, ''
37
+
38
+ js
39
+ end
40
+
41
+ def failure_message_for_should
42
+ <<-MSG.gsub(/^\s{10}/, '')
43
+ expected that element of type #{@actual.class} contained script:
44
+ expected: #{expected.is_a?(Regexp) ? '/' + canonize_js(expected.source) + '/' : canonize_js(expected)} (#{expected.class})
45
+ got: #{@actual.nil? ? 'nil' : canonize_js(@actual.to_s)}
46
+ MSG
47
+ end
48
+
49
+ def failure_message_for_should_not
50
+ <<-MSG.gsub(/^\s{10}/, '')
51
+ expected that element of type #{actual.class} would not contain a script:
52
+ script: #{expected.is_a?(Regexp) ? '/' + canonize_js(expected.source) + '/' : canonize_js(expected)} (#{expected.class})
53
+ MSG
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+ RSpec::Matchers.module_eval do
61
+ def contain_script(script)
62
+ Arbre::RSpec::ContainScriptMatcher.new(script)
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ require 'arbre/rspec/be_rendered_as_matcher'
2
+ require 'arbre/rspec/be_scripted_as_matcher'
3
+ require 'arbre/rspec/contain_script_matcher'
@@ -0,0 +1,35 @@
1
+ require 'erb'
2
+
3
+ module Arbre
4
+
5
+ # A 'raw' text node - it just outputs the HTML escaped version of the string it's
6
+ # built with.
7
+ class TextNode < Element
8
+ builder_method :text_node
9
+
10
+ # Builds a raw element from a string.
11
+ def self.from_string(string)
12
+ new.tap { |node| node.build!(string) }
13
+ end
14
+
15
+ def children
16
+ @children ||= ElementCollection.new.tap do |children|
17
+ def children.<<(*) raise NotImplementedError end
18
+ def children.add(*) raise NotImplementedError end
19
+ def children.concat(*) raise NotImplementedError end
20
+ end
21
+ end
22
+
23
+ attr_reader :text
24
+
25
+ def build!(text)
26
+ @text = text.to_s
27
+ end
28
+
29
+ def to_s
30
+ text
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,3 @@
1
+ module Arbre
2
+ VERSION = "2.1.0"
3
+ end
data/lib/arbre.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'active_support/inflector'
2
+ require 'active_support/dependencies/autoload'
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'active_support/core_ext/string/output_safety'
5
+
6
+ module Arbre
7
+ module Html
8
+ end
9
+ end
10
+
11
+ require 'arbre/element'
12
+ require 'arbre/container'
13
+ require 'arbre/context'
14
+ require 'arbre/text_node'
15
+
16
+ require 'arbre/element_collection'
17
+ require 'arbre/child_element_collection'
18
+
19
+ require 'arbre/html/attributes'
20
+ require 'arbre/html/class_list'
21
+ require 'arbre/html/querying'
22
+ require 'arbre/html/tag'
23
+ require 'arbre/html/comment'
24
+ require 'arbre/html/html_tags'
25
+ require 'arbre/html/document'
26
+
27
+ require 'arbre/rails' if defined?(Rails)