spec_views 1.1.1 → 3.1.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -5
  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 +35 -87
  8. data/app/models/spec_views/base_matcher.rb +66 -0
  9. data/app/models/spec_views/capybara_session_extractor.rb +30 -0
  10. data/app/models/spec_views/directory.rb +149 -0
  11. data/app/models/spec_views/html_matcher.rb +41 -0
  12. data/app/models/spec_views/http_response_extractor.rb +30 -0
  13. data/app/models/spec_views/mail_message_extractor.rb +31 -0
  14. data/app/models/spec_views/pdf_matcher.rb +53 -0
  15. data/app/models/spec_views/view_sanitizer.rb +20 -0
  16. data/app/views/layouts/spec_views.html.erb +261 -0
  17. data/app/views/spec_views/views/_actions.html.erb +11 -0
  18. data/app/views/spec_views/views/_directory_footer.html.erb +14 -0
  19. data/app/views/spec_views/views/compare.html.erb +11 -0
  20. data/app/views/spec_views/views/diff.html.erb +16 -0
  21. data/app/views/spec_views/views/index.html.erb +48 -0
  22. data/app/views/spec_views/views/preview.html.erb +32 -0
  23. data/config/routes.rb +3 -0
  24. data/lib/spec_views/configuration.rb +3 -2
  25. data/lib/spec_views/engine.rb +10 -0
  26. data/lib/spec_views/support.rb +61 -174
  27. data/lib/spec_views/version.rb +3 -1
  28. data/lib/spec_views.rb +3 -1
  29. data/lib/tasks/spec_views_tasks.rake +1 -0
  30. metadata +28 -15
  31. data/app/views/layouts/spec_views.html.haml +0 -200
  32. data/app/views/spec_views/views/compare.html.haml +0 -16
  33. data/app/views/spec_views/views/index.html.haml +0 -29
  34. 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
@@ -19,16 +19,16 @@ module SpecViews
19
19
  end
20
20
 
21
21
  def show
22
- view = filename('view')
23
- view = filename('challenger') if params[:view] == 'challenger'
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(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(view)
31
+ render html: get_view(path)
32
32
  end
33
33
  end
34
34
 
@@ -36,15 +36,25 @@ module SpecViews
36
36
  @directory = directory
37
37
  end
38
38
 
39
+ def diff
40
+ if directory.content_type.pdf?
41
+ redirect_to action: :compare
42
+ return
43
+ end
44
+ @champion = get_view(directory.champion_path, html_safe: false)
45
+ @challenger = get_view(directory.challenger_path, 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
47
- accept_directory(params[:id])
57
+ accept_directory(directory)
48
58
  redirect_to action: :index, challenger: :next
49
59
  end
50
60
 
@@ -56,8 +66,7 @@ module SpecViews
56
66
  end
57
67
 
58
68
  def reject
59
- challenger = file_path(filename('challenger'))
60
- FileUtils.remove_file(challenger)
69
+ FileUtils.remove_file(directory.challenger_path)
61
70
  redirect_to action: :index, challenger: :next
62
71
  end
63
72
 
@@ -72,66 +81,20 @@ module SpecViews
72
81
 
73
82
  private
74
83
 
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[0, 3]
104
- end
105
-
106
- def description
107
- splitted_name.third.gsub(/__pdf$/, '').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)
84
+ def directories
85
+ @directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
86
+ SpecViews::Directory.new(path)
120
87
  end
88
+ latest_run = @directories.map(&:last_run).max
121
89
 
122
- private
123
-
124
- def splitted_name
125
- @splitted_name ||= name.to_s.split(/_(DELETE|GET|PATCH|POST|PUT)_/)
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}"
126
95
  end
127
96
  end
128
97
 
129
- def directories
130
- @directories ||= Pathname.new(directory_path).children.select(&:directory?).map do |path|
131
- Directory.new(path)
132
- end.sort_by(&:basename)
133
- end
134
-
135
98
  def directory_path
136
99
  Rails.root.join(Rails.configuration.spec_views.directory)
137
100
  end
@@ -140,32 +103,17 @@ module SpecViews
140
103
  directories.detect { |dir| dir.to_param == params[:id] }
141
104
  end
142
105
 
143
- def get_view(filename = nil)
144
- filename ||= pdf? ? 'view.pdf' : 'view.html'
145
- File.read(file_path(filename)).html_safe # rubocop:disable Rails/OutputSafety
106
+ def get_view(path, html_safe: true)
107
+ content = File.read(path)
108
+ content = content.html_safe if html_safe
109
+ content
146
110
  rescue Errno::ENOENT
147
111
  ''
148
112
  end
149
113
 
150
- def file_path(filename, id: nil)
151
- id ||= params[:id]
152
- id = id.to_param if id.respond_to?(:to_param)
153
- directory_path.join(id, filename)
154
- end
155
-
156
- def filename(base = 'view')
157
- pdf? ? "#{base}.pdf" : "#{base}.html"
158
- end
159
-
160
- def pdf?
161
- params[:id] && params[:id].end_with?('__pdf')
162
- end
163
-
164
- def accept_directory(id)
165
- champion = file_path(filename('view'), id: id)
166
- challenger = file_path(filename('challenger'), id: id)
167
- FileUtils.copy_file(challenger, champion)
168
- 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)
169
117
  end
170
118
  end
171
119
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecViews
4
+ class BaseMatcher
5
+ attr_reader :response, :directory, :failure_message, :sanitizer, :type
6
+
7
+ delegate :champion_html, to: :directory
8
+
9
+ def initialize(response, description, run_time:, expected_status: :ok, sanitizer: nil, type: :request)
10
+ @response = response
11
+ @type = type
12
+ @sanitizer = sanitizer
13
+ @extractor = extractor_class.new(response, expected_status: expected_status)
14
+ @directory = SpecViews::Directory.for_description(description, content_type: content_type)
15
+ @extractor_failure = @extractor.extractor_failure?
16
+ @match = !extractor_failure? && match_challenger
17
+ directory.write_last_run(run_time) if champion_html
18
+ return if match?
19
+
20
+ if extractor_failure?
21
+ @failure_message = @extractor.failure_message
22
+ return
23
+ end
24
+
25
+ @failure_message = "#{subject_name} has changed."
26
+ @failure_message = "#{subject_name} has been added." if champion_html.nil?
27
+
28
+ directory.write_last_run(run_time)
29
+ directory.write_meta(description, type, content_type)
30
+ @directory.write_challenger(challenger_body)
31
+ end
32
+
33
+ def match?
34
+ @match
35
+ end
36
+
37
+ def extractor_failure?
38
+ @extractor_failure
39
+ end
40
+
41
+ protected
42
+
43
+ def subject_name
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def challenger_body
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def match_challenger
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def content_type
56
+ raise NotImplementedError
57
+ end
58
+
59
+ def extractor_class
60
+ return CapybaraSessionExtractor if type == :feature
61
+ return MailMessageExtractor if type == :mailer
62
+
63
+ HttpResponseExtractor
64
+ end
65
+ end
66
+ end
@@ -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
@@ -0,0 +1,149 @@
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
+ new(Rails.root.join(Rails.configuration.spec_views.directory, dir_name), content_type)
10
+ end
11
+
12
+ def initialize(path, content_type = nil)
13
+ @path = path
14
+ @content_type = ActiveSupport::StringInquirer.new(content_type.to_s) if content_type
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 controller_name
30
+ splitted_description.first.gsub(/Controller(_.*)$/, 'Controller').gsub(/Controller$/, '').gsub('_', '::')
31
+ end
32
+
33
+ def method
34
+ splitted_description.second[0, 4]
35
+ end
36
+
37
+ def description_tail
38
+ splitted_description.third
39
+ end
40
+
41
+ def delete!
42
+ FileUtils.remove_dir(path)
43
+ end
44
+
45
+ def champion_path
46
+ path.join("view.#{file_extension}")
47
+ end
48
+
49
+ def champion_html
50
+ File.read(champion_path)
51
+ rescue Errno::ENOENT
52
+ nil
53
+ end
54
+
55
+ def challenger_path
56
+ path.join("challenger.#{file_extension}")
57
+ end
58
+
59
+ def challenger?
60
+ File.file?(challenger_path)
61
+ end
62
+
63
+ def write_challenger(content)
64
+ FileUtils.mkdir_p(path)
65
+ File.open(challenger_path, binary? ? 'wb' : 'w') { |f| f.write(content) }
66
+ end
67
+
68
+ def meta_path
69
+ path.join('meta.txt')
70
+ end
71
+
72
+ def write_meta(description, spec_type, content_type)
73
+ FileUtils.mkdir_p(path)
74
+ lines = [
75
+ description.to_s.gsub("\n", ' '),
76
+ '',
77
+ spec_type.to_s,
78
+ content_type.to_s
79
+ ]
80
+ File.write(meta_path, lines.join("\n"))
81
+ end
82
+
83
+ def meta
84
+ @meta ||= File.read(meta_path).lines.map(&:strip)
85
+ end
86
+
87
+ def description
88
+ meta[0]
89
+ rescue Errno::ENOENT
90
+ name
91
+ end
92
+
93
+ def spec_type
94
+ ActiveSupport::StringInquirer.new(meta[2])
95
+ rescue Errno::ENOENT
96
+ :response
97
+ end
98
+
99
+ def content_type
100
+ @content_type ||= begin
101
+ ActiveSupport::StringInquirer.new(meta[3])
102
+ rescue Errno::ENOENT
103
+ :response
104
+ end
105
+ end
106
+
107
+ def binary?
108
+ content_type.pdf?
109
+ end
110
+
111
+ def last_run_path
112
+ path.join('last_run.txt')
113
+ end
114
+
115
+ def write_last_run(run_time)
116
+ FileUtils.mkdir_p(path)
117
+ File.write(last_run_path, run_time.to_s)
118
+ end
119
+
120
+ def last_run
121
+ Time.zone.parse(File.read(last_run_path).strip)
122
+ rescue Errno::ENOENT
123
+ Time.zone.at(0)
124
+ end
125
+
126
+ private
127
+
128
+ def splitted_description
129
+ @splitted_description ||= begin
130
+ if spec_type.feature?
131
+ split = ['', 'FEAT', description]
132
+ elsif spec_type.mailer?
133
+ split = description.to_s.match(/(\A[a-zA-Z0-9:]+)(Mailer)(.*)/)
134
+ split = split[1..3]
135
+ split[0] += 'Mailer'
136
+ split[1] = 'MAIL'
137
+ else
138
+ split = description.to_s.split(/\s(DELETE|GET|PATCH|POST|PUT)\s/)
139
+ end
140
+ split = ['', '', split[0]] if split.size == 1
141
+ split
142
+ end
143
+ end
144
+
145
+ def file_extension
146
+ content_type.pdf? ? 'pdf' : 'html'
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,41 @@
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
+ body = remove_pack_digests_from_body(remove_digests_from_body(@extractor.body))
29
+ body = sanitizer.sanitize(body) if sanitizer
30
+ body
31
+ end
32
+
33
+ def remove_digests_from_body(body)
34
+ body.gsub(/(-[a-z0-9]{64})(\.css|\.js|\.ico|\.png|\.jpg|\.jpeg|\.svg|\.gif)/, '\2')
35
+ end
36
+
37
+ def remove_pack_digests_from_body(body)
38
+ body.gsub(%r{(packs.*/js/[a-z0-9_]+)(-[a-z0-9]{20})(\.js)}, '\1\3')
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
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
+ @failure_message = "Unexpected response status #{response.status}."
13
+ end
14
+
15
+ def extractor_failure?
16
+ failure_message.present?
17
+ end
18
+
19
+ def body
20
+ response.body
21
+ end
22
+
23
+ private
24
+
25
+ def response_status_match?(response, expected_status)
26
+ response.status == expected_status ||
27
+ response.message.parameterize.underscore == expected_status.to_s
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
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
+
24
+ return if !part.respond_to?(:parts) && nil
25
+
26
+ part.parts.map do |inner_part|
27
+ extract_body(inner_part)
28
+ end.compact.first
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
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 ||= begin
39
+ body = remove_headers_from_pdf(@extractor.body)
40
+ body = sanitizer.sanitize(body) if sanitizer
41
+ body
42
+ end
43
+ end
44
+
45
+ def remove_headers_from_pdf(body)
46
+ body
47
+ .force_encoding('BINARY')
48
+ .gsub(%r{^/CreationDate \(.*\)$}, '')
49
+ .gsub(%r{^/ModDate \(.*\)$}, '')
50
+ .gsub(%r{^/ID \[<.+><.+>\]$}, '')
51
+ end
52
+ end
53
+ end
@@ -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