rspec-html 0.2.0 → 0.2.5
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 +4 -4
- data/.rubocop.yml +37 -1
- data/.ruby-version +1 -1
- data/Gemfile.lock +7 -7
- data/README.md +71 -17
- data/lib/rspec/html.rb +10 -0
- data/lib/rspec/html/version.rb +1 -1
- data/lib/rspec_html.rb +10 -3
- data/lib/rspec_html/element.rb +41 -3
- data/lib/rspec_html/matchers.rb +33 -0
- data/lib/rspec_html/matchers/base.rb +61 -0
- data/lib/rspec_html/matchers/contain_tag.rb +15 -0
- data/lib/rspec_html/matchers/contain_text.rb +17 -0
- data/lib/rspec_html/reconstituted_element.rb +36 -0
- data/lib/rspec_html/search.rb +134 -0
- data/lib/rspec_html/tags.rb +4 -0
- data/rspec-html.gemspec +1 -1
- data/templates/description/contain_tag.erb +1 -0
- data/templates/description/contain_text.erb +1 -0
- data/templates/failure/contain_tag.erb +5 -0
- data/templates/failure/contain_text.erb +5 -0
- metadata +18 -10
- data/lib/rspec_html/nameable.rb +0 -8
- data/lib/rspec_html/searchable.rb +0 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5241cc27dba3e36c05cd7595280908be306d659d6611e02b70d58984a2f8900
|
4
|
+
data.tar.gz: 18e5487083e27fe812565b15c0411026ba622cb82c0bbbeb36787167928dab48
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39065b13e1a1d959fcebecc3004b20e2940b560c06a9789d39b1ffa99c249f4cb0062c3888ed6af69e44d2523ff8ed4ea66989c4e35e3e924764610c51900414
|
7
|
+
data.tar.gz: 4c275980c39e8f9506a03cbb8ceca6bfdf16304aaa37f4eba318ae0e085106cfc5502f34148d4d6dfe86d7a000d145e39015ab20471ee812447f16d7b09d60b3
|
data/.rubocop.yml
CHANGED
@@ -3,6 +3,16 @@ Metrics/BlockLength:
|
|
3
3
|
- 'spec/**/*'
|
4
4
|
- 'rspec-html.gemspec'
|
5
5
|
|
6
|
+
Gemspec/RequiredRubyVersion:
|
7
|
+
Enabled: false
|
8
|
+
|
9
|
+
Layout/LineLength:
|
10
|
+
Max: 100
|
11
|
+
|
12
|
+
# Compatibility with older (< 2.6) Rubies.
|
13
|
+
Style/SlicingWithRange:
|
14
|
+
Enabled: false
|
15
|
+
|
6
16
|
Layout/EmptyLinesAroundAttributeAccessor:
|
7
17
|
Enabled: true
|
8
18
|
Layout/SpaceAroundMethodCallOperator:
|
@@ -47,5 +57,31 @@ Style/RedundantRegexpCharacterClass:
|
|
47
57
|
Enabled: true
|
48
58
|
Style/RedundantRegexpEscape:
|
49
59
|
Enabled: true
|
50
|
-
|
60
|
+
Lint/BinaryOperatorWithIdenticalOperands:
|
61
|
+
Enabled: true
|
62
|
+
Lint/DuplicateRescueException:
|
63
|
+
Enabled: true
|
64
|
+
Lint/EmptyConditionalBody:
|
65
|
+
Enabled: true
|
66
|
+
Lint/FloatComparison:
|
67
|
+
Enabled: true
|
68
|
+
Lint/MissingSuper:
|
69
|
+
Enabled: true
|
70
|
+
Lint/OutOfRangeRegexpRef:
|
71
|
+
Enabled: true
|
72
|
+
Lint/SelfAssignment:
|
73
|
+
Enabled: true
|
74
|
+
Lint/TopLevelReturnWithArgument:
|
75
|
+
Enabled: true
|
76
|
+
Lint/UnreachableLoop:
|
77
|
+
Enabled: true
|
78
|
+
Style/ExplicitBlockArgument:
|
79
|
+
Enabled: true
|
80
|
+
Style/GlobalStdStream:
|
81
|
+
Enabled: true
|
82
|
+
Style/OptionalBooleanParameter:
|
83
|
+
Enabled: true
|
84
|
+
Style/SingleArgumentDig:
|
85
|
+
Enabled: true
|
86
|
+
Style/StringConcatenation:
|
51
87
|
Enabled: true
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
2.7.1
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
rspec-html (0.2.
|
4
|
+
rspec-html (0.2.5)
|
5
5
|
nokogiri (~> 1.10)
|
6
6
|
rspec (~> 3.0)
|
7
7
|
|
@@ -42,17 +42,17 @@ GEM
|
|
42
42
|
diff-lcs (>= 1.2.0, < 2.0)
|
43
43
|
rspec-support (~> 3.9.0)
|
44
44
|
rspec-support (3.9.3)
|
45
|
-
rubocop (0.
|
45
|
+
rubocop (0.89.1)
|
46
46
|
parallel (~> 1.10)
|
47
47
|
parser (>= 2.7.1.1)
|
48
48
|
rainbow (>= 2.2.2, < 4.0)
|
49
49
|
regexp_parser (>= 1.7)
|
50
50
|
rexml
|
51
|
-
rubocop-ast (>= 0.
|
51
|
+
rubocop-ast (>= 0.3.0, < 1.0)
|
52
52
|
ruby-progressbar (~> 1.7)
|
53
53
|
unicode-display_width (>= 1.4.0, < 2.0)
|
54
|
-
rubocop-ast (0.
|
55
|
-
parser (>= 2.7.
|
54
|
+
rubocop-ast (0.3.0)
|
55
|
+
parser (>= 2.7.1.4)
|
56
56
|
rubocop-rspec (1.42.0)
|
57
57
|
rubocop (>= 0.87.0)
|
58
58
|
ruby-progressbar (1.10.1)
|
@@ -72,9 +72,9 @@ DEPENDENCIES
|
|
72
72
|
rake (~> 13.0)
|
73
73
|
rspec-html!
|
74
74
|
rspec-its (~> 1.3)
|
75
|
-
rubocop (~> 0.
|
75
|
+
rubocop (~> 0.89.1)
|
76
76
|
rubocop-rspec (~> 1.36)
|
77
77
|
strong_versions (~> 0.4.5)
|
78
78
|
|
79
79
|
BUNDLED WITH
|
80
|
-
2.
|
80
|
+
2.1.4
|
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.2.
|
10
|
+
gem 'rspec-html', '~> 0.2.5'
|
9
11
|
```
|
10
12
|
|
11
|
-
|
12
|
-
And then execute:
|
13
|
-
|
14
|
-
$ bundle
|
13
|
+
And rebuild your bundle:
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
```bash
|
16
|
+
$ bundle install
|
17
|
+
```
|
19
18
|
|
20
19
|
## Usage
|
21
20
|
|
@@ -26,34 +25,61 @@ Require the gem in your `spec_helper.rb`:
|
|
26
25
|
require 'rspec/html'
|
27
26
|
```
|
28
27
|
|
29
|
-
|
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).
|
30
29
|
|
31
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:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
before { parse_html('<html><body>hello</body></html>') }
|
39
|
+
it 'says hello' do
|
40
|
+
expect(document.body).to contain_text 'hello'
|
41
|
+
end
|
42
|
+
```
|
32
43
|
|
33
|
-
To
|
44
|
+
To navigate the _DOM_ by a sequence of tag names use chained method calls on the `document` object:
|
34
45
|
|
35
46
|
#### Tag Traversal
|
36
47
|
```ruby
|
37
|
-
expect(document.body.div.span).to
|
48
|
+
expect(document.body.div.span).to contain_text 'some text'
|
38
49
|
```
|
39
50
|
|
40
51
|
#### Attribute Matching
|
41
52
|
To select an element matching certain attributes pass a hash to any of the chained methods:
|
42
53
|
```ruby
|
43
|
-
expect(document.body.div(id: 'my-div').span(
|
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'
|
44
70
|
```
|
45
71
|
|
46
72
|
#### Attribute Retrieval
|
47
73
|
To select an attribute from an element use the hash-style interface:
|
48
74
|
```ruby
|
49
|
-
expect(document.body.div.span[:class]).to
|
50
|
-
expect(document.body.div.span['data-content']).to
|
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'
|
51
77
|
```
|
52
78
|
|
53
79
|
#### Indexing a Matching Set
|
54
80
|
To select an index from a set of matched elements use the array-style interface (the first matching element is `1`, not `0`):
|
55
81
|
```ruby
|
56
|
-
expect(document.body.div[1].span[1][:class]).to
|
82
|
+
expect(document.body.div[1].span[1][:class]).to contain_text 'my-class'
|
57
83
|
```
|
58
84
|
|
59
85
|
#### Element Existence
|
@@ -73,8 +99,8 @@ expect(document.body.div.length).to eql 3
|
|
73
99
|
#### XPath / CSS Selectors
|
74
100
|
If you need something more specific you can always use the _Nokogiri_ `#xpath` and `#css` methods on any element:
|
75
101
|
```ruby
|
76
|
-
expect(document.body.xpath('//span[@class="my-class"]')).to
|
77
|
-
expect(document.body.css('span.my-class')).to
|
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'
|
78
104
|
```
|
79
105
|
|
80
106
|
To simply check that an _XPath_ or _CSS_ selector exists use `have_xpath` and `have_css`:
|
@@ -83,6 +109,34 @@ expect(document.body).to have_css 'html body div.myclass'
|
|
83
109
|
expect(document.body).to have_xpath '//html/body/div[@class="myclass"]'
|
84
110
|
```
|
85
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
|
+
|
86
140
|
## Contributing
|
87
141
|
|
88
142
|
Feel free to make a pull request.
|
data/lib/rspec/html.rb
CHANGED
@@ -9,8 +9,18 @@ module RSpec
|
|
9
9
|
# Module extension for RSpec::SharedContext
|
10
10
|
module HTML
|
11
11
|
def document
|
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
|
+
|
12
18
|
RSpecHTML::Element.new(Nokogiri::HTML.parse(response.body), :document)
|
13
19
|
end
|
20
|
+
|
21
|
+
def parse_html(content)
|
22
|
+
@document = RSpecHTML::Element.new(Nokogiri::HTML.parse(content), :document)
|
23
|
+
end
|
14
24
|
end
|
15
25
|
end
|
16
26
|
|
data/lib/rspec/html/version.rb
CHANGED
data/lib/rspec_html.rb
CHANGED
@@ -1,16 +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/element'
|
9
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'
|
10
13
|
|
11
14
|
# Support module for rspec/html
|
12
15
|
module RSpecHTML
|
16
|
+
class Error < StandardError; end
|
17
|
+
class NoResponseError < Error; end
|
13
18
|
def self.root
|
14
19
|
Pathname.new(__dir__).parent
|
15
20
|
end
|
16
21
|
end
|
22
|
+
|
23
|
+
RSpec.configure { |config| config.include RSpecHTML::Matchers }
|
data/lib/rspec_html/element.rb
CHANGED
@@ -3,13 +3,51 @@
|
|
3
3
|
module RSpecHTML
|
4
4
|
# HTML DOM element abstraction
|
5
5
|
class Element
|
6
|
-
|
7
|
-
include Nameable
|
6
|
+
attr_reader :name, :element
|
8
7
|
|
9
|
-
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :@search,
|
11
|
+
:has_css?, :has_xpath?, :include?, :text, :truncated_text, :size, :length, :[]
|
12
|
+
|
13
|
+
def initialize(element, name, options: {}, siblings: [])
|
10
14
|
@name = name
|
11
15
|
@element = element
|
16
|
+
@options = options
|
12
17
|
@siblings = siblings
|
18
|
+
@search = Search.new(@element, @siblings)
|
19
|
+
end
|
20
|
+
|
21
|
+
def present?
|
22
|
+
return true if name == :document
|
23
|
+
|
24
|
+
@search.present?
|
25
|
+
end
|
26
|
+
alias exist? present?
|
27
|
+
|
28
|
+
def inspect
|
29
|
+
"<#{self.class}::#{name.to_s.capitalize}>"
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
@element.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
Tags.each do |tag|
|
37
|
+
define_method tag.downcase do |*args|
|
38
|
+
options = args.first
|
39
|
+
return @search.new_from_find(tag.downcase, options) if options.nil?
|
40
|
+
|
41
|
+
@search.new_from_where(tag.downcase, options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def reconstituted
|
46
|
+
self.class.reconstituted(name, @options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.reconstituted(tag, options = {})
|
50
|
+
ReconstitutedElement.new(tag, options).to_s
|
13
51
|
end
|
14
52
|
end
|
15
53
|
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..-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,134 @@
|
|
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 truncated_text
|
50
|
+
max = RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length
|
51
|
+
return text if text.size <= max
|
52
|
+
|
53
|
+
"#{text[0..max]}...#{text[-max..-1]}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def size
|
57
|
+
return @element.size if @element.respond_to?(:size)
|
58
|
+
|
59
|
+
@siblings.size
|
60
|
+
end
|
61
|
+
alias length size
|
62
|
+
|
63
|
+
def new_from_find(tag, options)
|
64
|
+
Element.new(
|
65
|
+
find(tag),
|
66
|
+
tag,
|
67
|
+
options: options,
|
68
|
+
siblings: find(tag, all: true)
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
def new_from_where(tag, options)
|
73
|
+
Element.new(
|
74
|
+
where(tag, options),
|
75
|
+
tag,
|
76
|
+
options: options,
|
77
|
+
siblings: where(tag, options, all: true)
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def index(val)
|
84
|
+
zero_index_error if val.zero?
|
85
|
+
self.class.new(@siblings[val - 1], @element.name)
|
86
|
+
end
|
87
|
+
|
88
|
+
def range(val)
|
89
|
+
zero_index_error if val.first.zero?
|
90
|
+
self.class.new(@siblings[(val.first - 1)..(val.last - 1)], :range)
|
91
|
+
end
|
92
|
+
|
93
|
+
def zero_index_error
|
94
|
+
raise ArgumentError, 'Index for matched sets starts at 1, not 0.'
|
95
|
+
end
|
96
|
+
|
97
|
+
def where(tag, query, all: false)
|
98
|
+
matched = if query[:class]
|
99
|
+
where_class(tag, query[:class]) & where_xpath(tag, query.merge(class: nil))
|
100
|
+
else
|
101
|
+
where_xpath(tag, query)
|
102
|
+
end
|
103
|
+
return matched&.first unless all
|
104
|
+
|
105
|
+
matched
|
106
|
+
end
|
107
|
+
|
108
|
+
def where_xpath(tag, query)
|
109
|
+
conditions = "[#{where_conditions(query)}]" unless query.compact.empty?
|
110
|
+
@element&.xpath("//#{tag}#{conditions}")
|
111
|
+
end
|
112
|
+
|
113
|
+
def where_conditions(query)
|
114
|
+
query.compact.map do |key, value|
|
115
|
+
next if value.nil?
|
116
|
+
next %(@#{key}="#{value}") unless key == :text
|
117
|
+
|
118
|
+
%[contains(text(),"#{value}")]
|
119
|
+
end.join ' and '
|
120
|
+
end
|
121
|
+
|
122
|
+
def where_class(tag, class_or_classes)
|
123
|
+
classes = class_or_classes.is_a?(Array) ? class_or_classes : class_or_classes.to_s.split
|
124
|
+
selector = classes.map(&:to_s).join('.')
|
125
|
+
@element&.css("#{tag}.#{selector}")
|
126
|
+
end
|
127
|
+
|
128
|
+
def find(tag, all: false)
|
129
|
+
return @element&.css(tag.to_s)&.first unless all
|
130
|
+
|
131
|
+
@element&.css(tag.to_s)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
data/lib/rspec_html/tags.rb
CHANGED
data/rspec-html.gemspec
CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
|
|
37
37
|
spec.add_development_dependency 'i18n', '~> 1.7'
|
38
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.
|
40
|
+
spec.add_development_dependency 'rubocop', '~> 0.89.1'
|
41
41
|
spec.add_development_dependency 'rubocop-rspec', '~> 1.36'
|
42
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
|
+
<% unless actual.exist? %>
|
2
|
+
Expected <%= actual.reconstituted %> to contain <%= expected.inspect %> but the element did not exist.
|
3
|
+
<% else %>
|
4
|
+
Expected text in <%= actual.reconstituted %> <%= (actual.truncated_text).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.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Farrell
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -128,14 +128,14 @@ dependencies:
|
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: 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.
|
138
|
+
version: 0.89.1
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: rubocop-rspec
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -188,10 +188,18 @@ files:
|
|
188
188
|
- lib/rspec/html/version.rb
|
189
189
|
- lib/rspec_html.rb
|
190
190
|
- lib/rspec_html/element.rb
|
191
|
-
- lib/rspec_html/
|
192
|
-
- lib/rspec_html/
|
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
|
193
197
|
- lib/rspec_html/tags.rb
|
194
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
|
195
203
|
homepage: https://github.com/bobf/rspec-html
|
196
204
|
licenses:
|
197
205
|
- MIT
|
@@ -199,7 +207,7 @@ metadata:
|
|
199
207
|
homepage_uri: https://github.com/bobf/rspec-html
|
200
208
|
source_code_uri: https://github.com/bobf/rspec-html
|
201
209
|
changelog_uri: https://github.com/bobf/rspec-html/blob/master/LICENSE
|
202
|
-
post_install_message:
|
210
|
+
post_install_message:
|
203
211
|
rdoc_options: []
|
204
212
|
require_paths:
|
205
213
|
- lib
|
@@ -214,8 +222,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
214
222
|
- !ruby/object:Gem::Version
|
215
223
|
version: '0'
|
216
224
|
requirements: []
|
217
|
-
rubygems_version: 3.
|
218
|
-
signing_key:
|
225
|
+
rubygems_version: 3.1.2
|
226
|
+
signing_key:
|
219
227
|
specification_version: 4
|
220
228
|
summary: RSpec HTML
|
221
229
|
test_files: []
|
data/lib/rspec_html/nameable.rb
DELETED
@@ -1,101 +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
|
-
@element.text.include?(val)
|
8
|
-
end
|
9
|
-
|
10
|
-
def css(*args)
|
11
|
-
self.class.new(@element&.css(*args), :css)
|
12
|
-
end
|
13
|
-
|
14
|
-
def xpath(*args)
|
15
|
-
self.class.new(@element&.xpath(*args), :xpath)
|
16
|
-
end
|
17
|
-
|
18
|
-
def present?
|
19
|
-
!@element.nil?
|
20
|
-
end
|
21
|
-
alias exist? present?
|
22
|
-
|
23
|
-
# rubocop:disable Naming/PredicateName
|
24
|
-
def has_css?(*args)
|
25
|
-
!@element&.css(*args)&.empty?
|
26
|
-
end
|
27
|
-
|
28
|
-
def has_xpath?(*args)
|
29
|
-
!@element&.xpath(*args)&.empty?
|
30
|
-
end
|
31
|
-
# rubocop:enable Naming/PredicateName
|
32
|
-
|
33
|
-
def to_s
|
34
|
-
@element&.text&.strip
|
35
|
-
end
|
36
|
-
|
37
|
-
def inspect
|
38
|
-
%("#{@element}")
|
39
|
-
end
|
40
|
-
|
41
|
-
def [](val)
|
42
|
-
return index(val) if val.is_a?(Integer)
|
43
|
-
return range(val) if val.is_a?(Range)
|
44
|
-
|
45
|
-
@element&.attr(val.to_s)
|
46
|
-
end
|
47
|
-
|
48
|
-
def size
|
49
|
-
return @element.size if @element.respond_to?(:size)
|
50
|
-
|
51
|
-
@siblings.size
|
52
|
-
end
|
53
|
-
alias length size
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def method_missing(tag, *args)
|
58
|
-
return super unless Tags.include?(tag)
|
59
|
-
return self.class.new(find(tag), tag, siblings: find(tag, all: true)) if args.empty?
|
60
|
-
|
61
|
-
self.class.new(where(tag, args.first), tag, siblings: where(tag, args.first, all: true))
|
62
|
-
end
|
63
|
-
|
64
|
-
def index(val)
|
65
|
-
zero_index_error if val.zero?
|
66
|
-
self.class.new(@siblings[val - 1], name)
|
67
|
-
end
|
68
|
-
|
69
|
-
def range(val)
|
70
|
-
zero_index_error if val.first.zero?
|
71
|
-
self.class.new(@siblings[(val.first - 1)..(val.last - 1)], :range)
|
72
|
-
end
|
73
|
-
|
74
|
-
def zero_index_error
|
75
|
-
raise ArgumentError, 'Index for matched sets starts at 1, not 0.'
|
76
|
-
end
|
77
|
-
|
78
|
-
def where(tag, query, all: false)
|
79
|
-
matched = @element&.xpath("//#{tag}[#{where_conditions(query)}]")
|
80
|
-
return matched&.first unless all
|
81
|
-
|
82
|
-
matched
|
83
|
-
end
|
84
|
-
|
85
|
-
def where_conditions(query)
|
86
|
-
query.map do |key, value|
|
87
|
-
%(@#{key}="#{value}")
|
88
|
-
end.join ' and '
|
89
|
-
end
|
90
|
-
|
91
|
-
def find(tag, all: false)
|
92
|
-
return @element&.css(tag.to_s)&.first unless all
|
93
|
-
|
94
|
-
@element&.css(tag.to_s)
|
95
|
-
end
|
96
|
-
|
97
|
-
def respond_to_missing?(method_name, *_)
|
98
|
-
Tags.include?(method_name)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|