spec_views 2.0.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|