From 708648806419e9b096de9ad9050a863c8b6653cd Mon Sep 17 00:00:00 2001 From: "Michael Mattiello (mcm)" Date: Thu, 11 Feb 2021 12:51:38 +0000 Subject: [PATCH] [REF] web, *: remove patchMixin + imp utils.patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * hr, hr_holidays, im_livechat, mail, snailmail, website, website_livechat This commit removes `patchMixin` and improve `utils.patch`. `utils.patch` now supports native classes and has a new parameter used to patch class members. `utils.patch` is now used everywhere `patchMixin` was and it must be used to patch classes. closes odoo/odoo#65967 Related: odoo/enterprise#16278 Signed-off-by: Géry Debongnie (ged) Co-authored-by: ged-odoo --- addons/hr/static/tests/helpers/mock_models.js | 47 +- .../src/components/thread_view/thread_view.js | 2 +- .../static/tests/helpers/mock_models.js | 37 +- .../static/src/components/discuss/discuss.js | 2 +- .../discuss_sidebar/discuss_sidebar.js | 2 +- .../discuss_sidebar_item.js | 2 +- .../notification_list/notification_list.js | 2 +- .../thread_needaction_preview.js | 2 +- .../thread_preview/thread_preview.js | 2 +- .../static/tests/helpers/mock_models.js | 65 +- .../src/components/chat_window/chat_window.js | 4 +- .../static/src/components/dialog/dialog.js | 4 +- .../static/src/components/discuss/discuss.js | 4 +- .../messaging_menu/messaging_menu.js | 4 +- .../mail/static/src/js/custom_filter_item.js | 7 +- .../js/views/activity/activity_renderer.js | 3 +- addons/mail/static/src/utils/utils.js | 4 +- .../mail/static/tests/helpers/mock_models.js | 4 +- .../notification_group/notification_group.js | 2 +- .../static/src/components/message/message.js | 2 +- .../notification_group/notification_group.js | 2 +- .../static/tests/helpers/mock_models.js | 43 +- ...stock_traceability_report_backend_tests.js | 29 +- .../src/js/control_panel/control_panel.js | 3 +- .../js/control_panel/custom_filter_item.js | 3 +- addons/web/static/src/js/core/owl_dialog.js | 4 +- addons/web/static/src/js/core/patch_mixin.js | 80 -- addons/web/static/src/js/core/popover.js | 4 +- addons/web/static/src/js/core/utils.js | 140 +- .../src/js/views/graph/graph_renderer.js | 3 +- .../src/js/views/pivot/pivot_renderer.js | 3 +- .../web/static/src/js/views/search_panel.js | 7 +- .../static/tests/core/patch_mixin_tests.js | 994 -------------- addons/web/static/tests/core/patch_tests.js | 1161 +++++++++++++++++ addons/web/static/tests/core/util_tests.js | 35 - .../relational_fields/field_one2many_tests.js | 38 +- .../tests/report/client_action_tests.js | 29 +- addons/web/static/tests/views/list_tests.js | 18 +- addons/web/views/webclient_templates.xml | 3 +- .../website/static/tests/dashboard_tests.js | 25 +- .../static/src/components/discuss/discuss.js | 29 +- .../static/tests/helpers/mock_models.js | 69 +- .../src/components/activity/activity.js | 2 +- doc/reference/javascript_reference.rst | 163 +++ 44 files changed, 1648 insertions(+), 1440 deletions(-) delete mode 100644 addons/web/static/src/js/core/patch_mixin.js delete mode 100644 addons/web/static/tests/core/patch_mixin_tests.js create mode 100644 addons/web/static/tests/core/patch_tests.js diff --git a/addons/hr/static/tests/helpers/mock_models.js b/addons/hr/static/tests/helpers/mock_models.js index d4ae7c86035..51fa5d5ee50 100644 --- a/addons/hr/static/tests/helpers/mock_models.js +++ b/addons/hr/static/tests/helpers/mock_models.js @@ -2,33 +2,32 @@ odoo.define('hr/static/tests/helpers/mock_models.js', function (require) { 'use strict'; const MockModels = require('mail/static/tests/helpers/mock_models.js'); +const { patch } = require('web.utils'); -MockModels.patch('hr/static/tests/helpers/mock_models.js', T => - class extends T { +patch(MockModels, 'hr/static/tests/helpers/mock_models.js', { - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- - /** - * @override - */ - static generateData() { - const data = super.generateData(...arguments); - Object.assign(data, { - 'hr.employee.public': { - fields: { - display_name: { string: "Name", type: "char" }, - user_id: { string: "User", type: "many2one", relation: 'res.users' }, - user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' }, - }, - records: [], + /** + * @override + */ + generateData() { + const data = this._super(...arguments); + Object.assign(data, { + 'hr.employee.public': { + fields: { + display_name: { string: "Name", type: "char" }, + user_id: { string: "User", type: "many2one", relation: 'res.users' }, + user_partner_id: { string: "Partner", type: "many2one", relation: 'res.partner' }, }, - }); - return data; - } - - } -); + records: [], + }, + }); + return data; + }, + +}); }); diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.js b/addons/hr_holidays/static/src/components/thread_view/thread_view.js index 73ca28d4d0c..12ec1fb01b8 100644 --- a/addons/hr_holidays/static/src/components/thread_view/thread_view.js +++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.ThreadView, 'hr_holidays/static/src/components/thread_view/thread_view.js', { +patch(components.ThreadView.prototype, 'hr_holidays/static/src/components/thread_view/thread_view.js', { //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- diff --git a/addons/hr_holidays/static/tests/helpers/mock_models.js b/addons/hr_holidays/static/tests/helpers/mock_models.js index 8513f29d2bf..a53657edb30 100644 --- a/addons/hr_holidays/static/tests/helpers/mock_models.js +++ b/addons/hr_holidays/static/tests/helpers/mock_models.js @@ -2,27 +2,26 @@ odoo.define('hr_holidays/static/tests/helpers/mock_models.js', function (require 'use strict'; const MockModels = require('mail/static/tests/helpers/mock_models.js'); +const { patch } = require('web.utils'); -MockModels.patch('hr_holidays/static/tests/helpers/mock_models.js', T => - class extends T { +patch(MockModels, 'hr_holidays/static/tests/helpers/mock_models.js', { - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- - /** - * @override - */ - static generateData() { - const data = super.generateData(...arguments); - Object.assign(data['res.partner'].fields, { - // Not a real field but ease the testing - out_of_office_date_end: { type: 'datetime' }, - }); - return data; - } - - } -); + /** + * @override + */ + generateData() { + const data = this._super(...arguments); + Object.assign(data['res.partner'].fields, { + // Not a real field but ease the testing + out_of_office_date_end: { type: 'datetime' }, + }); + return data; + }, + +}); }); diff --git a/addons/im_livechat/static/src/components/discuss/discuss.js b/addons/im_livechat/static/src/components/discuss/discuss.js index 5b3c266430a..c3458d20e96 100644 --- a/addons/im_livechat/static/src/components/discuss/discuss.js +++ b/addons/im_livechat/static/src/components/discuss/discuss.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.Discuss, 'im_livechat/static/src/components/discuss/discuss.js', { +patch(components.Discuss.prototype, 'im_livechat/static/src/components/discuss/discuss.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js index 35070cb8e6d..7481805e94e 100644 --- a/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js +++ b/addons/im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.DiscussSidebar, 'im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', { +patch(components.DiscussSidebar.prototype, 'im_livechat/static/src/components/discuss_sidebar/discuss_sidebar.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js index b2273d9989b..856f100a8a5 100644 --- a/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js +++ b/addons/im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.DiscussSidebarItem, 'im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', { +patch(components.DiscussSidebarItem.prototype, 'im_livechat/static/src/components/discuss_sidebar_item/discuss_sidebar_item.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/src/components/notification_list/notification_list.js b/addons/im_livechat/static/src/components/notification_list/notification_list.js index c0ab76b28f3..9e1f1f59afa 100644 --- a/addons/im_livechat/static/src/components/notification_list/notification_list.js +++ b/addons/im_livechat/static/src/components/notification_list/notification_list.js @@ -9,7 +9,7 @@ const { patch } = require('web.utils'); components.NotificationList._allowedFilters.push('livechat'); -patch(components.NotificationList, 'im_livechat/static/src/components/notification_list/notification_list.js', { +patch(components.NotificationList.prototype, 'im_livechat/static/src/components/notification_list/notification_list.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js index 3cd95824634..45deb68109b 100644 --- a/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js +++ b/addons/im_livechat/static/src/components/thread_needaction_preview/thread_needaction_preview.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.ThreadNeedactionPreview, 'thread_needaction_preview', { +patch(components.ThreadNeedactionPreview.prototype, 'thread_needaction_preview', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/src/components/thread_preview/thread_preview.js b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js index ea752b56936..61554d71530 100644 --- a/addons/im_livechat/static/src/components/thread_preview/thread_preview.js +++ b/addons/im_livechat/static/src/components/thread_preview/thread_preview.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.ThreadPreview, 'im_livechat/static/src/components/thread_preview/thread_preview.js', { +patch(components.ThreadPreview.prototype, 'im_livechat/static/src/components/thread_preview/thread_preview.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/im_livechat/static/tests/helpers/mock_models.js b/addons/im_livechat/static/tests/helpers/mock_models.js index db18e8f776b..4ee59970de2 100644 --- a/addons/im_livechat/static/tests/helpers/mock_models.js +++ b/addons/im_livechat/static/tests/helpers/mock_models.js @@ -2,41 +2,40 @@ odoo.define('im_livechat/static/tests/helpers/mock_models.js', function (require 'use strict'; const MockModels = require('mail/static/tests/helpers/mock_models.js'); +const { patch } = require('web.utils'); -MockModels.patch('im_livechat/static/tests/helpers/mock_models.js', T => - class extends T { +patch(MockModels, 'im_livechat/static/tests/helpers/mock_models.js', { - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- - /** - * @override - */ - static generateData() { - const data = super.generateData(...arguments); - Object.assign(data, { - 'im_livechat.channel': { - fields: { - user_ids: { string: "Operators", type: 'many2many', relation: 'res.users' } - }, - records: [], - } - }); - Object.assign(data['mail.channel'].fields, { - anonymous_name: { string: "Anonymous Name", type: 'char' }, - country_id: { string: "Country", type: 'many2one', relation: 'res.country' }, - livechat_active: { string: "Is livechat ongoing?", type: 'boolean', default: false }, - livechat_channel_id: { string: "Channel", type: 'many2one', relation: 'im_livechat.channel' }, - livechat_operator_id: { string: "Operator", type: 'many2one', relation: 'res.partner' }, - }); - Object.assign(data['res.users'].fields, { - livechat_username: { string: 'Livechat Username', type: 'string' }, - }); - return data; - } - - } -); + /** + * @override + */ + generateData() { + const data = this._super(...arguments); + Object.assign(data, { + 'im_livechat.channel': { + fields: { + user_ids: { string: "Operators", type: 'many2many', relation: 'res.users' } + }, + records: [], + } + }); + Object.assign(data['mail.channel'].fields, { + anonymous_name: { string: "Anonymous Name", type: 'char' }, + country_id: { string: "Country", type: 'many2one', relation: 'res.country' }, + livechat_active: { string: "Is livechat ongoing?", type: 'boolean', default: false }, + livechat_channel_id: { string: "Channel", type: 'many2one', relation: 'im_livechat.channel' }, + livechat_operator_id: { string: "Operator", type: 'many2one', relation: 'res.partner' }, + }); + Object.assign(data['res.users'].fields, { + livechat_username: { string: 'Livechat Username', type: 'string' }, + }); + return data; + }, + +}); }); diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js index f9271523651..6811eea6c6b 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.js +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -11,8 +11,6 @@ const useStore = require('mail/static/src/component_hooks/use_store/use_store.js const useUpdate = require('mail/static/src/component_hooks/use_update/use_update.js'); const { isEventHandled } = require('mail/static/src/utils/utils.js'); -const patchMixin = require('web.patchMixin'); - const { Component } = owl; const { useRef } = owl.hooks; @@ -358,6 +356,6 @@ Object.assign(ChatWindow, { template: 'mail.ChatWindow', }); -return patchMixin(ChatWindow); +return ChatWindow; }); diff --git a/addons/mail/static/src/components/dialog/dialog.js b/addons/mail/static/src/components/dialog/dialog.js index 65902c9ce23..cf64116bf04 100644 --- a/addons/mail/static/src/components/dialog/dialog.js +++ b/addons/mail/static/src/components/dialog/dialog.js @@ -4,8 +4,6 @@ odoo.define('mail/static/src/components/dialog/dialog.js', function (require) { const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const patchMixin = require('web.patchMixin'); - const { Component } = owl; const { useRef } = owl.hooks; @@ -114,6 +112,6 @@ Object.assign(Dialog, { template: 'mail.Dialog', }); -return patchMixin(Dialog); +return Dialog; }); diff --git a/addons/mail/static/src/components/discuss/discuss.js b/addons/mail/static/src/components/discuss/discuss.js index e303e495015..0ad12e48409 100644 --- a/addons/mail/static/src/components/discuss/discuss.js +++ b/addons/mail/static/src/components/discuss/discuss.js @@ -15,8 +15,6 @@ const components = { const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const patchMixin = require('web.patchMixin'); - const { Component } = owl; const { useRef } = owl.hooks; @@ -308,6 +306,6 @@ Object.assign(Discuss, { template: 'mail.Discuss', }); -return patchMixin(Discuss); +return Discuss; }); diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.js b/addons/mail/static/src/components/messaging_menu/messaging_menu.js index 9eb7fd71a49..e7c93d2cb66 100644 --- a/addons/mail/static/src/components/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.js @@ -9,8 +9,6 @@ const components = { const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const patchMixin = require('web.patchMixin'); - const { Component } = owl; class MessagingMenu extends Component { @@ -229,6 +227,6 @@ Object.assign(MessagingMenu, { template: 'mail.MessagingMenu', }); -return patchMixin(MessagingMenu); +return MessagingMenu; }); diff --git a/addons/mail/static/src/js/custom_filter_item.js b/addons/mail/static/src/js/custom_filter_item.js index abe15edab94..5dc590be354 100644 --- a/addons/mail/static/src/js/custom_filter_item.js +++ b/addons/mail/static/src/js/custom_filter_item.js @@ -2,8 +2,9 @@ odoo.define('mail.CustomFilterItem', function (require) { "use strict"; const CustomFilterItem = require('web.CustomFilterItem'); + const { patch } = require('web.utils'); - CustomFilterItem.patch('mail.CustomFilterItem', T => class extends T { + patch(CustomFilterItem.prototype, 'mail.CustomFilterItem', { /** * With the `mail` module installed, we want to filter out some of the @@ -11,10 +12,10 @@ odoo.define('mail.CustomFilterItem', function (require) { * @override */ _validateField(field) { - return super._validateField(...arguments) && + return this._super(...arguments) && field.relation !== 'mail.message' && field.name !== 'message_ids'; - } + }, }); return CustomFilterItem; diff --git a/addons/mail/static/src/js/views/activity/activity_renderer.js b/addons/mail/static/src/js/views/activity/activity_renderer.js index e62d416ea28..8f36231194d 100644 --- a/addons/mail/static/src/js/views/activity/activity_renderer.js +++ b/addons/mail/static/src/js/views/activity/activity_renderer.js @@ -7,7 +7,6 @@ const ActivityRecord = require('mail.ActivityRecord'); const { ComponentAdapter } = require('web.OwlCompatibility'); const core = require('web.core'); const KanbanColumnProgressBar = require('web.KanbanColumnProgressBar'); -const patchMixin = require('web.patchMixin'); const QWeb = require('web.QWeb'); const session = require('web.session'); const utils = require('web.utils'); @@ -205,6 +204,6 @@ ActivityRenderer.components = { }; ActivityRenderer.template = 'mail.ActivityRenderer'; -return patchMixin(ActivityRenderer); +return ActivityRenderer; }); diff --git a/addons/mail/static/src/utils/utils.js b/addons/mail/static/src/utils/utils.js index f502fc8f326..06a99ed11c8 100644 --- a/addons/mail/static/src/utils/utils.js +++ b/addons/mail/static/src/utils/utils.js @@ -124,7 +124,7 @@ function patchClassMethods(Class, patchName, patch) { * @returns {function} unpatch function */ function patchInstanceMethods(Class, patchName, patch) { - return webUtilsPatch(Class, patchName, patch); + return webUtilsPatch(Class.prototype, patchName, patch); } /** @@ -158,7 +158,7 @@ function unpatchClassMethods(Class, patchName) { * @param {string} patchName */ function unpatchInstanceMethods(Class, patchName) { - return webUtilsUnpatch(Class, patchName); + return webUtilsUnpatch(Class.prototype, patchName); } //------------------------------------------------------------------------------ diff --git a/addons/mail/static/tests/helpers/mock_models.js b/addons/mail/static/tests/helpers/mock_models.js index 873b5b0be70..4ce50094ac7 100644 --- a/addons/mail/static/tests/helpers/mock_models.js +++ b/addons/mail/static/tests/helpers/mock_models.js @@ -1,8 +1,6 @@ odoo.define('mail/static/tests/helpers/mock_models.js', function (require) { 'use strict'; -const patchMixin = require('web.patchMixin'); - /** * Allows to generate mocked models that will be used by the mocked server. * This is defined as a class to allow patches by dependent modules and a new @@ -253,6 +251,6 @@ class MockModels { } -return patchMixin(MockModels); +return MockModels; }); diff --git a/addons/sms/static/src/components/notification_group/notification_group.js b/addons/sms/static/src/components/notification_group/notification_group.js index 053fedc5bd2..973948992c6 100644 --- a/addons/sms/static/src/components/notification_group/notification_group.js +++ b/addons/sms/static/src/components/notification_group/notification_group.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.NotificationGroup, 'sms/static/src/components/notification_group/notification_group.js', { +patch(components.NotificationGroup.prototype, 'sms/static/src/components/notification_group/notification_group.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/snailmail/static/src/components/message/message.js b/addons/snailmail/static/src/components/message/message.js index 1033c901367..a51d131d2a4 100644 --- a/addons/snailmail/static/src/components/message/message.js +++ b/addons/snailmail/static/src/components/message/message.js @@ -16,7 +16,7 @@ Object.assign(components.Message.components, { SnailmailNotificationPopover: components.SnailmailNotificationPopover, }); -patch(components.Message, 'snailmail/static/src/components/message/message.js', { +patch(components.Message.prototype, 'snailmail/static/src/components/message/message.js', { /** * @override */ diff --git a/addons/snailmail/static/src/components/notification_group/notification_group.js b/addons/snailmail/static/src/components/notification_group/notification_group.js index 00cdebe15d9..97254dbea22 100644 --- a/addons/snailmail/static/src/components/notification_group/notification_group.js +++ b/addons/snailmail/static/src/components/notification_group/notification_group.js @@ -7,7 +7,7 @@ const components = { const { patch } = require('web.utils'); -patch(components.NotificationGroup, 'snailmail/static/src/components/notification_group/notification_group.js', { +patch(components.NotificationGroup.prototype, 'snailmail/static/src/components/notification_group/notification_group.js', { //-------------------------------------------------------------------------- // Public diff --git a/addons/snailmail/static/tests/helpers/mock_models.js b/addons/snailmail/static/tests/helpers/mock_models.js index d338504d024..ee1a442b5a3 100644 --- a/addons/snailmail/static/tests/helpers/mock_models.js +++ b/addons/snailmail/static/tests/helpers/mock_models.js @@ -2,31 +2,30 @@ odoo.define('snailmail/static/tests/helpers/mock_models.js', function (require) 'use strict'; const MockModels = require('mail/static/tests/helpers/mock_models.js'); +const { patch } = require('web.utils'); -MockModels.patch('snailmail/static/tests/helpers/mock_models.js', T => - class extends T { +patch(MockModels, 'snailmail/static/tests/helpers/mock_models.js', { - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- - /** - * @override - */ - static generateData() { - const data = super.generateData(...arguments); - Object.assign(data, { - 'snailmail.letter': { - fields: { - message_id: { string: 'Snailmail Status Message', type: 'many2one', relation: 'mail.message' }, - }, - records: [], + /** + * @override + */ + generateData() { + const data = this._super(...arguments); + Object.assign(data, { + 'snailmail.letter': { + fields: { + message_id: { string: 'Snailmail Status Message', type: 'many2one', relation: 'mail.message' }, }, - }); - return data; - } - - } -); + records: [], + }, + }); + return data; + }, + +}); }); diff --git a/addons/stock/static/tests/stock_traceability_report_backend_tests.js b/addons/stock/static/tests/stock_traceability_report_backend_tests.js index 5ab69403544..08181d8535c 100644 --- a/addons/stock/static/tests/stock_traceability_report_backend_tests.js +++ b/addons/stock/static/tests/stock_traceability_report_backend_tests.js @@ -5,6 +5,7 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require) const dom = require('web.dom'); const StockReportGeneric = require('stock.stock_report_generic'); const testUtils = require('web.test_utils'); + const { patch, unpatch } = require('web.utils'); const { createActionManager, dom: domUtils } = testUtils; @@ -77,20 +78,18 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require) let mountCount = 0; - ControlPanel.patch('test.ControlPanel', T => { - class ControlPanelPatchTest extends T { - mounted() { - mountCount = mountCount + 1; - this.__uniqueId = mountCount; - assert.step(`mounted ${this.__uniqueId}`); - super.mounted(...arguments); - } - willUnmount() { - assert.step(`willUnmount ${this.__uniqueId}`); - super.mounted(...arguments); - } - } - return ControlPanelPatchTest; + patch(ControlPanel.prototype, 'test.ControlPanel', { + mounted() { + mountCount = mountCount + 1; + this.__uniqueId = mountCount; + assert.step(`mounted ${this.__uniqueId}`); + this.__superMounted = this._super.bind(this); + this.__superMounted(...arguments); + }, + willUnmount() { + assert.step(`willUnmount ${this.__uniqueId}`); + this.__superMounted(...arguments); + }, }); const actionManager = await createActionManager({ @@ -145,7 +144,7 @@ odoo.define('stock.stock_traceability_report_backend_tests', function (require) 'willUnmount 3', ]); - ControlPanel.unpatch('test.ControlPanel'); + unpatch(ControlPanel.prototype, 'test.ControlPanel'); }); }); }); diff --git a/addons/web/static/src/js/control_panel/control_panel.js b/addons/web/static/src/js/control_panel/control_panel.js index 60e26f0b280..16cb905f683 100644 --- a/addons/web/static/src/js/control_panel/control_panel.js +++ b/addons/web/static/src/js/control_panel/control_panel.js @@ -7,7 +7,6 @@ odoo.define('web.ControlPanel', function (require) { const FavoriteMenu = require('web.FavoriteMenu'); const FilterMenu = require('web.FilterMenu'); const GroupByMenu = require('web.GroupByMenu'); - const patchMixin = require('web.patchMixin'); const Pager = require('web.Pager'); const SearchBar = require('web.SearchBar'); const { useModel } = require('web.Model'); @@ -217,5 +216,5 @@ odoo.define('web.ControlPanel', function (require) { }; ControlPanel.template = 'web.ControlPanel'; - return patchMixin(ControlPanel); + return ControlPanel; }); diff --git a/addons/web/static/src/js/control_panel/custom_filter_item.js b/addons/web/static/src/js/control_panel/custom_filter_item.js index 7aeb81fe4ea..29fc35084a5 100644 --- a/addons/web/static/src/js/control_panel/custom_filter_item.js +++ b/addons/web/static/src/js/control_panel/custom_filter_item.js @@ -6,7 +6,6 @@ odoo.define('web.CustomFilterItem', function (require) { const DropdownMenuItem = require('web.DropdownMenuItem'); const { FIELD_OPERATORS, FIELD_TYPES } = require('web.searchUtils'); const field_utils = require('web.field_utils'); - const patchMixin = require('web.patchMixin'); const { useModel } = require('web.Model'); /** @@ -262,5 +261,5 @@ odoo.define('web.CustomFilterItem', function (require) { }; CustomFilterItem.template = 'web.CustomFilterItem'; - return patchMixin(CustomFilterItem); + return CustomFilterItem; }); diff --git a/addons/web/static/src/js/core/owl_dialog.js b/addons/web/static/src/js/core/owl_dialog.js index 62c6d239dda..9c5dc93a7b3 100644 --- a/addons/web/static/src/js/core/owl_dialog.js +++ b/addons/web/static/src/js/core/owl_dialog.js @@ -1,8 +1,6 @@ odoo.define('web.OwlDialog', function (require) { "use strict"; - const patchMixin = require('web.patchMixin'); - const { Component, hooks, misc } = owl; const { Portal } = misc; const { useExternalListener, useRef } = hooks; @@ -271,5 +269,5 @@ odoo.define('web.OwlDialog', function (require) { }; Dialog.template = 'web.OwlDialog'; - return patchMixin(Dialog); + return Dialog; }); diff --git a/addons/web/static/src/js/core/patch_mixin.js b/addons/web/static/src/js/core/patch_mixin.js deleted file mode 100644 index 99f06f56588..00000000000 --- a/addons/web/static/src/js/core/patch_mixin.js +++ /dev/null @@ -1,80 +0,0 @@ -odoo.define("web.patchMixin", function () { - "use strict"; - - /** - * This module defines and exports the 'patchMixin' function. This function - * returns a 'monkey-patchable' version of the ES6 Class given in arguments. - * - * const patchMixin = require('web.patchMixin'); - * class MyClass { - * print() { - * console.log('MyClass'); - * } - * } - * const MyPatchedClass = patchMixin(MyClass); - * - * - * A patchable class has a 'patch' function, allowing to define a patch: - * - * MyPatchedClass.patch("module_name.key", T => - * class extends T { - * print() { - * console.log('MyPatchedClass'); - * super.print(); - * } - * } - * ); - * - * const myPatchedClass = new MyPatchedClass(); - * myPatchedClass.print(); // displays "MyPatchedClass" and "MyClass" - * - * - * The 'unpatch' function can be used to remove a patch, given its key: - * - * MyPatchedClass.unpatch("module_name.key"); - */ - function patchMixin(OriginalClass) { - let unpatchList = []; - class PatchableClass extends OriginalClass {} - - PatchableClass.patch = function (name, patch) { - if (unpatchList.find(x => x.name === name)) { - throw new Error(`Class ${OriginalClass.name} already has a patch ${name}`); - } - if (!Object.prototype.hasOwnProperty.call(this, 'patch')) { - throw new Error(`Class ${this.name} is not patchable`); - } - const SubClass = patch(Object.getPrototypeOf(this)); - unpatchList.push({ - name: name, - elem: this, - prototype: this.prototype, - origProto: Object.getPrototypeOf(this), - origPrototype: Object.getPrototypeOf(this.prototype), - patch: patch, - }); - Object.setPrototypeOf(this, SubClass); - Object.setPrototypeOf(this.prototype, SubClass.prototype); - }; - - PatchableClass.unpatch = function (name) { - if (!unpatchList.find(x => x.name === name)) { - throw new Error(`Class ${OriginalClass.name} does not have any patch ${name}`); - } - const toUnpatch = unpatchList.reverse(); - unpatchList = []; - for (let unpatch of toUnpatch) { - Object.setPrototypeOf(unpatch.elem, unpatch.origProto); - Object.setPrototypeOf(unpatch.prototype, unpatch.origPrototype); - } - for (let u of toUnpatch.reverse()) { - if (u.name !== name) { - PatchableClass.patch(u.name, u.patch); - } - } - }; - return PatchableClass; - } - - return patchMixin; -}); diff --git a/addons/web/static/src/js/core/popover.js b/addons/web/static/src/js/core/popover.js index d2102b56d3c..9796916ab87 100644 --- a/addons/web/static/src/js/core/popover.js +++ b/addons/web/static/src/js/core/popover.js @@ -1,8 +1,6 @@ odoo.define('web.Popover', function (require) { 'use strict'; - const patchMixin = require('web.patchMixin'); - const { Component, hooks, misc, QWeb } = owl; const { Portal } = misc; const { useRef, useState } = hooks; @@ -318,5 +316,5 @@ odoo.define('web.Popover', function (require) { QWeb.registerComponent('Popover', Popover); - return patchMixin(Popover); + return Popover; }); diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js index aa085e9b525..15f3f973f24 100644 --- a/addons/web/static/src/js/core/utils.js +++ b/addons/web/static/src/js/core/utils.js @@ -611,57 +611,80 @@ var utils = { }, []); }, /** - * Patch a class and return a function that remove the patch + * Patch an object and return a function that remove the patch * when called. * - * This function is the last resort solution for monkey-patching an - * ES6 Class, for people that do not control the code defining the Class - * to patch (e.g. partners), and when that Class isn't patchable already - * (i.e. when it doesn't have a 'patch' function, defined by the 'web.patchMixin'). - * - * @param {Class} C Class to patch + * @param {Object} obj Object to patch * @param {string} patchName * @param {Object} patch - * @returns {Function} */ - patch: function (C, patchName, patch) { - let metadata = patchMap.get(C.prototype); - if (!metadata) { - metadata = { - origMethods: {}, - patches: {}, - current: [] - }; - patchMap.set(C.prototype, metadata); - } - const proto = C.prototype; - if (metadata.patches[patchName]) { - throw new Error(`Patch [${patchName}] already exists`); - } - metadata.patches[patchName] = patch; - applyPatch(proto, patch); - metadata.current.push(patchName); - - function applyPatch(proto, patch) { - Object.keys(patch).forEach(function (methodName) { - const method = patch[methodName]; - if (typeof method === "function") { - const original = proto[methodName]; - if (!(methodName in metadata.origMethods)) { - metadata.origMethods[methodName] = original; - } - proto[methodName] = function (...args) { - const previousSuper = this._super; - this._super = original; - const res = method.call(this, ...args); - this._super = previousSuper; - return res; - }; - } + patch: function (obj, patchName, patch) { + if (!patchMap.has(obj)) { + patchMap.set(obj, { + original: {}, + patches: [], }); } + const objDesc = patchMap.get(obj); + if (objDesc.patches.some(p => p.name === patchName)) { + throw new Error(`Class ${obj.name} already has a patch ${patchName}`); + } + objDesc.patches.push({ + name: patchName, + patch, + }); - return utils.unpatch.bind(null, C, patchName); + for (const k in patch) { + let prevDesc = null; + let proto = obj; + do { + prevDesc = Object.getOwnPropertyDescriptor(proto, k); + proto = Object.getPrototypeOf(proto); + } while (!prevDesc && proto); + + const newDesc = Object.getOwnPropertyDescriptor(patch, k); + if (!objDesc.original.hasOwnProperty(k)) { + objDesc.original[k] = Object.getOwnPropertyDescriptor(obj, k); + } + + if (prevDesc) { + const patchedFnName = `${k} (patch ${patchName})`; + + if (prevDesc.value && typeof newDesc.value === "function") { + makeIntermediateFunction("value", prevDesc, newDesc, patchedFnName); + } + if (prevDesc.get || prevDesc.set) { + // get and set are defined together. If they are both defined + // in the previous descriptor but only one in the new descriptor + // then the other will be undefined so we need to apply the + // previous descriptor in the new one. + newDesc.get = newDesc.get || prevDesc.get; + newDesc.set = newDesc.set || prevDesc.set; + if (prevDesc.get && typeof newDesc.get === "function") { + makeIntermediateFunction("get", prevDesc, newDesc, patchedFnName); + } + if (prevDesc.set && typeof newDesc.set === "function") { + makeIntermediateFunction("set", prevDesc, newDesc, patchedFnName); + } + } + } + + Object.defineProperty(obj, k, newDesc); + } + + function makeIntermediateFunction(key, prevDesc, newDesc, patchedFnName) { + const _superFn = prevDesc[key]; + const patchFn = newDesc[key]; + newDesc[key] = { + [patchedFnName](...args) { + const prevSuper = this._super; + this._super = _superFn.bind(this); + const result = patchFn.call(this, ...args); + this._super = prevSuper; + return result; + } + }[patchedFnName]; + } }, /** * performs a half up rounding with a fixed amount of decimals, correcting for float loss of precision @@ -872,26 +895,29 @@ var utils = { * We define here an unpatch function. This is mostly useful if we want to * remove a patch. For example, for testing purposes * - * @param {Class} C + * @param {Object} obj * @param {string} patchName */ - unpatch: function (C, patchName) { - const proto = C.prototype; - let metadata = patchMap.get(proto); - if (!metadata) { - return; + unpatch: function (obj, patchName) { + const objDesc = patchMap.get(obj); + if (!objDesc.patches.some(p => p.name === patchName)) { + throw new Error(`Class ${obj.name} does not have any patch ${patchName}`); } - patchMap.delete(proto); + patchMap.delete(obj); - // reset to original - for (let k in metadata.origMethods) { - proto[k] = metadata.origMethods[k]; + // Restore original methods on the prototype and the class. + for (const k in objDesc.original) { + if (objDesc.original[k] === undefined) { + delete obj[k]; + } else { + Object.defineProperty(obj, k, objDesc.original[k]); + } } - // apply other patches - for (let name of metadata.current) { - if (name !== patchName) { - utils.patch(C, name, metadata.patches[name]); + // Re-apply the patches except the one to remove. + for (const patchDesc of objDesc.patches) { + if (patchDesc.name !== patchName) { + utils.patch(obj, patchDesc.name, patchDesc.patch); } } }, diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js index d8cc39a29df..45944e6ac63 100644 --- a/addons/web/static/src/js/views/graph/graph_renderer.js +++ b/addons/web/static/src/js/views/graph/graph_renderer.js @@ -5,7 +5,6 @@ odoo.define("web/static/src/js/views/graph/graph_renderer", function (require) { const { device } = require("web.config"); const { DateClasses } = require("web.dataComparisonUtils"); const fieldUtils = require("web.field_utils"); - const patchMixin = require("web.patchMixin"); const { sortBy } = require("web.utils"); const COLORS = [ @@ -1048,6 +1047,6 @@ odoo.define("web/static/src/js/views/graph/graph_renderer", function (require) { withSearchPanel: { type: Boolean, optional: 1 }, }; - return patchMixin(GraphRenderer); + return GraphRenderer; }); diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index ed9df45c098..07801fca0b9 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -2,7 +2,6 @@ import OwlAbstractRenderer from '../abstract_renderer_owl'; import field_utils from 'web.field_utils'; - import patchMixin from 'web.patchMixin'; const { useExternalListener, useState, onMounted, onPatched } = owl.hooks; @@ -196,4 +195,4 @@ PivotRenderer.template = 'web.PivotRenderer'; - export default patchMixin(PivotRenderer); + export default PivotRenderer; diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js index 270d1ad181c..e5936260e86 100644 --- a/addons/web/static/src/js/views/search_panel.js +++ b/addons/web/static/src/js/views/search_panel.js @@ -1,7 +1,6 @@ /** @odoo-module alias=web.searchPanel **/ import { Model, useModel } from "web.Model"; - import patchMixin from "web.patchMixin"; const { Component, hooks } = owl; const { useState, useSubEnv } = hooks; @@ -17,9 +16,7 @@ * @extends Component */ class SearchPanel extends Component { - constructor() { - super(...arguments); - + setup() { useSubEnv({ searchModel: this.props.searchModel }); this.state = useState({ @@ -209,4 +206,4 @@ }; SearchPanel.template = "web.SearchPanel"; - export default patchMixin(SearchPanel); + export default SearchPanel; diff --git a/addons/web/static/tests/core/patch_mixin_tests.js b/addons/web/static/tests/core/patch_mixin_tests.js deleted file mode 100644 index 7329a9607d4..00000000000 --- a/addons/web/static/tests/core/patch_mixin_tests.js +++ /dev/null @@ -1,994 +0,0 @@ -odoo.define("web.patchMixin_tests", function (require) { -"use strict"; - -const patchMixin = require('web.patchMixin'); - -QUnit.module('core', {}, function () { - - QUnit.module('patchMixin', {}, function () { - - QUnit.test('basic use', function (assert) { - assert.expect(4); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - const a = new A(); - a.f(); - - assert.ok(a instanceof A); - assert.verifySteps([ - 'A.constructor', - 'A.f', - ]); - }); - - QUnit.test('simple patch', function (assert) { - assert.expect(5); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch.constructor', - 'A.f', - 'patch.f', - ]); - }); - - QUnit.test('two patches on same base class', function (assert) { - assert.expect(7); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch1', T => - class extends T { - constructor() { - super(); - assert.step('patch1.constructor'); - } - f() { - super.f(); - assert.step('patch1.f'); - } - } - ); - - A.patch('patch2', T => - class extends T { - constructor() { - super(); - assert.step('patch2.constructor'); - } - f() { - super.f(); - assert.step('patch2.f'); - } - } - ); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch1.constructor', - 'patch2.constructor', - 'A.f', - 'patch1.f', - 'patch2.f', - ]); - }); - - QUnit.test('two patches with same name on same base class', function (assert) { - assert.expect(1); - - const A = patchMixin(class {}); - - A.patch('patch', T => class extends T {}); - - // keys should be unique - assert.throws(() => { - A.patch('patch', T => class extends T {}); - }); - }); - - QUnit.test('unpatch', function (assert) { - assert.expect(8); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch.constructor', - 'A.f', - 'patch.f', - ]); - - A.unpatch('patch'); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'A.f', - ]); - }); - - QUnit.test('unpatch 2', function (assert) { - assert.expect(12); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch1', T => - class extends T { - constructor() { - super(); - assert.step('patch1.constructor'); - } - f() { - super.f(); - assert.step('patch1.f'); - } - } - ); - - A.patch('patch2', T => - class extends T { - constructor() { - super(); - assert.step('patch2.constructor'); - } - f() { - super.f(); - assert.step('patch2.f'); - } - } - ); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch1.constructor', - 'patch2.constructor', - 'A.f', - 'patch1.f', - 'patch2.f', - ]); - - A.unpatch('patch1'); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch2.constructor', - 'A.f', - 'patch2.f', - ]); - }); - - QUnit.test('unpatch inexistent', function (assert) { - assert.expect(1); - - const A = patchMixin(class {}); - A.patch('patch', T => class extends T {}); - - A.unpatch('patch'); - assert.throws(() => { - A.unpatch('inexistent-patch'); - }); - }); - - QUnit.test('patch for specialization', function (assert) { - assert.expect(1); - - let args = []; - - const A = patchMixin( - class { - constructor() { - args = ['A', ...arguments]; - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super('patch', ...arguments); - } - } - ); - - new A('instantiation'); - - assert.deepEqual(args, ['A', 'patch', 'instantiation']); - }); - - QUnit.test('instance fields', function (assert) { - assert.expect(1); - - const A = patchMixin( - class { - constructor() { - this.x = ['A']; - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - this.x.push('patch'); - } - } - ); - - const a = new A(); - assert.deepEqual(a.x, ['A', 'patch']); - }); - - QUnit.test('call instance method defined in patch', function (assert) { - assert.expect(3); - - const A = patchMixin( - class {} - ); - - assert.notOk((new A()).f); - - A.patch('patch', T => - class extends T { - f() { - assert.step('patch.f'); - } - } - ); - - (new A()).f(); - assert.verifySteps(['patch.f']); - }); - - QUnit.test('class methods', function (assert) { - assert.expect(7); - - const A = patchMixin( - class { - static f() { - assert.step('A'); - } - } - ); - - A.f(); - assert.verifySteps(['A']); - - A.patch('patch', T => - class extends T { - static f() { - super.f(); - assert.step('patch'); - } - } - ); - - A.f(); - assert.verifySteps(['A', 'patch']); - - A.unpatch('patch'); - - A.f(); - assert.verifySteps(['A']); - }); - - QUnit.test('class fields', function (assert) { - assert.expect(4); - - class A {} - A.foo = ['A']; - A.bar = 'A'; - - const PatchableA = patchMixin(A); - - PatchableA.patch('patch', T => { - class Patch extends T {} - - Patch.foo = [...T.foo, 'patched A']; - Patch.bar = 'patched A'; - - return Patch; - }); - - assert.deepEqual(PatchableA.foo, ['A', 'patched A']); - assert.strictEqual(PatchableA.bar, 'patched A'); - - PatchableA.unpatch('patch'); - - assert.deepEqual(PatchableA.foo, ['A']); - assert.strictEqual(PatchableA.bar, 'A'); - }); - - QUnit.test('lazy patch', function (assert) { - assert.expect(4); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - const a = new A(); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - // will not be called - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - a.f(); - - assert.verifySteps([ - 'A.constructor', - 'A.f', - 'patch.f', - ]); - }); - - - QUnit.module('inheritance'); - - QUnit.test('inheriting a patchable class', function (assert) { - assert.expect(8); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - class B extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'A.f', - ]); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'A.f', - 'B.f', - ]); - }); - - QUnit.test('inheriting a patchable class that has patch', function (assert) { - assert.expect(12); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - class B extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch.constructor', - 'A.f', - 'patch.f', - ]); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'patch.constructor', - 'B.constructor', - 'A.f', - 'patch.f', - 'B.f', - ]); - }); - - QUnit.test('patch inherited patchable class', function (assert) { - assert.expect(10); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - const B = patchMixin( - class extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - ); - - B.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'A.f', - ]); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'patch.constructor', - 'A.f', - 'B.f', - 'patch.f', - ]); - }); - - QUnit.test('patch inherited patched class', function (assert) { - assert.expect(14); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('A.patch.constructor'); - } - f() { - super.f(); - assert.step('A.patch.f'); - } - } - ); - - /** - * /!\ WARNING /!\ - * - * If you want to patch class B, make it patchable - * otherwise it will patch class A! - */ - const B = patchMixin( - class extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - ); - - B.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('B.patch.constructor'); - } - f() { - super.f(); - assert.step('B.patch.f'); - } - } - ); - - const a = new A(); - a.f(); - - assert.verifySteps([ - 'A.constructor', - 'A.patch.constructor', - 'A.f', - 'A.patch.f', - ]); - - const b = new B(); - b.f(); - - assert.verifySteps([ - 'A.constructor', - 'A.patch.constructor', - 'B.constructor', - 'B.patch.constructor', - 'A.f', - 'A.patch.f', - 'B.f', - 'B.patch.f', - ]); - }); - - QUnit.test('unpatch inherited patched class', function (assert) { - assert.expect(15); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('A.patch.constructor'); - } - f() { - super.f(); - assert.step('A.patch.f'); - } - } - ); - - const B = patchMixin( - class extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - ); - - B.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('B.patch.constructor'); - } - f() { - super.f(); - assert.step('B.patch.f'); - } - } - ); - - A.unpatch('patch'); - - (new A()).f(); - - assert.verifySteps([ - 'A.constructor', - 'A.f', - ]); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'B.patch.constructor', - 'A.f', - 'B.f', - 'B.patch.f', - ]); - - B.unpatch('patch'); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'A.f', - 'B.f', - ]); - }); - - QUnit.test('unpatch inherited patched class 2', function (assert) { - assert.expect(12); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('A.patch.constructor'); - } - f() { - super.f(); - assert.step('A.patch.f'); - } - } - ); - - const B = patchMixin( - class extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - ); - - B.patch('patch', T => - class extends T { - constructor() { - super(); - assert.step('B.patch.constructor'); - } - f() { - super.f(); - assert.step('B.patch.f'); - } - } - ); - - B.unpatch('patch'); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'A.patch.constructor', - 'B.constructor', - 'A.f', - 'A.patch.f', - 'B.f', - ]); - - A.unpatch('patch'); - - (new B()).f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'A.f', - 'B.f', - ]); - }); - - QUnit.test('class methods', function (assert) { - assert.expect(12); - - const A = patchMixin( - class { - static f() { - assert.step('A'); - } - } - ); - - const B = patchMixin( - class extends A { - static f() { - super.f(); - assert.step('B'); - } - } - ); - - A.patch('patch', T => - class extends T { - static f() { - super.f(); - assert.step('A.patch'); - } - } - ); - - B.patch('patch', T => - class extends T { - static f() { - super.f(); - assert.step('B.patch'); - } - } - ); - - B.f(); - assert.verifySteps(['A', 'A.patch', 'B', 'B.patch']); - - A.unpatch('patch'); - - B.f(); - assert.verifySteps(['A', 'B', 'B.patch']); - - B.unpatch('patch'); - - B.f(); - assert.verifySteps(['A', 'B']); - }); - - QUnit.test('class fields', function (assert) { - assert.expect(3); - - class A {} - A.foo = ['A']; - A.bar = 'A'; - - const PatchableA = patchMixin(A); - - class B extends PatchableA {} - // /!\ This is not dynamic - // so if A.foo is patched after this assignment - // B.foo won't have the patches of A.foo - B.foo = [...PatchableA.foo, 'B']; - B.bar = 'B'; - - const PatchableB = patchMixin(B); - - PatchableA.patch('patch', T => { - class Patch extends T {} - - Patch.foo = [...T.foo, 'patched A']; - Patch.bar = 'patched A'; - - return Patch; - }); - - PatchableB.patch('patch', T => { - class Patch extends T {} - - Patch.foo = [...T.foo, 'patched B']; - Patch.bar = 'patched B'; - - return Patch; - }); - - assert.deepEqual(PatchableB.foo, [ 'A', /* 'patched A', */ 'B', 'patched B' ]); - assert.deepEqual(PatchableA.foo, [ 'A', 'patched A' ]); - assert.strictEqual(PatchableB.bar, 'patched B'); - }); - - QUnit.test('inheritance and lazy patch', function (assert) { - assert.expect(6); - - const A = patchMixin( - class { - constructor() { - assert.step('A.constructor'); - } - f() { - assert.step('A.f'); - } - } - ); - - class B extends A { - constructor() { - super(); - assert.step('B.constructor'); - } - f() { - super.f(); - assert.step('B.f'); - } - } - - const b = new B(); - - A.patch('patch', T => - class extends T { - constructor() { - super(); - // will not be called - assert.step('patch.constructor'); - } - f() { - super.f(); - assert.step('patch.f'); - } - } - ); - - b.f(); - - assert.verifySteps([ - 'A.constructor', - 'B.constructor', - 'A.f', - 'patch.f', - 'B.f', - ]); - }); - - QUnit.test('patch not patchable class that inherits patchable class', function (assert) { - assert.expect(1); - - const A = patchMixin(class {}); - class B extends A {} - - // class B is not patchable - assert.throws(() => { - B.patch('patch', T => class extends T {}); - }); - }); - }); -}); -}); diff --git a/addons/web/static/tests/core/patch_tests.js b/addons/web/static/tests/core/patch_tests.js new file mode 100644 index 00000000000..d1b8a340fe5 --- /dev/null +++ b/addons/web/static/tests/core/patch_tests.js @@ -0,0 +1,1161 @@ +odoo.define("web.patch_tests", function (require) { +"use strict"; + +const { patch, unpatch } = require('web.utils'); + +function makeBaseClass(assert, assertInSetup) { + class BaseClass { + constructor() { + this.setup(); + } + setup() { + this._dynamic = "base"; + + this.str = "base"; + this.obj = { base: "base" }; + this.arr = ["base"]; + + if (assertInSetup !== false) { + assert.step("base.setup"); + } + } + fn() { + assert.step("base.fn"); + } + async asyncFn() { + // also check this binding + assert.step(`base.${this.str}`); + } + get dynamic() { + return this._dynamic; + } + set dynamic(value) { + this._dynamic = value; + } + } + + BaseClass.staticStr = "base"; + BaseClass.staticObj = { base: "base" }; + BaseClass.staticArr = ["base"]; + BaseClass.staticFn = function () { + assert.step("base.staticFn"); + }; + + return BaseClass; +} + +QUnit.module('core', {}, function () { + + QUnit.module('patch', {}, function () { + QUnit.test('simple patch', async function (assert) { + assert.expect(5); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'base.fn', + 'patch.fn', + ]); + }); + + QUnit.test('two patches on same base class', async function (assert) { + assert.expect(7); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch1', { + setup() { + this._super(); + assert.step('patch1.setup'); + }, + fn() { + this._super(); + assert.step('patch1.fn'); + }, + }); + + patch(BaseClass.prototype, 'patch2', { + setup() { + this._super(); + assert.step('patch2.setup'); + }, + fn() { + this._super(); + assert.step('patch2.fn'); + }, + }); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch1.setup', + 'patch2.setup', + 'base.fn', + 'patch1.fn', + 'patch2.fn', + ]); + }); + + QUnit.test('two patches with same name on same base class', async function (assert) { + assert.expect(1); + + const A = class {}; + + patch(A.prototype, 'patch'); + + // keys should be unique + assert.throws(() => { + patch(A.prototype, 'patch'); + }); + }); + + QUnit.test('unpatch', async function (assert) { + assert.expect(8); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'base.fn', + 'patch.fn', + ]); + + unpatch(BaseClass.prototype, 'patch'); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'base.fn', + ]); + }); + + QUnit.test('unpatch two patches on the same base class', async function (assert) { + assert.expect(15); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch1', { + setup() { + this._super(); + assert.step('patch1.setup'); + }, + fn() { + this._super(); + assert.step('patch1.fn'); + }, + }); + + patch(BaseClass.prototype, 'patch2', { + setup() { + this._super(); + assert.step('patch2.setup'); + }, + fn() { + this._super(); + assert.step('patch2.fn'); + }, + }); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch1.setup', + 'patch2.setup', + 'base.fn', + 'patch1.fn', + 'patch2.fn', + ]); + + unpatch(BaseClass.prototype, 'patch1'); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch2.setup', + 'base.fn', + 'patch2.fn', + ]); + + unpatch(BaseClass.prototype, 'patch2'); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'base.fn', + ]); + }); + + QUnit.test('unpatch two patches on the same base class (returned by patch)', async function (assert) { + assert.expect(15); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch1', { + setup() { + this._super(); + assert.step('patch1.setup'); + }, + fn() { + this._super(); + assert.step('patch1.fn'); + }, + }); + + patch(BaseClass.prototype, 'patch2', { + setup() { + this._super(); + assert.step('patch2.setup'); + }, + fn() { + this._super(); + assert.step('patch2.fn'); + }, + }); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch1.setup', + 'patch2.setup', + 'base.fn', + 'patch1.fn', + 'patch2.fn', + ]); + + unpatch(BaseClass.prototype, 'patch1'); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch2.setup', + 'base.fn', + 'patch2.fn', + ]); + + unpatch(BaseClass.prototype, 'patch2'); + + (new BaseClass()).fn(); + + assert.verifySteps([ + 'base.setup', + 'base.fn', + ]); + }); + + QUnit.test('unpatch twice the same patch name', async function (assert) { + assert.expect(1); + + const A = class {}; + patch(A.prototype, 'patch'); + + unpatch(A.prototype, 'patch'); + assert.throws(() => { + unpatch(A.prototype, 'patch'); + }); + }); + + QUnit.test('patch for specialization', async function (assert) { + assert.expect(1); + + let args = []; + + const A = class { + constructor() { + this.setup(...arguments); + } + setup() { + args = ['A', ...arguments]; + } + }; + + patch(A.prototype, 'patch', { + setup() { + this._super('patch', ...arguments); + }, + }); + + new A('instantiation'); + + assert.deepEqual(args, ['A', 'patch', 'instantiation']); + }); + + QUnit.test('instance fields', async function (assert) { + assert.expect(3); + + const BaseClass = makeBaseClass(assert, false); + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(...arguments); + + this.str += 'patch'; + this.arr.push('patch'); + this.obj.patch = 'patch'; + } + }); + + const instance = new BaseClass(); + assert.strictEqual(instance.str, 'basepatch'); + assert.deepEqual(instance.arr, ['base', 'patch']); + assert.deepEqual(instance.obj, { base: 'base', patch: 'patch' }); + }); + + QUnit.test('call instance method defined in patch', async function (assert) { + assert.expect(4); + + const BaseClass = makeBaseClass(assert, false); + + assert.notOk((new BaseClass()).f); + + patch(BaseClass.prototype, 'patch', { + f() { + assert.step('patch.f'); + }, + }); + + (new BaseClass()).f(); + assert.verifySteps(['patch.f']); + + unpatch(BaseClass.prototype, 'patch'); + + assert.notOk((new BaseClass()).f); + }); + + QUnit.test('class methods', async function (assert) { + assert.expect(7); + + const BaseClass = makeBaseClass(assert); + + BaseClass.staticFn(); + assert.verifySteps(['base.staticFn']); + + patch(BaseClass, 'patch', { + staticFn() { + this._super(); + assert.step('patch.staticFn'); + } + }); + + BaseClass.staticFn(); + assert.verifySteps(['base.staticFn', 'patch.staticFn']); + + unpatch(BaseClass, 'patch'); + + BaseClass.staticFn(); + assert.verifySteps(['base.staticFn']); + }); + + QUnit.test('class fields', async function (assert) { + assert.expect(6); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass, 'patch', { + staticStr: BaseClass.staticStr + 'patch', + staticArr: [...BaseClass.staticArr, 'patch'], + staticObj: {...BaseClass.staticObj, patch: 'patch'}, + }); + + assert.strictEqual(BaseClass.staticStr, 'basepatch'); + assert.deepEqual(BaseClass.staticArr, ['base', 'patch']); + assert.deepEqual(BaseClass.staticObj, { base: 'base', patch: 'patch' }); + + unpatch(BaseClass, 'patch'); + + assert.strictEqual(BaseClass.staticStr, 'base'); + assert.deepEqual(BaseClass.staticArr, ['base']); + assert.deepEqual(BaseClass.staticObj, { base: 'base' }); + }); + + QUnit.test('lazy patch', async function (assert) { + assert.expect(6); + + const BaseClass = makeBaseClass(assert); + const instance = new BaseClass(); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + // will not be called + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + instance.fn(); + assert.verifySteps([ + 'base.setup', + 'base.fn', + 'patch.fn', + ]); + + unpatch(BaseClass.prototype, 'patch'); + + instance.fn(); + assert.verifySteps([ + 'base.fn', + ]); + }); + + QUnit.test('getter', async function (assert) { + assert.expect(2); + + const BaseClass = makeBaseClass(assert, false); + + patch(BaseClass.prototype, 'patch', { + get dynamic() { + return this._super() + 'patch'; + }, + }); + + const instance = new BaseClass(); + assert.strictEqual(instance.dynamic, 'basepatch'); + + unpatch(BaseClass.prototype, 'patch'); + assert.strictEqual(instance.dynamic, 'base'); + }); + + QUnit.test('setter', async function (assert) { + assert.expect(3); + + const BaseClass = makeBaseClass(assert, false); + + patch(BaseClass.prototype, 'patch', { + set dynamic(value) { + this._super('patch:' + value); + }, + }); + + const instance = new BaseClass(); + + assert.strictEqual(instance.dynamic, 'base'); + instance.dynamic = 'patch'; + assert.strictEqual(instance.dynamic, 'patch:patch'); + + unpatch(BaseClass.prototype, 'patch'); + + instance.dynamic = 'base'; + assert.strictEqual(instance.dynamic, 'base'); + }); + + QUnit.test('async function', async function (assert) { + assert.expect(3); + + const BaseClass = makeBaseClass(assert, false); + + patch(BaseClass.prototype, "patch", { + async asyncFn() { + const _super = this._super; + await Promise.resolve(); + await _super(...arguments); + assert.step('patch.asyncFn'); + }, + }); + + const instance = new BaseClass(); + instance.str = "asyncFn"; + await instance.asyncFn(); + + assert.verifySteps([ + 'base.asyncFn', + 'patch.asyncFn', + ]); + }); + + QUnit.test('async function (multiple patches)', async function (assert) { + assert.expect(4); + + const BaseClass = makeBaseClass(assert, false); + + patch(BaseClass.prototype, "patch1", { + async asyncFn() { + const _super = this._super; + await Promise.resolve(); + await _super(...arguments); + // also check this binding + assert.step(`patch1.${this.str}`); + }, + }); + patch(BaseClass.prototype, "patch2", { + async asyncFn() { + const _super = this._super; + await Promise.resolve(); + await _super(...arguments); + // also check this binding + assert.step(`patch2.${this.str}`); + }, + }); + + const instance = new BaseClass(); + instance.str = "asyncFn"; + await instance.asyncFn(); + + assert.verifySteps([ + 'base.asyncFn', + 'patch1.asyncFn', + 'patch2.asyncFn', + ]); + }); + + QUnit.module('inheritance'); + + QUnit.test('inherit a patched class (extends before patch)', async function (assert) { + assert.expect(12); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + ]); + }); + + QUnit.test('inherit a patched class (extends after patch)', async function (assert) { + assert.expect(7); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + (new Extension()).fn(); + + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + ]); + }); + + QUnit.test('patch an inherited class', async function (assert) { + assert.expect(12); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + + patch(Extension.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'patch.setup', + 'base.fn', + 'extension.fn', + 'patch.fn', + ]); + }); + + QUnit.test('patch an inherited patched class', async function (assert) { + assert.expect(9); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + patch(Extension.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.extension.setup'); + }, + fn() { + this._super(); + assert.step('patch.extension.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'patch.extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + 'patch.extension.fn', + ]); + }); + + QUnit.test('unpatch base class', async function (assert) { + assert.expect(12); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + ]); + + unpatch(BaseClass.prototype, 'patch'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + }); + + QUnit.test('unpatch an inherited class', async function (assert) { + assert.expect(12); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + patch(Extension.prototype, 'patch', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'patch.setup', + 'base.fn', + 'extension.fn', + 'patch.fn', + ]); + + unpatch(Extension.prototype, 'patch'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + }); + + QUnit.test('unpatch an inherited class and its base class (unpatch base first)', async function (assert) { + assert.expect(21); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch.BaseClass', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + patch(Extension.prototype, 'patch.Extension', { + setup() { + this._super(); + assert.step('patch.extension.setup'); + }, + fn() { + this._super(); + assert.step('patch.extension.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'patch.extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + 'patch.extension.fn', + ]); + + unpatch(BaseClass.prototype, 'patch.BaseClass'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'patch.extension.setup', + 'base.fn', + 'extension.fn', + 'patch.extension.fn', + ]); + + unpatch(Extension.prototype, 'patch.Extension'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + }); + + QUnit.test('unpatch an inherited class and its base class (unpatch extension first)', async function (assert) { + assert.expect(21); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass.prototype, 'patch.BaseClass', { + setup() { + this._super(); + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + patch(Extension.prototype, 'patch.Extension', { + setup() { + this._super(); + assert.step('patch.extension.setup'); + }, + fn() { + this._super(); + assert.step('patch.extension.fn'); + }, + }); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'patch.extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + 'patch.extension.fn', + ]); + + unpatch(Extension.prototype, 'patch.Extension'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'patch.setup', + 'extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + ]); + + unpatch(BaseClass.prototype, 'patch.BaseClass'); + + (new Extension()).fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'extension.fn', + ]); + }); + + QUnit.test('class methods', async function (assert) { + assert.expect(12); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass { + static staticFn() { + super.staticFn(); + assert.step('extension.staticFn'); + } + } + + patch(BaseClass, 'patch.BaseClass', { + staticFn() { + this._super(); + assert.step('patch.staticFn'); + } + }); + + patch(Extension, 'patch.Extension', { + staticFn() { + this._super(); + assert.step('patch.extension.staticFn'); + } + }); + + Extension.staticFn(); + assert.verifySteps([ + 'base.staticFn', + 'patch.staticFn', + 'extension.staticFn', + 'patch.extension.staticFn', + ]); + + unpatch(BaseClass, 'patch.BaseClass'); + + Extension.staticFn(); + assert.verifySteps([ + 'base.staticFn', + 'extension.staticFn', + 'patch.extension.staticFn', + ]); + + unpatch(Extension, 'patch.Extension'); + + Extension.staticFn(); + assert.verifySteps([ + 'base.staticFn', + 'extension.staticFn', + ]); + }); + + QUnit.test('class fields (patch before inherit)', async function (assert) { + assert.expect(6); + + const BaseClass = makeBaseClass(assert); + + patch(BaseClass, 'patch.BaseClass', { + staticStr: BaseClass.staticStr + 'patch', + staticArr: [...BaseClass.staticArr, 'patch'], + staticObj: {...BaseClass.staticObj, patch: 'patch'}, + }); + + class Extension extends BaseClass {} + Extension.staticStr = BaseClass.staticStr + 'extension'; + Extension.staticArr = [...BaseClass.staticArr, 'extension']; + Extension.staticObj = {...BaseClass.staticObj, extension: 'extension'}; + + assert.strictEqual(Extension.staticStr, 'basepatchextension'); + assert.deepEqual(Extension.staticArr, ['base', 'patch', 'extension']); + assert.deepEqual(Extension.staticObj, + { base: 'base', patch: 'patch', extension: 'extension' }); + + unpatch(BaseClass, 'patch.BaseClass'); + + // /!\ WARNING /!\ + // If inherit comes after the patch then extension will still have + // the patched data when unpatching. + assert.strictEqual(Extension.staticStr, 'basepatchextension'); + assert.deepEqual(Extension.staticArr, ['base', 'patch', 'extension']); + assert.deepEqual(Extension.staticObj, + { base: 'base', patch: 'patch', extension: 'extension' }); + }); + + QUnit.test('class fields (inherit before patch)', async function (assert) { + assert.expect(6); + + const BaseClass = makeBaseClass(assert); + + class Extension extends BaseClass {} + Extension.staticStr = BaseClass.staticStr + 'extension'; + Extension.staticArr = [...BaseClass.staticArr, 'extension']; + Extension.staticObj = {...BaseClass.staticObj, extension: 'extension'}; + + // /!\ WARNING /!\ + // If patch comes after the inherit then extension won't have + // the patched data. + patch(BaseClass, 'patch.BaseClass', { + staticStr: BaseClass.staticStr + 'patch', + staticArr: [...BaseClass.staticArr, 'patch'], + staticObj: {...BaseClass.staticObj, patch: 'patch'}, + }); + + assert.strictEqual(Extension.staticStr, 'baseextension'); + assert.deepEqual(Extension.staticArr, ['base', 'extension']); + assert.deepEqual(Extension.staticObj, + { base: 'base', extension: 'extension' }); + + unpatch(BaseClass, 'patch.BaseClass'); + + assert.strictEqual(Extension.staticStr, 'baseextension'); + assert.deepEqual(Extension.staticArr, ['base', 'extension']); + assert.deepEqual(Extension.staticObj, + { base: 'base', extension: 'extension' }); + }); + + QUnit.test('lazy patch', async function (assert) { + assert.expect(9); + + const BaseClass = makeBaseClass(assert); + class Extension extends BaseClass { + setup() { + super.setup(); + assert.step('extension.setup'); + } + fn() { + super.fn(); + assert.step('extension.fn'); + } + } + + const instance = new Extension(); + + patch(BaseClass.prototype, 'patch', { + setup() { + this._super(); + // will not be called + assert.step('patch.setup'); + }, + fn() { + this._super(); + assert.step('patch.fn'); + }, + }); + + instance.fn(); + assert.verifySteps([ + 'base.setup', + 'extension.setup', + 'base.fn', + 'patch.fn', + 'extension.fn', + ]); + + unpatch(BaseClass.prototype, 'patch'); + + instance.fn(); + assert.verifySteps([ + 'base.fn', + 'extension.fn', + ]); + }); + + QUnit.module('other'); + + QUnit.test('patch an object', async function (assert) { + assert.expect(7); + + const obj = { + var: "obj", + fn() { + assert.step("obj"); + }, + }; + + patch(obj, "patch", { + var: obj.var + "patch", + fn() { + this._super(...arguments); + assert.step("patch"); + } + }); + + assert.strictEqual(obj.var, "objpatch"); + + obj.fn(); + assert.verifySteps([ "obj", "patch" ]); + + unpatch(obj, "patch"); + + assert.strictEqual(obj.var, "obj"); + + obj.fn(); + assert.verifySteps([ "obj" ]); + }); + }); +}); +}); diff --git a/addons/web/static/tests/core/util_tests.js b/addons/web/static/tests/core/util_tests.js index 61ab088d137..6ebed595447 100644 --- a/addons/web/static/tests/core/util_tests.js +++ b/addons/web/static/tests/core/util_tests.js @@ -153,41 +153,6 @@ QUnit.module('core', {}, function () { assert.strictEqual(human_number(-1.012e+43, 2, 2), '-1.01e+43'); }); - QUnit.test('patch a class', function(assert) { - assert.expect(4); - - class Parent { - foo() { - return 'Parent foo'; - } - } - - class Child extends Parent { - bar() { - return 'Child bar'; - } - } - - const removePatch = utils.patch(Child, 'patch', { - foo() { - return this._super() + ' patch foo'; - }, - bar() { - return this._super() + ' patch bar'; - } - }) - - const child = new Child(); - - assert.strictEqual(child.foo(), 'Parent foo patch foo') - assert.strictEqual(child.bar(), 'Child bar patch bar') - - removePatch(); - - assert.strictEqual(child.foo(), 'Parent foo'); - assert.strictEqual(child.bar(), 'Child bar'); - }) - QUnit.test('round_decimals', function (assert) { assert.expect(21); diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js index 54606151e4b..f3424f18502 100644 --- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js +++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js @@ -13,6 +13,7 @@ var RamStorage = require('web.RamStorage'); var relationalFields = require('web.relational_fields'); var testUtils = require('web.test_utils'); var fieldUtils = require('web.field_utils'); +const { patch, unpatch } = require('web.utils'); const cpHelpers = testUtils.controlPanel; var createView = testUtils.createView; @@ -9602,24 +9603,23 @@ QUnit.module('fields', {}, function () { const prom = new Promise(r => { resolveCP = r; }); - ControlPanel.patch('cp_patch_mock', T => - class extends T { - constructor() { - super(...arguments); - owl.hooks.onMounted(() => { - assert.step('mounted'); - }); - owl.hooks.onWillUnmount(() => { - assert.step('willUnmount'); - }); - } - async update() { - // the issue is a race condition, so we manually delay the update to turn it deterministic - await prom; - super.update(...arguments); - } - } - ); + patch(ControlPanel.prototype, 'cp_patch_mock', { + setup() { + this._super(...arguments); + owl.hooks.onMounted(() => { + assert.step('mounted'); + }); + owl.hooks.onWillUnmount(() => { + assert.step('willUnmount'); + }); + }, + async update() { + const _super = this._super.bind(this); + // the issue is a race condition, so we manually delay the update to turn it deterministic + await prom; + _super.update(...arguments); + }, + }); const form = await createView({ View: FormView, @@ -9644,7 +9644,7 @@ QUnit.module('fields', {}, function () { assert.verifySteps([]); - ControlPanel.unpatch('cp_patch_mock'); + unpatch(ControlPanel.prototype, 'cp_patch_mock'); delete fieldRegistry.map.pad_like; form.destroy(); diff --git a/addons/web/static/tests/report/client_action_tests.js b/addons/web/static/tests/report/client_action_tests.js index c537f6c2540..29123183fab 100644 --- a/addons/web/static/tests/report/client_action_tests.js +++ b/addons/web/static/tests/report/client_action_tests.js @@ -4,6 +4,7 @@ odoo.define('web/static/tests/report/client_action_tests', function (require) { const ControlPanel = require('web.ControlPanel'); const ReportClientAction = require('report.client_action'); const testUtils = require("web.test_utils"); + const { patch, unpatch } = require('web.utils'); const { createActionManager, dom, mock } = testUtils; @@ -29,20 +30,18 @@ odoo.define('web/static/tests/report/client_action_tests', function (require) { } }); - ControlPanel.patch('test.ControlPanel', T => { - class ControlPanelPatchTest extends T { - mounted() { - mountCount = mountCount + 1; - this.__uniqueId = mountCount; - assert.step(`mounted ${this.__uniqueId}`); - super.mounted(...arguments); - } - willUnmount() { - assert.step(`willUnmount ${this.__uniqueId}`); - super.mounted(...arguments); - } - } - return ControlPanelPatchTest; + patch(ControlPanel.prototype, 'test.ControlPanel', { + mounted() { + mountCount = mountCount + 1; + this.__uniqueId = mountCount; + assert.step(`mounted ${this.__uniqueId}`); + this.__superMounted = this._super.bind(this); + this.__superMounted(...arguments); + }, + willUnmount() { + assert.step(`willUnmount ${this.__uniqueId}`); + this.__superMounted(...arguments); + }, }); const actionManager = await createActionManager({ actions: [ @@ -103,7 +102,7 @@ odoo.define('web/static/tests/report/client_action_tests', function (require) { 'willUnmount 3', ]); - ControlPanel.unpatch('test.ControlPanel'); + unpatch(ControlPanel.prototype, 'test.ControlPanel'); mock.unpatch(ReportClientAction); }); }); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 0fb6446aa9f..49390b3fc02 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -16,6 +16,7 @@ var mixins = require('web.mixins'); var NotificationService = require('web.NotificationService'); var RamStorage = require('web.RamStorage'); var testUtils = require('web.test_utils'); +const { patch, unpatch } = require('web.utils'); var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); @@ -11226,15 +11227,12 @@ QUnit.module('Views', { let mountedCounterCall = 0; - ControlPanel.patch('test.ControlPanel', T => { - class ControlPanelPatchTest extends T { - mounted() { - mountedCounterCall = mountedCounterCall + 1; - assert.step(`mountedCounterCall-${mountedCounterCall}`); - super.mounted(...arguments); - } - } - return ControlPanelPatchTest; + patch(ControlPanel.prototype, 'test.ControlPanel', { + mounted() { + mountedCounterCall = mountedCounterCall + 1; + assert.step(`mountedCounterCall-${mountedCounterCall}`); + this._super(...arguments); + }, }); const MyListView = ListView.extend({ @@ -11261,7 +11259,7 @@ QUnit.module('Views', { 'mountedCounterCall-1', ]); - ControlPanel.unpatch('test.ControlPanel'); + unpatch(ControlPanel.prototype, 'test.ControlPanel'); list.destroy(); }); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 60982a48392..dec1da40b4e 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -147,7 +147,6 @@ - @@ -739,7 +738,7 @@ - + diff --git a/addons/website/static/tests/dashboard_tests.js b/addons/website/static/tests/dashboard_tests.js index 04d9ce89176..26778ee4070 100644 --- a/addons/website/static/tests/dashboard_tests.js +++ b/addons/website/static/tests/dashboard_tests.js @@ -4,6 +4,7 @@ odoo.define('website/static/tests/dashboard_tests', function (require) { const ControlPanel = require('web.ControlPanel'); const Dashboard = require('website.backend.dashboard'); const testUtils = require("web.test_utils"); +const { patch, unpatch } = require('web.utils'); const { createParent, nextTick, prepareTarget } = testUtils; @@ -13,18 +14,16 @@ QUnit.module('Website Backend Dashboard', { // This test can be removed as soon as we don't mix legacy and owl layers anymore. assert.expect(5); - ControlPanel.patch('test.ControlPanel', T => { - class ControlPanelPatchTest extends T { - mounted() { - assert.step('mounted'); - super.mounted(...arguments); - } - willUnmount() { - assert.step('willUnmount'); - super.mounted(...arguments); - } - } - return ControlPanelPatchTest; + patch(ControlPanel.prototype, 'test.ControlPanel', { + mounted() { + this.__superMounted = this._super.bind(this); + assert.step('mounted'); + this.__superMounted(...arguments); + }, + willUnmount() { + assert.step('willUnmount'); + this.__superMounted(...arguments); + }, }); const params = { @@ -60,7 +59,7 @@ QUnit.module('Website Backend Dashboard', { dashboard.destroy(); assert.verifySteps(['willUnmount']); - ControlPanel.unpatch('test.ControlPanel'); + unpatch(ControlPanel.prototype, 'test.ControlPanel'); }); }); diff --git a/addons/website_livechat/static/src/components/discuss/discuss.js b/addons/website_livechat/static/src/components/discuss/discuss.js index 778b1513730..17ce3bdc07a 100644 --- a/addons/website_livechat/static/src/components/discuss/discuss.js +++ b/addons/website_livechat/static/src/components/discuss/discuss.js @@ -5,24 +5,23 @@ const components = { Discuss: require('mail/static/src/components/discuss/discuss.js'), VisitorBanner: require('website_livechat/static/src/components/visitor_banner/visitor_banner.js'), }; +const { patch } = require('web.utils'); -components.Discuss.patch('website_livechat/static/src/components/discuss/discuss.js', T => - class extends T { +patch(components.Discuss.prototype, 'website_livechat/static/src/components/discuss/discuss.js', { - /** - * @override - */ - _useStoreSelector(props) { - const res = super._useStoreSelector(...arguments); - const thread = res.thread; - const visitor = thread && thread.visitor; - return Object.assign({}, res, { - visitor, - }); - } + /** + * @override + */ + _useStoreSelector(props) { + const res = this._super(...arguments); + const thread = res.thread; + const visitor = thread && thread.visitor; + return Object.assign({}, res, { + visitor, + }); + }, - } -); +}); Object.assign(components.Discuss.components, { VisitorBanner: components.VisitorBanner, diff --git a/addons/website_livechat/static/tests/helpers/mock_models.js b/addons/website_livechat/static/tests/helpers/mock_models.js index bc9f1d3b830..b0da281cbd2 100644 --- a/addons/website_livechat/static/tests/helpers/mock_models.js +++ b/addons/website_livechat/static/tests/helpers/mock_models.js @@ -2,44 +2,43 @@ odoo.define('website_livechat/static/tests/helpers/mock_models.js', function (re 'use strict'; const MockModels = require('mail/static/tests/helpers/mock_models.js'); +const { patch } = require('web.utils'); -MockModels.patch('website_livechat/static/tests/helpers/mock_models.js', T => - class extends T { +patch(MockModels, 'website_livechat/static/tests/helpers/mock_models.js', { - //---------------------------------------------------------------------- - // Public - //---------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- - /** - * @override - */ - static generateData() { - const data = super.generateData(...arguments); - Object.assign(data, { - 'website.visitor': { - fields: { - country_id: { string: "Country", type: 'many2one', relation: 'res.country' }, - display_name: { string: "Display name", type: 'string' }, - // Represent the browsing history of the visitor as a string. - // To ease testing this allows tests to set it directly instead - // of implementing the computation made on server. - // This should normally not be a field. - history: { string: "History", type: 'string'}, - is_connected: { string: "Is connected", type: 'boolean' }, - lang_name: { string: "Language name", type: 'string'}, - partner_id: {string: "partner", type: "many2one", relation: 'res.partner'}, - website_name: { string: "Website name", type: 'string' }, - }, - records: [], + /** + * @override + */ + generateData() { + const data = this._super(...arguments); + Object.assign(data, { + 'website.visitor': { + fields: { + country_id: { string: "Country", type: 'many2one', relation: 'res.country' }, + display_name: { string: "Display name", type: 'string' }, + // Represent the browsing history of the visitor as a string. + // To ease testing this allows tests to set it directly instead + // of implementing the computation made on server. + // This should normally not be a field. + history: { string: "History", type: 'string'}, + is_connected: { string: "Is connected", type: 'boolean' }, + lang_name: { string: "Language name", type: 'string'}, + partner_id: {string: "partner", type: "many2one", relation: 'res.partner'}, + website_name: { string: "Website name", type: 'string' }, }, - }); - Object.assign(data['mail.channel'].fields, { - livechat_visitor_id: { string: "Visitor", type: 'many2one', relation: 'website.visitor' }, - }); - return data; - } - - } -); + records: [], + }, + }); + Object.assign(data['mail.channel'].fields, { + livechat_visitor_id: { string: "Visitor", type: 'many2one', relation: 'website.visitor' }, + }); + return data; + }, + +}); }); diff --git a/addons/website_slides/static/src/components/activity/activity.js b/addons/website_slides/static/src/components/activity/activity.js index 4bd79cddc17..6dc8c58744d 100644 --- a/addons/website_slides/static/src/components/activity/activity.js +++ b/addons/website_slides/static/src/components/activity/activity.js @@ -6,7 +6,7 @@ const components = { }; const { patch } = require('web.utils'); -patch(components.Activity, 'website_slides/static/src/components/activity/activity.js', { +patch(components.Activity.prototype, 'website_slides/static/src/components/activity/activity.js', { //-------------------------------------------------------------------------- // Handlers diff --git a/doc/reference/javascript_reference.rst b/doc/reference/javascript_reference.rst index 8ad49f0c507..e6d0ad9b56f 100644 --- a/doc/reference/javascript_reference.rst +++ b/doc/reference/javascript_reference.rst @@ -607,6 +607,14 @@ to use inheritance, mixins. For these reasons, Odoo decided to use its own class system, inspired by John Resig. The base Class is located in *web.Class*, in the file *class.js*. + +.. note :: + + Note that the custom class system should be avoided for creating new code. It + will be deprecated at some point, and then removed. New classes should use + the standard ES6 class system. + + Creating a subclass ------------------- @@ -2650,3 +2658,158 @@ For more information, look into the `control_panel_renderer.js