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