task-2317862 closes odoo/odoo#56303 X-original-commit: 3b5cd048f1dfa18869e95b173b4232b4bb189025 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
783 lines
28 KiB
JavaScript
783 lines
28 KiB
JavaScript
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);
|
|
// patch session
|
|
Object.assign(session, {
|
|
getTZOffset() { return 0; },
|
|
async user_has_group() { return false; },
|
|
}, params.session);
|
|
|
|
// 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;
|
|
if ('session' in params) {
|
|
for (key in session) {
|
|
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,
|
|
};
|
|
|
|
});
|