[REF] web: Update generic files

General cleaning of some helpers and generic files. The idea is that they are
less likely to be entirely re-written in the near future so this is a way to
keep the code clean, readable and compliant to the current standards. Changes include:
- Use of async/await when possible
- Use of const/let instead of var
- Removal of underscore.js
- Use of native JS instead of jQuery when possible

The logic of the affected code remains the same.
This commit is contained in:
Julien Mougenot
2020-03-13 09:37:29 +00:00
parent 11ba58a311
commit 384e516218
12 changed files with 541 additions and 623 deletions
@@ -26,7 +26,7 @@ var Widget = require('web.Widget');
* This is async due to mail_manager/mail_service that needs to be ready.
*
* @param {Object} params
* @return {Promise} resolved with the discuss widget
* @returns {Promise<Discuss>}
*/
async function createDiscuss(params) {
var Parent = Widget.extend({
@@ -20,9 +20,9 @@ odoo.define('web.ActionMixin', function (require) {
* @module web.ActionMixin
*/
var core = require('web.core');
const core = require('web.core');
var ActionMixin = {
const ActionMixin = {
template: 'Action',
/**
@@ -79,7 +79,7 @@ odoo.define('web.ActionMixin', function (require) {
renderElement: function () {
this._super.apply(this, arguments);
if (this.contentTemplate) {
var content = core.qweb.render(this.contentTemplate, { widget: this });
const content = core.qweb.render(this.contentTemplate, { widget: this });
this.$('.o_content').append(content);
}
},
@@ -223,7 +223,7 @@ odoo.define('web.ActionMixin', function (require) {
*/
_onGetControllerQueryParams: function (ev) {
ev.stopPropagation();
var state = this.getOwnedQueryParams();
const state = this.getOwnedQueryParams();
ev.data.callback(state || {});
},
};
@@ -14,18 +14,18 @@ odoo.define('web.custom_hooks', function () {
*/
function useFocusOnUpdate() {
const component = Component.current;
component.__willFocus = null;
let willFocus = null;
function _focusSelector() {
if (component.__willFocus) {
const target = component.el.querySelector(component.__willFocus);
if (willFocus) {
const target = component.el.querySelector(willFocus);
if (target) {
target.focus();
if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
target.selectionStart = target.selectionEnd = target.value.length;
}
}
component.__willFocus = null;
willFocus = null;
}
}
@@ -33,7 +33,7 @@ odoo.define('web.custom_hooks', function () {
onPatched(_focusSelector);
return function focusOnUpdate(selector = '[autofocus]') {
component.__willFocus = selector;
willFocus = selector;
};
}
+1
View File
@@ -1,4 +1,5 @@
odoo.define('web.web_client', function (require) {
"use strict";
const env = require('web.env');
const session = require("web.session");
@@ -11,7 +11,7 @@ odoo.define('web.qunit_asserts', function (require) {
* matches exactly one element.
*/
var Widget = require('web.Widget');
const Widget = require('web.Widget');
//-------------------------------------------------------------------------
// Private functions
@@ -56,8 +56,8 @@ odoo.define('web.qunit_asserts', function (require) {
*/
function _checkVisible(w, shouldBeVisible, msg) {
if (w instanceof jQuery && w.length !== 1) {
const assertion = shouldHaveClass ? 'hasClass' : 'doesNotHaveClass';
QUnit.assert.ok(false, `Assertion '${assertion} ${className}' targets ${w.length} elements instead of 1`);
const assertion = shouldBeVisible ? 'isVisible' : 'isNotVisible';
QUnit.assert.ok(false, `Assertion '${assertion}' targets ${w.length} elements instead of 1`);
}
const el = w instanceof Widget || w instanceof owl.Component ? w.el :
@@ -184,12 +184,12 @@ odoo.define('web.qunit_asserts', function (require) {
* @param {string} [msg]
*/
function hasAttrValue(w, attr, value, msg) {
var $el = w instanceof Widget ? w.$el :
const $el = w instanceof Widget ? w.$el :
w instanceof HTMLElement ? $(w) :
w; // jquery element
if ($el.length !== 1) {
var descr = `hasAttrValue (${attr}: ${value})`;
const descr = `hasAttrValue (${attr}: ${value})`;
QUnit.assert.ok(false,
`Assertion '${descr}' targets ${$el.length} elements instead of 1`
);
+126 -128
View File
@@ -1,4 +1,4 @@
odoo.define('web.test_utils', function (require) {
odoo.define('web.test_utils', async function (require) {
"use strict";
/**
@@ -10,26 +10,25 @@ odoo.define('web.test_utils', function (require) {
* instance of a view, appended in the dom, ready to be tested.
*/
var ajax = require('web.ajax');
var concurrency = require('web.concurrency');
var core = require('web.core');
var relationalFields = require('web.relational_fields');
var session = require('web.session');
var testUtilsCreate = require('web.test_utils_create');
var testUtilsDom = require('web.test_utils_dom');
var testUtilsFields = require('web.test_utils_fields');
var testUtilsFile = require('web.test_utils_file');
var testUtilsForm = require('web.test_utils_form');
var testUtilsGraph = require('web.test_utils_graph');
var testUtilsKanban = require('web.test_utils_kanban');
var testUtilsMock = require('web.test_utils_mock');
var testUtilsModal = require('web.test_utils_modal');
var testUtilsPivot = require('web.test_utils_pivot');
var tools = require('web.tools');
const ajax = require('web.ajax');
const core = require('web.core');
const relationalFields = require('web.relational_fields');
const session = require('web.session');
const testUtilsCreate = require('web.test_utils_create');
const testUtilsDom = require('web.test_utils_dom');
const testUtilsFields = require('web.test_utils_fields');
const testUtilsFile = require('web.test_utils_file');
const testUtilsForm = require('web.test_utils_form');
const testUtilsGraph = require('web.test_utils_graph');
const testUtilsKanban = require('web.test_utils_kanban');
const testUtilsMock = require('web.test_utils_mock');
const testUtilsModal = require('web.test_utils_modal');
const testUtilsPivot = require('web.test_utils_pivot');
const tools = require('web.tools');
function deprecated(fn, type) {
var msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
const msg = `Helper 'testUtils.${fn.name}' is deprecated. ` +
`Please use 'testUtils.${type}.${fn.name}' instead.`;
return tools.deprecated(fn, msg);
}
@@ -41,9 +40,9 @@ odoo.define('web.test_utils', function (require) {
* @returns {Promise + resolve and reject function}
*/
function makeTestPromise() {
var resolve;
var reject;
var promise = new Promise(function (_resolve, _reject) {
let resolve;
let reject;
const promise = new Promise(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
@@ -69,7 +68,7 @@ odoo.define('web.test_utils', function (require) {
* @returns {Promise + resolve and reject function}
*/
function makeTestPromiseWithAssert(assert, str) {
var prom = makeTestPromise();
const prom = makeTestPromise();
prom.then(() => assert.step('ok ' + str)).catch(function () { });
prom.catch(() => assert.step('ko ' + str));
return prom;
@@ -102,118 +101,117 @@ odoo.define('web.test_utils', function (require) {
// before starting the qunit test suite.
// (session.js is in charge of loading the static xml bundle and we also have
// to load xml files that are normally lazy loaded by specific widgets).
return Promise.all([
await Promise.all([
session.is_bound,
ajax.loadXML('/web/static/src/xml/debug.xml', core.qweb),
ajax.loadXML('/web/static/src/xml/dialog.xml', core.qweb),
ajax.loadXML('/web/static/src/xml/translation_dialog.xml', core.qweb),
]).then(function () {
setTimeout(function () {
// jquery autocomplete refines the search in a setTimeout() parameterized
// with a delay, so we force this delay to 0 s.t. the dropdown is filtered
// directly on the next tick
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
]);
setTimeout(function () {
// jquery autocomplete refines the search in a setTimeout() parameterized
// with a delay, so we force this delay to 0 s.t. the dropdown is filtered
// directly on the next tick
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
// this is done with the hope that tests are
// only started all together...
QUnit.start();
}, 0);
return {
mock: {
addMockEnvironment: testUtilsMock.addMockEnvironment,
getMockedOwlEnv: testUtilsMock.getMockedOwlEnv,
intercept: testUtilsMock.intercept,
patch: testUtilsMock.patch,
patchDate: testUtilsMock.patchDate,
unpatch: testUtilsMock.unpatch,
fieldsViewGet: testUtilsMock.fieldsViewGet,
patchSetTimeout: testUtilsMock.patchSetTimeout,
},
dom: {
triggerKeypressEvent: testUtilsDom.triggerKeypressEvent,
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
dragAndDrop: testUtilsDom.dragAndDrop,
openDatepicker: testUtilsDom.openDatepicker,
click: testUtilsDom.click,
clickFirst: testUtilsDom.clickFirst,
clickLast: testUtilsDom.clickLast,
triggerEvents: testUtilsDom.triggerEvents,
triggerEvent: testUtilsDom.triggerEvent,
},
form: {
clickEdit: testUtilsForm.clickEdit,
clickSave: testUtilsForm.clickSave,
clickCreate: testUtilsForm.clickCreate,
clickDiscard: testUtilsForm.clickDiscard,
reload: testUtilsForm.reload,
},
graph: {
reload: testUtilsGraph.reload,
},
kanban: {
reload: testUtilsKanban.reload,
clickCreate: testUtilsKanban.clickCreate,
quickCreate: testUtilsKanban.quickCreate,
toggleGroupSettings: testUtilsKanban.toggleGroupSettings,
toggleRecordDropdown: testUtilsKanban.toggleRecordDropdown,
},
modal: {
clickButton: testUtilsModal.clickButton,
},
pivot: {
clickMeasure: testUtilsPivot.clickMeasure,
toggleMeasuresDropdown: testUtilsPivot.toggleMeasuresDropdown,
reload: testUtilsPivot.reload,
},
fields: {
many2one: {
clickOpenDropdown: testUtilsFields.clickOpenM2ODropdown,
clickHighlightedItem: testUtilsFields.clickM2OHighlightedItem,
clickItem: testUtilsFields.clickM2OItem,
searchAndClickItem: testUtilsFields.searchAndClickM2OItem,
},
editInput: testUtilsFields.editInput,
editSelect: testUtilsFields.editSelect,
editAndTrigger: testUtilsFields.editAndTrigger,
triggerKey: testUtilsFields.triggerKey,
triggerKeydown: testUtilsFields.triggerKeydown,
triggerKeyup: testUtilsFields.triggerKeyup,
},
file: {
createFile: testUtilsFile.createFile,
dragoverFile: testUtilsFile.dragoverFile,
dropFile: testUtilsFile.dropFile,
dropFiles: testUtilsFile.dropFiles,
// this is done with the hope that tests are
// only started all together...
QUnit.start();
}, 0);
return {
mock: {
addMockEnvironment: testUtilsMock.addMockEnvironment,
getMockedOwlEnv: testUtilsMock.getMockedOwlEnv,
intercept: testUtilsMock.intercept,
patch: testUtilsMock.patch,
patchDate: testUtilsMock.patchDate,
unpatch: testUtilsMock.unpatch,
fieldsViewGet: testUtilsMock.fieldsViewGet,
patchSetTimeout: testUtilsMock.patchSetTimeout,
},
dom: {
triggerKeypressEvent: testUtilsDom.triggerKeypressEvent,
triggerMouseEvent: testUtilsDom.triggerMouseEvent,
triggerPositionalMouseEvent: testUtilsDom.triggerPositionalMouseEvent,
dragAndDrop: testUtilsDom.dragAndDrop,
openDatepicker: testUtilsDom.openDatepicker,
click: testUtilsDom.click,
clickFirst: testUtilsDom.clickFirst,
clickLast: testUtilsDom.clickLast,
triggerEvents: testUtilsDom.triggerEvents,
triggerEvent: testUtilsDom.triggerEvent,
},
form: {
clickEdit: testUtilsForm.clickEdit,
clickSave: testUtilsForm.clickSave,
clickCreate: testUtilsForm.clickCreate,
clickDiscard: testUtilsForm.clickDiscard,
reload: testUtilsForm.reload,
},
graph: {
reload: testUtilsGraph.reload,
},
kanban: {
reload: testUtilsKanban.reload,
clickCreate: testUtilsKanban.clickCreate,
quickCreate: testUtilsKanban.quickCreate,
toggleGroupSettings: testUtilsKanban.toggleGroupSettings,
toggleRecordDropdown: testUtilsKanban.toggleRecordDropdown,
},
modal: {
clickButton: testUtilsModal.clickButton,
},
pivot: {
clickMeasure: testUtilsPivot.clickMeasure,
toggleMeasuresDropdown: testUtilsPivot.toggleMeasuresDropdown,
reload: testUtilsPivot.reload,
},
fields: {
many2one: {
clickOpenDropdown: testUtilsFields.clickOpenM2ODropdown,
clickHighlightedItem: testUtilsFields.clickM2OHighlightedItem,
clickItem: testUtilsFields.clickM2OItem,
searchAndClickItem: testUtilsFields.searchAndClickM2OItem,
},
editInput: testUtilsFields.editInput,
editSelect: testUtilsFields.editSelect,
editAndTrigger: testUtilsFields.editAndTrigger,
triggerKey: testUtilsFields.triggerKey,
triggerKeydown: testUtilsFields.triggerKeydown,
triggerKeyup: testUtilsFields.triggerKeyup,
},
file: {
createFile: testUtilsFile.createFile,
dragoverFile: testUtilsFile.dragoverFile,
dropFile: testUtilsFile.dropFile,
dropFiles: testUtilsFile.dropFiles,
},
createActionManager: testUtilsCreate.createActionManager,
createDebugManager: testUtilsCreate.createDebugManager,
createAsyncView: testUtilsCreate.createView,
createCalendarView: testUtilsCreate.createCalendarView,
createControlPanel: testUtilsCreate.createControlPanel,
createView: testUtilsCreate.createView,
createModel: testUtilsCreate.createModel,
createParent: testUtilsCreate.createParent,
makeTestPromise: makeTestPromise,
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
nextMicrotaskTick: nextMicrotaskTick,
nextTick: nextTick,
prepareTarget: testUtilsCreate.prepareTarget,
returnAfterNextAnimationFrame: testUtilsDom.returnAfterNextAnimationFrame,
createActionManager: testUtilsCreate.createActionManager,
createDebugManager: testUtilsCreate.createDebugManager,
createAsyncView: testUtilsCreate.createView,
createCalendarView: testUtilsCreate.createCalendarView,
createControlPanel: testUtilsCreate.createControlPanel,
createView: testUtilsCreate.createView,
createModel: testUtilsCreate.createModel,
createParent: testUtilsCreate.createParent,
makeTestPromise: makeTestPromise,
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
nextMicrotaskTick: nextMicrotaskTick,
nextTick: nextTick,
prepareTarget: testUtilsCreate.prepareTarget,
returnAfterNextAnimationFrame: testUtilsDom.returnAfterNextAnimationFrame,
// backward-compatibility
addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'),
dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'),
fieldsViewGet: deprecated(testUtilsMock.fieldsViewGet, 'mock'),
intercept: deprecated(testUtilsMock.intercept, 'mock'),
openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'),
patch: deprecated(testUtilsMock.patch, 'mock'),
patchDate: deprecated(testUtilsMock.patchDate, 'mock'),
triggerKeypressEvent: deprecated(testUtilsDom.triggerKeypressEvent, 'dom'),
triggerMouseEvent: deprecated(testUtilsDom.triggerMouseEvent, 'dom'),
triggerPositionalMouseEvent: deprecated(testUtilsDom.triggerPositionalMouseEvent, 'dom'),
unpatch: deprecated(testUtilsMock.unpatch, 'mock'),
};
});
// backward-compatibility
addMockEnvironment: deprecated(testUtilsMock.addMockEnvironment, 'mock'),
dragAndDrop: deprecated(testUtilsDom.dragAndDrop, 'dom'),
fieldsViewGet: deprecated(testUtilsMock.fieldsViewGet, 'mock'),
intercept: deprecated(testUtilsMock.intercept, 'mock'),
openDatepicker: deprecated(testUtilsDom.openDatepicker, 'dom'),
patch: deprecated(testUtilsMock.patch, 'mock'),
patchDate: deprecated(testUtilsMock.patchDate, 'mock'),
triggerKeypressEvent: deprecated(testUtilsDom.triggerKeypressEvent, 'dom'),
triggerMouseEvent: deprecated(testUtilsDom.triggerMouseEvent, 'dom'),
triggerPositionalMouseEvent: deprecated(testUtilsDom.triggerPositionalMouseEvent, 'dom'),
unpatch: deprecated(testUtilsMock.unpatch, 'mock'),
};
});
@@ -10,70 +10,68 @@ odoo.define('web.test_utils_create', function (require) {
* testUtils file.
*/
var ActionManager = require('web.ActionManager');
var config = require('web.config');
var ControlPanelView = require('web.ControlPanelView');
var concurrency = require('web.concurrency');
var DebugManager = require('web.DebugManager.Backend');
var dom = require('web.dom');
var testUtilsMock = require('web.test_utils_mock');
var Widget = require('web.Widget');
const ActionManager = require('web.ActionManager');
const concurrency = require('web.concurrency');
const config = require('web.config');
const ControlPanelView = require('web.ControlPanelView');
const DebugManager = require('web.DebugManager.Backend');
const dom = require('web.dom');
const testUtilsMock = require('web.test_utils_mock');
const Widget = require('web.Widget');
/**
* create and return an instance of ActionManager with all rpcs going through a
* 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={}]
* @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 {ActionManager}
* @returns {Promise<ActionManager>}
*/
async function createActionManager(params) {
params = params || {};
async function createActionManager(params = {}) {
const target = prepareTarget(params.debug);
var widget = new Widget();
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
var mockRPC = params.mockRPC;
_.extend(params, {
mockRPC: function (route, args) {
const mockRPC = params.mockRPC;
Object.assign(params, {
async mockRPC(route, args) {
if (args.model === 'ir.attachment') {
return Promise.resolve([]);
return [];
}
if (mockRPC) {
return mockRPC.apply(this, arguments);
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
});
testUtilsMock.addMockEnvironment(widget, _.defaults(params, { debounce: false }));
testUtilsMock.addMockEnvironment(widget, Object.assign({ debounce: false }, params));
await widget.prependTo(target);
widget.$el.addClass('o_web_client');
widget.el.classList.add('o_web_client');
if (config.device.isMobile) {
widget.$el.addClass('o_touch_device');
widget.el.classList.add('o_touch_device');
}
var userContext = params.context && params.context.user_context || {};
var actionManager = new ActionManager(widget, userContext);
const userContext = params.context && params.context.user_context || {};
const actionManager = new ActionManager(widget, userContext);
var originalDestroy = ActionManager.prototype.destroy;
const originalDestroy = ActionManager.prototype.destroy;
actionManager.destroy = function () {
actionManager.destroy = originalDestroy;
widget.destroy();
};
var fragment = document.createDocumentFragment();
return actionManager.appendTo(fragment).then(function () {
dom.append(widget.$el, fragment, {
callbacks: [{ widget: actionManager }],
in_DOM: true,
});
return actionManager;
const fragment = document.createDocumentFragment();
await actionManager.appendTo(fragment);
dom.append(widget.el, fragment, {
callbacks: [{ widget: actionManager }],
in_DOM: true,
});
return actionManager;
}
/**
@@ -85,21 +83,22 @@ odoo.define('web.test_utils_create', function (require) {
* 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} params @see createView
* @param {Object} [options]
* @param {boolean} [options.positionalClicks=false]
* @returns {Promise<CalendarController>}
*/
async function createCalendarView(params, options) {
var calendar = await createView(params);
const calendar = await createView(params);
if (!options || !options.positionalClicks) {
return calendar;
}
var $view = $('#qunit-fixture').contents();
$view.prependTo('body');
var destroy = calendar.destroy;
calendar.destroy = function () {
$view.remove();
const viewElements = [...document.getElementById('qunit-fixture').children];
viewElements.forEach(el => document.body.prepend(el));
const destroy = calendar.destroy;
calendar.destroy = () => {
viewElements.forEach(el => el.remove());
destroy();
};
await concurrency.delay(0);
@@ -107,7 +106,7 @@ odoo.define('web.test_utils_create', function (require) {
}
/**
* create a controlPanel from various parameters.
* Create a controlPanel from various parameters.
*
* It returns an instance of ControlPanelController
*
@@ -138,17 +137,17 @@ odoo.define('web.test_utils_create', function (require) {
*
* @returns {Promise<ControlPanel>} resolves with an instance of the ControlPanelController
*/
function createControlPanel(params) {
async function createControlPanel(params) {
params = params || {};
const target = prepareTarget(params.debug);
// reproduce the DOM environment of a view control panel
var $webClient = $('<div>').addClass('o_web_client').prependTo(target);
var $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
var $action = $('<div>').addClass('o_action').appendTo($actionManager);
const $webClient = $('<div>').addClass('o_web_client').prependTo(target);
const $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
const $action = $('<div>').addClass('o_action').appendTo($actionManager);
var widget = new Widget();
const widget = new Widget();
// add mock environment: mock server, session, fieldviewget, ...
var mockServer = testUtilsMock.addMockEnvironment(widget, params);
const mockServer = testUtilsMock.addMockEnvironment(widget, params);
if (!params.viewInfo) {
try {
params.viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
@@ -159,37 +158,35 @@ odoo.define('web.test_utils_create', function (require) {
}
}
var viewOptions = _.defaults({}, params.viewOptions, {
const viewOptions = Object.assign({
context: params.context,
modelName: params.model,
searchMenuTypes: params.searchMenuTypes,
viewInfo: params.viewInfo,
});
var controlPanelView = new ControlPanelView(viewOptions);
return controlPanelView.getController(widget).then(function (controlPanel) {
// override the controlPanel's 'destroy' so that it calls 'destroy' on
// the widget instead, as the widget is the parent of the controlPanel
// and the mockServer.
controlPanel.__destroy = controlPanel.destroy;
controlPanel.destroy = function () {
// remove the override to properly destroy the controlPanel and its
// children when it will be called the second time (by its parent)
delete controlPanel.destroy;
widget.destroy();
$webClient.remove();
};
}, params.viewOptions);
const controlPanelView = new ControlPanelView(viewOptions);
const controlPanel = await controlPanelView.getController(widget);
// override the controlPanel's 'destroy' so that it calls 'destroy' on
// the widget instead, as the widget is the parent of the controlPanel
// and the mockServer.
controlPanel.__destroy = controlPanel.destroy;
controlPanel.destroy = function () {
// remove the override to properly destroy the controlPanel and its
// children when it will be called the second time (by its parent)
delete controlPanel.destroy;
widget.destroy();
$webClient.remove();
};
// render the controlPanel in a fragment as it must be able to render
// correctly without being in the DOM
var fragment = document.createDocumentFragment();
return controlPanel.appendTo(fragment).then(function () {
dom.prepend($action, fragment, {
callbacks: [{ widget: controlPanel }],
in_DOM: true,
});
return controlPanel;
});
// render the controlPanel in a fragment as it must be able to render
// correctly without being in the DOM
const fragment = document.createDocumentFragment();
await controlPanel.appendTo(fragment);
dom.prepend($action, fragment, {
callbacks: [{ widget: controlPanel }],
in_DOM: true,
});
return controlPanel;
}
/**
@@ -197,39 +194,39 @@ odoo.define('web.test_utils_create', function (require) {
* mock method, assuming that the user has access rights, and is an admin.
*
* @param {Object} [params={}]
* @returns {DebugManager}
*/
var createDebugManager = function (params) {
params = params || {};
var mockRPC = params.mockRPC;
_.extend(params, {
mockRPC: function (route, args) {
function createDebugManager(params = {}) {
const mockRPC = params.mockRPC;
Object.assign(params, {
async mockRPC(route, args) {
if (args.method === 'check_access_rights') {
return Promise.resolve(true);
return true;
}
if (args.method === 'xmlid_to_res_id') {
return Promise.resolve(true);
return true;
}
if (mockRPC) {
return mockRPC.apply(this, arguments);
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
session: {
user_has_group: function (group) {
async user_has_group(group) {
if (group === 'base.group_no_one') {
return Promise.resolve(true);
return true;
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
},
});
var debugManager = new DebugManager();
const debugManager = new DebugManager();
testUtilsMock.addMockEnvironment(debugManager, params);
return debugManager;
};
}
/**
* create a model from given parameters.
* Create a model from given parameters.
*
* @param {Object} params This object will be given to addMockEnvironment, so
* any parameters from that method applies
@@ -237,9 +234,9 @@ odoo.define('web.test_utils_create', function (require) {
* @returns {Model}
*/
function createModel(params) {
var widget = new Widget();
const widget = new Widget();
var model = new params.Model(widget);
const model = new params.Model(widget);
testUtilsMock.addMockEnvironment(widget, params);
@@ -256,20 +253,20 @@ odoo.define('web.test_utils_create', function (require) {
}
/**
* create a widget parent from given parameters.
* 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 {Widget}
*/
function createParent(params) {
var widget = new Widget();
const widget = new Widget();
testUtilsMock.addMockEnvironment(widget, params);
return widget;
}
/**
* create a view from various parameters. Here, a view means a javascript
* 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.
*
@@ -291,37 +288,42 @@ odoo.define('web.test_utils_create', function (require) {
* 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.
* @param {Boolean} [params.doNotDisableAHref=false] will not preventDefault on the A elements of the view if true.
* Default is false.
* @returns {Promise<AbstractController>} resolves with the instance of the view
* @returns {Promise<AbstractController>} the instance of the view
*/
async function createView(params) {
const target = prepareTarget(params.debug);
var widget = new Widget();
const widget = new Widget();
// reproduce the DOM environment of views
var $webClient = $('<div>').addClass('o_web_client').prependTo(target);
var $actionManager = $('<div>').addClass('o_action_manager').appendTo($webClient);
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, ...
var mockServer = testUtilsMock.addMockEnvironment(widget, params);
var viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
const mockServer = testUtilsMock.addMockEnvironment(widget, params);
const viewInfo = testUtilsMock.fieldsViewGet(mockServer, params);
// create the view
var View = params.View;
var viewOptions = _.defaults({}, params.viewOptions, {
const View = params.View;
const viewOptions = Object.assign({
modelName: params.model || 'foo',
ids: 'res_id' in params ? [params.res_id] : undefined,
currentId: 'res_id' in params ? params.res_id : undefined,
domain: params.domain || [],
context: params.context || {},
hasSidebar: 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: function () {
this._super.apply(this, arguments);
_updateMVCParams() {
this._super(...arguments);
this.loadParams.groupedBy = params.groupBy || viewOptions.groupBy || [];
testUtilsMock.unpatch(View);
},
@@ -330,7 +332,7 @@ odoo.define('web.test_utils_create', function (require) {
viewOptions.hasSelectors = params.hasSelectors;
}
var view;
let view;
if (viewInfo.type === 'controlpanel' || viewInfo.type === 'search') {
// TODO: probably needs to create an helper just for that
view = new params.View({
@@ -347,42 +349,40 @@ odoo.define('web.test_utils_create', function (require) {
}
if (params.interceptsPropagate) {
_.each(params.interceptsPropagate, function (cb, name) {
testUtilsMock.intercept(widget, name, cb, true);
});
for (const name in params.interceptsPropagate) {
testUtilsMock.intercept(widget, name, params.interceptsPropagate[name], true);
}
}
return view.getController(widget).then(function (view) {
// 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.
view.__destroy = view.destroy;
view.destroy = function () {
// remove the override to properly destroy the view and its children
// when it will be called the second time (by its parent)
delete view.destroy;
widget.destroy();
$webClient.remove();
};
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();
};
// render the view in a fragment as they must be able to render correctly
// without being in the DOM
var fragment = document.createDocumentFragment();
return view.appendTo(fragment).then(() => {
dom.prepend($actionManager, fragment, {
callbacks: [{ widget: view }],
in_DOM: true,
});
if (!params.doNotDisableAHref) {
[...view.el.getElementsByTagName('A')].forEach((elem) => {
elem.addEventListener('click', (ev) => {
ev.preventDefault();
});
});
}
return view;
});
// 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;
}
/**
@@ -395,17 +395,17 @@ odoo.define('web.test_utils_create', function (require) {
*/
function prepareTarget(debug = false) {
document.body.classList.toggle('debug', debug);
return debug ? document.body : document.querySelector("#qunit-fixture");
return debug ? document.body : document.getElementById('qunit-fixture');
}
return {
createActionManager: createActionManager,
createCalendarView: createCalendarView,
createControlPanel: createControlPanel,
createDebugManager: createDebugManager,
createModel: createModel,
createParent: createParent,
createView: createView,
prepareTarget: prepareTarget,
createActionManager,
createCalendarView,
createControlPanel,
createDebugManager,
createModel,
createParent,
createView,
prepareTarget,
};
});
+89 -105
View File
@@ -1,7 +1,7 @@
odoo.define('web.test_utils_dom', function (require) {
"use strict";
var concurrency = require('web.concurrency');
const concurrency = require('web.concurrency');
/**
* DOM Test Utils
@@ -13,40 +13,29 @@ odoo.define('web.test_utils_dom', function (require) {
*/
//-------------------------------------------------------------------------
// Private
// Private functions
//-------------------------------------------------------------------------
function keyboardEventBubble(args) {
return Object.assign({}, args, { bubbles: true, keyCode: args.which });
}
function mouseEventMapping(args) {
return Object.assign({}, args, {
view: window,
bubbles: true,
cancelable: true,
clientX: args ? args.pageX : undefined,
clientY: args ? args.pageY : undefined,
});
}
function mouseEventNoBubble(args) {
return Object.assign({}, args, {
view: window,
bubbles: false,
cancelable: false,
clientX: args ? args.pageX : undefined,
clientY: args ? args.pageY : undefined,
});
}
function onlyBubble(args) {
return Object.assign({}, args, {
bubbles: true
});
}
const eventTypes = {
// TriggerEvent helpers
const keyboardEventBubble = args => Object.assign({}, args, { bubbles: true, keyCode: args.which });
const mouseEventMapping = args => Object.assign({}, args, {
bubbles: true,
cancelable: true,
clientX: args ? args.pageX : undefined,
clientY: args ? args.pageY : undefined,
view: window,
});
const mouseEventNoBubble = args => Object.assign({}, args, {
bubbles: false,
cancelable: false,
clientX: args ? args.pageX : undefined,
clientY: args ? args.pageY : undefined,
view: window,
});
const noBubble = args => Object.assign({}, args, { bubbles: false });
const onlyBubble = args => Object.assign({}, args, { bubbles: true });
// TriggerEvent constructor/args processor mapping
const EVENT_TYPES = {
auxclick: { constructor: MouseEvent, processParameters: mouseEventMapping },
click: { constructor: MouseEvent, processParameters: mouseEventMapping },
contextmenu: { constructor: MouseEvent, processParameters: mouseEventMapping },
@@ -60,17 +49,9 @@ odoo.define('web.test_utils_dom', function (require) {
mouseover: { constructor: MouseEvent, processParameters: mouseEventMapping },
mouseout: { constructor: MouseEvent, processParameters: mouseEventMapping },
focus: {
constructor: FocusEvent, processParameters: (args) => {
return Object.assign({}, args, { bubbles: false });
}
},
focus: { constructor: FocusEvent, processParameters: noBubble },
focusin: { constructor: FocusEvent, processParameters: onlyBubble },
blur: {
constructor: FocusEvent, processParameters: (args) => {
return Object.assign({}, args, { bubbles: false });
}
},
blur: { constructor: FocusEvent, processParameters: noBubble },
cut: { constructor: ClipboardEvent, processParameters: onlyBubble },
copy: { constructor: ClipboardEvent, processParameters: onlyBubble },
@@ -95,7 +76,10 @@ odoo.define('web.test_utils_dom', function (require) {
* @param {Object} node
* @returns {boolean}
*/
function isEventTarget(node) {
function _isEventTarget(node) {
if (!node) {
throw new Error(`Provided node is ${node}.`);
}
if (node instanceof window.top.EventTarget) {
return true;
}
@@ -105,7 +89,7 @@ odoo.define('web.test_utils_dom', function (require) {
}
//-------------------------------------------------------------------------
// Public
// Public functions
//-------------------------------------------------------------------------
/**
@@ -126,7 +110,7 @@ odoo.define('web.test_utils_dom', function (require) {
if (typeof el === 'string') {
el = $(el);
}
if (isEventTarget(el)) {
if (_isEventTarget(el)) {
// EventTarget
matches = [el];
} else {
@@ -174,8 +158,8 @@ odoo.define('web.test_utils_dom', function (require) {
* element event if it is invisible
* @returns {Promise}
*/
function clickFirst(el, options) {
return click(el, _.extend({}, options, { first: true }));
async function clickFirst(el, options) {
return click(el, Object.assign({}, options, { first: true }));
}
/**
@@ -189,12 +173,12 @@ odoo.define('web.test_utils_dom', function (require) {
* element event if it is invisible
* @returns {Promise}
*/
function clickLast(el, options) {
return click(el, _.extend({}, options, { last: true }));
async function clickLast(el, options) {
return click(el, Object.assign({}, options, { last: true }));
}
/**
* simulate a drag and drop operation between 2 jquery nodes: $el and $to.
* Simulate a drag and drop operation between 2 jquery nodes: $el and $to.
* This is a crude simulation, with only the mousedown, mousemove and mouseup
* events, but it is enough to help test drag and drop operations with jqueryUI
* sortable.
@@ -225,25 +209,25 @@ odoo.define('web.test_utils_dom', function (require) {
*/
async function dragAndDrop($el, $to, options) {
let el = null;
if (isEventTarget($el)) {
if (_isEventTarget($el)) {
el = $el;
$el = $(el);
}
if (isEventTarget($to)) {
if (_isEventTarget($to)) {
$to = $($to);
}
options = options || {};
var position = options.position || 'center';
var elementCenter = $el.offset();
var toOffset = $to.offset();
const position = options.position || 'center';
const elementCenter = $el.offset();
const toOffset = $to.offset();
if (_.isObject(position)) {
if (typeof position === 'object') {
toOffset.top += position.top + 1;
toOffset.left += position.left + 1;
} else {
toOffset.top += $to.outerHeight() / 2;
toOffset.left += $to.outerWidth() / 2;
var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
const vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
if (position === 'top') {
toOffset.top -= $to.outerHeight() / 2 + vertical_offset;
} else if (position === 'bottom') {
@@ -257,7 +241,7 @@ odoo.define('web.test_utils_dom', function (require) {
if ($to[0].ownerDocument !== document) {
// we are in an iframe
var bound = $('iframe')[0].getBoundingClientRect();
const bound = $('iframe')[0].getBoundingClientRect();
toOffset.left += bound.left;
toOffset.top += bound.top;
}
@@ -301,11 +285,11 @@ odoo.define('web.test_utils_dom', function (require) {
}
/**
* Opens the datepicker of a given element.
* Open the datepicker of a given element.
*
* @param {jQuery} $datepickerEl element to which a datepicker is attached
*/
function openDatepicker($datepickerEl) {
async function openDatepicker($datepickerEl) {
return click($datepickerEl.find('.o_datepicker_input'));
}
@@ -325,7 +309,7 @@ odoo.define('web.test_utils_dom', function (require) {
}
/**
* Triggers an event on the specified target.
* Trigger an event on the specified target.
* This function will dispatch a native event to an EventTarget or a
* jQuery event to a jQuery object. This behaviour can be overridden by the
* jquery option.
@@ -340,7 +324,7 @@ odoo.define('web.test_utils_dom', function (require) {
async function triggerEvent(el, eventType, eventAttrs = {}, fast = false) {
let matches;
let selectorMsg = "";
if (isEventTarget(el)) {
if (_isEventTarget(el)) {
matches = [el];
} else {
matches = [...el];
@@ -353,15 +337,15 @@ odoo.define('web.test_utils_dom', function (require) {
const target = matches[0];
let event;
if (!eventTypes[eventType] && !eventTypes[eventType.type]) {
if (!EVENT_TYPES[eventType] && !EVENT_TYPES[eventType.type]) {
event = new Event(eventType, Object.assign({}, eventAttrs, { bubbles: true }));
} else {
if (typeof eventType === "object") {
const { constructor, processParameters } = eventTypes[eventType.type];
const { constructor, processParameters } = EVENT_TYPES[eventType.type];
const eventParameters = processParameters(eventType);
event = new constructor(eventType.type, eventParameters);
} else {
const { constructor, processParameters } = eventTypes[eventType];
const { constructor, processParameters } = EVENT_TYPES[eventType];
event = new constructor(eventType, processParameters(eventAttrs));
}
}
@@ -370,7 +354,7 @@ odoo.define('web.test_utils_dom', function (require) {
}
/**
* Triggers multiple events on the specified element.
* Trigger multiple events on the specified element.
*
* @param {EventTarget|EventTarget[]} el
* @param {string[]} events the events you want to trigger
@@ -387,10 +371,32 @@ odoo.define('web.test_utils_dom', function (require) {
}
for (let e = 0; e < events.length; e++) {
await triggerEvent(el, events[e], {});
await triggerEvent(el, events[e]);
}
}
/**
* Simulate a keypress event for a given character
*
* @param {string} char the character, or 'ENTER'
* @returns {Promise}
*/
async function triggerKeypressEvent(char) {
let keycode;
if (char === 'Enter') {
keycode = $.ui.keyCode.ENTER;
} else if (char === "Tab") {
keycode = $.ui.keyCode.TAB;
} else {
keycode = char.charCodeAt(0);
}
return triggerEvent(document.body, 'keypress', {
key: char,
keyCode: keycode,
which: keycode,
});
}
/**
* simulate a mouse event with a custom event who add the item position. This is
* sometimes necessary because the basic way to trigger an event (such as
@@ -400,7 +406,7 @@ odoo.define('web.test_utils_dom', function (require) {
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
* @returns {Promise}
*/
function triggerMouseEvent($el, type) {
async function triggerMouseEvent($el, type) {
const el = $el instanceof jQuery ? $el[0] : $el;
if (!el) {
throw new Error(`no target found to trigger MouseEvent`);
@@ -426,9 +432,9 @@ odoo.define('web.test_utils_dom', function (require) {
* @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
* @returns {HTMLElement}
*/
function triggerPositionalMouseEvent(x, y, type) {
var ev = document.createEvent("MouseEvent");
var el = document.elementFromPoint(x, y);
async function triggerPositionalMouseEvent(x, y, type) {
const ev = document.createEvent("MouseEvent");
const el = document.elementFromPoint(x, y);
ev.initMouseEvent(
type,
true /* bubble */,
@@ -442,39 +448,17 @@ odoo.define('web.test_utils_dom', function (require) {
return el;
}
/**
* simulate a keypress event for a given character
*
* @param {string} char the character, or 'ENTER'
* @returns {Promise}
*/
function triggerKeypressEvent(char) {
var keycode;
if (char === 'Enter') {
keycode = $.ui.keyCode.ENTER;
} else if (char === "Tab") {
keycode = $.ui.keyCode.TAB;
} else {
keycode = char.charCodeAt(0);
}
return triggerEvent(document.body, 'keypress', {
key: char,
keyCode: keycode,
which: keycode,
});
}
return {
click: click,
clickFirst: clickFirst,
clickLast: clickLast,
dragAndDrop: dragAndDrop,
openDatepicker: openDatepicker,
returnAfterNextAnimationFrame: returnAfterNextAnimationFrame,
triggerEvent: triggerEvent,
triggerEvents: triggerEvents,
triggerKeypressEvent: triggerKeypressEvent,
triggerMouseEvent: triggerMouseEvent,
triggerPositionalMouseEvent: triggerPositionalMouseEvent,
click,
clickFirst,
clickLast,
dragAndDrop,
openDatepicker,
returnAfterNextAnimationFrame,
triggerEvent,
triggerEvents,
triggerKeypressEvent,
triggerMouseEvent,
triggerPositionalMouseEvent,
};
});
@@ -10,8 +10,14 @@ odoo.define('web.test_utils_fields', function (require) {
* testUtils file.
*/
var concurrency = require('web.concurrency');
var domUtils = require('web.test_utils_dom');
const testUtilsDom = require('web.test_utils_dom');
const ARROW_KEYS_MAPPING = {
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
up: 'ArrowUp',
};
//-------------------------------------------------------------------------
// Public functions
@@ -23,13 +29,13 @@ odoo.define('web.test_utils_fields', function (require) {
* @param {string} fieldName
* @param {[string]} selector if set, this will restrict the search for the m2o
* input
* @returns {Promise}
*/
function clickM2OHighlightedItem(fieldName, selector) {
var m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
var $dropdown = $(m2oSelector).autocomplete('widget');
async function clickM2OHighlightedItem(fieldName, selector) {
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
const $dropdown = $(m2oSelector).autocomplete('widget');
// clicking on an li (no matter which one), will select the focussed one
$dropdown.find('li:first()').click();
return concurrency.delay(0);
return testUtilsDom.click($dropdown[0].querySelector('li'));
}
/**
@@ -44,17 +50,15 @@ odoo.define('web.test_utils_fields', function (require) {
* @param {string} searchText
* @returns {Promise}
*/
function clickM2OItem(fieldName, searchText) {
var m2oSelector = `.o_field_many2one[name=${fieldName}] input`;
var $dropdown = $(m2oSelector).autocomplete('widget');
var $target = $dropdown.find(`li:contains(${searchText})`).first();
async function clickM2OItem(fieldName, searchText) {
const m2oSelector = `.o_field_many2one[name=${fieldName}] input`;
const $dropdown = $(m2oSelector).autocomplete('widget');
const $target = $dropdown.find(`li:contains(${searchText})`).first();
if ($target.length !== 1 || !$target.is(':visible')) {
throw new Error('Menu item should be unique and visible');
throw new Error('Menu item should be visible');
}
$target.mouseenter();
$target.click();
return concurrency.delay(0);
$target.mouseenter(); // This is NOT a mouseenter event. See jquery.js:5516 for more headaches.
return testUtilsDom.click($target);
}
/**
@@ -63,16 +67,16 @@ odoo.define('web.test_utils_fields', function (require) {
* @param {string} fieldName
* @param {[string]} selector if set, this will restrict the search for the m2o
* input
* @returns {HTMLInputElement} the main many2one input
* @returns {Promise<HTMLInputElement>} the main many2one input
*/
async function clickOpenM2ODropdown(fieldName, selector) {
var m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
var matches = document.querySelectorAll(m2oSelector);
const m2oSelector = `${selector || ''} .o_field_many2one[name=${fieldName}] input`;
const matches = document.querySelectorAll(m2oSelector);
if (matches.length !== 1) {
throw new Error(`cannot open m2o: selector ${selector} has been found ${matches.length} instead of 1`);
}
await domUtils.click(matches[0]);
await testUtilsDom.click(matches[0]);
return matches[0];
}
@@ -88,7 +92,7 @@ odoo.define('web.test_utils_fields', function (require) {
* @param {string[]} events
* @returns {Promise}
*/
function editAndTrigger(el, value, events) {
async function editAndTrigger(el, value, events) {
if (el instanceof jQuery) {
if (el.length !== 1) {
throw new Error(`target ${el.selector} has length ${el.length} instead of 1`);
@@ -97,7 +101,7 @@ odoo.define('web.test_utils_fields', function (require) {
} else {
el.value = value;
}
return domUtils.triggerEvents(el, events);
return testUtilsDom.triggerEvents(el, events);
}
/**
@@ -112,7 +116,7 @@ odoo.define('web.test_utils_fields', function (require) {
* @param {string|number} value
* @returns {Promise}
*/
function editInput(el, value) {
async function editInput(el, value) {
return editAndTrigger(el, value, ['input']);
}
@@ -143,30 +147,22 @@ odoo.define('web.test_utils_fields', function (require) {
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', {search: 'George'});
*
* @param {string} fieldName
* @param {[Object]} options
* @param {[string]} options.selector
* @param {[string]} options.search
* @param {[string]} options.item
* @param {[Object]} [options = {}]
* @param {[string]} [options.selector]
* @param {[string]} [options.search]
* @param {[string]} [options.item]
* @returns {Promise}
*/
function searchAndClickM2OItem(fieldName, options) {
options = options || {};
return clickOpenM2ODropdown(fieldName, options.selector).then(function (input) {
var def;
if (options.search) {
input.value = options.search;
input.dispatchEvent(new Event('input'));
def = concurrency.delay(0);
}
return Promise.resolve(def).then(function () {
if (options.item) {
return clickM2OItem(fieldName, options.item);
} else {
return clickM2OHighlightedItem(fieldName, options.selector);
}
});
});
async function searchAndClickM2OItem(fieldName, options = {}) {
const input = await clickOpenM2ODropdown(fieldName, options.selector);
if (options.search) {
await editInput(input, options.search);
}
if (options.item) {
return clickM2OItem(fieldName, options.item);
} else {
return clickM2OHighlightedItem(fieldName, options.selector);
}
}
/**
@@ -181,7 +177,15 @@ odoo.define('web.test_utils_fields', function (require) {
*/
function triggerKey(type, $el, keyCode) {
type = 'key' + type;
const params = {};
if (typeof keyCode === 'string') {
// Key (new API)
if (keyCode in ARROW_KEYS_MAPPING) {
params.key = ARROW_KEYS_MAPPING[keyCode];
} else {
params.key = keyCode[0].toUpperCase() + keyCode.slice(1).toLowerCase();
}
// KeyCode/which (jQuery)
if (keyCode.length > 1) {
keyCode = keyCode.toUpperCase();
keyCode = $.ui.keyCode[keyCode];
@@ -189,8 +193,9 @@ odoo.define('web.test_utils_fields', function (require) {
keyCode = keyCode.charCodeAt(0);
}
}
$el.trigger({ type: type, which: keyCode, keyCode: keyCode });
return concurrency.delay(0);
params.keyCode = keyCode;
params.which = keyCode;
return testUtilsDom.triggerEvent($el, type, params);
}
/**
@@ -216,15 +221,15 @@ odoo.define('web.test_utils_fields', function (require) {
}
return {
clickM2OHighlightedItem: clickM2OHighlightedItem,
clickM2OItem: clickM2OItem,
clickOpenM2ODropdown: clickOpenM2ODropdown,
editAndTrigger: editAndTrigger,
editInput: editInput,
editSelect: editSelect,
searchAndClickM2OItem: searchAndClickM2OItem,
triggerKey: triggerKey,
triggerKeydown: triggerKeydown,
triggerKeyup: triggerKeyup,
clickM2OHighlightedItem,
clickM2OItem,
clickOpenM2ODropdown,
editAndTrigger,
editInput,
editSelect,
searchAndClickM2OItem,
triggerKey,
triggerKeydown,
triggerKeyup,
};
});
@@ -465,7 +465,7 @@ function intercept(widget, eventName, fn, propagate) {
* @param {integer} hours the digits for hours (24h)
* @param {integer} minutes
* @param {integer} seconds
* @returns {function} a callback to unpatch window.Date.
* @returns {Function} a callback to unpatch window.Date.
*/
function patchDate(year, month, day, hours, minutes, seconds) {
var RealDate = window.Date;
@@ -1,36 +1,26 @@
odoo.define('web.test_utils_modal', function (require) {
"use strict";
"use strict";
/**
* Modal Test Utils
*
* This module defines various utility functions to help test pivot views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
/**
* Modal Test Utils
*
* This module defines various utility functions to help test pivot views.
*
* Note that all methods defined in this module are exported in the main
* testUtils file.
*/
var core = require('web.core');
var concurrency = require('web.concurrency');
const { _t } = require('web.core');
const testUtilsDom = require('web.test_utils_dom');
/**
* Click on a button in the footer of a modal (which contains a given string).
* Note that this method checks the unicity of the button.
*
* @param {string} text (in english: this method will perform the translation)
*/
function clickButton(text) {
var selector = `.modal-footer button:contains(${core._t(text)})`;
var $button = $(selector);
if ($button.length !== 1) {
throw new Error(`Found ${$button.length} button(s) containing '${text}'`);
/**
* Click on a button in the footer of a modal (which contains a given string).
*
* @param {string} text (in english: this method will perform the translation)
*/
function clickButton(text) {
return testUtilsDom.click($(`.modal-footer button:contains(${_t(text)})`));
}
$button.click();
return concurrency.delay(0);
}
return {
clickButton: clickButton,
};
return { clickButton };
});
+83 -143
View File
@@ -8,6 +8,80 @@ var Widget = require('web.Widget');
var Wysiwyg = require('web_editor.wysiwyg');
var options = require('web_editor.snippets.options');
const COLOR_PICKER_TEMPLATE = `
<t t-name="web_editor.colorpicker">
<colorpicker>
<div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">
<button data-color="alpha"/>
<button data-color="beta"/>
<button data-color="gamma"/>
<button data-color="delta"/>
<button data-color="epsilon"/>
</div>
<div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">
<button class="o_btn_transparent"/>
<button data-color="black-25"/>
<button data-color="black-50"/>
<button data-color="black-75"/>
<button data-color="white-25"/>
<button data-color="white-50"/>
<button data-color="white-75"/>
</div>
<div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"/>
</colorpicker>
</t>`;
const SNIPPETS_TEMPLATE = `
<h2 id="snippets_menu">Add blocks</h2>
<div id="o_scroll">
<div id="snippet_structure" class="o_panel">
<div class="o_panel_header">
<i class="fa fa-th-large"/> First Panel
</div>
<div class="o_panel_body">
<div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">
<div class="s_hr pt32 pb32">
<hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>
</div>
</div>
<div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
<section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">
<div class="container">
<div class="row">
<div class="col-lg-10 offset-lg-1 pt32 pb32">
<h2>Title</h2>
<p class="lead o_default_snippet_text">Content</p>
</div>
</div>
</div>
</section>
</div>
</div>
</div>
</div>
<div id="snippet_options" class="d-none">
<div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"/>
<div data-js="content"
data-selector=".s_hr, .test_option_all"
data-drop-in=".note-editable"
data-drop-near="p, h1, h2, h3, blockquote, .s_hr"/>
<div data-js="sizing_y" data-selector=".s_hr, .test_option_all"/>
<div data-selector=".test_option_all">
<we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>
</div>
<div data-js="background" data-selector=".test_option_all">
<we-button data-choose-image="true" data-no-preview="true">
<i class="fa fa-picture-o"/> Background Image
</we-button>
</div>
<div data-js="option_test" data-selector=".s_hr">
<we-select string="Alignment">
<we-button data-select-class="align-items-start">Top</we-button>
<we-button data-select-class="align-items-center">Middle</we-button>
<we-button data-select-class="align-items-end">Bottom</we-button>
<we-button data-select-class="align-items-stretch">Equal height</we-button>
</we-select>
</div>
</div>`;
MockServer.include({
//--------------------------------------------------------------------------
@@ -19,84 +93,16 @@ MockServer.include({
* @private
* @returns {Promise}
*/
_performRpc: function (route, args) {
async _performRpc(route, args) {
if (args.model === "ir.ui.view") {
if (args.method === 'read_template' && args.args[0] === "web_editor.colorpicker") {
var template = '<templates><t t-name="web_editor.colorpicker">' +
'<colorpicker>' +
' <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">' +
' <button data-color="alpha"></button>' +
' <button data-color="beta"></button>' +
' <button data-color="gamma"></button>' +
' <button data-color="delta"></button>' +
' <button data-color="epsilon"></button>' +
' </div>' +
' <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">' +
' <button class="o_btn_transparent"></button>' +
' <button data-color="black-25"></button>' +
' <button data-color="black-50"></button>' +
' <button data-color="black-75"></button>' +
' <button data-color="white-25"></button>' +
' <button data-color="white-50"></button>' +
' <button data-color="white-75"></button>' +
' </div>' +
' <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"></div>' +
'</colorpicker>' +
'</t></templates>';
return Promise.resolve(template);
return COLOR_PICKER_TEMPLATE;
}
if (args.method === 'render_template' && args.args[0] === "web_editor.snippets") {
var template = '<h2 id="snippets_menu">Add blocks</h2>' +
'<div id="o_scroll">' +
' <div id="snippet_structure" class="o_panel">' +
' <div class="o_panel_header">' +
' <i class="fa fa-th-large"></i> First Panel' +
' </div>' +
' <div class="o_panel_body">' +
' <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">' +
' <div class="s_hr pt32 pb32">' +
' <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>' +
' </div>' +
' </div>' +
' <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
' <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
' <div class="container">' +
' <div class="row">' +
' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
' <h2>Title</h2>' +
' <p class="lead o_default_snippet_text">Content</p>' +
' </div>' +
' </div>' +
' </div>' +
' </section>' +
' </div>' +
' </div>' +
' </div>' +
'</div>' +
'<div id="snippet_options" class="d-none">' +
' <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"></div>' +
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
' <div data-selector=".test_option_all">' +
' <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>' +
' </div>' +
' <div data-js="background" data-selector=".test_option_all">' +
' <we-button data-choose-image="true" data-no-preview="true"><i class="fa fa-picture-o"/> Background Image</we-button>' +
' </div>' +
' <div data-js="option_test" data-selector=".s_hr">' +
' <we-select string="Alignment">' +
' <we-button data-select-class="align-items-start">Top</we-button>' +
' <we-button data-select-class="align-items-center">Middle</we-button>' +
' <we-button data-select-class="align-items-end">Bottom</we-button>' +
' <we-button data-select-class="align-items-stretch">Equal height</we-button>' +
' </we-select>' +
' </div>' +
'</div>';
return Promise.resolve(template);
return SNIPPETS_TEMPLATE;
}
}
return this._super.apply(this, arguments);
return this._super(...arguments);
},
});
@@ -188,80 +194,14 @@ function wysiwygData(data) {
},
},
records: [],
read_template: function (args) {
read_template(args) {
if (args[0] === 'web_editor.colorpicker') {
return '<templates><t t-name="web_editor.colorpicker">' +
'<colorpicker>' +
' <div class="o_colorpicker_section" data-name="theme" data-display="Theme Colors" data-icon-class="fa fa-flask">' +
' <button data-color="alpha"></button>' +
' <button data-color="beta"></button>' +
' <button data-color="gamma"></button>' +
' <button data-color="delta"></button>' +
' <button data-color="epsilon"></button>' +
' </div>' +
' <div class="o_colorpicker_section" data-name="transparent_grayscale" data-display="Transparent Colors" data-icon-class="fa fa-eye-slash">' +
' <button class="o_btn_transparent"></button>' +
' <button data-color="black-25"></button>' +
' <button data-color="black-50"></button>' +
' <button data-color="black-75"></button>' +
' <button data-color="white-25"></button>' +
' <button data-color="white-50"></button>' +
' <button data-color="white-75"></button>' +
' </div>' +
' <div class="o_colorpicker_section" data-name="common" data-display="Common Colors" data-icon-class="fa fa-paint-brush"></div>' +
'</colorpicker>' +
'</t></templates>';
return COLOR_PICKER_TEMPLATE;
}
},
render_template: function (args) {
render_template(args) {
if (args[0] === 'web_editor.snippets') {
return '<h2 id="snippets_menu">Add blocks</h2>' +
'<div id="o_scroll">' +
' <div id="snippet_structure" class="o_panel">' +
' <div class="o_panel_header">' +
' <i class="fa fa-th-large"></i> First Panel' +
' </div>' +
' <div class="o_panel_body">' +
' <div name="Separator" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_separator.png">' +
' <div class="s_hr pt32 pb32">' +
' <hr class="s_hr_1px s_hr_solid w-100 mx-auto"/>' +
' </div>' +
' </div>' +
' <div name="Content" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
' <section name="Content+Options" class="test_option_all pt32 pb32" data-oe-type="snippet" data-oe-thumbnail="/website/static/src/img/snippets_thumbs/s_text_block.png">' +
' <div class="container">' +
' <div class="row">' +
' <div class="col-lg-10 offset-lg-1 pt32 pb32">' +
' <h2>Title</h2>' +
' <p class="lead o_default_snippet_text">Content</p>' +
' </div>' +
' </div>' +
' </div>' +
' </section>' +
' </div>' +
' </div>' +
' </div>' +
'</div>' +
'<div id="snippet_options" class="d-none">' +
' <div data-js="many2one" data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" data-no-check="true"></div>' +
' <div data-js="content" data-selector=".s_hr, .test_option_all" ' +
'data-drop-in=".note-editable" data-drop-near="p, h1, h2, h3, blockquote, .s_hr"></div>' +
' <div data-js="sizing_y" data-selector=".s_hr, .test_option_all"></div>' +
' <div data-selector=".test_option_all">' +
' <we-colorpicker string="Background Color" data-select-style="true" data-css-property="background-color" data-color-prefix="bg-"/>' +
' </div>' +
' <div data-js="background" data-selector=".test_option_all">' +
' <we-button data-choose-image="true" data-no-preview="true"><i class="fa fa-picture-o"/> Background Image</we-button>' +
' </div>' +
' <div data-js="option_test" data-selector=".s_hr">' +
' <we-select string="Alignment">' +
' <we-button data-select-class="align-items-start">Top</we-button>' +
' <we-button data-select-class="align-items-center">Middle</we-button>' +
' <we-button data-select-class="align-items-end">Bottom</we-button>' +
' <we-button data-select-class="align-items-stretch">Equal height</we-button>' +
' </we-select>' +
' </div>' +
'</div>';
return SNIPPETS_TEMPLATE;
}
},
},
@@ -758,7 +698,7 @@ var textInput = function (target, char) {
if (!ev.defaultPrevented) {
document.execCommand("insertText", 0, ev.data);
}
}
};
return {
wysiwygData: wysiwygData,