Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@percy/dom

Package Overview
Dependencies
Maintainers
1
Versions
240
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@percy/dom - npm Package Compare versions

Comparing version 1.29.1-alpha.0 to 1.29.1-beta.0

475

dist/bundle.js

@@ -9,44 +9,120 @@ (function() {

// Creates a resource object from an element's unique ID and data URL
function resourceFromDataURL(uid, dataURL) {
// split dataURL into desired parts
let [data, content] = dataURL.split(',');
let [, mimetype] = data.split(':');
[mimetype] = mimetype.split(';');
// build a URL for the serialized asset
let [, ext] = mimetype.split('/');
let path = `/__serialized__/${uid}.${ext}`;
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
// return the url, base64 content, and mimetype
return {
url,
content,
mimetype
};
}
function resourceFromText(uid, mimetype, data) {
// build a URL for the serialized asset
let [, ext] = mimetype.split('/');
let path = `/__serialized__/${uid}.${ext}`;
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
// return the url, text content, and mimetype
return {
url,
content: data,
mimetype
};
}
function styleSheetFromNode(node) {
/* istanbul ignore if: sanity check */
if (node.sheet) return node.sheet;
// Cloned style nodes don't have a sheet instance unless they are within
// a document; we get it by temporarily adding the rules to DOM
const tempStyle = node.cloneNode();
tempStyle.setAttribute('data-percy-style-helper', '');
tempStyle.innerHTML = node.innerHTML;
const clone = document.cloneNode();
clone.appendChild(tempStyle);
const sheet = tempStyle.sheet;
// Cleanup node
tempStyle.remove();
return sheet;
}
function rewriteLocalhostURL(url) {
return url.replace(/(http[s]{0,1}:\/\/)(localhost|127.0.0.1)[:\d+]*/, '$1render.percy.local');
}
// Utility function to handle errors
function handleErrors(error, prefixMessage) {
let element = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
let additionalData = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
let elementData = {};
if (element) {
elementData = {
nodeName: element.nodeName,
classNames: element.className,
id: element.id
};
}
additionalData = {
...additionalData,
...elementData
};
error.message += `\n${prefixMessage} \n${JSON.stringify(additionalData)}`;
error.message += '\n Please validate that your DOM is as per W3C standards using any online tool';
error.handled = true;
throw error;
}
// Translates JavaScript properties of inputs into DOM attributes.
function serializeInputElements(_ref) {
function serializeInputElements(ctx) {
let {
dom,
clone,
warnings
} = _ref;
clone
} = ctx;
for (let elem of dom.querySelectorAll('input, textarea, select')) {
let inputId = elem.getAttribute('data-percy-element-id');
let cloneEl = clone.querySelector(`[data-percy-element-id="${inputId}"]`);
switch (elem.type) {
case 'checkbox':
case 'radio':
/*
here we are removing the checked attr if present by default,
so that only the current selected radio-button will have the checked attr present in the dom
this happens because in html,
when the checked attribute is present in the multiple radio-buttons for which only one can be selected at a time,
the browser will only render the last checked radio-button by default,
when a user selects any particular radio-button, the checked attribute on other buttons is not removed,
hence sometimes it shows inconsistent state as html will still show the last radio as selected.
*/
cloneEl.removeAttribute('checked');
if (elem.checked) {
cloneEl.setAttribute('checked', '');
}
break;
case 'select-one':
if (elem.selectedIndex !== -1) {
cloneEl.options[elem.selectedIndex].setAttribute('selected', 'true');
}
break;
case 'select-multiple':
for (let option of elem.selectedOptions) {
cloneEl.options[option.index].setAttribute('selected', 'true');
}
break;
case 'textarea':
cloneEl.innerHTML = elem.value;
break;
default:
cloneEl.setAttribute('value', elem.value);
try {
let inputId = elem.getAttribute('data-percy-element-id');
let cloneEl = clone.querySelector(`[data-percy-element-id="${inputId}"]`);
switch (elem.type) {
case 'checkbox':
case 'radio':
/*
here we are removing the checked attr if present by default,
so that only the current selected radio-button will have the checked attr present in the dom
this happens because in html,
when the checked attribute is present in the multiple radio-buttons for which only one can be selected at a time,
the browser will only render the last checked radio-button by default,
when a user selects any particular radio-button, the checked attribute on other buttons is not removed,
hence sometimes it shows inconsistent state as html will still show the last radio as selected.
*/
cloneEl.removeAttribute('checked');
if (elem.checked) {
cloneEl.setAttribute('checked', '');
}
break;
case 'select-one':
if (elem.selectedIndex !== -1) {
cloneEl.options[elem.selectedIndex].setAttribute('selected', 'true');
}
break;
case 'select-multiple':
for (let option of elem.selectedOptions) {
cloneEl.options[option.index].setAttribute('selected', 'true');
}
break;
case 'textarea':
cloneEl.innerHTML = elem.value;
break;
default:
cloneEl.setAttribute('value', elem.value);
}
} catch (err) {
handleErrors(err, 'Error serializing input element: ', elem);
}

@@ -121,53 +197,2 @@ }

// Creates a resource object from an element's unique ID and data URL
function resourceFromDataURL(uid, dataURL) {
// split dataURL into desired parts
let [data, content] = dataURL.split(',');
let [, mimetype] = data.split(':');
[mimetype] = mimetype.split(';');
// build a URL for the serialized asset
let [, ext] = mimetype.split('/');
let path = `/__serialized__/${uid}.${ext}`;
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
// return the url, base64 content, and mimetype
return {
url,
content,
mimetype
};
}
function resourceFromText(uid, mimetype, data) {
// build a URL for the serialized asset
let [, ext] = mimetype.split('/');
let path = `/__serialized__/${uid}.${ext}`;
let url = rewriteLocalhostURL(new URL(path, document.URL).toString());
// return the url, text content, and mimetype
return {
url,
content: data,
mimetype
};
}
function styleSheetFromNode(node) {
/* istanbul ignore if: sanity check */
if (node.sheet) return node.sheet;
// Cloned style nodes don't have a sheet instance unless they are within
// a document; we get it by temporarily adding the rules to DOM
const tempStyle = node.cloneNode();
tempStyle.setAttribute('data-percy-style-helper', '');
tempStyle.innerHTML = node.innerHTML;
const clone = document.cloneNode();
clone.appendChild(tempStyle);
const sheet = tempStyle.sheet;
// Cleanup node
tempStyle.remove();
return sheet;
}
function rewriteLocalhostURL(url) {
return url.replace(/(http[s]{0,1}:\/\/)(localhost|127.0.0.1)[:\d+]*/, '$1render.percy.local');
}
// Returns a mostly random uid.

@@ -216,3 +241,3 @@ function uid() {

}
function serializeCSSOM(_ref) {
function serializeCSSOM(ctx) {
let {

@@ -224,3 +249,3 @@ dom,

warnings
} = _ref;
} = ctx;
// in-memory CSSOM into their respective DOM nodes.

@@ -238,26 +263,40 @@ let styleSheets = null;

if (isCSSOM(styleSheet)) {
let styleId = styleSheet.ownerNode.getAttribute('data-percy-element-id');
let cloneOwnerNode = clone.querySelector(`[data-percy-element-id="${styleId}"]`);
if (styleSheetsMatch(styleSheet, styleSheetFromNode(cloneOwnerNode))) continue;
let style = document.createElement('style');
style.type = 'text/css';
style.setAttribute('data-percy-element-id', styleId);
style.setAttribute('data-percy-cssom-serialized', 'true');
style.innerHTML = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling);
cloneOwnerNode.remove();
let styleId;
let cloneOwnerNode;
try {
styleId = styleSheet.ownerNode.getAttribute('data-percy-element-id');
cloneOwnerNode = clone.querySelector(`[data-percy-element-id="${styleId}"]`);
if (styleSheetsMatch(styleSheet, styleSheetFromNode(cloneOwnerNode))) continue;
let style = document.createElement('style');
style.type = 'text/css';
style.setAttribute('data-percy-element-id', styleId);
style.setAttribute('data-percy-cssom-serialized', 'true');
style.innerHTML = Array.from(styleSheet.cssRules).map(cssRule => cssRule.cssText).join('\n');
cloneOwnerNode.parentNode.insertBefore(style, cloneOwnerNode.nextSibling);
cloneOwnerNode.remove();
} catch (err) {
handleErrors(err, 'Error serializing stylesheet: ', cloneOwnerNode, {
styleId: styleId
});
}
} else if ((_styleSheet$href = styleSheet.href) !== null && _styleSheet$href !== void 0 && _styleSheet$href.startsWith('blob:')) {
const styleLink = document.createElement('link');
styleLink.setAttribute('rel', 'stylesheet');
let resource = createStyleResource(styleSheet);
resources.add(resource);
styleLink.setAttribute('data-percy-blob-stylesheets-serialized', 'true');
styleLink.setAttribute('data-percy-serialized-attribute-href', resource.url);
try {
const styleLink = document.createElement('link');
styleLink.setAttribute('rel', 'stylesheet');
let resource = createStyleResource(styleSheet);
resources.add(resource);
styleLink.setAttribute('data-percy-blob-stylesheets-serialized', 'true');
styleLink.setAttribute('data-percy-serialized-attribute-href', resource.url);
/* istanbul ignore next: tested, but coverage is stripped */
if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
// handle document and iframe
clone.body.prepend(styleLink);
} else if (clone.constructor.name === 'ShadowRoot') {
clone.prepend(styleLink);
/* istanbul ignore next: tested, but coverage is stripped */
if (clone.constructor.name === 'HTMLDocument' || clone.constructor.name === 'DocumentFragment') {
// handle document and iframe
clone.body.prepend(styleLink);
} else if (clone.constructor.name === 'ShadowRoot') {
clone.prepend(styleLink);
}
} catch (err) {
handleErrors(err, 'Error serializing stylesheet from blob: ', null, {
stylesheetHref: styleSheet.href
});
}

@@ -297,3 +336,3 @@ }

// Serialize in-memory canvas elements into images.
function serializeCanvas(_ref) {
function serializeCanvas(ctx) {
let {

@@ -303,45 +342,49 @@ dom,

resources
} = _ref;
} = ctx;
for (let canvas of dom.querySelectorAll('canvas')) {
// Note: the `.toDataURL` API requires WebGL canvas elements to use
// `preserveDrawingBuffer: true`. This is because `.toDataURL` uses the
// drawing buffer, which is cleared after each render for WebGL by default.
let dataUrl = canvas.toDataURL();
try {
// Note: the `.toDataURL` API requires WebGL canvas elements to use
// `preserveDrawingBuffer: true`. This is because `.toDataURL` uses the
// drawing buffer, which is cleared after each render for WebGL by default.
let dataUrl = canvas.toDataURL();
// skip empty canvases
if (!dataUrl || dataUrl === 'data:,') continue;
// skip empty canvases
if (!dataUrl || dataUrl === 'data:,') continue;
// get the element's percy id and create a resource for it
let percyElementId = canvas.getAttribute('data-percy-element-id');
let resource = resourceFromDataURL(percyElementId, dataUrl);
resources.add(resource);
// get the element's percy id and create a resource for it
let percyElementId = canvas.getAttribute('data-percy-element-id');
let resource = resourceFromDataURL(percyElementId, dataUrl);
resources.add(resource);
// create an image element in the cloned dom
let img = document.createElement('img');
// use a data attribute to avoid making a real request
img.setAttribute('data-percy-serialized-attribute-src', resource.url);
// create an image element in the cloned dom
let img = document.createElement('img');
// use a data attribute to avoid making a real request
img.setAttribute('data-percy-serialized-attribute-src', resource.url);
// copy canvas element attributes to the image element such as style, class,
// or data attributes that may be targeted by CSS
for (let {
name,
value
} of canvas.attributes) {
img.setAttribute(name, value);
}
// copy canvas element attributes to the image element such as style, class,
// or data attributes that may be targeted by CSS
for (let {
name,
value
} of canvas.attributes) {
img.setAttribute(name, value);
}
// mark the image as serialized (can be targeted by CSS)
img.setAttribute('data-percy-canvas-serialized', '');
// set a default max width to account for canvases that might resize with JS
img.style.maxWidth = img.style.maxWidth || '100%';
// mark the image as serialized (can be targeted by CSS)
img.setAttribute('data-percy-canvas-serialized', '');
// set a default max width to account for canvases that might resize with JS
img.style.maxWidth = img.style.maxWidth || '100%';
// insert the image into the cloned DOM and remove the cloned canvas element
let cloneEl = clone.querySelector(`[data-percy-element-id=${percyElementId}]`);
// `parentElement` for elements directly under shadow root is `null` -> Incase of Nested Shadow DOM.
if (cloneEl.parentElement) {
cloneEl.parentElement.insertBefore(img, cloneEl);
} else {
clone.insertBefore(img, cloneEl);
// insert the image into the cloned DOM and remove the cloned canvas element
let cloneEl = clone.querySelector(`[data-percy-element-id=${percyElementId}]`);
// `parentElement` for elements directly under shadow root is `null` -> Incase of Nested Shadow DOM.
if (cloneEl.parentElement) {
cloneEl.parentElement.insertBefore(img, cloneEl);
} else {
clone.insertBefore(img, cloneEl);
}
cloneEl.remove();
} catch (err) {
handleErrors(err, 'Error serializing canvas element: ', canvas);
}
cloneEl.remove();
}

@@ -351,3 +394,3 @@ }

// Captures the current frame of videos and sets the poster image
function serializeVideos(_ref) {
function serializeVideos(ctx) {
let {

@@ -358,28 +401,32 @@ dom,

warnings
} = _ref;
} = ctx;
for (let video of dom.querySelectorAll('video')) {
// if the video already has a poster image, no work for us to do
if (video.getAttribute('poster')) continue;
let videoId = video.getAttribute('data-percy-element-id');
let cloneEl = clone.querySelector(`[data-percy-element-id="${videoId}"]`);
let canvas = document.createElement('canvas');
let width = canvas.width = video.videoWidth;
let height = canvas.height = video.videoHeight;
let dataUrl;
canvas.getContext('2d').drawImage(video, 0, 0, width, height);
try {
dataUrl = canvas.toDataURL();
} catch (e) {
warnings.add(`data-percy-element-id="${videoId}" : ${e.toString()}`);
}
// if the video already has a poster image, no work for us to do
if (video.getAttribute('poster')) continue;
let videoId = video.getAttribute('data-percy-element-id');
let cloneEl = clone.querySelector(`[data-percy-element-id="${videoId}"]`);
let canvas = document.createElement('canvas');
let width = canvas.width = video.videoWidth;
let height = canvas.height = video.videoHeight;
let dataUrl;
canvas.getContext('2d').drawImage(video, 0, 0, width, height);
try {
dataUrl = canvas.toDataURL();
} catch (e) {
warnings.add(`data-percy-element-id="${videoId}" : ${e.toString()}`);
}
// if the canvas produces a blank image, skip
if (!dataUrl || dataUrl === 'data:,') continue;
// if the canvas produces a blank image, skip
if (!dataUrl || dataUrl === 'data:,') continue;
// create a resource from the serialized data url
let resource = resourceFromDataURL(videoId, dataUrl);
resources.add(resource);
// create a resource from the serialized data url
let resource = resourceFromDataURL(videoId, dataUrl);
resources.add(resource);
// use a data attribute to avoid making a real request
cloneEl.setAttribute('data-percy-serialized-attribute-poster', resource.url);
// use a data attribute to avoid making a real request
cloneEl.setAttribute('data-percy-serialized-attribute-poster', resource.url);
} catch (err) {
handleErrors(err, 'Error serializing video element: ', video);
}
}

@@ -446,3 +493,3 @@ }

const ignoreTags = ['NOSCRIPT'];
function cloneNodeAndShadow(_ref) {
function cloneNodeAndShadow(ctx) {
let {

@@ -452,45 +499,53 @@ dom,

resources
} = _ref;
} = ctx;
// clones shadow DOM and light DOM for a given node
let cloneNode = (node, parent) => {
let walkTree = (nextn, nextp) => {
while (nextn) {
if (!ignoreTags.includes(nextn.nodeName)) {
cloneNode(nextn, nextp);
try {
let walkTree = (nextn, nextp) => {
while (nextn) {
if (!ignoreTags.includes(nextn.nodeName)) {
cloneNode(nextn, nextp);
}
nextn = nextn.nextSibling;
}
nextn = nextn.nextSibling;
}
};
};
// mark the node before cloning
markElement(node, disableShadowDOM);
let clone = node.cloneNode();
// mark the node before cloning
markElement(node, disableShadowDOM);
let clone = node.cloneNode();
// We apply any element transformations here to avoid another treeWalk
applyElementTransformations(clone);
serializeBase64(clone, resources);
parent.appendChild(clone);
// We apply any element transformations here to avoid another treeWalk
applyElementTransformations(clone);
serializeBase64(clone, resources);
parent.appendChild(clone);
// shallow clone should not contain children
if (clone.children) {
Array.from(clone.children).forEach(child => clone.removeChild(child));
}
// shallow clone should not contain children
if (clone.children) {
Array.from(clone.children).forEach(child => clone.removeChild(child));
}
// clone shadow DOM
if (node.shadowRoot && !disableShadowDOM) {
// create shadowRoot
if (clone.shadowRoot) {
// it may be set up in a custom element's constructor
clone.shadowRoot.innerHTML = '';
// clone shadow DOM
if (node.shadowRoot && !disableShadowDOM) {
// create shadowRoot
if (clone.shadowRoot) {
// it may be set up in a custom element's constructor
clone.shadowRoot.innerHTML = '';
} else {
clone.attachShadow({
mode: 'open'
});
}
// clone dom elements
walkTree(node.shadowRoot.firstChild, clone.shadowRoot);
}
// clone light DOM
walkTree(node.firstChild, clone);
} catch (err) {
if (!err.handled) {
handleErrors(err, 'Error cloning node: ', node);
} else {
clone.attachShadow({
mode: 'open'
});
throw err;
}
// clone dom elements
walkTree(node.shadowRoot.firstChild, clone.shadowRoot);
}
// clone light DOM
walkTree(node.firstChild, clone);
};

@@ -616,4 +671,14 @@ let fragment = dom.createDocumentFragment();

}
let cookies = '';
// Collecting cookies fail for about://blank page
try {
cookies = dom.cookie;
} catch (err) /* istanbul ignore next */ /* Tested this part in discovery.test.js with about:blank page */{
const errorMessage = `Could not capture cookies: ${err.message}`;
ctx.warnings.add(errorMessage);
console.error(errorMessage);
}
let result = {
html: serializeHTML(ctx),
cookies: cookies,
warnings: Array.from(ctx.warnings),

@@ -620,0 +685,0 @@ resources: Array.from(ctx.resources),

{
"name": "@percy/dom",
"version": "1.29.1-alpha.0",
"version": "1.29.1-beta.0",
"license": "MIT",

@@ -12,3 +12,3 @@ "repository": {

"access": "public",
"tag": "alpha"
"tag": "beta"
},

@@ -39,3 +39,3 @@ "main": "dist/bundle.js",

},
"gitHead": "5044adb5caa7507fdec629eda8f33d6ddde07997"
"gitHead": "d325b7bbe56764dbde494477d1f4f3bfdc562d6e"
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc