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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f6b29e232904e95b181e02e0381bd459dfd09a49
4
- data.tar.gz: e6c802f2770ba1ab8201838b315945adc3b54cf3
3
+ metadata.gz: 0373826717cf4a83f083c9d8d71d92dfea1b595c
4
+ data.tar.gz: e32236928c98afe4613579a5a9db5869ea506f90
5
5
  SHA512:
6
- metadata.gz: 634f54da9d0ca8f752dc4c3f22fe151072b6064cd8045cb4a7ae280e624dd1090fa5591c898fa57ef83eec37b92049ecafaf44adcd9ef4e84d5af2a0b1b326b7
7
- data.tar.gz: 997c23094a26023115a2a3245d3429329829e9136c84f3370aa9d54c75e40f6ccc8a233ab8078ac1acfbbb07bcb764443a832bee2ae3c6e15b316bb1aad83506
6
+ metadata.gz: bbc8b68ae8f7df03bc575f64cd6efa4c9bcb7bfbde16e478735e57d43b1e5d3bb64bd7e380db3025f81bc8c3bfdd7d8f0283b21da075258f98357ac94c9a15d9
7
+ data.tar.gz: 053c1512eba240ff18904cb7790f5aa8093cc8356cfe086665be2b241ec9291679d2fb2d69a09ed790e055ee8f075ba652be3a26bed671ad23ffb2ec90eac5b6
@@ -25,8 +25,8 @@ module Percy
25
25
  # multi-process tests where a single build must be created before forking.
26
26
  #
27
27
  # @see Percy::Capybara::Client::Builds#initialize_build
28
- def self.initialize_build
29
- capybara_client.initialize_build
28
+ def self.initialize_build(options = {})
29
+ capybara_client.initialize_build(options)
30
30
  end
31
31
 
32
32
  # Finalize the current build.
@@ -1,5 +1,7 @@
1
1
  require 'percy/capybara/client/builds'
2
2
  require 'percy/capybara/client/snapshots'
3
+ require 'percy/capybara/loaders/native_loader'
4
+ require 'percy/capybara/loaders/sprockets_loader'
3
5
 
4
6
  module Percy
5
7
  module Capybara
@@ -13,15 +15,33 @@ module Percy
13
15
 
14
16
  attr_reader :client
15
17
 
18
+ attr_accessor :sprockets_environment
19
+ attr_accessor :sprockets_options
20
+
16
21
  def initialize(options = {})
17
22
  @client = options[:client] || Percy.client
18
23
  @enabled = options[:enabled]
24
+
25
+ if defined?(Rails)
26
+ @sprockets_environment = options[:sprockets_environment] || Rails.application.assets
27
+ @sprockets_options = options[:sprockets_options] || Rails.application.config.assets
28
+ end
19
29
  end
20
30
 
21
31
  def enabled?
22
32
  # Only enable if in supported CI environment or local dev with PERCY_ENABLE=1.
23
33
  @enabled ||= !Percy::Client::Environment.current_ci.nil? || ENV['PERCY_ENABLE'] == '1'
24
34
  end
35
+
36
+ def initialize_loader(options = {})
37
+ if sprockets_environment && sprockets_options
38
+ options[:sprockets_environment] = sprockets_environment
39
+ options[:sprockets_options] = sprockets_options
40
+ Percy::Capybara::Loaders::SprocketsLoader.new(options)
41
+ else
42
+ Percy::Capybara::Loaders::NativeLoader.new(options)
43
+ end
44
+ end
25
45
  end
26
46
  end
27
47
  end
@@ -2,12 +2,32 @@ module Percy
2
2
  module Capybara
3
3
  class Client
4
4
  module Builds
5
- def current_build
5
+ def current_build(options = {})
6
6
  return if !enabled? # Silently skip if the client is disabled.
7
- @current_build ||= client.create_build(client.config.repo)
7
+ @current_build ||= client.create_build(client.config.repo, options)
8
8
  end
9
9
  alias_method :initialize_build, :current_build
10
10
 
11
+ def upload_missing_build_resources(build_resources)
12
+ # Upload any missing build resources.
13
+ new_build_resources = current_build['data'] &&
14
+ current_build['data']['relationships'] &&
15
+ current_build['data']['relationships']['missing-resources'] &&
16
+ current_build['data']['relationships']['missing-resources']['data']
17
+ return 0 if !new_build_resources
18
+
19
+ if !new_build_resources.empty?
20
+ puts "[percy] Uploading #{new_build_resources.length} new resources..."
21
+ end
22
+ new_build_resources.each do |missing_resource|
23
+ sha = missing_resource['id']
24
+ resource = build_resources.find { |r| r.sha == sha }
25
+ content = resource.content || File.read(resource.path)
26
+ client.upload_resource(current_build['data']['id'], content)
27
+ end
28
+ new_build_resources.length
29
+ end
30
+
11
31
  def build_initialized?
12
32
  !!@current_build
13
33
  end
@@ -2,48 +2,13 @@ require 'set'
2
2
  require 'faraday'
3
3
  require 'httpclient'
4
4
  require 'digest'
5
+ require 'uri'
6
+ require 'pathname'
5
7
 
6
8
  module Percy
7
9
  module Capybara
8
10
  class Client
9
11
  module Snapshots
10
- # Modified version of Diego Perini's URL regex. https://gist.github.com/dperini/729294
11
- URL_REGEX = Regexp.new(
12
- # protocol identifier
13
- "(?:(?:https?:)?//)" +
14
- "(" +
15
- # IP address exclusion
16
- # private & local networks
17
- "(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
18
- "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
19
- "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
20
- # IP address dotted notation octets
21
- # excludes loopback network 0.0.0.0
22
- # excludes reserved space >= 224.0.0.0
23
- # excludes network & broacast addresses
24
- # (first & last IP address of each class)
25
- "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
26
- "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
27
- "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
28
- "|" +
29
- # host name
30
- "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
31
- # domain name
32
- "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
33
- ")" +
34
- # port number
35
- "(?::\\d{2,5})?" +
36
- # resource path
37
- "(?:/[^\\s\"']*)?"
38
- )
39
- PATH_REGEX = /\A\/[^\\s\"']*/
40
- DATA_URL_REGEX = /\Adata:/
41
- LOCAL_HOSTNAMES = [
42
- 'localhost',
43
- '127.0.0.1',
44
- '0.0.0.0',
45
- ].freeze
46
-
47
12
  # Takes a snapshot of the given page HTML and its assets.
48
13
  #
49
14
  # @param [Capybara::Session] page The Capybara page to snapshot.
@@ -54,11 +19,22 @@ module Percy
54
19
  def snapshot(page, options = {})
55
20
  return if !enabled? # Silently skip if the client is disabled.
56
21
  name = options[:name]
22
+ loader = initialize_loader(page: page)
23
+
24
+ # If this is the first snapshot, create the build and upload build resources.
25
+ if !build_initialized?
26
+ build_resources = loader.build_resources
27
+ initialize_build(resources: build_resources)
28
+ upload_missing_build_resources(build_resources)
29
+ end
30
+
57
31
  current_build_id = current_build['data']['id']
58
- resource_map = _find_resources(page)
59
- snapshot = client.create_snapshot(current_build_id, resource_map.values, name: name)
32
+ resources = loader.snapshot_resources
33
+ resource_map = {}
34
+ resources.each { |r| resource_map[r.sha] = r }
60
35
 
61
- # Upload the content for any missing resources.
36
+ # Create the snapshot and upload any missing snapshot resources.
37
+ snapshot = client.create_snapshot(current_build_id, resources, name: name)
62
38
  snapshot['data']['relationships']['missing-resources']['data'].each do |missing_resource|
63
39
  sha = missing_resource['id']
64
40
  client.upload_resource(current_build_id, resource_map[sha].content)
@@ -69,204 +45,6 @@ module Percy
69
45
 
70
46
  true
71
47
  end
72
-
73
- # @private
74
- def _find_resources(page)
75
- resource_map = {}
76
- resources = []
77
- resources << _get_root_html_resource(page)
78
- resources += _get_css_resources(page)
79
- resources += _get_image_resources(page)
80
- resources.each { |resource| resource_map[resource.sha] = resource }
81
- resource_map
82
- end
83
- private :_find_resources
84
-
85
- # @private
86
- def _get_root_html_resource(page)
87
- # Primary HTML.
88
- script = <<-JS
89
- var htmlElement = document.getElementsByTagName('html')[0];
90
- return htmlElement.outerHTML;
91
- JS
92
- html = _evaluate_script(page, script)
93
- resource_url = page.current_url
94
- Percy::Client::Resource.new(
95
- resource_url, is_root: true, mimetype: 'text/html', content: html)
96
- end
97
- private :_get_root_html_resource
98
-
99
- # @private
100
- def _get_css_resources(page)
101
- resources = []
102
- # Find all CSS resources.
103
- # http://www.quirksmode.org/dom/w3c_css.html#access
104
- script = <<-JS
105
- function findStylesRecursively(stylesheet, css_urls) {
106
- if (stylesheet.href) { // Skip stylesheet without hrefs (inline stylesheets).
107
- css_urls.push(stylesheet.href);
108
-
109
- // Remote stylesheet rules cannot be accessed because of the same-origin policy.
110
- // Unfortunately, if you touch .cssRules in Selenium, it throws a JavascriptError
111
- // with 'The operation is insecure'. To work around this, skip reading rules of
112
- // remote stylesheets but still include them for fetching.
113
- //
114
- // TODO: If a remote stylesheet has an @import, it will be missing because we don't
115
- // notice it here. We could potentially recursively fetch remote imports in
116
- // ruby-land below.
117
- var parser = document.createElement('a');
118
- parser.href = stylesheet.href;
119
- if (parser.host != window.location.host) {
120
- return;
121
- }
122
- }
123
- for (var i = 0; i < stylesheet.cssRules.length; i++) {
124
- var rule = stylesheet.cssRules[i];
125
- // Depth-first search, handle recursive @imports.
126
- if (rule.styleSheet) {
127
- findStylesRecursively(rule.styleSheet, css_urls);
128
- }
129
- }
130
- }
131
-
132
- var css_urls = [];
133
- for (var i = 0; i < document.styleSheets.length; i++) {
134
- findStylesRecursively(document.styleSheets[i], css_urls);
135
- }
136
- return css_urls;
137
- JS
138
- resource_urls = _evaluate_script(page, script)
139
-
140
- resource_urls.each do |url|
141
- next if !_should_include_url?(url)
142
- response = _fetch_resource_url(url)
143
- next if !response
144
- sha = Digest::SHA256.hexdigest(response.body)
145
- resources << Percy::Client::Resource.new(
146
- url, mimetype: 'text/css', content: response.body)
147
- end
148
- resources
149
- end
150
- private :_get_css_resources
151
-
152
- # @private
153
- def _get_image_resources(page)
154
- resources = []
155
- image_urls = Set.new
156
-
157
- # Find all image tags on the page.
158
- page.all('img').each do |image_element|
159
- srcs = []
160
- srcs << image_element[:src] if !image_element[:src].nil?
161
-
162
- srcset_raw_urls = image_element[:srcset] || ''
163
- temp_urls = srcset_raw_urls.split(',')
164
- temp_urls.each do |temp_url|
165
- srcs << temp_url.split(' ').first
166
- end
167
-
168
- srcs.each do |url|
169
- image_urls << url
170
- end
171
- end
172
-
173
- # Find all CSS-loaded images which set a background-image.
174
- script = <<-JS
175
- var raw_image_urls = [];
176
-
177
- var tags = document.getElementsByTagName('*');
178
- var el;
179
- var rawValue;
180
-
181
- for (var i = 0; i < tags.length; i++) {
182
- el = tags[i];
183
- if (el.currentStyle) {
184
- rawValue = el.currentStyle['backgroundImage'];
185
- } else if (window.getComputedStyle) {
186
- rawValue = window.getComputedStyle(el).getPropertyValue('background-image');
187
- }
188
- if (!rawValue || rawValue === "none") {
189
- continue;
190
- } else {
191
- raw_image_urls.push(rawValue);
192
- }
193
- }
194
- return raw_image_urls;
195
- JS
196
- raw_image_urls = _evaluate_script(page, script)
197
- raw_image_urls.each do |raw_image_url|
198
- temp_urls = raw_image_url.scan(/url\(["']?(.*?)["']?\)/)
199
- # background-image can accept multiple url()s, so temp_urls is an array of URLs.
200
- temp_urls.each do |temp_url|
201
- url = temp_url[0]
202
- image_urls << url
203
- end
204
- end
205
-
206
- image_urls.each do |image_url|
207
- # If url references are blank, browsers will often fill them with the current page's
208
- # URL, which makes no sense and will never be renderable. Strip these.
209
- next if image_url == page.current_url || image_url.strip.empty?
210
-
211
- # Make the resource URL absolute to the current page. If it is already absolute, this
212
- # will have no effect.
213
- resource_url = URI.join(page.current_url, image_url).to_s
214
-
215
- next if !_should_include_url?(resource_url)
216
-
217
- # Fetch the images.
218
- # TODO(fotinakis): this can be pretty inefficient for image-heavy pages because the
219
- # browser has already loaded them once and this fetch cannot easily leverage the
220
- # browser's cache. However, often these images are probably local resources served by a
221
- # development server, so it may not be so bad. Re-evaluate if this becomes an issue.
222
- response = _fetch_resource_url(resource_url)
223
- next if !response
224
-
225
- sha = Digest::SHA256.hexdigest(response.body)
226
- resources << Percy::Client::Resource.new(
227
- resource_url, mimetype: response.content_type, content: response.body)
228
- end
229
- resources
230
- end
231
- private :_get_image_resources
232
-
233
- # @private
234
- def _should_include_url?(url)
235
- # It is a URL or a path, but not a data URI.
236
- url_match = URL_REGEX.match(url)
237
- data_url_match = DATA_URL_REGEX.match(url)
238
- result = (url_match || PATH_REGEX.match(url)) && !data_url_match
239
-
240
- # Is not a remote URL.
241
- if url_match && !data_url_match
242
- host = url_match[1]
243
- result = LOCAL_HOSTNAMES.include?(host)
244
- end
245
-
246
- !!result
247
- end
248
-
249
- # @private
250
- def _fetch_resource_url(url)
251
- response = Percy::Capybara::HttpFetcher.fetch(url)
252
- if !response
253
- STDERR.puts "[percy] Warning: failed to fetch page resource, this might be a bug: #{url}"
254
- return nil
255
- end
256
- response
257
- end
258
- private :_fetch_resource_url
259
-
260
- # @private
261
- def _evaluate_script(page, script)
262
- script = <<-JS
263
- (function() {
264
- #{script}
265
- })();
266
- JS
267
- page.evaluate_script(script)
268
- end
269
- private :_evaluate_script
270
48
  end
271
49
  end
272
50
  end
@@ -0,0 +1,71 @@
1
+ module Percy
2
+ module Capybara
3
+ module Loaders
4
+ class BaseLoader
5
+ # Modified version of Diego Perini's URL regex. https://gist.github.com/dperini/729294
6
+ URL_REGEX = Regexp.new(
7
+ # protocol identifier
8
+ "((?:https?:)?//)" +
9
+ "(" +
10
+ # IP address exclusion
11
+ # private & local networks
12
+ "(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
13
+ "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
14
+ "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
15
+ # IP address dotted notation octets
16
+ # excludes loopback network 0.0.0.0
17
+ # excludes reserved space >= 224.0.0.0
18
+ # excludes network & broacast addresses
19
+ # (first & last IP address of each class)
20
+ "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
21
+ "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
22
+ "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
23
+ "|" +
24
+ # host name
25
+ "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" +
26
+ # domain name
27
+ "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" +
28
+ ")" +
29
+ # port number
30
+ "(:\\d{2,5})?" +
31
+ # resource path
32
+ "(/[^\\s\"']*)?"
33
+ )
34
+
35
+ attr_reader :page
36
+
37
+ # @param [Capybara::Session] page The Capybara page.
38
+ def initialize(options = {})
39
+ @page = options[:page]
40
+ end
41
+
42
+ def build_resources
43
+ raise NotImplementedError.new('subclass must implement abstract method')
44
+ end
45
+
46
+ def snapshot_resources
47
+ raise NotImplementedError.new('subclass must implement abstract method')
48
+ end
49
+
50
+ # @private
51
+ def root_html_resource
52
+ Percy::Client::Resource.new(
53
+ current_path, is_root: true, mimetype: 'text/html', content: page.html)
54
+ end
55
+
56
+ # Transformed version of the current URL to be a relative path.
57
+ # This important because Rack::Test uses "www.example.com" as the actual current URL,
58
+ # which would force Percy to actually render example.com instead of the page. By always
59
+ # using a URL path as the resource URL, we guarantee that Percy will render what it's given.
60
+ #
61
+ # @private
62
+ def current_path
63
+ current_url = page.current_url
64
+ url_match = URL_REGEX.match(current_url)
65
+ return url_match[4] if url_match
66
+ current_url
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end