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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3184a08acda31e87ed515d6f32819e70183e5ae887f430e3d21b8e84828a66fe
4
- data.tar.gz: 7a852a0f05d557cef81590cea27faf5db52a4c82726a384823985b4a3ebf2cb2
3
+ metadata.gz: 42a03f1d324852c51d56e4afdadadcc00e6449954917f396599df6c4568176d0
4
+ data.tar.gz: 466b8bddc2f1c78bed3bbe57370b26bc87f71ecf38e2476ccc46fa4237a5410a
5
5
  SHA512:
6
- metadata.gz: 19c5aeeef63e2db2b68073076fca8217631241a1aa65e45f94ebacf45d141135e93f9f001c882aff5b9f2b2e62aeaa0bcf3afd38a890682256b1dfad8e53daec
7
- data.tar.gz: 1bb5e90a3fe9fad27653d439bc6b91d140f18c0be9500125f8a2bda0573fb7f68aca301c0fef0f545a6bf6fa9e5e4f5698c91fcbd02c0ce3a4b89c86bf782f25
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, :type => :mailer do
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
- 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
 
@@ -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(filename('view'), html_safe: false)
45
- @challenger = get_view(filename('challenger'), html_safe: false)
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(params[:id])
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
- challenger = file_path(filename('challenger'))
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.sort_by(&:basename)
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(filename = nil, html_safe: true)
100
- filename ||= pdf? ? 'view.pdf' : 'view.html'
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 file_path(filename, id: nil)
109
- id ||= params[:id]
110
- id = id.to_param if id.respond_to?(:to_param)
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
- @directory.write_last_run(run_time) if champion_html
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
- @directory.write_last_run(run_time)
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
- dir_name = "#{dir_name}__pdf" if content_type == :pdf
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.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
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 last_run_path
77
- path.join('last_run.txt')
68
+ def meta_path
69
+ path.join('meta.txt')
78
70
  end
79
71
 
80
- def write_last_run(time)
72
+ def write_meta(description, run_time, spec_type, content_type)
81
73
  FileUtils.mkdir_p(path)
82
- File.write(last_run_path, time)
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 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)
83
+ def meta
84
+ @meta ||= File.read(meta_path).lines.map(&:strip)
92
85
  end
93
86
 
94
87
  def description
95
- File.read(description_path)
88
+ meta[0]
96
89
  rescue Errno::ENOENT
97
90
  name
98
91
  end
99
92
 
100
- def pdf?
101
- name.to_s.ends_with?('__pdf')
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 mailer?
105
- @mailer ||= name.to_s.start_with?(/[a-zA-Z0-9:]+Mailer[^a-zA-Z0-9]/)
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 mailer?
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,6 @@ module SpecViews
9
9
  @status_match = response_status_match?(response, expected_status)
10
10
  return if @status_match
11
11
 
12
-
13
12
  @failure_message = "Unexpected response status #{response.status}."
14
13
  end
15
14
 
@@ -9,7 +9,7 @@ module SpecViews
9
9
  @body = extract_body(mail)
10
10
  return if body.present?
11
11
 
12
- @failure_message = "Failed to find mail part"
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
- return if nil unless part.respond_to?(:parts)
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 ||= remove_headers_from_pdf(@extractor.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
@@ -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
- %i[controller mailer request].each do |type|
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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SpecViews
4
- VERSION = '2.0.0'
4
+ VERSION = '3.0.0'
5
5
  end
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: 2.0.0
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-07-26 00:00:00.000000000 Z
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