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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -16
  3. data/Rakefile +3 -1
  4. data/app/assets/config/spec_views_manifest.js +1 -0
  5. data/app/assets/javascripts/spec_views/diff.js +60 -0
  6. data/app/assets/javascripts/spec_views/jsdiff.js +1055 -0
  7. data/app/controllers/spec_views/views_controller.rb +18 -60
  8. data/app/models/spec_views/base_matcher.rb +64 -0
  9. data/app/models/spec_views/directory.rb +133 -0
  10. data/app/models/spec_views/html_matcher.rb +39 -0
  11. data/app/models/spec_views/http_response_extractor.rb +31 -0
  12. data/app/models/spec_views/mail_message_extractor.rb +30 -0
  13. data/app/models/spec_views/pdf_matcher.rb +49 -0
  14. data/app/views/layouts/spec_views.html.erb +261 -0
  15. data/app/views/spec_views/views/_actions.html.erb +11 -0
  16. data/app/views/spec_views/views/_directory_footer.html.erb +14 -0
  17. data/app/views/spec_views/views/compare.html.erb +11 -0
  18. data/app/views/spec_views/views/diff.html.erb +16 -0
  19. data/app/views/spec_views/views/index.html.erb +48 -0
  20. data/app/views/spec_views/views/preview.html.erb +32 -0
  21. data/config/routes.rb +3 -0
  22. data/lib/spec_views/configuration.rb +3 -2
  23. data/lib/spec_views/engine.rb +10 -0
  24. data/lib/spec_views/support.rb +56 -166
  25. data/lib/spec_views/version.rb +3 -1
  26. data/lib/spec_views.rb +3 -1
  27. data/lib/tasks/spec_views_tasks.rake +1 -0
  28. metadata +26 -15
  29. data/app/views/layouts/spec_views.html.haml +0 -200
  30. data/app/views/spec_views/views/compare.html.haml +0 -16
  31. data/app/views/spec_views/views/index.html.haml +0 -29
  32. 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 > 0
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)).html_safe # rubocop:disable Rails/OutputSafety
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] && params[:id].end_with?('__pdf')
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