cy-mobile-commands
Version:
Mobile testing helper for Cypress
281 lines (280 loc) • 11.2 kB
JavaScript
/// <reference types="cypress" />
const SVG_NS = 'http://www.w3.org/2000/svg';
const COLORS = ['#00F', '#0CE', '#0E0', '#EA0', '#F00'];
class SVGCanvas {
constructor(doc) {
this.doc = doc;
this.svg = doc.createElementNS(SVG_NS, 'svg');
this.svg.setAttribute('width', doc.body.clientWidth.toString());
this.svg.setAttribute('height', doc.body.clientHeight.toString());
this.svg.style.position = 'absolute';
this.svg.style.top = '0';
this.svg.style.left = '0';
this.svg.style.zIndex = '99999999';
this.svg.style.pointerEvents = 'none';
doc.body.appendChild(this.svg);
this.lines = [];
}
startLine(finger, x, y) {
let line = this.doc.createElementNS(SVG_NS, 'path');
line.setAttribute('fill', 'transparent');
line.setAttribute('stroke', '#FFF'); // COLORS[finger])
line.setAttribute('stroke-width', '6');
line.setAttribute('stroke-linecap', 'round');
line.setAttribute('stroke-linejoin', 'round');
line.setAttribute('opacity', '0.5');
line.setAttribute('d', `M ${x},${y}`);
this.svg.appendChild(line);
this.lines[finger] = line;
}
extendLine(finger, x, y) {
const line = this.lines[finger];
line.setAttribute('d', line.getAttribute('d') + ` L ${x},${y}`);
}
mkDot(className, x, y, radius, color, stroke) {
let dot = this.doc.createElementNS(SVG_NS, 'circle');
dot.setAttribute('cx', x.toString());
dot.setAttribute('cy', y.toString());
dot.setAttribute('r', radius.toString());
dot.setAttribute('class', className);
if (stroke) {
let dotBorder = this.doc.createElementNS(SVG_NS, 'circle');
dotBorder.setAttribute('cx', x.toString());
dotBorder.setAttribute('cy', y.toString());
dotBorder.setAttribute('r', radius.toString());
dotBorder.setAttribute('opacity', '0.5');
dotBorder.setAttribute('fill', 'transparent');
dotBorder.setAttribute('stroke', '#FFF');
dotBorder.setAttribute('stroke-width', `${stroke + 4}`);
this.svg.appendChild(dotBorder);
dot.setAttribute('fill', 'transparent');
dot.setAttribute('stroke', color);
dot.setAttribute('stroke-width', stroke.toString());
}
else {
dot.setAttribute('fill', color);
}
dot.setAttribute('opacity', '0.6');
this.svg.appendChild(dot);
return dot;
}
touchstart(finger, x, y) {
this.mkDot('start-' + finger, x, y, 8, COLORS[finger], 3);
this.startLine(finger, x, y);
}
touchmove(finger, x, y, checkpoint) {
const className = (checkpoint ? 'checkpoint-' : 'move-') + finger;
this.mkDot(className, x, y, (checkpoint ? 4 : 2), COLORS[finger]);
this.extendLine(finger, x, y);
}
touchend(finger, x, y) {
this.mkDot('end-' + finger, x, y, 5, COLORS[finger], 3);
this.extendLine(finger, x, y);
if (finger === 0)
setTimeout(() => this.doc.body.removeChild(this.svg), 100);
}
}
function getOffset(element, offset = { top: 0, left: 0 }) {
if (element.offsetTop && element.offsetLeft) {
offset = {
top: element.offsetTop + offset.top,
left: element.offsetLeft + offset.left
};
}
return element.parentElement ? getOffset(element.parentElement, offset) : offset;
}
function normalizeCheckpointFingers(target, fingers) {
if (typeof (fingers) === 'string')
return [notationToPoint(target, fingers)];
if (typeof (fingers[0]) == 'number' && typeof (fingers[1]) == 'number')
return [fingers];
if (fingers instanceof Array)
return fingers.map((fingerPos) => notationToPoint(target, fingerPos));
else
throw Error('Invalid chekpoint definition ${JSON.stringify(fingers)}.');
}
function notationToPoint(target, position) {
if (position instanceof Array && typeof (position[0]) == 'number' && typeof (position[1]) == 'number') {
return position;
}
let box = target[0].getBoundingClientRect();
let paddingW = box.width / 10;
let paddingH = box.height / 10;
switch (position) {
case 'left':
return [
Math.ceil(box.left + paddingW),
Math.round(box.top + box.height / 2)
];
case 'right':
return [
Math.floor(box.left + box.width - paddingW),
Math.round(box.top + box.height / 2)
];
case 'top':
return [
Math.round(box.left + box.width / 2),
Math.ceil(box.top + paddingH)
];
case 'bottom':
return [
Math.round(box.left + box.width / 2),
Math.floor(box.top + box.height - paddingH)
];
case 'top-left':
case 'topLeft':
return [
Math.ceil(box.left + paddingW),
Math.round(box.top + paddingW)
];
case 'top-right':
case 'topRight':
return [
Math.floor(box.left + box.width - paddingW),
Math.round(box.top + paddingW)
];
case 'bottom-left':
case 'bottomLeft':
return [
Math.ceil(box.left + paddingW),
Math.round(box.top + box.height - paddingW)
];
case 'bottom-right':
case 'bottomRight':
return [
Math.floor(box.left + box.width - paddingW),
Math.round(box.top + box.height - paddingW)
];
case 'center':
return [
Math.round(box.left + box.width / 2),
Math.floor(box.top + box.height / 2)
];
default:
throw Error(`Invalid position definition ${JSON.stringify(position)}`);
}
}
Cypress.Commands.add('visitMobile', (...args) => {
let conf = args[0];
if (args.length === 2)
conf = Object.assign({ url: args[0] }, args[1]);
if (typeof (conf) === 'string')
conf = { url: conf };
const userBeforeLoad = conf.onBeforeLoad || (() => void (0));
conf.onBeforeLoad = (win) => {
win.ontouchstart = null;
return userBeforeLoad(win);
};
return cy.visit(conf);
});
Cypress.Commands.add('swipe', { prevSubject: 'element' }, (target, ...path) => {
let config = { delay: 300, draw: true };
if (typeof (path[0]) !== 'string' && !('length' in path[0])) {
config = Object.assign(config, path.shift());
}
new Swipe(target, config, path);
});
class Swipe {
constructor(target, { steps = 0, delay, draw }, path) {
this.target = target;
this.touchCanvas = null;
this.delay = delay;
if (!steps) {
steps = Math.round(12 / (path.length - 1));
if (steps < 2)
steps = 2;
}
this.stepDelay = Math.round(this.delay / (steps * (path.length - 1)));
if (this.stepDelay > 150) {
steps *= 2;
this.stepDelay = Math.round(this.delay / (steps * (path.length - 1)));
}
this.steps = steps;
this.path = path;
if (draw) {
this.touchCanvas = new SVGCanvas(this.target[0].ownerDocument);
}
cy.window({ log: false }).then((win) => {
this.win = win;
this.doIt();
});
}
doIt() {
this.promiseChain = Cypress.Promise.resolve();
for (let checkpoint = 1; checkpoint < this.path.length; checkpoint++) {
this.updateFingerMove(this.path[checkpoint - 1], this.path[checkpoint], checkpoint);
}
const myTarget = this.target;
const myPath = this.path.map((step) => `${step}: ${normalizeCheckpointFingers(myTarget, step)}`);
Cypress.log({
$el: myTarget,
name: 'do swipe',
message: this.path.join(', '),
consoleProps: () => ({ target: myTarget[0], delay: this.delay, path: myPath })
});
cy.wait(0, { log: false }).then(() => this.promiseChain);
}
updateFingerMove(from, to, checkpoint) {
var _a, _b, _c;
let fingersFrom = normalizeCheckpointFingers(this.target, from);
let fingersTo = normalizeCheckpointFingers(this.target, to);
let checkpointEv = checkpoint === 1 ? 'touchstart' : 'touchmove';
let evConfCP = { fingers: fingersFrom, checkpoint: true };
(_a = this.promiseChain) === null || _a === void 0 ? void 0 : _a.then(() => this.dispatchTouchEvent(checkpointEv, evConfCP));
for (let i = 1; i < this.steps; i++) {
let fingers = fingersFrom.map(([x, y], f) => [
x * (1 - i / this.steps) + fingersTo[f][0] * (i / this.steps),
y * (1 - i / this.steps) + fingersTo[f][1] * (i / this.steps)
]);
(_b = this.promiseChain) === null || _b === void 0 ? void 0 : _b.then(() => this.dispatchTouchEvent('touchmove', { fingers }));
}
if (checkpoint === this.path.length - 1) {
let evConfEnd = { fingers: fingersTo };
(_c = this.promiseChain) === null || _c === void 0 ? void 0 : _c.then(() => this.dispatchTouchEvent('touchend', evConfEnd));
}
}
dispatchTouchEvent(evName, { fingers, checkpoint }) {
let scrollX = this.win.scrollX;
let scrollY = this.win.scrollY;
let touches = fingers.map(([x, y], index) => {
x = Math.round(x);
y = Math.round(y);
return new Touch({
identifier: index,
target: this.target[0],
screenX: x,
screenY: y,
clientX: x,
clientY: y,
pageX: x + scrollX,
pageY: y + scrollY
});
});
let conf = {
bubbles: true,
composed: true,
isTrusted: true,
cancelable: true,
touches: touches,
targetTouches: touches,
changedTouches: touches,
sourceCapabilities: new InputDeviceCapabilities({ firesTouchEvents: true })
};
cy.wait(this.stepDelay, { log: false }).then(() => {
const baseName = 'swipe ' + (evName === 'touchstart' ? 'start' :
evName === 'touchend' ? 'end' : 'checkpoint');
touches.forEach((touch, i) => {
const name = (touches.length === 1) ? baseName : baseName + ' ' + i;
if (checkpoint || evName === 'touchend')
Cypress.log({
$el: this.target, name, message: `${touch.pageX}, ${touch.pageY}`
});
if (this.touchCanvas) {
this.touchCanvas[evName](i, touch.pageX, touch.pageY, !!checkpoint);
}
});
this.target[0].dispatchEvent(new TouchEvent(evName, conf));
return this.target;
});
}
}