summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/helpers/test_utils_mock.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/helpers/test_utils_mock.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/helpers/test_utils_mock.js')
-rw-r--r--addons/web/static/tests/helpers/test_utils_mock.js781
1 files changed, 781 insertions, 0 deletions
diff --git a/addons/web/static/tests/helpers/test_utils_mock.js b/addons/web/static/tests/helpers/test_utils_mock.js
new file mode 100644
index 00000000..8474ed22
--- /dev/null
+++ b/addons/web/static/tests/helpers/test_utils_mock.js
@@ -0,0 +1,781 @@
+odoo.define('web.test_utils_mock', function (require) {
+"use strict";
+
+/**
+ * Mock Test Utils
+ *
+ * This module defines various utility functions to help mocking data.
+ *
+ * Note that all methods defined in this module are exported in the main
+ * testUtils file.
+ */
+
+const AbstractStorageService = require('web.AbstractStorageService');
+const AjaxService = require('web.AjaxService');
+const basic_fields = require('web.basic_fields');
+const Bus = require('web.Bus');
+const config = require('web.config');
+const core = require('web.core');
+const dom = require('web.dom');
+const makeTestEnvironment = require('web.test_env');
+const MockServer = require('web.MockServer');
+const RamStorage = require('web.RamStorage');
+const session = require('web.session');
+
+const DebouncedField = basic_fields.DebouncedField;
+
+
+//------------------------------------------------------------------------------
+// Private functions
+//------------------------------------------------------------------------------
+
+/**
+ * Returns a mocked environment to be used by OWL components in tests, with
+ * requested services (+ ajax, local_storage and session_storage) deployed.
+ *
+ * @private
+ * @param {Object} params
+ * @param {Bus} [params.bus]
+ * @param {boolean} [params.debug]
+ * @param {Object} [params.env]
+ * @param {Bus} [params.env.bus]
+ * @param {Object} [params.env.dataManager]
+ * @param {Object} [params.env.services]
+ * @param {Object[]} [params.favoriteFilters]
+ * @param {Object} [params.services]
+ * @param {Object} [params.session]
+ * @param {MockServer} [mockServer]
+ * @returns {Promise<Object>} env
+ */
+async function _getMockedOwlEnv(params, mockServer) {
+ params.env = params.env || {};
+
+ // build the env
+ const favoriteFilters = params.favoriteFilters;
+ const debug = params.debug;
+ const services = {};
+ const env = Object.assign({}, params.env, {
+ browser: Object.assign({
+ fetch: (resource, init) => mockServer.performFetch(resource, init),
+ }, params.env.browser),
+ bus: params.bus || params.env.bus || new Bus(),
+ dataManager: Object.assign({
+ load_action: (actionID, context) => {
+ return mockServer.performRpc('/web/action/load', {
+ action_id: actionID,
+ additional_context: context,
+ });
+ },
+ load_views: (params, options) => {
+ return mockServer.performRpc('/web/dataset/call_kw/' + params.model, {
+ args: [],
+ kwargs: {
+ context: params.context,
+ options: options,
+ views: params.views_descr,
+ },
+ method: 'load_views',
+ model: params.model,
+ }).then(function (views) {
+ views = _.mapObject(views, viewParams => {
+ return fieldsViewGet(mockServer, viewParams);
+ });
+ if (favoriteFilters && 'search' in views) {
+ views.search.favoriteFilters = favoriteFilters;
+ }
+ return views;
+ });
+ },
+ load_filters: params => {
+ if (debug) {
+ console.log('[mock] load_filters', params);
+ }
+ return Promise.resolve([]);
+ },
+ }, params.env.dataManager),
+ services: Object.assign(services, params.env.services),
+ session: params.env.session || params.session || {},
+ });
+
+ // deploy services into the env
+ // determine services to instantiate (classes), and already register function services
+ const servicesToDeploy = {};
+ for (const name in params.services || {}) {
+ const Service = params.services[name];
+ if (Service.constructor.name === 'Class') {
+ servicesToDeploy[name] = Service;
+ } else {
+ services[name] = Service;
+ }
+ }
+ // always deploy ajax, local storage and session storage
+ if (!servicesToDeploy.ajax) {
+ const MockedAjaxService = AjaxService.extend({
+ rpc: mockServer.performRpc.bind(mockServer),
+ });
+ services.ajax = new MockedAjaxService(env);
+ }
+ const RamStorageService = AbstractStorageService.extend({
+ storage: new RamStorage(),
+ });
+ if (!servicesToDeploy.local_storage) {
+ services.local_storage = new RamStorageService(env);
+ }
+ if (!servicesToDeploy.session_storage) {
+ services.session_storage = new RamStorageService(env);
+ }
+ // deploy other requested services
+ let done = false;
+ while (!done) {
+ const serviceName = Object.keys(servicesToDeploy).find(serviceName => {
+ const Service = servicesToDeploy[serviceName];
+ return Service.prototype.dependencies.every(depName => {
+ return env.services[depName];
+ });
+ });
+ if (serviceName) {
+ const Service = servicesToDeploy[serviceName];
+ services[serviceName] = new Service(env);
+ delete servicesToDeploy[serviceName];
+ services[serviceName].start();
+ } else {
+ const serviceNames = _.keys(servicesToDeploy);
+ if (serviceNames.length) {
+ console.warn("Non loaded services:", serviceNames);
+ }
+ done = true;
+ }
+ }
+ // wait for asynchronous services to properly start
+ await new Promise(setTimeout);
+
+ return env;
+}
+/**
+ * This function is used to mock global objects (session, config...) in tests.
+ * It is necessary for legacy widgets. It returns a cleanUp function to call at
+ * the end of the test.
+ *
+ * The function could be removed as soon as we do not support legacy widgets
+ * anymore.
+ *
+ * @private
+ * @param {Object} params
+ * @param {Object} [params.config] if given, it is used to extend the global
+ * config,
+ * @param {Object} [params.session] if given, it is used to extend the current,
+ * real session.
+ * @param {Object} [params.translateParameters] if given, it will be used to
+ * extend the core._t.database.parameters object.
+ * @returns {function} a cleanUp function to restore everything, to call at the
+ * end of the test
+ */
+function _mockGlobalObjects(params) {
+ // store initial session state (for restoration)
+ const initialSession = Object.assign({}, session);
+ const sessionPatch = Object.assign({
+ getTZOffset() { return 0; },
+ async user_has_group() { return false; },
+ }, params.session);
+ // patch session
+ Object.assign(session, sessionPatch);
+
+ // patch config
+ let initialConfig;
+ if ('config' in params) {
+ initialConfig = Object.assign({}, config);
+ initialConfig.device = Object.assign({}, config.device);
+ if ('device' in params.config) {
+ Object.assign(config.device, params.config.device);
+ }
+ if ('debug' in params.config) {
+ odoo.debug = params.config.debug;
+ }
+ }
+
+ // patch translate params
+ let initialParameters;
+ if ('translateParameters' in params) {
+ initialParameters = Object.assign({}, core._t.database.parameters);
+ Object.assign(core._t.database.parameters, params.translateParameters);
+ }
+
+ // build the cleanUp function to restore everything at the end of the test
+ function cleanUp() {
+ let key;
+ for (key in sessionPatch) {
+ delete session[key];
+ }
+ Object.assign(session, initialSession);
+ if ('config' in params) {
+ for (key in config) {
+ delete config[key];
+ }
+ _.extend(config, initialConfig);
+ }
+ if ('translateParameters' in params) {
+ for (key in core._t.database.parameters) {
+ delete core._t.database.parameters[key];
+ }
+ _.extend(core._t.database.parameters, initialParameters);
+ }
+ }
+
+ return cleanUp;
+}
+/**
+ * logs all event going through the target widget.
+ *
+ * @param {Widget} widget
+ */
+function _observe(widget) {
+ var _trigger_up = widget._trigger_up.bind(widget);
+ widget._trigger_up = function (event) {
+ console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event);
+ _trigger_up(event);
+ };
+}
+
+//------------------------------------------------------------------------------
+// Public functions
+//------------------------------------------------------------------------------
+
+/**
+ * performs a fields_view_get, and mocks the postprocessing done by the
+ * data_manager to return an equivalent structure.
+ *
+ * @param {MockServer} server
+ * @param {Object} params
+ * @param {string} params.model
+ * @returns {Object} an object with 3 keys: arch, fields and viewFields
+ */
+function fieldsViewGet(server, params) {
+ var fieldsView = server.fieldsViewGet(params);
+ // mock the structure produced by the DataManager
+ fieldsView.viewFields = fieldsView.fields;
+ fieldsView.fields = server.fieldsGet(params.model);
+ return fieldsView;
+}
+
+/**
+ * intercepts an event bubbling up the widget hierarchy. The event intercepted
+ * must be a "custom event", i.e. an event generated by the method 'trigger_up'.
+ *
+ * Note that this method really intercepts the event if @propagate is not set.
+ * It will not be propagated further, and even the handlers on the target will
+ * not fire.
+ *
+ * @param {Widget} widget the target widget (any Odoo widget)
+ * @param {string} eventName description of the event
+ * @param {function} fn callback executed when the even is intercepted
+ * @param {boolean} [propagate=false]
+ */
+function intercept(widget, eventName, fn, propagate) {
+ var _trigger_up = widget._trigger_up.bind(widget);
+ widget._trigger_up = function (event) {
+ if (event.name === eventName) {
+ fn(event);
+ if (!propagate) { return; }
+ }
+ _trigger_up(event);
+ };
+}
+
+/**
+ * Removes the src attribute on images and iframes to prevent not found errors,
+ * and optionally triggers an rpc with the src url as route on a widget.
+ * This method is critical and must be fastest (=> no jQuery, no underscore)
+ *
+ * @param {HTMLElement} el
+ * @param {[function]} rpc
+ */
+function removeSrcAttribute(el, rpc) {
+ var nodes;
+ if (el.nodeName === "#comment") {
+ return;
+ }
+ el = el.nodeType === 8 ? el.nextSibling : el;
+ if (el.nodeName === 'IMG' || el.nodeName === 'IFRAME') {
+ nodes = [el];
+ } else {
+ nodes = Array.prototype.slice.call(el.getElementsByTagName('img'))
+ .concat(Array.prototype.slice.call(el.getElementsByTagName('iframe')));
+ }
+ var node;
+ while (node = nodes.pop()) {
+ var src = node.attributes.src && node.attributes.src.value;
+ if (src && src !== 'about:blank') {
+ node.setAttribute('data-src', src);
+ if (node.nodeName === 'IMG') {
+ node.attributes.removeNamedItem('src');
+ } else {
+ node.setAttribute('src', 'about:blank');
+ }
+ if (rpc) {
+ rpc(src, []);
+ }
+ $(node).trigger('load');
+ }
+ }
+}
+
+/**
+ * Add a mock environment to test Owl Components. This function generates a test
+ * env and sets it on the given Component. It also has several side effects,
+ * like patching the global session or config objects. It returns a cleanup
+ * function to call at the end of the test.
+ *
+ * @param {Component} Component
+ * @param {Object} [params]
+ * @param {Object} [params.actions]
+ * @param {Object} [params.archs]
+ * @param {string} [params.currentDate]
+ * @param {Object} [params.data]
+ * @param {boolean} [params.debug]
+ * @param {function} [params.mockFetch]
+ * @param {function} [params.mockRPC]
+ * @param {number} [params.fieldDebounce=0] the value of the DEBOUNCE attribute
+ * of fields
+ * @param {boolean} [params.debounce=true] if false, patch _.debounce to remove
+ * its behavior
+ * @param {boolean} [params.throttle=false] by default, _.throttle is patched to
+ * remove its behavior, except if this params is set to true
+ * @param {boolean} [params.mockSRC=false] if true, redirect src GET requests to
+ * the mockServer
+ * @param {MockServer} [mockServer]
+ * @returns {Promise<function>} the cleanup function
+ */
+async function addMockEnvironmentOwl(Component, params, mockServer) {
+ params = params || {};
+
+ // instantiate a mockServer if not provided
+ if (!mockServer) {
+ let Server = MockServer;
+ if (params.mockFetch) {
+ Server = MockServer.extend({ _performFetch: params.mockFetch });
+ }
+ if (params.mockRPC) {
+ Server = Server.extend({ _performRpc: params.mockRPC });
+ }
+ mockServer = new Server(params.data, {
+ actions: params.actions,
+ archs: params.archs,
+ currentDate: params.currentDate,
+ debug: params.debug,
+ });
+ }
+
+ // make sure the debounce value for input fields is set to 0
+ const initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
+ DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
+ const initialDOMDebounceValue = dom.DEBOUNCE;
+ dom.DEBOUNCE = 0;
+
+ // patch underscore debounce/throttle functions
+ const initialDebounce = _.debounce;
+ if (params.debounce === false) {
+ _.debounce = function (func) {
+ return func;
+ };
+ }
+ // fixme: throttle is inactive by default, should we make it explicit ?
+ const initialThrottle = _.throttle;
+ if (!('throttle' in params) || !params.throttle) {
+ _.throttle = function (func) {
+ return func;
+ };
+ }
+
+ // make sure images do not trigger a GET on the server
+ $('body').on('DOMNodeInserted.removeSRC', function (ev) {
+ let rpc;
+ if (params.mockSRC) {
+ rpc = mockServer.performRpc.bind(mockServer);
+ }
+ removeSrcAttribute(ev.target, rpc);
+ });
+
+ // mock global objects for legacy widgets (session, config...)
+ const restoreMockedGlobalObjects = _mockGlobalObjects(params);
+
+ // set the test env on owl Component
+ const env = await _getMockedOwlEnv(params, mockServer);
+ const originalEnv = Component.env;
+ Component.env = makeTestEnvironment(env, mockServer.performRpc.bind(mockServer));
+
+ // while we have a mix between Owl and legacy stuff, some of them triggering
+ // events on the env.bus (a new Bus instance especially created for the current
+ // test), the others using core.bus, we have to ensure that events triggered
+ // on env.bus are also triggered on core.bus (note that outside the testing
+ // environment, both are the exact same instance of Bus)
+ const envBusTrigger = env.bus.trigger;
+ env.bus.trigger = function () {
+ core.bus.trigger(...arguments);
+ envBusTrigger.call(env.bus, ...arguments);
+ };
+
+ // build the clean up function to call at the end of the test
+ function cleanUp() {
+ env.bus.destroy();
+ Object.keys(env.services).forEach(function (s) {
+ var service = env.services[s];
+ if (service.destroy && !service.isDestroyed()) {
+ service.destroy();
+ }
+ });
+
+ DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
+ dom.DEBOUNCE = initialDOMDebounceValue;
+ _.debounce = initialDebounce;
+ _.throttle = initialThrottle;
+
+ // clear the caches (e.g. data_manager, ModelFieldSelector) at the end
+ // of each test to avoid collisions
+ core.bus.trigger('clear_cache');
+
+ $('body').off('DOMNodeInserted.removeSRC');
+ $('.blockUI').remove(); // fixme: move to qunit_config in OdooAfterTestHook?
+
+ restoreMockedGlobalObjects();
+
+ Component.env = originalEnv;
+ }
+
+ return cleanUp;
+}
+
+/**
+ * Add a mock environment to a widget. This helper function can simulate
+ * various kind of side effects, such as mocking RPCs, changing the session,
+ * or the translation settings.
+ *
+ * The simulated environment lasts for the lifecycle of the widget, meaning it
+ * disappears when the widget is destroyed. It is particularly relevant for the
+ * session mocks, because the previous session is restored during the destroy
+ * call. So, it means that you have to be careful and make sure that it is
+ * properly destroyed before another test is run, otherwise you risk having
+ * interferences between tests.
+ *
+ * @param {Widget} widget
+ * @param {Object} params
+ * @param {Object} [params.archs] a map of string [model,view_id,view_type] to
+ * a arch object. It is used to mock answers to 'load_views' custom events.
+ * This is useful when the widget instantiate a formview dialog that needs
+ * to load a particular arch.
+ * @param {string} [params.currentDate] a string representation of the current
+ * date. It is given to the mock server.
+ * @param {Object} params.data the data given to the created mock server. It is
+ * used to generate mock answers for every kind of routes supported by odoo
+ * @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo
+ * events.
+ * @param {Object} [params.bus] the instance of Bus that will be used (in the env)
+ * @param {function} [params.mockFetch] a function that will be used to override
+ * the _performFetch method from the mock server. It is really useful to add
+ * some custom fetch mocks, or to check some assertions.
+ * @param {function} [params.mockRPC] a function that will be used to override
+ * the _performRpc method from the mock server. It is really useful to add
+ * some custom rpc mocks, or to check some assertions.
+ * @param {Object} [params.session] if it is given, it will be used as answer
+ * for all calls to this.getSession() by the widget, of its children. Also,
+ * it will be used to extend the current, real session. This side effect is
+ * undone when the widget is destroyed.
+ * @param {Object} [params.translateParameters] if given, it will be used to
+ * extend the core._t.database.parameters object. After the widget
+ * destruction, the original parameters will be restored.
+ * @param {Object} [params.intercepts] an object with event names as key, and
+ * callback as value. Each key,value will be used to intercept the event.
+ * Note that this is particularly useful if you want to intercept events going
+ * up in the init process of the view, because there are no other way to do it
+ * after this method returns. Some events ('call_service', "load_views",
+ * "get_session", "load_filters") have a special treatment beforehand.
+ * @param {Object} [params.services={}] list of services to load in
+ * addition to the ajax service. For instance, if a test needs the local
+ * storage service in order to work, it can provide a mock version of it.
+ * @param {boolean} [debounce=true] set to false to completely remove the
+ * debouncing, forcing the handler to be called directly (not on the next
+ * execution stack, like it does with delay=0).
+ * @param {boolean} [throttle=false] set to true to keep the throttling, which
+ * is completely removed by default.
+ *
+ * @returns {Promise<MockServer>} the instance of the mock server, created by this
+ * function. It is necessary for createView so that method can call some
+ * other methods on it.
+ */
+async function addMockEnvironment(widget, params) {
+ // log events triggered up if debug flag is true
+ if (params.debug) {
+ _observe(widget);
+ var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
+ var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId;
+ console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
+ }
+
+ // instantiate mock server
+ var Server = MockServer;
+ if (params.mockFetch) {
+ Server = MockServer.extend({ _performFetch: params.mockFetch });
+ }
+ if (params.mockRPC) {
+ Server = Server.extend({ _performRpc: params.mockRPC });
+ }
+ var mockServer = new Server(params.data, {
+ actions: params.actions,
+ archs: params.archs,
+ currentDate: params.currentDate,
+ debug: params.debug,
+ widget: widget,
+ });
+
+ // build and set the Owl env on Component
+ if (!('mockSRC' in params)) { // redirect src rpcs to the mock server
+ params.mockSRC = true;
+ }
+ const cleanUp = await addMockEnvironmentOwl(owl.Component, params, mockServer);
+ const env = owl.Component.env;
+
+ // ensure to clean up everything when the widget will be destroyed
+ const destroy = widget.destroy;
+ widget.destroy = function () {
+ cleanUp();
+ destroy.call(this, ...arguments);
+ };
+
+ // intercept service/data manager calls and redirect them to the env
+ intercept(widget, 'call_service', function (ev) {
+ if (env.services[ev.data.service]) {
+ var service = env.services[ev.data.service];
+ const result = service[ev.data.method].apply(service, ev.data.args || []);
+ ev.data.callback(result);
+ }
+ });
+ intercept(widget, 'load_action', async ev => {
+ const action = await env.dataManager.load_action(ev.data.actionID, ev.data.context);
+ ev.data.on_success(action);
+ });
+ intercept(widget, "load_views", async ev => {
+ const params = {
+ model: ev.data.modelName,
+ context: ev.data.context,
+ views_descr: ev.data.views,
+ };
+ const views = await env.dataManager.load_views(params, ev.data.options);
+ if ('search' in views && params.favoriteFilters) {
+ views.search.favoriteFilters = params.favoriteFilters;
+ }
+ ev.data.on_success(views);
+ });
+ intercept(widget, "get_session", ev => {
+ ev.data.callback(session);
+ });
+ intercept(widget, "load_filters", async ev => {
+ const filters = await env.dataManager.load_filters(ev.data);
+ ev.data.on_success(filters);
+ });
+
+ // make sure all other Odoo events bubbling up are intercepted
+ Object.keys(params.intercepts || {}).forEach(function (name) {
+ intercept(widget, name, params.intercepts[name]);
+ });
+
+ return mockServer;
+}
+
+/**
+ * Patch window.Date so that the time starts its flow from the provided Date.
+ *
+ * Usage:
+ *
+ * ```
+ * var unpatchDate = testUtils.mock.patchDate(2018, 0, 10, 17, 59, 30)
+ * new window.Date(); // "Wed Jan 10 2018 17:59:30 GMT+0100 (Central European Standard Time)"
+ * ... // 5 hours delay
+ * new window.Date(); // "Wed Jan 10 2018 22:59:30 GMT+0100 (Central European Standard Time)"
+ * ...
+ * unpatchDate();
+ * new window.Date(); // actual current date time
+ * ```
+ *
+ * @param {integer} year
+ * @param {integer} month index of the month, starting from zero.
+ * @param {integer} day the day of the month.
+ * @param {integer} hours the digits for hours (24h)
+ * @param {integer} minutes
+ * @param {integer} seconds
+ * @returns {Function} a callback to unpatch window.Date.
+ */
+function patchDate(year, month, day, hours, minutes, seconds) {
+ var RealDate = window.Date;
+ var actualDate = new RealDate();
+ var fakeDate = new RealDate(year, month, day, hours, minutes, seconds);
+ var timeInterval = actualDate.getTime() - (fakeDate.getTime());
+
+ Date = (function (NativeDate) {
+ function Date(Y, M, D, h, m, s, ms) {
+ var length = arguments.length;
+ if (arguments.length > 0) {
+ var date = length == 1 && String(Y) === Y ? // isString(Y)
+ // We explicitly pass it through parse:
+ new NativeDate(Date.parse(Y)) :
+ // We have to manually make calls depending on argument
+ // length here
+ length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
+ length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
+ length >= 5 ? new NativeDate(Y, M, D, h, m) :
+ length >= 4 ? new NativeDate(Y, M, D, h) :
+ length >= 3 ? new NativeDate(Y, M, D) :
+ length >= 2 ? new NativeDate(Y, M) :
+ length >= 1 ? new NativeDate(Y) :
+ new NativeDate();
+ // Prevent mixups with unfixed Date object
+ date.constructor = Date;
+ return date;
+ } else {
+ var date = new NativeDate();
+ var time = date.getTime();
+ time -= timeInterval;
+ date.setTime(time);
+ return date;
+ }
+ }
+
+ // Copy any custom methods a 3rd party library may have added
+ for (var key in NativeDate) {
+ Date[key] = NativeDate[key];
+ }
+
+ // Copy "native" methods explicitly; they may be non-enumerable
+ // exception: 'now' uses fake date as reference
+ Date.now = function () {
+ var date = new NativeDate();
+ var time = date.getTime();
+ time -= timeInterval;
+ return time;
+ };
+ Date.UTC = NativeDate.UTC;
+ Date.prototype = NativeDate.prototype;
+ Date.prototype.constructor = Date;
+
+ // Upgrade Date.parse to handle simplified ISO 8601 strings
+ Date.parse = NativeDate.parse;
+ return Date;
+ })(Date);
+
+ return function () { window.Date = RealDate; };
+}
+
+var patches = {};
+/**
+ * Patches a given Class or Object with the given properties.
+ *
+ * @param {Class|Object} target
+ * @param {Object} props
+ */
+function patch(target, props) {
+ var patchID = _.uniqueId('patch_');
+ target.__patchID = patchID;
+ patches[patchID] = {
+ target: target,
+ otherPatchedProps: [],
+ ownPatchedProps: [],
+ };
+ if (target.prototype) {
+ _.each(props, function (value, key) {
+ if (target.prototype.hasOwnProperty(key)) {
+ patches[patchID].ownPatchedProps.push({
+ key: key,
+ initialValue: target.prototype[key],
+ });
+ } else {
+ patches[patchID].otherPatchedProps.push(key);
+ }
+ });
+ target.include(props);
+ } else {
+ _.each(props, function (value, key) {
+ if (key in target) {
+ var oldValue = target[key];
+ patches[patchID].ownPatchedProps.push({
+ key: key,
+ initialValue: oldValue,
+ });
+ if (typeof value === 'function') {
+ target[key] = function () {
+ var oldSuper = this._super;
+ this._super = oldValue;
+ var result = value.apply(this, arguments);
+ if (oldSuper === undefined) {
+ delete this._super;
+ } else {
+ this._super = oldSuper;
+ }
+ return result;
+ };
+ } else {
+ target[key] = value;
+ }
+ } else {
+ patches[patchID].otherPatchedProps.push(key);
+ target[key] = value;
+ }
+ });
+ }
+}
+
+/**
+ * Unpatches a given Class or Object.
+ *
+ * @param {Class|Object} target
+ */
+function unpatch(target) {
+ var patchID = target.__patchID;
+ var patch = patches[patchID];
+ if (target.prototype) {
+ _.each(patch.ownPatchedProps, function (p) {
+ target.prototype[p.key] = p.initialValue;
+ });
+ _.each(patch.otherPatchedProps, function (key) {
+ delete target.prototype[key];
+ });
+ } else {
+ _.each(patch.ownPatchedProps, function (p) {
+ target[p.key] = p.initialValue;
+ });
+ _.each(patch.otherPatchedProps, function (key) {
+ delete target[key];
+ });
+ }
+ delete patches[patchID];
+ delete target.__patchID;
+}
+
+window.originalSetTimeout = window.setTimeout;
+function patchSetTimeout() {
+ var original = window.setTimeout;
+ var self = this;
+ window.setTimeout = function (handler, delay) {
+ console.log("calling setTimeout on " + (handler.name || "some function") + "with delay of " + delay);
+ console.trace();
+ var handlerArguments = Array.prototype.slice.call(arguments, 1);
+ return original(function () {
+ handler.bind(self, handlerArguments)();
+ console.log('after doing the action of the setTimeout');
+ }, delay);
+ };
+
+ return function () {
+ window.setTimeout = original;
+ };
+}
+
+return {
+ addMockEnvironment: addMockEnvironment,
+ fieldsViewGet: fieldsViewGet,
+ addMockEnvironmentOwl: addMockEnvironmentOwl,
+ intercept: intercept,
+ patchDate: patchDate,
+ patch: patch,
+ unpatch: unpatch,
+ patchSetTimeout: patchSetTimeout,
+};
+
+});