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.
- checksums.yaml +4 -4
- data/lib/percy/capybara.rb +2 -2
- data/lib/percy/capybara/client.rb +20 -0
- data/lib/percy/capybara/client/builds.rb +22 -2
- data/lib/percy/capybara/client/snapshots.rb +16 -238
- data/lib/percy/capybara/loaders/base_loader.rb +71 -0
- data/lib/percy/capybara/loaders/native_loader.rb +209 -0
- data/lib/percy/capybara/loaders/sprockets_loader.rb +50 -0
- data/lib/percy/capybara/rspec.rb +5 -0
- data/lib/percy/capybara/version.rb +1 -1
- data/percy-capybara.gemspec +2 -1
- data/spec/lib/percy/capybara/client/builds_spec.rb +50 -1
- data/spec/lib/percy/capybara/client/snapshots_spec.rb +31 -220
- data/spec/lib/percy/capybara/client/testdata/index.html +1 -1
- data/spec/lib/percy/capybara/client_spec.rb +13 -0
- data/spec/lib/percy/capybara/loaders/base_loader_spec.rb +52 -0
- data/spec/lib/percy/capybara/loaders/native_loader_spec.rb +224 -0
- data/spec/lib/percy/capybara/loaders/sprockets_loader_spec.rb +58 -0
- data/spec/lib/percy/capybara_spec.rb +0 -1
- data/spec/spec_helper.rb +26 -0
- data/spec/support/test_helpers.rb +21 -0
- metadata +30 -6
@@ -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
|
data/percy-capybara.gemspec
CHANGED
@@ -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', '
|
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
|
-
|
11
|
+
before(:each) { setup_sprockets(capybara_client) }
|
216
12
|
|
217
|
-
it 'creates a snapshot and uploads missing
|
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' =>
|
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: /#{
|
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
|