percy-capybara 3.2.0 → 4.0.0.pre.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +39 -0
- data/.gitignore +1 -0
- data/DEVELOPING.md +26 -0
- data/README.md +2 -2
- data/lib/environment.rb +31 -0
- data/lib/percy.rb +119 -0
- data/lib/version.rb +3 -0
- data/package-lock.json +2155 -0
- data/package.json +6 -0
- data/percy-capybara.gemspec +3 -12
- data/spec/lib/percy/environment_spec.rb +31 -0
- data/spec/lib/percy/percy_spec.rb +55 -0
- data/spec/spec_helper.rb +4 -44
- metadata +17 -233
- data/.travis.yml +0 -17
- data/lib/percy/capybara.rb +0 -60
- data/lib/percy/capybara/anywhere.rb +0 -37
- data/lib/percy/capybara/client.rb +0 -127
- data/lib/percy/capybara/client/builds.rb +0 -81
- data/lib/percy/capybara/client/snapshots.rb +0 -66
- data/lib/percy/capybara/client/user_agent.rb +0 -37
- data/lib/percy/capybara/httpfetcher.rb +0 -36
- data/lib/percy/capybara/loaders/base_loader.rb +0 -161
- data/lib/percy/capybara/loaders/ember_cli_rails_loader.rb +0 -52
- data/lib/percy/capybara/loaders/filesystem_loader.rb +0 -37
- data/lib/percy/capybara/loaders/native_loader.rb +0 -287
- data/lib/percy/capybara/loaders/sprockets_loader.rb +0 -99
- data/lib/percy/capybara/rspec.rb +0 -8
- data/lib/percy/capybara/version.rb +0 -5
- data/spec/lib/percy/capybara/client/builds_spec.rb +0 -171
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/admin/assets/percy-admin.svg +0 -34
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/admin/index.html +0 -1
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/admin/percy-admin-public.svg +0 -34
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/frontend/assets/percy-frontend.svg +0 -34
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/frontend/index.html +0 -1
- data/spec/lib/percy/capybara/client/ember_test_data/ember-cli/frontend/percy-frontend-public.svg +0 -34
- data/spec/lib/percy/capybara/client/rails_public_test_data/large-file-skipped.png +0 -0
- data/spec/lib/percy/capybara/client/rails_public_test_data/percy-from-public.svg +0 -34
- data/spec/lib/percy/capybara/client/rails_public_test_data/symlink_to_images +0 -1
- data/spec/lib/percy/capybara/client/snapshots_spec.rb +0 -97
- data/spec/lib/percy/capybara/client/symlink_test_data/test.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/assets/css/digested-f3420c6aee71c137a3ca39727052811bae84b2f37d898f4db242e20656a1579e.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/assets/images/large-file-skipped.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/css/base.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/css/digested.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/css/font.css +0 -8
- data/spec/lib/percy/capybara/client/test_data/css/imports.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/css/level0-imports.css +0 -2
- data/spec/lib/percy/capybara/client/test_data/css/level1-imports.css +0 -2
- data/spec/lib/percy/capybara/client/test_data/css/level2-imports.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/css/simple-imports.css +0 -1
- data/spec/lib/percy/capybara/client/test_data/css/source.css.map +0 -2
- data/spec/lib/percy/capybara/client/test_data/iframe.html +0 -7
- data/spec/lib/percy/capybara/client/test_data/images/bg-relative-to-root.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/bg-relative.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/bg-stacked.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/img-relative-to-root.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/img-relative.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/percy.svg +0 -34
- data/spec/lib/percy/capybara/client/test_data/images/srcset-base.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/srcset-first.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/images/srcset-second.png +0 -0
- data/spec/lib/percy/capybara/client/test_data/index.html +0 -1
- data/spec/lib/percy/capybara/client/test_data/js/base.js +0 -1
- data/spec/lib/percy/capybara/client/test_data/test-css.html +0 -20
- data/spec/lib/percy/capybara/client/test_data/test-font.html +0 -9
- data/spec/lib/percy/capybara/client/test_data/test-iframe.html +0 -8
- data/spec/lib/percy/capybara/client/test_data/test-images.html +0 -51
- data/spec/lib/percy/capybara/client/test_data/test-localtest-me-images.html +0 -8
- data/spec/lib/percy/capybara/client/user_agent_spec.rb +0 -45
- data/spec/lib/percy/capybara/client_spec.rb +0 -232
- data/spec/lib/percy/capybara/http_fetcher_spec.rb +0 -17
- data/spec/lib/percy/capybara/loaders/base_loader_spec.rb +0 -122
- data/spec/lib/percy/capybara/loaders/ember_cli_rails_loader_spec.rb +0 -86
- data/spec/lib/percy/capybara/loaders/filesystem_loader_spec.rb +0 -166
- data/spec/lib/percy/capybara/loaders/native_loader_spec.rb +0 -300
- data/spec/lib/percy/capybara/loaders/sprockets_loader_spec.rb +0 -119
- data/spec/lib/percy/capybara_spec.rb +0 -101
- data/spec/support/test_helpers.rb +0 -60
@@ -1,161 +0,0 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
require 'percy/capybara'
|
3
|
-
|
4
|
-
module Percy
|
5
|
-
module Capybara
|
6
|
-
module Loaders
|
7
|
-
class BaseLoader
|
8
|
-
# Modified version of Diego Perini's URL regex. https://gist.github.com/dperini/729294
|
9
|
-
URL_REGEX = Regexp.new(
|
10
|
-
# protocol identifier
|
11
|
-
'((?:https?:)?//)' \
|
12
|
-
'(' +
|
13
|
-
# IP address exclusion
|
14
|
-
# private & local networks
|
15
|
-
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' \
|
16
|
-
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' \
|
17
|
-
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
|
18
|
-
# IP address dotted notation octets
|
19
|
-
# excludes loopback network 0.0.0.0
|
20
|
-
# excludes reserved space >= 224.0.0.0
|
21
|
-
# excludes network & broacast addresses
|
22
|
-
# (first & last IP address of each class)
|
23
|
-
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' \
|
24
|
-
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' \
|
25
|
-
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' \
|
26
|
-
'|' +
|
27
|
-
# host name
|
28
|
-
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
|
29
|
-
# domain name
|
30
|
-
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' \
|
31
|
-
')' +
|
32
|
-
# port number
|
33
|
-
'(:\\d{2,5})?' +
|
34
|
-
# resource path
|
35
|
-
"(/[^\\s\"']*)?",
|
36
|
-
)
|
37
|
-
|
38
|
-
SKIP_RESOURCE_EXTENSIONS = [
|
39
|
-
'.map', # Ignore source maps.
|
40
|
-
'.gz', # Ignore gzipped files.
|
41
|
-
].freeze
|
42
|
-
|
43
|
-
MAX_FILESIZE_BYTES = 15 * 1024**2 # 15 MB.
|
44
|
-
|
45
|
-
attr_reader :page
|
46
|
-
|
47
|
-
# @param [Capybara::Session] page The Capybara page.
|
48
|
-
# @param [bool] include_iframes Include iframes in the snapshot
|
49
|
-
def initialize(options = {})
|
50
|
-
@page = options[:page]
|
51
|
-
@include_iframes = options[:include_iframes] || false
|
52
|
-
end
|
53
|
-
|
54
|
-
def build_resources
|
55
|
-
raise NotImplementedError, 'subclass must implement abstract method'
|
56
|
-
end
|
57
|
-
|
58
|
-
def snapshot_resources
|
59
|
-
raise NotImplementedError, 'subclass must implement abstract method'
|
60
|
-
end
|
61
|
-
|
62
|
-
# @private
|
63
|
-
def root_html_resource
|
64
|
-
Percy::Client::Resource.new(
|
65
|
-
current_path, is_root: true, mimetype: 'text/html', content: page.html,
|
66
|
-
)
|
67
|
-
end
|
68
|
-
|
69
|
-
# Transformed version of the current URL to be a relative path.
|
70
|
-
# This important because Rack::Test uses "www.example.com" as the actual current URL,
|
71
|
-
# which would force Percy to actually render example.com instead of the page. By always
|
72
|
-
# using a URL path as the resource URL, we guarantee that Percy will render what it's given.
|
73
|
-
#
|
74
|
-
# @private
|
75
|
-
def current_path
|
76
|
-
current_url = page.current_url
|
77
|
-
url_match = URL_REGEX.match(current_url)
|
78
|
-
return url_match[4] if url_match
|
79
|
-
|
80
|
-
# Special case: prepend a slash to the path to force a valid URL for things like
|
81
|
-
# "about:srcdoc" iframe srcdoc pages.
|
82
|
-
current_url = "/#{current_url}" if current_url[0] != '/'
|
83
|
-
|
84
|
-
current_url
|
85
|
-
end
|
86
|
-
|
87
|
-
# NOTES:
|
88
|
-
# - Doesn't handle multiple iframes with the same URL (`src` attribute)
|
89
|
-
# @private
|
90
|
-
def iframes_resources
|
91
|
-
return [] unless @include_iframes
|
92
|
-
|
93
|
-
resources = []
|
94
|
-
|
95
|
-
page.all(:css, 'iframe').each do |iframe_element|
|
96
|
-
iframe_url = iframe_element[:src]
|
97
|
-
root_page_host = page.current_host
|
98
|
-
begin
|
99
|
-
page.within_frame(iframe_element) do
|
100
|
-
next unless page.current_host == root_page_host
|
101
|
-
path = URI.parse(iframe_url).path
|
102
|
-
content = page.html
|
103
|
-
sha = Digest::SHA256.hexdigest(content)
|
104
|
-
resources <<
|
105
|
-
Percy::Client::Resource.new(
|
106
|
-
path,
|
107
|
-
content: content,
|
108
|
-
sha: sha,
|
109
|
-
mimetype: 'text/html',
|
110
|
-
)
|
111
|
-
end
|
112
|
-
rescue StandardError => e
|
113
|
-
# Skip frame not found errors. This library doesn't explicitly depend on Poltergeist,
|
114
|
-
# so we check the string class name.
|
115
|
-
raise e unless e.class.to_s == 'Capybara::Poltergeist::FrameNotFound' ||
|
116
|
-
e.class.to_s == 'Capybara::Poltergeist::TimeoutError'
|
117
|
-
end
|
118
|
-
end
|
119
|
-
|
120
|
-
resources
|
121
|
-
rescue ::Capybara::NotSupportedByDriverError
|
122
|
-
[]
|
123
|
-
end
|
124
|
-
|
125
|
-
def _resources_from_dir(root_dir, base_url: '/')
|
126
|
-
resources = []
|
127
|
-
|
128
|
-
_find_files(root_dir).each do |path|
|
129
|
-
# Skip certain extensions.
|
130
|
-
next if SKIP_RESOURCE_EXTENSIONS.include?(File.extname(path))
|
131
|
-
# Skip large files, these are hopefully downloads and not used in page rendering.
|
132
|
-
next if File.size(path) > MAX_FILESIZE_BYTES
|
133
|
-
|
134
|
-
# Replace the assets_dir with the base_url to generate the resource_url
|
135
|
-
resource_url = _uri_join(base_url, path.sub(root_dir.to_s, ''))
|
136
|
-
|
137
|
-
sha = Digest::SHA256.hexdigest(File.read(path))
|
138
|
-
|
139
|
-
resources << Percy::Client::Resource.new(resource_url, sha: sha, path: path)
|
140
|
-
end
|
141
|
-
|
142
|
-
resources
|
143
|
-
end
|
144
|
-
|
145
|
-
# A simplified version of Find.find that only returns files and follows symlinks.
|
146
|
-
def _find_files(*paths)
|
147
|
-
paths.flatten.map do |path|
|
148
|
-
path = Pathname.new(path)
|
149
|
-
path.file? ? [path.to_s] : _find_files(path.children)
|
150
|
-
end.flatten
|
151
|
-
end
|
152
|
-
|
153
|
-
def _uri_join(*paths)
|
154
|
-
# We must swap File::SEPARATOR for '/' here because on Windows File.join
|
155
|
-
# will use backslashes and this is a URL.
|
156
|
-
File.join(paths).gsub(File::SEPARATOR, '/')
|
157
|
-
end
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
@@ -1,52 +0,0 @@
|
|
1
|
-
require 'percy/capybara/loaders/sprockets_loader'
|
2
|
-
require 'set'
|
3
|
-
|
4
|
-
module Percy
|
5
|
-
module Capybara
|
6
|
-
module Loaders
|
7
|
-
class EmberCliRailsLoader < SprocketsLoader
|
8
|
-
attr_reader :mounted_apps
|
9
|
-
|
10
|
-
def initialize(mounted_apps, options = {})
|
11
|
-
super(options)
|
12
|
-
|
13
|
-
raise 'mounted_apps is required' unless mounted_apps
|
14
|
-
@mounted_apps = mounted_apps
|
15
|
-
end
|
16
|
-
|
17
|
-
def build_resources
|
18
|
-
resources = super # adds sprockets resources first
|
19
|
-
|
20
|
-
sprockets_resource_urls = resources.collect(&:resource_url)
|
21
|
-
loaded_resource_urls = Set.new(sprockets_resource_urls)
|
22
|
-
|
23
|
-
@mounted_apps.map do |app_name, mount_path|
|
24
|
-
# full path on disk to this ember app
|
25
|
-
# e.g. /Users/djones/Code/rails-ember-app/tmp/ember-cli/apps/frontend
|
26
|
-
dist_path = _dist_path_for_app(app_name)
|
27
|
-
|
28
|
-
resources_from_dir = _resources_from_dir(dist_path, base_url: mount_path)
|
29
|
-
|
30
|
-
resources_from_dir.each do |resource|
|
31
|
-
# avoid loading in duplicate resource_urls
|
32
|
-
next if loaded_resource_urls.include? resource.resource_url
|
33
|
-
|
34
|
-
resources << resource
|
35
|
-
loaded_resource_urls << resource.resource_url
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
resources
|
40
|
-
end
|
41
|
-
|
42
|
-
def _dist_path_for_app(app_name)
|
43
|
-
_ember_cli.apps[app_name].dist_path
|
44
|
-
end
|
45
|
-
|
46
|
-
def _ember_cli
|
47
|
-
EmberCli if defined?(EmberCli)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
require 'percy/capybara/loaders/base_loader'
|
2
|
-
require 'digest'
|
3
|
-
require 'find'
|
4
|
-
require 'pathname'
|
5
|
-
|
6
|
-
module Percy
|
7
|
-
module Capybara
|
8
|
-
module Loaders
|
9
|
-
# Resource loader that looks for resources in the specified folder.
|
10
|
-
class FilesystemLoader < BaseLoader
|
11
|
-
def initialize(options = {})
|
12
|
-
# @assets_dir should point to a _compiled_ static assets directory, not source assets.
|
13
|
-
@assets_dir = options[:assets_dir]
|
14
|
-
@base_url = options[:base_url] || '/'
|
15
|
-
|
16
|
-
raise ArgumentError, 'assets_dir is required' if @assets_dir.nil? || @assets_dir == ''
|
17
|
-
unless Pathname.new(@assets_dir).absolute?
|
18
|
-
raise ArgumentError, "assets_dir needs to be an absolute path. Received: #{@assets_dir}"
|
19
|
-
end
|
20
|
-
unless Dir.exist?(@assets_dir)
|
21
|
-
raise ArgumentError, "assets_dir provided was not found. Received: #{@assets_dir}"
|
22
|
-
end
|
23
|
-
|
24
|
-
super
|
25
|
-
end
|
26
|
-
|
27
|
-
def snapshot_resources
|
28
|
-
[root_html_resource]
|
29
|
-
end
|
30
|
-
|
31
|
-
def build_resources
|
32
|
-
_resources_from_dir(@assets_dir, base_url: @base_url)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
@@ -1,287 +0,0 @@
|
|
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 uses the native Capybara browser interface to discover resources.
|
9
|
-
# This loader uses JavaScript to discover page resources, so specs must be tagged with
|
10
|
-
# "js: true" because the default Rack::Test driver does not support executing JavaScript.
|
11
|
-
class NativeLoader < BaseLoader
|
12
|
-
PATH_REGEX = /\A\/[^\\s\"']*/
|
13
|
-
DATA_URL_REGEX = /\Adata:/
|
14
|
-
LOCAL_HOSTNAMES = [
|
15
|
-
'localhost',
|
16
|
-
'127.0.0.1',
|
17
|
-
'0.0.0.0',
|
18
|
-
].freeze
|
19
|
-
|
20
|
-
def initialize(options = {})
|
21
|
-
super(options)
|
22
|
-
|
23
|
-
@asset_hostnames = options[:asset_hostnames] || []
|
24
|
-
@assets_from_stylesheets = options[:include_assets_from_stylesheets] || :all
|
25
|
-
@assets_from_stylesheets = ->(_) { true } if @assets_from_stylesheets == :all
|
26
|
-
end
|
27
|
-
|
28
|
-
def snapshot_resources
|
29
|
-
resources = []
|
30
|
-
resources << root_html_resource
|
31
|
-
resources += _get_css_resources
|
32
|
-
resources += _get_image_resources
|
33
|
-
resources += iframes_resources
|
34
|
-
resources
|
35
|
-
end
|
36
|
-
|
37
|
-
def build_resources
|
38
|
-
[]
|
39
|
-
end
|
40
|
-
|
41
|
-
# @private
|
42
|
-
def _get_css_resources
|
43
|
-
resources = []
|
44
|
-
# Find all CSS resources.
|
45
|
-
# http://www.quirksmode.org/dom/w3c_css.html#access
|
46
|
-
script = <<-JS
|
47
|
-
function findStylesRecursively(stylesheet, css_urls) {
|
48
|
-
if (stylesheet.href) { // Skip stylesheet without hrefs (inline stylesheets).
|
49
|
-
css_urls.push(stylesheet.href);
|
50
|
-
|
51
|
-
// Remote stylesheet rules cannot be accessed because of the same-origin policy.
|
52
|
-
// Unfortunately, if you touch .cssRules in Selenium, it throws a JavascriptError
|
53
|
-
// with 'The operation is insecure'. To work around this, skip reading rules of
|
54
|
-
// remote stylesheets but still include them for fetching.
|
55
|
-
//
|
56
|
-
// TODO: If a remote stylesheet has an @import, it will be missing because we don't
|
57
|
-
// notice it here. We could potentially recursively fetch remote imports in
|
58
|
-
// ruby-land below.
|
59
|
-
var parser = document.createElement('a');
|
60
|
-
parser.href = stylesheet.href;
|
61
|
-
if (parser.host != window.location.host) {
|
62
|
-
return;
|
63
|
-
}
|
64
|
-
}
|
65
|
-
for (var i = 0; i < stylesheet.cssRules.length; i++) {
|
66
|
-
var rule = stylesheet.cssRules[i];
|
67
|
-
// Depth-first search, handle recursive @imports.
|
68
|
-
if (rule.styleSheet) {
|
69
|
-
findStylesRecursively(rule.styleSheet, css_urls);
|
70
|
-
}
|
71
|
-
}
|
72
|
-
}
|
73
|
-
|
74
|
-
var css_urls = [];
|
75
|
-
for (var i = 0; i < document.styleSheets.length; i++) {
|
76
|
-
findStylesRecursively(document.styleSheets[i], css_urls);
|
77
|
-
}
|
78
|
-
return css_urls;
|
79
|
-
JS
|
80
|
-
resource_urls = _evaluate_script(page, script)
|
81
|
-
urls_referred_by_css = []
|
82
|
-
|
83
|
-
resource_urls.each do |url|
|
84
|
-
next unless _should_include_url?(url)
|
85
|
-
response = _fetch_resource_url(url)
|
86
|
-
urls_referred_by_css.concat(_parse_urls_from_css(response.body))
|
87
|
-
_absolute_url_to_relative!(url, _current_host_port)
|
88
|
-
next unless response
|
89
|
-
resources << Percy::Client::Resource.new(
|
90
|
-
url, mimetype: 'text/css', content: response.body,
|
91
|
-
)
|
92
|
-
end
|
93
|
-
@urls_referred_by_css = urls_referred_by_css
|
94
|
-
resources
|
95
|
-
end
|
96
|
-
private :_get_css_resources
|
97
|
-
|
98
|
-
# @private
|
99
|
-
def _get_image_resources
|
100
|
-
resources = []
|
101
|
-
image_urls = Set.new
|
102
|
-
|
103
|
-
# Find all image tags on the page.
|
104
|
-
page.all('img').each do |image_element|
|
105
|
-
srcs = []
|
106
|
-
srcs << image_element[:src] unless image_element[:src].nil?
|
107
|
-
|
108
|
-
srcset_raw_urls = image_element[:srcset] || ''
|
109
|
-
temp_urls = srcset_raw_urls.split(',')
|
110
|
-
temp_urls.each do |temp_url|
|
111
|
-
srcs << temp_url.split(' ').first
|
112
|
-
end
|
113
|
-
|
114
|
-
srcs.each do |url|
|
115
|
-
image_urls << url
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
raw_image_urls = _evaluate_script(page, _find_all_css_loaded_background_image_js)
|
120
|
-
raw_image_urls.each do |raw_image_url|
|
121
|
-
temp_urls = raw_image_url.scan(/url\(["']?(.*?)["']?\)/)
|
122
|
-
# background-image can accept multiple url()s, so temp_urls is an array of URLs.
|
123
|
-
temp_urls.each do |temp_url|
|
124
|
-
url = temp_url[0]
|
125
|
-
image_urls << url
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
if @assets_from_stylesheets && @assets_from_stylesheets != :none
|
130
|
-
image_urls.merge(@urls_referred_by_css.select { |path| @assets_from_stylesheets[path] })
|
131
|
-
end
|
132
|
-
|
133
|
-
image_urls.each do |image_url|
|
134
|
-
# If url references are blank, browsers will often fill them with the current page's
|
135
|
-
# URL, which makes no sense and will never be renderable. Strip these.
|
136
|
-
next if image_url == current_path \
|
137
|
-
|| image_url == page.current_url \
|
138
|
-
|| image_url.strip.empty?
|
139
|
-
|
140
|
-
# Make the resource URL absolute to the current page. If it is already absolute, this
|
141
|
-
# will have no effect.
|
142
|
-
resource_url = URI.join(page.current_url, image_url).to_s
|
143
|
-
|
144
|
-
# Skip duplicates.
|
145
|
-
next if resources.find { |r| r.resource_url == resource_url }
|
146
|
-
|
147
|
-
next unless _should_include_url?(resource_url)
|
148
|
-
|
149
|
-
# Fetch the images.
|
150
|
-
# TODO(fotinakis): this can be pretty inefficient for image-heavy pages because the
|
151
|
-
# browser has already loaded them once and this fetch cannot easily leverage the
|
152
|
-
# browser's cache. However, often these images are probably local resources served by a
|
153
|
-
# development server, so it may not be so bad. Re-evaluate if this becomes an issue.
|
154
|
-
response = _fetch_resource_url(resource_url)
|
155
|
-
_absolute_url_to_relative!(resource_url, _current_host_port)
|
156
|
-
next unless response
|
157
|
-
|
158
|
-
resources << Percy::Client::Resource.new(
|
159
|
-
resource_url, mimetype: response.content_type, content: response.body,
|
160
|
-
)
|
161
|
-
end
|
162
|
-
resources
|
163
|
-
end
|
164
|
-
private :_get_image_resources
|
165
|
-
|
166
|
-
# @private
|
167
|
-
def _find_all_css_loaded_background_image_js
|
168
|
-
<<-JS
|
169
|
-
var raw_image_urls = [];
|
170
|
-
|
171
|
-
var tags = document.getElementsByTagName('*');
|
172
|
-
var el;
|
173
|
-
var rawValue;
|
174
|
-
|
175
|
-
for (var i = 0; i < tags.length; i++) {
|
176
|
-
el = tags[i];
|
177
|
-
if (el.currentStyle) {
|
178
|
-
rawValue = el.currentStyle['backgroundImage'];
|
179
|
-
} else if (window.getComputedStyle) {
|
180
|
-
rawValue = window.getComputedStyle(el).getPropertyValue('background-image');
|
181
|
-
}
|
182
|
-
if (!rawValue || rawValue === "none") {
|
183
|
-
continue;
|
184
|
-
} else {
|
185
|
-
raw_image_urls.push(rawValue);
|
186
|
-
}
|
187
|
-
}
|
188
|
-
return raw_image_urls;
|
189
|
-
JS
|
190
|
-
end
|
191
|
-
|
192
|
-
# @private
|
193
|
-
def _parse_urls_from_css(css_content)
|
194
|
-
css_content.scan(/url\(([^\)]+)\)/)
|
195
|
-
.map { |i| _remove_quotes(i.first) }
|
196
|
-
.select { |path| _should_include_url?(path) }
|
197
|
-
.map { |path| _remove_hash_from_url(path) }
|
198
|
-
end
|
199
|
-
|
200
|
-
# @private
|
201
|
-
def _remove_hash_from_url(string)
|
202
|
-
if /^(?<url_base>.+)?\#[^#]+$/ =~ string
|
203
|
-
if url_base.end_with?('?')
|
204
|
-
url_base[0...-1]
|
205
|
-
else
|
206
|
-
url_base
|
207
|
-
end
|
208
|
-
else
|
209
|
-
string
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
# @private
|
214
|
-
def _remove_quotes(string)
|
215
|
-
if string.length >= 2 && (string[0] == string[-1]) && ['"', "'"].include?(string[0])
|
216
|
-
string[1...-1]
|
217
|
-
else
|
218
|
-
string
|
219
|
-
end
|
220
|
-
end
|
221
|
-
|
222
|
-
# @private
|
223
|
-
def _fetch_resource_url(url)
|
224
|
-
response = Percy::Capybara::HttpFetcher.fetch(url)
|
225
|
-
unless response
|
226
|
-
STDERR.puts '[percy] Warning: failed to fetch page resource, ' \
|
227
|
-
"this might be a bug: #{url}"
|
228
|
-
return nil
|
229
|
-
end
|
230
|
-
response
|
231
|
-
end
|
232
|
-
private :_fetch_resource_url
|
233
|
-
|
234
|
-
# @private
|
235
|
-
def _evaluate_script(page, script)
|
236
|
-
script = <<-JS
|
237
|
-
(function() {
|
238
|
-
#{script}
|
239
|
-
})();
|
240
|
-
JS
|
241
|
-
page.evaluate_script(script)
|
242
|
-
end
|
243
|
-
private :_evaluate_script
|
244
|
-
|
245
|
-
# @private
|
246
|
-
def _should_include_url?(url)
|
247
|
-
# It is a URL or a path, but not a data URI.
|
248
|
-
url_match = URL_REGEX.match(url)
|
249
|
-
data_url_match = DATA_URL_REGEX.match(url)
|
250
|
-
result = (url_match || PATH_REGEX.match(url)) && !data_url_match
|
251
|
-
|
252
|
-
# Is not a remote URL.
|
253
|
-
if url_match && !data_url_match
|
254
|
-
host = url_match[2]
|
255
|
-
result = asset_hostnames.include?(host) || _same_server?(url, _current_host_port)
|
256
|
-
end
|
257
|
-
|
258
|
-
!!result
|
259
|
-
end
|
260
|
-
|
261
|
-
# @private
|
262
|
-
def _current_host_port
|
263
|
-
url_match = URL_REGEX.match(page.current_url)
|
264
|
-
url_match[1] + url_match[2] + (url_match[3] || '')
|
265
|
-
end
|
266
|
-
private :_current_host_port
|
267
|
-
|
268
|
-
# @private
|
269
|
-
def _same_server?(url, host_port)
|
270
|
-
url.start_with?(host_port + '/') || url == host_port
|
271
|
-
end
|
272
|
-
private :_same_server?
|
273
|
-
|
274
|
-
# @private
|
275
|
-
def _absolute_url_to_relative!(url, host_port)
|
276
|
-
url.gsub!(host_port + '/', '/') if url.start_with?(host_port + '/')
|
277
|
-
end
|
278
|
-
private :_absolute_url_to_relative!
|
279
|
-
|
280
|
-
def asset_hostnames
|
281
|
-
LOCAL_HOSTNAMES + @asset_hostnames
|
282
|
-
end
|
283
|
-
private :asset_hostnames
|
284
|
-
end
|
285
|
-
end
|
286
|
-
end
|
287
|
-
end
|