eyes_selenium 3.15.29 → 3.15.30
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/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
|