cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
258 lines (257 loc) • 10.2 kB
JavaScript
const { _ } = Cypress;
import { C8yHighlightStyleDefaults, } from "../../shared/types";
Cypress.Commands.add("highlight", { prevSubject: "element" }, (subject, highlightStyle, options) => {
let customizedElements = Cypress.env("_c8yscrnCustomizedElements") || [];
let highlightElements = Cypress.env("_c8yscrnHighlightElements") || [];
if (_.isObject(options) && (options?.clear ?? false) === true) {
customizedElements.forEach(([$element, styles]) => $element.css(styles));
customizedElements = [];
highlightElements.forEach(($element) => $element.remove());
highlightElements = [];
}
const style = { ...(highlightStyle ?? C8yHighlightStyleDefaults) };
const consoleProps = {
style: style || null,
options: options || null,
};
const logger = Cypress.log({
name: "highlight",
consoleProps: () => consoleProps,
message: options?.clear === true ? "(clear)" : "",
$el: subject,
autoEnd: false,
});
if (subject.length === 0) {
logger.end();
return;
}
const applyElementStyle = ($elements) => {
const styledProperties = Object.keys(style);
const currentStyles = $elements.css(styledProperties);
customizedElements.push([$elements, currentStyles]);
$elements.css(style);
logger.set({ $el: $elements });
};
const applyMultiStyle = ($elements, width, height) => {
const $parent = Cypress.$("body").get(0);
let rect = getUnionDOMRect($elements, $parent);
let _w = rect.width;
if (width != null) {
_w = width <= 1 ? _w * width : width;
}
let _h = rect.height;
if (height != null) {
_h = height <= 1 ? _h * height : height;
}
rect = new DOMRectReadOnly(rect.x, rect.y, _w, _h);
const css = {
position: "absolute",
top: `${rect.top}px`,
left: `${rect.left}px`,
width: `${_w}px`,
height: `${_h}px`,
pointerEvents: "none",
...style,
};
const $container = Cypress.$("<div _c8yscrn-highlight-container></div>").css(css);
Cypress.$($parent).append($container);
highlightElements.push($container);
const container = {
rect: rect || null,
style: css || null,
element: $container || null,
parent: $parent || null,
};
if (consoleProps.container) {
if (_.isArray(consoleProps.container)) {
consoleProps.container.push(container);
}
else {
consoleProps.container = [consoleProps.container, container];
}
}
else {
consoleProps.container = container;
}
};
const needsSizeConstraints = options?.width != null || options?.height != null;
// we need to wait for the element to transition and animate into final
// position before we can calculate the absolute highlight area
cy.wait(500, { log: false }).then(() => {
const e = options?.multiple === true ? subject.toArray() : [subject];
e.forEach(($el) => {
const $element = !isJQueryElement($el) ? Cypress.$($el) : $el;
// check only single elements for disabled state
// multiple elements are the same as disabled anyway
const isDisabled = $element.length === 1 ? isElementDisabled($element) : false;
if ($element.length > 1 || needsSizeConstraints || isDisabled) {
applyMultiStyle($element, options?.width, options?.height);
}
else {
applyElementStyle($element);
}
});
});
cy.then(() => {
Cypress.env("_c8yscrnCustomizedElements", customizedElements);
Cypress.env("_c8yscrnHighlightElements", highlightElements);
logger.end();
return subject;
});
});
Cypress.Commands.add("clearHighlights", () => {
let customizedElements = Cypress.env("_c8yscrnCustomizedElements") || [];
let highlightElements = Cypress.env("_c8yscrnHighlightElements") || [];
const consoleProps = {
customizedElements: customizedElements || null,
highlightElements: highlightElements || null,
};
Cypress.log({
name: "clearHighlights",
consoleProps: () => consoleProps,
message: "",
});
customizedElements.forEach(([$element, styles]) => $element.css(styles));
customizedElements = [];
highlightElements.forEach(($element) => $element.remove());
highlightElements = [];
});
/**
* Calculates the union DOM rect of multiple elements within a common parent. If no
* parent is provided, the viewport is used for the calculation. The options object
* can be used to provide padding around the union rect.
*
* The union rect is the smallest rectangle that contains all elements.
*
* @param {JQuery<HTMLElement>} elements - The elements to calculate the union rect for
* @param {HTMLElement} parent - The parent element to calculate the union rect within
* @param {Object} options - The options to customize the union rect calculation
*/
export function getUnionDOMRect(elements, parent, options) {
let $parent;
let opts;
if (isHTMLElement(parent)) {
$parent = parent;
opts = options;
}
else if (options != null) {
opts = options;
}
else if (_.isPlainObject(parent)) {
opts = parent;
}
const p = opts?.padding;
const unionRect = elements.toArray().reduce((acc, el) => {
const rect = $parent != null
? getElementPositionWithinParent(el, $parent)
: el.getBoundingClientRect();
acc.top = Math.min(acc.top, rect.top);
acc.left = Math.min(acc.left, rect.left);
acc.bottom = Math.max(acc.bottom, rect.bottom);
acc.right = Math.max(acc.right, rect.right);
return acc;
}, {
top: Infinity,
left: Infinity,
bottom: -Infinity,
right: -Infinity,
});
let padding = _.isNumber(p) ? [p, p, p, p] : p;
if (!_.isArray(padding) || !_.every(padding, _.isNumber)) {
padding = [0, 0, 0, 0];
}
else {
// map clockwise use in Cypress.Padding to our padding [left, top, right, bottom]
// see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Shorthand_properties
// this ensures compatibility with global Cypress SchreenShotOptions used with Cypress.Padding
// [left, top, right, bottom]
if (padding.length === 1) {
padding = [padding[0], padding[0], padding[0], padding[0]];
}
else if (padding.length === 2) {
padding = [padding[1], padding[0], padding[1], padding[0]];
}
else if (padding.length === 3) {
padding = [padding[1], padding[0], padding[1], padding[2]];
}
else if (padding.length >= 4) {
padding = [padding[3], padding[0], padding[1], padding[2]];
}
}
const x = unionRect.left - padding[0];
if (x < 0) {
padding[0] = unionRect.left;
}
const y = unionRect.top - padding[1];
if (y < 0) {
padding[1] = unionRect.top;
}
return new DOMRectReadOnly(unionRect.left - padding[0], unionRect.top - padding[1], unionRect.right - unionRect.left + padding[2] + padding[0], unionRect.bottom - unionRect.top + padding[3] + padding[1]);
}
function isJQueryElement(obj) {
return (obj instanceof Cypress.$ && _.isArray(obj) && obj[0] instanceof HTMLElement);
}
function isHTMLElement(obj) {
return (obj &&
typeof obj === "object" &&
obj.nodeType === 1 &&
typeof obj.nodeName === "string");
}
/**
* Finds the common parent element of multiple elements
* @param {JQuery<HTMLElement>} elements - The elements to find the common parent for
* @returns {HTMLElement} - The common parent element
*/
export function findCommonParent($elements) {
if (!$elements || $elements.length === 0)
return undefined;
const getParents = (element) => {
const parents = [];
while (element.parentElement) {
parents.push(element.parentElement);
element = element.parentElement;
}
return parents;
};
const firstElementParents = getParents($elements[0]);
for (const parent of firstElementParents) {
const isCommonParent = Array.from($elements).every((el) => hasParent(el, parent));
const r = parent.getBoundingClientRect();
// When an element has display: contents, it is rendered as if it weren't there
// at all. Its children are rendered as if they were direct children of its parent.
// This means that the element itself doesn't have a bounding rectangle, which is
// why getBoundingClientRect() returns 0 for all values.
// Make sure that the parent has a width and height so the parent is actually visible
if (isCommonParent === true && r.width > 0 && r.height > 0) {
return parent;
}
}
return undefined;
}
/**
* Checks if an element has a specific parent element
* @param {HTMLElement} element - The element to check
* @param {HTMLElement} parent - The parent element to check for
* @returns {boolean} - True if the element has the parent element
*/
function hasParent(element, parent) {
let currentElement = element;
while (currentElement.parentElement) {
if (currentElement.parentElement === parent) {
return true;
}
currentElement = currentElement.parentElement;
}
return false;
}
function getElementPositionWithinParent($e, $p) {
const childRect = $e.getBoundingClientRect();
const parentRect = $p.getBoundingClientRect();
return new DOMRectReadOnly(childRect.left - parentRect.left, childRect.top - parentRect.top, childRect.width, childRect.height);
}
function isElementDisabled($element) {
return ($element.is(":disabled") ||
$element.attr("aria-disabled") === "true" ||
$element.hasClass("disabled") ||
$element.css("pointer-events") === "none");
}