@percy/agent
Version:
An agent process for integrating with Percy.
253 lines (252 loc) • 11.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const FORM_ELEMENTS_SELECTOR = 'input, textarea, select';
/**
* A single class to encapsulate all DOM operations that need to be performed to
* capture the customer's application state.
*
*/
class DOM {
constructor(dom, options) {
this.defaultDoctype = '';
this.originalDOM = dom;
this.options = options || {};
this.clonedDOM = this.cloneDOM();
}
/**
* Returns the final DOM string with all of the necessary transforms
* applied. This is the string that is passed to the API and then rendered by
* our API.
*
*/
snapshotString() {
// any since the cloned DOMs type can shift
let dom = this.clonedDOM;
const doctype = this.getDoctype();
// Sometimes you'll want to transform the DOM provided into one ready for snapshotting
// For example, if your test suite runs tests in an element inside a page that
// lists all yours tests. You'll want to "hoist" the contents of the testing container to be
// the full page. Using a dom transformation is how you'd achieve that.
if (this.options.domTransformation) {
try {
dom = this.options.domTransformation(dom);
}
catch (error) {
console.error('Could not transform the dom: ', error.toString());
}
}
return doctype + dom.outerHTML;
}
getDoctype() {
return this.clonedDOM.doctype
? this.doctypeToString(this.clonedDOM.doctype)
: this.defaultDoctype;
}
doctypeToString(doctype) {
const publicDeclaration = doctype.publicId
? ` PUBLIC "${doctype.publicId}" `
: '';
const systemDeclaration = doctype.systemId
? ` SYSTEM "${doctype.systemId}" `
: '';
return (` 0;
return !hasHref && !hasStyleInDom && styleSheet.cssRules;
}
if (isCSSOM()) {
const $style = documentClone.createElement('style');
const cssRules = Array.from(styleSheet.cssRules);
const serializedStyles = cssRules.reduce((prev, cssRule) => {
return prev + cssRule.cssText;
}, '');
// Append the serialized styles to the cloned document
$style.type = 'text/css';
$style.setAttribute('data-percy-cssom-serialized', 'true');
$style.innerHTML = serializedStyles;
// TODO, it'd be better if we appended it right after the ownerNode in the clone
documentClone.head.appendChild($style);
}
});
}
/**
* Capture in-memory canvas elements & serialize them to images into the
* cloned DOM.
*
* Without this, applications that have canvas elements will be missing and
* appear broken. The Canvas DOM API allows you to covert them to images, which
* is what we're doing here to capture that in-memory state & serialize it
* into the DOM Percy captures.
*
* It's important to note the `.toDataURL` API requires WebGL canvas elements
* to use `preserveDrawingBuffer: true`. This is because `.toDataURL` captures
* from the drawing buffer, which is cleared after each render by default for
* performance.
*
*/
serializeCanvasElements(clonedDOM) {
for (const $canvas of this.originalDOM.querySelectorAll('canvas')) {
const $image = clonedDOM.createElement('img');
const canvasId = $canvas.getAttribute('data-percy-element-id');
const $clonedCanvas = clonedDOM.querySelector(`[data-percy-element-id=${canvasId}]`);
$image.setAttribute('style', 'max-width: 100%');
$image.classList.add('percy-canvas-image');
$image.src = $canvas.toDataURL();
$image.setAttribute('data-percy-canvas-serialized', 'true');
$clonedCanvas.parentElement.insertBefore($image, $clonedCanvas);
$clonedCanvas.remove();
}
}
/**
* A single place to mutate the original DOM. This should be the last resort!
* This will change the customer's DOM and have a possible impact on the
* customer's application.
*
*/
mutateOriginalDOM() {
const createUID = () => `_${Math.random().toString(36).substr(2, 9)}`;
const formNodes = this.originalDOM.querySelectorAll(FORM_ELEMENTS_SELECTOR);
const frameNodes = this.originalDOM.querySelectorAll('iframe');
const canvasNodes = this.originalDOM.querySelectorAll('canvas');
const elements = [...formNodes, ...frameNodes, ...canvasNodes];
// loop through each element and apply an ID for serialization later
elements.forEach((elem) => {
if (!elem.getAttribute('data-percy-element-id')) {
elem.setAttribute('data-percy-element-id', createUID());
}
});
}
}
exports.default = DOM;