spec_views 1.0.1 → 2.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 +101 -16
- data/Rakefile +3 -1
- data/app/assets/config/spec_views_manifest.js +1 -0
- data/app/assets/javascripts/spec_views/diff.js +60 -0
- data/app/assets/javascripts/spec_views/jsdiff.js +1055 -0
- data/app/controllers/spec_views/views_controller.rb +18 -60
- data/app/models/spec_views/base_matcher.rb +64 -0
- data/app/models/spec_views/directory.rb +133 -0
- data/app/models/spec_views/html_matcher.rb +39 -0
- data/app/models/spec_views/http_response_extractor.rb +31 -0
- data/app/models/spec_views/mail_message_extractor.rb +30 -0
- data/app/models/spec_views/pdf_matcher.rb +49 -0
- data/app/views/layouts/spec_views.html.erb +261 -0
- data/app/views/spec_views/views/_actions.html.erb +11 -0
- data/app/views/spec_views/views/_directory_footer.html.erb +14 -0
- data/app/views/spec_views/views/compare.html.erb +11 -0
- data/app/views/spec_views/views/diff.html.erb +16 -0
- data/app/views/spec_views/views/index.html.erb +48 -0
- data/app/views/spec_views/views/preview.html.erb +32 -0
- data/config/routes.rb +3 -0
- data/lib/spec_views/configuration.rb +3 -2
- data/lib/spec_views/engine.rb +10 -0
- data/lib/spec_views/support.rb +56 -166
- data/lib/spec_views/version.rb +3 -1
- data/lib/spec_views.rb +3 -1
- data/lib/tasks/spec_views_tasks.rake +1 -0
- metadata +26 -15
- data/app/views/layouts/spec_views.html.haml +0 -200
- data/app/views/spec_views/views/compare.html.haml +0 -16
- data/app/views/spec_views/views/index.html.haml +0 -29
- data/app/views/spec_views/views/preview.html.haml +0 -19
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module SpecViews
|
4
4
|
class ViewsController < ApplicationController
|
5
|
-
skip_authorization_check
|
5
|
+
skip_authorization_check if respond_to?(:skip_authorization_check)
|
6
6
|
layout 'spec_views'
|
7
7
|
|
8
8
|
def index
|
@@ -36,11 +36,21 @@ module SpecViews
|
|
36
36
|
@directory = directory
|
37
37
|
end
|
38
38
|
|
39
|
+
def diff
|
40
|
+
if pdf?
|
41
|
+
redirect_to action: :compare
|
42
|
+
return
|
43
|
+
end
|
44
|
+
@champion = get_view(filename('view'), html_safe: false)
|
45
|
+
@challenger = get_view(filename('challenger'), html_safe: false)
|
46
|
+
@directory = directory
|
47
|
+
end
|
48
|
+
|
39
49
|
def preview
|
40
50
|
@directory = directory
|
41
51
|
@next = directories[directories.index(@directory) + 1]
|
42
52
|
index = directories.index(@directory)
|
43
|
-
@previous = directories[index - 1] if index
|
53
|
+
@previous = directories[index - 1] if index.positive?
|
44
54
|
end
|
45
55
|
|
46
56
|
def accept
|
@@ -72,63 +82,9 @@ module SpecViews
|
|
72
82
|
|
73
83
|
private
|
74
84
|
|
75
|
-
class Directory
|
76
|
-
attr_reader :path
|
77
|
-
|
78
|
-
def initialize(path)
|
79
|
-
@path = path
|
80
|
-
end
|
81
|
-
|
82
|
-
def basename
|
83
|
-
path.basename
|
84
|
-
end
|
85
|
-
|
86
|
-
def to_param
|
87
|
-
basename.to_s
|
88
|
-
end
|
89
|
-
|
90
|
-
def name
|
91
|
-
basename
|
92
|
-
end
|
93
|
-
|
94
|
-
def challenger?
|
95
|
-
File.file?(path.join('challenger.html')) || File.file?(path.join('challenger.pdf'))
|
96
|
-
end
|
97
|
-
|
98
|
-
def controller_name
|
99
|
-
splitted_name.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
|
100
|
-
end
|
101
|
-
|
102
|
-
def method
|
103
|
-
splitted_name.second
|
104
|
-
end
|
105
|
-
|
106
|
-
def description
|
107
|
-
splitted_name.third.humanize
|
108
|
-
end
|
109
|
-
|
110
|
-
def last_run
|
111
|
-
@last_run ||= begin
|
112
|
-
Time.zone.parse(File.read(path.join('last_run.txt')))
|
113
|
-
rescue Errno::ENOENT
|
114
|
-
Time.zone.at(0)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
def delete!
|
119
|
-
FileUtils.remove_dir(path)
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def splitted_name
|
125
|
-
@splitted_name ||= name.to_s.split(/_(DELETE|GET|PATCH|POST|PUT)_/)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
85
|
def directories
|
130
86
|
@directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
|
131
|
-
Directory.new(path)
|
87
|
+
SpecViews::Directory.new(path)
|
132
88
|
end.sort_by(&:basename)
|
133
89
|
end
|
134
90
|
|
@@ -140,9 +96,11 @@ module SpecViews
|
|
140
96
|
directories.detect { |dir| dir.to_param == params[:id] }
|
141
97
|
end
|
142
98
|
|
143
|
-
def get_view(filename = nil)
|
99
|
+
def get_view(filename = nil, html_safe: true)
|
144
100
|
filename ||= pdf? ? 'view.pdf' : 'view.html'
|
145
|
-
File.read(file_path(filename))
|
101
|
+
content = File.read(file_path(filename))
|
102
|
+
content = content.html_safe if html_safe
|
103
|
+
content
|
146
104
|
rescue Errno::ENOENT
|
147
105
|
''
|
148
106
|
end
|
@@ -158,7 +116,7 @@ module SpecViews
|
|
158
116
|
end
|
159
117
|
|
160
118
|
def pdf?
|
161
|
-
params[:id]
|
119
|
+
params[:id]&.end_with?('__pdf')
|
162
120
|
end
|
163
121
|
|
164
122
|
def accept_directory(id)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class BaseMatcher
|
5
|
+
attr_reader :response, :directory, :failure_message, :type
|
6
|
+
|
7
|
+
delegate :champion_html, to: :directory
|
8
|
+
|
9
|
+
def initialize(response, description, run_time:, expected_status: :ok, type: :request)
|
10
|
+
@response = response
|
11
|
+
@type = type
|
12
|
+
@extractor = extractor_class.new(response, expected_status: expected_status)
|
13
|
+
@directory = SpecViews::Directory.for_description(description, content_type: content_type)
|
14
|
+
@extractor_failure = @extractor.extractor_failure?
|
15
|
+
@match = !extractor_failure? && match_challenger
|
16
|
+
@directory.write_last_run(run_time) if champion_html
|
17
|
+
return if match?
|
18
|
+
|
19
|
+
if extractor_failure?
|
20
|
+
@failure_message = @extractor.failure_message
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
@failure_message = "#{subject_name} has changed."
|
25
|
+
@failure_message = "#{subject_name} has been added." if champion_html.nil?
|
26
|
+
|
27
|
+
@directory.write_last_run(run_time)
|
28
|
+
@directory.write_description(description)
|
29
|
+
@directory.write_challenger(challenger_body)
|
30
|
+
end
|
31
|
+
|
32
|
+
def match?
|
33
|
+
@match
|
34
|
+
end
|
35
|
+
|
36
|
+
def extractor_failure?
|
37
|
+
@extractor_failure
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
def subject_name
|
43
|
+
raise NotImplementedError
|
44
|
+
end
|
45
|
+
|
46
|
+
def challenger_body
|
47
|
+
raise NotImplementedError
|
48
|
+
end
|
49
|
+
|
50
|
+
def match_challenger
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def content_type
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
def extractor_class
|
59
|
+
return MailMessageExtractor if type == :mailer
|
60
|
+
|
61
|
+
HttpResponseExtractor
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class Directory
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def self.for_description(description, content_type: :html)
|
8
|
+
dir_name = description.strip.gsub(/[^0-9A-Za-z.\-]/, '_').gsub('__', '_')
|
9
|
+
dir_name = "#{dir_name}__pdf" if content_type == :pdf
|
10
|
+
new(Rails.root.join(Rails.configuration.spec_views.directory, dir_name))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(path)
|
14
|
+
@path = path
|
15
|
+
end
|
16
|
+
|
17
|
+
def basename
|
18
|
+
path.basename
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_param
|
22
|
+
basename.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
basename
|
27
|
+
end
|
28
|
+
|
29
|
+
def challenger?
|
30
|
+
File.file?(path.join('challenger.html')) || File.file?(path.join('challenger.pdf'))
|
31
|
+
end
|
32
|
+
|
33
|
+
def controller_name
|
34
|
+
splitted_description.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
|
35
|
+
end
|
36
|
+
|
37
|
+
def method
|
38
|
+
splitted_description.second[0, 4]
|
39
|
+
end
|
40
|
+
|
41
|
+
def description_tail
|
42
|
+
splitted_description.third.gsub(/__pdf$/, '')
|
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
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete!
|
54
|
+
FileUtils.remove_dir(path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def champion_path
|
58
|
+
path.join("view.#{file_extension}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def champion_html
|
62
|
+
File.read(champion_path)
|
63
|
+
rescue Errno::ENOENT
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def challenger_path
|
68
|
+
path.join("challenger.#{file_extension}")
|
69
|
+
end
|
70
|
+
|
71
|
+
def write_challenger(content)
|
72
|
+
FileUtils.mkdir_p(path)
|
73
|
+
File.open(challenger_path, binary? ? 'wb' : 'w') { |f| f.write(content) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def last_run_path
|
77
|
+
path.join('last_run.txt')
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_last_run(time)
|
81
|
+
FileUtils.mkdir_p(path)
|
82
|
+
File.write(last_run_path, time)
|
83
|
+
end
|
84
|
+
|
85
|
+
def description_path
|
86
|
+
path.join('description.txt')
|
87
|
+
end
|
88
|
+
|
89
|
+
def write_description(description)
|
90
|
+
FileUtils.mkdir_p(path)
|
91
|
+
File.write(description_path, description)
|
92
|
+
end
|
93
|
+
|
94
|
+
def description
|
95
|
+
File.read(description_path)
|
96
|
+
rescue Errno::ENOENT
|
97
|
+
name
|
98
|
+
end
|
99
|
+
|
100
|
+
def pdf?
|
101
|
+
name.to_s.ends_with?('__pdf')
|
102
|
+
end
|
103
|
+
|
104
|
+
def mailer?
|
105
|
+
@mailer ||= name.to_s.start_with?(/[a-zA-Z0-9:]+Mailer[^a-zA-Z0-9]/)
|
106
|
+
end
|
107
|
+
|
108
|
+
def binary?
|
109
|
+
pdf?
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def splitted_description
|
115
|
+
@splitted_description ||= begin
|
116
|
+
if mailer?
|
117
|
+
split = description.to_s.match(/(\A[a-zA-Z0-9:]+)(Mailer)(.*)/)
|
118
|
+
split = split[1..3]
|
119
|
+
split[0] += 'Mailer'
|
120
|
+
split[1] = 'MAIL'
|
121
|
+
else
|
122
|
+
split = description.to_s.split(/\s(DELETE|GET|PATCH|POST|PUT)\s/)
|
123
|
+
end
|
124
|
+
split = ['', '', split[0]] if split.size == 1
|
125
|
+
split
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def file_extension
|
130
|
+
pdf? ? 'pdf' : 'html'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class HtmlMatcher < BaseMatcher
|
5
|
+
delegate :champion_html, to: :directory
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
def subject_name
|
10
|
+
'View'
|
11
|
+
end
|
12
|
+
|
13
|
+
def content_type
|
14
|
+
:html
|
15
|
+
end
|
16
|
+
|
17
|
+
def challenger_body
|
18
|
+
sanitized_body
|
19
|
+
end
|
20
|
+
|
21
|
+
def match_challenger
|
22
|
+
champion_html == challenger_body
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def sanitized_body
|
28
|
+
remove_pack_digests_from_body(remove_digests_from_body(@extractor.body))
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_digests_from_body(body)
|
32
|
+
body.gsub(/(-[a-z0-9]{64})(\.css|\.js|\.ico|\.png|\.jpg|\.jpeg|\.svg|\.gif)/, '\2')
|
33
|
+
end
|
34
|
+
|
35
|
+
def remove_pack_digests_from_body(body)
|
36
|
+
body.gsub(%r{(packs.*/js/[a-z0-9_]+)(-[a-z0-9]{20})(\.js)}, '\1\3')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class HttpResponseExtractor
|
5
|
+
attr_reader :response, :failure_message
|
6
|
+
|
7
|
+
def initialize(response, expected_status:)
|
8
|
+
@response = response
|
9
|
+
@status_match = response_status_match?(response, expected_status)
|
10
|
+
return if @status_match
|
11
|
+
|
12
|
+
|
13
|
+
@failure_message = "Unexpected response status #{response.status}."
|
14
|
+
end
|
15
|
+
|
16
|
+
def extractor_failure?
|
17
|
+
failure_message.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def body
|
21
|
+
response.body
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def response_status_match?(response, expected_status)
|
27
|
+
response.status == expected_status ||
|
28
|
+
response.message.parameterize.underscore == expected_status.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class MailMessageExtractor
|
5
|
+
attr_reader :mail, :failure_message, :body
|
6
|
+
|
7
|
+
def initialize(mail, expected_status:)
|
8
|
+
@mail = mail
|
9
|
+
@body = extract_body(mail)
|
10
|
+
return if body.present?
|
11
|
+
|
12
|
+
@failure_message = "Failed to find mail part"
|
13
|
+
end
|
14
|
+
|
15
|
+
def extractor_failure?
|
16
|
+
failure_message.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_body(part)
|
20
|
+
return part.raw_source if part.respond_to?(:raw_source) && part.raw_source.present?
|
21
|
+
return extract_body(part.body) if part.respond_to?(:body)
|
22
|
+
return part if part.is_a?(String)
|
23
|
+
return if nil unless part.respond_to?(:parts)
|
24
|
+
|
25
|
+
part.parts.map do |inner_part|
|
26
|
+
extract_body(inner_part)
|
27
|
+
end.compact.first
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecViews
|
4
|
+
class PdfMatcher < BaseMatcher
|
5
|
+
protected
|
6
|
+
|
7
|
+
def subject_name
|
8
|
+
'PDF'
|
9
|
+
end
|
10
|
+
|
11
|
+
def content_type
|
12
|
+
:pdf
|
13
|
+
end
|
14
|
+
|
15
|
+
def challenger_body
|
16
|
+
sanitized_body
|
17
|
+
end
|
18
|
+
|
19
|
+
def match_challenger
|
20
|
+
champion_hash == challenger_hash
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def champion_hash
|
26
|
+
@champion_hash ||= begin
|
27
|
+
Digest::MD5.file(directory.champion_path).hexdigest
|
28
|
+
rescue Errno::ENOENT
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def challenger_hash
|
34
|
+
@challenger_hash ||= Digest::MD5.hexdigest(sanitized_body)
|
35
|
+
end
|
36
|
+
|
37
|
+
def sanitized_body
|
38
|
+
@sanitized_body ||= remove_headers_from_pdf(@extractor.body)
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_headers_from_pdf(body)
|
42
|
+
body
|
43
|
+
.force_encoding('BINARY')
|
44
|
+
.gsub(%r{^/CreationDate \(.*\)$}, '')
|
45
|
+
.gsub(%r{^/ModDate \(.*\)$}, '')
|
46
|
+
.gsub(%r{^/ID \[<.+><.+>\]$}, '')
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|