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 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
+ }
@@ -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: