spec_views 2.0.0 → 3.0.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 +4 -4
- data/README.md +24 -1
- data/app/controllers/spec_views/views_controller.rb +24 -34
- data/app/models/spec_views/base_matcher.rb +7 -6
- data/app/models/spec_views/capybara_session_extractor.rb +30 -0
- data/app/models/spec_views/directory.rb +42 -35
- data/app/models/spec_views/html_matcher.rb +3 -1
- data/app/models/spec_views/http_response_extractor.rb +0 -1
- data/app/models/spec_views/mail_message_extractor.rb +3 -2
- data/app/models/spec_views/pdf_matcher.rb +5 -1
- data/app/models/spec_views/view_sanitizer.rb +20 -0
- data/lib/spec_views/support.rb +7 -2
- data/lib/spec_views/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42a03f1d324852c51d56e4afdadadcc00e6449954917f396599df6c4568176d0
|
4
|
+
data.tar.gz: 466b8bddc2f1c78bed3bbe57370b26bc87f71ecf38e2476ccc46fa4237a5410a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48a9730de38ccc7927d852015fcb7c051e783a8a603cb665df17095a45799e882a1cc276d09b31177a2bc7ee93c8c86a455e5d0f24314e80ad19b77adcb36860
|
7
|
+
data.tar.gz: a837bcd14d1e95707830291dbd621c41bef508c3fc858854bc22c6142aee2170b24d29f991c055b457da173db0b6a94e0920aaeef87cca13b5d4f13724393238
|
data/README.md
CHANGED
@@ -40,6 +40,18 @@ end
|
|
40
40
|
|
41
41
|
Run this spec to see it failing. Open SpecView UI [http://localhost:3000/spec_views](http://localhost:3000/spec_views) on Rails development server to accept the new view. When rerunning the spec it compares its rendering with the reference view. Use SpecView UI to review, accept or reject changed views.
|
42
42
|
|
43
|
+
### view_sanitizer
|
44
|
+
Sometimes it is hard to remove dynamic elements like IDs and times. Use `view_sanitizer.gsub` to replace dynamic strings with fixed ones before comparing and saving the views automatically:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
before do
|
48
|
+
view_sanitizer.gsub(%r{(users/)[0-9]+}, '\1ID')
|
49
|
+
view_sanitizer.gsub(/value="[0-9]+"/, 'value="ID"')
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
`view_sanitizer.gsub` supports the same arguments as `String#gsub` including blocks.
|
54
|
+
|
43
55
|
### it_renders
|
44
56
|
You can also use the shortcut to skip the matcher and the word "renders" from your description:
|
45
57
|
|
@@ -85,7 +97,7 @@ end
|
|
85
97
|
Compare HTML mailers. `match_html_fixture` tries to find the HTML part of your mail automatically:
|
86
98
|
|
87
99
|
```ruby
|
88
|
-
RSpec.describe NotificationsMailer, :
|
100
|
+
RSpec.describe NotificationsMailer, type: :mailer do
|
89
101
|
describe '#notify' do
|
90
102
|
let(:mail) { NotificationsMailer.signup }
|
91
103
|
|
@@ -96,6 +108,17 @@ RSpec.describe NotificationsMailer, :type => :mailer do
|
|
96
108
|
end
|
97
109
|
```
|
98
110
|
|
111
|
+
### Feature specs
|
112
|
+
Compare pages from feature specs:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
RSpec.describe 'Articles', type: :feature do
|
116
|
+
it 'are shown as list' do
|
117
|
+
expect(page).to match_html_fixture
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
99
122
|
### Controller specs
|
100
123
|
Prefer request specs over controller specs. If you still want to use controller specs enable the `render_views` feature:
|
101
124
|
|
@@ -19,16 +19,16 @@ module SpecViews
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def show
|
22
|
-
|
23
|
-
|
24
|
-
if pdf?
|
22
|
+
path = directory.champion_path
|
23
|
+
path = directory.challenger_path if params[:view] == 'challenger'
|
24
|
+
if directory.content_type.pdf?
|
25
25
|
send_data(
|
26
|
-
get_view(
|
26
|
+
get_view(path),
|
27
27
|
filename: 'a.pdf',
|
28
28
|
type: 'application/pdf', disposition: 'inline'
|
29
29
|
)
|
30
30
|
else
|
31
|
-
render html: get_view(
|
31
|
+
render html: get_view(path)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -37,12 +37,12 @@ module SpecViews
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def diff
|
40
|
-
if pdf?
|
40
|
+
if directory.content_type.pdf?
|
41
41
|
redirect_to action: :compare
|
42
42
|
return
|
43
43
|
end
|
44
|
-
@champion = get_view(
|
45
|
-
@challenger = get_view(
|
44
|
+
@champion = get_view(directory.champion_path, html_safe: false)
|
45
|
+
@challenger = get_view(directory.challenger_path, html_safe: false)
|
46
46
|
@directory = directory
|
47
47
|
end
|
48
48
|
|
@@ -54,7 +54,7 @@ module SpecViews
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def accept
|
57
|
-
accept_directory(
|
57
|
+
accept_directory(directory)
|
58
58
|
redirect_to action: :index, challenger: :next
|
59
59
|
end
|
60
60
|
|
@@ -66,8 +66,7 @@ module SpecViews
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def reject
|
69
|
-
|
70
|
-
FileUtils.remove_file(challenger)
|
69
|
+
FileUtils.remove_file(directory.challenger_path)
|
71
70
|
redirect_to action: :index, challenger: :next
|
72
71
|
end
|
73
72
|
|
@@ -85,7 +84,15 @@ module SpecViews
|
|
85
84
|
def directories
|
86
85
|
@directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
|
87
86
|
SpecViews::Directory.new(path)
|
88
|
-
end
|
87
|
+
end
|
88
|
+
latest_run = @directories.map(&:last_run).max
|
89
|
+
|
90
|
+
@directories.sort_by! do |dir|
|
91
|
+
prefix = '3-'
|
92
|
+
prefix = '2-' if dir.last_run < latest_run
|
93
|
+
prefix = '1-' if dir.challenger?
|
94
|
+
"#{prefix}#{dir.basename}"
|
95
|
+
end
|
89
96
|
end
|
90
97
|
|
91
98
|
def directory_path
|
@@ -96,34 +103,17 @@ module SpecViews
|
|
96
103
|
directories.detect { |dir| dir.to_param == params[:id] }
|
97
104
|
end
|
98
105
|
|
99
|
-
def get_view(
|
100
|
-
|
101
|
-
content = File.read(file_path(filename))
|
106
|
+
def get_view(path, html_safe: true)
|
107
|
+
content = File.read(path)
|
102
108
|
content = content.html_safe if html_safe
|
103
109
|
content
|
104
110
|
rescue Errno::ENOENT
|
105
111
|
''
|
106
112
|
end
|
107
113
|
|
108
|
-
def
|
109
|
-
|
110
|
-
|
111
|
-
directory_path.join(id, filename)
|
112
|
-
end
|
113
|
-
|
114
|
-
def filename(base = 'view')
|
115
|
-
pdf? ? "#{base}.pdf" : "#{base}.html"
|
116
|
-
end
|
117
|
-
|
118
|
-
def pdf?
|
119
|
-
params[:id]&.end_with?('__pdf')
|
120
|
-
end
|
121
|
-
|
122
|
-
def accept_directory(id)
|
123
|
-
champion = file_path(filename('view'), id: id)
|
124
|
-
challenger = file_path(filename('challenger'), id: id)
|
125
|
-
FileUtils.copy_file(challenger, champion)
|
126
|
-
FileUtils.remove_file(challenger)
|
114
|
+
def accept_directory(dir)
|
115
|
+
FileUtils.copy_file(dir.challenger_path, dir.champion_path)
|
116
|
+
FileUtils.remove_file(dir.challenger_path)
|
127
117
|
end
|
128
118
|
end
|
129
119
|
end
|
@@ -2,30 +2,30 @@
|
|
2
2
|
|
3
3
|
module SpecViews
|
4
4
|
class BaseMatcher
|
5
|
-
attr_reader :response, :directory, :failure_message, :type
|
5
|
+
attr_reader :response, :directory, :failure_message, :sanitizer, :type
|
6
6
|
|
7
7
|
delegate :champion_html, to: :directory
|
8
8
|
|
9
|
-
def initialize(response, description, run_time:, expected_status: :ok, type: :request)
|
9
|
+
def initialize(response, description, run_time:, expected_status: :ok, sanitizer: nil, type: :request)
|
10
10
|
@response = response
|
11
11
|
@type = type
|
12
|
+
@sanitizer = sanitizer
|
12
13
|
@extractor = extractor_class.new(response, expected_status: expected_status)
|
13
14
|
@directory = SpecViews::Directory.for_description(description, content_type: content_type)
|
14
15
|
@extractor_failure = @extractor.extractor_failure?
|
15
16
|
@match = !extractor_failure? && match_challenger
|
16
|
-
|
17
|
+
directory.write_meta(description, run_time, type, content_type) if champion_html
|
17
18
|
return if match?
|
18
19
|
|
19
20
|
if extractor_failure?
|
20
|
-
@failure_message = @extractor.failure_message
|
21
|
+
@failure_message = @extractor.failure_message
|
21
22
|
return
|
22
23
|
end
|
23
24
|
|
24
25
|
@failure_message = "#{subject_name} has changed."
|
25
26
|
@failure_message = "#{subject_name} has been added." if champion_html.nil?
|
26
27
|
|
27
|
-
|
28
|
-
@directory.write_description(description)
|
28
|
+
directory.write_meta(description, run_time, type, content_type)
|
29
29
|
@directory.write_challenger(challenger_body)
|
30
30
|
end
|
31
31
|
|
@@ -56,6 +56,7 @@ module SpecViews
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def extractor_class
|
59
|
+
return CapybaraSessionExtractor if type == :feature
|
59
60
|
return MailMessageExtractor if type == :mailer
|
60
61
|
|
61
62
|
HttpResponseExtractor
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class CapybaraSessionExtractor
|
5
|
+
attr_reader :page, :failure_message
|
6
|
+
|
7
|
+
def initialize(page, expected_status:)
|
8
|
+
@page = page
|
9
|
+
@status_match = response_status_match?(page, expected_status)
|
10
|
+
return if @status_match
|
11
|
+
|
12
|
+
@failure_message = "Unexpected response status #{page.status_code}."
|
13
|
+
end
|
14
|
+
|
15
|
+
def extractor_failure?
|
16
|
+
failure_message.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def body
|
20
|
+
page.source
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def response_status_match?(page, expected_status)
|
26
|
+
page.status_code == expected_status ||
|
27
|
+
Rack::Utils::SYMBOL_TO_STATUS_CODE.invert[page.status_code] == expected_status.to_sym
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -6,12 +6,12 @@ module SpecViews
|
|
6
6
|
|
7
7
|
def self.for_description(description, content_type: :html)
|
8
8
|
dir_name = description.strip.gsub(/[^0-9A-Za-z.\-]/, '_').gsub('__', '_')
|
9
|
-
|
10
|
-
new(Rails.root.join(Rails.configuration.spec_views.directory, dir_name))
|
9
|
+
new(Rails.root.join(Rails.configuration.spec_views.directory, dir_name), content_type)
|
11
10
|
end
|
12
11
|
|
13
|
-
def initialize(path)
|
12
|
+
def initialize(path, content_type = nil)
|
14
13
|
@path = path
|
14
|
+
@content_type = ActiveSupport::StringInquirer.new(content_type.to_s) if content_type
|
15
15
|
end
|
16
16
|
|
17
17
|
def basename
|
@@ -26,10 +26,6 @@ module SpecViews
|
|
26
26
|
basename
|
27
27
|
end
|
28
28
|
|
29
|
-
def challenger?
|
30
|
-
File.file?(path.join('challenger.html')) || File.file?(path.join('challenger.pdf'))
|
31
|
-
end
|
32
|
-
|
33
29
|
def controller_name
|
34
30
|
splitted_description.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
|
35
31
|
end
|
@@ -39,15 +35,7 @@ module SpecViews
|
|
39
35
|
end
|
40
36
|
|
41
37
|
def description_tail
|
42
|
-
splitted_description.third
|
43
|
-
end
|
44
|
-
|
45
|
-
def last_run
|
46
|
-
@last_run ||= begin
|
47
|
-
Time.zone.parse(File.read(path.join('last_run.txt')))
|
48
|
-
rescue Errno::ENOENT
|
49
|
-
Time.zone.at(0)
|
50
|
-
end
|
38
|
+
splitted_description.third
|
51
39
|
end
|
52
40
|
|
53
41
|
def delete!
|
@@ -68,52 +56,71 @@ module SpecViews
|
|
68
56
|
path.join("challenger.#{file_extension}")
|
69
57
|
end
|
70
58
|
|
59
|
+
def challenger?
|
60
|
+
File.file?(challenger_path)
|
61
|
+
end
|
62
|
+
|
71
63
|
def write_challenger(content)
|
72
64
|
FileUtils.mkdir_p(path)
|
73
65
|
File.open(challenger_path, binary? ? 'wb' : 'w') { |f| f.write(content) }
|
74
66
|
end
|
75
67
|
|
76
|
-
def
|
77
|
-
path.join('
|
68
|
+
def meta_path
|
69
|
+
path.join('meta.txt')
|
78
70
|
end
|
79
71
|
|
80
|
-
def
|
72
|
+
def write_meta(description, run_time, spec_type, content_type)
|
81
73
|
FileUtils.mkdir_p(path)
|
82
|
-
|
74
|
+
lines = [
|
75
|
+
description.to_s.gsub("\n", ' '),
|
76
|
+
run_time.to_s,
|
77
|
+
spec_type.to_s,
|
78
|
+
content_type.to_s
|
79
|
+
]
|
80
|
+
File.write(meta_path, lines.join("\n"))
|
83
81
|
end
|
84
82
|
|
85
|
-
def
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
def write_description(description)
|
90
|
-
FileUtils.mkdir_p(path)
|
91
|
-
File.write(description_path, description)
|
83
|
+
def meta
|
84
|
+
@meta ||= File.read(meta_path).lines.map(&:strip)
|
92
85
|
end
|
93
86
|
|
94
87
|
def description
|
95
|
-
|
88
|
+
meta[0]
|
96
89
|
rescue Errno::ENOENT
|
97
90
|
name
|
98
91
|
end
|
99
92
|
|
100
|
-
def
|
101
|
-
|
93
|
+
def last_run
|
94
|
+
Time.zone.parse(meta[1])
|
95
|
+
rescue Errno::ENOENT
|
96
|
+
Time.zone.at(0)
|
97
|
+
end
|
98
|
+
|
99
|
+
def spec_type
|
100
|
+
ActiveSupport::StringInquirer.new(meta[2])
|
101
|
+
rescue Errno::ENOENT
|
102
|
+
:response
|
102
103
|
end
|
103
104
|
|
104
|
-
def
|
105
|
-
@
|
105
|
+
def content_type
|
106
|
+
@content_type ||= begin
|
107
|
+
ActiveSupport::StringInquirer.new(meta[3])
|
108
|
+
rescue Errno::ENOENT
|
109
|
+
:response
|
110
|
+
end
|
106
111
|
end
|
107
112
|
|
108
113
|
def binary?
|
109
|
-
pdf?
|
114
|
+
content_type.pdf?
|
110
115
|
end
|
111
116
|
|
112
117
|
private
|
113
118
|
|
114
119
|
def splitted_description
|
115
120
|
@splitted_description ||= begin
|
116
|
-
if
|
121
|
+
if spec_type.feature?
|
122
|
+
split = ['', 'FEAT', description]
|
123
|
+
elsif spec_type.mailer?
|
117
124
|
split = description.to_s.match(/(\A[a-zA-Z0-9:]+)(Mailer)(.*)/)
|
118
125
|
split = split[1..3]
|
119
126
|
split[0] += 'Mailer'
|
@@ -127,7 +134,7 @@ module SpecViews
|
|
127
134
|
end
|
128
135
|
|
129
136
|
def file_extension
|
130
|
-
pdf? ? 'pdf' : 'html'
|
137
|
+
content_type.pdf? ? 'pdf' : 'html'
|
131
138
|
end
|
132
139
|
end
|
133
140
|
end
|
@@ -25,7 +25,9 @@ module SpecViews
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def sanitized_body
|
28
|
-
remove_pack_digests_from_body(remove_digests_from_body(@extractor.body))
|
28
|
+
body = remove_pack_digests_from_body(remove_digests_from_body(@extractor.body))
|
29
|
+
body = sanitizer.sanitize(body) if sanitizer
|
30
|
+
body
|
29
31
|
end
|
30
32
|
|
31
33
|
def remove_digests_from_body(body)
|
@@ -9,7 +9,7 @@ module SpecViews
|
|
9
9
|
@body = extract_body(mail)
|
10
10
|
return if body.present?
|
11
11
|
|
12
|
-
@failure_message =
|
12
|
+
@failure_message = 'Failed to find mail part'
|
13
13
|
end
|
14
14
|
|
15
15
|
def extractor_failure?
|
@@ -20,7 +20,8 @@ module SpecViews
|
|
20
20
|
return part.raw_source if part.respond_to?(:raw_source) && part.raw_source.present?
|
21
21
|
return extract_body(part.body) if part.respond_to?(:body)
|
22
22
|
return part if part.is_a?(String)
|
23
|
-
|
23
|
+
|
24
|
+
return if !part.respond_to?(:parts) && nil
|
24
25
|
|
25
26
|
part.parts.map do |inner_part|
|
26
27
|
extract_body(inner_part)
|
@@ -35,7 +35,11 @@ module SpecViews
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def sanitized_body
|
38
|
-
@sanitized_body ||=
|
38
|
+
@sanitized_body ||= begin
|
39
|
+
body = remove_headers_from_pdf(@extractor.body)
|
40
|
+
body = sanitizer.sanitize(body) if sanitizer
|
41
|
+
body
|
42
|
+
end
|
39
43
|
end
|
40
44
|
|
41
45
|
def remove_headers_from_pdf(body)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module SpecViews
|
2
|
+
class ViewSanitizer
|
3
|
+
delegate :each, to: :@sanitizers
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@sanitizers = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def gsub(*args, &block)
|
10
|
+
@sanitizers << [:gsub, args, block]
|
11
|
+
end
|
12
|
+
|
13
|
+
def sanitize(string)
|
14
|
+
each do |sanitize_method, args, block|
|
15
|
+
string = string.send(sanitize_method, *args, &block)
|
16
|
+
end
|
17
|
+
string
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/spec_views/support.rb
CHANGED
@@ -22,11 +22,16 @@ module SpecViews
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
RSpec.shared_context 'SpecViews ViewSanitizer' do
|
26
|
+
let(:view_sanitizer) { SpecViews::ViewSanitizer.new }
|
27
|
+
end
|
28
|
+
|
25
29
|
RSpec.configure do |c|
|
26
30
|
c.before(:suite) do |_example|
|
27
31
|
$_spec_view_time = Time.zone.now # rubocop:disable Style/GlobalVars
|
28
32
|
end
|
29
|
-
|
33
|
+
c.include_context 'SpecViews ViewSanitizer'
|
34
|
+
%i[controller feature mailer request].each do |type|
|
30
35
|
c.extend SpecViews::Support::SpecViewExample, type: type
|
31
36
|
c.before(type: type) do |example|
|
32
37
|
@_spec_view_example = example
|
@@ -40,7 +45,6 @@ RSpec.configure do |c|
|
|
40
45
|
end
|
41
46
|
end
|
42
47
|
|
43
|
-
|
44
48
|
matchers = [
|
45
49
|
[:match_html_fixture, SpecViews::HtmlMatcher],
|
46
50
|
[:match_pdf_fixture, SpecViews::PdfMatcher]
|
@@ -63,6 +67,7 @@ matchers.each do |matcher|
|
|
63
67
|
description,
|
64
68
|
expected_status: @status,
|
65
69
|
run_time: run_time,
|
70
|
+
sanitizer: view_sanitizer,
|
66
71
|
type: type
|
67
72
|
)
|
68
73
|
return @matcher.match?
|
data/lib/spec_views/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spec_views
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Gaul
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -81,11 +81,13 @@ files:
|
|
81
81
|
- app/assets/javascripts/spec_views/jsdiff.js
|
82
82
|
- app/controllers/spec_views/views_controller.rb
|
83
83
|
- app/models/spec_views/base_matcher.rb
|
84
|
+
- app/models/spec_views/capybara_session_extractor.rb
|
84
85
|
- app/models/spec_views/directory.rb
|
85
86
|
- app/models/spec_views/html_matcher.rb
|
86
87
|
- app/models/spec_views/http_response_extractor.rb
|
87
88
|
- app/models/spec_views/mail_message_extractor.rb
|
88
89
|
- app/models/spec_views/pdf_matcher.rb
|
90
|
+
- app/models/spec_views/view_sanitizer.rb
|
89
91
|
- app/views/layouts/spec_views.html.erb
|
90
92
|
- app/views/spec_views/views/_actions.html.erb
|
91
93
|
- app/views/spec_views/views/_directory_footer.html.erb
|