rspec-html 0.1.2 → 0.2.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 413a58c112d2fbb3af070285d637a63c860b6bcb66c4a4370f253c387d600cf5
4
- data.tar.gz: ecef355b3d1e0c766b76c2107298fdff7eca6b508b80fefecc2b01847ebb8f1f
3
+ metadata.gz: cf565ab0af76a2408b1185d8516aa6a18ac94bdf3521bd76235ffeab2cf85c84
4
+ data.tar.gz: cceb245c801bb10e3a84d942a8101014a585ac5b00e958bd682b6b0a3599143a
5
5
  SHA512:
6
- metadata.gz: c142dad521ba1f3b5ef4b789954d64eb0ae8b8a58ad437f3c73bf3341f167593eb0b7ea1a14b46c83994d536ece5f44a1a10f18c01e8d01f9822ec86cc7e3737
7
- data.tar.gz: 1d83c8fb486c4bc71da176760f29e9ed631fd9e02ac26531e40114ed4169ad4e556a42c6d0cbd2f2bbc4933b49b63897c3f8248a7078d8d485aff0ed3e704018
6
+ metadata.gz: 07ea3aedf2366de58c8c39af7ff8fcf17ff273ea1df71799215a0f19fdd029a0e32a8517b154e140175b6211d258d6376ef4faa40fdcb645ca4183196a4cec96
7
+ data.tar.gz: ca57887c542a4817181d1da196fc05f9739be992aa7a10b2cdb7aae293d066ddfc6c2009aeb204cfc315ac2745ef36ac1792fa29b22f11a09cb02cb441a7d14b
@@ -2,3 +2,84 @@ Metrics/BlockLength:
2
2
  Exclude:
3
3
  - 'spec/**/*'
4
4
  - 'rspec-html.gemspec'
5
+
6
+ Gemspec/RequiredRubyVersion:
7
+ Enabled: false
8
+
9
+ Layout/LineLength:
10
+ Max: 100
11
+
12
+ Layout/EmptyLinesAroundAttributeAccessor:
13
+ Enabled: true
14
+ Layout/SpaceAroundMethodCallOperator:
15
+ Enabled: true
16
+ Lint/DeprecatedOpenSSLConstant:
17
+ Enabled: true
18
+ Lint/DuplicateElsifCondition:
19
+ Enabled: true
20
+ Lint/MixedRegexpCaptureTypes:
21
+ Enabled: true
22
+ Lint/RaiseException:
23
+ Enabled: true
24
+ Lint/StructNewOverride:
25
+ Enabled: true
26
+ Style/AccessorGrouping:
27
+ Enabled: true
28
+ Style/ArrayCoercion:
29
+ Enabled: true
30
+ Style/BisectedAttrAccessor:
31
+ Enabled: true
32
+ Style/CaseLikeIf:
33
+ Enabled: true
34
+ Style/ExponentialNotation:
35
+ Enabled: true
36
+ Style/HashAsLastArrayItem:
37
+ Enabled: true
38
+ Style/HashEachMethods:
39
+ Enabled: true
40
+ Style/HashLikeCase:
41
+ Enabled: true
42
+ Style/HashTransformKeys:
43
+ Enabled: true
44
+ Style/HashTransformValues:
45
+ Enabled: true
46
+ Style/RedundantAssignment:
47
+ Enabled: true
48
+ Style/RedundantFetchBlock:
49
+ Enabled: true
50
+ Style/RedundantFileExtensionInRequire:
51
+ Enabled: true
52
+ Style/RedundantRegexpCharacterClass:
53
+ Enabled: true
54
+ Style/RedundantRegexpEscape:
55
+ Enabled: true
56
+ Style/SlicingWithRange:
57
+ Enabled: true
58
+ Lint/BinaryOperatorWithIdenticalOperands:
59
+ Enabled: true
60
+ Lint/DuplicateRescueException:
61
+ Enabled: true
62
+ Lint/EmptyConditionalBody:
63
+ Enabled: true
64
+ Lint/FloatComparison:
65
+ Enabled: true
66
+ Lint/MissingSuper:
67
+ Enabled: true
68
+ Lint/OutOfRangeRegexpRef:
69
+ Enabled: true
70
+ Lint/SelfAssignment:
71
+ Enabled: true
72
+ Lint/TopLevelReturnWithArgument:
73
+ Enabled: true
74
+ Lint/UnreachableLoop:
75
+ Enabled: true
76
+ Style/ExplicitBlockArgument:
77
+ Enabled: true
78
+ Style/GlobalStdStream:
79
+ Enabled: true
80
+ Style/OptionalBooleanParameter:
81
+ Enabled: true
82
+ Style/SingleArgumentDig:
83
+ Enabled: true
84
+ Style/StringConcatenation:
85
+ Enabled: true
@@ -1,38 +1,38 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-html (0.1.2)
4
+ rspec-html (0.2.3)
5
5
  nokogiri (~> 1.10)
6
6
  rspec (~> 3.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- ast (2.4.0)
12
- betterp (0.1.3)
13
- paint (~> 2.0)
14
- byebug (11.0.1)
15
- concurrent-ruby (1.1.5)
16
- diff-lcs (1.3)
17
- i18n (1.7.0)
11
+ ast (2.4.1)
12
+ byebug (11.1.3)
13
+ concurrent-ruby (1.1.6)
14
+ devpack (0.1.2)
15
+ diff-lcs (1.4.4)
16
+ i18n (1.8.5)
18
17
  concurrent-ruby (~> 1.0)
19
- jaro_winkler (1.5.4)
20
18
  mini_portile2 (2.4.0)
21
- nokogiri (1.10.9)
19
+ nokogiri (1.10.10)
22
20
  mini_portile2 (~> 2.4.0)
23
- paint (2.1.1)
24
- parallel (1.19.1)
25
- parser (2.6.5.0)
26
- ast (~> 2.4.0)
21
+ paint (2.2.0)
22
+ parallel (1.19.2)
23
+ parser (2.7.1.4)
24
+ ast (~> 2.4.1)
27
25
  rainbow (3.0.0)
28
- rake (10.5.0)
26
+ rake (13.0.1)
27
+ regexp_parser (1.7.1)
28
+ rexml (3.2.4)
29
29
  rspec (3.9.0)
30
30
  rspec-core (~> 3.9.0)
31
31
  rspec-expectations (~> 3.9.0)
32
32
  rspec-mocks (~> 3.9.0)
33
- rspec-core (3.9.0)
34
- rspec-support (~> 3.9.0)
35
- rspec-expectations (3.9.0)
33
+ rspec-core (3.9.2)
34
+ rspec-support (~> 3.9.3)
35
+ rspec-expectations (3.9.2)
36
36
  diff-lcs (>= 1.2.0, < 2.0)
37
37
  rspec-support (~> 3.9.0)
38
38
  rspec-its (1.3.0)
@@ -41,36 +41,40 @@ GEM
41
41
  rspec-mocks (3.9.1)
42
42
  diff-lcs (>= 1.2.0, < 2.0)
43
43
  rspec-support (~> 3.9.0)
44
- rspec-support (3.9.0)
45
- rubocop (0.76.0)
46
- jaro_winkler (~> 1.5.1)
44
+ rspec-support (3.9.3)
45
+ rubocop (0.89.1)
47
46
  parallel (~> 1.10)
48
- parser (>= 2.6)
47
+ parser (>= 2.7.1.1)
49
48
  rainbow (>= 2.2.2, < 4.0)
49
+ regexp_parser (>= 1.7)
50
+ rexml
51
+ rubocop-ast (>= 0.3.0, < 1.0)
50
52
  ruby-progressbar (~> 1.7)
51
- unicode-display_width (>= 1.4.0, < 1.7)
52
- rubocop-rspec (1.36.0)
53
- rubocop (>= 0.68.1)
53
+ unicode-display_width (>= 1.4.0, < 2.0)
54
+ rubocop-ast (0.3.0)
55
+ parser (>= 2.7.1.4)
56
+ rubocop-rspec (1.42.0)
57
+ rubocop (>= 0.87.0)
54
58
  ruby-progressbar (1.10.1)
55
- strong_versions (0.3.2)
56
- i18n (>= 0.5.0)
59
+ strong_versions (0.4.5)
60
+ i18n (>= 0.5)
57
61
  paint (~> 2.0)
58
- unicode-display_width (1.6.0)
62
+ unicode-display_width (1.7.0)
59
63
 
60
64
  PLATFORMS
61
65
  ruby
62
66
 
63
67
  DEPENDENCIES
64
- betterp (~> 0.1.3)
65
68
  bundler (~> 2.0)
66
69
  byebug (~> 11.0)
70
+ devpack (~> 0.1.2)
67
71
  i18n (~> 1.7)
68
- rake (~> 10.0)
72
+ rake (~> 13.0)
69
73
  rspec-html!
70
74
  rspec-its (~> 1.3)
71
- rubocop (~> 0.76.0)
75
+ rubocop (~> 0.89.1)
72
76
  rubocop-rspec (~> 1.36)
73
- strong_versions (~> 0.3.2)
77
+ strong_versions (~> 0.4.5)
74
78
 
75
79
  BUNDLED WITH
76
80
  2.0.2
data/README.md CHANGED
@@ -4,18 +4,17 @@ _RSpec::HTML_ provides a simple object interface to HTML responses from [_RSpec
4
4
 
5
5
  ## Installation
6
6
 
7
+ Add the gem to your `Gemfile`:
8
+
7
9
  ```ruby
8
- gem 'rspec-html', '~> 0.1.2'
10
+ gem 'rspec-html', '~> 0.2.3'
9
11
  ```
10
12
 
11
- Bundle
12
- And then execute:
13
-
14
- $ bundle
13
+ And rebuild your bundle:
15
14
 
16
- Or install it yourself as:
17
-
18
- $ gem install rspec-html
15
+ ```bash
16
+ $ bundle install
17
+ ```
19
18
 
20
19
  ## Usage
21
20
 
@@ -26,19 +25,118 @@ Require the gem in your `spec_helper.rb`:
26
25
  require 'rspec/html'
27
26
  ```
28
27
 
29
- In request specs, access the HTML document through the provided object interface:
28
+ Several [matchers](#matchers) are provided to identify text and _HTML_ elements within the _DOM_. These matchers can only be used with the provided [object interface](#object-interface).
29
+
30
+ ### Object Interface
31
+ <a name="object-interface"></a>
32
+
33
+ The top-level object `document` is available in all tests which reflects the current response body (e.g. in request specs).
34
+
35
+ If you need to parse _HTML_ manually you can use the provided `parse_html` helper and then access `document` as normal:
30
36
 
31
37
  ```ruby
32
- RSpec.describe 'something', type: :request do
33
- it 'does something' do
34
- get '/'
35
- expect(document.body).to include 'something'
36
- expect(document.body).to have_css 'html body div.myclass'
37
- expect(document.body).to have_xpath '//html/body/div[@class="myclass"]'
38
- end
38
+ before { parse_html('<html><body>hello</body></html>') }
39
+ it 'says hello' do
40
+ expect(document.body).to contain_text 'hello'
39
41
  end
40
42
  ```
41
43
 
44
+ To navigate the _DOM_ by a sequence of tag names use chained method calls on the `document` object:
45
+
46
+ #### Tag Traversal
47
+ ```ruby
48
+ expect(document.body.div.span).to contain_text 'some text'
49
+ ```
50
+
51
+ #### Attribute Matching
52
+ To select an element matching certain attributes pass a hash to any of the chained methods:
53
+ ```ruby
54
+ expect(document.body.div(id: 'my-div').span(align: 'left')).to contain_text 'some text'
55
+ ```
56
+
57
+ #### Class Matching
58
+ _CSS_ classes are treated as a special case: to select an element matching a set of classes pass the `class` parameter:
59
+ ```ruby
60
+ expect(document.body.div(id: 'my-div').span(class: 'my-class')).to contain_text 'some text'
61
+ expect(document.body.div(id: 'my-div').span(class: 'my-class my-other-class')).to contain_text 'some text'
62
+ ```
63
+
64
+ Classes can be provided in any order, i.e. `'my-class my-other-class'` is equivalent to `'my-other-class my-class'`.
65
+
66
+ #### Text Matching
67
+ To select an element that includes a given text string (i.e. excluding mark-up) use the `text` option:
68
+ ```ruby
69
+ expect(document.body.div(text: 'some text').input[:value]).to eql 'some-value'
70
+ ```
71
+
72
+ #### Attribute Retrieval
73
+ To select an attribute from an element use the hash-style interface:
74
+ ```ruby
75
+ expect(document.body.div.span[:class]).to contain_text 'my-class'
76
+ expect(document.body.div.span['data-content']).to contain_text 'my content'
77
+ ```
78
+
79
+ #### Indexing a Matching Set
80
+ To select an index from a set of matched elements use the array-style interface (the first matching element is `1`, not `0`):
81
+ ```ruby
82
+ expect(document.body.div[1].span[1][:class]).to contain_text 'my-class'
83
+ ```
84
+
85
+ #### Element Existence
86
+ To test if a matching element was found use the `exist` matcher:
87
+ ```ruby
88
+ expect(document.body.div[1]).to exist
89
+ expect(document.body.div[4]).to_not exist
90
+ ```
91
+
92
+ #### Length of matched attributes
93
+ To test the length of matched elements use the `#size` or `#length` method:
94
+ ```ruby
95
+ expect(document.body.div.size).to eql 3
96
+ expect(document.body.div.length).to eql 3
97
+ ```
98
+
99
+ #### XPath / CSS Selectors
100
+ If you need something more specific you can always use the _Nokogiri_ `#xpath` and `#css` methods on any element:
101
+ ```ruby
102
+ expect(document.body.xpath('//span[@class="my-class"]')).to contain_text 'some text'
103
+ expect(document.body.css('span.my-class')).to contain_text 'some text'
104
+ ```
105
+
106
+ To simply check that an _XPath_ or _CSS_ selector exists use `have_xpath` and `have_css`:
107
+ ```ruby
108
+ expect(document.body).to have_css 'html body div.myclass'
109
+ expect(document.body).to have_xpath '//html/body/div[@class="myclass"]'
110
+ ```
111
+
112
+ ### Custom Matchers
113
+ <a name="matchers"></a>
114
+
115
+ #### contain_text
116
+
117
+ Use the `contain_text` matcher to locate text within a _DOM_ element. All mark-up elements are stripped when using this matcher.
118
+
119
+ ```ruby
120
+ expect(document.body.form).to contain_text 'Please enter your password'
121
+ ```
122
+
123
+ #### contain_tag
124
+
125
+ Use the `contain_tag` matcher to locate _DOM_ elements within any given element. This matcher accepts two arguments:
126
+
127
+ * The tag name of the element you want to match (e.g. `:div`);
128
+ * _(Optional)_ A hash of options. All options supported by the [object interface](#object-interface) can be used here.
129
+
130
+ Without options:
131
+ ```ruby
132
+ expect(document.div(class: 'my-class')).to contain_tag :span
133
+ ```
134
+
135
+ With options:
136
+ ```ruby
137
+ expect(document.form(class: 'my-form')).to contain_tag :input, name: 'email', class: 'email-input'
138
+ ```
139
+
42
140
  ## Contributing
43
141
 
44
142
  Feel free to make a pull request.
@@ -9,7 +9,17 @@ module RSpec
9
9
  # Module extension for RSpec::SharedContext
10
10
  module HTML
11
11
  def document
12
- RSpecHTML::Document.new(response.body)
12
+ return @document if @document
13
+
14
+ if !defined?(response) || response.nil?
15
+ raise RSpecHTML::NoResponseError, 'No `response` object found. Make a request first.'
16
+ end
17
+
18
+ RSpecHTML::Element.new(Nokogiri::HTML.parse(response.body), :document)
19
+ end
20
+
21
+ def parse_html(content)
22
+ @document = RSpecHTML::Element.new(Nokogiri::HTML.parse(content), :document)
13
23
  end
14
24
  end
15
25
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module HTML
5
- VERSION = '0.1.2'
5
+ VERSION = '0.2.3'
6
6
  end
7
7
  end
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'nokogiri'
4
+
4
5
  require 'pathname'
6
+ require 'forwardable'
5
7
 
6
- require 'rspec_html/nameable'
7
- require 'rspec_html/searchable'
8
- require 'rspec_html/body'
9
- require 'rspec_html/document'
10
- require 'rspec_html/head'
11
- require 'rspec_html/title'
8
+ require 'rspec_html/tags'
9
+ require 'rspec_html/element'
10
+ require 'rspec_html/search'
11
+ require 'rspec_html/reconstituted_element'
12
+ require 'rspec_html/matchers'
12
13
 
13
14
  # Support module for rspec/html
14
15
  module RSpecHTML
16
+ class Error < StandardError; end
17
+ class NoResponseError < Error; end
15
18
  def self.root
16
19
  Pathname.new(__dir__).parent
17
20
  end
18
21
  end
22
+
23
+ RSpec.configure { |config| config.include RSpecHTML::Matchers }
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ # HTML DOM element abstraction
5
+ class Element
6
+ attr_reader :name, :element
7
+
8
+ extend Forwardable
9
+
10
+ def_delegators :@search,
11
+ :has_css?, :has_xpath?, :include?, :present?, :exist?,
12
+ :text, :size, :length, :[]
13
+
14
+ def initialize(element, name, options: {}, siblings: [])
15
+ @name = name
16
+ @element = element
17
+ @options = options
18
+ @siblings = siblings
19
+ @search = Search.new(@element, @siblings)
20
+ end
21
+
22
+ def inspect
23
+ "<#{self.class}::#{name.to_s.capitalize}>"
24
+ end
25
+
26
+ def to_s
27
+ @element.to_s
28
+ end
29
+
30
+ Tags.each do |tag|
31
+ define_method tag.downcase do |*args|
32
+ options = args.first
33
+ return @search.new_from_find(tag.downcase, options) if options.nil?
34
+
35
+ @search.new_from_where(tag.downcase, options)
36
+ end
37
+ end
38
+
39
+ def reconstituted
40
+ self.class.reconstituted(name, @options)
41
+ end
42
+
43
+ def self.reconstituted(tag, options = {})
44
+ ReconstitutedElement.new(tag, options).to_s
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec_html/matchers/base'
4
+ require 'rspec_html/matchers/contain_text'
5
+ require 'rspec_html/matchers/contain_tag'
6
+
7
+ module RSpecHTML
8
+ # Provides matchers for identifying elements and text within a DOM element.
9
+ module Matchers
10
+ extend RSpec::Matchers::DSL
11
+ extend RSpec::Matchers::DSL::Macros
12
+
13
+ # rubocop:disable Metrics/MethodLength
14
+ def self.define_matcher(name, class_)
15
+ matcher name do |expected, options|
16
+ rspec_html_matcher = class_.new(expected, options || {})
17
+ match do |actual|
18
+ rspec_html_matcher
19
+ .save_actual(actual)
20
+ .match(actual)
21
+ .tap { @actual = rspec_html_matcher.rspec_actual }
22
+ end
23
+ description { rspec_html_matcher.description }
24
+ failure_message { rspec_html_matcher.failure_message }
25
+ diffable if class_.diffable?
26
+ end
27
+ end
28
+ # rubocop:enable Metrics/MethodLength
29
+
30
+ define_matcher(:contain_text, ContainText)
31
+ define_matcher(:contain_tag, ContainTag)
32
+ end
33
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ module Matchers
5
+ # Mix-in class to provide a uniform interface and message templating for all matchers.
6
+ module Base
7
+ def self.included(base)
8
+ base.class_eval do
9
+ class << self
10
+ def diffable
11
+ @diffable = true
12
+ end
13
+
14
+ def diffable?
15
+ @diffable
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ attr_reader :rspec_actual
22
+
23
+ def initialize(expected, options)
24
+ @expected = expected
25
+ @options = options
26
+ end
27
+
28
+ def description
29
+ template(:description, @options, @expected)
30
+ end
31
+
32
+ def failure_message
33
+ template(:failure, @options, @expected, @actual)
34
+ end
35
+
36
+ def save_actual(actual)
37
+ @actual = actual
38
+ self
39
+ end
40
+
41
+ def reconstituted(element, options)
42
+ RSpecHTML::Element.reconstituted(element, options)
43
+ end
44
+
45
+ private
46
+
47
+ def template(type, options, expected, actual = nil)
48
+ ERB.new(template_path(type).read).result(binding)
49
+ end
50
+
51
+ def template_path(type)
52
+ RSpecHTML.root.join('templates', type.to_s, "#{filename}.erb")
53
+ end
54
+
55
+ def filename
56
+ _, _, name = self.class.name.rpartition('::')
57
+ (name[0] + name[1..].gsub(/(.)([A-Z])/, '\1_\2')).downcase
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ module Matchers
5
+ # Matches elements within a given DOM element.
6
+ class ContainTag
7
+ include Base
8
+
9
+ def match(actual)
10
+ @actual = actual.to_s
11
+ actual.public_send(@expected, @options).present?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ module Matchers
5
+ # Matches text within a given DOM element.
6
+ class ContainText
7
+ include Base
8
+
9
+ diffable
10
+
11
+ def match(actual)
12
+ @rspec_actual = actual&.text
13
+ (actual&.text || '').include?(@expected.to_s)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ # Reconstructs an HTML representation of an element from provided parameters.
5
+ class ReconstitutedElement
6
+ def initialize(tag, options)
7
+ @tag = tag
8
+ @options = options
9
+ end
10
+
11
+ def to_s
12
+ name = @tag.to_s.downcase
13
+ return '#document' if name == 'document'
14
+ return name if name == 'document'
15
+ return "<#{name}#{formatted_attributes} />" unless @options&.key?(:text)
16
+
17
+ "<#{name}#{formatted_attributes}>#{@options[:text]}</#{name}>"
18
+ end
19
+
20
+ private
21
+
22
+ def mapped_attributes
23
+ return [] if @options.nil?
24
+
25
+ @options.reject { |key| key.to_sym == :text }.map do |key, value|
26
+ next %(#{key}="#{value}") unless key.to_sym == :class && value.is_a?(Array)
27
+
28
+ %(#{key}="#{value.join(' ')}")
29
+ end
30
+ end
31
+
32
+ def formatted_attributes
33
+ mapped_attributes.empty? ? nil : " #{mapped_attributes.join(' ')}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ # Provides element/attribute/text searching for HTML entities
5
+ class Search
6
+ def initialize(element, siblings)
7
+ @element = element
8
+ @siblings = siblings
9
+ end
10
+
11
+ def include?(val)
12
+ text.include?(val)
13
+ end
14
+
15
+ def css(*args)
16
+ self.class.new(@element&.css(*args), :css)
17
+ end
18
+
19
+ def xpath(*args)
20
+ self.class.new(@element&.xpath(*args), :xpath)
21
+ end
22
+
23
+ def present?
24
+ !@element.nil?
25
+ end
26
+ alias exist? present?
27
+
28
+ # rubocop:disable Naming/PredicateName
29
+ def has_css?(*args)
30
+ !@element&.css(*args)&.empty?
31
+ end
32
+
33
+ def has_xpath?(*args)
34
+ !@element&.xpath(*args)&.empty?
35
+ end
36
+ # rubocop:enable Naming/PredicateName
37
+
38
+ def [](val)
39
+ return index(val) if val.is_a?(Integer)
40
+ return range(val) if val.is_a?(Range)
41
+
42
+ @element&.attr(val.to_s)
43
+ end
44
+
45
+ def text
46
+ @element&.text&.gsub(/\s+/, ' ')&.strip || ''
47
+ end
48
+
49
+ def size
50
+ return @element.size if @element.respond_to?(:size)
51
+
52
+ @siblings.size
53
+ end
54
+ alias length size
55
+
56
+ def new_from_find(tag, options)
57
+ Element.new(
58
+ find(tag),
59
+ tag,
60
+ options: options,
61
+ siblings: find(tag, all: true)
62
+ )
63
+ end
64
+
65
+ def new_from_where(tag, options)
66
+ Element.new(
67
+ where(tag, options),
68
+ tag,
69
+ options: options,
70
+ siblings: where(tag, options, all: true)
71
+ )
72
+ end
73
+
74
+ private
75
+
76
+ def index(val)
77
+ zero_index_error if val.zero?
78
+ self.class.new(@siblings[val - 1], @element.name)
79
+ end
80
+
81
+ def range(val)
82
+ zero_index_error if val.first.zero?
83
+ self.class.new(@siblings[(val.first - 1)..(val.last - 1)], :range)
84
+ end
85
+
86
+ def zero_index_error
87
+ raise ArgumentError, 'Index for matched sets starts at 1, not 0.'
88
+ end
89
+
90
+ def where(tag, query, all: false)
91
+ matched = if query[:class]
92
+ where_class(tag, query[:class]) & where_xpath(tag, query.merge(class: nil))
93
+ else
94
+ where_xpath(tag, query)
95
+ end
96
+ return matched&.first unless all
97
+
98
+ matched
99
+ end
100
+
101
+ def where_xpath(tag, query)
102
+ conditions = "[#{where_conditions(query)}]" unless query.compact.empty?
103
+ @element&.xpath("//#{tag}#{conditions}")
104
+ end
105
+
106
+ def where_conditions(query)
107
+ query.compact.map do |key, value|
108
+ next if value.nil?
109
+ next %(@#{key}="#{value}") unless key == :text
110
+
111
+ %[contains(text(),"#{value}")]
112
+ end.join ' and '
113
+ end
114
+
115
+ def where_class(tag, class_or_classes)
116
+ classes = class_or_classes.is_a?(Array) ? class_or_classes : class_or_classes.to_s.split
117
+ selector = classes.map(&:to_s).join('.')
118
+ @element&.css("#{tag}.#{selector}")
119
+ end
120
+
121
+ def find(tag, all: false)
122
+ return @element&.css(tag.to_s)&.first unless all
123
+
124
+ @element&.css(tag.to_s)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecHTML
4
+ # HTML tag identification, used to determine viability for DOM traversal via chained methods.
5
+ class Tags
6
+ def self.include?(val)
7
+ tags.include?(val.to_s.upcase)
8
+ end
9
+
10
+ def self.each(&block)
11
+ tags.each { |tag| block.call(tag) }
12
+ end
13
+
14
+ # rubocop:disable Metrics/MethodLength
15
+ def self.tags
16
+ %w[
17
+ A ABBR ACRONYM ADDRESS APPLET AREA ARTICLE ASIDE AUDIO B BASE BASEFONT BDI BDO BGSOUND
18
+ BIG BLINK BLOCKQUOTE BODY BR BUTTON CANVAS CAPTION CENTER CITE CODE COL COLGROUP COMMAND
19
+ CONTENT DATA DATALIST DD DEL DETAILS DFN DIALOG DIR DIV DL DT ELEMENT EM EMBED FIELDSET
20
+ FIGCAPTION FIGURE FONT FOOTER FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HEADER HGROUP HR
21
+ HTML I IFRAME IMAGE IMG INPUT INS ISINDEX KBD KEYGEN LABEL LEGEND LI LINK LISTING MAIN
22
+ MAIN MAP MARK MARQUEE MENU MENUITEM META METER MULTICOL NAV NEXTID NOBR NOEMBED NOFRAMES
23
+ NOSCRIPT OBJECT OL OPTGROUP OPTION OUTPUT P PARAM PICTURE PLAINTEXT PRE PROGRESS Q RB RP
24
+ RT RTC RUBY S SAMP SCRIPT SECTION SELECT SHADOW SLOT SMALL SOURCE SPACER SPAN STRIKE
25
+ STRONG STYLE SUB SUMMARY SUP TABLE TBODY TD TEMPLATE TEXTAREA TFOOT TH THEAD TIME TITLE
26
+ TR TRACK TT U UL VAR VIDEO WBR XMP
27
+ ]
28
+ end
29
+ # rubocop:enable Metrics/MethodLength
30
+ end
31
+ end
@@ -31,13 +31,13 @@ Gem::Specification.new do |spec|
31
31
  spec.add_dependency 'nokogiri', '~> 1.10'
32
32
  spec.add_dependency 'rspec', '~> 3.0'
33
33
 
34
- spec.add_development_dependency 'betterp', '~> 0.1.3'
35
34
  spec.add_development_dependency 'bundler', '~> 2.0'
36
35
  spec.add_development_dependency 'byebug', '~> 11.0'
36
+ spec.add_development_dependency 'devpack', '~> 0.1.2'
37
37
  spec.add_development_dependency 'i18n', '~> 1.7'
38
- spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'rake', '~> 13.0'
39
39
  spec.add_development_dependency 'rspec-its', '~> 1.3'
40
- spec.add_development_dependency 'rubocop', '~> 0.76.0'
40
+ spec.add_development_dependency 'rubocop', '~> 0.89.1'
41
41
  spec.add_development_dependency 'rubocop-rspec', '~> 1.36'
42
- spec.add_development_dependency 'strong_versions', '~> 0.3.2'
42
+ spec.add_development_dependency 'strong_versions', '~> 0.4.5'
43
43
  end
@@ -0,0 +1 @@
1
+ contain tag <%= RSpecHTML::Element.reconstituted(expected, @options) %>
@@ -0,0 +1 @@
1
+ contain text <%= expected.inspect %>
@@ -0,0 +1,5 @@
1
+ <% if actual.element.nil? %>
2
+ Expected <%= reconstituted(actual, @options) %> to contain <%= reconstituted(expected, @options) %> but the element did not exist.
3
+ <% else %>
4
+ Expected <%= reconstituted(actual, @options) %> to contain <%= reconstituted(expected, @options) %> but it did not.
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% if actual.element.nil? %>
2
+ Expected <%= actual.reconstituted %> to contain <%= expected.inspect %> but the element did not exist.
3
+ <% else %>
4
+ Expected text in <%= actual.reconstituted %> <%= (actual.text&.strip || '').inspect %> to contain <%= expected.inspect %> but it did not.
5
+ <% end %>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-html
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-27 00:00:00.000000000 Z
11
+ date: 2020-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -39,47 +39,47 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '3.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: betterp
42
+ name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.1.3
47
+ version: '2.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 0.1.3
54
+ version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: bundler
56
+ name: byebug
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.0'
61
+ version: '11.0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '2.0'
68
+ version: '11.0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: byebug
70
+ name: devpack
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '11.0'
75
+ version: 0.1.2
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '11.0'
82
+ version: 0.1.2
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: i18n
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '10.0'
103
+ version: '13.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '10.0'
110
+ version: '13.0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rspec-its
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.76.0
131
+ version: 0.89.1
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.76.0
138
+ version: 0.89.1
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rubocop-rspec
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 0.3.2
159
+ version: 0.4.5
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 0.3.2
166
+ version: 0.4.5
167
167
  description: HTML document abstraction and matchers for RSpec
168
168
  email:
169
169
  - git@bob.frl
@@ -187,13 +187,19 @@ files:
187
187
  - lib/rspec/html.rb
188
188
  - lib/rspec/html/version.rb
189
189
  - lib/rspec_html.rb
190
- - lib/rspec_html/body.rb
191
- - lib/rspec_html/document.rb
192
- - lib/rspec_html/head.rb
193
- - lib/rspec_html/nameable.rb
194
- - lib/rspec_html/searchable.rb
195
- - lib/rspec_html/title.rb
190
+ - lib/rspec_html/element.rb
191
+ - lib/rspec_html/matchers.rb
192
+ - lib/rspec_html/matchers/base.rb
193
+ - lib/rspec_html/matchers/contain_tag.rb
194
+ - lib/rspec_html/matchers/contain_text.rb
195
+ - lib/rspec_html/reconstituted_element.rb
196
+ - lib/rspec_html/search.rb
197
+ - lib/rspec_html/tags.rb
196
198
  - rspec-html.gemspec
199
+ - templates/description/contain_tag.erb
200
+ - templates/description/contain_text.erb
201
+ - templates/failure/contain_tag.erb
202
+ - templates/failure/contain_text.erb
197
203
  homepage: https://github.com/bobf/rspec-html
198
204
  licenses:
199
205
  - MIT
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # HTML/BODY abstraction
5
- class Body
6
- include Searchable
7
- include Nameable
8
-
9
- def initialize(parsed_html)
10
- @name = :body
11
- @entity = parsed_html.css('body')
12
- end
13
- end
14
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # HTML Document representation
5
- class Document
6
- def initialize(html)
7
- @html = html
8
- end
9
-
10
- # rubocop:disable Naming/PredicateName
11
- def has_xpath?(*args)
12
- !parsed_html.xpath(*args).empty?
13
- end
14
-
15
- def has_css?(*args)
16
- !parsed_html.css(*args).empty?
17
- end
18
- # rubocop:enable Naming/PredicateName
19
-
20
- def body
21
- Body.new(parsed_html)
22
- end
23
-
24
- def head
25
- Head.new(parsed_html)
26
- end
27
-
28
- private
29
-
30
- def parsed_html
31
- @parsed_html ||= Nokogiri::HTML(@html)
32
- end
33
- end
34
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # HTML/BODY abstraction
5
- class Head
6
- include Nameable
7
-
8
- def initialize(parsed_html)
9
- @parsed_html = parsed_html
10
- @entity = parsed_html.css('head')
11
- @name = :head
12
- end
13
-
14
- def title
15
- Title.new(@parsed_html)
16
- end
17
-
18
- def include?(val)
19
- title.include?(val)
20
- end
21
- end
22
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # Mixin module providing methods allowing an entity to specify its name
5
- module Nameable
6
- attr_reader :name
7
- end
8
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # Mixin module providing methods for searching text content of HTML entities
5
- module Searchable
6
- def include?(val)
7
- @entity.text.include?(val)
8
- end
9
-
10
- def to_s
11
- @entity.text.strip
12
- end
13
-
14
- def inspect
15
- %("#{self}")
16
- end
17
- end
18
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RSpecHTML
4
- # HTML/HEAD/TITLE abstraction
5
- class Title
6
- include Searchable
7
- include Nameable
8
-
9
- def initialize(parsed_html)
10
- @name = :title
11
- @entity = parsed_html.css('head title')
12
- end
13
- end
14
- end