percy-capybara 0.3.0 → 0.4.0

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