diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/services | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/tests/services')
3 files changed, 590 insertions, 0 deletions
diff --git a/addons/web/static/tests/services/crash_manager_tests.js b/addons/web/static/tests/services/crash_manager_tests.js new file mode 100644 index 00000000..edbc74f2 --- /dev/null +++ b/addons/web/static/tests/services/crash_manager_tests.js @@ -0,0 +1,62 @@ +odoo.define('web.crash_manager_tests', function (require) { + "use strict"; + const CrashManager = require('web.CrashManager').CrashManager; + const Bus = require('web.Bus'); + const testUtils = require('web.test_utils'); + const core = require('web.core'); + const createActionManager = testUtils.createActionManager; + +QUnit.module('Services', {}, function() { + + QUnit.module('CrashManager'); + + QUnit.test("Execute an action and close the RedirectWarning when clicking on the primary button", async function (assert) { + assert.expect(4); + + var dummy_action_name = "crash_manager_tests_dummy_action"; + var dummy_action = function() { + assert.step('do_action'); + }; + core.action_registry.add(dummy_action_name, dummy_action); + + // What we want to test is a do-action triggered by the crashManagerService + // the intercept feature of testUtilsMock is not fit for this, because it is too low in the hierarchy + const bus = new Bus(); + bus.on('do-action', null, payload => { + const { action, options } = payload; + actionManager.doAction(action, options); + }); + + var actionManager = await createActionManager({ + actions: [dummy_action], + services: { + crash_manager: CrashManager, + }, + bus + }); + actionManager.call('crash_manager', 'rpc_error', { + code: 200, + data: { + name: "odoo.exceptions.RedirectWarning", + arguments: [ + "crash_manager_tests_warning_modal_text", + dummy_action_name, + "crash_manager_tests_button_text", + null, + ] + } + }); + await testUtils.nextTick(); + + var modal_selector = 'div.modal:contains("crash_manager_tests_warning_modal_text")'; + assert.containsOnce($, modal_selector, "Warning Modal should be opened"); + + await testUtils.dom.click($(modal_selector).find('button.btn-primary')); + + assert.containsNone($, modal_selector, "Warning Modal should be closed"); + assert.verifySteps(['do_action'], "Warning Modal Primary Button Action should be executed"); + + actionManager.destroy(); + }); +}); +}); diff --git a/addons/web/static/tests/services/data_manager_tests.js b/addons/web/static/tests/services/data_manager_tests.js new file mode 100644 index 00000000..38ee8d71 --- /dev/null +++ b/addons/web/static/tests/services/data_manager_tests.js @@ -0,0 +1,239 @@ +odoo.define('web.data_manager_tests', function (require) { + "use strict"; + + const config = require('web.config'); + const DataManager = require('web.DataManager'); + const MockServer = require('web.MockServer'); + const rpc = require('web.rpc'); + const testUtils = require('web.test_utils'); + + /** + * Create a simple data manager with mocked functions: + * - mockRPC -> rpc.query + * - isDebug -> config.isDebug + * @param {Object} params + * @param {Object} params.archs + * @param {Object} params.data + * @param {Function} params.isDebug + * @param {Function} params.mockRPC + * @returns {DataManager} + */ + function createDataManager({ archs, data, isDebug, mockRPC }) { + const dataManager = new DataManager(); + const server = new MockServer(data, { archs }); + + const serverMethods = { + async load_views({ kwargs, model }) { + const { options, views } = kwargs; + const fields = server.fieldsGet(model); + const fields_views = {}; + for (const [viewId, viewType] of views) { + const arch = archs[[model, viewId || false, viewType].join()]; + fields_views[viewType] = server.fieldsViewGet({ arch, model, viewId }); + } + const result = { fields, fields_views }; + if (options.load_filters) { + result.filters = data['ir.filters'].records.filter(r => r.model_id === model); + } + return result; + }, + async get_filters({ args, model }) { + return data[model].records.filter(r => r.model_id === args[0]); + }, + async create_or_replace({ args }) { + const id = data['ir.filters'].records.reduce((i, r) => Math.max(i, r.id), 0) + 1; + const filter = Object.assign(args[0], { id }); + data['ir.filters'].records.push(filter); + return id; + }, + async unlink({ args }) { + data['ir.filters'].records = data['ir.filters'].records.filter( + r => r.id !== args[0] + ); + return true; + }, + }; + + testUtils.mock.patch(rpc, { + async query({ method }) { + this._super = serverMethods[method].bind(this, ...arguments); + return mockRPC.apply(this, arguments); + }, + }); + testUtils.mock.patch(config, { isDebug }); + + return dataManager; + } + + QUnit.module("Services", { + beforeEach() { + this.archs = { + 'oui,10,kanban': '<kanban/>', + 'oui,20,search': '<search/>', + }; + this.data = { + oui: { fields: {}, records: [] }, + 'ir.filters': { + fields: { + context: { type: "Text", string: "Context" }, + domain: { type: "Text", string: "Domain" }, + model_id: { type: "Selection", string: "Model" }, + name: { type: "Char", string: "Name" }, + }, + records: [{ + id: 2, + context: '{}', + domain: '[]', + model_id: 'oui', + name: "Favorite", + }] + } + }; + this.loadViewsParams = { + model: "oui", + context: {}, + views_descr: [ + [10, 'kanban'], + [20, 'search'], + ], + }; + }, + afterEach() { + testUtils.mock.unpatch(rpc); + testUtils.mock.unpatch(config); + }, + }, function () { + + QUnit.module("Data manager"); + + QUnit.test("Load views with filters (non-debug mode)", async function (assert) { + assert.expect(4); + + const dataManager = createDataManager({ + archs: this.archs, + data: this.data, + isDebug() { + return false; + }, + async mockRPC({ method, model }) { + assert.step([model, method].join('.')); + return this._super(...arguments); + }, + }); + + const firstLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + const secondLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + const filters = await dataManager.load_filters({ modelName: 'oui' }); + + assert.deepEqual(firstLoad, secondLoad, + "query with same params and options should yield the same results"); + assert.deepEqual(firstLoad.search.favoriteFilters, filters, + "load filters should yield the same result as the first load_views' filters"); + assert.verifySteps(['oui.load_views'], + "only load once when not in assets debugging"); + }); + + QUnit.test("Load views with filters (debug mode)", async function (assert) { + assert.expect(6); + + const dataManager = createDataManager({ + archs: this.archs, + data: this.data, + isDebug() { + return true; // assets + }, + async mockRPC({ method, model }) { + assert.step([model, method].join('.')); + return this._super(...arguments); + }, + }); + + const firstLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + const secondLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + const filters = await dataManager.load_filters({ modelName: 'oui' }); + + assert.deepEqual(firstLoad, secondLoad, + "query with same params and options should yield the same results"); + assert.deepEqual(firstLoad.search.favoriteFilters, filters, + "load filters should yield the same result as the first load_views' filters"); + assert.verifySteps([ + 'oui.load_views', + 'oui.load_views', + 'ir.filters.get_filters', + ], "reload each time when in assets debugging"); + }); + + QUnit.test("Cache invalidation and filters addition/deletion", async function (assert) { + assert.expect(10); + + const dataManager = createDataManager({ + archs: this.archs, + data: this.data, + isDebug() { + return false; // Cache only works if 'debug !== assets' + }, + async mockRPC({ method, model }) { + assert.step([model, method].join('.')); + return this._super(...arguments); + }, + }); + + // A few unnecessary 'load_filters' are done in this test to assert + // that the cache invalidation mechanics are working. + let filters; + + const firstLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + // Cache is valid -> should not trigger an RPC + filters = await dataManager.load_filters({ modelName: 'oui' }); + assert.deepEqual(firstLoad.search.favoriteFilters, filters, + "load_filters and load_views.search should return the same filters"); + + const filterId = await dataManager.create_filter({ + context: "{}", + domain: "[]", + model_id: 'oui', + name: "Temp", + }); + // Cache is not valid anymore -> triggers a 'get_filters' + filters = await dataManager.load_filters({ modelName: 'oui' }); + // Cache is valid -> should not trigger an RPC + filters = await dataManager.load_filters({ modelName: 'oui' }); + + assert.strictEqual(filters.length, 2, + "A new filter should have been added"); + assert.ok(filters.find(f => f.id === filterId) === filters[filters.length - 1], + "Create filter should return the id of the last created filter"); + + await dataManager.delete_filter(filterId); + + // Views cache is valid but filters cache is not -> triggers a 'get_filters' + const secondLoad = await dataManager.load_views(this.loadViewsParams, { + load_filters: true, + }); + filters = secondLoad.search.favoriteFilters; + // Filters cache is once again valid -> no RPC + const expectedFilters = await dataManager.load_filters({ modelName: 'oui' }); + + assert.deepEqual(filters, expectedFilters, + "Filters loaded by the load_views should be equal to the result of a load_filters"); + + assert.verifySteps([ + 'oui.load_views', + 'ir.filters.create_or_replace', + 'ir.filters.get_filters', + 'ir.filters.unlink', + 'ir.filters.get_filters', + ], "server should have been called only when needed"); + }); + }); +}); diff --git a/addons/web/static/tests/services/notification_service_tests.js b/addons/web/static/tests/services/notification_service_tests.js new file mode 100644 index 00000000..45b4de51 --- /dev/null +++ b/addons/web/static/tests/services/notification_service_tests.js @@ -0,0 +1,289 @@ +odoo.define('web.notification_tests', function (require) { +"use strict"; + +var AbstractView = require('web.AbstractView'); +var Notification = require('web.Notification'); +var NotificationService = require('web.NotificationService'); + +var testUtils = require('web.test_utils'); +var createView = testUtils.createView; + +var waitCloseNotification = function () { + return new Promise(function (resolve) { + setTimeout(resolve, 1); + }); +} + +QUnit.module('Services', { + beforeEach: function () { + // We need to use a delay above 0 ms because otherwise the notification will close right after it opens + // before we can perform any test. + testUtils.mock.patch(Notification, { + _autoCloseDelay: 1, + _animation: false, + }); + this.viewParams = { + View: AbstractView, + arch: '<fake/>', + data: { + fake_model: { + fields: {}, + record: [], + }, + }, + model: 'fake_model', + services: { + notification: NotificationService, + }, + }; + }, + afterEach: function () { + // The Notification Service has a side effect: it adds a div inside + // document.body. We could implement a cleanup mechanism for services, + // but this seems a little overkill since services are not supposed to + // be destroyed anyway. + $('.o_notification_manager').remove(); + testUtils.mock.unpatch(Notification); + } +}, function () { + QUnit.module('Notification'); + + QUnit.test('Display a warning notification', async function (assert) { + assert.expect(4); + + var view = await createView(this.viewParams); + view.call('notification', 'notify', { + title: 'a', + message: 'b', + }); + await testUtils.nextMicrotaskTick(); + var $notification = $('body .o_notification_manager .o_notification'); + assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '), + "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-lightbulb-o o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> <button type=\"button\" class=\"close o_notification_close\" data-dismiss=\"toast\" aria-label=\"Close\"> <span class=\"d-inline\" aria-hidden=\"true\">×</span> </button> </div> <div class=\"toast-body\"> <div class=\"mr-auto o_notification_content\">b</div> </div>", + "should display notification"); + assert.containsOnce($notification, '.o_notification_close'); + await waitCloseNotification(); + assert.strictEqual($notification.is(':hidden'), true, "should hide the notification"); + assert.strictEqual($('body .o_notification_manager .o_notification').length, 0, "should destroy the notification"); + view.destroy(); + }); + + QUnit.test('Display a danger notification', async function (assert) { + assert.expect(1); + + var view = await createView(this.viewParams); + view.call('notification', 'notify', { + title: 'a', + message: 'b', + type: 'danger' + }); + await testUtils.nextMicrotaskTick(); + var $notification = $('body .o_notification_manager .o_notification'); + assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '), + "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-exclamation o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a</div> <button type=\"button\" class=\"close o_notification_close\" data-dismiss=\"toast\" aria-label=\"Close\"> <span class=\"d-inline\" aria-hidden=\"true\">×</span> </button> </div> <div class=\"toast-body\"> <div class=\"mr-auto o_notification_content\">b</div> </div>", + "should display notification"); + view.destroy(); + }); + + QUnit.test('Display a sticky notification', async function (assert) { + assert.expect(3); + + var view = await createView(this.viewParams); + view.call('notification', 'notify', { + title: 'a', + message: 'b', + sticky: true, + }); + await testUtils.nextTick(); + var $notification = $('body .o_notification_manager .o_notification'); + assert.containsOnce($notification, '.o_notification_close', "should display the close button in notification"); + + assert.strictEqual($notification.is(':hidden'), false, "should not hide the notification automatically"); + await testUtils.dom.click($notification.find('.o_notification_close')); + assert.strictEqual($('body .o_notification_manager .o_notification').length, + 0, "should destroy the notification"); + view.destroy(); + }); + + QUnit.test('Display a notification without title', async function (assert) { + assert.expect(3); + + const view = await createView(this.viewParams); + view.call('notification', 'notify', { + title: false, + message: 'b', + sticky: true, + }); + await testUtils.nextTick(); + const $notification = $('body .o_notification_manager .o_notification'); + assert.containsNone($notification, '.toast-header .o_notification_title'); + assert.containsNone($notification, '.o_notification_icon'); + assert.containsOnce($notification, '.toast-body .o_notification_close'); + + view.destroy(); + }); + + // FIXME skip because the feature is unused and do not understand why the test even worked before + QUnit.skip('Display a simple notification with onClose callback when automatically close', async function (assert) { + assert.expect(2); + + var close = 0; + var view = await createView(this.viewParams); + view.call('notification', 'notify', { + title: 'a', + message: 'b', + onClose: function () { + close++; + } + }); + await testUtils.nextMicrotaskTick(); + view.destroy(); + assert.strictEqual(close, 0, "should wait to call onClose method once"); + await testUtils.nextTick(); + assert.strictEqual(close, 1, "should call onClose method once"); + }); + + QUnit.test('Display a sticky notification with onClose callback', async function (assert) { + assert.expect(2); + + testUtils.mock.unpatch(Notification); + testUtils.mock.patch(Notification, { + _autoCloseDelay: 2500, + _animation: false, + }); + var view = await createView(this.viewParams); + + var close = 0; + view.call('notification', 'notify', { + title: 'a', + message: 'b', + sticky: true, + onClose: function () { + close++; + } + }); + await testUtils.nextMicrotaskTick(); + assert.strictEqual(close, 0, "should wait to call onClose method once"); + testUtils.dom.click($('body .o_notification_manager .o_notification .o_notification_close')); + assert.strictEqual(close, 1, "should call onClose method once"); + view.destroy(); + }); + + QUnit.test('Display a question', async function (assert) { + assert.expect(8); + + var view = await createView(this.viewParams); + function notification (inc) { + return { + title: 'a' + inc, + message: 'b' + inc, + buttons: [ + { + text: 'accept' + inc, + primary: true, + click: function () { + assert.step('accept' + inc); + }, + }, + { + text: 'refuse' + inc, + click: function () { + assert.step('refuse' + inc); + }, + } + ], + onClose: function () { + assert.step('close' + inc); + } + }; + }; + view.call('notification', 'notify', notification(0)); + view.call('notification', 'notify', notification(1)); + view.call('notification', 'notify', notification(2)); + await testUtils.nextTick(); + + var $notification = $('body .o_notification_manager .o_notification'); + assert.containsOnce($notification.eq(0), '.o_notification_close', + "should display the close button in notification"); + assert.strictEqual($notification.html().trim().replace(/\s+/g, ' '), + "<div class=\"toast-header\"> <span class=\"fa fa-2x mr-3 fa-question-circle-o o_notification_icon\" role=\"img\" aria-label=\"Notification undefined\" title=\"Notification undefined\"></span> <div class=\"d-flex align-items-center mr-auto font-weight-bold o_notification_title\">a0</div> <button type=\"button\" class=\"close o_notification_close\" data-dismiss=\"toast\" aria-label=\"Close\"> <span class=\"d-inline\" aria-hidden=\"true\">×</span> </button> </div> <div class=\"toast-body\"> <div class=\"mr-auto o_notification_content\">b0</div> <div class=\"mt-2 o_notification_buttons\"> <button type=\"button\" class=\"btn btn-sm btn-primary\"> <span>accept0</span> </button><button type=\"button\" class=\"btn btn-sm btn-secondary\"> <span>refuse0</span> </button> </div> </div>", + "should display notification"); + + testUtils.dom.click($notification.find('.o_notification_buttons button:contains(accept0)')); + testUtils.dom.click($notification.find('.o_notification_buttons button:contains(refuse1)')); + testUtils.dom.click($notification.eq(2).find('.o_notification_close')); + + assert.strictEqual($notification.is(':hidden'), true, "should hide the notification"); + assert.strictEqual($('body .o_notification_manager .o_notification').length, + 0, "should destroy the notification"); + assert.verifySteps(['accept0', 'refuse1', 'close2']); + view.destroy(); + }); + + QUnit.test('call close notification service', async function (assert) { + assert.expect(2); + + testUtils.mock.unpatch(Notification); + testUtils.mock.patch(Notification, { + _autoCloseDelay: 2500, + _animation: false, + }); + var view = await createView(this.viewParams); + + var close = 0; + var notificationId0 = view.call('notification', 'notify', { + title: 'a', + message: 'b', + onClose: function () { + close++; + } + }); + var notificationId1 = view.call('notification', 'notify', { + title: 'a', + message: 'b', + sticky: true, + onClose: function () { + close++; + } + }); + await testUtils.nextTick(); + + view.call('notification', 'close', notificationId0); + view.call('notification', 'close', notificationId1); + await testUtils.nextTick(); + + assert.strictEqual($('body .o_notification_manager .o_notification').length, 0, "should destroy the notifications"); + assert.strictEqual(close, 2, "should call onClose method twice"); + view.destroy(); + }); + + QUnit.test('Display a custom notification', async function (assert) { + assert.expect(3); + + var Custom = Notification.extend({ + init: function (parent, params) { + this._super.apply(this, arguments); + assert.ok(params.customParams, 'instantiate custom notification'); + }, + start: function () { + var self = this; + return this._super().then(function () { + self.$el.html('Custom'); + }); + }, + }); + + var view = await createView(this.viewParams); + view.call('notification', 'notify', { + Notification: Custom, + customParams: true, + }); + await testUtils.nextMicrotaskTick(); + assert.containsOnce($('body'), '.o_notification_manager .o_notification:contains(Custom)', + "should display the notification"); + view.destroy(); + assert.containsNone($('body'), '.o_notification_manager .o_notification', + "should destroy the notification"); + }); + +});}); |
