percy-capybara 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 19a9bfb3b76169ea2b1ba301f69e6466d2da4cef
4
- data.tar.gz: 1f1963b73fa8ce3df14d48c34003cc631b5aa4a0
3
+ metadata.gz: 5897b9782db61c4a242df39bcb2f57124260d7c4
4
+ data.tar.gz: e38619b2dcc0b856fcc21963180aa187838b531a
5
5
  SHA512:
6
- metadata.gz: cccbace3dee7650f246cd96736e620719428ba22eaa88cec41f7aafa3564901d6af0271e77247a3a2b4f8f2758efb2e570bf33b9ea6f409138bb048af8e967b9
7
- data.tar.gz: 99c6891ee227853322830ebff8648df6dbf7bc5fc11488151ed43c9e6a9065948c16faee2e82c8e9ad47d7f87863801d5a594c547e12ac120fba96cd5b4d4924
6
+ metadata.gz: cb70c98eb8a3fdbaf66e75762cbbd6b5eef5e1e9eda8a05fd2bfc77e49cd76cfe20e33b478e9ca8c588ddc8fb4fefe5aa34f3513d58928cc0328351a53290513
7
+ data.tar.gz: 520bf4dadc5e701b0736a1b305b872c2766446995c046b36c2b58ec907b2000d2d90b8977ae05caea084d8d25cbfa4ff43c9a1cb27d792b45665828f2ecaf08b
data/.travis.yml CHANGED
@@ -4,4 +4,4 @@ rvm:
4
4
  - 2.1.1
5
5
  - 2.2.2
6
6
  - ruby-head
7
- script: bundle exec rspec
7
+ script: xvfb-run rspec
@@ -1,5 +1,6 @@
1
1
  require 'percy'
2
2
  require 'percy/capybara/version'
3
+ require 'percy/capybara/httpfetcher'
3
4
  require 'percy/capybara/client'
4
5
 
5
6
  module Percy
@@ -9,6 +9,7 @@ module Percy
9
9
 
10
10
  class Error < Exception; end
11
11
  class BuildNotInitializedError < Error; end
12
+ class WebMockBlockingConnectionsError < Error; end
12
13
 
13
14
  attr_reader :client
14
15
 
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'faraday'
2
3
  require 'httpclient'
3
4
  require 'digest'
@@ -6,9 +7,6 @@ module Percy
6
7
  module Capybara
7
8
  class Client
8
9
  module Snapshots
9
- # @private
10
- FETCH_SENTINEL_VALUE = '[[FETCH]]'
11
-
12
10
  # Takes a snapshot of the given page HTML and its assets.
13
11
  #
14
12
  # @param [Capybara::Session] page The Capybara page to snapshot.
@@ -35,6 +33,7 @@ module Percy
35
33
  resources = []
36
34
  resources << _get_root_html_resource(page)
37
35
  resources += _get_css_resources(page)
36
+ resources += _get_image_resources(page)
38
37
  resources.each { |resource| resource_map[resource.sha] = resource }
39
38
  resource_map
40
39
  end
@@ -61,75 +60,140 @@ module Percy
61
60
  # Find all CSS resources.
62
61
  # http://www.quirksmode.org/dom/w3c_css.html#access
63
62
  script = <<-JS
64
- function findStylesRecursively(stylesheet, result_data) {
65
- result_data = result_data || {};
66
- if (stylesheet.href) {
67
- result_data[stylesheet.href] = result_data[stylesheet.href] || '';
63
+ function findStylesRecursively(stylesheet, css_urls) {
64
+ if (stylesheet.href) { // Skip stylesheet without hrefs (inline stylesheets).
65
+ css_urls.push(stylesheet.href);
68
66
 
69
67
  // Remote stylesheet rules cannot be accessed because of the same-origin policy.
70
68
  // Unfortunately, if you touch .cssRules in Selenium, it throws a JavascriptError
71
- // with 'The operation is insecure'. To work around this, skip any remote stylesheets
72
- // and mark them with a sentinel value so we can fetch them later.
69
+ // with 'The operation is insecure'. To work around this, skip reading rules of
70
+ // remote stylesheets but still include them for fetching.
71
+ //
72
+ // TODO: If a remote stylesheet has an @import, it will be missing because we don't
73
+ // notice it here. We could potentially recursively fetch remote imports in
74
+ // ruby-land below.
73
75
  var parser = document.createElement('a');
74
76
  parser.href = stylesheet.href;
75
77
  if (parser.host != window.location.host) {
76
- result_data[stylesheet.href] = '#{FETCH_SENTINEL_VALUE}'; // Must be a string.
77
78
  return;
78
79
  }
79
80
  }
80
-
81
81
  for (var i = 0; i < stylesheet.cssRules.length; i++) {
82
82
  var rule = stylesheet.cssRules[i];
83
- // Skip stylesheet without hrefs (inline stylesheets).
84
- // These will be present in the HTML snapshot.
85
- if (stylesheet.href) {
86
- // Append current rule text.
87
- result_data[stylesheet.href] += rule.cssText + '\\n';
88
- }
89
-
90
- // Handle recursive @imports.
83
+ // Depth-first search, handle recursive @imports.
91
84
  if (rule.styleSheet) {
92
- findStylesRecursively(rule.styleSheet, result_data);
85
+ findStylesRecursively(rule.styleSheet, css_urls);
93
86
  }
94
87
  }
95
88
  }
96
89
 
97
- var percy_resources = {};
90
+ var css_urls = [];
98
91
  for (var i = 0; i < document.styleSheets.length; i++) {
99
- findStylesRecursively(document.styleSheets[i], percy_resources);
92
+ findStylesRecursively(document.styleSheets[i], css_urls);
100
93
  }
101
- return percy_resources;
94
+ return css_urls;
102
95
  JS
96
+ resource_urls = _evaluate_script(page, script)
97
+
98
+ resource_urls.each do |resource_url|
99
+ response = _fetch_resource_url(resource_url)
100
+ next if !response
101
+ sha = Digest::SHA256.hexdigest(response.body)
102
+ resources << Percy::Client::Resource.new(
103
+ sha, resource_url, mimetype: 'text/css', content: response.body)
104
+ end
105
+ resources
106
+ end
107
+ private :_get_css_resources
108
+
109
+ # @private
110
+ def _get_image_resources(page)
111
+ resources = []
112
+ image_urls = Set.new
113
+
114
+ # Find all image tags on the page.
115
+ page.all('img').each do |image_element|
116
+ srcs = []
117
+ srcs << image_element[:src] if !image_element[:src].nil?
103
118
 
104
- # Returned datastructure: {"<absolute URL>" => "<CSS text>", ...}
105
- resource_data = _evaluate_script(page, script)
119
+ srcset_raw_urls = image_element[:srcset] || ''
120
+ temp_urls = srcset_raw_urls.split(',')
121
+ temp_urls.each do |temp_url|
122
+ srcs << temp_url.split(' ').first
123
+ end
106
124
 
107
- resource_data.each do |resource_url, css|
108
- if css == FETCH_SENTINEL_VALUE
109
- # Handle sentinel value that indicates a remote CSS resource that must be fetched.
110
- response = _fetch_resource_url(resource_url)
111
- next if !response
112
- css = response.body
125
+ srcs.each do |src|
126
+ # Skip data URIs.
127
+ next if src.match(/\Adata:/)
128
+ image_urls << src
113
129
  end
130
+ end
131
+
132
+ # Find all CSS-loaded images which set a background-image.
133
+ script = <<-JS
134
+ var raw_image_urls = [];
114
135
 
115
- sha = Digest::SHA256.hexdigest(css)
136
+ var tags = document.getElementsByTagName('*');
137
+ var el;
138
+ var rawValue;
139
+
140
+ for (var i = 0; i < tags.length; i++) {
141
+ el = tags[i];
142
+ if (el.currentStyle) {
143
+ rawValue = el.currentStyle['backgroundImage'];
144
+ } else if (window.getComputedStyle) {
145
+ rawValue = window.getComputedStyle(el).getPropertyValue('background-image');
146
+ }
147
+ if (!rawValue || rawValue === "none") {
148
+ continue;
149
+ } else {
150
+ raw_image_urls.push(rawValue);
151
+ }
152
+ }
153
+ return raw_image_urls;
154
+ JS
155
+ raw_image_urls = _evaluate_script(page, script)
156
+ raw_image_urls.each do |raw_image_url|
157
+ temp_urls = raw_image_url.scan(/url\(["']?(.*?)["']?\)/)
158
+ # background-image can accept multiple url()s, so temp_urls is an array of URLs.
159
+ temp_urls.each do |temp_url|
160
+ # Skip data URIs.
161
+ next if temp_url[0].match(/\Adata:/)
162
+ image_urls << temp_url[0]
163
+ end
164
+ end
165
+
166
+ image_urls.each do |image_url|
167
+ # Make the resource URL absolute to the current page. If it is already absolute, this
168
+ # will have no effect.
169
+ resource_url = URI.join(page.current_url, image_url).to_s
170
+
171
+ # Fetch the images.
172
+ # TODO(fotinakis): this can be pretty inefficient for image-heavy pages because the
173
+ # browser has already loaded them once and this fetch cannot easily leverage the
174
+ # browser's cache. However, often these images are probably local resources served by a
175
+ # development server, so it may not be so bad. Re-evaluate if this becomes an issue.
176
+ response = _fetch_resource_url(resource_url)
177
+ next if !response
178
+
179
+ sha = Digest::SHA256.hexdigest(response.body)
116
180
  resources << Percy::Client::Resource.new(
117
- sha, resource_url, mimetype: 'text/css', content: css)
181
+ sha, resource_url, mimetype: response.content_type, content: response.body)
118
182
  end
119
183
  resources
120
184
  end
121
- private :_get_css_resources
185
+ private :_get_image_resources
122
186
 
123
187
  # @private
124
188
  def _fetch_resource_url(url)
125
- response = Faraday.get(url)
126
- content = response.body
127
- if response.status != 200
189
+ response = Percy::Capybara::HttpFetcher.fetch(url)
190
+ if !response
128
191
  STDERR.puts "[percy] Warning: failed to fetch page resource, this might be a bug: #{url}"
129
192
  return nil
130
193
  end
131
194
  response
132
195
  end
196
+ private :_fetch_resource_url
133
197
 
134
198
  # @private
135
199
  def _evaluate_script(page, script)
@@ -0,0 +1,34 @@
1
+ require 'tempfile'
2
+
3
+ module Percy
4
+ module Capybara
5
+ module HttpFetcher
6
+ class Response < Struct.new(:body, :content_type); end
7
+
8
+ def self.fetch(url)
9
+ tempfile = Tempfile.new('percy-capybara-fetch')
10
+ temppath = tempfile.path
11
+
12
+ # Close and delete the tempfile, we just wanted the name. Also, we use the existence of the
13
+ # file as a signal below.
14
+ tempfile.close
15
+ tempfile.unlink
16
+
17
+ # Use curl as a magical subprocess weapon which escapes this Ruby sandbox and is not
18
+ # influenced by any HTTP middleware/restrictions. This helps us avoid causing lots of
19
+ # problems for people using gems like VCR/WebMock. We also disable certificate checking
20
+ # because, as odd as that is, it's the default state for Selenium Firefox and others.
21
+ output = `curl --insecure -v -o #{temppath} "#{url}" 2>&1`
22
+ content_type = output.match(/< Content-Type:(.*)/i)
23
+ content_type = content_type[1].strip if content_type
24
+
25
+ if File.exist?(temppath)
26
+ response = Percy::Capybara::HttpFetcher::Response.new(File.read(temppath), content_type)
27
+ # We've broken the tempfile so it won't get deleted when garbage collected. Delete!
28
+ File.delete(temppath)
29
+ response
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  module Percy
2
2
  module Capybara
3
- VERSION = '0.1.0'
3
+ VERSION = '0.1.1'
4
4
  end
5
5
  end
@@ -1,12 +1,12 @@
1
1
  require 'json'
2
2
  require 'digest'
3
3
 
4
- RSpec.describe Percy::Capybara::Client::Snapshots do
4
+ RSpec.describe Percy::Capybara::Client::Snapshots, type: :feature do
5
5
  let(:capybara_client) { Percy::Capybara::Client.new }
6
6
 
7
7
  # Start a temp webserver that serves the testdata directory.
8
8
  # You can test this server manually by running:
9
- # ruby -run -e httpd spec/lib/percy/capybara/testdata -p 9090
9
+ # ruby -run -e httpd spec/lib/percy/capybara/client/testdata/ -p 9090
10
10
  before(:all) do
11
11
  port = get_random_open_port
12
12
  Capybara.app_host = "http://localhost:#{port}"
@@ -19,10 +19,25 @@ RSpec.describe Percy::Capybara::Client::Snapshots do
19
19
  ].flatten)
20
20
 
21
21
  # Block until the server is up.
22
+ WebMock.disable_net_connect!(allow_localhost: true)
22
23
  verify_server_up(Capybara.app_host)
23
24
  end
24
25
  after(:all) { Process.kill('INT', @process.pid) }
25
26
 
27
+ before(:each) do
28
+ # Special setting for capybara-webkit. If clients are using capybara-webkit they would
29
+ # also have to have this setting enabled since apparently all resources are blocked by default.
30
+ page.driver.respond_to?(:allow_url) && page.driver.allow_url('*')
31
+ end
32
+
33
+ def find_resource(resources, regex)
34
+ begin
35
+ resources.select { |resource| resource.resource_url.match(regex) }.fetch(0)
36
+ rescue IndexError
37
+ raise "Missing expected image with resource_url that matches: #{regex}"
38
+ end
39
+ end
40
+
26
41
  describe '#_get_root_html_resource', type: :feature, js: true do
27
42
  it 'includes the root DOM HTML' do
28
43
  visit '/'
@@ -37,67 +52,141 @@ RSpec.describe Percy::Capybara::Client::Snapshots do
37
52
  end
38
53
  describe '#_get_css_resources', type: :feature, js: true do
39
54
  it 'includes all linked and imported stylesheets' do
40
- # For capybara-webkit.
41
- page.driver.respond_to?(:allow_url) && page.driver.allow_url('maxcdn.bootstrapcdn.com')
42
-
43
55
  visit '/test-css.html'
44
56
  resources = capybara_client.send(:_get_css_resources, page)
45
57
 
46
- expect(resources.length).to eq(7)
47
- expect(resources.collect(&:mimetype).uniq).to eq(['text/css'])
48
-
49
- resource = resources.select do |resource|
50
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/base\.css/)
51
- end.fetch(0)
52
- expect(resource.is_root).to be_falsey
58
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/base\.css/)
53
59
 
54
60
  expect(resource.content).to include('.colored-by-base { color: red; }')
55
61
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
56
62
 
57
- resource = resources.select do |resource|
58
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/simple-imports\.css/)
59
- end.fetch(0)
60
- expect(resource.is_root).to be_falsey
61
- expect(resource.content).to include('@import url("imports.css")')
63
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/simple-imports\.css/)
64
+ expect(resource.content).to include("@import url('imports.css');")
62
65
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
63
66
 
64
- resource = resources.select do |resource|
65
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/imports\.css/)
66
- end.fetch(0)
67
- expect(resource.is_root).to be_falsey
67
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/imports\.css/)
68
68
  expect(resource.content).to include('.colored-by-imports { color: red; }')
69
69
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
70
70
 
71
- resource = resources.select do |resource|
72
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level0-imports\.css/)
73
- end.fetch(0)
74
- expect(resource.is_root).to be_falsey
75
- expect(resource.content).to include('@import url("level1-imports.css")')
71
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level0-imports\.css/)
72
+ expect(resource.content).to include("@import url('level1-imports.css')")
76
73
  expect(resource.content).to include('.colored-by-level0-imports { color: red; }')
77
74
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
78
75
 
79
- resource = resources.select do |resource|
80
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level1-imports\.css/)
81
- end.fetch(0)
82
- expect(resource.is_root).to be_falsey
83
- expect(resource.content).to include('@import url("level2-imports.css")')
76
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level1-imports\.css/)
77
+ expect(resource.content).to include("@import url('level2-imports.css')")
84
78
  expect(resource.content).to include('.colored-by-level1-imports { color: red; }')
85
79
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
86
80
 
87
- resource = resources.select do |resource|
88
- resource.resource_url.match(/http:\/\/localhost:\d+\/css\/level2-imports\.css/)
89
- end.fetch(0)
90
- expect(resource.is_root).to be_falsey
81
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level2-imports\.css/)
91
82
  expect(resource.content).to include(".colored-by-level2-imports { color: red; }")
92
83
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
93
84
 
94
- resource = resources.select do |resource|
95
- resource.resource_url == (
96
- 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css')
97
- end.fetch(0)
98
- expect(resource.is_root).to be_falsey
85
+ resource = find_resource(
86
+ resources, /https:\/\/maxcdn.bootstrapcdn.com\/bootstrap\/3.3.4\/css\/bootstrap.min.css/)
99
87
  expect(resource.content).to include('Bootstrap v3.3.4 (http://getbootstrap.com)')
100
88
  expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
89
+
90
+ expect(resources.length).to eq(7)
91
+ expect(resources.collect(&:mimetype).uniq).to eq(['text/css'])
92
+ expect(resources.collect(&:is_root).uniq).to match_array([nil])
93
+ end
94
+ end
95
+ describe '#_get_image_resources', type: :feature, js: true do
96
+ it 'includes all images' do
97
+ visit '/test-images.html'
98
+ resources = capybara_client.send(:_get_image_resources, page)
99
+
100
+ # The order of these is just for convenience, they match the order in test-images.html.
101
+
102
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/img-relative\.png/)
103
+ content = File.read(File.expand_path('../testdata/images/img-relative.png', __FILE__))
104
+ expect(resource.mimetype).to eq('image/png')
105
+ expected_sha = Digest::SHA256.hexdigest(content)
106
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
107
+ expect(resource.sha).to eq(expected_sha)
108
+
109
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/img-relative-to-root\.png/)
110
+ content = File.read(File.expand_path('../testdata/images/img-relative-to-root.png', __FILE__))
111
+ expect(resource.mimetype).to eq('image/png')
112
+ expected_sha = Digest::SHA256.hexdigest(content)
113
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
114
+ expect(resource.sha).to eq(expected_sha)
115
+
116
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/percy\.svg/)
117
+ content = File.read(File.expand_path('../testdata/images/percy.svg', __FILE__))
118
+ # In Ruby 1.9.3 the SVG mimetype is not registered so our mini ruby webserver doesn't serve
119
+ # the correct content type. Allow either to work here so we can test older Rubies fully.
120
+ expect(resource.mimetype).to match(/image\/svg\+xml|application\/octet-stream/)
121
+ expected_sha = Digest::SHA256.hexdigest(content)
122
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
123
+ expect(resource.sha).to eq(expected_sha)
124
+
125
+ resource = find_resource(resources, /http:\/\/i.imgur.com\/Umkjdao.png/)
126
+ content = Faraday.get('http://i.imgur.com/Umkjdao.png').body
127
+ expect(resource.mimetype).to eq('image/png')
128
+ expected_sha = Digest::SHA256.hexdigest(content)
129
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
130
+ expect(resource.sha).to eq(expected_sha)
131
+
132
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-relative\.png/)
133
+ content = File.read(File.expand_path('../testdata/images/bg-relative.png', __FILE__))
134
+ expect(resource.mimetype).to eq('image/png')
135
+ expected_sha = Digest::SHA256.hexdigest(content)
136
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
137
+ expect(resource.sha).to eq(expected_sha)
138
+
139
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-relative-to-root\.png/)
140
+ content = File.read(File.expand_path('../testdata/images/bg-relative-to-root.png', __FILE__))
141
+ expect(resource.mimetype).to eq('image/png')
142
+ expected_sha = Digest::SHA256.hexdigest(content)
143
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
144
+ expect(resource.sha).to eq(expected_sha)
145
+
146
+ resource = find_resource(resources, /http:\/\/i.imgur.com\/5mLoBs1.png/)
147
+ content = Faraday.get('http://i.imgur.com/5mLoBs1.png').body
148
+ expect(resource.mimetype).to eq('image/png')
149
+ expected_sha = Digest::SHA256.hexdigest(content)
150
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
151
+ expect(resource.sha).to eq(expected_sha)
152
+
153
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-stacked\.png/)
154
+ content = File.read(File.expand_path('../testdata/images/bg-stacked.png', __FILE__))
155
+ expect(resource.mimetype).to eq('image/png')
156
+ expected_sha = Digest::SHA256.hexdigest(content)
157
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
158
+ expect(resource.sha).to eq(expected_sha)
159
+
160
+ resource = find_resource(resources, /http:\/\/i.imgur.com\/61AQuplb.jpg/)
161
+ content = Faraday.get('http://i.imgur.com/61AQuplb.jpg').body
162
+ expect(resource.mimetype).to eq('image/jpeg')
163
+ expected_sha = Digest::SHA256.hexdigest(content)
164
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
165
+ expect(resource.sha).to eq(expected_sha)
166
+
167
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-base\.png/)
168
+ content = File.read(File.expand_path('../testdata/images/srcset-base.png', __FILE__))
169
+ expect(resource.mimetype).to eq('image/png')
170
+ expected_sha = Digest::SHA256.hexdigest(content)
171
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
172
+ expect(resource.sha).to eq(expected_sha)
173
+
174
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-first\.png/)
175
+ content = File.read(File.expand_path('../testdata/images/srcset-first.png', __FILE__))
176
+ expect(resource.mimetype).to eq('image/png')
177
+ expected_sha = Digest::SHA256.hexdigest(content)
178
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
179
+ expect(resource.sha).to eq(expected_sha)
180
+
181
+ resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-second\.png/)
182
+ content = File.read(File.expand_path('../testdata/images/srcset-second.png', __FILE__))
183
+ expect(resource.mimetype).to eq('image/png')
184
+ expected_sha = Digest::SHA256.hexdigest(content)
185
+ expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
186
+ expect(resource.sha).to eq(expected_sha)
187
+
188
+ expect(resources.length).to eq(12)
189
+ expect(resources.collect(&:is_root).uniq).to match_array([nil])
101
190
  end
102
191
  end
103
192
  describe '#snapshot', type: :feature, js: true do
@@ -140,6 +229,12 @@ RSpec.describe Percy::Capybara::Client::Snapshots do
140
229
  stub_request(:post, "https://percy.io/api/v1/builds/123/resources/")
141
230
  .with(body: /#{resource.sha}/).to_return(status: 201, body: {success: true}.to_json)
142
231
 
232
+ expect(capybara_client).to receive(:_get_root_html_resource)
233
+ .with(page).once.and_call_original
234
+ expect(capybara_client).to receive(:_get_css_resources)
235
+ .with(page).once.and_call_original
236
+ expect(capybara_client).to receive(:_get_image_resources)
237
+ .with(page).once.and_call_original
143
238
  resource_map = capybara_client.snapshot(page)
144
239
  end
145
240
  end
@@ -0,0 +1,34 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+ viewBox="0 0 170 110" enable-background="new 0 0 170 110" xml:space="preserve">
5
+ <g>
6
+ <g>
7
+ <polyline fill="#1F0924" points="49.2,97.6 9.4,103.4 46,86.8 6.2,81.2 46,75.6 9.4,59 49.2,64.9 18.7,38.6 55.2,55.4 33.4,21.7
8
+ 63.7,48.1 52.2,9.6 73.9,43.4 73.7,3.2 85,41.8 96.2,3.2 96.1,43.4 117.7,9.5 106.3,48.1 136.5,21.6 114.7,55.4 151.2,38.6
9
+ 120.8,64.8 160.6,59 124,75.6 163.8,81.1 124,86.8 160.6,103.3 120.8,97.5 "/>
10
+ <polyline fill="#EDDDC2" points="52.8,90.2 18.5,90.3 51.4,80.7 18.5,71.2 52.8,71.3 23.9,52.8 56.8,62.6 63,55.4 48.7,24.2
11
+ 71.1,50.2 66.1,16.3 80.2,47.5 85,13.5 89.8,47.5 103.9,16.3 98.9,50.2 121.3,24.2 107,55.4 113.2,62.6 146.1,52.8 117.2,71.3
12
+ 151.5,71.2 118.6,80.7 151.5,90.3 117.2,90.2 "/>
13
+ </g>
14
+ <polygon fill="#EDDDC2" points="108.3,45.7 134.4,39.1 127.9,65.2 "/>
15
+ <polygon fill="#1F0924" points="114.2,48.6 128.5,45 124.9,59.3 114.2,59.3 "/>
16
+ <polygon fill="#EDDDC2" points="61.7,45.7 35.6,39.1 42.1,65.2 "/>
17
+ <polygon fill="#1F0924" points="55.8,48.6 41.5,45 45.1,59.3 55.8,59.3 "/>
18
+ <polyline fill="#B157B5" points="38.3,101.1 131.7,101.1 166.8,88.5 131.5,76 155.8,47.6 119,54.5 125.9,17.7 97.5,42 85,6.7
19
+ 72.5,42 44.1,17.7 51,54.5 14.2,47.6 38.5,76 3.2,88.5 "/>
20
+ <circle fill="#EDDDC2" cx="61.4" cy="78.7" r="12.1"/>
21
+ <circle fill="#1F0924" cx="61.4" cy="78.7" r="9.5"/>
22
+ <circle fill="#EDDDC2" cx="58.2" cy="75.5" r="4.2"/>
23
+ <circle fill="#EDDDC2" cx="108.6" cy="78.7" r="12.1"/>
24
+ <circle fill="#1F0924" cx="108.6" cy="78.7" r="9.5"/>
25
+ <circle fill="#EDDDC2" cx="105.3" cy="75.5" r="4.2"/>
26
+ <path fill="#EDDDC2" d="M62.9,97.6c0-12.2,9.9-22.1,22.1-22.1s22.1,9.9,22.1,22.1S62.9,109.8,62.9,97.6z"/>
27
+ <path fill="#1F0924" d="M89.8,96.5c-2,0-3.7-1-4.8-2.5c-1,1.5-2.8,2.5-4.8,2.5c-3.2,0-5.8-2.6-5.8-5.8c0-0.6,0.4-1,1-1s1,0.4,1,1
28
+ c0,2.1,1.7,3.8,3.8,3.8s3.8-1.7,3.8-3.8h2c0,2.1,1.7,3.8,3.8,3.8s3.8-1.7,3.8-3.8c0-0.6,0.4-1,1-1s1,0.4,1,1
29
+ C95.5,93.9,93,96.5,89.8,96.5z"/>
30
+ <path fill="#1F0924" d="M92.8,83.6c0,4.3-3.5,7.8-7.8,7.8s-7.8-3.5-7.8-7.8C77.2,79.3,92.8,79.3,92.8,83.6z"/>
31
+ <path fill="#EDDDC2" d="M47,101.3c0-4,3.2-7.3,7.3-7.3s7.3,3.2,7.3,7.3S47,105.3,47,101.3z"/>
32
+ <path fill="#EDDDC2" d="M108.4,101.3c0-4,3.2-7.3,7.3-7.3c4,0,7.3,3.2,7.3,7.3S108.4,105.3,108.4,101.3z"/>
33
+ </g>
34
+ </svg>
@@ -0,0 +1,45 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <title>Test Percy::Capybara</title>
4
+ <h1>Test images</h1>
5
+
6
+ <h2>img local PNG (relative)</h2>
7
+ <img src="images/img-relative.png">
8
+
9
+ <h2>img local PNG (relative to root)</h2>
10
+ <img src="/images/img-relative-to-root.png">
11
+
12
+ <h2>img PNG (remote)</h2>
13
+ <img src="http://i.imgur.com/Umkjdao.png">
14
+
15
+ <h2>img SVG (relative)</h2>
16
+ <img src="../../../images/percy.svg" width="300">
17
+
18
+ <h2>CSS background-image (relative)</h2>
19
+ <div style="background-image: url(images/bg-relative.png); width: 200px; height: 200px;"></div>
20
+
21
+ <h2>CSS background-image (relative to root)</h2>
22
+ <div style="background-image: url(/images/bg-relative-to-root.png); width: 200px; height: 200px;"></div>
23
+
24
+ <h2>CSS background-image (remote)</h2>
25
+ <div style="background-image: url(http://i.imgur.com/5mLoBs1.png); width: 408px; height: 376px;"></div>
26
+
27
+ <h2>img data-uri (ignored)</h2>
28
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRFVyxjAAAAmU4baAAAACNJREFUeNrswYEAAAAAw6D5U1/hAFUBAAAAAAAAAAAAjwkwACd0AAGLtJfaAAAAAElFTkSuQmCC">
29
+
30
+ <h2>CSS background-image data-uri (ignored)</h2>
31
+ <div style="background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAMAAABHPGVmAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRFVyxjAAAAmU4baAAAACNJREFUeNrswYEAAAAAw6D5U1/hAFUBAAAAAAAAAAAAjwkwACd0AAGLtJfaAAAAAElFTkSuQmCC); width: 100px; height: 100px;"></div>
32
+
33
+ <h2>CSS background stacked (local and remote)</h2>
34
+ <div style="background: url(/images/bg-stacked.png) 200px 200px no-repeat, url('http://i.imgur.com/61AQuplb.jpg'); width: 100px; height: 100px;"></div>
35
+
36
+ <h2>img srcset</h2>
37
+ <img width="200" src="/images/srcset-base.png" srcset="/images/srcset-first.png 200w, /images/srcset-second.png 400w">
38
+
39
+ <h2>Duplicates of some of the above images, to verify they are not duplicated in resources:</h2>
40
+ <img src="images/img-relative.png">
41
+ <img src="/images/img-relative-to-root.png">
42
+ <img src="http://i.imgur.com/Umkjdao.png">
43
+ <div style="background-image: url(/images/bg-relative-to-root.png); width: 200px; height: 200px;"></div>
44
+ <div style="background-image: url(http://i.imgur.com/5mLoBs1.png); width: 408px; height: 376px;"></div>
45
+ </html>
@@ -0,0 +1,15 @@
1
+ RSpec.describe Percy::Capybara::HttpFetcher do
2
+ it 'takes a URL and returns a response' do
3
+ response = Percy::Capybara::HttpFetcher.fetch('http://i.imgur.com/Umkjdao.png')
4
+
5
+ # Slightly magical hash, just a SHA-256 sum of the image above.
6
+ expect(Digest::SHA256.hexdigest(response.body)).to eq(
7
+ '4beb51550bef8e9e30d37ea8c13658e99bb01722062f218185e419af5ad93e13')
8
+ expect(response.content_type).to eq('image/png')
9
+ end
10
+ it 'returns nil if fetch failed' do
11
+ expect(Percy::Capybara::HttpFetcher.fetch('bad-url')).to be_nil
12
+ expect(Percy::Capybara::HttpFetcher.fetch('http://i.imgur.com/fake-image.png')).to be_nil
13
+ end
14
+ end
15
+
@@ -20,7 +20,7 @@ RSpec.describe Percy::Capybara do
20
20
  end
21
21
  end
22
22
  describe '#snapshot' do
23
- it 'delgates to Percy::Capybara::Client' do
23
+ it 'delegates to Percy::Capybara::Client' do
24
24
  capybara_client = Percy::Capybara.capybara_client
25
25
  expect(capybara_client).to receive(:initialize_build).once
26
26
  Percy::Capybara.initialize_build
@@ -30,7 +30,7 @@ RSpec.describe Percy::Capybara do
30
30
  it 'returns silently if no build is initialized' do
31
31
  expect { Percy::Capybara.finalize_build }.to_not raise_error
32
32
  end
33
- it 'delgates to Percy::Capybara::Client' do
33
+ it 'delegates to Percy::Capybara::Client' do
34
34
  capybara_client = Percy::Capybara.capybara_client
35
35
  build_data = {'data' => {'id' => 123}}
36
36
  expect(capybara_client.client).to receive(:create_build).and_return(build_data).once
data/spec/spec_helper.rb CHANGED
@@ -35,7 +35,8 @@ RSpec.configure do |config|
35
35
  # Comment this out to test the default Selenium/Firefox flow:
36
36
  Capybara.javascript_driver = :webkit
37
37
 
38
- config.before(:all) do
39
- WebMock.disable_net_connect!(allow_localhost: true, allow: [/maxcdn.bootstrapcdn.com/])
38
+ config.before(:each) do
39
+ WebMock.disable_net_connect!(allow_localhost: true)
40
40
  end
41
+ config.before(:each, type: :feature) { WebMock.allow_net_connect! }
41
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: percy-capybara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Perceptual Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-20 00:00:00.000000000 Z
11
+ date: 2015-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: percy-client
@@ -156,6 +156,7 @@ files:
156
156
  - lib/percy/capybara/client.rb
157
157
  - lib/percy/capybara/client/builds.rb
158
158
  - lib/percy/capybara/client/snapshots.rb
159
+ - lib/percy/capybara/httpfetcher.rb
159
160
  - lib/percy/capybara/version.rb
160
161
  - percy-capybara.gemspec
161
162
  - spec/lib/percy/capybara/client/builds_spec.rb
@@ -166,8 +167,19 @@ files:
166
167
  - spec/lib/percy/capybara/client/testdata/css/level1-imports.css
167
168
  - spec/lib/percy/capybara/client/testdata/css/level2-imports.css
168
169
  - spec/lib/percy/capybara/client/testdata/css/simple-imports.css
170
+ - spec/lib/percy/capybara/client/testdata/images/bg-relative-to-root.png
171
+ - spec/lib/percy/capybara/client/testdata/images/bg-relative.png
172
+ - spec/lib/percy/capybara/client/testdata/images/bg-stacked.png
173
+ - spec/lib/percy/capybara/client/testdata/images/img-relative-to-root.png
174
+ - spec/lib/percy/capybara/client/testdata/images/img-relative.png
175
+ - spec/lib/percy/capybara/client/testdata/images/percy.svg
176
+ - spec/lib/percy/capybara/client/testdata/images/srcset-base.png
177
+ - spec/lib/percy/capybara/client/testdata/images/srcset-first.png
178
+ - spec/lib/percy/capybara/client/testdata/images/srcset-second.png
169
179
  - spec/lib/percy/capybara/client/testdata/index.html
170
180
  - spec/lib/percy/capybara/client/testdata/test-css.html
181
+ - spec/lib/percy/capybara/client/testdata/test-images.html
182
+ - spec/lib/percy/capybara/httpfetcher_spec.rb
171
183
  - spec/lib/percy/capybara_spec.rb
172
184
  - spec/spec_helper.rb
173
185
  - spec/support/test_helpers.rb
@@ -204,8 +216,19 @@ test_files:
204
216
  - spec/lib/percy/capybara/client/testdata/css/level1-imports.css
205
217
  - spec/lib/percy/capybara/client/testdata/css/level2-imports.css
206
218
  - spec/lib/percy/capybara/client/testdata/css/simple-imports.css
219
+ - spec/lib/percy/capybara/client/testdata/images/bg-relative-to-root.png
220
+ - spec/lib/percy/capybara/client/testdata/images/bg-relative.png
221
+ - spec/lib/percy/capybara/client/testdata/images/bg-stacked.png
222
+ - spec/lib/percy/capybara/client/testdata/images/img-relative-to-root.png
223
+ - spec/lib/percy/capybara/client/testdata/images/img-relative.png
224
+ - spec/lib/percy/capybara/client/testdata/images/percy.svg
225
+ - spec/lib/percy/capybara/client/testdata/images/srcset-base.png
226
+ - spec/lib/percy/capybara/client/testdata/images/srcset-first.png
227
+ - spec/lib/percy/capybara/client/testdata/images/srcset-second.png
207
228
  - spec/lib/percy/capybara/client/testdata/index.html
208
229
  - spec/lib/percy/capybara/client/testdata/test-css.html
230
+ - spec/lib/percy/capybara/client/testdata/test-images.html
231
+ - spec/lib/percy/capybara/httpfetcher_spec.rb
209
232
  - spec/lib/percy/capybara_spec.rb
210
233
  - spec/spec_helper.rb
211
234
  - spec/support/test_helpers.rb