eyes_selenium 3.15.29 → 3.15.30
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/applitools/selenium/css_parser/find_embedded_resources.rb +69 -0
- data/lib/applitools/selenium/dom_capture/dom_capture.rb +148 -94
- data/lib/applitools/selenium/dom_capture/dom_capture_script.rb +448 -89
- data/lib/applitools/selenium/render_resources.rb +5 -4
- data/lib/applitools/selenium/selenium_eyes.rb +1 -1
- data/lib/applitools/selenium/visual_grid/render_task.rb +50 -18
- data/lib/applitools/selenium/visual_grid/resource_cache.rb +17 -9
- data/lib/applitools/selenium/visual_grid/thread_pool.rb +2 -3
- data/lib/applitools/selenium/visual_grid/vg_resource.rb +21 -9
- data/lib/applitools/selenium/visual_grid/visual_grid_eyes.rb +30 -19
- data/lib/applitools/version.rb +1 -1
- data/lib/eyes_selenium.rb +2 -0
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f0a153b343d111f258a0628e29043634a0a000a6b28a0f46e338f2ee61d315dc
|
4
|
+
data.tar.gz: 056e78da5023f2f69d3056484b5b06c5c710bcaab45fce6c6af28af81cce1e4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc23f5c1c1443b7a5d0c1b1296e9d75fdf0a0766ded52eff8321bfbabe224ba1215755a4d76b4020526233c75ec7fcd5aa9433669257111b9171a6c32e8cd002
|
7
|
+
data.tar.gz: 41131e9e97acf4848ee15a07aae65cc38c7f86ddc5b94b6054f5086a9f77280bad76ac8c66aba2105f321b9680724d1dd93e8bd174154693ecac31c2bb2f37df
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'crass'
|
2
|
+
|
3
|
+
module Applitools
|
4
|
+
module Selenium
|
5
|
+
module CssParser
|
6
|
+
class FindEmbeddedResources
|
7
|
+
class << self
|
8
|
+
end
|
9
|
+
|
10
|
+
class CssParseError < Applitools::EyesError; end
|
11
|
+
|
12
|
+
attr_accessor :css
|
13
|
+
|
14
|
+
def initialize(css)
|
15
|
+
self.css = css
|
16
|
+
end
|
17
|
+
|
18
|
+
def imported_css
|
19
|
+
fetch_urls(import_rules)
|
20
|
+
end
|
21
|
+
|
22
|
+
def fonts
|
23
|
+
fetch_urls(font_face_rules)
|
24
|
+
end
|
25
|
+
|
26
|
+
def images
|
27
|
+
fetch_urls(images_rules)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def url(node)
|
33
|
+
url = node[:tokens].select { |t| t[:node] == :url }.first
|
34
|
+
return url[:value] if url && !url.empty?
|
35
|
+
url = node[:tokens].select { |t| t[:node] == :function && t[:value] == 'url' }.first
|
36
|
+
url_index = node[:tokens].index(url)
|
37
|
+
url_string_node = url_index && node[:tokens][url_index + 1]
|
38
|
+
url_string_node && url_string_node[:node] == :string && !url_string_node[:value].empty? && url_string_node[:value]
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch_urls(nodes)
|
42
|
+
nodes.map { |n| url(n) }.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
def import_rules
|
46
|
+
css_nodes.select { |n| n[:node] == :at_rule && n[:name] == 'import' }
|
47
|
+
end
|
48
|
+
|
49
|
+
def font_face_rules
|
50
|
+
css_nodes.select { |n| n[:node] == :at_rule && n[:name] == 'font-face' }
|
51
|
+
end
|
52
|
+
|
53
|
+
def images_rules
|
54
|
+
css_nodes.select { |n| n[:node] == :style_rule }.map { |n| n[:children] }
|
55
|
+
.flatten
|
56
|
+
.select { |n| n[:node] == :property && (n[:name] == 'background' || n[:name] == 'background-image') }
|
57
|
+
end
|
58
|
+
|
59
|
+
def css_nodes
|
60
|
+
@css_nodes ||= parse_nodes
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse_nodes
|
64
|
+
Crass.parse css
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,113 +1,167 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
CSS_DOWNLOAD_TIMEOUT = 2 # 2 seconds
|
11
|
-
DOM_CAPTURE_TIMEOUT = 10 # 10 seconds
|
12
|
-
|
13
|
-
extend self
|
14
|
-
|
15
|
-
def get_window_dom(driver, logger)
|
16
|
-
args_obj = {
|
17
|
-
'styleProps' => %w(
|
18
|
-
background-color background-image background-size color border-width
|
19
|
-
border-color border-style padding margin
|
20
|
-
),
|
21
|
-
'attributeProps' => nil,
|
22
|
-
'rectProps' => %w(width height top left),
|
23
|
-
'ignoredTagNames' => %w(HEAD SCRIPT)
|
24
|
-
}
|
25
|
-
dom_tree = ''
|
26
|
-
if Timeout.respond_to?(:timeout)
|
27
|
-
Timeout.timeout(DOM_CAPTURE_TIMEOUT) do
|
28
|
-
dom_tree = driver.execute_script(Applitools::Selenium::DomCapture::DOM_CAPTURE_SCRIPT, args_obj)
|
29
|
-
get_frame_dom(driver, { 'childNodes' => [dom_tree], 'tagName' => 'OUTER_HTML' }, logger)
|
30
|
-
end
|
31
|
-
else
|
32
|
-
timeout(DOM_CAPTURE_TIMEOUT) do
|
33
|
-
dom_tree = driver.execute_script(Applitools::Selenium::DomCapture::DOM_CAPTURE_SCRIPT, args_obj)
|
34
|
-
get_frame_dom(driver, { 'childNodes' => [dom_tree], 'tagName' => 'OUTER_HTML' }, logger)
|
1
|
+
module Applitools
|
2
|
+
module Selenium
|
3
|
+
module DomCapture
|
4
|
+
extend self
|
5
|
+
DOM_EXTRACTION_TIMEOUT = 300 #seconds
|
6
|
+
def full_window_dom(driver, server_connector, logger, position_provider = nil)
|
7
|
+
return get_dom(driver, server_connector, logger) unless position_provider
|
8
|
+
scroll_top_and_return_back(position_provider) do
|
9
|
+
get_dom(driver, server_connector, logger)
|
35
10
|
end
|
36
11
|
end
|
37
|
-
dom_tree
|
38
|
-
end
|
39
12
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
driver.switch_to.parent_frame
|
51
|
-
frame_index += 1
|
13
|
+
def get_dom(driver, server_connector, logger)
|
14
|
+
original_frame_chain = driver.frame_chain
|
15
|
+
dom = get_frame_dom(driver, server_connector, logger)
|
16
|
+
unless original_frame_chain.empty?
|
17
|
+
driver.switch_to.default_content
|
18
|
+
driver.switch_to.frames(frame_chain: original_frame_chain)
|
19
|
+
end
|
20
|
+
# CSS processing
|
21
|
+
|
22
|
+
dom
|
52
23
|
end
|
53
|
-
end
|
54
24
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
25
|
+
def get_frame_dom(driver, server_connector, logger)
|
26
|
+
logger.info 'Trying to get DOM from driver'
|
27
|
+
start_time = Time.now
|
28
|
+
script_response = nil
|
29
|
+
loop do
|
30
|
+
result_as_string = driver.execute_script(CAPTURE_FRAME_SCRIPT + ' return __captureDomAndPoll();')
|
31
|
+
script_response = Oj.load(result_as_string)
|
32
|
+
status = script_response['status']
|
33
|
+
break if status == 'SUCCESS'
|
34
|
+
raise Applitools::EyesError, 'DOM extraction timeout!' if Time.now - start_time > DOM_EXTRACTION_TIMEOUT
|
35
|
+
raise Applitools::EyesError, "DOM extraction error: #{script_response['error']}" if script_response['error']
|
36
|
+
sleep(0.2)
|
37
|
+
end
|
38
|
+
response_lines = script_response['value'].split /\r?\n/
|
39
|
+
separators = Oj.load(response_lines.shift)
|
40
|
+
missing_css_list = []
|
41
|
+
missing_frame_list = []
|
42
|
+
data = []
|
43
|
+
|
44
|
+
puts separators
|
45
|
+
|
46
|
+
blocks = DomParts.new(missing_css_list, missing_frame_list, data)
|
47
|
+
collector = blocks.collectors.next
|
48
|
+
response_lines.each do |line|
|
49
|
+
if line == separators['separator']
|
50
|
+
collector = blocks.collectors.next
|
62
51
|
else
|
63
|
-
|
64
|
-
iterate_child_nodes.call(node['childNodes']) unless node['childNodes'].nil?
|
52
|
+
collector << line
|
65
53
|
end
|
66
54
|
end
|
55
|
+
logger.info "Missing CSS: #{missing_css_list.count}"
|
56
|
+
logger.info "Missing frames: #{missing_frame_list.count}"
|
57
|
+
#fetch_css_files(missing_css_list)
|
58
|
+
|
59
|
+
frame_data = recurse_frames(driver, server_connector, logger, missing_frame_list)
|
60
|
+
result = replace(separators['iframeStartToken'], separators['iframeEndToken'], data.first, frame_data)
|
61
|
+
css_data = fetch_css_files(missing_css_list, server_connector)
|
62
|
+
replace(separators['cssStartToken'], separators['cssEndToken'], result, css_data)
|
63
|
+
rescue StandardError
|
64
|
+
logger.error(e.class)
|
65
|
+
logger.error(e.message)
|
66
|
+
logger.error(e)
|
67
|
+
return ''
|
67
68
|
end
|
68
|
-
iterate_child_nodes.call(child_nodes)
|
69
|
-
end
|
70
69
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
else
|
89
|
-
timeout(CSS_DOWNLOAD_TIMEOUT) do
|
90
|
-
css_string, = parser.send(:read_remote_file, url)
|
91
|
-
css_items[i] = [css_string, { base_uri: url }]
|
92
|
-
end
|
70
|
+
def fetch_css_files(missing_css_list, server_connector)
|
71
|
+
result = {}
|
72
|
+
missing_css_list.each do |url|
|
73
|
+
next if url.empty?
|
74
|
+
next if /^blob:/ =~ url
|
75
|
+
|
76
|
+
begin
|
77
|
+
missing_css_response = server_connector.download_resource(url)
|
78
|
+
response_headers = missing_css_response.headers
|
79
|
+
raise Applitools::EyesError, "Wrong response header: #{response_headers['content-type']}" unless
|
80
|
+
%r{^text/css.*}i =~ response_headers['content-type']
|
81
|
+
|
82
|
+
css = missing_css_response.body
|
83
|
+
|
84
|
+
found_and_missing_css = Applitools::Selenium::CssParser::FindEmbeddedResources.new(css).imported_css.map do |found_url|
|
85
|
+
base_url(url).merge(found_url).to_s
|
93
86
|
end
|
87
|
+
fetch_css_files(found_and_missing_css, server_connector).each do |_k, v|
|
88
|
+
css += v
|
89
|
+
end
|
90
|
+
|
91
|
+
result[url] = Oj.dump("\n/** #{url} **/\n" + css).gsub(/^\"|\"$/, '')
|
92
|
+
rescue StandardError
|
93
|
+
result[url] = ''
|
94
94
|
end
|
95
95
|
end
|
96
|
+
result
|
96
97
|
end
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
98
|
+
|
99
|
+
def recurse_frames(driver, server_connector, logger, missing_frame_list)
|
100
|
+
return if missing_frame_list.empty?
|
101
|
+
frame_data = {}
|
102
|
+
frame_chain = driver.frame_chain
|
103
|
+
origin_location = driver.execute_script('return document.location.href')
|
104
|
+
missing_frame_list.each do |missing_frame_line|
|
105
|
+
logger.info "Switching to frame line: #{missing_frame_line}"
|
106
|
+
missing_frame_line.split(/,/).each do |xpath|
|
107
|
+
logger.info "switching to specific frame: #{xpath}"
|
108
|
+
frame_element = driver.find_element(:xpath, xpath)
|
109
|
+
frame_src = frame_element.attribute('src')
|
110
|
+
driver.switch_to.frame(frame_element)
|
111
|
+
logger.info "Switched to frame ( #{xpath} ) with src( #{frame_src} )"
|
112
|
+
end
|
113
|
+
location_after_switch = driver.execute_script('return document.location.href')
|
114
|
+
|
115
|
+
if origin_location == location_after_switch
|
116
|
+
logger.info "Switch to frame (#{missing_frame_line}) failed"
|
117
|
+
frame_data[missing_frame_line] = ''
|
118
|
+
else
|
119
|
+
result = get_frame_dom(driver, server_connector, logger)
|
120
|
+
frame_data[missing_frame_line] = result
|
121
|
+
end
|
122
|
+
end
|
123
|
+
driver.switch_to.default_content
|
124
|
+
driver.switch_to.frames(frame_chain: frame_chain)
|
125
|
+
frame_data
|
126
|
+
end
|
127
|
+
|
128
|
+
def scroll_top_and_return_back(position_provider)
|
129
|
+
original_position = position_provider.current_position
|
130
|
+
return yield if block_given? && original_position.nil?
|
131
|
+
position_provider.scroll_to Applitools::Location.new(0, 0)
|
132
|
+
result = yield if block_given?
|
133
|
+
position_provider.scroll_to original_position
|
134
|
+
result
|
135
|
+
end
|
136
|
+
|
137
|
+
def replace(open_token, close_token, input, replacements)
|
138
|
+
pattern = /#{open_token}(?<key>.+?)#{close_token}/
|
139
|
+
input.gsub(pattern) { |_m| replacements[Regexp.last_match(1)] }
|
105
140
|
end
|
106
141
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
142
|
+
def base_url(url)
|
143
|
+
uri = URI.parse(url)
|
144
|
+
uri.query = uri.fragment = nil;
|
145
|
+
uri.path = ''
|
146
|
+
uri
|
147
|
+
end
|
148
|
+
|
149
|
+
class DomParts
|
150
|
+
attr_accessor :dom_part_collectors
|
151
|
+
def initialize(*args)
|
152
|
+
self.dom_part_collectors = args
|
153
|
+
@index = 0
|
154
|
+
end
|
155
|
+
|
156
|
+
def collectors
|
157
|
+
@collectors ||= Enumerator.new(dom_part_collectors.length) do |y|
|
158
|
+
loop do
|
159
|
+
y << dom_part_collectors[@index]
|
160
|
+
@index += 1
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
111
165
|
end
|
112
166
|
end
|
113
167
|
end
|
@@ -1,103 +1,462 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
module Applitools
|
2
|
+
module Selenium
|
3
|
+
module DomCapture
|
4
|
+
CAPTURE_FRAME_SCRIPT = <<'SCRIPT'
|
5
|
+
/* @applitools/dom-capture@7.0.14 */
|
6
|
+
|
7
|
+
function __captureDomAndPoll() {
|
8
|
+
var captureDomAndPoll = (function () {
|
9
|
+
'use strict';
|
10
|
+
|
11
|
+
const styleProps = [
|
12
|
+
'background-repeat',
|
13
|
+
'background-origin',
|
14
|
+
'background-position',
|
15
|
+
'background-color',
|
16
|
+
'background-image',
|
17
|
+
'background-size',
|
18
|
+
'border-width',
|
19
|
+
'border-color',
|
20
|
+
'border-style',
|
21
|
+
'color',
|
22
|
+
'display',
|
23
|
+
'font-size',
|
24
|
+
'line-height',
|
25
|
+
'margin',
|
26
|
+
'opacity',
|
27
|
+
'overflow',
|
28
|
+
'padding',
|
29
|
+
'visibility',
|
30
|
+
];
|
31
|
+
|
32
|
+
const rectProps = ['width', 'height', 'top', 'left'];
|
33
|
+
|
34
|
+
const ignoredTagNames = ['HEAD', 'SCRIPT'];
|
35
|
+
|
36
|
+
var defaultDomProps = {
|
37
|
+
styleProps,
|
38
|
+
rectProps,
|
39
|
+
ignoredTagNames,
|
40
|
+
};
|
41
|
+
|
42
|
+
const bgImageRe = /url\((?!['"]?:)['"]?([^'")]*)['"]?\)/;
|
43
|
+
|
44
|
+
function getBackgroundImageUrl(cssText) {
|
45
|
+
const match = cssText ? cssText.match(bgImageRe) : undefined;
|
46
|
+
return match ? match[1] : match;
|
47
|
+
}
|
48
|
+
|
49
|
+
var getBackgroundImageUrl_1 = getBackgroundImageUrl;
|
50
|
+
|
51
|
+
const psetTimeout = t =>
|
52
|
+
new Promise(res => {
|
53
|
+
setTimeout(res, t);
|
54
|
+
});
|
55
|
+
|
56
|
+
async function getImageSizes({bgImages, timeout = 5000, Image = window.Image}) {
|
57
|
+
return (await Promise.all(
|
58
|
+
Array.from(bgImages).map(url =>
|
59
|
+
Promise.race([
|
60
|
+
new Promise(resolve => {
|
61
|
+
const img = new Image();
|
62
|
+
img.onload = () => resolve({url, width: img.naturalWidth, height: img.naturalHeight});
|
63
|
+
img.onerror = () => resolve();
|
64
|
+
img.src = url;
|
65
|
+
}),
|
66
|
+
psetTimeout(timeout),
|
67
|
+
]),
|
68
|
+
),
|
69
|
+
)).reduce((images, curr) => {
|
70
|
+
if (curr) {
|
71
|
+
images[curr.url] = {width: curr.width, height: curr.height};
|
72
|
+
}
|
73
|
+
return images;
|
74
|
+
}, {});
|
75
|
+
}
|
76
|
+
|
77
|
+
var getImageSizes_1 = getImageSizes;
|
78
|
+
|
79
|
+
function genXpath(el) {
|
80
|
+
if (!el.ownerDocument) return ''; // this is the document node
|
81
|
+
|
82
|
+
let xpath = '',
|
83
|
+
currEl = el,
|
84
|
+
doc = el.ownerDocument,
|
85
|
+
frameElement = doc.defaultView.frameElement;
|
86
|
+
while (currEl !== doc) {
|
87
|
+
xpath = `${currEl.tagName}[${getIndex(currEl)}]/${xpath}`;
|
88
|
+
currEl = currEl.parentNode;
|
89
|
+
}
|
90
|
+
if (frameElement) {
|
91
|
+
xpath = `${genXpath(frameElement)},${xpath}`;
|
92
|
+
}
|
93
|
+
return xpath.replace(/\/$/, '');
|
94
|
+
}
|
95
|
+
|
96
|
+
function getIndex(el) {
|
97
|
+
return (
|
98
|
+
Array.prototype.filter
|
99
|
+
.call(el.parentNode.childNodes, node => node.tagName === el.tagName)
|
100
|
+
.indexOf(el) + 1
|
101
|
+
);
|
102
|
+
}
|
103
|
+
|
104
|
+
var genXpath_1 = genXpath;
|
105
|
+
|
106
|
+
function absolutizeUrl(url, absoluteUrl) {
|
107
|
+
return new URL(url, absoluteUrl).href;
|
108
|
+
}
|
109
|
+
|
110
|
+
var absolutizeUrl_1 = absolutizeUrl;
|
111
|
+
|
112
|
+
function makeGetBundledCssFromCssText({
|
113
|
+
parseCss,
|
114
|
+
CSSImportRule,
|
115
|
+
absolutizeUrl,
|
116
|
+
fetchCss,
|
117
|
+
unfetchedToken,
|
118
|
+
}) {
|
119
|
+
return async function getBundledCssFromCssText(cssText, resourceUrl) {
|
120
|
+
let unfetchedResources;
|
121
|
+
let bundledCss = '';
|
122
|
+
|
123
|
+
try {
|
124
|
+
const styleSheet = parseCss(cssText);
|
125
|
+
for (const rule of Array.from(styleSheet.cssRules)) {
|
126
|
+
if (rule instanceof CSSImportRule) {
|
127
|
+
const nestedUrl = absolutizeUrl(rule.href, resourceUrl);
|
128
|
+
const nestedResource = await fetchCss(nestedUrl);
|
129
|
+
if (nestedResource !== undefined) {
|
130
|
+
const {
|
131
|
+
bundledCss: nestedCssText,
|
132
|
+
unfetchedResources: nestedUnfetchedResources,
|
133
|
+
} = await getBundledCssFromCssText(nestedResource, nestedUrl);
|
134
|
+
|
135
|
+
nestedUnfetchedResources && (unfetchedResources = new Set(nestedUnfetchedResources));
|
136
|
+
bundledCss = `${nestedCssText}${bundledCss}`;
|
137
|
+
} else {
|
138
|
+
unfetchedResources = new Set([nestedUrl]);
|
139
|
+
bundledCss = `\n${unfetchedToken}${nestedUrl}${unfetchedToken}`;
|
140
|
+
}
|
12
141
|
}
|
13
|
-
}
|
142
|
+
}
|
143
|
+
} catch (ex) {
|
144
|
+
console.log(`error during getBundledCssFromCssText, resourceUrl=${resourceUrl}`, ex);
|
14
145
|
}
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
146
|
+
|
147
|
+
bundledCss = `${bundledCss}${getCss(cssText, resourceUrl)}`;
|
148
|
+
|
149
|
+
return {
|
150
|
+
bundledCss,
|
151
|
+
unfetchedResources,
|
152
|
+
};
|
153
|
+
};
|
154
|
+
}
|
155
|
+
|
156
|
+
function getCss(newText, url) {
|
157
|
+
return `\n/** ${url} **/\n${newText}`;
|
158
|
+
}
|
159
|
+
|
160
|
+
var getBundledCssFromCssText = makeGetBundledCssFromCssText;
|
161
|
+
|
162
|
+
function parseCss(styleContent) {
|
163
|
+
var doc = document.implementation.createHTMLDocument(''),
|
164
|
+
styleElement = doc.createElement('style');
|
165
|
+
styleElement.textContent = styleContent;
|
166
|
+
// the style will only be parsed once it is added to a document
|
167
|
+
doc.body.appendChild(styleElement);
|
168
|
+
|
169
|
+
return styleElement.sheet;
|
170
|
+
}
|
171
|
+
|
172
|
+
var parseCss_1 = parseCss;
|
173
|
+
|
174
|
+
function makeFetchCss(fetch) {
|
175
|
+
return async function fetchCss(url) {
|
176
|
+
try {
|
177
|
+
const response = await fetch(url, {cache: 'force-cache'});
|
178
|
+
if (response.ok) {
|
179
|
+
return await response.text();
|
25
180
|
}
|
26
|
-
|
27
|
-
|
181
|
+
console.log('/failed to fetch (status ' + response.status + ') css from: ' + url + '/');
|
182
|
+
} catch (err) {
|
183
|
+
console.log('/failed to fetch (error ' + err.toString() + ') css from: ' + url + '/');
|
184
|
+
}
|
185
|
+
};
|
186
|
+
}
|
187
|
+
|
188
|
+
var fetchCss = makeFetchCss;
|
189
|
+
|
190
|
+
function makeExtractCssFromNode({fetchCss, absolutizeUrl}) {
|
191
|
+
return async function extractCssFromNode(node, baseUrl) {
|
192
|
+
let cssText, resourceUrl, isUnfetched;
|
193
|
+
if (isStyleElement(node)) {
|
194
|
+
cssText = Array.from(node.childNodes)
|
195
|
+
.map(node => node.nodeValue)
|
196
|
+
.join('');
|
197
|
+
resourceUrl = baseUrl;
|
198
|
+
} else if (isLinkToStyleSheet(node)) {
|
199
|
+
resourceUrl = absolutizeUrl(getHrefAttr(node), baseUrl);
|
200
|
+
cssText = await fetchCss(resourceUrl);
|
201
|
+
if (cssText === undefined) {
|
202
|
+
isUnfetched = true;
|
28
203
|
}
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
204
|
+
}
|
205
|
+
return {cssText, resourceUrl, isUnfetched};
|
206
|
+
};
|
207
|
+
}
|
208
|
+
|
209
|
+
function isStyleElement(node) {
|
210
|
+
return node.nodeName && node.nodeName.toUpperCase() === 'STYLE';
|
211
|
+
}
|
212
|
+
|
213
|
+
function getHrefAttr(node) {
|
214
|
+
const attr = Array.from(node.attributes).find(attr => attr.name.toLowerCase() === 'href');
|
215
|
+
return attr && attr.value;
|
216
|
+
}
|
217
|
+
|
218
|
+
function isLinkToStyleSheet(node) {
|
219
|
+
return (
|
220
|
+
node.nodeName &&
|
221
|
+
node.nodeName.toUpperCase() === 'LINK' &&
|
222
|
+
node.attributes &&
|
223
|
+
Array.from(node.attributes).find(
|
224
|
+
attr => attr.name.toLowerCase() === 'rel' && attr.value.toLowerCase() === 'stylesheet',
|
225
|
+
)
|
226
|
+
);
|
227
|
+
}
|
228
|
+
|
229
|
+
var extractCssFromNode = makeExtractCssFromNode;
|
230
|
+
|
231
|
+
function makeCaptureNodeCss({extractCssFromNode, getBundledCssFromCssText, unfetchedToken}) {
|
232
|
+
return async function captureNodeCss(node, baseUrl) {
|
233
|
+
const {resourceUrl, cssText, isUnfetched} = await extractCssFromNode(node, baseUrl);
|
234
|
+
|
235
|
+
let unfetchedResources;
|
236
|
+
let bundledCss = '';
|
237
|
+
if (cssText) {
|
238
|
+
const {
|
239
|
+
bundledCss: nestedCss,
|
240
|
+
unfetchedResources: nestedUnfetched,
|
241
|
+
} = await getBundledCssFromCssText(cssText, resourceUrl);
|
242
|
+
|
243
|
+
bundledCss += nestedCss;
|
244
|
+
unfetchedResources = new Set(nestedUnfetched);
|
245
|
+
} else if (isUnfetched) {
|
246
|
+
bundledCss += `${unfetchedToken}${resourceUrl}${unfetchedToken}`;
|
247
|
+
unfetchedResources = new Set([resourceUrl]);
|
248
|
+
}
|
249
|
+
return {bundledCss, unfetchedResources};
|
250
|
+
};
|
251
|
+
}
|
252
|
+
|
253
|
+
var captureNodeCss = makeCaptureNodeCss;
|
254
|
+
|
255
|
+
const NODE_TYPES = {
|
256
|
+
ELEMENT: 1,
|
257
|
+
TEXT: 3,
|
258
|
+
};
|
259
|
+
const API_VERSION = '1.0.0';
|
260
|
+
|
261
|
+
async function captureFrame(
|
262
|
+
{styleProps, rectProps, ignoredTagNames} = defaultDomProps,
|
263
|
+
doc = document,
|
264
|
+
) {
|
265
|
+
const start = Date.now();
|
266
|
+
const unfetchedResources = new Set();
|
267
|
+
const iframeCors = [];
|
268
|
+
const iframeToken = '@@@@@';
|
269
|
+
const unfetchedToken = '#####';
|
270
|
+
const separator = '-----';
|
271
|
+
|
272
|
+
const fetchCss$$1 = fetchCss(fetch);
|
273
|
+
const getBundledCssFromCssText$$1 = getBundledCssFromCssText({
|
274
|
+
parseCss: parseCss_1,
|
275
|
+
CSSImportRule,
|
276
|
+
fetchCss: fetchCss$$1,
|
277
|
+
absolutizeUrl: absolutizeUrl_1,
|
278
|
+
unfetchedToken,
|
279
|
+
});
|
280
|
+
const extractCssFromNode$$1 = extractCssFromNode({fetchCss: fetchCss$$1, absolutizeUrl: absolutizeUrl_1});
|
281
|
+
const captureNodeCss$$1 = captureNodeCss({
|
282
|
+
extractCssFromNode: extractCssFromNode$$1,
|
283
|
+
getBundledCssFromCssText: getBundledCssFromCssText$$1,
|
284
|
+
unfetchedToken,
|
285
|
+
});
|
286
|
+
|
287
|
+
// Note: Change the API_VERSION when changing json structure.
|
288
|
+
const capturedFrame = await doCaptureFrame(doc);
|
289
|
+
capturedFrame.version = API_VERSION;
|
290
|
+
|
291
|
+
const iframePrefix = iframeCors.length ? `${iframeCors.join('\n')}\n` : '';
|
292
|
+
const unfetchedPrefix = unfetchedResources.size
|
293
|
+
? `${Array.from(unfetchedResources).join('\n')}\n`
|
294
|
+
: '';
|
295
|
+
const metaPrefix = JSON.stringify({
|
296
|
+
separator,
|
297
|
+
cssStartToken: unfetchedToken,
|
298
|
+
cssEndToken: unfetchedToken,
|
299
|
+
iframeStartToken: `"${iframeToken}`,
|
300
|
+
iframeEndToken: `${iframeToken}"`,
|
301
|
+
});
|
302
|
+
const ret = `${metaPrefix}\n${unfetchedPrefix}${separator}\n${iframePrefix}${separator}\n${JSON.stringify(
|
303
|
+
capturedFrame,
|
304
|
+
)}`;
|
305
|
+
console.log('[captureFrame]', Date.now() - start);
|
306
|
+
return ret;
|
307
|
+
|
308
|
+
function filter(x) {
|
309
|
+
return !!x;
|
310
|
+
}
|
311
|
+
|
312
|
+
function notEmptyObj(obj) {
|
313
|
+
return Object.keys(obj).length ? obj : undefined;
|
314
|
+
}
|
315
|
+
|
316
|
+
function captureTextNode(node) {
|
317
|
+
return {
|
318
|
+
tagName: '#text',
|
319
|
+
text: node.textContent,
|
320
|
+
};
|
321
|
+
}
|
322
|
+
|
323
|
+
async function doCaptureFrame(frameDoc) {
|
324
|
+
const bgImages = new Set();
|
325
|
+
let bundledCss = '';
|
326
|
+
const ret = await captureNode(frameDoc.documentElement);
|
327
|
+
ret.css = bundledCss;
|
328
|
+
ret.images = await getImageSizes_1({bgImages});
|
329
|
+
return ret;
|
330
|
+
|
331
|
+
async function captureNode(node) {
|
332
|
+
const {bundledCss: nodeCss, unfetchedResources: nodeUnfetched} = await captureNodeCss$$1(
|
333
|
+
node,
|
334
|
+
frameDoc.location.href,
|
335
|
+
);
|
336
|
+
bundledCss += nodeCss;
|
337
|
+
if (nodeUnfetched) for (const elem of nodeUnfetched) unfetchedResources.add(elem);
|
338
|
+
|
339
|
+
switch (node.nodeType) {
|
340
|
+
case NODE_TYPES.TEXT: {
|
341
|
+
return captureTextNode(node);
|
38
342
|
}
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
const style = {};
|
46
|
-
for (const p of styleProps) style[p] = computedStyle.getPropertyValue(p);
|
47
|
-
const rect = {};
|
48
|
-
for (const p of rectProps) rect[p] = boundingClientRect[p];
|
49
|
-
const attributes = {};
|
50
|
-
if (!attributeProps) {
|
51
|
-
if (el.hasAttributes()) {
|
52
|
-
var attrs = el.attributes;
|
53
|
-
for (const p of attrs) {
|
54
|
-
attributes[p.name] = p.value;
|
55
|
-
}
|
343
|
+
case NODE_TYPES.ELEMENT: {
|
344
|
+
const tagName = node.tagName.toUpperCase();
|
345
|
+
if (tagName === 'IFRAME') {
|
346
|
+
return await iframeToJSON(node);
|
347
|
+
} else {
|
348
|
+
return await await elementToJSON(node);
|
56
349
|
}
|
57
350
|
}
|
58
|
-
|
59
|
-
|
60
|
-
for (const p of attributeProps.all) {
|
61
|
-
if (el.hasAttribute(p)) attributes[p] = el.getAttribute(p);
|
62
|
-
}
|
63
|
-
}
|
64
|
-
if (attributeProps[tagName]) {
|
65
|
-
for (const p of attributeProps[tagName]) {
|
66
|
-
if (el.hasAttribute(p)) attributes[p] = el.getAttribute(p);
|
67
|
-
}
|
68
|
-
}
|
351
|
+
default: {
|
352
|
+
return null;
|
69
353
|
}
|
70
|
-
return {
|
71
|
-
tagName,
|
72
|
-
style: notEmptyObj(style),
|
73
|
-
rect: notEmptyObj(rect),
|
74
|
-
attributes: notEmptyObj(attributes),
|
75
|
-
childNodes: Array.prototype.map.call(el.childNodes, captureNode).filter(filter),
|
76
|
-
};
|
77
354
|
}
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
355
|
+
}
|
356
|
+
|
357
|
+
async function elementToJSON(el) {
|
358
|
+
const childNodes = (await Promise.all(
|
359
|
+
Array.prototype.map.call(el.childNodes, captureNode),
|
360
|
+
)).filter(filter);
|
361
|
+
|
362
|
+
const tagName = el.tagName.toUpperCase();
|
363
|
+
if (ignoredTagNames.indexOf(tagName) > -1) return null;
|
364
|
+
|
365
|
+
const computedStyle = window.getComputedStyle(el);
|
366
|
+
const boundingClientRect = el.getBoundingClientRect();
|
367
|
+
|
368
|
+
const style = {};
|
369
|
+
for (const p of styleProps) style[p] = computedStyle.getPropertyValue(p);
|
370
|
+
|
371
|
+
const rect = {};
|
372
|
+
for (const p of rectProps) rect[p] = boundingClientRect[p];
|
373
|
+
|
374
|
+
const attributes = Array.from(el.attributes)
|
375
|
+
.map(a => ({key: a.name, value: a.value}))
|
376
|
+
.reduce((obj, attr) => {
|
377
|
+
obj[attr.key] = attr.value;
|
378
|
+
return obj;
|
379
|
+
}, {});
|
380
|
+
|
381
|
+
const bgImage = getBackgroundImageUrl_1(computedStyle.getPropertyValue('background-image'));
|
382
|
+
if (bgImage) {
|
383
|
+
bgImages.add(bgImage);
|
384
|
+
}
|
385
|
+
|
386
|
+
return {
|
387
|
+
tagName,
|
388
|
+
style: notEmptyObj(style),
|
389
|
+
rect: notEmptyObj(rect),
|
390
|
+
attributes: notEmptyObj(attributes),
|
391
|
+
childNodes,
|
392
|
+
};
|
393
|
+
}
|
394
|
+
|
395
|
+
async function iframeToJSON(el) {
|
396
|
+
const obj = await elementToJSON(el);
|
397
|
+
let doc;
|
398
|
+
try {
|
399
|
+
doc = el.contentDocument;
|
400
|
+
} catch (ex) {
|
401
|
+
markFrameAsCors();
|
402
|
+
return obj;
|
83
403
|
}
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
if (node.tagName.toUpperCase() === 'IFRAME') {
|
90
|
-
return iframeToJSON(node);
|
91
|
-
} else {
|
92
|
-
return elementToJSON(node);
|
93
|
-
}
|
94
|
-
default:
|
95
|
-
return null;
|
404
|
+
try {
|
405
|
+
if (doc) {
|
406
|
+
obj.childNodes = [await doCaptureFrame(el.contentDocument)];
|
407
|
+
} else {
|
408
|
+
markFrameAsCors();
|
96
409
|
}
|
410
|
+
} catch (ex) {
|
411
|
+
console.log('error in iframeToJSON', ex);
|
412
|
+
}
|
413
|
+
return obj;
|
414
|
+
|
415
|
+
function markFrameAsCors() {
|
416
|
+
const xpath = genXpath_1(el);
|
417
|
+
iframeCors.push(xpath);
|
418
|
+
obj.childNodes = [`${iframeToken}${xpath}${iframeToken}`];
|
97
419
|
}
|
98
|
-
return captureNode(document.documentElement);
|
99
420
|
}
|
100
|
-
|
101
|
-
|
421
|
+
}
|
422
|
+
}
|
423
|
+
|
424
|
+
var captureFrame_1 = captureFrame;
|
425
|
+
|
426
|
+
const EYES_NAME_SPACE = '__EYES__APPLITOOLS__';
|
427
|
+
|
428
|
+
function captureFrameAndPoll(...args) {
|
429
|
+
if (!window[EYES_NAME_SPACE]) {
|
430
|
+
window[EYES_NAME_SPACE] = {};
|
431
|
+
}
|
432
|
+
if (!window[EYES_NAME_SPACE].captureDomResult) {
|
433
|
+
window[EYES_NAME_SPACE].captureDomResult = {
|
434
|
+
status: 'WIP',
|
435
|
+
value: null,
|
436
|
+
error: null,
|
437
|
+
};
|
438
|
+
captureFrame_1(...args)
|
439
|
+
.then(r => ((resultObject.status = 'SUCCESS'), (resultObject.value = r)))
|
440
|
+
.catch(e => ((resultObject.status = 'ERROR'), (resultObject.error = e.message)));
|
441
|
+
}
|
442
|
+
|
443
|
+
const resultObject = window[EYES_NAME_SPACE].captureDomResult;
|
444
|
+
if (resultObject.status === 'SUCCESS') {
|
445
|
+
window[EYES_NAME_SPACE].captureDomResult = null;
|
446
|
+
}
|
447
|
+
|
448
|
+
return JSON.stringify(resultObject);
|
449
|
+
}
|
450
|
+
|
451
|
+
var captureFrameAndPoll_1 = captureFrameAndPoll;
|
452
|
+
|
453
|
+
return captureFrameAndPoll_1;
|
454
|
+
|
455
|
+
}());
|
456
|
+
|
457
|
+
return captureDomAndPoll.apply(this, arguments);
|
458
|
+
}
|
459
|
+
SCRIPT
|
460
|
+
end
|
102
461
|
end
|
103
|
-
end
|
462
|
+
end
|