diffux_ci 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/diffux_ci +37 -0
- data/lib/diffux_ci-diffs.html.erb +17 -0
- data/lib/diffux_ci_action.rb +29 -0
- data/lib/diffux_ci_runner.rb +116 -0
- data/lib/diffux_ci_server.rb +42 -0
- data/lib/diffux_ci_uploader.rb +55 -0
- data/lib/diffux_ci_utils.rb +71 -0
- data/lib/public/diffux_ci-runner.js +191 -0
- data/lib/views/index.erb +26 -0
- data/lib/views/review.erb +44 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0e45d93f1149b5ad73fe788533dc5ed95d737732
|
4
|
+
data.tar.gz: de408672cf9cb59e357e9f08243b7a03760c60a3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c1f8b36ff77a17db7742e695982346a7f8fc667c715888af935b3696de3bcd6ff589f37c98ca3fc28dc8c1b953929344b84d14d68222fe575374a69bd4bf04a2
|
7
|
+
data.tar.gz: 50f65df9ec4732dc4451002c33fdae827c872ebd49a713eaea2a8c09eea67208d4f284a251d8cba0e132fad5355d4a0de137cde60aef9cd5d4e3005a168bd47e
|
data/bin/diffux_ci
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'diffux_ci_utils'
|
4
|
+
require 'diffux_ci_action'
|
5
|
+
require 'diffux_ci_uploader'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
action = ARGV[0] || 'run'
|
9
|
+
case action
|
10
|
+
when 'run'
|
11
|
+
Thread.abort_on_exception = true
|
12
|
+
Thread.new do
|
13
|
+
require 'diffux_ci_runner'
|
14
|
+
exit
|
15
|
+
end
|
16
|
+
require 'diffux_ci_server'
|
17
|
+
|
18
|
+
when 'review'
|
19
|
+
system 'open', DiffuxCIUtils.construct_url('/review')
|
20
|
+
require 'diffux_ci_server'
|
21
|
+
|
22
|
+
when 'clean'
|
23
|
+
if File.directory? DiffuxCIUtils.config['snapshots_folder']
|
24
|
+
FileUtils.remove_entry_secure DiffuxCIUtils.config['snapshots_folder']
|
25
|
+
end
|
26
|
+
|
27
|
+
when 'approve', 'reject'
|
28
|
+
abort 'Missing example name' unless example_name = ARGV[1]
|
29
|
+
abort 'Missing viewport name' unless viewport_name = ARGV[2]
|
30
|
+
DiffuxCIAction.new(example_name, viewport_name).send(action)
|
31
|
+
|
32
|
+
when 'upload_diffs'
|
33
|
+
# `upload_diffs` returns a URL to a static html file
|
34
|
+
puts DiffuxCIUploader.new.upload_diffs
|
35
|
+
else
|
36
|
+
abort "Unknown action \"#{action}\""
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Diffux-CI diffs</title>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<h1>Diffux-CI diffs</h1>
|
8
|
+
<p>File generated: <%= Time.now %></p>
|
9
|
+
|
10
|
+
<% diff_images.each do |diff| %>
|
11
|
+
<h3>
|
12
|
+
<%= diff[:name] %> @ <%= diff[:viewport] %>
|
13
|
+
</h3>
|
14
|
+
<p><img src="<%= diff[:url] %>"></p>
|
15
|
+
<% end %>
|
16
|
+
</body>
|
17
|
+
</html>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'diffux_ci_utils'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class DiffuxCIAction
|
5
|
+
def initialize(example_name, viewport_name)
|
6
|
+
@example_name = example_name
|
7
|
+
@viewport_name = viewport_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def approve
|
11
|
+
diff_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'diff.png')
|
12
|
+
baseline_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'baseline.png')
|
13
|
+
candidate_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'candidate.png')
|
14
|
+
|
15
|
+
FileUtils.rm(diff_path, force: true)
|
16
|
+
|
17
|
+
if File.exist? candidate_path
|
18
|
+
FileUtils.mv(candidate_path, baseline_path)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def reject
|
23
|
+
diff_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'diff.png')
|
24
|
+
candidate_path = DiffuxCIUtils.path_to(@example_name, @viewport_name, 'candidate.png')
|
25
|
+
|
26
|
+
FileUtils.rm(diff_path, force: true)
|
27
|
+
FileUtils.rm(candidate_path, force: true)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
require 'diffux_core/snapshot_comparer'
|
3
|
+
require 'diffux_core/snapshot_comparison_image/base'
|
4
|
+
require 'diffux_core/snapshot_comparison_image/gutter'
|
5
|
+
require 'diffux_core/snapshot_comparison_image/before'
|
6
|
+
require 'diffux_core/snapshot_comparison_image/overlayed'
|
7
|
+
require 'diffux_core/snapshot_comparison_image/after'
|
8
|
+
require 'chunky_png'
|
9
|
+
require 'diffux_ci_utils'
|
10
|
+
require 'fileutils'
|
11
|
+
|
12
|
+
def resolve_viewports(example)
|
13
|
+
configured_viewports = DiffuxCIUtils.config['viewports']
|
14
|
+
|
15
|
+
(example['options']['viewports'] || [configured_viewports.first.first]).map do |viewport|
|
16
|
+
configured_viewports[viewport].merge('name' => viewport)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
begin
|
21
|
+
driver = Selenium::WebDriver.for DiffuxCIUtils.config['driver'].to_sym
|
22
|
+
rescue Selenium::WebDriver::Error::WebDriverError
|
23
|
+
# "unable to obtain stable firefox connection in 60 seconds"
|
24
|
+
#
|
25
|
+
# This seems to happen sporadically for some versions of Firefox, so we want
|
26
|
+
# to retry it in case it will work the second time around.
|
27
|
+
driver = Selenium::WebDriver.for DiffuxCIUtils.config['driver'].to_sym
|
28
|
+
end
|
29
|
+
|
30
|
+
begin
|
31
|
+
driver.manage.timeouts.script_timeout = 3 # move to config?
|
32
|
+
driver.navigate.to DiffuxCIUtils.construct_url('/')
|
33
|
+
|
34
|
+
# Check for errors during startup
|
35
|
+
errors = driver.execute_script('return window.diffux.errors;')
|
36
|
+
unless errors.empty?
|
37
|
+
fail "JavaScript errors found during initialization: \n#{errors.inspect}"
|
38
|
+
end
|
39
|
+
|
40
|
+
while current = driver.execute_script('return window.diffux.next()') do
|
41
|
+
resolve_viewports(current).each do |viewport|
|
42
|
+
# Resize window to the right size before rendering
|
43
|
+
driver.manage.window.resize_to(viewport['width'], viewport['height'])
|
44
|
+
|
45
|
+
# Render the example
|
46
|
+
|
47
|
+
# WebDriver's `execute_async_script` takes a string that is executed in
|
48
|
+
# the context of a function. `execute_async_script` injects a callback
|
49
|
+
# function as this function's argument here. WebDriver will wait until
|
50
|
+
# this callback is called (if it is passed a value it will pass that
|
51
|
+
# through to Rubyland), or until WebDriver's `script_timeout` is reached,
|
52
|
+
# before continuing. Since we don't define the signature of this function,
|
53
|
+
# we can't name the argument so we access it using JavaScript's magic
|
54
|
+
# arguments object and pass it down to `renderCurrent()` which calls it
|
55
|
+
# when it is done--either synchronously if our example doesn't take an
|
56
|
+
# argument, or asynchronously via the Promise and `done` callback if it
|
57
|
+
# does.
|
58
|
+
script = <<-EOS
|
59
|
+
var doneFunc = arguments[arguments.length - 1];
|
60
|
+
window.diffux.renderCurrent(doneFunc);
|
61
|
+
EOS
|
62
|
+
rendered = driver.execute_async_script(script)
|
63
|
+
|
64
|
+
if rendered['error']
|
65
|
+
fail <<-EOS
|
66
|
+
Error while rendering "#{current['name']}" @#{viewport['name']}:
|
67
|
+
#{rendered['error']}
|
68
|
+
Debug by pointing your browser to
|
69
|
+
#{DiffuxCIUtils.construct_url('/', name: current['name'])}
|
70
|
+
EOS
|
71
|
+
end
|
72
|
+
output_file = DiffuxCIUtils.path_to(
|
73
|
+
current['name'], viewport['name'], 'candidate.png')
|
74
|
+
|
75
|
+
# Create the folder structure if it doesn't already exist
|
76
|
+
unless File.directory?(dirname = File.dirname(output_file))
|
77
|
+
FileUtils.mkdir_p(dirname)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Save and crop the screenshot
|
81
|
+
driver.save_screenshot(output_file)
|
82
|
+
cropped = ChunkyPNG::Image.from_file(output_file)
|
83
|
+
cropped.crop!(rendered['left'],
|
84
|
+
rendered['top'],
|
85
|
+
[rendered['width'], 1].max,
|
86
|
+
[rendered['height'], 1].max)
|
87
|
+
cropped.save(output_file)
|
88
|
+
|
89
|
+
print "Checking \"#{current['name']}\" at [#{viewport['name']}]... "
|
90
|
+
|
91
|
+
# Run the diff if needed
|
92
|
+
baseline_file = DiffuxCIUtils.path_to(current['name'], viewport['name'], 'baseline.png')
|
93
|
+
|
94
|
+
if File.exist? baseline_file
|
95
|
+
comparison = Diffux::SnapshotComparer.new(
|
96
|
+
ChunkyPNG::Image.from_file(baseline_file),
|
97
|
+
cropped
|
98
|
+
).compare!
|
99
|
+
|
100
|
+
if img = comparison[:diff_image]
|
101
|
+
diff_output = DiffuxCIUtils.path_to(current['name'], viewport['name'], 'diff.png')
|
102
|
+
img.save(diff_output)
|
103
|
+
puts "#{comparison[:diff_in_percent].round(1)}% (#{diff_output})"
|
104
|
+
else
|
105
|
+
File.delete(output_file)
|
106
|
+
puts 'No diff.'
|
107
|
+
end
|
108
|
+
else
|
109
|
+
File.rename(output_file, baseline_file)
|
110
|
+
puts "First snapshot created (#{baseline_file})"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
ensure
|
115
|
+
driver.quit
|
116
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'yaml'
|
3
|
+
require 'diffux_ci_utils'
|
4
|
+
require 'diffux_ci_action'
|
5
|
+
|
6
|
+
class DiffuxCIServer < Sinatra::Base
|
7
|
+
configure do
|
8
|
+
enable :static
|
9
|
+
set :port, DiffuxCIUtils.config['port']
|
10
|
+
end
|
11
|
+
|
12
|
+
get '/' do
|
13
|
+
@config = DiffuxCIUtils.config
|
14
|
+
erb :index
|
15
|
+
end
|
16
|
+
|
17
|
+
get '/review' do
|
18
|
+
@snapshots = DiffuxCIUtils.current_snapshots
|
19
|
+
erb :review
|
20
|
+
end
|
21
|
+
|
22
|
+
get '/resource' do
|
23
|
+
file = params[:file]
|
24
|
+
if file.start_with? 'http'
|
25
|
+
redirect file
|
26
|
+
else
|
27
|
+
send_file file
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
post '/reject' do
|
32
|
+
DiffuxCIAction.new(params[:name], params[:viewport]).reject
|
33
|
+
redirect back
|
34
|
+
end
|
35
|
+
|
36
|
+
post '/approve' do
|
37
|
+
DiffuxCIAction.new(params[:name], params[:viewport]).approve
|
38
|
+
redirect back
|
39
|
+
end
|
40
|
+
|
41
|
+
run!
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'diffux_ci_utils'
|
2
|
+
require 's3'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
class DiffuxCIUploader
|
6
|
+
BUCKET_NAME = 'diffux_ci-diffs'
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@s3_access_key_id = DiffuxCIUtils.config['s3_access_key_id']
|
10
|
+
@s3_secret_access_key = DiffuxCIUtils.config['s3_secret_access_key']
|
11
|
+
end
|
12
|
+
|
13
|
+
def upload_diffs
|
14
|
+
current_snapshots = DiffuxCIUtils.current_snapshots
|
15
|
+
return [] if current_snapshots[:diffs].empty?
|
16
|
+
|
17
|
+
bucket = find_or_build_bucket
|
18
|
+
|
19
|
+
dir = SecureRandom.uuid
|
20
|
+
|
21
|
+
diff_images = current_snapshots[:diffs].map do |diff|
|
22
|
+
image = bucket.objects.build("#{dir}/#{diff[:name]}_#{diff[:viewport]}.png")
|
23
|
+
image.content = open(diff[:file])
|
24
|
+
image.content_type = 'image/png'
|
25
|
+
image.save
|
26
|
+
diff[:url] = image.url
|
27
|
+
diff
|
28
|
+
end
|
29
|
+
|
30
|
+
html = bucket.objects.build("#{dir}/index.html")
|
31
|
+
html.content =
|
32
|
+
ERB.new(
|
33
|
+
File.read(File.expand_path(
|
34
|
+
File.join(File.dirname(__FILE__), 'diffux_ci-diffs.html.erb')))
|
35
|
+
).result(binding)
|
36
|
+
html.content_type = 'text/html'
|
37
|
+
html.save
|
38
|
+
html.url
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def find_or_build_bucket
|
44
|
+
service = S3::Service.new(access_key_id: @s3_access_key_id,
|
45
|
+
secret_access_key: @s3_secret_access_key)
|
46
|
+
bucket = service.buckets.find(BUCKET_NAME)
|
47
|
+
|
48
|
+
if bucket.nil?
|
49
|
+
bucket = service.buckets.build(BUCKET_NAME)
|
50
|
+
bucket.save(location: :us)
|
51
|
+
end
|
52
|
+
|
53
|
+
bucket
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
class DiffuxCIUtils
|
5
|
+
def self.config
|
6
|
+
@@config ||= {
|
7
|
+
'snapshots_folder' => './snapshots',
|
8
|
+
'source_files' => [],
|
9
|
+
'stylesheets' => [],
|
10
|
+
'port' => 4567,
|
11
|
+
'driver' => :firefox,
|
12
|
+
'viewports' => {
|
13
|
+
'large' => {
|
14
|
+
'width' => 1024,
|
15
|
+
'height' => 768
|
16
|
+
},
|
17
|
+
'medium' => {
|
18
|
+
'width' => 640,
|
19
|
+
'height' => 888
|
20
|
+
},
|
21
|
+
'small' => {
|
22
|
+
'width' => 320,
|
23
|
+
'height' => 444
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}.merge(YAML.load(ERB.new(File.read(
|
27
|
+
ENV['DIFFUX_CI_CONFIG_FILE'] || '.diffux_ci.yaml')).result))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.normalize_name(name)
|
31
|
+
name.gsub(/[^a-zA-Z0-9\-_]/, '_')
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.path_to(name, viewport_name, file_name)
|
35
|
+
File.join(
|
36
|
+
config['snapshots_folder'],
|
37
|
+
normalize_name(name),
|
38
|
+
"@#{viewport_name}",
|
39
|
+
file_name
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.construct_url(absolute_path, params = {})
|
44
|
+
params_str = params.map do |key, value|
|
45
|
+
"#{key}=#{URI.escape(value)}"
|
46
|
+
end.join('&')
|
47
|
+
unless params_str.empty?
|
48
|
+
params_str = "?#{params_str}"
|
49
|
+
end
|
50
|
+
|
51
|
+
return "http://localhost:#{config['port']}#{absolute_path}#{params_str}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.current_snapshots
|
55
|
+
prepare_file = lambda do |file|
|
56
|
+
viewport_dir = File.expand_path('..', file)
|
57
|
+
name_dir = File.expand_path('..', viewport_dir)
|
58
|
+
{
|
59
|
+
name: File.basename(name_dir),
|
60
|
+
viewport: File.basename(viewport_dir).sub('@', ''),
|
61
|
+
file: file,
|
62
|
+
}
|
63
|
+
end
|
64
|
+
diff_files = Dir.glob("#{DiffuxCIUtils.config['snapshots_folder']}/**/diff.png")
|
65
|
+
baselines = Dir.glob("#{DiffuxCIUtils.config['snapshots_folder']}/**/baseline.png")
|
66
|
+
{
|
67
|
+
diffs: diff_files.map(&prepare_file),
|
68
|
+
baselines: baselines.map(&prepare_file)
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
window.diffux = {
|
2
|
+
defined: [],
|
3
|
+
currentIndex: 0,
|
4
|
+
currentExample: undefined,
|
5
|
+
currentRenderedElement: undefined,
|
6
|
+
errors: [],
|
7
|
+
|
8
|
+
define: function(name, func, options) {
|
9
|
+
this.defined.push({
|
10
|
+
name: name,
|
11
|
+
func: func,
|
12
|
+
options: options || {}
|
13
|
+
});
|
14
|
+
},
|
15
|
+
|
16
|
+
fdefine: function() {
|
17
|
+
this.defined = []; // clear out all previously added examples
|
18
|
+
this.define.apply(this, arguments); // add the example
|
19
|
+
this.define = function() {}; // make `define` a no-op from now on
|
20
|
+
},
|
21
|
+
|
22
|
+
next: function() {
|
23
|
+
if (this.currentRenderedElement) {
|
24
|
+
if (window.React) {
|
25
|
+
window.React.unmountComponentAtNode(document.body.lastChild);
|
26
|
+
} else {
|
27
|
+
this.currentRenderedElement.parentNode.removeChild(this.currentRenderedElement);
|
28
|
+
}
|
29
|
+
}
|
30
|
+
this.currentExample = this.defined[this.currentIndex];
|
31
|
+
if (!this.currentExample) {
|
32
|
+
return;
|
33
|
+
}
|
34
|
+
this.currentIndex++;
|
35
|
+
return this.currentExample;
|
36
|
+
},
|
37
|
+
|
38
|
+
setCurrent: function(exampleName) {
|
39
|
+
this.defined.forEach(function(example, index) {
|
40
|
+
if (example.name === exampleName) {
|
41
|
+
this.currentExample = example;
|
42
|
+
}
|
43
|
+
}.bind(this));
|
44
|
+
if (!this.currentExample) {
|
45
|
+
throw 'No example found with name "' + exampleName + '"';
|
46
|
+
}
|
47
|
+
},
|
48
|
+
|
49
|
+
clearVisibleElements: function() {
|
50
|
+
var allElements = Array.prototype.slice.call(document.querySelectorAll('body > *'));
|
51
|
+
allElements.forEach(function(element) {
|
52
|
+
var style = window.getComputedStyle(element);
|
53
|
+
if (style.display !== 'none') {
|
54
|
+
element.parentNode.removeChild(element);
|
55
|
+
}
|
56
|
+
});
|
57
|
+
},
|
58
|
+
|
59
|
+
handleError: function(error) {
|
60
|
+
console.error(error);
|
61
|
+
return {
|
62
|
+
name: this.currentExample.name,
|
63
|
+
error: error.message
|
64
|
+
};
|
65
|
+
},
|
66
|
+
|
67
|
+
/**
|
68
|
+
* @param {Function} func The diffux.describe function from the current
|
69
|
+
* example being rendered. This function takes a callback as an argument
|
70
|
+
* that is called when it is done.
|
71
|
+
* @return {Promise}
|
72
|
+
*/
|
73
|
+
tryAsync: function(func) {
|
74
|
+
return new Promise(function(resolve, reject) {
|
75
|
+
// Saftey valve: if the function does not finish after 3s, then something
|
76
|
+
// went haywire and we need to move on.
|
77
|
+
var timeout = setTimeout(function() {
|
78
|
+
reject(new Error('Async callback was not invoked within timeout.'));
|
79
|
+
}, 3000);
|
80
|
+
|
81
|
+
// This function is called by the example when it is done executing.
|
82
|
+
var doneCallback = function(elem) {
|
83
|
+
clearTimeout(timeout);
|
84
|
+
|
85
|
+
if (!arguments.length) {
|
86
|
+
return reject(new Error(
|
87
|
+
'The async done callback expects the rendered element as an ' +
|
88
|
+
'argument, but there were no arguments.'
|
89
|
+
));
|
90
|
+
}
|
91
|
+
|
92
|
+
resolve(elem);
|
93
|
+
};
|
94
|
+
|
95
|
+
func(doneCallback);
|
96
|
+
});
|
97
|
+
},
|
98
|
+
|
99
|
+
/**
|
100
|
+
* @param {Function} doneFunc injected by driver.execute_async_script in
|
101
|
+
* diffux_ci_runner.rb
|
102
|
+
*/
|
103
|
+
renderCurrent: function(doneFunc) {
|
104
|
+
try {
|
105
|
+
this.clearVisibleElements();
|
106
|
+
|
107
|
+
var func = this.currentExample.func;
|
108
|
+
if (func.length) {
|
109
|
+
// The function takes an argument, which is a callback that is called
|
110
|
+
// once it is done executing. This can be used to write functions that
|
111
|
+
// have asynchronous code in them.
|
112
|
+
this.tryAsync(func).then(function(elem) {
|
113
|
+
doneFunc(this.processElem(elem));
|
114
|
+
}.bind(this)).catch(function(error) {
|
115
|
+
doneFunc(this.handleError(error));
|
116
|
+
}.bind(this));
|
117
|
+
} else {
|
118
|
+
// The function does not take an argument, so we can run it
|
119
|
+
// synchronously.
|
120
|
+
var elem = func();
|
121
|
+
doneFunc(this.processElem(elem));
|
122
|
+
}
|
123
|
+
} catch (error) {
|
124
|
+
doneFunc(this.handleError(error));
|
125
|
+
}
|
126
|
+
},
|
127
|
+
|
128
|
+
processElem: function(elem) {
|
129
|
+
try {
|
130
|
+
// TODO: elem.getDOMNode is deprecated in React, so we need to convert
|
131
|
+
// this to React.findDOMNode(elem) at some point, or push this requirement
|
132
|
+
// into the examples.
|
133
|
+
if (elem.getDOMNode) {
|
134
|
+
// Soft-dependency to React here. If the thing returned has a
|
135
|
+
// `getDOMNode` method, call it to get the real DOM node.
|
136
|
+
elem = elem.getDOMNode();
|
137
|
+
}
|
138
|
+
|
139
|
+
this.currentRenderedElement = elem;
|
140
|
+
|
141
|
+
var width = elem.offsetWidth;
|
142
|
+
var height = elem.offsetHeight;
|
143
|
+
var top = elem.offsetTop;
|
144
|
+
var left = elem.offsetLeft;
|
145
|
+
|
146
|
+
if (this.currentExample.options.snapshotEntireScreen) {
|
147
|
+
width = window.innerWidth;
|
148
|
+
height = window.innerHeight;
|
149
|
+
top = 0;
|
150
|
+
left = 0;
|
151
|
+
}
|
152
|
+
return {
|
153
|
+
name: this.currentExample.name,
|
154
|
+
width: width,
|
155
|
+
height: height,
|
156
|
+
top: top,
|
157
|
+
left: left
|
158
|
+
};
|
159
|
+
} catch (error) {
|
160
|
+
return this.handleError(error);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
};
|
164
|
+
|
165
|
+
window.addEventListener('load', function() {
|
166
|
+
var matches = window.location.search.match(/name=([^&]*)/);
|
167
|
+
if (!matches) {
|
168
|
+
return;
|
169
|
+
}
|
170
|
+
var example = decodeURIComponent(matches[1]);
|
171
|
+
window.diffux.setCurrent(example);
|
172
|
+
window.diffux.renderCurrent();
|
173
|
+
});
|
174
|
+
|
175
|
+
// We need to redefine a few global functions that halt execution. Without this,
|
176
|
+
// there's a chance that the Ruby code can't communicate with the browser.
|
177
|
+
window.alert = function(message) {
|
178
|
+
console.log('`window.alert` called', message);
|
179
|
+
};
|
180
|
+
window.confirm = function(message) {
|
181
|
+
console.log('`window.confirm` called', message);
|
182
|
+
return true;
|
183
|
+
};
|
184
|
+
window.prompt = function(message, value) {
|
185
|
+
console.log('`window.prompt` called', message, value);
|
186
|
+
return null;
|
187
|
+
};
|
188
|
+
|
189
|
+
window.onerror = function(message, url, lineNumber) {
|
190
|
+
window.diffux.errors.push({ message: message, url: url, lineNumber: lineNumber });
|
191
|
+
}
|
data/lib/views/index.erb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<style type="text/css">
|
5
|
+
* {
|
6
|
+
-webkit-transition: none !important;
|
7
|
+
-moz-transition: none !important;
|
8
|
+
transition: none !important;
|
9
|
+
-webkit-animation-duration: 0 !important;
|
10
|
+
-moz-animation-duration: 0s !important;
|
11
|
+
animation-duration: 0s !important;
|
12
|
+
}
|
13
|
+
</style>
|
14
|
+
|
15
|
+
<% @config['stylesheets'].each do |stylesheet| %>
|
16
|
+
<link rel="stylesheet" type="text/css"
|
17
|
+
href="/resource?file=<%= ERB::Util.url_encode(stylesheet) %>">
|
18
|
+
<% end %>
|
19
|
+
</head>
|
20
|
+
<body style="background-color: #fff; margin: 0; pointer-events: none;">
|
21
|
+
<script src="/diffux_ci-runner.js"></script>
|
22
|
+
<% @config['source_files'].each do |source_file| %>
|
23
|
+
<script src="/resource?file=<%= ERB::Util.url_encode(source_file) %>"></script>
|
24
|
+
<% end %>
|
25
|
+
</body>
|
26
|
+
</html>
|
@@ -0,0 +1,44 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<style type="text/css">
|
5
|
+
body {
|
6
|
+
background-color: #f0f0f0;
|
7
|
+
font-family: helvetica, arial;
|
8
|
+
}
|
9
|
+
form {
|
10
|
+
display: inline-block;
|
11
|
+
}
|
12
|
+
</style>
|
13
|
+
</head>
|
14
|
+
<body>
|
15
|
+
<h1>Diffux-CI Review Tool</h1>
|
16
|
+
<h2>DIFFS</h2>
|
17
|
+
<% @snapshots[:diffs].each do |diff| %>
|
18
|
+
<h3>
|
19
|
+
<%= diff[:name] %> @ <%= diff[:viewport] %>
|
20
|
+
</h3>
|
21
|
+
<p><img src="/resource?file=<%= ERB::Util.url_encode(diff[:file]) %>"></p>
|
22
|
+
<form style="display: inline-block"
|
23
|
+
action="/approve?name=<%= diff[:name] %>&viewport=<%= diff[:viewport] %>"
|
24
|
+
method="POST">
|
25
|
+
<button type="submit">Approve</button>
|
26
|
+
</form>
|
27
|
+
<form style="display: inline-block"
|
28
|
+
action="/reject?name=<%= diff[:name] %>&viewport=<%= diff[:viewport] %>"
|
29
|
+
method="POST">
|
30
|
+
<button type="submit">Reject</button>
|
31
|
+
</form>
|
32
|
+
<% end %>
|
33
|
+
|
34
|
+
<hr>
|
35
|
+
|
36
|
+
<h2>BASELINES</h2>
|
37
|
+
<% @snapshots[:baselines].each do |baseline| %>
|
38
|
+
<h3>
|
39
|
+
<%= baseline[:name] %> @ <%= baseline[:viewport] %>
|
40
|
+
</h3>
|
41
|
+
<p><img src="/resource?file=<%= ERB::Util.url_encode(baseline[:file]) %>"></p>
|
42
|
+
<% end %>
|
43
|
+
</body>
|
44
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: diffux_ci
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henric Trotzig
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: diffux-core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.0'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.0.2
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.0'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.0.2
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: chunky_png
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.3.4
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '1.3'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 1.3.4
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: selenium-webdriver
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '2.44'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 2.44.0
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '2.44'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 2.44.0
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: thin
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - "~>"
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '1.6'
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.6.3
|
83
|
+
type: :runtime
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.6'
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 1.6.3
|
93
|
+
- !ruby/object:Gem::Dependency
|
94
|
+
name: sinatra
|
95
|
+
requirement: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - "~>"
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '1.4'
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 1.4.5
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '1.4'
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 1.4.5
|
113
|
+
- !ruby/object:Gem::Dependency
|
114
|
+
name: s3
|
115
|
+
requirement: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - "~>"
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0.3'
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 0.3.22
|
123
|
+
type: :runtime
|
124
|
+
prerelease: false
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
126
|
+
requirements:
|
127
|
+
- - "~>"
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0.3'
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: 0.3.22
|
133
|
+
description: Diffux-CI, a perceptual diff tool for JS components
|
134
|
+
email: henric.trotzig@gmail.com
|
135
|
+
executables:
|
136
|
+
- diffux_ci
|
137
|
+
extensions: []
|
138
|
+
extra_rdoc_files: []
|
139
|
+
files:
|
140
|
+
- bin/diffux_ci
|
141
|
+
- lib/diffux_ci-diffs.html.erb
|
142
|
+
- lib/diffux_ci_action.rb
|
143
|
+
- lib/diffux_ci_runner.rb
|
144
|
+
- lib/diffux_ci_server.rb
|
145
|
+
- lib/diffux_ci_uploader.rb
|
146
|
+
- lib/diffux_ci_utils.rb
|
147
|
+
- lib/public/diffux_ci-runner.js
|
148
|
+
- lib/views/index.erb
|
149
|
+
- lib/views/review.erb
|
150
|
+
homepage: http://rubygems.org/gems/diffux_ci
|
151
|
+
licenses:
|
152
|
+
- MIT
|
153
|
+
metadata: {}
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
requirements:
|
165
|
+
- - ">="
|
166
|
+
- !ruby/object:Gem::Version
|
167
|
+
version: '0'
|
168
|
+
requirements: []
|
169
|
+
rubyforge_project:
|
170
|
+
rubygems_version: 2.4.5
|
171
|
+
signing_key:
|
172
|
+
specification_version: 4
|
173
|
+
summary: Diffux-CI
|
174
|
+
test_files: []
|
175
|
+
has_rdoc:
|