summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/helpers/test_utils_create.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_create.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/helpers/test_utils_create.js')
-rw-r--r--addons/web/static/tests/helpers/test_utils_create.js512
1 files changed, 512 insertions, 0 deletions
diff --git a/addons/web/static/tests/helpers/test_utils_create.js b/addons/web/static/tests/helpers/test_utils_create.js
new file mode 100644
index 00000000..908ac97a
--- /dev/null
+++ b/addons/web/static/tests/helpers/test_utils_create.js
@@ -0,0 +1,512 @@
+odoo.define('web.test_utils_create', function (require) {
+ "use strict";
+
+ /**
+ * Create Test Utils
+ *
+ * This module defines various utility functions to help creating mock widgets
+ *
+ * Note that all methods defined in this module are exported in the main
+ * testUtils file.
+ */
+
+ const ActionManager = require('web.ActionManager');
+ const ActionMenus = require('web.ActionMenus');
+ const concurrency = require('web.concurrency');
+ const config = require('web.config');
+ const ControlPanel = require('web.ControlPanel');
+ const customHooks = require('web.custom_hooks');
+ const DebugManager = require('web.DebugManager.Backend');
+ const dom = require('web.dom');
+ const makeTestEnvironment = require('web.test_env');
+ const ActionModel = require('web/static/src/js/views/action_model.js');
+ const Registry = require('web.Registry');
+ const testUtilsMock = require('web.test_utils_mock');
+ const Widget = require('web.Widget');
+
+ const { Component } = owl;
+ const { useRef, useState } = owl.hooks;
+ const { xml } = owl.tags;
+
+ /**
+ * Create and return an instance of ActionManager with all rpcs going through a
+ * mock method using the data, actions and archs objects as sources.
+ *
+ * @param {Object} [params={}]
+ * @param {Object} [params.actions] the actions given to the mock server
+ * @param {Object} [params.archs] this archs given to the mock server
+ * @param {Object} [params.data] the business data given to the mock server
+ * @param {function} [params.mockRPC]
+ * @returns {Promise<ActionManager>}
+ */
+ async function createActionManager(params = {}) {
+ const target = prepareTarget(params.debug);
+
+ const widget = new Widget();
+ // when 'document' addon is installed, the sidebar does a 'search_read' on
+ // model 'ir_attachment' each time a record is open, so we monkey-patch
+ // 'mockRPC' to mute those RPCs, so that the tests can be written uniformly,
+ // whether or not 'document' is installed
+ const mockRPC = params.mockRPC;
+ Object.assign(params, {
+ async mockRPC(route, args) {
+ if (args.model === 'ir.attachment') {
+ return [];
+ }
+ if (mockRPC) {
+ return mockRPC.apply(this, arguments);
+ }
+ return this._super(...arguments);
+ },
+ });
+ const mockServer = await testUtilsMock.addMockEnvironment(widget, Object.assign({ debounce: false }, params));
+ await widget.prependTo(target);
+ widget.el.classList.add('o_web_client');
+ if (config.device.isMobile) {
+ widget.el.classList.add('o_touch_device');
+ }
+
+ params.server = mockServer;
+
+ const userContext = params.context && params.context.user_context || {};
+ const actionManager = new ActionManager(widget, userContext);
+
+ // Override the ActionMenus registry unless told otherwise.
+ let actionMenusRegistry = ActionMenus.registry;
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = new Registry();
+ }
+
+ const originalDestroy = ActionManager.prototype.destroy;
+ actionManager.destroy = function () {
+ actionManager.destroy = originalDestroy;
+ widget.destroy();
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = actionMenusRegistry;
+ }
+ };
+ const fragment = document.createDocumentFragment();
+ await actionManager.appendTo(fragment);
+ dom.append(widget.el, fragment, {
+ callbacks: [{ widget: actionManager }],
+ in_DOM: true,
+ });
+ return actionManager;
+ }
+
+ /**
+ * Similar as createView, but specific for calendar views. Some calendar
+ * tests need to trigger positional clicks on the DOM produced by fullcalendar.
+ * Those tests must use this helper with option positionalClicks set to true.
+ * This will move the rendered calendar to the body (required to do positional
+ * clicks), and wait for a setTimeout(0) before returning, because fullcalendar
+ * makes the calendar scroll to 6:00 in a setTimeout(0), which might have an
+ * impact according to where we want to trigger positional clicks.
+ *
+ * @param {Object} params @see createView
+ * @param {Object} [options]
+ * @param {boolean} [options.positionalClicks=false]
+ * @returns {Promise<CalendarController>}
+ */
+ async function createCalendarView(params, options) {
+ const calendar = await createView(params);
+ if (!options || !options.positionalClicks) {
+ return calendar;
+ }
+ const viewElements = [...document.getElementById('qunit-fixture').children];
+ // prepend reset the scrollTop to zero so we restore it manually
+ let fcScroller = document.querySelector('.fc-scroller');
+ const scrollPosition = fcScroller.scrollTop;
+ viewElements.forEach(el => document.body.prepend(el));
+ fcScroller = document.querySelector('.fc-scroller');
+ fcScroller.scrollTop = scrollPosition;
+
+ const destroy = calendar.destroy;
+ calendar.destroy = () => {
+ viewElements.forEach(el => el.remove());
+ destroy();
+ };
+ await concurrency.delay(0);
+ return calendar;
+ }
+
+ /**
+ * Create a simple component environment with a basic Parent component, an
+ * extensible env and a mocked server. The returned value is the instance of
+ * the given constructor.
+ * @param {class} constructor Component class to instantiate
+ * @param {Object} [params = {}]
+ * @param {boolean} [params.debug]
+ * @param {Object} [params.env]
+ * @param {Object} [params.intercepts] object in which the keys represent the
+ * intercepted event names and the values are their callbacks.
+ * @param {Object} [params.props]
+ * @returns {Promise<Component>} instance of `constructor`
+ */
+ async function createComponent(constructor, params = {}) {
+ if (!constructor) {
+ throw new Error(`Missing argument "constructor".`);
+ }
+ if (!(constructor.prototype instanceof Component)) {
+ throw new Error(`Argument "constructor" must be an Owl Component.`);
+ }
+ const cleanUp = await testUtilsMock.addMockEnvironmentOwl(Component, params);
+ class Parent extends Component {
+ constructor() {
+ super(...arguments);
+ this.Component = constructor;
+ this.state = useState(params.props || {});
+ this.component = useRef('component');
+ for (const eventName in params.intercepts || {}) {
+ customHooks.useListener(eventName, params.intercepts[eventName]);
+ }
+ }
+ }
+ Parent.template = xml`<t t-component="Component" t-props="state" t-ref="component"/>`;
+ const parent = new Parent();
+ await parent.mount(prepareTarget(params.debug), { position: 'first-child' });
+ const child = parent.component.comp;
+ const originalDestroy = child.destroy;
+ child.destroy = function () {
+ child.destroy = originalDestroy;
+ cleanUp();
+ parent.destroy();
+ };
+ return child;
+ }
+
+ /**
+ * Create a Control Panel instance, with an extensible environment and
+ * its related Control Panel Model. Event interception is done through
+ * params['get-controller-query-params'] and params.search, for the two
+ * available event handlers respectively.
+ * @param {Object} [params={}]
+ * @param {Object} [params.cpProps]
+ * @param {Object} [params.cpModelConfig]
+ * @param {boolean} [params.debug]
+ * @param {Object} [params.env]
+ * @returns {Object} useful control panel testing elements:
+ * - controlPanel: the control panel instance
+ * - el: the control panel HTML element
+ * - helpers: a suite of bound helpers (see above functions for all
+ * available helpers)
+ */
+ async function createControlPanel(params = {}) {
+ const debug = params.debug || false;
+ const env = makeTestEnvironment(params.env || {});
+ const props = Object.assign({
+ action: {},
+ fields: {},
+ }, params.cpProps);
+ const globalConfig = Object.assign({
+ context: {},
+ domain: [],
+ }, params.cpModelConfig);
+
+ if (globalConfig.arch && globalConfig.fields) {
+ const model = "__mockmodel__";
+ const serverParams = {
+ model,
+ data: { [model]: { fields: globalConfig.fields, records: [] } },
+ };
+ const mockServer = await testUtilsMock.addMockEnvironment(
+ new Widget(),
+ serverParams,
+ );
+ const { arch, fields } = testUtilsMock.fieldsViewGet(mockServer, {
+ arch: globalConfig.arch,
+ fields: globalConfig.fields,
+ model,
+ viewOptions: { context: globalConfig.context },
+ });
+ Object.assign(globalConfig, { arch, fields });
+ }
+
+ globalConfig.env = env;
+ const archs = (globalConfig.arch && { search: globalConfig.arch, }) || {};
+ const { ControlPanel: controlPanelInfo, } = ActionModel.extractArchInfo(archs);
+ const extensions = {
+ ControlPanel: { archNodes: controlPanelInfo.children, },
+ };
+
+ class Parent extends Component {
+ constructor() {
+ super();
+ this.searchModel = new ActionModel(extensions, globalConfig);
+ this.state = useState(props);
+ this.controlPanel = useRef("controlPanel");
+ }
+ async willStart() {
+ await this.searchModel.load();
+ }
+ mounted() {
+ if (params['get-controller-query-params']) {
+ this.searchModel.on('get-controller-query-params', this,
+ params['get-controller-query-params']);
+ }
+ if (params.search) {
+ this.searchModel.on('search', this, params.search);
+ }
+ }
+ }
+ Parent.components = { ControlPanel };
+ Parent.env = env;
+ Parent.template = xml`
+ <ControlPanel
+ t-ref="controlPanel"
+ t-props="state"
+ searchModel="searchModel"
+ />`;
+
+ const parent = new Parent();
+ await parent.mount(prepareTarget(debug), { position: 'first-child' });
+
+ const controlPanel = parent.controlPanel.comp;
+ const destroy = controlPanel.destroy;
+ controlPanel.destroy = function () {
+ controlPanel.destroy = destroy;
+ parent.destroy();
+ };
+ controlPanel.getQuery = () => parent.searchModel.get('query');
+
+ return controlPanel;
+ }
+
+ /**
+ * Create and return an instance of DebugManager with all rpcs going through a
+ * mock method, assuming that the user has access rights, and is an admin.
+ *
+ * @param {Object} [params={}]
+ * @returns {Promise<DebugManager>}
+ */
+ async function createDebugManager(params = {}) {
+ const mockRPC = params.mockRPC;
+ Object.assign(params, {
+ async mockRPC(route, args) {
+ if (args.method === 'check_access_rights') {
+ return true;
+ }
+ if (args.method === 'xmlid_to_res_id') {
+ return true;
+ }
+ if (mockRPC) {
+ return mockRPC.apply(this, arguments);
+ }
+ return this._super(...arguments);
+ },
+ session: {
+ async user_has_group(group) {
+ if (group === 'base.group_no_one') {
+ return true;
+ }
+ return this._super(...arguments);
+ },
+ },
+ });
+ const debugManager = new DebugManager();
+ await testUtilsMock.addMockEnvironment(debugManager, params);
+ return debugManager;
+ }
+
+ /**
+ * Create a model from given parameters.
+ *
+ * @param {Object} params This object will be given to addMockEnvironment, so
+ * any parameters from that method applies
+ * @param {Class} params.Model the model class to use
+ * @returns {Model}
+ */
+ async function createModel(params) {
+ const widget = new Widget();
+
+ const model = new params.Model(widget, params);
+
+ await testUtilsMock.addMockEnvironment(widget, params);
+
+ // override the model's 'destroy' so that it calls 'destroy' on the widget
+ // instead, as the widget is the parent of the model and the mockServer.
+ model.destroy = function () {
+ // remove the override to properly destroy the model when it will be
+ // called the second time (by its parent)
+ delete model.destroy;
+ widget.destroy();
+ };
+
+ return model;
+ }
+
+ /**
+ * Create a widget parent from given parameters.
+ *
+ * @param {Object} params This object will be given to addMockEnvironment, so
+ * any parameters from that method applies
+ * @returns {Promise<Widget>}
+ */
+ async function createParent(params) {
+ const widget = new Widget();
+ await testUtilsMock.addMockEnvironment(widget, params);
+ return widget;
+ }
+
+ /**
+ * Create a view from various parameters. Here, a view means a javascript
+ * instance of an AbstractView class, such as a form view, a list view or a
+ * kanban view.
+ *
+ * It returns the instance of the view, properly created, with all rpcs going
+ * through a mock method using the data object as source, and already loaded/
+ * started.
+ *
+ * @param {Object} params
+ * @param {string} params.arch the xml (arch) of the view to be instantiated
+ * @param {any[]} [params.domain] the initial domain for the view
+ * @param {Object} [params.context] the initial context for the view
+ * @param {string[]} [params.groupBy] the initial groupBy for the view
+ * @param {Object[]} [params.favoriteFilters] the favorite filters one would like to have at initialization
+ * @param {integer} [params.fieldDebounce=0] the debounce value to use for the
+ * duration of the test.
+ * @param {AbstractView} params.View the class that will be instantiated
+ * @param {string} params.model a model name, will be given to the view
+ * @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
+ * @param {Boolean} [params.doNotDisableAHref=false] will not preventDefault on the A elements of the view if true.
+ * Default is false.
+ * @returns {Promise<AbstractController>} the instance of the view
+ */
+ async function createView(params) {
+ const target = prepareTarget(params.debug);
+ const widget = new Widget();
+ // reproduce the DOM environment of views
+ const webClient = Object.assign(document.createElement('div'), {
+ className: 'o_web_client',
+ });
+ const actionManager = Object.assign(document.createElement('div'), {
+ className: 'o_action_manager',
+ });
+ target.prepend(webClient);
+ webClient.append(actionManager);
+
+ // add mock environment: mock server, session, fieldviewget, ...
+ const mockServer = await testUtilsMock.addMockEnvironment(widget, params);
+ const viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
+
+ params.server = mockServer;
+
+ // create the view
+ const View = params.View;
+ const modelName = params.model || 'foo';
+ const defaultAction = {
+ res_model: modelName,
+ context: {},
+ };
+ const viewOptions = Object.assign({
+ action: Object.assign(defaultAction, params.action),
+ view: viewInfo,
+ modelName: modelName,
+ ids: 'res_id' in params ? [params.res_id] : undefined,
+ currentId: 'res_id' in params ? params.res_id : undefined,
+ domain: params.domain || [],
+ context: params.context || {},
+ hasActionMenus: false,
+ }, params.viewOptions);
+ // patch the View to handle the groupBy given in params, as we can't give it
+ // in init (unlike the domain and context which can be set in the action)
+ testUtilsMock.patch(View, {
+ _updateMVCParams() {
+ this._super(...arguments);
+ this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
+ testUtilsMock.unpatch(View);
+ },
+ });
+ if ('hasSelectors' in params) {
+ viewOptions.hasSelectors = params.hasSelectors;
+ }
+
+ let view;
+ if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
+ // TODO: probably needs to create an helper just for that
+ view = new params.View({ viewInfo, modelName });
+ } else {
+ viewOptions.controlPanelFieldsView = Object.assign(testUtilsMock.fieldsViewGet(mockServer, {
+ arch: params.archs && params.archs[params.model + ',false,search'] || '<search/>',
+ fields: viewInfo.fields,
+ model: params.model,
+ }), { favoriteFilters: params.favoriteFilters });
+
+ view = new params.View(viewInfo, viewOptions);
+ }
+
+ if (params.interceptsPropagate) {
+ for (const name in params.interceptsPropagate) {
+ testUtilsMock.intercept(widget, name, params.interceptsPropagate[name], true);
+ }
+ }
+
+ // Override the ActionMenus registry unless told otherwise.
+ let actionMenusRegistry = ActionMenus.registry;
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = new Registry();
+ }
+
+ const viewController = await view.getController(widget);
+ // override the view's 'destroy' so that it calls 'destroy' on the widget
+ // instead, as the widget is the parent of the view and the mockServer.
+ viewController.__destroy = viewController.destroy;
+ viewController.destroy = function () {
+ // remove the override to properly destroy the viewController and its children
+ // when it will be called the second time (by its parent)
+ delete viewController.destroy;
+ widget.destroy();
+ webClient.remove();
+ if (params.actionMenusRegistry !== true) {
+ ActionMenus.registry = actionMenusRegistry;
+ }
+ };
+
+ // render the viewController in a fragment as they must be able to render correctly
+ // without being in the DOM
+ const fragment = document.createDocumentFragment();
+ await viewController.appendTo(fragment);
+ dom.prepend(actionManager, fragment, {
+ callbacks: [{ widget: viewController }],
+ in_DOM: true,
+ });
+
+ if (!params.doNotDisableAHref) {
+ [...viewController.el.getElementsByTagName('A')].forEach(elem => {
+ elem.addEventListener('click', ev => {
+ ev.preventDefault();
+ });
+ });
+ }
+ return viewController;
+ }
+
+ /**
+ * Get the target (fixture or body) of the document and adds event listeners
+ * to intercept custom or DOM events.
+ *
+ * @param {boolean} [debug=false] if true, the widget will be appended in
+ * the DOM. Also, RPCs and uncaught OdooEvent will be logged
+ * @returns {HTMLElement}
+ */
+ function prepareTarget(debug = false) {
+ document.body.classList.toggle('debug', debug);
+ return debug ? document.body : document.getElementById('qunit-fixture');
+ }
+
+ return {
+ createActionManager,
+ createCalendarView,
+ createComponent,
+ createControlPanel,
+ createDebugManager,
+ createModel,
+ createParent,
+ createView,
+ prepareTarget,
+ };
+});