UNPKG

cy-mobile-commands

Version:

Mobile testing helper for Cypress

281 lines (280 loc) 11.2 kB
/// <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; }); } }