summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/services
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/services
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/services')
-rw-r--r--addons/web/static/tests/services/crash_manager_tests.js62
-rw-r--r--addons/web/static/tests/services/data_manager_tests.js239
-rw-r--r--addons/web/static/tests/services/notification_service_tests.js289
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");
+ });
+
+});});