Files
notifications-admin/tests/javascripts/support/helpers.js
Tom Byers 0e7b529fbc Add mock helpers for Range and Selection
To add the text from an element to the clipboard
you need to:
1. get the current Selection
1. create a Range from the contents of the element
2. clear any existing Ranges from the Selection
   and add the new Range to the selection
3. execute the 'copy' command

To track calls to all the DOM APIs involved in
this we need mocks for Range and Selection.

Range:

https://developer.mozilla.org/en-US/docs/Web/API/Range

Selection:

https://developer.mozilla.org/en-US/docs/Web/API/Selection

Also includes a base class to help building out
Web API interface mocks.
2019-08-20 13:49:09 +01:00

287 lines
7.3 KiB
JavaScript

const triggerEvent = (el, evtType, options) => {
const eventInit = {
bubbles: true,
cancelable: true
};
let setPositionData = () => {
const browserUI = {
leftFrameBorder: 0,
topHeight: 100
};
const cursorOffset = {
x: 5,
y: 5
};
const elBoundingBox = el.getBoundingClientRect();
if (!eventInit.clientX) { eventInit.clientX = elBoundingBox.left + cursorOffset.x; }
if (!eventInit.clientY) { eventInit.clientY = elBoundingBox.top + cursorOffset.y; }
if (!eventInit.pageX) { eventInit.pageX = elBoundingBox.left + cursorOffset.x; }
if (!eventInit.pageY) { eventInit.pageY = elBoundingBox.top + cursorOffset.y; }
if (!eventInit.screenX) { eventInit.screenX = eventInit.clientX + browserUI.leftFrameBorder; }
if (!eventInit.screenY) { eventInit.screenY = eventInit.clientY + browserUI.topHeight; }
if (!eventInit.offsetX) { eventInit.offsetX = cursorOffset.x; }
if (!eventInit.offsetY) { eventInit.offsetY = cursorOffset.y; }
};
let Instance;
// mixin any specified event properties with the defaults
if (options && ('eventInit' in options)) {
Object.assign(eventInit, options.eventInit);
}
// use event interface if specified
if (options && ('interface' in options)) {
Instance = options.interface;
} else {
// otherwise, derive from the event type
switch (evtType) {
case 'click':
// click events are part of the MouseEvent interface
Instance = window.MouseEvent;
break;
case 'mousedown':
Instance = window.MouseEvent;
break;
case 'mouseup':
Instance = window.MouseEvent;
break;
case 'keydown':
Instance = window.KeyboardEvent;
break;
case 'keyup':
Instance = window.KeyboardEvent;
break;
default:
Instance = Event;
}
}
if (evtType === 'click') {
// hack for click events to simulate details of pointer interaction
setPositionData();
}
const evt = new Instance(evtType, eventInit);
el.dispatchEvent(evt);
};
function clickElementWithMouse (el) {
triggerEvent(el, 'mousedown');
triggerEvent(el, 'mouseup');
triggerEvent(el, 'click');
};
function moveSelectionToRadio (el, options) {
// movement within a radio group with arrow keys fires no keyboard events
// click event fired from option radio being activated
triggerEvent(el, 'click', {
eventInit: { pageX: 0 }
});
};
function activateRadioWithSpace (el) {
// simulate events for space key press to confirm selection
// event for space key press
triggerEvent(el, 'keydown', {
eventInit: { which: 32 }
});
// click event fired from option radio being activated
triggerEvent(el, 'click', {
eventInit: { pageX: 0 }
});
};
class ElementQuery {
constructor (el) {
this.el = el;
}
get nodeName () {
return this.el.nodeName.toLowerCase();
}
get firstTextNodeValue () {
const textNodes = Array.from(this.el.childNodes).filter(el => el.nodeType === 3);
return textNodes.length ? textNodes[0].nodeValue : undefined;
};
// returns the elements attributes as an object
hasAttributesSetTo (mappings) {
if (!this.el.hasAttributes()) { return false; }
const keys = Object.keys(mappings);
let matches = 0;
keys.forEach(key => {
if (this.el.hasAttribute(key) && (this.el.attributes[key].value === mappings[key])) {
matches++;
}
});
return matches === keys.length;
}
hasClass (classToken) {
return Array.from(this.el.classList).includes(classToken);
}
is (state) {
const test = `_is${state.charAt(0).toUpperCase()}${state.slice(1)}`;
if (ElementQuery.prototype.hasOwnProperty(test)) {
return this[test]();
}
}
// looks for a sibling before the el that matches the supplied test function
// the test function gets sent each sibling, wrapped in an Element instance
getPreviousSibling (test) {
let node = this.el.previousElementSibling;
let el;
while(node) {
el = element(node);
if (test(el)) {
return node;
}
node = node.previousElementSibling;
}
return null;
}
_isHidden () {
const display = window.getComputedStyle(this.el).getPropertyValue('display');
return display === 'none';
}
};
class WindowMock {
constructor (jest) {
this._defaults = {
height: window.innerHeight,
width: window.innerWidth
};
this._spies = {
document: {}
};
this._jest = jest;
}
setHeightTo (height) {
// mock DOM calls for window height
window.innerHeight = height;
// remove calls to document.documentElement.clientHeight when jQuery is gone. It's called to support older browsers like IE8
this._spies.document.clientHeight = this._jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => height);
}
setWidthTo (width) {
// mock DOM calls for window width
window.innerWidth = width;
// remove calls to document.documentElement.clientWidth when jQuery is gone. It's called to support older browsers like IE8
this._spies.document.clientWidth = this._jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => height);
}
resizeTo (dimensions) {
this.setHeightTo(dimensions.height);
this.setWidthTo(dimensions.width);
triggerEvent(window, 'resize');
}
scrollBy (scrollPosition) {
document.documentElement.scrollTop = scrollPosition;
triggerEvent(window, 'scroll');
}
reset () {
window.innerHeight = this._defaults.height;
window.innerWidth = this._defaults.width;
document.documentElement.scrollTop = 0;
// reset all spies
Object.keys(this._spies).forEach(key => {
const objectSpies = this._spies[key];
Object.keys(objectSpies).forEach(key => objectSpies[key].mockClear());
});
}
}
// Base class for mocking an DOM APi interfaces not in JSDOM
class DOMInterfaceMock {
constructor (jest, spec) {
// set up methods so their calls can be tracked
// leave implementation/return values to the test
spec.methods.forEach(method => this[method] = jest.fn(() => {}));
// set up props
// any spies should be relative to the test so not set here
spec.props.forEach(prop => {
Object.defineProperty(this, prop, {
get: () => this[prop],
set: value => this[prop] = value
});
});
}
}
// Very basic class for stubbing the Range interface
// Only contains methods required for current tests
class RangeMock extends DOMInterfaceMock {
constructor (jest) {
super(jest, { props: [], methods: ['selectNodeContents'] });
}
}
// Very basic class for stubbing the Selection interface
// Only contains methods required for current tests
class SelectionMock extends DOMInterfaceMock {
constructor (jest) {
super(jest, { props: [], methods: ['removeAllRanges', 'addRange'] });
}
}
// function to ask certain questions of a DOM Element
const element = function (el) {
return new ElementQuery(el);
};
exports.triggerEvent = triggerEvent;
exports.clickElementWithMouse = clickElementWithMouse;
exports.moveSelectionToRadio = moveSelectionToRadio;
exports.activateRadioWithSpace = activateRadioWithSpace;
exports.RangeMock = RangeMock;
exports.SelectionMock = SelectionMock;
exports.element = element;
exports.WindowMock = WindowMock;