@applitools/dom-capture
Advanced tools
Comparing version 3.1.5 to 4.0.1
function(props={"styleProps":["background-color","background-image","background-size","color","border-width","border-color","border-style","padding","margin"],"rectProps":["width","height","top","left","bottom","right"],"ignoredTagNames":["HEAD"]}, doc=document) { | ||
(function() { | ||
var captureDom = (function () { | ||
'use strict'; | ||
function absolutizeUrl(url, absoluteUrl) { | ||
return new URL(url, absoluteUrl).href; | ||
} | ||
var absolutizeUrl_1 = absolutizeUrl; | ||
async function getBundledCssFromCssText(cssText, resourceUrl) { | ||
try { | ||
let bundledCss = ''; | ||
const styleSheet = parseCss(cssText); | ||
for (const rule of Array.from(styleSheet.cssRules)) { | ||
if (rule instanceof CSSImportRule) { | ||
const nestedUrl = absolutizeUrl_1(rule.href, resourceUrl); | ||
const nestedResource = await fetchCss(nestedUrl); | ||
if (nestedResource) { | ||
const nestedCssText = await getBundledCssFromCssText( | ||
nestedResource.content.toString(), | ||
nestedUrl, | ||
); | ||
bundledCss = `${nestedCssText}${bundledCss}`; | ||
} | ||
} | ||
} | ||
return `${bundledCss}${getCss(cssText, resourceUrl)}`; | ||
} catch (ex) {} | ||
} | ||
async function fetchCss(url) { | ||
try { | ||
const response = await fetch(url); | ||
if (response.ok) { | ||
return await response.text(); | ||
} | ||
return '/failed to fetch (status ' + response.status + ') css from: ' + url + '/'; | ||
} catch (err) { | ||
return '/failed to fetch (error ' + err.toString() + ') css from: ' + url + '/'; | ||
} | ||
} | ||
function parseCss(styleContent) { | ||
var doc = document.implementation.createHTMLDocument(''), | ||
styleElement = doc.createElement('style'); | ||
styleElement.textContent = styleContent; | ||
// the style will only be parsed once it is added to a document | ||
doc.body.appendChild(styleElement); | ||
return styleElement.sheet; | ||
} | ||
function getCss(newText, url) { | ||
return `\n/** ${url} **/\n${newText}`; | ||
} | ||
function isStyleElement(node) { | ||
return node.nodeName && node.nodeName.toUpperCase() === 'STYLE'; | ||
} | ||
function getHrefAttr(node) { | ||
const attr = Array.from(node.attributes).find(attr => attr.name.toLowerCase() === 'href'); | ||
return attr && attr.value; | ||
} | ||
function isLinkToStyleSheet(node) { | ||
return ( | ||
node.nodeName && | ||
node.nodeName.toUpperCase() === 'LINK' && | ||
node.attributes && | ||
Array.from(node.attributes).find( | ||
attr => attr.name.toLowerCase() === 'rel' && attr.value.toLowerCase() === 'stylesheet', | ||
) | ||
); | ||
} | ||
async function captureNodeCss(baseUrl, node) { | ||
let bundledCss = ''; | ||
let cssText, resourceUrl; | ||
if (isStyleElement(node)) { | ||
cssText = Array.from(node.childNodes) | ||
.map(node => node.nodeValue) | ||
.join(''); | ||
resourceUrl = baseUrl; | ||
} else if (isLinkToStyleSheet(node)) { | ||
resourceUrl = absolutizeUrl_1(getHrefAttr(node), baseUrl); | ||
const resource = await fetchCss(resourceUrl); | ||
if (resource) { | ||
cssText = resource.toString(); | ||
} | ||
} | ||
if (cssText) { | ||
const moreCss = await getBundledCssFromCssText(cssText, resourceUrl); | ||
bundledCss = `${bundledCss}${moreCss}`; | ||
} | ||
for (const subNode of node.childNodes) { | ||
const nodeCss = await captureNodeCss(baseUrl, subNode); | ||
bundledCss += nodeCss; | ||
} | ||
return bundledCss; | ||
} | ||
async function captureFrameCss(url, doc) { | ||
return await captureNodeCss(url, doc.documentElement); | ||
} | ||
var captureFrameCss_1 = captureFrameCss; | ||
// TODO conditionally add/remove properties based on value (need to account for cross browser values) | ||
const styleProps = [ | ||
'background-repeat', | ||
'background-origin', | ||
'background-position', | ||
'background-color', | ||
'background-image', | ||
'background-size', | ||
'color', | ||
'border-width', | ||
'border-color', | ||
'border-style', | ||
'color', | ||
'display', | ||
'font-size', | ||
'line-height', | ||
'margin', | ||
'opacity', | ||
'padding', | ||
'margin', | ||
'visibility', | ||
]; | ||
const rectProps = [ | ||
'width', | ||
'height', | ||
'top', | ||
'left', | ||
'bottom', // TODO is this needed given that we have top+height ? | ||
'right', // TODO is this needed given that we have left+width ? | ||
]; | ||
const rectProps = ['width', 'height', 'top', 'left']; | ||
const ignoredTagNames = ['HEAD']; | ||
const ignoredTagNames = ['HEAD', 'SCRIPT']; | ||
@@ -183,2 +75,185 @@ var defaultDomProps = { | ||
function genXpath(el) { | ||
if (!el.ownerDocument) return ''; // this is the document node | ||
let xpath = '', | ||
currEl = el, | ||
doc = el.ownerDocument, | ||
frameElement = doc.defaultView.frameElement; | ||
while (currEl !== doc) { | ||
xpath = `${currEl.tagName}[${getIndex(currEl)}]/${xpath}`; | ||
currEl = currEl.parentNode; | ||
} | ||
if (frameElement) { | ||
xpath = `${genXpath(frameElement)},${xpath}`; | ||
} | ||
return xpath.replace(/\/$/, ''); | ||
} | ||
function getIndex(el) { | ||
return ( | ||
Array.prototype.filter | ||
.call(el.parentNode.childNodes, node => node.tagName === el.tagName) | ||
.indexOf(el) + 1 | ||
); | ||
} | ||
var genXpath_1 = genXpath; | ||
function absolutizeUrl(url, absoluteUrl) { | ||
return new URL(url, absoluteUrl).href; | ||
} | ||
var absolutizeUrl_1 = absolutizeUrl; | ||
function makeGetBundledCssFromCssText({ | ||
parseCss, | ||
CSSImportRule, | ||
absolutizeUrl, | ||
fetchCss, | ||
unfetchedToken, | ||
}) { | ||
return async function getBundledCssFromCssText(cssText, resourceUrl) { | ||
let unfetchedResources; | ||
let bundledCss = ''; | ||
try { | ||
const styleSheet = parseCss(cssText); | ||
for (const rule of Array.from(styleSheet.cssRules)) { | ||
if (rule instanceof CSSImportRule) { | ||
const nestedUrl = absolutizeUrl(rule.href, resourceUrl); | ||
const nestedResource = await fetchCss(nestedUrl); | ||
if (nestedResource !== undefined) { | ||
const { | ||
bundledCss: nestedCssText, | ||
unfetchedResources: nestedUnfetchedResources, | ||
} = await getBundledCssFromCssText(nestedResource, nestedUrl); | ||
nestedUnfetchedResources && (unfetchedResources = new Set(nestedUnfetchedResources)); | ||
bundledCss = `${nestedCssText}${bundledCss}`; | ||
} else { | ||
unfetchedResources = new Set([nestedUrl]); | ||
bundledCss = `\n${unfetchedToken}${nestedUrl}${unfetchedToken}`; | ||
} | ||
} | ||
} | ||
} catch (ex) { | ||
console.log(`error during getBundledCssFromCssText, resourceUrl=${resourceUrl}`, ex); | ||
} | ||
bundledCss = `${bundledCss}${getCss(cssText, resourceUrl)}`; | ||
return { | ||
bundledCss, | ||
unfetchedResources, | ||
}; | ||
}; | ||
} | ||
function getCss(newText, url) { | ||
return `\n/** ${url} **/\n${newText}`; | ||
} | ||
var getBundledCssFromCssText = makeGetBundledCssFromCssText; | ||
/* global document*/ | ||
function parseCss(styleContent) { | ||
var doc = document.implementation.createHTMLDocument(''), | ||
styleElement = doc.createElement('style'); | ||
styleElement.textContent = styleContent; | ||
// the style will only be parsed once it is added to a document | ||
doc.body.appendChild(styleElement); | ||
return styleElement.sheet; | ||
} | ||
var parseCss_1 = parseCss; | ||
function makeFetchCss(fetch) { | ||
return async function fetchCss(url) { | ||
try { | ||
const response = await fetch(url); | ||
if (response.ok) { | ||
return await response.text(); | ||
} | ||
console.log('/failed to fetch (status ' + response.status + ') css from: ' + url + '/'); | ||
} catch (err) { | ||
console.log('/failed to fetch (error ' + err.toString() + ') css from: ' + url + '/'); | ||
} | ||
}; | ||
} | ||
var fetchCss = makeFetchCss; | ||
function makeExtractCssFromNode({fetchCss, absolutizeUrl}) { | ||
return async function extractCssFromNode(node, baseUrl) { | ||
let cssText, resourceUrl, isUnfetched; | ||
if (isStyleElement(node)) { | ||
cssText = Array.from(node.childNodes) | ||
.map(node => node.nodeValue) | ||
.join(''); | ||
resourceUrl = baseUrl; | ||
} else if (isLinkToStyleSheet(node)) { | ||
resourceUrl = absolutizeUrl(getHrefAttr(node), baseUrl); | ||
cssText = await fetchCss(resourceUrl); | ||
if (cssText === undefined) { | ||
isUnfetched = true; | ||
} | ||
} | ||
return {cssText, resourceUrl, isUnfetched}; | ||
}; | ||
} | ||
function isStyleElement(node) { | ||
return node.nodeName && node.nodeName.toUpperCase() === 'STYLE'; | ||
} | ||
function getHrefAttr(node) { | ||
const attr = Array.from(node.attributes).find(attr => attr.name.toLowerCase() === 'href'); | ||
return attr && attr.value; | ||
} | ||
function isLinkToStyleSheet(node) { | ||
return ( | ||
node.nodeName && | ||
node.nodeName.toUpperCase() === 'LINK' && | ||
node.attributes && | ||
Array.from(node.attributes).find( | ||
attr => attr.name.toLowerCase() === 'rel' && attr.value.toLowerCase() === 'stylesheet', | ||
) | ||
); | ||
} | ||
var extractCssFromNode = makeExtractCssFromNode; | ||
function makeCaptureNodeCss({extractCssFromNode, getBundledCssFromCssText, unfetchedToken}) { | ||
return async function captureNodeCss(node, baseUrl) { | ||
const {resourceUrl, cssText, isUnfetched} = await extractCssFromNode(node, baseUrl); | ||
let unfetchedResources; | ||
let bundledCss = ''; | ||
if (cssText) { | ||
const { | ||
bundledCss: nestedCss, | ||
unfetchedResources: nestedUnfetched, | ||
} = await getBundledCssFromCssText(cssText, resourceUrl); | ||
bundledCss += nestedCss; | ||
unfetchedResources = new Set(nestedUnfetched); | ||
} else if (isUnfetched) { | ||
bundledCss += `${unfetchedToken}${resourceUrl}${unfetchedToken}`; | ||
unfetchedResources = new Set([resourceUrl]); | ||
} | ||
return {bundledCss, unfetchedResources}; | ||
}; | ||
} | ||
var captureNodeCss = makeCaptureNodeCss; | ||
const NODE_TYPES = { | ||
ELEMENT: 1, | ||
TEXT: 3, | ||
}; | ||
async function captureFrame( | ||
@@ -188,7 +263,33 @@ {styleProps, rectProps, ignoredTagNames} = defaultDomProps, | ||
) { | ||
const NODE_TYPES = { | ||
ELEMENT: 1, | ||
TEXT: 3, | ||
}; | ||
const start = Date.now(); | ||
const unfetchedResources = new Set(); | ||
const iframeCors = []; | ||
const iframeToken = '@@@@@'; | ||
const unfetchedToken = '#####'; | ||
const separator = '-----'; | ||
const fetchCss$$1 = fetchCss(fetch); | ||
const getBundledCssFromCssText$$1 = getBundledCssFromCssText({ | ||
parseCss: parseCss_1, | ||
CSSImportRule, | ||
fetchCss: fetchCss$$1, | ||
absolutizeUrl: absolutizeUrl_1, | ||
unfetchedToken, | ||
}); | ||
const extractCssFromNode$$1 = extractCssFromNode({fetchCss: fetchCss$$1, absolutizeUrl: absolutizeUrl_1}); | ||
const captureNodeCss$$1 = captureNodeCss({ | ||
extractCssFromNode: extractCssFromNode$$1, | ||
getBundledCssFromCssText: getBundledCssFromCssText$$1, | ||
unfetchedToken, | ||
}); | ||
const ret = await doCaptureFrame(doc); | ||
const iframePrefix = iframeCors.length ? `${iframeToken}\n${iframeCors.join('\n')}\n` : ''; | ||
const unfetchedPrefix = unfetchedResources.size | ||
? `${unfetchedToken}\n${Array.from(unfetchedResources).join('\n')}\n` | ||
: ''; | ||
const prefix = | ||
unfetchedPrefix || iframePrefix ? `${unfetchedPrefix}${iframePrefix}${separator}\n` : ''; | ||
console.log('[captureFrame]', Date.now() - start); | ||
return `${prefix}${JSON.stringify(ret)}`; | ||
function filter(x) { | ||
@@ -211,4 +312,5 @@ return !!x; | ||
const bgImages = new Set(); | ||
let bundledCss = ''; | ||
const ret = await captureNode(frameDoc.documentElement); | ||
ret.css = await captureFrameCss_1(window.location, frameDoc); | ||
ret.css = bundledCss; | ||
ret.images = await getImageSizes_1({bgImages}); | ||
@@ -218,2 +320,9 @@ return ret; | ||
async function captureNode(node) { | ||
const {bundledCss: nodeCss, unfetchedResources: nodeUnfetched} = await captureNodeCss$$1( | ||
node, | ||
frameDoc.location.href, | ||
); | ||
bundledCss += nodeCss; | ||
if (nodeUnfetched) for (const elem of nodeUnfetched) unfetchedResources.add(elem); | ||
switch (node.nodeType) { | ||
@@ -235,4 +344,9 @@ case NODE_TYPES.TEXT: | ||
async function elementToJSON(el) { | ||
const childNodes = (await Promise.all( | ||
Array.prototype.map.call(el.childNodes, captureNode), | ||
)).filter(filter); | ||
const tagName = el.tagName.toUpperCase(); | ||
if (ignoredTagNames.indexOf(tagName) > -1) return null; | ||
const computedStyle = window.getComputedStyle(el); | ||
@@ -264,5 +378,3 @@ const boundingClientRect = el.getBoundingClientRect(); | ||
attributes: notEmptyObj(attributes), | ||
childNodes: (await Promise.all( | ||
Array.prototype.map.call(el.childNodes, captureNode), | ||
)).filter(filter), | ||
childNodes, | ||
}; | ||
@@ -276,4 +388,7 @@ } | ||
obj.childNodes = [await doCaptureFrame(el.contentDocument)]; | ||
} else { | ||
const xpath = genXpath_1(el); | ||
iframeCors.push(xpath); | ||
obj.childNodes = [`${iframeToken}${xpath}${iframeToken}`]; | ||
} | ||
} catch (ex) { | ||
} finally { | ||
@@ -284,7 +399,2 @@ return obj; | ||
} | ||
const start = Date.now(); | ||
const ret = await doCaptureFrame(doc); | ||
console.log('[captureFrame]', Date.now() - start); | ||
return ret; | ||
} | ||
@@ -298,3 +408,3 @@ | ||
return captureDom(props, doc); | ||
} | ||
return captureDom(); | ||
})() |
@@ -1,10 +0,4 @@ | ||
const captureWindow = require('./src/captureWindow'); | ||
const captureFrame = require('./src/captureFrame'); | ||
const getCaptureDomScript = require('./src/getCaptureDomScript'); | ||
const defaultDomProps = require('./src/defaultDomProps'); | ||
module.exports = { | ||
captureWindow, | ||
captureFrame, // backward compatibility. Can probably be removed on December 2018. | ||
getCaptureDomScript, | ||
defaultDomProps, | ||
}; |
{ | ||
"name": "@applitools/dom-capture", | ||
"version": "3.1.5", | ||
"version": "4.0.1", | ||
"main": "index.js", | ||
@@ -24,2 +24,3 @@ "license": "MIT", | ||
"chai": "^4.2.0", | ||
"cssom": "^0.3.4", | ||
"eslint": "5.7.0", | ||
@@ -30,2 +31,3 @@ "eslint-plugin-mocha-no-only": "1.0.0", | ||
"express": "^4.16.4", | ||
"jsdom": "^13.0.0", | ||
"mocha": "5.2.0", | ||
@@ -32,0 +34,0 @@ "prettier": "^1.14.3", |
@@ -1,7 +0,17 @@ | ||
/* global window, document */ | ||
/* global window, document, CSSImportRule, fetch */ | ||
'use strict'; | ||
const captureFrameCss = require('./captureFrameCss'); | ||
const defaultDomProps = require('./defaultDomProps'); | ||
const getBackgroundImageUrl = require('./getBackgroundImageUrl'); | ||
const getImageSizes = require('./getImageSizes'); | ||
const genXpath = require('./genXpath'); | ||
const absolutizeUrl = require('./absolutizeUrl'); | ||
const makeGetBundledCssFromCssText = require('./getBundledCssFromCssText'); | ||
const parseCss = require('./parseCss'); | ||
const makeFetchCss = require('./fetchCss'); | ||
const makeExtractCssFromNode = require('./extractCssFromNode'); | ||
const makeCaptureNodeCss = require('./captureNodeCss'); | ||
const NODE_TYPES = { | ||
ELEMENT: 1, | ||
TEXT: 3, | ||
}; | ||
@@ -12,7 +22,33 @@ async function captureFrame( | ||
) { | ||
const NODE_TYPES = { | ||
ELEMENT: 1, | ||
TEXT: 3, | ||
}; | ||
const start = Date.now(); | ||
const unfetchedResources = new Set(); | ||
const iframeCors = []; | ||
const iframeToken = '@@@@@'; | ||
const unfetchedToken = '#####'; | ||
const separator = '-----'; | ||
const fetchCss = makeFetchCss(fetch); | ||
const getBundledCssFromCssText = makeGetBundledCssFromCssText({ | ||
parseCss, | ||
CSSImportRule, | ||
fetchCss, | ||
absolutizeUrl, | ||
unfetchedToken, | ||
}); | ||
const extractCssFromNode = makeExtractCssFromNode({fetchCss, absolutizeUrl}); | ||
const captureNodeCss = makeCaptureNodeCss({ | ||
extractCssFromNode, | ||
getBundledCssFromCssText, | ||
unfetchedToken, | ||
}); | ||
const ret = await doCaptureFrame(doc); | ||
const iframePrefix = iframeCors.length ? `${iframeToken}\n${iframeCors.join('\n')}\n` : ''; | ||
const unfetchedPrefix = unfetchedResources.size | ||
? `${unfetchedToken}\n${Array.from(unfetchedResources).join('\n')}\n` | ||
: ''; | ||
const prefix = | ||
unfetchedPrefix || iframePrefix ? `${unfetchedPrefix}${iframePrefix}${separator}\n` : ''; | ||
console.log('[captureFrame]', Date.now() - start); | ||
return `${prefix}${JSON.stringify(ret)}`; | ||
function filter(x) { | ||
@@ -35,4 +71,5 @@ return !!x; | ||
const bgImages = new Set(); | ||
let bundledCss = ''; | ||
const ret = await captureNode(frameDoc.documentElement); | ||
ret.css = await captureFrameCss(window.location, frameDoc); | ||
ret.css = bundledCss; | ||
ret.images = await getImageSizes({bgImages}); | ||
@@ -42,2 +79,9 @@ return ret; | ||
async function captureNode(node) { | ||
const {bundledCss: nodeCss, unfetchedResources: nodeUnfetched} = await captureNodeCss( | ||
node, | ||
frameDoc.location.href, | ||
); | ||
bundledCss += nodeCss; | ||
if (nodeUnfetched) for (const elem of nodeUnfetched) unfetchedResources.add(elem); | ||
switch (node.nodeType) { | ||
@@ -59,4 +103,9 @@ case NODE_TYPES.TEXT: | ||
async function elementToJSON(el) { | ||
const childNodes = (await Promise.all( | ||
Array.prototype.map.call(el.childNodes, captureNode), | ||
)).filter(filter); | ||
const tagName = el.tagName.toUpperCase(); | ||
if (ignoredTagNames.indexOf(tagName) > -1) return null; | ||
const computedStyle = window.getComputedStyle(el); | ||
@@ -88,5 +137,3 @@ const boundingClientRect = el.getBoundingClientRect(); | ||
attributes: notEmptyObj(attributes), | ||
childNodes: (await Promise.all( | ||
Array.prototype.map.call(el.childNodes, captureNode), | ||
)).filter(filter), | ||
childNodes, | ||
}; | ||
@@ -100,4 +147,7 @@ } | ||
obj.childNodes = [await doCaptureFrame(el.contentDocument)]; | ||
} else { | ||
const xpath = genXpath(el); | ||
iframeCors.push(xpath); | ||
obj.childNodes = [`${iframeToken}${xpath}${iframeToken}`]; | ||
} | ||
} catch (ex) { | ||
} finally { | ||
@@ -108,9 +158,4 @@ return obj; | ||
} | ||
const start = Date.now(); | ||
const ret = await doCaptureFrame(doc); | ||
console.log('[captureFrame]', Date.now() - start); | ||
return ret; | ||
} | ||
module.exports = captureFrame; |
@@ -1,24 +0,24 @@ | ||
// TODO conditionally add/remove properties based on value (need to account for cross browser values) | ||
const styleProps = [ | ||
'background-repeat', | ||
'background-origin', | ||
'background-position', | ||
'background-color', | ||
'background-image', | ||
'background-size', | ||
'color', | ||
'border-width', | ||
'border-color', | ||
'border-style', | ||
'color', | ||
'display', | ||
'font-size', | ||
'line-height', | ||
'margin', | ||
'opacity', | ||
'padding', | ||
'margin', | ||
'visibility', | ||
]; | ||
const rectProps = [ | ||
'width', | ||
'height', | ||
'top', | ||
'left', | ||
'bottom', // TODO is this needed given that we have top+height ? | ||
'right', // TODO is this needed given that we have left+width ? | ||
]; | ||
const rectProps = ['width', 'height', 'top', 'left']; | ||
const ignoredTagNames = ['HEAD']; | ||
const ignoredTagNames = ['HEAD', 'SCRIPT']; | ||
@@ -25,0 +25,0 @@ module.exports = { |
@@ -1,6 +0,26 @@ | ||
function genXpath(el, parent, parentXPath) { | ||
const index = parent.childNodes.filter(node => node.tagName === el.tagName).indexOf(el) + 1; | ||
return `${parentXPath}/${el.tagName}[${index}]`; | ||
function genXpath(el) { | ||
if (!el.ownerDocument) return ''; // this is the document node | ||
let xpath = '', | ||
currEl = el, | ||
doc = el.ownerDocument, | ||
frameElement = doc.defaultView.frameElement; | ||
while (currEl !== doc) { | ||
xpath = `${currEl.tagName}[${getIndex(currEl)}]/${xpath}`; | ||
currEl = currEl.parentNode; | ||
} | ||
if (frameElement) { | ||
xpath = `${genXpath(frameElement)},${xpath}`; | ||
} | ||
return xpath.replace(/\/$/, ''); | ||
} | ||
function getIndex(el) { | ||
return ( | ||
Array.prototype.filter | ||
.call(el.parentNode.childNodes, node => node.tagName === el.tagName) | ||
.indexOf(el) + 1 | ||
); | ||
} | ||
module.exports = genXpath; |
'use strict'; | ||
const defaultDomProps = require('../src/defaultDomProps'); | ||
// This rollup plugin is meant to create a standalone function that can be consumed as a string and be sent to a remote browser for evaluation. | ||
@@ -14,6 +12,6 @@ // I couldn't find a way in rollup to create an iife that runs the actual code (instead of returning a function that runs the code) without polluting the global scope | ||
bundleFile.code = ` | ||
function(props=${JSON.stringify(defaultDomProps)}, doc=document) { | ||
(function() { | ||
${bundleFile.code} | ||
return ${filename}(props, doc); | ||
}`; | ||
return ${filename}(); | ||
})()`; | ||
}, | ||
@@ -20,0 +18,0 @@ }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
25248
16
719
17
5