wraith 2.6.0 → 2.7.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 +1 -1
- data/{wraith-logo.png → assets/wraith-logo.png} +0 -0
- data/lib/wraith/cli.rb +2 -9
- data/lib/wraith/folder.rb +5 -1
- data/lib/wraith/gallery.rb +11 -1
- data/lib/wraith/gallery_template/gallery_template.erb +3 -0
- data/lib/wraith/version.rb +1 -1
- data/lib/wraith/wraith.rb +1 -1
- data/{configs → spec/configs}/test_config--casper.yaml +6 -1
- data/{configs/test_config.yaml → spec/configs/test_config--phantom.yaml} +6 -1
- data/spec/helpers.rb +6 -2
- data/spec/js/global.js +2 -2
- data/spec/js/path.js +2 -2
- data/spec/thumbnails/test/test_image-1.png +0 -0
- data/spec/thumbnails/test/test_image-2.png +0 -0
- data/spec/thumbnails/test/test_image-diff.png +0 -0
- data/spec/wraith_spec.rb +79 -7
- data/templates/README.md +5 -0
- data/templates/configs/component.yaml +58 -0
- data/templates/configs/multiple_domains.yaml +54 -0
- data/templates/configs/spider.yaml +54 -0
- data/templates/javascript/README.md +5 -0
- data/templates/javascript/_getDimensions.js +7 -0
- data/templates/javascript/_phantom__common.js +122 -0
- data/templates/javascript/beforeCapture--casper_example.js +5 -0
- data/templates/javascript/beforeCapture--phantom_example.js +9 -0
- data/templates/javascript/casper.js +59 -0
- data/templates/javascript/phantom--nojs.js +6 -0
- data/templates/javascript/phantom.js +6 -0
- metadata +26 -17
- data/configs/component.yaml +0 -47
- data/configs/config_nojs.yaml +0 -43
- data/configs/custom_height.yaml +0 -42
- data/configs/history.yaml +0 -52
- data/configs/templates/component.yaml +0 -51
- data/configs/templates/config.yaml +0 -43
- data/lib/wraith/javascript/beforeCapture.js +0 -5
- data/lib/wraith/javascript/casper.js +0 -48
- data/lib/wraith/javascript/customHeight.js +0 -99
- data/lib/wraith/javascript/nojs.js +0 -85
- data/lib/wraith/javascript/snap.js +0 -85
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7d0d54a5e957a901f42244cecc6523883865a91a
|
4
|
+
data.tar.gz: 94e209bfface9c61b060e3e9624d5cf9609a59ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21ff9b3f556c593da98bdd1bef23ad62b5d16898d8f38589cbf0eb1d4b1c9535a1e57e6bfd1f0bf32fda5207fc7ecbcf87a879c19e291d406e3f2731ee041a66
|
7
|
+
data.tar.gz: ff94dad2fb1d688a3fbebc110e27c234c656945e56db980a41f7570bd1a598aa05ff9eb520b87426efc2d75655109d78f0fc5da8f1138b65e036c72579285faa
|
data/README.md
CHANGED
File without changes
|
data/lib/wraith/cli.rb
CHANGED
@@ -19,15 +19,8 @@ class Wraith::CLI < Thor
|
|
19
19
|
|
20
20
|
desc "setup", "creates config folder and default config"
|
21
21
|
def setup
|
22
|
-
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
desc "setup_casper", "creates config folder and default config for casper"
|
27
|
-
def setup_casper
|
28
|
-
template("configs/templates/component.yaml", "configs/component.yaml")
|
29
|
-
template("lib/wraith/javascript/casper.js", "javascript/casper.js")
|
30
|
-
template("lib/wraith/javascript/beforeCapture.js", "javascript/beforeCapture.js")
|
22
|
+
directory("templates/configs", "configs")
|
23
|
+
directory("templates/javascript", "javascript")
|
31
24
|
end
|
32
25
|
|
33
26
|
desc "reset_shots [config_name]", "removes all the files in the shots folder"
|
data/lib/wraith/folder.rb
CHANGED
@@ -34,7 +34,11 @@ class Wraith::FolderManager
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def copy_old_shots
|
37
|
-
|
37
|
+
if history_dir.nil?
|
38
|
+
abort 'Error: no `history_dir` attribute found in config. Cannot copy files.'
|
39
|
+
else
|
40
|
+
FileUtils.cp_r("#{dir}/.", "#{history_dir}/")
|
41
|
+
end
|
38
42
|
end
|
39
43
|
|
40
44
|
def restore_shots
|
data/lib/wraith/gallery.rb
CHANGED
@@ -50,7 +50,7 @@ class Wraith::GalleryGenerator
|
|
50
50
|
|
51
51
|
def matcher(match, filename, dirname, category)
|
52
52
|
@size = match[1].to_i
|
53
|
-
@group = match
|
53
|
+
@group = get_group_from_match match
|
54
54
|
@filepath = category + "/" + filename
|
55
55
|
@thumbnail = "thumbnails/#{category}/#{filename}"
|
56
56
|
|
@@ -63,6 +63,16 @@ class Wraith::GalleryGenerator
|
|
63
63
|
data_group(@group, size_dict, dirname, @filepath)
|
64
64
|
end
|
65
65
|
|
66
|
+
def get_group_from_match(match)
|
67
|
+
group = match[2]
|
68
|
+
dash = match[2].rindex('-')
|
69
|
+
|
70
|
+
if !dash.nil?
|
71
|
+
group = match[2][dash+1..-1]
|
72
|
+
end
|
73
|
+
group
|
74
|
+
end
|
75
|
+
|
66
76
|
def data_group(group, size_dict, dirname, filepath)
|
67
77
|
case group
|
68
78
|
when "diff"
|
data/lib/wraith/version.rb
CHANGED
data/lib/wraith/wraith.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
##########
|
2
|
+
### NB: the paths in this YAML config are relative to the root of the Wraith directory,
|
3
|
+
### as `bundle exec rspec` is run from the root.
|
4
|
+
##########
|
5
|
+
|
1
6
|
#Headless browser option
|
2
7
|
browser:
|
3
8
|
phantomjs: "casperjs"
|
4
9
|
# slimerjs: "slimerjs"
|
5
10
|
|
6
11
|
#If you want to have multiple snapping files, set the file name here
|
7
|
-
snap_file: "
|
12
|
+
snap_file: "templates/javascript/casper.js"
|
8
13
|
|
9
14
|
# Type the name of the directory that shots will be stored in
|
10
15
|
directory: 'shots'
|
@@ -1,10 +1,15 @@
|
|
1
|
+
##########
|
2
|
+
### NB: the paths in this YAML config are relative to the root of the Wraith directory,
|
3
|
+
### as `bundle exec rspec` is run from the root.
|
4
|
+
##########
|
5
|
+
|
1
6
|
#Headless browser option
|
2
7
|
browser:
|
3
8
|
phantomjs: "phantomjs"
|
4
9
|
# slimerjs: "slimerjs"
|
5
10
|
|
6
11
|
#If you want to have multiple snapping files, set the file name here
|
7
|
-
snap_file: "
|
12
|
+
snap_file: "templates/javascript/phantom.js"
|
8
13
|
|
9
14
|
# Type the name of the directory that shots will be stored in
|
10
15
|
directory: 'shots'
|
data/spec/helpers.rb
CHANGED
@@ -15,8 +15,12 @@ end
|
|
15
15
|
|
16
16
|
def run_js_then_capture(config)
|
17
17
|
generated_image = 'shots/test/temporary_jsified_image.png'
|
18
|
-
saving.capture_page_image(
|
18
|
+
saving.capture_page_image(config[:engine], test_url1, 320, generated_image, selector, config[:global_js], config[:path_js])
|
19
19
|
Wraith::CompareImages.new(config_name).compare_task(generated_image, config[:output_should_look_like], diff_image, data_txt)
|
20
20
|
diff = File.open('shots/test/test.txt', "rb").read
|
21
21
|
expect(diff).to eq '0.0'
|
22
|
-
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_path_relative_to(current_file, file_to_find)
|
25
|
+
File.expand_path(File.join(File.dirname(current_file), file_to_find))
|
26
|
+
end
|
data/spec/js/global.js
CHANGED
data/spec/js/path.js
CHANGED
File without changes
|
File without changes
|
File without changes
|
data/spec/wraith_spec.rb
CHANGED
@@ -4,7 +4,7 @@ require "helpers"
|
|
4
4
|
require "./lib/wraith/cli"
|
5
5
|
|
6
6
|
describe Wraith do
|
7
|
-
let(:config_name) { "test_config" }
|
7
|
+
let(:config_name) { get_path_relative_to __FILE__, "./configs/test_config--phantom.yaml" }
|
8
8
|
let(:test_url1) { "http://www.bbc.com/afrique" }
|
9
9
|
let(:test_url2) { "http://www.bbc.com/russian" }
|
10
10
|
let(:test_image1) { "shots/test/test1.png" }
|
@@ -101,8 +101,40 @@ describe Wraith do
|
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
describe "When
|
105
|
-
let(:
|
104
|
+
describe "When generating gallery" do
|
105
|
+
let(:gallery) { Wraith::GalleryGenerator.new(config_name, false) }
|
106
|
+
|
107
|
+
it "should not break when there is a `-` in the filename" do
|
108
|
+
dirs = gallery.parse_directories 'spec/thumbnails'
|
109
|
+
|
110
|
+
images = [
|
111
|
+
{
|
112
|
+
:filename => 'test/test_image-1.png',
|
113
|
+
:thumb => 'thumbnails/test/test_image-1.png'
|
114
|
+
},
|
115
|
+
{
|
116
|
+
:filename => 'test/test_image-2.png',
|
117
|
+
:thumb => 'thumbnails/test/test_image-2.png'
|
118
|
+
}
|
119
|
+
]
|
120
|
+
|
121
|
+
dirs['test'][0][:variants].each_with_index do |image, i|
|
122
|
+
expect(image[:filename]).to eq images[i][:filename]
|
123
|
+
expect(image[:thumb]).to eq images[i][:thumb]
|
124
|
+
end
|
125
|
+
|
126
|
+
diff = {
|
127
|
+
:filename => 'test/test_image-diff.png',
|
128
|
+
:thumb => 'thumbnails/test/test_image-diff.png'
|
129
|
+
}
|
130
|
+
|
131
|
+
expect(dirs['test'][0][:diff][:filename]).to eq 'test/test_image-diff.png'
|
132
|
+
expect(dirs['test'][0][:diff][:thumb]).to eq 'thumbnails/test/test_image-diff.png'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "When hooking into beforeCapture (CasperJS)" do
|
137
|
+
let(:config_name) { get_path_relative_to __FILE__, "./configs/test_config--casper.yaml" }
|
106
138
|
let(:saving) { Wraith::SaveImages.new(config_name) }
|
107
139
|
let(:wraith) { Wraith::Wraith.new(config_name) }
|
108
140
|
let(:selector) { "body" }
|
@@ -112,8 +144,9 @@ describe Wraith do
|
|
112
144
|
it "Executes the global JS before capturing" do
|
113
145
|
run_js_then_capture(
|
114
146
|
global_js: before_suite_js,
|
115
|
-
path_js:
|
116
|
-
output_should_look_like: 'spec/base/global.png'
|
147
|
+
path_js: 'false',
|
148
|
+
output_should_look_like: 'spec/base/global.png',
|
149
|
+
engine: 'casperjs'
|
117
150
|
)
|
118
151
|
end
|
119
152
|
|
@@ -121,7 +154,8 @@ describe Wraith do
|
|
121
154
|
run_js_then_capture(
|
122
155
|
global_js: 'false',
|
123
156
|
path_js: before_capture_js,
|
124
|
-
output_should_look_like: 'spec/base/path.png'
|
157
|
+
output_should_look_like: 'spec/base/path.png',
|
158
|
+
engine: 'casperjs'
|
125
159
|
)
|
126
160
|
end
|
127
161
|
|
@@ -129,9 +163,47 @@ describe Wraith do
|
|
129
163
|
run_js_then_capture(
|
130
164
|
global_js: before_suite_js,
|
131
165
|
path_js: before_capture_js,
|
132
|
-
output_should_look_like: 'spec/base/path.png'
|
166
|
+
output_should_look_like: 'spec/base/path.png',
|
167
|
+
engine: 'casperjs'
|
133
168
|
)
|
134
169
|
end
|
135
170
|
end
|
136
171
|
|
172
|
+
# @TODO - uncomment and figure out why broken
|
173
|
+
# describe "When hooking into beforeCapture (PhantomJS)" do
|
174
|
+
# let(:config_name) { get_path_relative_to __FILE__, "./configs/test_config--phantom.yaml" }
|
175
|
+
# let(:saving) { Wraith::SaveImages.new(config_name) }
|
176
|
+
# let(:wraith) { Wraith::Wraith.new(config_name) }
|
177
|
+
# let(:selector) { "body" }
|
178
|
+
# let(:before_suite_js) { "../../spec/js/global.js" }
|
179
|
+
# let(:before_capture_js) { "../../spec/js/path.js" }
|
180
|
+
|
181
|
+
# it "Executes the global JS before capturing" do
|
182
|
+
# run_js_then_capture(
|
183
|
+
# global_js: before_suite_js,
|
184
|
+
# path_js: 'false',
|
185
|
+
# output_should_look_like: 'spec/base/global.png',
|
186
|
+
# engine: 'phantomjs'
|
187
|
+
# )
|
188
|
+
# end
|
189
|
+
|
190
|
+
# it "Executes the path-level JS before capturing" do
|
191
|
+
# run_js_then_capture(
|
192
|
+
# global_js: 'false',
|
193
|
+
# path_js: before_capture_js,
|
194
|
+
# output_should_look_like: 'spec/base/path.png',
|
195
|
+
# engine: 'phantomjs'
|
196
|
+
# )
|
197
|
+
# end
|
198
|
+
|
199
|
+
# it "Executes the global JS before the path-level JS" do
|
200
|
+
# run_js_then_capture(
|
201
|
+
# global_js: before_suite_js,
|
202
|
+
# path_js: before_capture_js,
|
203
|
+
# output_should_look_like: 'spec/base/path.png',
|
204
|
+
# engine: 'phantomjs'
|
205
|
+
# )
|
206
|
+
# end
|
207
|
+
# end
|
208
|
+
|
137
209
|
end
|
data/templates/README.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Everything in this directory (with the exception of this README) gets copied to the user's project when they run `wraith setup`.
|
2
|
+
|
3
|
+
The idea is that the user can run any of these as an example, e.g. `wraith capture configs/component.yaml`.
|
4
|
+
|
5
|
+
The configs and javascript are set up on the assumption that they are called from the directory above.
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Add as many domains as necessary. Key will act as a label
|
2
|
+
domains:
|
3
|
+
english: "http://www.bbc.co.uk"
|
4
|
+
|
5
|
+
# (optional) JavaScript file to execute before taking screenshot of every path.
|
6
|
+
# Can be used to reload current page with headers specified, or to dynamically
|
7
|
+
# AJAX in content, or all manner of other uses!
|
8
|
+
#before_capture: 'javascript/global.js'
|
9
|
+
|
10
|
+
# Type page URL paths below, here are a couple of examples
|
11
|
+
paths:
|
12
|
+
clickable_guide:
|
13
|
+
path: /news/entertainment-arts-27221191
|
14
|
+
selector: '.idt__news' # selector to take a screenshot of
|
15
|
+
clickable_guide__after_click:
|
16
|
+
path: /news/entertainment-arts-27221191
|
17
|
+
selector: '.idt__news'
|
18
|
+
# (optional) JavaScript file to execute before taking the screenshot of this path.
|
19
|
+
# This is always executed AFTER the 'global' before_capture hook toward the top of this file.
|
20
|
+
before_capture: 'javascript/beforeCapture--casper_example.js'
|
21
|
+
|
22
|
+
# amount of fuzz ImageMagick will use when comparing images. A higher fuzz makes the comparison less strict.
|
23
|
+
fuzz: '20%'
|
24
|
+
|
25
|
+
# the maximum acceptable level of difference (in %) between two images.
|
26
|
+
# Wraith considers it a failure if an image diff goes above this threshold.
|
27
|
+
threshold: 5
|
28
|
+
|
29
|
+
# screen widths (and optional height) to resize the browser to before taking the screenshot
|
30
|
+
screen_widths:
|
31
|
+
- 320
|
32
|
+
- 600x768
|
33
|
+
- 768
|
34
|
+
- 1024
|
35
|
+
- 1280
|
36
|
+
|
37
|
+
# the engine to run Wraith with.
|
38
|
+
browser:
|
39
|
+
phantomjs: "casperjs" # variant of PhantomJS that allows screenshots by element selector
|
40
|
+
#phantomjs: "phantomjs" # PhantomJS, built on top of Webkit and JavascriptCore (like Safari)
|
41
|
+
#slimerjs: "slimerjs" # SlimerJS, built on top of Gecko and SpiderMonkey (like Firefox)
|
42
|
+
|
43
|
+
# the file in charge of taking the screenshot
|
44
|
+
snap_file: "javascript/casper.js"
|
45
|
+
|
46
|
+
# the directory that your base screenshots will be stored in
|
47
|
+
history_dir: 'shots_base'
|
48
|
+
|
49
|
+
# the directory that your latest screenshots will be stored in
|
50
|
+
directory: 'shots'
|
51
|
+
|
52
|
+
# choose how results are displayed in the gallery (default is `alphanumeric` if omitted)
|
53
|
+
# Different screen widths are always grouped together.
|
54
|
+
# Options:
|
55
|
+
# alphanumeric - all paths (with or without a difference) are shown, sorted by path
|
56
|
+
# diffs_first - all paths (with or without a difference) are shown, sorted by difference size (largest first)
|
57
|
+
# diffs_only - only paths with a difference are shown, sorted by difference size (largest first)
|
58
|
+
mode: alphanumeric
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Add as many domains as necessary. Key will act as a label
|
2
|
+
domains:
|
3
|
+
arabic: "http://www.bbc.com/arabic"
|
4
|
+
persian: "http://www.bbc.com/persian"
|
5
|
+
russian: "http://www.bbc.com/russian"
|
6
|
+
|
7
|
+
# (optional) JavaScript file to execute before taking screenshot of every path.
|
8
|
+
# Can be used to reload current page with headers specified, or to dynamically
|
9
|
+
# AJAX in content, or all manner of other uses!
|
10
|
+
#before_capture: 'javascript/global.js'
|
11
|
+
|
12
|
+
# Type page URL paths below, here are a couple of examples.
|
13
|
+
# They should exist for all of the domains you've specified above.
|
14
|
+
paths:
|
15
|
+
home: /
|
16
|
+
interactivity: /interactivity
|
17
|
+
|
18
|
+
# amount of fuzz ImageMagick will use when comparing images. A higher fuzz makes the comparison less strict.
|
19
|
+
fuzz: '20%'
|
20
|
+
|
21
|
+
# the maximum acceptable level of difference (in %) between two images.
|
22
|
+
# Wraith considers it a failure if an image diff goes above this threshold.
|
23
|
+
threshold: 5
|
24
|
+
|
25
|
+
# screen widths (and optional height) to resize the browser to before taking the screenshot
|
26
|
+
screen_widths:
|
27
|
+
- 320
|
28
|
+
- 600x768
|
29
|
+
- 768
|
30
|
+
- 1024
|
31
|
+
- 1280
|
32
|
+
|
33
|
+
# the engine to run Wraith with.
|
34
|
+
browser:
|
35
|
+
#phantomjs: "casperjs" # variant of PhantomJS that allows screenshots by element selector
|
36
|
+
phantomjs: "phantomjs" # PhantomJS, built on top of Webkit and JavascriptCore (like Safari)
|
37
|
+
#slimerjs: "slimerjs" # SlimerJS, built on top of Gecko and SpiderMonkey (like Firefox)
|
38
|
+
|
39
|
+
# the file in charge of taking the screenshot
|
40
|
+
snap_file: "javascript/phantom--nojs.js"
|
41
|
+
|
42
|
+
# the directory that your base screenshots will be stored in
|
43
|
+
history_dir: 'shots_base'
|
44
|
+
|
45
|
+
# the directory that your latest screenshots will be stored in
|
46
|
+
directory: 'shots'
|
47
|
+
|
48
|
+
# choose how results are displayed in the gallery (default is `alphanumeric` if omitted)
|
49
|
+
# Different screen widths are always grouped together.
|
50
|
+
# Options:
|
51
|
+
# alphanumeric - all paths (with or without a difference) are shown, sorted by path
|
52
|
+
# diffs_first - all paths (with or without a difference) are shown, sorted by difference size (largest first)
|
53
|
+
# diffs_only - only paths with a difference are shown, sorted by difference size (largest first)
|
54
|
+
mode: diffs_first
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# Add as many domains as necessary. Key will act as a label
|
2
|
+
domains:
|
3
|
+
my_site: "http://www.example.com"
|
4
|
+
|
5
|
+
# Notice the absence of a `paths` property. When no paths are provided, Wraith defaults to
|
6
|
+
# spidering mode to check your entire website.
|
7
|
+
|
8
|
+
# A list of URLs to skip when spidering.
|
9
|
+
# Ruby regular expressions can be used, if prefixed with `!ruby/regexp` as defined in the YAML Cookbook.
|
10
|
+
# See http://www.yaml.org/YAML_for_ruby.html#regexps
|
11
|
+
spider_skips:
|
12
|
+
- /foo/bar.html # Matches /foo/bar.html explicitly
|
13
|
+
- !ruby/regexp /^\/baz\// # Matches any URLs that start with /baz
|
14
|
+
|
15
|
+
# the filename of the spider file to use. Default: spider.txt
|
16
|
+
spider_file: example_com_spider.txt
|
17
|
+
|
18
|
+
# the number of days to keep the site spider file
|
19
|
+
spider_days: 10
|
20
|
+
|
21
|
+
# amount of fuzz ImageMagick will use when comparing images. A higher fuzz makes the comparison less strict.
|
22
|
+
fuzz: '20%'
|
23
|
+
|
24
|
+
# the maximum acceptable level of difference (in %) between two images.
|
25
|
+
# Wraith considers it a failure if an image diff goes above this threshold.
|
26
|
+
threshold: 5
|
27
|
+
|
28
|
+
# screen widths (and optional height) to resize the browser to before taking the screenshot
|
29
|
+
screen_widths:
|
30
|
+
- 320
|
31
|
+
- 600x768
|
32
|
+
- 768
|
33
|
+
- 1024
|
34
|
+
- 1280
|
35
|
+
|
36
|
+
# the engine to run Wraith with.
|
37
|
+
browser:
|
38
|
+
#phantomjs: "casperjs" # variant of PhantomJS that allows screenshots by element selector
|
39
|
+
phantomjs: "phantomjs" # PhantomJS, built on top of Webkit and JavascriptCore (like Safari)
|
40
|
+
#slimerjs: "slimerjs" # SlimerJS, built on top of Gecko and SpiderMonkey (like Firefox)
|
41
|
+
|
42
|
+
# the file in charge of taking the screenshot
|
43
|
+
snap_file: "javascript/phantom.js"
|
44
|
+
|
45
|
+
# the directory that your latest screenshots will be stored in
|
46
|
+
directory: 'shots_nojs'
|
47
|
+
|
48
|
+
# choose how results are displayed in the gallery (default is `alphanumeric` if omitted)
|
49
|
+
# Different screen widths are always grouped together.
|
50
|
+
# Options:
|
51
|
+
# alphanumeric - all paths (with or without a difference) are shown, sorted by path
|
52
|
+
# diffs_first - all paths (with or without a difference) are shown, sorted by difference size (largest first)
|
53
|
+
# diffs_only - only paths with a difference are shown, sorted by difference size (largest first)
|
54
|
+
mode: diffs_first
|