611 lines
24 KiB
JavaScript
611 lines
24 KiB
JavaScript
odoo.define('web.test_utils_dom', function (require) {
|
|
"use strict";
|
|
|
|
const concurrency = require('web.concurrency');
|
|
const Widget = require('web.Widget');
|
|
|
|
const { Component } = owl;
|
|
|
|
/**
|
|
* DOM Test Utils
|
|
*
|
|
* This module defines various utility functions to help simulate DOM events.
|
|
*
|
|
* Note that all methods defined in this module are exported in the main
|
|
* testUtils file.
|
|
*/
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Private functions
|
|
//-------------------------------------------------------------------------
|
|
|
|
// TriggerEvent helpers
|
|
const keyboardEventBubble = args => Object.assign({}, args, { bubbles: true, keyCode: args.which });
|
|
const mouseEventMapping = args => Object.assign({}, args, {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
clientX: args ? args.clientX || args.pageX : undefined,
|
|
clientY: args ? args.clientY || args.pageY : undefined,
|
|
view: window,
|
|
});
|
|
const mouseEventNoBubble = args => Object.assign({}, args, {
|
|
bubbles: false,
|
|
cancelable: false,
|
|
clientX: args ? args.clientX || args.pageX : undefined,
|
|
clientY: args ? args.clientY || args.pageY : undefined,
|
|
view: window,
|
|
});
|
|
const touchEventMapping = args => Object.assign({}, args, {
|
|
cancelable: true,
|
|
bubbles: true,
|
|
composed: true,
|
|
view: window,
|
|
rotation: 0.0,
|
|
zoom: 1.0,
|
|
});
|
|
const touchEventCancelMapping = args => Object.assign({}, touchEventMapping(args), {
|
|
cancelable: false,
|
|
});
|
|
const noBubble = args => Object.assign({}, args, { bubbles: false });
|
|
const onlyBubble = args => Object.assign({}, args, { bubbles: true });
|
|
// TriggerEvent constructor/args processor mapping
|
|
const EVENT_TYPES = {
|
|
auxclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
click: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
contextmenu: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
dblclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mousedown: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseup: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
|
|
mousemove: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseenter: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
|
mouseleave: { constructor: MouseEvent, processParameters: mouseEventNoBubble },
|
|
mouseover: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
mouseout: { constructor: MouseEvent, processParameters: mouseEventMapping },
|
|
|
|
focus: { constructor: FocusEvent, processParameters: noBubble },
|
|
focusin: { constructor: FocusEvent, processParameters: onlyBubble },
|
|
blur: { constructor: FocusEvent, processParameters: noBubble },
|
|
|
|
cut: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
copy: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
paste: { constructor: ClipboardEvent, processParameters: onlyBubble },
|
|
|
|
keydown: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
keypress: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
keyup: { constructor: KeyboardEvent, processParameters: keyboardEventBubble },
|
|
|
|
drag: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragend: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragenter: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragstart: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragleave: { constructor: DragEvent, processParameters: onlyBubble },
|
|
dragover: { constructor: DragEvent, processParameters: onlyBubble },
|
|
drop: { constructor: DragEvent, processParameters: onlyBubble },
|
|
|
|
input: { constructor: InputEvent, processParameters: onlyBubble },
|
|
|
|
compositionstart: { constructor: CompositionEvent, processParameters: onlyBubble },
|
|
compositionend: { constructor: CompositionEvent, processParameters: onlyBubble },
|
|
};
|
|
|
|
if (typeof TouchEvent === 'function') {
|
|
Object.assign(EVENT_TYPES, {
|
|
touchstart: {constructor: TouchEvent, processParameters: touchEventMapping},
|
|
touchend: {constructor: TouchEvent, processParameters: touchEventMapping},
|
|
touchmove: {constructor: TouchEvent, processParameters: touchEventMapping},
|
|
touchcancel: {constructor: TouchEvent, processParameters: touchEventCancelMapping},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if an object is an instance of EventTarget.
|
|
*
|
|
* @param {Object} node
|
|
* @returns {boolean}
|
|
*/
|
|
function _isEventTarget(node) {
|
|
if (!node) {
|
|
throw new Error(`Provided node is ${node}.`);
|
|
}
|
|
if (node instanceof window.top.EventTarget) {
|
|
return true;
|
|
}
|
|
const contextWindow = node.defaultView || // document
|
|
(node.ownerDocument && node.ownerDocument.defaultView); // iframe node
|
|
return contextWindow && node instanceof contextWindow.EventTarget;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------
|
|
// Public functions
|
|
//-------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Click on a specified element. If the option first or last is not specified,
|
|
* this method also check the unicity and the visibility of the target.
|
|
*
|
|
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
|
|
* @param {Object} [options={}] click options
|
|
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
|
|
* element event if it is invisible
|
|
* @param {boolean} [options.first=false] if true, clicks on the first element
|
|
* @param {boolean} [options.last=false] if true, clicks on the last element
|
|
* @returns {Promise}
|
|
*/
|
|
async function click(el, options = {}) {
|
|
let matches, target;
|
|
let selectorMsg = "";
|
|
if (typeof el === 'string') {
|
|
el = $(el);
|
|
}
|
|
if (el.disabled || (el instanceof jQuery && el.get(0).disabled)) {
|
|
throw new Error("Can't click on a disabled button");
|
|
}
|
|
if (_isEventTarget(el)) {
|
|
// EventTarget
|
|
matches = [el];
|
|
} else {
|
|
// Any other iterable object containing EventTarget objects (jQuery, HTMLCollection, etc.)
|
|
matches = [...el];
|
|
}
|
|
|
|
const validMatches = options.allowInvisible ?
|
|
matches : matches.filter(t => $(t).is(':visible'));
|
|
|
|
if (options.first) {
|
|
if (validMatches.length === 1) {
|
|
throw new Error(`There should be more than one visible target ${selectorMsg}. If` +
|
|
' you are sure that there is exactly one target, please use the ' +
|
|
'click function instead of the clickFirst function');
|
|
}
|
|
target = validMatches[0];
|
|
} else if (options.last) {
|
|
if (validMatches.length === 1) {
|
|
throw new Error(`There should be more than one visible target ${selectorMsg}. If` +
|
|
' you are sure that there is exactly one target, please use the ' +
|
|
'click function instead of the clickLast function');
|
|
}
|
|
target = validMatches[validMatches.length - 1];
|
|
} else if (validMatches.length !== 1) {
|
|
throw new Error(`Found ${validMatches.length} elements to click on, instead of 1 ${selectorMsg}`);
|
|
} else {
|
|
target = validMatches[0];
|
|
}
|
|
if (validMatches.length === 0 && matches.length > 0) {
|
|
throw new Error(`Element to click on is not visible ${selectorMsg}`);
|
|
}
|
|
if (target.disabled) {
|
|
return;
|
|
}
|
|
|
|
return triggerEvent(target, 'click');
|
|
}
|
|
|
|
/**
|
|
* Click on the first element of a list of elements. Note that if the list has
|
|
* only one visible element, we trigger an error. In that case, it is better to
|
|
* use the click helper instead.
|
|
*
|
|
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
|
|
* @param {boolean} [options={}] click options
|
|
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
|
|
* element event if it is invisible
|
|
* @returns {Promise}
|
|
*/
|
|
async function clickFirst(el, options) {
|
|
return click(el, Object.assign({}, options, { first: true }));
|
|
}
|
|
|
|
/**
|
|
* Click on the last element of a list of elements. Note that if the list has
|
|
* only one visible element, we trigger an error. In that case, it is better to
|
|
* use the click helper instead.
|
|
*
|
|
* @param {string|EventTarget|EventTarget[]} el (if string: it is a (jquery) selector)
|
|
* @param {boolean} [options={}] click options
|
|
* @param {boolean} [options.allowInvisible=false] if true, clicks on the
|
|
* element event if it is invisible
|
|
* @returns {Promise}
|
|
*/
|
|
async function clickLast(el, options) {
|
|
return click(el, Object.assign({}, options, { last: true }));
|
|
}
|
|
|
|
/**
|
|
* Simulate a drag and drop operation between 2 jquery nodes: $el and $to.
|
|
* This is a crude simulation, with only the mousedown, mousemove and mouseup
|
|
* events, but it is enough to help test drag and drop operations with jqueryUI
|
|
* sortable.
|
|
*
|
|
* @todo: remove the withTrailingClick option when the jquery update branch is
|
|
* merged. This is not the default as of now, because handlers are triggered
|
|
* synchronously, which is not the same as the 'reality'.
|
|
*
|
|
* @param {jQuery|EventTarget} $el
|
|
* @param {jQuery|EventTarget} $to
|
|
* @param {Object} [options]
|
|
* @param {string|Object} [options.position='center'] target position:
|
|
* can either be one of {'top', 'bottom', 'left', 'right'} or
|
|
* an object with two attributes (top and left))
|
|
* @param {boolean} [options.disableDrop=false] whether to trigger the drop action
|
|
* @param {boolean} [options.continueMove=false] whether to trigger the
|
|
* mousedown action (will only work after another call of this function with
|
|
* without this option)
|
|
* @param {boolean} [options.withTrailingClick=false] if true, this utility
|
|
* function will also trigger a click on the target after the mouseup event
|
|
* (this is actually what happens when a drag and drop operation is done)
|
|
* @param {jQuery|EventTarget} [options.mouseenterTarget=undefined] target of the mouseenter event
|
|
* @param {jQuery|EventTarget} [options.mousedownTarget=undefined] target of the mousedown event
|
|
* @param {jQuery|EventTarget} [options.mousemoveTarget=undefined] target of the mousemove event
|
|
* @param {jQuery|EventTarget} [options.mouseupTarget=undefined] target of the mouseup event
|
|
* @param {jQuery|EventTarget} [options.ctrlKey=undefined] if the ctrl key should be considered pressed at the time of mouseup
|
|
* @returns {Promise}
|
|
*/
|
|
async function dragAndDrop($el, $to, options) {
|
|
let el = null;
|
|
if (_isEventTarget($el)) {
|
|
el = $el;
|
|
$el = $(el);
|
|
}
|
|
if (_isEventTarget($to)) {
|
|
$to = $($to);
|
|
}
|
|
options = options || {};
|
|
const position = options.position || 'center';
|
|
const elementCenter = $el.offset();
|
|
const toOffset = $to.offset();
|
|
|
|
if (typeof position === 'object') {
|
|
toOffset.top += position.top + 1;
|
|
toOffset.left += position.left + 1;
|
|
} else {
|
|
toOffset.top += $to.outerHeight() / 2;
|
|
toOffset.left += $to.outerWidth() / 2;
|
|
const vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
|
|
if (position === 'top') {
|
|
toOffset.top -= $to.outerHeight() / 2 + vertical_offset;
|
|
} else if (position === 'bottom') {
|
|
toOffset.top += $to.outerHeight() / 2 - vertical_offset;
|
|
} else if (position === 'left') {
|
|
toOffset.left -= $to.outerWidth() / 2;
|
|
} else if (position === 'right') {
|
|
toOffset.left += $to.outerWidth() / 2;
|
|
}
|
|
}
|
|
|
|
if ($to[0].ownerDocument !== document) {
|
|
// we are in an iframe
|
|
const bound = $('iframe')[0].getBoundingClientRect();
|
|
toOffset.left += bound.left;
|
|
toOffset.top += bound.top;
|
|
}
|
|
await triggerEvent(options.mouseenterTarget || el || $el, 'mouseover', {}, true);
|
|
if (!(options.continueMove)) {
|
|
elementCenter.left += $el.outerWidth() / 2;
|
|
elementCenter.top += $el.outerHeight() / 2;
|
|
|
|
await triggerEvent(options.mousedownTarget || el || $el, 'mousedown', {
|
|
which: 1,
|
|
pageX: elementCenter.left,
|
|
pageY: elementCenter.top
|
|
}, true);
|
|
}
|
|
await triggerEvent(options.mousemoveTarget || el || $el, 'mousemove', {
|
|
which: 1,
|
|
pageX: toOffset.left,
|
|
pageY: toOffset.top
|
|
}, true);
|
|
|
|
if (!options.disableDrop) {
|
|
await triggerEvent(options.mouseupTarget || el || $el, 'mouseup', {
|
|
which: 1,
|
|
pageX: toOffset.left,
|
|
pageY: toOffset.top,
|
|
ctrlKey: options.ctrlKey,
|
|
}, true);
|
|
if (options.withTrailingClick) {
|
|
await triggerEvent(options.mouseupTarget || el || $el, 'click', {}, true);
|
|
}
|
|
} else {
|
|
// It's impossible to drag another element when one is already
|
|
// being dragged. So it's necessary to finish the drop when the test is
|
|
// over otherwise it's impossible for the next tests to drag and
|
|
// drop elements.
|
|
$el.on('remove', function () {
|
|
triggerEvent($el, 'mouseup', {}, true);
|
|
});
|
|
}
|
|
return returnAfterNextAnimationFrame();
|
|
}
|
|
|
|
/**
|
|
* Helper method to retrieve a distinct item from a collection of elements defined
|
|
* by the given "selector" string. It can either be the index of the item or its
|
|
* inner text.
|
|
* @param {Element} el
|
|
* @param {string} selector
|
|
* @param {number | string} [elFinder=0]
|
|
* @returns {Element | null}
|
|
*/
|
|
function findItem(el, selector, elFinder = 0) {
|
|
const elements = [...getNode(el).querySelectorAll(selector)];
|
|
if (!elements.length) {
|
|
throw new Error(`No element found with selector "${selector}".`);
|
|
}
|
|
switch (typeof elFinder) {
|
|
case "number": {
|
|
const match = elements[elFinder];
|
|
if (!match) {
|
|
throw new Error(
|
|
`No element with selector "${selector}" at index ${elFinder}.`
|
|
);
|
|
}
|
|
return match;
|
|
}
|
|
case "string": {
|
|
const match = elements.find(
|
|
(el) => el.innerText.trim().toLowerCase() === elFinder.toLowerCase()
|
|
);
|
|
if (!match) {
|
|
throw new Error(
|
|
`No element with selector "${selector}" containing "${elFinder}".
|
|
`);
|
|
}
|
|
return match;
|
|
}
|
|
default: throw new Error(
|
|
`Invalid provided element finder: must be a number|string|function.`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function used to extract an HTML EventTarget element from a given
|
|
* target. The extracted element will depend on the target type:
|
|
* - Component|Widget -> el
|
|
* - jQuery -> associated element (must have 1)
|
|
* - HTMLCollection (or similar) -> first element (must have 1)
|
|
* - string -> result of document.querySelector with string
|
|
* - else -> as is
|
|
* @private
|
|
* @param {(Component|Widget|jQuery|HTMLCollection|HTMLElement|string)} target
|
|
* @returns {EventTarget}
|
|
*/
|
|
function getNode(target) {
|
|
let nodes;
|
|
if (target instanceof Component || target instanceof Widget) {
|
|
nodes = [target.el];
|
|
} else if (typeof target === 'string') {
|
|
nodes = document.querySelectorAll(target);
|
|
} else if (target === jQuery) { // jQuery (or $)
|
|
nodes = [document.body];
|
|
} else if (target.length) { // jQuery instance, HTMLCollection or array
|
|
nodes = target;
|
|
} else {
|
|
nodes = [target];
|
|
}
|
|
if (nodes.length !== 1) {
|
|
throw new Error(`Found ${nodes.length} nodes instead of 1.`);
|
|
}
|
|
const node = nodes[0];
|
|
if (!node) {
|
|
throw new Error(`Expected a node and got ${node}.`);
|
|
}
|
|
if (!_isEventTarget(node)) {
|
|
throw new Error(`Expected node to be an instance of EventTarget and got ${node.constructor.name}.`);
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Open the datepicker of a given element.
|
|
*
|
|
* @param {jQuery} $datepickerEl element to which a datepicker is attached
|
|
*/
|
|
async function openDatepicker($datepickerEl) {
|
|
return click($datepickerEl.find('.o_datepicker_input'));
|
|
}
|
|
|
|
/**
|
|
* Returns a promise that will be resolved after the nextAnimationFrame after
|
|
* the next tick
|
|
*
|
|
* This is useful to guarantee that OWL has had the time to render
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
async function returnAfterNextAnimationFrame() {
|
|
await concurrency.delay(0);
|
|
await new Promise(resolve => {
|
|
window.requestAnimationFrame(resolve);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger an event on the specified target.
|
|
* This function will dispatch a native event to an EventTarget or a
|
|
* jQuery event to a jQuery object. This behaviour can be overridden by the
|
|
* jquery option.
|
|
*
|
|
* @param {EventTarget|EventTarget[]} el
|
|
* @param {string} eventType event type
|
|
* @param {Object} [eventAttrs] event attributes
|
|
* on a jQuery element with the `$.fn.trigger` function
|
|
* @param {Boolean} [fast=false] true if the trigger event have to wait for a single tick instead of waiting for the next animation frame
|
|
* @returns {Promise}
|
|
*/
|
|
async function triggerEvent(el, eventType, eventAttrs = {}, fast = false) {
|
|
let matches;
|
|
let selectorMsg = "";
|
|
if (_isEventTarget(el)) {
|
|
matches = [el];
|
|
} else {
|
|
matches = [...el];
|
|
}
|
|
|
|
if (matches.length !== 1) {
|
|
throw new Error(`Found ${matches.length} elements to trigger "${eventType}" on, instead of 1 ${selectorMsg}`);
|
|
}
|
|
|
|
const target = matches[0];
|
|
let event;
|
|
|
|
if (!EVENT_TYPES[eventType] && !EVENT_TYPES[eventType.type]) {
|
|
event = new Event(eventType, Object.assign({}, eventAttrs, { bubbles: true }));
|
|
} else {
|
|
if (typeof eventType === "object") {
|
|
const { constructor, processParameters } = EVENT_TYPES[eventType.type];
|
|
const eventParameters = processParameters(eventType);
|
|
event = new constructor(eventType.type, eventParameters);
|
|
} else {
|
|
const { constructor, processParameters } = EVENT_TYPES[eventType];
|
|
event = new constructor(eventType, processParameters(eventAttrs));
|
|
}
|
|
}
|
|
target.dispatchEvent(event);
|
|
return fast ? undefined : returnAfterNextAnimationFrame();
|
|
}
|
|
|
|
/**
|
|
* Trigger multiple events on the specified element.
|
|
*
|
|
* @param {EventTarget|EventTarget[]} el
|
|
* @param {string[]} events the events you want to trigger
|
|
* @returns {Promise}
|
|
*/
|
|
async function triggerEvents(el, events) {
|
|
if (el instanceof jQuery) {
|
|
if (el.length !== 1) {
|
|
throw new Error(`target has length ${el.length} instead of 1`);
|
|
}
|
|
}
|
|
if (typeof events === 'string') {
|
|
events = [events];
|
|
}
|
|
|
|
for (let e = 0; e < events.length; e++) {
|
|
await triggerEvent(el, events[e]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simulate a keypress event for a given character
|
|
*
|
|
* @param {string} char the character, or 'ENTER'
|
|
* @returns {Promise}
|
|
*/
|
|
async function triggerKeypressEvent(char) {
|
|
let keycode;
|
|
if (char === 'Enter') {
|
|
keycode = $.ui.keyCode.ENTER;
|
|
} else if (char === "Tab") {
|
|
keycode = $.ui.keyCode.TAB;
|
|
} else {
|
|
keycode = char.charCodeAt(0);
|
|
}
|
|
return triggerEvent(document.body, 'keypress', {
|
|
key: char,
|
|
keyCode: keycode,
|
|
which: keycode,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* simulate a mouse event with a custom event who add the item position. This is
|
|
* sometimes necessary because the basic way to trigger an event (such as
|
|
* $el.trigger('mousemove')); ) is too crude for some uses.
|
|
*
|
|
* @param {jQuery|EventTarget} $el
|
|
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
|
|
* @returns {Promise}
|
|
*/
|
|
async function triggerMouseEvent($el, type) {
|
|
const el = $el instanceof jQuery ? $el[0] : $el;
|
|
if (!el) {
|
|
throw new Error(`no target found to trigger MouseEvent`);
|
|
}
|
|
const rect = el.getBoundingClientRect();
|
|
// try to click around the center of the element, biased to the bottom
|
|
// right as chrome messes up when clicking on the top-left corner...
|
|
const left = rect.x + Math.ceil(rect.width / 2);
|
|
const top = rect.y + Math.ceil(rect.height / 2);
|
|
return triggerEvent(el, type, {which: 1, clientX: left, clientY: top});
|
|
}
|
|
|
|
/**
|
|
* simulate a mouse event with a custom event on a position x and y. This is
|
|
* sometimes necessary because the basic way to trigger an event (such as
|
|
* $el.trigger('mousemove')); ) is too crude for some uses.
|
|
*
|
|
* @param {integer} x
|
|
* @param {integer} y
|
|
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
|
|
* @returns {HTMLElement}
|
|
*/
|
|
async function triggerPositionalMouseEvent(x, y, type) {
|
|
const ev = document.createEvent("MouseEvent");
|
|
const el = document.elementFromPoint(x, y);
|
|
ev.initMouseEvent(
|
|
type,
|
|
true /* bubble */,
|
|
true /* cancelable */,
|
|
window, null,
|
|
x, y, x, y, /* coordinates */
|
|
false, false, false, false, /* modifier keys */
|
|
0 /*left button*/, null
|
|
);
|
|
el.dispatchEvent(ev);
|
|
return el;
|
|
}
|
|
|
|
/**
|
|
* Simulate a "TAP" (touch) event with a custom position x and y.
|
|
*
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @returns {HTMLElement}
|
|
*/
|
|
async function triggerPositionalTapEvents(x, y) {
|
|
const element = document.elementFromPoint(x, y);
|
|
const touch = new Touch({
|
|
identifier: 0,
|
|
target: element,
|
|
clientX: x,
|
|
clientY: y,
|
|
pageX: x,
|
|
pageY: y,
|
|
});
|
|
await triggerEvent(element, 'touchstart', {
|
|
touches: [touch],
|
|
targetTouches: [touch],
|
|
changedTouches: [touch],
|
|
});
|
|
await triggerEvent(element, 'touchmove', {
|
|
touches: [touch],
|
|
targetTouches: [touch],
|
|
changedTouches: [touch],
|
|
});
|
|
await triggerEvent(element, 'touchend', {
|
|
changedTouches: [touch],
|
|
});
|
|
return element;
|
|
}
|
|
|
|
return {
|
|
click,
|
|
clickFirst,
|
|
clickLast,
|
|
dragAndDrop,
|
|
findItem,
|
|
getNode,
|
|
openDatepicker,
|
|
returnAfterNextAnimationFrame,
|
|
triggerEvent,
|
|
triggerEvents,
|
|
triggerKeypressEvent,
|
|
triggerMouseEvent,
|
|
triggerPositionalMouseEvent,
|
|
triggerPositionalTapEvents,
|
|
};
|
|
});
|