spec_views 1.0.1 → 2.0.0

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