@applitools/eyes.cypress
Advanced tools
Comparing version 1.4.2 to 1.5.0
{ | ||
"name": "@applitools/eyes.cypress", | ||
"version": "1.4.2", | ||
"version": "1.5.0", | ||
"main": "index.js", | ||
@@ -24,4 +24,5 @@ "license": "MIT", | ||
"@applitools/eyes.sdk.core": "^1.6.0", | ||
"body-parser": "^1.18.2", | ||
"cors": "^2.8.4", | ||
"cssom": "0.3.1", | ||
"cssom": "git+https://github.com/amitzur/CSSOM.git#925260ff2c8f8387cf76df4d5776a06044a644c8", | ||
"dotenv": "^5.0.1", | ||
@@ -28,0 +29,0 @@ "express": "^4.16.3", |
@@ -1,2 +0,2 @@ | ||
/* global Cypress, cy */ | ||
/* global Cypress,cy,window */ | ||
'use strict'; | ||
@@ -12,11 +12,23 @@ const extractResources = require('../render-grid/browser-util/extractResources'); | ||
open(args) { | ||
return sendRequest('open', args); | ||
return sendRequest({command: 'open', data: args}); | ||
}, | ||
checkWindow({resourceUrls, cdt, tag, sizeMode}) { | ||
return sendRequest('checkWindow', {resourceUrls, cdt, tag, sizeMode}); | ||
putResource({url, type, value}) { | ||
return sendRequest({ | ||
command: `resource/${url}`, | ||
data: new window.frameElement.ownerDocument.defaultView.Blob([value]), // yucky! cypress uses socket.io to communicate between browser and node. In order to encode the data in binary format, socket.io checks for binary values. But `value instanceof Blob` is falsy since Blob from the cypress runner window is not the Blob from the command's window. So using the Blob from cypress runner window here. | ||
method: 'PUT', | ||
headers: {'Content-Type': type}, | ||
}); | ||
}, | ||
checkWindow({resourceUrls, blobs, cdt, tag, sizeMode}) { | ||
const blobData = blobs.map(({url, type}) => ({url, type})); | ||
return Promise.all(blobs.map(EyesServer.putResource)).then(() => | ||
sendRequest({command: 'checkWindow', data: {resourceUrls, cdt, tag, sizeMode, blobData}}), | ||
); | ||
}, | ||
close: poll(function({timeout}) { | ||
return sendRequest('close', {timeout}); | ||
return sendRequest({command: 'close', data: {timeout}}); | ||
}), | ||
@@ -43,7 +55,10 @@ }; | ||
Cypress.log({name: 'Eyes: check window'}); | ||
return cy.document({log: false}).then(doc => { | ||
const cdt = domNodesToCdt(doc); | ||
const resourceUrls = extractResources(doc); | ||
return EyesServer.checkWindow({resourceUrls, cdt, tag, sizeMode}); | ||
}); | ||
return cy.document({log: false}).then(doc => | ||
cy.window().then(win => { | ||
const cdt = domNodesToCdt(doc); | ||
return extractResources(doc, win).then(({resourceUrls, blobs}) => { | ||
return EyesServer.checkWindow({resourceUrls, blobs, cdt, tag, sizeMode}); | ||
}); | ||
}), | ||
); | ||
}); | ||
@@ -56,4 +71,4 @@ | ||
function sendRequest(command, data) { | ||
return send(command, data).then(resp => { | ||
function sendRequest(args) { | ||
return send(args).then(resp => { | ||
if (!resp.body.success) { | ||
@@ -60,0 +75,0 @@ throw new Error(resp.body.error); |
'use strict'; | ||
module.exports = (port, fetch) => (command, data) => | ||
module.exports = (port, fetch) => ({command, data, method = 'POST', headers}) => | ||
fetch({ | ||
url: `http://localhost:${port}/eyes/${command}`, | ||
method: 'POST', | ||
method, | ||
body: data, | ||
log: false, | ||
headers, | ||
}); |
@@ -34,3 +34,5 @@ 'use strict'; | ||
return config => { | ||
return Object.assign({}, priorConfig, config); | ||
const ret = Object.assign({}, priorConfig, config); | ||
console.log('running with config:', ret); | ||
return ret; | ||
}; | ||
@@ -37,0 +39,0 @@ } |
@@ -6,6 +6,4 @@ 'use strict'; | ||
const DEFAULT_TIMEOUT = 120000; | ||
function makeHandlers(openEyes) { | ||
let checkWindow, close; | ||
let checkWindow, close, resources; | ||
@@ -18,6 +16,14 @@ return { | ||
close = pollingHandler(eyes.close); | ||
resources = {}; | ||
return eyes; | ||
}, | ||
checkWindow: async ({resourceUrls, cdt, tag}) => { | ||
putResource: (id, buffer) => { | ||
if (!resources) { | ||
throw new Error('Please call cy.eyesOpen() before calling cy.eyesCheckWindow()'); | ||
} | ||
resources[id] = buffer; | ||
}, | ||
checkWindow: async ({resourceUrls, cdt, tag, blobData = [], sizeMode}) => { | ||
if (!checkWindow) { | ||
@@ -27,6 +33,11 @@ throw new Error('Please call cy.eyesOpen() before calling cy.eyesCheckWindow()'); | ||
return await checkWindow({resourceUrls, cdt, tag}); | ||
const resourceContents = blobData.reduce((acc, {url, type}) => { | ||
acc[url] = {url, type, value: resources[url]}; | ||
return acc; | ||
}, {}); | ||
return await checkWindow({resourceUrls, resourceContents, cdt, tag, sizeMode}); | ||
}, | ||
close: async ({timeout = DEFAULT_TIMEOUT} = {}) => { | ||
close: async ({timeout} = {}) => { | ||
if (!close) { | ||
@@ -36,3 +47,4 @@ throw new Error('Please call cy.eyesOpen() before calling cy.eyesClose()'); | ||
return await close({timeout}); | ||
resources = null; | ||
return await close(timeout); | ||
}, | ||
@@ -39,0 +51,0 @@ }; |
@@ -10,2 +10,4 @@ 'use strict'; | ||
const DEFAULT_TIMEOUT = 120000; | ||
const TIMEOUT_MSG = | ||
@@ -20,3 +22,3 @@ "The cy.eyesClose command timed out. The default timeout is 2 minutes. It's possible to increase this timeout by passing a larger value, e.g. for 3 minutes: cy.eyesClose({ timeout: 180000 })"; | ||
return ({timeout}) => { | ||
return (timeout = DEFAULT_TIMEOUT) => { | ||
switch (pollingStatus) { | ||
@@ -23,0 +25,0 @@ case PollingStatus.IDLE: |
'use strict'; | ||
const express = require('express'); | ||
const morgan = require('morgan'); | ||
const bodyParser = require('body-parser'); | ||
const cors = require('cors'); | ||
@@ -40,2 +41,7 @@ const log = require('../../render-grid/sdk/log'); | ||
app.put('/eyes/resource/:id', bodyParser.raw({type: '*/*'}), async (req, res) => { | ||
handlers.putResource(req.params.id, Buffer.from(JSON.parse(req.body).data)); | ||
res.status(200).send({success: true}); | ||
}); | ||
app.post('/eyes/:command', express.json({limit: '100mb'}), async (req, res) => { | ||
@@ -42,0 +48,0 @@ log(`eyes api: ${req.params.command}, ${Object.keys(req.body)}`); |
@@ -38,13 +38,24 @@ /* eslint-disable no-use-before-define */ | ||
if (nodeType === NODE_TYPES.ELEMENT) { | ||
node = { | ||
nodeType: NODE_TYPES.ELEMENT, | ||
nodeName: elementNode.nodeName, | ||
attributes: Object.keys(elementNode.attributes).map(key => ({ | ||
name: elementNode.attributes[key].localName, | ||
value: elementNode.attributes[key].value, | ||
})), | ||
childNodeIndexes: elementNode.childNodes.length | ||
? childrenFactory(domNodes, elementNode.childNodes) | ||
: [], | ||
}; | ||
if (elementNode.nodeName !== 'SCRIPT') { | ||
node = { | ||
nodeType: NODE_TYPES.ELEMENT, | ||
nodeName: elementNode.nodeName, | ||
attributes: Object.keys(elementNode.attributes).map(key => { | ||
let value = elementNode.attributes[key].value; | ||
const name = elementNode.attributes[key].localName; | ||
if (/^blob:/.test(value)) { | ||
value = value.replace(/^blob:http:\/\/localhost:\d+\/(.+)/, '$1'); // TODO don't replace localhost once render-grid implements absolute urls | ||
} | ||
return { | ||
name, | ||
value, | ||
}; | ||
}), | ||
childNodeIndexes: elementNode.childNodes.length | ||
? childrenFactory(domNodes, elementNode.childNodes) | ||
: [], | ||
}; | ||
} | ||
} else if (nodeType === NODE_TYPES.TEXT) { | ||
@@ -51,0 +62,0 @@ node = { |
@@ -7,30 +7,3 @@ 'use strict'; | ||
*/ | ||
function extractResources(el) { | ||
function extractResourcesFromStyleSheet(styleSheet) { | ||
const resourceUrls = [...styleSheet.cssRules].reduce((acc, rule) => { | ||
if (isRuleOfType(rule, 'CSSImportRule')) { | ||
return acc.concat(rule.href); | ||
} else if (isRuleOfType(rule, 'CSSFontFaceRule')) { | ||
return acc.concat(getUrlFromCssText(rule.style.getPropertyValue('src'))); | ||
} else if (isRuleOfType(rule, 'CSSStyleRule')) { | ||
for (let i = 0, ii = rule.style.length; i < ii; i++) { | ||
const url = getUrlFromCssText(rule.style.getPropertyValue(rule.style[i])); | ||
url && acc.push(url); | ||
} | ||
} | ||
return acc; | ||
}, []); | ||
return [...new Set(resourceUrls)]; | ||
} | ||
// NOTE: this is also implemented on the server side (copy pasted to enable unit testing `extractResources` with puppeteer) | ||
function getUrlFromCssText(cssText) { | ||
const match = cssText.match(/url\((?!['"]?(?:data|http):)['"]?([^'"\)]*)['"]?\)/); | ||
return match ? match[1] : match; | ||
} | ||
function isRuleOfType(rule, ruleType) { | ||
return rule instanceof rule.parentStyleSheet.ownerNode.ownerDocument.defaultView[ruleType]; | ||
} | ||
function extractResources(el, win) { | ||
function uniq(arr) { | ||
@@ -52,12 +25,31 @@ return Array.from(new Set(arr)); | ||
const urlsFromStyleElements = [...el.getElementsByTagName('style')] | ||
.map(styleEl => styleEl.sheet) | ||
.reduce((acc, curr) => { | ||
const resourceUrls = extractResourcesFromStyleSheet(curr); | ||
return acc.concat(resourceUrls); | ||
}, []); | ||
const allResourceUrls = uniq([...srcUrls, ...cssUrls, ...videoPosterUrls]); | ||
return uniq([...srcUrls, ...cssUrls, ...urlsFromStyleElements, ...videoPosterUrls]); | ||
const blobUrls = [], | ||
resourceUrls = []; | ||
allResourceUrls.forEach(url => { | ||
if (/^blob:/.test(url)) { | ||
blobUrls.push(url); | ||
} else { | ||
resourceUrls.push(url); | ||
} | ||
}); | ||
return Promise.all( | ||
blobUrls.map(blobUrl => | ||
win.fetch(blobUrl).then(resp => | ||
resp.arrayBuffer().then(buff => ({ | ||
url: blobUrl.replace(/^blob:http:\/\/localhost:\d+\/(.+)/, '$1'), // TODO don't replace localhost once render-grid implements absolute urls | ||
type: resp.headers.get('Content-Type'), | ||
value: buff, | ||
})), | ||
), | ||
), | ||
).then(blobs => ({ | ||
resourceUrls, | ||
blobs, | ||
})); | ||
} | ||
module.exports = extractResources; |
'use strict'; | ||
const {parse, CSSImportRule, CSSStyleRule, CSSFontFaceRule} = require('cssom'); | ||
const {URL} = require('url'); | ||
const {parse, CSSImportRule, CSSStyleRule, CSSFontFaceRule, CSSSupportsRule} = require('cssom'); | ||
const absolutizeUrl = require('./absolutizeUrl'); | ||
// NOTE: this is also implemented on the client side (copy pasted to enable unit testing `extractResources` with puppeteer) | ||
function getUrlFromCssText(cssText) { | ||
@@ -11,8 +10,2 @@ const match = cssText.match(/url\((?!['"]?:)['"]?([^'"\)]*)['"]?\)/); | ||
// should this simple yet repetitive thing be exported as a separate module? | ||
function absolutizeUrl(url, absoluteUrl) { | ||
return new URL(url, absoluteUrl).href; | ||
} | ||
// NOTE: this is also implemented on the client side (copy pasted to enable unit testing `extractResources` with puppeteer) | ||
function extractResourcesFromStyleSheet(styleSheet) { | ||
@@ -24,2 +17,4 @@ const resourceUrls = [...styleSheet.cssRules].reduce((acc, rule) => { | ||
return acc.concat(getUrlFromCssText(rule.style.getPropertyValue('src'))); | ||
} else if (rule instanceof CSSSupportsRule) { | ||
return acc.concat(extractResourcesFromStyleSheet(rule)); | ||
} else if (rule instanceof CSSStyleRule) { | ||
@@ -26,0 +21,0 @@ for (let i = 0, ii = rule.style.length; i < ii; i++) { |
@@ -55,4 +55,25 @@ 'use strict'; | ||
async function getOrFetchResources(resourceUrls, cache) { | ||
async function getDependantResources({url, type, value}, cache) { | ||
let dependentResources, fetchedResources; | ||
if (/text\/css/.test(type)) { | ||
dependentResources = extractCssResources(value.toString(), url); | ||
fetchedResources = await getOrFetchResources(dependentResources, cache); | ||
} | ||
return {dependentResources, fetchedResources}; | ||
} | ||
async function processResource(resource, cache) { | ||
let {dependentResources, fetchedResources} = await getDependantResources(resource, cache); | ||
const rGridResource = fromFetchedToRGridResource(resource); | ||
cache.add(toCacheEntry(rGridResource), dependentResources); | ||
return Object.assign({[resource.url]: rGridResource}, fetchedResources); | ||
} | ||
async function getOrFetchResources(resourceUrls, cache, preResources = {}) { | ||
const resources = {}; | ||
for (const url in preResources) { | ||
Object.assign(resources, await processResource(preResources[url], cache)); | ||
} | ||
const missingResourceUrls = []; | ||
@@ -70,13 +91,5 @@ for (const url of resourceUrls) { | ||
missingResourceUrls.map(url => | ||
fetchResource(url).then(async resource => { | ||
let dependentResources; | ||
if (/text\/css/.test(resource.type)) { | ||
dependentResources = extractCssResources(resource.value.toString(), url); | ||
const fetchedResources = await getOrFetchResources(dependentResources, cache); | ||
Object.assign(resources, fetchedResources); | ||
} | ||
const rGridResource = fromFetchedToRGridResource(resource); | ||
resources[url] = rGridResource; | ||
cache.add(toCacheEntry(rGridResource), dependentResources); | ||
}), | ||
fetchResource(url).then(async resource => | ||
Object.assign(resources, await processResource(resource, cache)), | ||
), | ||
), | ||
@@ -88,4 +101,4 @@ ); | ||
async function getAllResources(absoluteUrls = []) { | ||
return await getOrFetchResources(absoluteUrls, allResources); | ||
async function getAllResources(absoluteUrls = [], preResources) { | ||
return await getOrFetchResources(absoluteUrls, allResources, preResources); | ||
} | ||
@@ -92,0 +105,0 @@ |
@@ -5,3 +5,4 @@ 'use strict'; | ||
const waitForRenderedStatus = require('./waitForRenderedStatus'); | ||
const {URL} = require('url'); | ||
const absolutizeUrl = require('./absolutizeUrl'); | ||
const {mapKeys, mapValues} = require('lodash'); | ||
const saveData = require('../troubleshoot/saveData'); | ||
@@ -12,2 +13,3 @@ const {setIsVerbose} = require('./log'); | ||
const {BatchInfo} = require('@applitools/eyes.sdk.core'); | ||
const extractCssResourcesFromCdt = require('./extractCssResourcesFromCdt'); | ||
@@ -30,6 +32,18 @@ // TODO replace with getInferredEnvironment once render service returns userAgent | ||
}) { | ||
async function checkWindow({resourceUrls, cdt, tag, sizeMode}) { | ||
async function checkWindow({ | ||
resourceUrls = [], | ||
resourceContents = {}, | ||
cdt, | ||
tag, | ||
sizeMode = 'full-page', | ||
}) { | ||
async function checkWindowJob(renderPromise, prevJobPromise, index) { | ||
const renderId = (await renderPromise)[index]; | ||
renderWrapper._logger.log( | ||
`render request complete for ${renderId}. tag=${tag} sizeMode=${sizeMode} browser: ${JSON.stringify( | ||
browsers[index], | ||
)}`, | ||
); | ||
const [screenshotUrl] = await waitForRenderedStatus([renderId], renderWrapper); | ||
renderWrapper._logger.log(`screenshot available for ${renderId} at ${screenshotUrl}`); | ||
await prevJobPromise; | ||
@@ -42,6 +56,2 @@ results.push(await wrappers[index].checkWindow({screenshotUrl, tag})); | ||
const absoluteUrls = | ||
resourceUrls && resourceUrls.map(resourceUrl => new URL(resourceUrl, url).href); | ||
const resources = await getAllResources(absoluteUrls); | ||
const renderRequests = createRenderRequests({ | ||
@@ -67,2 +77,9 @@ url, | ||
/******* checkWindow body start *******/ | ||
const resourceUrlsWithCss = resourceUrls.concat(extractCssResourcesFromCdt(cdt, url)); | ||
const absoluteUrls = resourceUrlsWithCss.map(resourceUrl => absolutizeUrl(resourceUrl, url)); | ||
const absoluteResourceContents = mapValues( | ||
mapKeys(resourceContents, (_value, key) => absolutizeUrl(key, url)), | ||
({url: resourceUrl, type, value}) => ({url: absolutizeUrl(resourceUrl, url), type, value}), | ||
); | ||
const resources = await getAllResources(absoluteUrls, absoluteResourceContents); | ||
@@ -69,0 +86,0 @@ const renderPromise = startRender(); |
@@ -29,3 +29,10 @@ 'use strict'; | ||
resolve(path, 'resources.json'), | ||
JSON.stringify(mapValues(resources, resource => resource.getContentType()), null, 2), | ||
JSON.stringify( | ||
mapValues(resources, resource => ({ | ||
type: resource.getContentType(), | ||
hash: resource.getSha256Hash(), | ||
})), | ||
null, | ||
2, | ||
), | ||
); | ||
@@ -32,0 +39,0 @@ Object.keys(resources).map(resourceUrl => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Git dependency
Supply chain riskContains a dependency which resolves to a remote git URL. Dependencies fetched from git URLs are not immutable can be used to inject untrusted code or reduce the likelihood of a reproducible install.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
47459
33
1095
12
1
+ Addedbody-parser@^1.18.2
- Removedcssom@0.3.1(transitive)
Updatedcssom@git+https://github.com/amitzur/CSSOM.git#925260ff2c8f8387cf76df4d5776a06044a644c8