diffux_ci 0.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.
- 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:
|