arbre2 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)