percy-capybara 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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