percy-capybara 0.3.0 → 0.4.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.
@@ -0,0 +1,209 @@
1
+ require 'percy/capybara/loaders/base_loader'
2
+ require 'digest'
3
+ require 'uri'
4
+
5
+ module Percy
6
+ module Capybara
7
+ module Loaders
8
+
9
+ # Resource loader that uses the native Capybara browser interface to discover resources.
10
+ # This loader uses JavaScript to discover page resources, so specs must be tagged with
11
+ # "js: true" because the default Rack::Test driver does not support executing JavaScript.
12
+ class NativeLoader < BaseLoader
13
+ PATH_REGEX = /\A\/[^\\s\"']*/
14
+ DATA_URL_REGEX = /\Adata:/
15
+ LOCAL_HOSTNAMES = [
16
+ 'localhost',
17
+ '127.0.0.1',
18
+ '0.0.0.0',
19
+ ].freeze
20
+
21
+ def snapshot_resources
22
+ resources = []
23
+ resources << root_html_resource
24
+ resources += _get_css_resources
25
+ resources += _get_image_resources
26
+ resources
27
+ end
28
+
29
+ def build_resources
30
+ []
31
+ end
32
+
33
+ # @private
34
+ def _get_css_resources
35
+ resources = []
36
+ # Find all CSS resources.
37
+ # http://www.quirksmode.org/dom/w3c_css.html#access
38
+ script = <<-JS
39
+ function findStylesRecursively(stylesheet, css_urls) {
40
+ if (stylesheet.href) { // Skip stylesheet without hrefs (inline stylesheets).
41
+ css_urls.push(stylesheet.href);
42
+
43
+ // Remote stylesheet rules cannot be accessed because of the same-origin policy.
44
+ // Unfortunately, if you touch .cssRules in Selenium, it throws a JavascriptError
45
+ // with 'The operation is insecure'. To work around this, skip reading rules of
46
+ // remote stylesheets but still include them for fetching.
47
+ //
48
+ // TODO: If a remote stylesheet has an @import, it will be missing because we don't
49
+ // notice it here. We could potentially recursively fetch remote imports in
50
+ // ruby-land below.
51
+ var parser = document.createElement('a');
52
+ parser.href = stylesheet.href;
53
+ if (parser.host != window.location.host) {
54
+ return;
55
+ }
56
+ }
57
+ for (var i = 0; i < stylesheet.cssRules.length; i++) {
58
+ var rule = stylesheet.cssRules[i];
59
+ // Depth-first search, handle recursive @imports.
60
+ if (rule.styleSheet) {
61
+ findStylesRecursively(rule.styleSheet, css_urls);
62
+ }
63
+ }
64
+ }
65
+
66
+ var css_urls = [];
67
+ for (var i = 0; i < document.styleSheets.length; i++) {
68
+ findStylesRecursively(document.styleSheets[i], css_urls);
69
+ }
70
+ return css_urls;
71
+ JS
72
+ resource_urls = _evaluate_script(page, script)
73
+
74
+ resource_urls.each do |url|
75
+ next if !_should_include_url?(url)
76
+ response = _fetch_resource_url(url)
77
+ next if !response
78
+ sha = Digest::SHA256.hexdigest(response.body)
79
+ resources << Percy::Client::Resource.new(
80
+ url, mimetype: 'text/css', content: response.body)
81
+ end
82
+ resources
83
+ end
84
+ private :_get_css_resources
85
+
86
+ # @private
87
+ def _get_image_resources
88
+ resources = []
89
+ image_urls = Set.new
90
+
91
+ # Find all image tags on the page.
92
+ page.all('img').each do |image_element|
93
+ srcs = []
94
+ srcs << image_element[:src] if !image_element[:src].nil?
95
+
96
+ srcset_raw_urls = image_element[:srcset] || ''
97
+ temp_urls = srcset_raw_urls.split(',')
98
+ temp_urls.each do |temp_url|
99
+ srcs << temp_url.split(' ').first
100
+ end
101
+
102
+ srcs.each do |url|
103
+ image_urls << url
104
+ end
105
+ end
106
+
107
+ # Find all CSS-loaded images which set a background-image.
108
+ script = <<-JS
109
+ var raw_image_urls = [];
110
+
111
+ var tags = document.getElementsByTagName('*');
112
+ var el;
113
+ var rawValue;
114
+
115
+ for (var i = 0; i < tags.length; i++) {
116
+ el = tags[i];
117
+ if (el.currentStyle) {
118
+ rawValue = el.currentStyle['backgroundImage'];
119
+ } else if (window.getComputedStyle) {
120
+ rawValue = window.getComputedStyle(el).getPropertyValue('background-image');
121
+ }
122
+ if (!rawValue || rawValue === "none") {
123
+ continue;
124
+ } else {
125
+ raw_image_urls.push(rawValue);
126
+ }
127
+ }
128
+ return raw_image_urls;
129
+ JS
130
+ raw_image_urls = _evaluate_script(page, script)
131
+ raw_image_urls.each do |raw_image_url|
132
+ temp_urls = raw_image_url.scan(/url\(["']?(.*?)["']?\)/)
133
+ # background-image can accept multiple url()s, so temp_urls is an array of URLs.
134
+ temp_urls.each do |temp_url|
135
+ url = temp_url[0]
136
+ image_urls << url
137
+ end
138
+ end
139
+
140
+ image_urls.each do |image_url|
141
+ # If url references are blank, browsers will often fill them with the current page's
142
+ # URL, which makes no sense and will never be renderable. Strip these.
143
+ next if image_url == current_path \
144
+ || image_url == page.current_url \
145
+ || image_url.strip.empty?
146
+
147
+ # Make the resource URL absolute to the current page. If it is already absolute, this
148
+ # will have no effect.
149
+ resource_url = URI.join(page.current_url, image_url).to_s
150
+
151
+ next if !_should_include_url?(resource_url)
152
+
153
+ # Fetch the images.
154
+ # TODO(fotinakis): this can be pretty inefficient for image-heavy pages because the
155
+ # browser has already loaded them once and this fetch cannot easily leverage the
156
+ # browser's cache. However, often these images are probably local resources served by a
157
+ # development server, so it may not be so bad. Re-evaluate if this becomes an issue.
158
+ response = _fetch_resource_url(resource_url)
159
+ next if !response
160
+
161
+ sha = Digest::SHA256.hexdigest(response.body)
162
+ resources << Percy::Client::Resource.new(
163
+ resource_url, mimetype: response.content_type, content: response.body)
164
+ end
165
+ resources
166
+ end
167
+ private :_get_image_resources
168
+
169
+ # @private
170
+ def _fetch_resource_url(url)
171
+ response = Percy::Capybara::HttpFetcher.fetch(url)
172
+ if !response
173
+ STDERR.puts "[percy] Warning: failed to fetch page resource, this might be a bug: #{url}"
174
+ return nil
175
+ end
176
+ response
177
+ end
178
+ private :_fetch_resource_url
179
+
180
+ # @private
181
+ def _evaluate_script(page, script)
182
+ script = <<-JS
183
+ (function() {
184
+ #{script}
185
+ })();
186
+ JS
187
+ page.evaluate_script(script)
188
+ end
189
+ private :_evaluate_script
190
+
191
+ # @private
192
+ def _should_include_url?(url)
193
+ # It is a URL or a path, but not a data URI.
194
+ url_match = URL_REGEX.match(url)
195
+ data_url_match = DATA_URL_REGEX.match(url)
196
+ result = (url_match || PATH_REGEX.match(url)) && !data_url_match
197
+
198
+ # Is not a remote URL.
199
+ if url_match && !data_url_match
200
+ host = url_match[2]
201
+ result = LOCAL_HOSTNAMES.include?(host)
202
+ end
203
+
204
+ !!result
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,50 @@
1
+ require 'percy/capybara/loaders/base_loader'
2
+ require 'digest'
3
+ require 'uri'
4
+
5
+ module Percy
6
+ module Capybara
7
+ module Loaders
8
+ # Resource loader that loads assets via Sprockets (ie. the Rails asset pipeline).
9
+ class SprocketsLoader < BaseLoader
10
+ attr_reader :page
11
+ attr_reader :sprockets_environment
12
+ attr_reader :sprockets_options
13
+
14
+ def initialize(options = {})
15
+ @sprockets_environment = options[:sprockets_environment]
16
+ @sprockets_options = options[:sprockets_options]
17
+ super
18
+ end
19
+
20
+ def snapshot_resources
21
+ # When loading via Sprockets, all other resources are associated to the build, so the only
22
+ # snapshot resource to upload is the root HTML.
23
+ [root_html_resource]
24
+ end
25
+
26
+ def build_resources
27
+ # Re-implement the same technique that "rake assets:precompile" uses to generate the
28
+ # list of asset paths to include in compiled assets. https://goo.gl/sy2R4z
29
+ # We can't just use environment.each_logical_path without any filters, because then
30
+ # we will attempt to compile assets before they're rendered (such as _mixins.css).
31
+ precompile_list = sprockets_options.precompile
32
+ logical_paths = sprockets_environment.each_logical_path(*precompile_list).to_a
33
+ logical_paths += precompile_list.flatten.select do |filename|
34
+ Pathname.new(filename).absolute? if filename.is_a?(String)
35
+ end
36
+
37
+ resources = []
38
+ logical_paths.each do |logical_path|
39
+ content = sprockets_environment.find_asset(logical_path).to_s
40
+ sha = Digest::SHA256.hexdigest(content)
41
+ resource_url = URI.escape("/assets/#{logical_path}")
42
+ resources << Percy::Client::Resource.new(
43
+ resource_url, sha: sha, content: content)
44
+ end
45
+ resources
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,5 @@
1
+ require 'rspec/core'
2
+
3
+ RSpec.configure do |config|
4
+ config.after(:suite) { Percy::Capybara.finalize_build }
5
+ end
@@ -1,5 +1,5 @@
1
1
  module Percy
2
2
  module Capybara
3
- VERSION = '0.3.0'
3
+ VERSION = '0.4.0'
4
4
  end
5
5
  end
@@ -24,8 +24,9 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'rake', '~> 10.0'
25
25
  spec.add_development_dependency 'rspec', '~> 3.2'
26
26
  spec.add_development_dependency 'capybara', '~> 2.4'
27
- spec.add_development_dependency 'capybara-webkit', '~> 1.5'
27
+ spec.add_development_dependency 'capybara-webkit', '>= 1.6'
28
28
  spec.add_development_dependency 'selenium-webdriver'
29
29
  spec.add_development_dependency 'webmock', '~> 1'
30
30
  spec.add_development_dependency 'faraday', '>= 0.8'
31
+ spec.add_development_dependency 'sprockets', '>= 3.2.0'
31
32
  end
@@ -5,7 +5,7 @@ RSpec.describe Percy::Capybara::Client::Builds do
5
5
  it 'returns the current build or creates a new one' do
6
6
  mock_double = double('build')
7
7
  expect(capybara_client.client).to receive(:create_build)
8
- .with(capybara_client.client.config.repo)
8
+ .with(capybara_client.client.config.repo, {})
9
9
  .and_return(mock_double)
10
10
  .once
11
11
 
@@ -15,6 +15,55 @@ RSpec.describe Percy::Capybara::Client::Builds do
15
15
  expect(current_build).to eq(mock_double)
16
16
  end
17
17
  end
18
+ describe '#upload_missing_build_resources', type: :feature, js: true do
19
+ before(:each) { setup_sprockets(capybara_client) }
20
+
21
+ it 'returns 0 if there are no missing build resources to upload' do
22
+ mock_response = {
23
+ 'data' => {
24
+ 'id' => '123',
25
+ 'type' => 'builds',
26
+ },
27
+ }
28
+ stub_request(:post, 'https://percy.io/api/v1/repos/percy/percy-capybara/builds/')
29
+ .to_return(status: 201, body: mock_response.to_json)
30
+
31
+ loader = capybara_client.initialize_loader
32
+ expect(capybara_client.upload_missing_build_resources(loader.build_resources)).to eq(0)
33
+ end
34
+ it 'uploads missing resources and returns the number uploaded' do
35
+ visit '/'
36
+ loader = capybara_client.initialize_loader(page: page)
37
+
38
+ mock_response = {
39
+ 'data' => {
40
+ 'id' => '123',
41
+ 'type' => 'builds',
42
+ 'relationships' => {
43
+ 'self' => "/api/v1/snapshots/123",
44
+ 'missing-resources' => {
45
+ 'data' => [
46
+ {
47
+ 'type' => 'resources',
48
+ 'id' => loader.build_resources.first.sha,
49
+ },
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ }
55
+ # Stub create build.
56
+ stub_request(:post, 'https://percy.io/api/v1/repos/percy/percy-capybara/builds/')
57
+ .to_return(status: 201, body: mock_response.to_json)
58
+ capybara_client.initialize_build
59
+
60
+ # Stub resource upload.
61
+ stub_request(:post, "https://percy.io/api/v1/builds/123/resources/")
62
+ .to_return(status: 201, body: {success: true}.to_json)
63
+ result = capybara_client.upload_missing_build_resources(loader.build_resources)
64
+ expect(result).to eq(1)
65
+ end
66
+ end
18
67
  describe '#build_initialized?' do
19
68
  it 'is false before a build is initialized and true afterward' do
20
69
  expect(capybara_client.client).to receive(:create_build).and_return(double('build'))
@@ -1,232 +1,42 @@
1
1
  require 'json'
2
2
  require 'digest'
3
+ require 'capybara'
4
+ require 'capybara/webkit'
3
5
 
4
6
  RSpec.describe Percy::Capybara::Client::Snapshots, type: :feature do
5
7
  let(:capybara_client) { Percy::Capybara::Client.new(enabled: true) }
6
8
 
7
- # Start a temp webserver that serves the testdata directory.
8
- # You can test this server manually by running:
9
- # ruby -run -e httpd spec/lib/percy/capybara/client/testdata/ -p 9090
10
- before(:all) do
11
- port = get_random_open_port
12
- Capybara.app_host = "http://localhost:#{port}"
13
- Capybara.run_server = false
14
-
15
- # Note: using this form of popen to keep stdout and stderr silent and captured.
16
- dir = File.expand_path('../testdata/', __FILE__)
17
- @process = IO.popen([
18
- 'ruby', '-run', '-e', 'httpd', dir, '-p', port.to_s, err: [:child, :out]
19
- ].flatten)
20
-
21
- # Block until the server is up.
22
- WebMock.disable_net_connect!(allow_localhost: true)
23
- verify_server_up(Capybara.app_host)
24
- end
25
- after(:all) { Process.kill('INT', @process.pid) }
26
-
27
- before(:each, js: true) 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
-
41
- describe '#_should_include_url?' do
42
- it 'returns true for valid, local URLs' do
43
- expect(capybara_client._should_include_url?('http://localhost/')).to eq(true)
44
- expect(capybara_client._should_include_url?('http://localhost:123/')).to eq(true)
45
- expect(capybara_client._should_include_url?('http://localhost/foo')).to eq(true)
46
- expect(capybara_client._should_include_url?('http://localhost:123/foo')).to eq(true)
47
- expect(capybara_client._should_include_url?('http://localhost/foo/test.html')).to eq(true)
48
- expect(capybara_client._should_include_url?('http://127.0.0.1/')).to eq(true)
49
- expect(capybara_client._should_include_url?('http://127.0.0.1:123/')).to eq(true)
50
- expect(capybara_client._should_include_url?('http://127.0.0.1/foo')).to eq(true)
51
- expect(capybara_client._should_include_url?('http://127.0.0.1:123/foo')).to eq(true)
52
- expect(capybara_client._should_include_url?('http://127.0.0.1/foo/test.html')).to eq(true)
53
- expect(capybara_client._should_include_url?('http://0.0.0.0/foo/test.html')).to eq(true)
54
- # Also works for paths:
55
- expect(capybara_client._should_include_url?('/')).to eq(true)
56
- expect(capybara_client._should_include_url?('/foo')).to eq(true)
57
- expect(capybara_client._should_include_url?('/foo/test.png')).to eq(true)
58
- end
59
- it 'returns false for invalid URLs' do
60
- expect(capybara_client._should_include_url?('')).to eq(false)
61
- expect(capybara_client._should_include_url?('http://local host/foo')).to eq(false)
62
- expect(capybara_client._should_include_url?('bad-url/')).to eq(false)
63
- expect(capybara_client._should_include_url?('bad-url/foo/test.html')).to eq(false)
64
- end
65
- it 'returns false for data URLs' do
66
- expect(capybara_client._should_include_url?('data:image/gif;base64,R0')).to eq(false)
67
- end
68
- it 'returns false for remote URLs' do
69
- expect(capybara_client._should_include_url?('http://foo/')).to eq(false)
70
- expect(capybara_client._should_include_url?('http://example.com/')).to eq(false)
71
- expect(capybara_client._should_include_url?('http://example.com/foo')).to eq(false)
72
- expect(capybara_client._should_include_url?('https://example.com/foo')).to eq(false)
73
- end
74
- end
75
- describe '#_get_root_html_resource', type: :feature, js: true do
76
- it 'includes the root DOM HTML' do
77
- visit '/'
78
- resource = capybara_client.send(:_get_root_html_resource, page)
79
-
80
- expect(resource.is_root).to be_truthy
81
- expect(resource.mimetype).to eq('text/html')
82
- expect(resource.resource_url).to match(/http:\/\/localhost:\d+\//)
83
- expect(resource.content).to include('Hello World!')
84
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
85
- end
86
- end
87
- describe '#_get_css_resources', type: :feature, js: true do
88
- it 'includes all linked and imported stylesheets' do
89
- visit '/test-css.html'
90
- resources = capybara_client.send(:_get_css_resources, page)
91
-
92
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/base\.css/)
93
-
94
- expect(resource.content).to include('.colored-by-base { color: red; }')
95
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
96
-
97
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/simple-imports\.css/)
98
- expect(resource.content).to include("@import url('imports.css');")
99
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
100
-
101
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/imports\.css/)
102
- expect(resource.content).to include('.colored-by-imports { color: red; }')
103
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
104
-
105
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level0-imports\.css/)
106
- expect(resource.content).to include("@import url('level1-imports.css')")
107
- expect(resource.content).to include('.colored-by-level0-imports { color: red; }')
108
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
109
-
110
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level1-imports\.css/)
111
- expect(resource.content).to include("@import url('level2-imports.css')")
112
- expect(resource.content).to include('.colored-by-level1-imports { color: red; }')
113
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
114
-
115
- resource = find_resource(resources, /http:\/\/localhost:\d+\/css\/level2-imports\.css/)
116
- expect(resource.content).to include(".colored-by-level2-imports { color: red; }")
117
- expect(resource.sha).to eq(Digest::SHA256.hexdigest(resource.content))
118
-
119
- expect(resources.length).to eq(6)
120
- expect(resources.collect(&:mimetype).uniq).to eq(['text/css'])
121
- expect(resources.collect(&:is_root).uniq).to match_array([nil])
122
- end
123
- end
124
- describe '#_get_image_resources', type: :feature, js: true do
125
- it 'includes all images' do
126
- visit '/test-images.html'
127
- resources = capybara_client.send(:_get_image_resources, page)
128
-
129
- # The order of these is just for convenience, they match the order in test-images.html.
130
-
131
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/img-relative\.png/)
132
- content = File.read(File.expand_path('../testdata/images/img-relative.png', __FILE__))
133
- expect(resource.mimetype).to eq('image/png')
134
- expected_sha = Digest::SHA256.hexdigest(content)
135
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
136
- expect(resource.sha).to eq(expected_sha)
137
-
138
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/img-relative-to-root\.png/)
139
- content = File.read(File.expand_path('../testdata/images/img-relative-to-root.png', __FILE__))
140
- expect(resource.mimetype).to eq('image/png')
141
- expected_sha = Digest::SHA256.hexdigest(content)
142
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
143
- expect(resource.sha).to eq(expected_sha)
144
-
145
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/percy\.svg/)
146
- content = File.read(File.expand_path('../testdata/images/percy.svg', __FILE__))
147
- # In Ruby 1.9.3 the SVG mimetype is not registered so our mini ruby webserver doesn't serve
148
- # the correct content type. Allow either to work here so we can test older Rubies fully.
149
- expect(resource.mimetype).to match(/image\/svg\+xml|application\/octet-stream/)
150
- expected_sha = Digest::SHA256.hexdigest(content)
151
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
152
- expect(resource.sha).to eq(expected_sha)
153
-
154
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-relative\.png/)
155
- content = File.read(File.expand_path('../testdata/images/bg-relative.png', __FILE__))
156
- expect(resource.mimetype).to eq('image/png')
157
- expected_sha = Digest::SHA256.hexdigest(content)
158
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
159
- expect(resource.sha).to eq(expected_sha)
160
-
161
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-relative-to-root\.png/)
162
- content = File.read(File.expand_path('../testdata/images/bg-relative-to-root.png', __FILE__))
163
- expect(resource.mimetype).to eq('image/png')
164
- expected_sha = Digest::SHA256.hexdigest(content)
165
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
166
- expect(resource.sha).to eq(expected_sha)
167
-
168
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/bg-stacked\.png/)
169
- content = File.read(File.expand_path('../testdata/images/bg-stacked.png', __FILE__))
170
- expect(resource.mimetype).to eq('image/png')
171
- expected_sha = Digest::SHA256.hexdigest(content)
172
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
173
- expect(resource.sha).to eq(expected_sha)
174
-
175
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-base\.png/)
176
- content = File.read(File.expand_path('../testdata/images/srcset-base.png', __FILE__))
177
- expect(resource.mimetype).to eq('image/png')
178
- expected_sha = Digest::SHA256.hexdigest(content)
179
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
180
- expect(resource.sha).to eq(expected_sha)
181
-
182
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-first\.png/)
183
- content = File.read(File.expand_path('../testdata/images/srcset-first.png', __FILE__))
184
- expect(resource.mimetype).to eq('image/png')
185
- expected_sha = Digest::SHA256.hexdigest(content)
186
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
187
- expect(resource.sha).to eq(expected_sha)
188
-
189
- resource = find_resource(resources, /http:\/\/localhost:\d+\/images\/srcset-second\.png/)
190
- content = File.read(File.expand_path('../testdata/images/srcset-second.png', __FILE__))
191
- expect(resource.mimetype).to eq('image/png')
192
- expected_sha = Digest::SHA256.hexdigest(content)
193
- expect(Digest::SHA256.hexdigest(resource.content)).to eq(expected_sha)
194
- expect(resource.sha).to eq(expected_sha)
195
-
196
- resource_urls = resources.collect(&:resource_url).map do |url|
197
- url.gsub(/localhost:\d+/, 'localhost')
198
- end
199
- expect(resource_urls).to match_array([
200
- "http://localhost/images/img-relative.png",
201
- "http://localhost/images/img-relative-to-root.png",
202
- "http://localhost/images/percy.svg",
203
- "http://localhost/images/srcset-base.png",
204
- "http://localhost/images/srcset-first.png",
205
- "http://localhost/images/srcset-second.png",
206
- "http://localhost/images/bg-relative.png",
207
- "http://localhost/images/bg-relative-to-root.png",
208
- "http://localhost/images/bg-stacked.png"
209
- ])
210
- expect(resources.collect(&:is_root).uniq).to match_array([nil])
211
- end
212
- end
213
9
  describe '#snapshot', type: :feature, js: true do
214
10
  context 'simple page with no resources' do
215
- let(:content) { '<html><body>Hello World!</body><head></head></html>' }
11
+ before(:each) { setup_sprockets(capybara_client) }
216
12
 
217
- it 'creates a snapshot and uploads missing resource' do
13
+ it 'creates a snapshot and uploads missing build resources and missing snapshot resources' do
218
14
  visit '/'
15
+ loader = capybara_client.initialize_loader(page: page)
16
+
17
+ build_resource_sha = loader.build_resources.first.sha
18
+ snapshot_resource_sha = loader.snapshot_resources.first.sha
219
19
 
220
20
  mock_response = {
221
21
  'data' => {
222
22
  'id' => '123',
223
23
  'type' => 'builds',
24
+ 'relationships' => {
25
+ 'self' => "/api/v1/snapshots/123",
26
+ 'missing-resources' => {
27
+ 'data' => [
28
+ {
29
+ 'type' => 'resources',
30
+ 'id' => build_resource_sha,
31
+ },
32
+ ],
33
+ },
34
+ },
224
35
  },
225
36
  }
226
37
  stub_request(:post, 'https://percy.io/api/v1/repos/percy/percy-capybara/builds/')
227
38
  .to_return(status: 201, body: mock_response.to_json)
228
39
 
229
- resource = capybara_client.send(:_get_root_html_resource, page)
230
40
  mock_response = {
231
41
  'data' => {
232
42
  'id' => '256',
@@ -237,7 +47,7 @@ RSpec.describe Percy::Capybara::Client::Snapshots, type: :feature do
237
47
  'data' => [
238
48
  {
239
49
  'type' => 'resources',
240
- 'id' => resource.sha,
50
+ 'id' => snapshot_resource_sha,
241
51
  },
242
52
  ],
243
53
  },
@@ -246,20 +56,21 @@ RSpec.describe Percy::Capybara::Client::Snapshots, type: :feature do
246
56
  }
247
57
  stub_request(:post, 'https://percy.io/api/v1/builds/123/snapshots/')
248
58
  .to_return(status: 201, body: mock_response.to_json)
249
-
59
+ build_resource_stub = stub_request(:post, "https://percy.io/api/v1/builds/123/resources/")
60
+ .with(body: /#{build_resource_sha}/)
61
+ .to_return(status: 201, body: {success: true}.to_json)
250
62
  stub_request(:post, "https://percy.io/api/v1/builds/123/resources/")
251
- .with(body: /#{resource.sha}/).to_return(status: 201, body: {success: true}.to_json)
252
-
253
- expect(capybara_client).to receive(:_get_root_html_resource)
254
- .with(page).once.and_call_original
255
- expect(capybara_client).to receive(:_get_css_resources)
256
- .with(page).once.and_call_original
257
- expect(capybara_client).to receive(:_get_image_resources)
258
- .with(page).once.and_call_original
259
-
63
+ .with(body: /#{snapshot_resource_sha}/)
64
+ .to_return(status: 201, body: {success: true}.to_json)
260
65
  stub_request(:post, "https://percy.io/api/v1/snapshots/256/finalize")
261
66
  .to_return(status: 200, body: '{"success":true}')
262
67
 
68
+ expect(capybara_client.build_initialized?).to eq(false)
69
+ expect(capybara_client.snapshot(page)).to eq(true)
70
+ expect(capybara_client.build_initialized?).to eq(true)
71
+
72
+ # Second time, no build resources are uploaded.
73
+ remove_request_stub(build_resource_stub)
263
74
  expect(capybara_client.snapshot(page)).to eq(true)
264
75
  end
265
76
  end