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.
- checksums.yaml +7 -0
- data/.gitignore +30 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +75 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +93 -0
- data/LICENSE +20 -0
- data/README.md +92 -0
- data/Rakefile +7 -0
- data/arbre.gemspec +28 -0
- data/lib/arbre/child_element_collection.rb +86 -0
- data/lib/arbre/container.rb +20 -0
- data/lib/arbre/context.rb +83 -0
- data/lib/arbre/element/building.rb +151 -0
- data/lib/arbre/element.rb +194 -0
- data/lib/arbre/element_collection.rb +93 -0
- data/lib/arbre/html/attributes.rb +91 -0
- data/lib/arbre/html/class_list.rb +53 -0
- data/lib/arbre/html/comment.rb +47 -0
- data/lib/arbre/html/document.rb +93 -0
- data/lib/arbre/html/html_tags.rb +67 -0
- data/lib/arbre/html/querying.rb +256 -0
- data/lib/arbre/html/tag.rb +317 -0
- data/lib/arbre/rails/layouts.rb +126 -0
- data/lib/arbre/rails/legacy_document.rb +29 -0
- data/lib/arbre/rails/rendering.rb +76 -0
- data/lib/arbre/rails/rspec/arbre_support.rb +61 -0
- data/lib/arbre/rails/rspec.rb +2 -0
- data/lib/arbre/rails/template_handler.rb +32 -0
- data/lib/arbre/rails.rb +35 -0
- data/lib/arbre/rspec/be_rendered_as_matcher.rb +103 -0
- data/lib/arbre/rspec/be_scripted_as_matcher.rb +68 -0
- data/lib/arbre/rspec/contain_script_matcher.rb +64 -0
- data/lib/arbre/rspec.rb +3 -0
- data/lib/arbre/text_node.rb +35 -0
- data/lib/arbre/version.rb +3 -0
- data/lib/arbre.rb +27 -0
- data/spec/arbre/integration/html_document_spec.rb +90 -0
- data/spec/arbre/integration/html_spec.rb +283 -0
- data/spec/arbre/integration/querying_spec.rb +187 -0
- data/spec/arbre/integration/rails_spec.rb +183 -0
- data/spec/arbre/rails/rspec/arbre_support_spec.rb +75 -0
- data/spec/arbre/rspec/be_rendered_as_matcher_spec.rb +80 -0
- data/spec/arbre/rspec/be_scripted_as_matcher_spec.rb +61 -0
- data/spec/arbre/rspec/contain_script_matcher_spec.rb +40 -0
- data/spec/arbre/support/arbre_example_group.rb +0 -0
- data/spec/arbre/unit/child_element_collection_spec.rb +146 -0
- data/spec/arbre/unit/container_spec.rb +23 -0
- data/spec/arbre/unit/context_spec.rb +95 -0
- data/spec/arbre/unit/element/building_spec.rb +300 -0
- data/spec/arbre/unit/element_collection_spec.rb +169 -0
- data/spec/arbre/unit/element_spec.rb +297 -0
- data/spec/arbre/unit/html/attributes_spec.rb +219 -0
- data/spec/arbre/unit/html/class_list_spec.rb +109 -0
- data/spec/arbre/unit/html/comment_spec.rb +42 -0
- data/spec/arbre/unit/html/querying_spec.rb +32 -0
- data/spec/arbre/unit/html/tag_spec.rb +300 -0
- data/spec/arbre/unit/rails/layouts_spec.rb +127 -0
- data/spec/arbre/unit/text_node_spec.rb +40 -0
- data/spec/rails/app/controllers/example_controller.rb +18 -0
- data/spec/rails/app/views/example/_arbre_partial.html.arb +7 -0
- data/spec/rails/app/views/example/_erb_partial.html.erb +1 -0
- data/spec/rails/app/views/example/arbre.html.arb +1 -0
- data/spec/rails/app/views/example/arbre_partial_result.html.arb +3 -0
- data/spec/rails/app/views/example/erb.html.erb +5 -0
- data/spec/rails/app/views/example/erb_partial_result.html.arb +3 -0
- data/spec/rails/app/views/example/partials.html.arb +11 -0
- data/spec/rails/app/views/layouts/empty.html.arb +1 -0
- data/spec/rails/app/views/layouts/with_title.html.arb +5 -0
- data/spec/rails/config/routes.rb +4 -0
- data/spec/rails_spec_helper.rb +13 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/arbre_example_group.rb +19 -0
- 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,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
|
data/lib/arbre/rails.rb
ADDED
@@ -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
|
data/lib/arbre/rspec.rb
ADDED
@@ -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
|
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)
|