summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/utils/test_utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static/src/utils/test_utils.js')
-rw-r--r--addons/mail/static/src/utils/test_utils.js767
1 files changed, 767 insertions, 0 deletions
diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js
new file mode 100644
index 00000000..be15afe7
--- /dev/null
+++ b/addons/mail/static/src/utils/test_utils.js
@@ -0,0 +1,767 @@
+odoo.define('mail/static/src/utils/test_utils.js', function (require) {
+'use strict';
+
+const BusService = require('bus.BusService');
+
+const {
+ addMessagingToEnv,
+ addTimeControlToEnv,
+} = require('mail/static/src/env/test_env.js');
+const ModelManager = require('mail/static/src/model/model_manager.js');
+const ChatWindowService = require('mail/static/src/services/chat_window_service/chat_window_service.js');
+const DialogService = require('mail/static/src/services/dialog_service/dialog_service.js');
+const { nextTick } = require('mail/static/src/utils/utils.js');
+const DiscussWidget = require('mail/static/src/widgets/discuss/discuss.js');
+const MessagingMenuWidget = require('mail/static/src/widgets/messaging_menu/messaging_menu.js');
+const MockModels = require('mail/static/tests/helpers/mock_models.js');
+
+const AbstractStorageService = require('web.AbstractStorageService');
+const NotificationService = require('web.NotificationService');
+const RamStorage = require('web.RamStorage');
+const {
+ createActionManager,
+ createView,
+ makeTestPromise,
+ mock: {
+ addMockEnvironment,
+ patch: legacyPatch,
+ unpatch: legacyUnpatch,
+ },
+} = require('web.test_utils');
+const Widget = require('web.Widget');
+
+const { Component } = owl;
+
+//------------------------------------------------------------------------------
+// Private
+//------------------------------------------------------------------------------
+
+/**
+ * Create a fake object 'dataTransfer', linked to some files,
+ * which is passed to drag and drop events.
+ *
+ * @param {Object[]} files
+ * @returns {Object}
+ */
+function _createFakeDataTransfer(files) {
+ return {
+ dropEffect: 'all',
+ effectAllowed: 'all',
+ files,
+ items: [],
+ types: ['Files'],
+ };
+}
+
+/**
+ * @private
+ * @param {Object} callbacks
+ * @param {function[]} callbacks.init
+ * @param {function[]} callbacks.mount
+ * @param {function[]} callbacks.destroy
+ * @param {function[]} callbacks.return
+ * @returns {Object} update callbacks
+ */
+function _useChatWindow(callbacks) {
+ const {
+ mount: prevMount,
+ destroy: prevDestroy,
+ } = callbacks;
+ return Object.assign({}, callbacks, {
+ mount: prevMount.concat(async () => {
+ // trigger mounting of chat window manager
+ await Component.env.services['chat_window']._onWebClientReady();
+ }),
+ destroy: prevDestroy.concat(() => {
+ Component.env.services['chat_window'].destroy();
+ }),
+ });
+}
+
+/**
+ * @private
+ * @param {Object} callbacks
+ * @param {function[]} callbacks.init
+ * @param {function[]} callbacks.mount
+ * @param {function[]} callbacks.destroy
+ * @param {function[]} callbacks.return
+ * @returns {Object} update callbacks
+ */
+function _useDialog(callbacks) {
+ const {
+ mount: prevMount,
+ destroy: prevDestroy,
+ } = callbacks;
+ return Object.assign({}, callbacks, {
+ mount: prevMount.concat(async () => {
+ // trigger mounting of dialog manager
+ await Component.env.services['dialog']._onWebClientReady();
+ }),
+ destroy: prevDestroy.concat(() => {
+ Component.env.services['dialog'].destroy();
+ }),
+ });
+}
+
+/**
+ * @private
+ * @param {Object} callbacks
+ * @param {function[]} callbacks.init
+ * @param {function[]} callbacks.mount
+ * @param {function[]} callbacks.destroy
+ * @param {function[]} callbacks.return
+ * @return {Object} update callbacks
+ */
+function _useDiscuss(callbacks) {
+ const {
+ init: prevInit,
+ mount: prevMount,
+ return: prevReturn,
+ } = callbacks;
+ let discussWidget;
+ const state = {
+ autoOpenDiscuss: false,
+ discussData: {},
+ };
+ return Object.assign({}, callbacks, {
+ init: prevInit.concat(params => {
+ const {
+ autoOpenDiscuss = state.autoOpenDiscuss,
+ discuss: discussData = state.discussData
+ } = params;
+ Object.assign(state, { autoOpenDiscuss, discussData });
+ delete params.autoOpenDiscuss;
+ delete params.discuss;
+ }),
+ mount: prevMount.concat(async params => {
+ const { selector, widget } = params;
+ DiscussWidget.prototype._pushStateActionManager = () => {};
+ discussWidget = new DiscussWidget(widget, state.discussData);
+ await discussWidget.appendTo($(selector));
+ if (state.autoOpenDiscuss) {
+ await discussWidget.on_attach_callback();
+ }
+ }),
+ return: prevReturn.concat(result => {
+ Object.assign(result, { discussWidget });
+ }),
+ });
+}
+
+/**
+ * @private
+ * @param {Object} callbacks
+ * @param {function[]} callbacks.init
+ * @param {function[]} callbacks.mount
+ * @param {function[]} callbacks.destroy
+ * @param {function[]} callbacks.return
+ * @returns {Object} update callbacks
+ */
+function _useMessagingMenu(callbacks) {
+ const {
+ mount: prevMount,
+ return: prevReturn,
+ } = callbacks;
+ let messagingMenuWidget;
+ return Object.assign({}, callbacks, {
+ mount: prevMount.concat(async ({ selector, widget }) => {
+ messagingMenuWidget = new MessagingMenuWidget(widget, {});
+ await messagingMenuWidget.appendTo($(selector));
+ await messagingMenuWidget.on_attach_callback();
+ }),
+ return: prevReturn.concat(result => {
+ Object.assign(result, { messagingMenuWidget });
+ }),
+ });
+}
+
+//------------------------------------------------------------------------------
+// Public: rendering timers
+//------------------------------------------------------------------------------
+
+/**
+ * Returns a promise resolved at the next animation frame.
+ *
+ * @returns {Promise}
+ */
+function nextAnimationFrame() {
+ const requestAnimationFrame = owl.Component.scheduler.requestAnimationFrame;
+ return new Promise(function (resolve) {
+ setTimeout(() => requestAnimationFrame(() => resolve()));
+ });
+}
+
+/**
+ * Returns a promise resolved the next time OWL stops rendering.
+ *
+ * @param {function} func function which, when called, is
+ * expected to trigger OWL render(s).
+ * @param {number} [timeoutDelay=5000] in ms
+ * @returns {Promise}
+ */
+const afterNextRender = (function () {
+ const stop = owl.Component.scheduler.stop;
+ const stopPromises = [];
+
+ owl.Component.scheduler.stop = function () {
+ const wasRunning = this.isRunning;
+ stop.call(this);
+ if (wasRunning) {
+ while (stopPromises.length) {
+ stopPromises.pop().resolve();
+ }
+ }
+ };
+
+ async function afterNextRender(func, timeoutDelay = 5000) {
+ // Define the potential errors outside of the promise to get a proper
+ // trace if they happen.
+ const startError = new Error("Timeout: the render didn't start.");
+ const stopError = new Error("Timeout: the render didn't stop.");
+ // Set up the timeout to reject if no render happens.
+ let timeoutNoRender;
+ const timeoutProm = new Promise((resolve, reject) => {
+ timeoutNoRender = setTimeout(() => {
+ let error = startError;
+ if (owl.Component.scheduler.isRunning) {
+ error = stopError;
+ }
+ console.error(error);
+ reject(error);
+ }, timeoutDelay);
+ });
+ // Set up the promise to resolve if a render happens.
+ const prom = makeTestPromise();
+ stopPromises.push(prom);
+ // Start the function expected to trigger a render after the promise
+ // has been registered to not miss any potential render.
+ const funcRes = func();
+ // Make them race (first to resolve/reject wins).
+ await Promise.race([prom, timeoutProm]);
+ clearTimeout(timeoutNoRender);
+ // Wait the end of the function to ensure all potential effects are
+ // taken into account during the following verification step.
+ await funcRes;
+ // Wait one more frame to make sure no new render has been queued.
+ await nextAnimationFrame();
+ if (owl.Component.scheduler.isRunning) {
+ await afterNextRender(() => {}, timeoutDelay);
+ }
+ }
+
+ return afterNextRender;
+})();
+
+
+//------------------------------------------------------------------------------
+// Public: test lifecycle
+//------------------------------------------------------------------------------
+
+function beforeEach(self) {
+ const data = MockModels.generateData();
+
+ data.partnerRootId = 2;
+ data['res.partner'].records.push({
+ active: false,
+ display_name: "OdooBot",
+ id: data.partnerRootId,
+ });
+
+ data.currentPartnerId = 3;
+ data['res.partner'].records.push({
+ display_name: "Your Company, Mitchell Admin",
+ id: data.currentPartnerId,
+ name: "Mitchell Admin",
+ });
+ data.currentUserId = 2;
+ data['res.users'].records.push({
+ display_name: "Your Company, Mitchell Admin",
+ id: data.currentUserId,
+ name: "Mitchell Admin",
+ partner_id: data.currentPartnerId,
+ });
+
+ data.publicPartnerId = 4;
+ data['res.partner'].records.push({
+ active: false,
+ display_name: "Public user",
+ id: data.publicPartnerId,
+ });
+ data.publicUserId = 3;
+ data['res.users'].records.push({
+ active: false,
+ display_name: "Public user",
+ id: data.publicUserId,
+ name: "Public user",
+ partner_id: data.publicPartnerId,
+ });
+
+ const originals = {
+ '_.debounce': _.debounce,
+ '_.throttle': _.throttle,
+ };
+
+ (function patch() {
+ // patch _.debounce and _.throttle to be fast and synchronous
+ _.debounce = _.identity;
+ _.throttle = _.identity;
+ })();
+
+ function unpatch() {
+ _.debounce = originals['_.debounce'];
+ _.throttle = originals['_.throttle'];
+ }
+
+ Object.assign(self, {
+ components: [],
+ data,
+ unpatch,
+ widget: undefined
+ });
+}
+
+function afterEach(self) {
+ if (self.env) {
+ self.env.bus.off('hide_home_menu', null);
+ self.env.bus.off('show_home_menu', null);
+ self.env.bus.off('will_hide_home_menu', null);
+ self.env.bus.off('will_show_home_menu', null);
+ }
+ // The components must be destroyed before the widget, because the
+ // widget might destroy the models before destroying the components,
+ // and the components might still rely on messaging (or other) record(s).
+ while (self.components.length > 0) {
+ const component = self.components.pop();
+ component.destroy();
+ }
+ if (self.widget) {
+ self.widget.destroy();
+ self.widget = undefined;
+ }
+ self.env = undefined;
+ self.unpatch();
+}
+
+/**
+ * Creates and returns a new root Component with the given props and mounts it
+ * on target.
+ * Assumes that self.env is set to the correct value.
+ * Components created this way are automatically registered for clean up after
+ * the test, which will happen when `afterEach` is called.
+ *
+ * @param {Object} self the current QUnit instance
+ * @param {Class} Component the component class to create
+ * @param {Object} param2
+ * @param {Object} [param2.props={}] forwarded to component constructor
+ * @param {DOM.Element} param2.target mount target for the component
+ * @returns {owl.Component} the new component instance
+ */
+async function createRootComponent(self, Component, { props = {}, target }) {
+ Component.env = self.env;
+ const component = new Component(null, props);
+ delete Component.env;
+ self.components.push(component);
+ await afterNextRender(() => component.mount(target));
+ return component;
+}
+
+/**
+ * Main function used to make a mocked environment with mocked messaging env.
+ *
+ * @param {Object} [param0={}]
+ * @param {string} [param0.arch] makes only sense when `param0.hasView` is set:
+ * the arch to use in createView.
+ * @param {Object} [param0.archs]
+ * @param {boolean} [param0.autoOpenDiscuss=false] makes only sense when
+ * `param0.hasDiscuss` is set: determine whether mounted discuss should be
+ * open initially.
+ * @param {boolean} [param0.debug=false]
+ * @param {Object} [param0.data] makes only sense when `param0.hasView` is set:
+ * the data to use in createView.
+ * @param {Object} [param0.discuss={}] makes only sense when `param0.hasDiscuss`
+ * is set: provide data that is passed to discuss widget (= client action) as
+ * 2nd positional argument.
+ * @param {Object} [param0.env={}]
+ * @param {function} [param0.mockFetch]
+ * @param {function} [param0.mockRPC]
+ * @param {boolean} [param0.hasActionManager=false] if set, use
+ * createActionManager.
+ * @param {boolean} [param0.hasChatWindow=false] if set, mount chat window
+ * service.
+ * @param {boolean} [param0.hasDiscuss=false] if set, mount discuss app.
+ * @param {boolean} [param0.hasMessagingMenu=false] if set, mount messaging
+ * menu.
+ * @param {boolean} [param0.hasTimeControl=false] if set, all flow of time
+ * with `env.browser.setTimeout` are fully controlled by test itself.
+ * @see addTimeControlToEnv that adds `advanceTime` function in
+ * `env.testUtils`.
+ * @param {boolean} [param0.hasView=false] if set, use createView to create a
+ * view instead of a generic widget.
+ * @param {Deferred|Promise} [param0.messagingBeforeCreationDeferred=Promise.resolve()]
+ * Deferred that let tests block messaging creation and simulate resolution.
+ * Useful for testing working components when messaging is not yet created.
+ * @param {string} [param0.model] makes only sense when `param0.hasView` is set:
+ * the model to use in createView.
+ * @param {integer} [param0.res_id] makes only sense when `param0.hasView` is set:
+ * the res_id to use in createView.
+ * @param {Object} [param0.services]
+ * @param {Object} [param0.session]
+ * @param {Object} [param0.View] makes only sense when `param0.hasView` is set:
+ * the View class to use in createView.
+ * @param {Object} [param0.viewOptions] makes only sense when `param0.hasView`
+ * is set: the view options to use in createView.
+ * @param {Object} [param0.waitUntilEvent]
+ * @param {String} [param0.waitUntilEvent.eventName]
+ * @param {String} [param0.waitUntilEvent.message]
+ * @param {function} [param0.waitUntilEvent.predicate]
+ * @param {integer} [param0.waitUntilEvent.timeoutDelay]
+ * @param {string} [param0.waitUntilMessagingCondition='initialized'] Determines
+ * the condition of messaging when this function is resolved.
+ * Supported values: ['none', 'created', 'initialized'].
+ * - 'none': the function resolves regardless of whether messaging is created.
+ * - 'created': the function resolves when messaging is created, but
+ * regardless of whether messaging is initialized.
+ * - 'initialized' (default): the function resolves when messaging is
+ * initialized.
+ * To guarantee messaging is not created, test should pass a pending deferred
+ * as param of `messagingBeforeCreationDeferred`. To make sure messaging is
+ * not initialized, test should mock RPC `mail/init_messaging` and block its
+ * resolution.
+ * @param {...Object} [param0.kwargs]
+ * @throws {Error} in case some provided parameters are wrong, such as
+ * `waitUntilMessagingCondition`.
+ * @returns {Object}
+ */
+async function start(param0 = {}) {
+ let callbacks = {
+ init: [],
+ mount: [],
+ destroy: [],
+ return: [],
+ };
+ const {
+ env: providedEnv,
+ hasActionManager = false,
+ hasChatWindow = false,
+ hasDialog = false,
+ hasDiscuss = false,
+ hasMessagingMenu = false,
+ hasTimeControl = false,
+ hasView = false,
+ messagingBeforeCreationDeferred = Promise.resolve(),
+ waitUntilEvent,
+ waitUntilMessagingCondition = 'initialized',
+ } = param0;
+ if (!['none', 'created', 'initialized'].includes(waitUntilMessagingCondition)) {
+ throw Error(`Unknown parameter value ${waitUntilMessagingCondition} for 'waitUntilMessaging'.`);
+ }
+ delete param0.env;
+ delete param0.hasActionManager;
+ delete param0.hasChatWindow;
+ delete param0.hasDiscuss;
+ delete param0.hasMessagingMenu;
+ delete param0.hasTimeControl;
+ delete param0.hasView;
+ if (hasChatWindow) {
+ callbacks = _useChatWindow(callbacks);
+ }
+ if (hasDialog) {
+ callbacks = _useDialog(callbacks);
+ }
+ if (hasDiscuss) {
+ callbacks = _useDiscuss(callbacks);
+ }
+ if (hasMessagingMenu) {
+ callbacks = _useMessagingMenu(callbacks);
+ }
+ const {
+ init: initCallbacks,
+ mount: mountCallbacks,
+ destroy: destroyCallbacks,
+ return: returnCallbacks,
+ } = callbacks;
+ const { debug = false } = param0;
+ initCallbacks.forEach(callback => callback(param0));
+
+ let env = Object.assign(providedEnv || {});
+ env.session = Object.assign(
+ {
+ is_bound: Promise.resolve(),
+ url: s => s,
+ },
+ env.session
+ );
+ env = addMessagingToEnv(env);
+ if (hasTimeControl) {
+ env = addTimeControlToEnv(env);
+ }
+
+ const services = Object.assign({}, {
+ bus_service: BusService.extend({
+ _beep() {}, // Do nothing
+ _poll() {}, // Do nothing
+ _registerWindowUnload() {}, // Do nothing
+ isOdooFocused() {
+ return true;
+ },
+ updateOption() {},
+ }),
+ chat_window: ChatWindowService.extend({
+ _getParentNode() {
+ return document.querySelector(debug ? 'body' : '#qunit-fixture');
+ },
+ _listenHomeMenu: () => {},
+ }),
+ dialog: DialogService.extend({
+ _getParentNode() {
+ return document.querySelector(debug ? 'body' : '#qunit-fixture');
+ },
+ _listenHomeMenu: () => {},
+ }),
+ local_storage: AbstractStorageService.extend({ storage: new RamStorage() }),
+ notification: NotificationService.extend(),
+ }, param0.services);
+
+ const kwargs = Object.assign({}, param0, {
+ archs: Object.assign({}, {
+ 'mail.message,false,search': '<search/>'
+ }, param0.archs),
+ debug: param0.debug || false,
+ services: Object.assign({}, services, param0.services),
+ }, { env });
+ let widget;
+ let mockServer; // only in basic mode
+ let testEnv;
+ const selector = debug ? 'body' : '#qunit-fixture';
+ if (hasView) {
+ widget = await createView(kwargs);
+ legacyPatch(widget, {
+ destroy() {
+ destroyCallbacks.forEach(callback => callback({ widget }));
+ this._super(...arguments);
+ legacyUnpatch(widget);
+ if (testEnv) {
+ testEnv.destroyMessaging();
+ }
+ }
+ });
+ } else if (hasActionManager) {
+ widget = await createActionManager(kwargs);
+ legacyPatch(widget, {
+ destroy() {
+ destroyCallbacks.forEach(callback => callback({ widget }));
+ this._super(...arguments);
+ legacyUnpatch(widget);
+ if (testEnv) {
+ testEnv.destroyMessaging();
+ }
+ }
+ });
+ } else {
+ const Parent = Widget.extend({ do_push_state() {} });
+ const parent = new Parent();
+ mockServer = await addMockEnvironment(parent, kwargs);
+ widget = new Widget(parent);
+ await widget.appendTo($(selector));
+ Object.assign(widget, {
+ destroy() {
+ delete widget.destroy;
+ destroyCallbacks.forEach(callback => callback({ widget }));
+ parent.destroy();
+ if (testEnv) {
+ testEnv.destroyMessaging();
+ }
+ },
+ });
+ }
+
+ testEnv = Component.env;
+
+ /**
+ * Components cannot use web.bus, because they cannot use
+ * EventDispatcherMixin, and webclient cannot easily access env.
+ * Communication between webclient and components by core.bus
+ * (usable by webclient) and messagingBus (usable by components), which
+ * the messaging service acts as mediator since it can easily use both
+ * kinds of buses.
+ */
+ testEnv.bus.on(
+ 'hide_home_menu',
+ null,
+ () => testEnv.messagingBus.trigger('hide_home_menu')
+ );
+ testEnv.bus.on(
+ 'show_home_menu',
+ null,
+ () => testEnv.messagingBus.trigger('show_home_menu')
+ );
+ testEnv.bus.on(
+ 'will_hide_home_menu',
+ null,
+ () => testEnv.messagingBus.trigger('will_hide_home_menu')
+ );
+ testEnv.bus.on(
+ 'will_show_home_menu',
+ null,
+ () => testEnv.messagingBus.trigger('will_show_home_menu')
+ );
+
+ /**
+ * Returns a promise resolved after the expected event is received.
+ *
+ * @param {Object} param0
+ * @param {string} param0.eventName event to wait
+ * @param {function} param0.func function which, when called, is expected to
+ * trigger the event
+ * @param {string} [param0.message] assertion message
+ * @param {function} [param0.predicate] predicate called with event data.
+ * If not provided, only the event name has to match.
+ * @param {number} [param0.timeoutDelay=5000] how long to wait at most in ms
+ * @returns {Promise}
+ */
+ const afterEvent = (async ({ eventName, func, message, predicate, timeoutDelay = 5000 }) => {
+ // Set up the timeout to reject if the event is not triggered.
+ let timeoutNoEvent;
+ const timeoutProm = new Promise((resolve, reject) => {
+ timeoutNoEvent = setTimeout(() => {
+ let error = message
+ ? new Error(message)
+ : new Error(`Timeout: the event ${eventName} was not triggered.`);
+ console.error(error);
+ reject(error);
+ }, timeoutDelay);
+ });
+ // Set up the promise to resolve if the event is triggered.
+ const eventProm = new Promise(resolve => {
+ testEnv.messagingBus.on(eventName, null, data => {
+ if (!predicate || predicate(data)) {
+ resolve();
+ }
+ });
+ });
+ // Start the function expected to trigger the event after the
+ // promise has been registered to not miss any potential event.
+ const funcRes = func();
+ // Make them race (first to resolve/reject wins).
+ await Promise.race([eventProm, timeoutProm]);
+ clearTimeout(timeoutNoEvent);
+ // If the event is triggered before the end of the async function,
+ // ensure the function finishes its job before returning.
+ await funcRes;
+ });
+
+ const result = {
+ afterEvent,
+ env: testEnv,
+ mockServer,
+ widget,
+ };
+
+ const start = async () => {
+ messagingBeforeCreationDeferred.then(async () => {
+ /**
+ * Some models require session data, like locale text direction
+ * (depends on fully loaded translation).
+ */
+ await env.session.is_bound;
+
+ testEnv.modelManager = new ModelManager(testEnv);
+ testEnv.modelManager.start();
+ /**
+ * Create the messaging singleton record.
+ */
+ testEnv.messaging = testEnv.models['mail.messaging'].create();
+ testEnv.messaging.start().then(() =>
+ testEnv.messagingInitializedDeferred.resolve()
+ );
+ testEnv.messagingCreatedPromise.resolve();
+ });
+ if (waitUntilMessagingCondition === 'created') {
+ await testEnv.messagingCreatedPromise;
+ }
+ if (waitUntilMessagingCondition === 'initialized') {
+ await testEnv.messagingInitializedDeferred;
+ }
+
+ if (mountCallbacks.length > 0) {
+ await afterNextRender(async () => {
+ await Promise.all(mountCallbacks.map(callback => callback({ selector, widget })));
+ });
+ }
+ returnCallbacks.forEach(callback => callback(result));
+ };
+ if (waitUntilEvent) {
+ await afterEvent(Object.assign({ func: start }, waitUntilEvent));
+ } else {
+ await start();
+ }
+ return result;
+}
+
+//------------------------------------------------------------------------------
+// Public: file utilities
+//------------------------------------------------------------------------------
+
+/**
+ * Drag some files over a DOM element
+ *
+ * @param {DOM.Element} el
+ * @param {Object[]} file must have been create beforehand
+ * @see testUtils.file.createFile
+ */
+function dragenterFiles(el, files) {
+ const ev = new Event('dragenter', { bubbles: true });
+ Object.defineProperty(ev, 'dataTransfer', {
+ value: _createFakeDataTransfer(files),
+ });
+ el.dispatchEvent(ev);
+}
+
+/**
+ * Drop some files on a DOM element
+ *
+ * @param {DOM.Element} el
+ * @param {Object[]} files must have been created beforehand
+ * @see testUtils.file.createFile
+ */
+function dropFiles(el, files) {
+ const ev = new Event('drop', { bubbles: true });
+ Object.defineProperty(ev, 'dataTransfer', {
+ value: _createFakeDataTransfer(files),
+ });
+ el.dispatchEvent(ev);
+}
+
+/**
+ * Paste some files on a DOM element
+ *
+ * @param {DOM.Element} el
+ * @param {Object[]} files must have been created beforehand
+ * @see testUtils.file.createFile
+ */
+function pasteFiles(el, files) {
+ const ev = new Event('paste', { bubbles: true });
+ Object.defineProperty(ev, 'clipboardData', {
+ value: _createFakeDataTransfer(files),
+ });
+ el.dispatchEvent(ev);
+}
+
+//------------------------------------------------------------------------------
+// Export
+//------------------------------------------------------------------------------
+
+return {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ dragenterFiles,
+ dropFiles,
+ nextAnimationFrame,
+ nextTick,
+ pasteFiles,
+ start,
+};
+
+});