spec_views 1.1.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +111 -5
- 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 +35 -87
- data/app/models/spec_views/base_matcher.rb +65 -0
- data/app/models/spec_views/capybara_session_extractor.rb +30 -0
- data/app/models/spec_views/directory.rb +140 -0
- data/app/models/spec_views/html_matcher.rb +41 -0
- data/app/models/spec_views/http_response_extractor.rb +30 -0
- data/app/models/spec_views/mail_message_extractor.rb +31 -0
- data/app/models/spec_views/pdf_matcher.rb +53 -0
- data/app/models/spec_views/view_sanitizer.rb +20 -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 +61 -170
- 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 +28 -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
@@ -0,0 +1,261 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/>
|
5
|
+
<title>Spec Views</title>
|
6
|
+
<meta charset="utf-8"/>
|
7
|
+
<meta content="width=device-width, initial-scale=1, shrink-to-fit=no" name="viewport"/>
|
8
|
+
<meta content="no-cache" name="turbolinks-cache-control"/>
|
9
|
+
<%= csrf_meta_tags %>
|
10
|
+
<%= csp_meta_tag %>
|
11
|
+
<style>
|
12
|
+
:root {
|
13
|
+
--dark-bg-light: #2A2D2E;
|
14
|
+
--dark-bg-lighter: #37373D;
|
15
|
+
--dark-text: #DADADA;
|
16
|
+
|
17
|
+
--accept-bg: #5fa285;
|
18
|
+
--accept-text: var(--dark-text);
|
19
|
+
|
20
|
+
--reject-bg: #d27575;
|
21
|
+
--reject-text: var(--dark-text);
|
22
|
+
|
23
|
+
--highlight: #0E639C;
|
24
|
+
--challenger: var(--highlight);
|
25
|
+
--outdated: var(--reject-bg);
|
26
|
+
}
|
27
|
+
|
28
|
+
* {
|
29
|
+
box-sizing: border-box;
|
30
|
+
margin: 0;
|
31
|
+
padding: 0;
|
32
|
+
}
|
33
|
+
|
34
|
+
body {
|
35
|
+
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
|
36
|
+
background: #252526;
|
37
|
+
color: #FFF;
|
38
|
+
margin: 0;
|
39
|
+
padding: 0;
|
40
|
+
}
|
41
|
+
|
42
|
+
.iframes {
|
43
|
+
position: relative;
|
44
|
+
z-index: 5;
|
45
|
+
display: flex;
|
46
|
+
justify-content: space-between;
|
47
|
+
align-items: stretch;
|
48
|
+
height: calc(100vh - 50px);
|
49
|
+
overflow: auto;
|
50
|
+
}
|
51
|
+
|
52
|
+
.iframes > * {
|
53
|
+
width: calc(50% - 5px);
|
54
|
+
height: 100%;
|
55
|
+
display: flex;
|
56
|
+
flex-direction: column;
|
57
|
+
justify-content: stretch;
|
58
|
+
}
|
59
|
+
|
60
|
+
.w-100 {
|
61
|
+
width: 100%;
|
62
|
+
}
|
63
|
+
|
64
|
+
iframe {
|
65
|
+
border: 0;
|
66
|
+
margin: 0;
|
67
|
+
padding: 0;
|
68
|
+
width: 100%;
|
69
|
+
flex-grow: 1;
|
70
|
+
}
|
71
|
+
|
72
|
+
h2 {
|
73
|
+
margin: 0;
|
74
|
+
padding: 4px 0;
|
75
|
+
text-align: center;
|
76
|
+
font-weight: 300;
|
77
|
+
text-transform: uppercase;
|
78
|
+
font-size: 12px;
|
79
|
+
line-height: 1;
|
80
|
+
}
|
81
|
+
|
82
|
+
.footer {
|
83
|
+
position: relative;
|
84
|
+
z-index: 10;
|
85
|
+
display: flex;
|
86
|
+
justify-content: space-between;
|
87
|
+
height: 50px;
|
88
|
+
box-shadow: 0px -1px 4px rgba(0, 0, 0, 0.65);
|
89
|
+
}
|
90
|
+
|
91
|
+
.info {
|
92
|
+
padding: 0 0 0 10px;
|
93
|
+
font-size: 14px;
|
94
|
+
display: flex;
|
95
|
+
align-items: center;
|
96
|
+
}
|
97
|
+
.info > div {
|
98
|
+
padding: 0.5rem 0.2rem;
|
99
|
+
}
|
100
|
+
|
101
|
+
.actions {
|
102
|
+
display: flex;
|
103
|
+
justify-content: flex-end;
|
104
|
+
padding: 2px 2rem 0 0;
|
105
|
+
}
|
106
|
+
.actions > * {
|
107
|
+
margin-left: 2rem;
|
108
|
+
}
|
109
|
+
|
110
|
+
form.button_to {
|
111
|
+
display: flex;
|
112
|
+
}
|
113
|
+
|
114
|
+
input[type=submit], .btn {
|
115
|
+
display: block;
|
116
|
+
padding: 0.5rem 0.5rem 0.4rem;
|
117
|
+
border: 0 solid var(--dark-bg-lighter);
|
118
|
+
border-width: 0 0 8px 0;
|
119
|
+
color: var(--dark-text);
|
120
|
+
border-radius: 0;
|
121
|
+
background: var(--dark-bg-light);
|
122
|
+
cursor: pointer;
|
123
|
+
font-size: 0.9rem;
|
124
|
+
line-height: 1.75;
|
125
|
+
min-width: 5rem;
|
126
|
+
text-align: center;
|
127
|
+
}
|
128
|
+
input[type=submit]:hover, .btn:hover {
|
129
|
+
background: var(--dark-bg-lighter);
|
130
|
+
}
|
131
|
+
input[type=submit].accept, .btn.accept {
|
132
|
+
border-color: var(--accept-bg);
|
133
|
+
color: var(--accept-text);
|
134
|
+
}
|
135
|
+
input[type=submit].reject, .btn.reject {
|
136
|
+
border-color: var(--reject-bg);
|
137
|
+
color: var(--reject-text);
|
138
|
+
}
|
139
|
+
input[type=submit].btn, .btn.btn {
|
140
|
+
border-color: var(--highlight);
|
141
|
+
}
|
142
|
+
input[type=submit].disabled, .btn.disabled {
|
143
|
+
opacity: 0.5;
|
144
|
+
cursor: default;
|
145
|
+
}
|
146
|
+
input[type=submit].disabled:hover, .btn.disabled:hover {
|
147
|
+
background: var(--dark-bg-light);
|
148
|
+
}
|
149
|
+
|
150
|
+
a {
|
151
|
+
color: var(--dark-text);
|
152
|
+
text-decoration: none;
|
153
|
+
}
|
154
|
+
|
155
|
+
.directories {
|
156
|
+
display: flex;
|
157
|
+
flex-direction: column;
|
158
|
+
align-items: center;
|
159
|
+
margin: 5rem auto 0;
|
160
|
+
list-style-type: none;
|
161
|
+
padding: 0;
|
162
|
+
padding-bottom: 2rem;
|
163
|
+
}
|
164
|
+
.directories > div {
|
165
|
+
margin-top: 1rem;
|
166
|
+
width: 58rem;
|
167
|
+
}
|
168
|
+
.directories > a {
|
169
|
+
display: flex;
|
170
|
+
flex: 1 0 auto;
|
171
|
+
align-items: stretch;
|
172
|
+
line-height: 1.5;
|
173
|
+
}
|
174
|
+
.directories > a:hover, .directories > a:focus {
|
175
|
+
background-color: var(--dark-bg-light);
|
176
|
+
}
|
177
|
+
.directories > a:active {
|
178
|
+
background-color: var(--dark-bg-lighter);
|
179
|
+
}
|
180
|
+
.directories > a.challenger > div:first-child {
|
181
|
+
border-color: var(--challenger);
|
182
|
+
text-align: center;
|
183
|
+
}
|
184
|
+
.directories > a.outdated > div:first-child {
|
185
|
+
border-color: var(--outdated);
|
186
|
+
text-align: center;
|
187
|
+
}
|
188
|
+
.directories > a > div {
|
189
|
+
padding: 0.5rem;
|
190
|
+
}
|
191
|
+
.directories > a > :first-child {
|
192
|
+
width: 8rem;
|
193
|
+
border: 0 solid transparent;
|
194
|
+
border-left-width: 1rem;
|
195
|
+
font-size: 0.75rem;
|
196
|
+
display: flex;
|
197
|
+
align-items: center;
|
198
|
+
}
|
199
|
+
.directories > a > :last-child {
|
200
|
+
display: flex;
|
201
|
+
flex: 1 0 auto;
|
202
|
+
width: calc(100vw - 8rem - 1rem);
|
203
|
+
align-items: center;
|
204
|
+
}
|
205
|
+
@media screen and (min-width: 1300px) {
|
206
|
+
.directories > a > :last-child {
|
207
|
+
width: 75vw;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
@media screen and (min-width: 2500px) {
|
211
|
+
.directories > a > :last-child {
|
212
|
+
width: 65vw;
|
213
|
+
}
|
214
|
+
}
|
215
|
+
.directories > a > :last-child > :nth-child(1) {
|
216
|
+
flex-shrink: 0;
|
217
|
+
overflow: hidden;
|
218
|
+
text-overflow: ellipsis;
|
219
|
+
width: 15rem;
|
220
|
+
text-align: right;
|
221
|
+
}
|
222
|
+
.directories > a > :last-child > :nth-child(2) {
|
223
|
+
flex-shrink: 0;
|
224
|
+
padding: 0 1rem;
|
225
|
+
text-align: center;
|
226
|
+
width: 4rem;
|
227
|
+
}
|
228
|
+
|
229
|
+
#diff-settings {
|
230
|
+
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.65);
|
231
|
+
margin: 0;
|
232
|
+
padding: 1rem;
|
233
|
+
}
|
234
|
+
|
235
|
+
#diff-settings span, #diff-settings label {
|
236
|
+
margin-right: 1rem;
|
237
|
+
}
|
238
|
+
#diff-settings span {
|
239
|
+
margin-right: 1rem;
|
240
|
+
}
|
241
|
+
|
242
|
+
#diff-challenger, #diff-champion {
|
243
|
+
display: none;
|
244
|
+
}
|
245
|
+
|
246
|
+
#diff-result {
|
247
|
+
margin-top: 1rem;
|
248
|
+
padding: 0 1rem;
|
249
|
+
}
|
250
|
+
#diff-result ins {
|
251
|
+
color: #9dd795;
|
252
|
+
}
|
253
|
+
#diff-result del {
|
254
|
+
color: #e39ca2;
|
255
|
+
}
|
256
|
+
</style>
|
257
|
+
</head>
|
258
|
+
<body>
|
259
|
+
<%= yield %>
|
260
|
+
</body>
|
261
|
+
</html>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div class="actions">
|
2
|
+
<%= link_to 'Index', url_for(action: :index), class: 'index btn' %>
|
3
|
+
<% unless action_name == 'compare' %>
|
4
|
+
<%= link_to 'Compare', url_for(action: :compare), class: 'compare btn' %>
|
5
|
+
<% end %>
|
6
|
+
<% if action_name != 'diff' && diffable %>
|
7
|
+
<%= link_to 'Diff', url_for(action: :diff), class: 'diff btn' %>
|
8
|
+
<% end %>
|
9
|
+
<%= button_to 'Reject', url_for(action: :reject), class: 'reject' %>
|
10
|
+
<%= button_to 'Accept', url_for(action: :accept), class: 'accept' %>
|
11
|
+
</div>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<div class="footer">
|
2
|
+
<div class="info">
|
3
|
+
<div>
|
4
|
+
<%= directory.controller_name %>
|
5
|
+
</div>
|
6
|
+
<div>
|
7
|
+
<%= directory.method %>
|
8
|
+
</div>
|
9
|
+
<div>
|
10
|
+
<%= directory.description_tail %>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
<%= render 'actions', diffable: !directory.binary? %>
|
14
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div class="iframes">
|
2
|
+
<div>
|
3
|
+
<h2>Champion</h2>
|
4
|
+
<iframe src="<%= url_for(action: :show) %>"></iframe>
|
5
|
+
</div>
|
6
|
+
<div>
|
7
|
+
<h2>Challenger</h2>
|
8
|
+
<iframe src="<%= url_for(action: :show, view: :challenger) %>"></iframe>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
<%= render 'directory_footer', directory: @directory %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<div class="iframes">
|
2
|
+
<div class="w-100">
|
3
|
+
<div id="diff-settings">
|
4
|
+
<span>Diff</span>
|
5
|
+
<label><input type="radio" name="diff_type" value="diffChars" checked> Characters</label>
|
6
|
+
<label><input type="radio" name="diff_type" value="diffWords"> Words</label>
|
7
|
+
<label><input type="radio" name="diff_type" value="diffLines"> Lines</label>
|
8
|
+
</div>
|
9
|
+
<div id="diff-champion"><%= @champion %></div>
|
10
|
+
<div id="diff-challenger"><%= @challenger %></div>
|
11
|
+
<pre id="diff-result"></pre>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
<%= render 'directory_footer', directory: @directory %>
|
15
|
+
|
16
|
+
<%= javascript_include_tag 'spec_views/diff' %>
|
@@ -0,0 +1,48 @@
|
|
1
|
+
<div class="iframes">
|
2
|
+
<div class="w-100">
|
3
|
+
<div class="directories">
|
4
|
+
<% @directories.each do |dir| %>
|
5
|
+
<% body = capture do %>
|
6
|
+
<div>
|
7
|
+
<div>
|
8
|
+
<%= dir.controller_name %>
|
9
|
+
</div>
|
10
|
+
<div>
|
11
|
+
<%= dir.method %>
|
12
|
+
</div>
|
13
|
+
<div>
|
14
|
+
<%= dir.description_tail %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
<% if dir.challenger? %>
|
19
|
+
<%= link_to({ action: :compare, id: dir }, class: 'challenger') do %>
|
20
|
+
<div>CHALLENGER</div>
|
21
|
+
<%= body %>
|
22
|
+
<% end %>
|
23
|
+
<% elsif dir.last_run < @latest_run %>
|
24
|
+
<%= link_to({ action: :preview, id: dir }, class: 'outdated') do %>
|
25
|
+
<div>OUTDATED</div>
|
26
|
+
<%= body %>
|
27
|
+
<% end %>
|
28
|
+
<% else %>
|
29
|
+
<%= link_to(action: :preview, id: dir) do %>
|
30
|
+
<div></div>
|
31
|
+
<%= body %>
|
32
|
+
<% end %>
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
35
|
+
</div>
|
36
|
+
</div>
|
37
|
+
</div>
|
38
|
+
<div class="footer">
|
39
|
+
<div class="info"></div>
|
40
|
+
<div class="actions">
|
41
|
+
<% if @directories.any?{ |dir| dir.last_run < @latest_run } %>
|
42
|
+
<%= button_to 'Remove Outdated', url_for(action: :destroy_outdated), method: :delete, class: 'reject' %>
|
43
|
+
<% end %>
|
44
|
+
<% if @directories.any?(&:challenger?) %>
|
45
|
+
<%= button_to 'Accept All', url_for(action: :accept_all), method: :post, class: 'accept' %>
|
46
|
+
<% end %>
|
47
|
+
</div>
|
48
|
+
</div>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<div class="iframes">
|
2
|
+
<div class="w-100">
|
3
|
+
<h2>Champion</h2>
|
4
|
+
<iframe src="<%= url_for(action: :show) %>"></iframe>
|
5
|
+
</div>
|
6
|
+
</div>
|
7
|
+
<div class="footer">
|
8
|
+
<div class="info">
|
9
|
+
<div>
|
10
|
+
<%= @directory.controller_name %>
|
11
|
+
</div>
|
12
|
+
<div>
|
13
|
+
<%= @directory.method %>
|
14
|
+
</div>
|
15
|
+
<div>
|
16
|
+
<%= @directory.description_tail %>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
<div class="actions">
|
20
|
+
<%= link_to 'Index', url_for(action: :index), class: 'index btn' %>
|
21
|
+
<% if @previous %>
|
22
|
+
<%= link_to 'Previous', url_for(action: :preview, id: @previous), class: 'previous btn', disabled: true %>
|
23
|
+
<% else %>
|
24
|
+
<span class="previous btn disabled">Previous</span>
|
25
|
+
<% end %>
|
26
|
+
<% if @next %>
|
27
|
+
<%= link_to 'Next', url_for(action: :preview, id: @next), class: 'next btn' %>
|
28
|
+
<% else %>
|
29
|
+
<span class="previous btn disabled">Next</span>
|
30
|
+
<% end %>
|
31
|
+
</div>
|
32
|
+
</div>
|
data/config/routes.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
Rails.application.routes.draw do
|
2
4
|
namespace :spec_views do
|
3
5
|
root to: redirect('spec_views/views')
|
@@ -9,6 +11,7 @@ Rails.application.routes.draw do
|
|
9
11
|
end
|
10
12
|
member do
|
11
13
|
get :compare
|
14
|
+
get :diff
|
12
15
|
get :preview
|
13
16
|
post :accept
|
14
17
|
post :reject
|
data/lib/spec_views/engine.rb
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_views/configuration'
|
2
4
|
|
3
5
|
module SpecViews
|
4
6
|
class Engine < ::Rails::Engine
|
5
7
|
config.spec_views = Configuration.default
|
8
|
+
|
9
|
+
initializer 'spec_views.assets.precompile' do |app|
|
10
|
+
app.config.assets.precompile += %w[spec_views/diff.js]
|
11
|
+
end
|
12
|
+
|
13
|
+
config.generators do |g|
|
14
|
+
g.test_framework :rspec
|
15
|
+
end
|
6
16
|
end
|
7
17
|
end
|
data/lib/spec_views/support.rb
CHANGED
@@ -1,190 +1,81 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'timecop'
|
4
|
+
|
3
5
|
module SpecViews
|
4
6
|
module Support
|
5
|
-
class SpecView
|
6
|
-
attr_reader :body, :context, :example, :spec_name, :directory_path, :last_run_path,
|
7
|
-
:time, :response
|
8
|
-
|
9
|
-
class ReadError < StandardError
|
10
|
-
def initialize(msg = 'My default message')
|
11
|
-
super
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def initialize(context, example, response, time)
|
16
|
-
@context = context
|
17
|
-
@response = response
|
18
|
-
@example = example
|
19
|
-
@spec_name = example.full_description.strip
|
20
|
-
.gsub(/[^0-9A-Za-z.\-]/, '_').gsub('__', '_')
|
21
|
-
@spec_name = "#{@spec_name}__pdf" if pdf?
|
22
|
-
@directory_path = Rails.root.join(Rails.configuration.spec_views.directory, spec_name)
|
23
|
-
@last_run_path = directory_path.join('last_run.txt')
|
24
|
-
@body = response.body
|
25
|
-
@time = time
|
26
|
-
end
|
27
|
-
|
28
|
-
def write
|
29
|
-
FileUtils.mkdir_p(directory_path)
|
30
|
-
write_to_path(champion_path, sanitized_body)
|
31
|
-
put_write_instructions
|
32
|
-
end
|
33
|
-
|
34
|
-
def write_challenger
|
35
|
-
return unless changed?
|
36
|
-
|
37
|
-
FileUtils.mkdir_p(directory_path)
|
38
|
-
write_to_path(challenger_path, sanitized_body)
|
39
|
-
end
|
40
|
-
|
41
|
-
def delete_challenger
|
42
|
-
FileUtils.rm_f(challenger_path) unless changed?
|
43
|
-
end
|
44
|
-
|
45
|
-
def write_last_run
|
46
|
-
FileUtils.mkdir_p(directory_path)
|
47
|
-
write_to_path(last_run_path, time)
|
48
|
-
end
|
49
|
-
|
50
|
-
def expect_eq
|
51
|
-
pdf? ? expect_eq_pdf : expect_eq_text
|
52
|
-
delete_challenger
|
53
|
-
end
|
54
|
-
|
55
|
-
def expect_eq_text
|
56
|
-
context.expect(sanitized_body).to context.eq(read)
|
57
|
-
end
|
58
|
-
|
59
|
-
def expect_eq_pdf
|
60
|
-
champion_md5 = nil
|
61
|
-
begin
|
62
|
-
champion_md5 = Digest::MD5.file(champion_path).hexdigest
|
63
|
-
rescue Errno::ENOENT # rubocop:disable Lint/SuppressedException
|
64
|
-
end
|
65
|
-
current_md5 = Digest::MD5.hexdigest(sanitized_body)
|
66
|
-
context.expect(current_md5).to context.eq(champion_md5), 'MD5s of PDF do not match'
|
67
|
-
end
|
68
|
-
|
69
|
-
def read
|
70
|
-
File.read(champion_path)
|
71
|
-
rescue Errno::ENOENT
|
72
|
-
raise ReadError, "Cannot find view fixture #{champion_path.to_s.gsub(Rails.root.to_s, '')}\n" \
|
73
|
-
"Create the file by adding the follwing to your spec:\n" \
|
74
|
-
'spec_view.write'
|
75
|
-
end
|
76
|
-
|
77
|
-
def changed?
|
78
|
-
return pdf_changed? if pdf?
|
79
|
-
|
80
|
-
begin
|
81
|
-
champion = read
|
82
|
-
rescue ReadError
|
83
|
-
champion = nil
|
84
|
-
end
|
85
|
-
sanitized_body != champion
|
86
|
-
end
|
87
|
-
|
88
|
-
def pdf_changed?
|
89
|
-
champion_md5 = Digest::MD5.file(champion_path).hexdigest
|
90
|
-
current_md5 = Digest::MD5.hexdigest(sanitized_body)
|
91
|
-
champion_md5 != current_md5
|
92
|
-
rescue Errno::ENOENT
|
93
|
-
true
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def champion_path
|
99
|
-
pdf? ? directory_path.join('view.pdf') : directory_path.join('view.html')
|
100
|
-
end
|
101
|
-
|
102
|
-
def challenger_path
|
103
|
-
pdf? ? directory_path.join('challenger.pdf') : directory_path.join('challenger.html')
|
104
|
-
end
|
105
|
-
|
106
|
-
def sanitized_body
|
107
|
-
return remove_headers_from_pdf(body) if pdf?
|
108
|
-
|
109
|
-
remove_pack_digests_from_body(
|
110
|
-
remove_digests_from_body(body),
|
111
|
-
)
|
112
|
-
end
|
113
|
-
|
114
|
-
def remove_digests_from_body(body)
|
115
|
-
body.gsub(/(-[a-z0-9]{64})(\.css|\.js|\.png|\.jpg|\.jpeg|\.svg|\.gif)/, '\2')
|
116
|
-
end
|
117
|
-
|
118
|
-
def remove_pack_digests_from_body(body)
|
119
|
-
body.gsub(%r{(packs.*/js/[a-z0-9_]+)(-[a-z0-9]{20})(\.js)}, '\1\3')
|
120
|
-
end
|
121
|
-
|
122
|
-
def remove_headers_from_pdf(pdf)
|
123
|
-
pdf.gsub(/^\/CreationDate.*$/, '')
|
124
|
-
end
|
125
|
-
|
126
|
-
def put_write_instructions
|
127
|
-
puts
|
128
|
-
puts "\e[33mWarning:\e[0m Writing view fixture to #{champion_path.to_s.gsub(Rails.root.to_s, '')}"
|
129
|
-
puts 'xdg-open "http://localhost:3100/spec_views/"'
|
130
|
-
end
|
131
|
-
|
132
|
-
def write_to_path(path, content)
|
133
|
-
File.open(path.to_s, pdf? ? 'wb' : 'w') do |file|
|
134
|
-
file.write(content)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def pdf?
|
139
|
-
response.content_type == 'application/pdf'
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def spec_view
|
144
|
-
SpecView.new(self, @_spec_view_example, response, $_spec_view_time)
|
145
|
-
end
|
146
|
-
|
147
7
|
module SpecViewExample
|
148
|
-
def
|
149
|
-
context do
|
150
|
-
render_views
|
8
|
+
def it_renders(description = nil, focus: nil, pending: nil, status: :ok, &block)
|
9
|
+
context do
|
10
|
+
render_views if respond_to?(:render_views)
|
151
11
|
options = {}
|
152
12
|
options[:focus] = focus unless focus.nil?
|
153
13
|
options[:pending] = pending unless pending.nil?
|
14
|
+
description = "renders #{description}" if description.is_a?(String)
|
154
15
|
it(description, options) do
|
155
16
|
instance_eval(&block)
|
156
|
-
|
157
|
-
write = status.in?(
|
158
|
-
[
|
159
|
-
response.message.parameterize.underscore.to_sym,
|
160
|
-
response.status,
|
161
|
-
],
|
162
|
-
)
|
163
|
-
spec_view.write_challenger if write
|
164
|
-
spec_view.expect_eq
|
17
|
+
expect(response).to match_html_fixture.for_status(status)
|
165
18
|
end
|
166
19
|
end
|
167
20
|
end
|
168
21
|
end
|
22
|
+
end
|
23
|
+
end
|
169
24
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
25
|
+
RSpec.shared_context 'SpecViews ViewSanitizer' do
|
26
|
+
let(:view_sanitizer) { SpecViews::ViewSanitizer.new }
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec.configure do |c|
|
30
|
+
c.before(:suite) do |_example|
|
31
|
+
$_spec_view_time = Time.zone.now # rubocop:disable Style/GlobalVars
|
32
|
+
end
|
33
|
+
c.include_context 'SpecViews ViewSanitizer'
|
34
|
+
%i[controller feature mailer request].each do |type|
|
35
|
+
c.extend SpecViews::Support::SpecViewExample, type: type
|
36
|
+
c.before(type: type) do |example|
|
37
|
+
@_spec_view_example = example
|
38
|
+
end
|
39
|
+
c.before(:each, type: type) do
|
40
|
+
Timecop.freeze(Time.zone.local(2024, 2, 29, 0o0, 17, 42))
|
41
|
+
end
|
42
|
+
c.after(:each, type: type) do
|
43
|
+
Timecop.return
|
184
44
|
end
|
185
45
|
end
|
186
46
|
end
|
187
47
|
|
188
|
-
|
189
|
-
|
48
|
+
matchers = [
|
49
|
+
[:match_html_fixture, SpecViews::HtmlMatcher],
|
50
|
+
[:match_pdf_fixture, SpecViews::PdfMatcher]
|
51
|
+
]
|
52
|
+
|
53
|
+
matchers.each do |matcher|
|
54
|
+
RSpec::Matchers.define matcher.first do |_expected|
|
55
|
+
chain :for_status do |status|
|
56
|
+
@status = status
|
57
|
+
end
|
58
|
+
|
59
|
+
match do |actual|
|
60
|
+
example = @matcher_execution_context.instance_variable_get('@_spec_view_example')
|
61
|
+
description = example.full_description
|
62
|
+
type = example.metadata[:type]
|
63
|
+
run_time = $_spec_view_time # rubocop:disable Style/GlobalVars
|
64
|
+
@status ||= :ok
|
65
|
+
@matcher = matcher.second.new(
|
66
|
+
actual,
|
67
|
+
description,
|
68
|
+
expected_status: @status,
|
69
|
+
run_time: run_time,
|
70
|
+
sanitizer: view_sanitizer,
|
71
|
+
type: type
|
72
|
+
)
|
73
|
+
return @matcher.match?
|
74
|
+
end
|
75
|
+
|
76
|
+
failure_message do |_actual|
|
77
|
+
"#{@matcher.failure_message} " \
|
78
|
+
"Review the challenger: #{Rails.configuration.spec_views.ui_url}"
|
79
|
+
end
|
80
|
+
end
|
190
81
|
end
|