odoo.define('web.action_manager_tests', function (require) { "use strict"; var ReportClientAction = require('report.client_action'); var NotificationService = require('web.NotificationService'); var AbstractAction = require('web.AbstractAction'); var AbstractStorageService = require('web.AbstractStorageService'); var BasicFields = require('web.basic_fields'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); var ListController = require('web.ListController'); var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin'); var RamStorage = require('web.RamStorage'); var ReportService = require('web.ReportService'); var SessionStorageService = require('web.SessionStorageService'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); var createActionManager = testUtils.createActionManager; QUnit.module('ActionManager', { beforeEach: function () { this.data = { partner: { fields: { foo: {string: "Foo", type: "char"}, bar: {string: "Bar", type: "many2one", relation: 'partner'}, }, records: [ {id: 1, display_name: "First record", foo: "yop", bar: 2}, {id: 2, display_name: "Second record", foo: "blip", bar: 1}, {id: 3, display_name: "Third record", foo: "gnap", bar: 1}, {id: 4, display_name: "Fourth record", foo: "plop", bar: 2}, {id: 5, display_name: "Fifth record", foo: "zoup", bar: 2}, ], }, pony: { fields: { name: {string: 'Name', type: 'char'}, }, records: [ {id: 4, name: 'Twilight Sparkle'}, {id: 6, name: 'Applejack'}, {id: 9, name: 'Fluttershy'} ], }, }; this.actions = [{ id: 1, name: 'Partners Action 1', res_model: 'partner', type: 'ir.actions.act_window', views: [[1, 'kanban']], }, { id: 2, type: 'ir.actions.server', }, { id: 3, name: 'Partners', res_model: 'partner', type: 'ir.actions.act_window', views: [[false, 'list'], [1, 'kanban'], [false, 'form']], }, { id: 4, name: 'Partners Action 4', res_model: 'partner', type: 'ir.actions.act_window', views: [[1, 'kanban'], [2, 'list'], [false, 'form']], }, { id: 5, name: 'Create a Partner', res_model: 'partner', target: 'new', type: 'ir.actions.act_window', views: [[false, 'form']], }, { id: 6, name: 'Partner', res_id: 2, res_model: 'partner', target: 'inline', type: 'ir.actions.act_window', views: [[false, 'form']], }, { id: 7, name: "Some Report", report_name: 'some_report', report_type: 'qweb-pdf', type: 'ir.actions.report', }, { id: 8, name: 'Favorite Ponies', res_model: 'pony', type: 'ir.actions.act_window', views: [[false, 'list'], [false, 'form']], }, { id: 9, name: 'A Client Action', tag: 'ClientAction', type: 'ir.actions.client', }]; this.archs = { // kanban views 'partner,1,kanban': '' + '
' + '
', // list views 'partner,false,list': '', 'partner,2,list': '', 'pony,false,list': '', // form views 'partner,false,form': '
' + '
' + '
' + '' + '' + '' + '' + '
', 'pony,false,form': '
' + '' + '', // search views 'partner,false,search': '', 'partner,1,search': '' + '' + '', 'pony,false,search': '', }; }, }, function () { QUnit.module('Misc'); QUnit.test('breadcrumbs and actions with target inline', function (assert) { assert.expect(3); this.actions[3].views = [[false, 'form']]; this.actions[3].target = 'inline'; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); actionManager.doAction(4); assert.ok(!$('.o_control_panel').is(':visible'), "control panel should not be visible"); actionManager.doAction(1, {clear_breadcrumbs: true}); assert.ok($('.o_control_panel').is(':visible'), "control panel should now be visible"); assert.strictEqual($('.o_control_panel .breadcrumb').text(), "Partners Action 1", "should have only one current action visible in breadcrumbs"); actionManager.destroy(); }); QUnit.test('no widget memory leaks when doing some action stuff', function (assert) { assert.expect(1); var delta = 0; testUtils.patch(Widget, { init: function () { delta++; this._super.apply(this, arguments); }, destroy: function () { delta--; this._super.apply(this, arguments); }, }); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); actionManager.doAction(8); var n = delta; actionManager.doAction(4); // kanban view is loaded, switch to list view $('.o_control_panel .o_cp_switch_list').click(); // open a record in form view actionManager.$('.o_list_view .o_data_row:first').click(); // go back to action 7 in breadcrumbs $('.o_control_panel .breadcrumb a:first').click(); assert.strictEqual(delta, n, "should have properly destroyed all other widgets"); actionManager.destroy(); testUtils.unpatch(Widget); }); QUnit.test('no widget memory leaks when executing actions in dialog', function (assert) { assert.expect(1); var delta = 0; testUtils.patch(Widget, { init: function () { delta++; this._super.apply(this, arguments); }, destroy: function () { if (!this.isDestroyed()) { delta--; } this._super.apply(this, arguments); }, }); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); var n = delta; actionManager.doAction(5); actionManager.doAction({type: 'ir.actions.act_window_close'}); assert.strictEqual(delta, n, "should have properly destroyed all widgets"); actionManager.destroy(); testUtils.unpatch(Widget); }); QUnit.test('no memory leaks when executing an action while switching view', function (assert) { assert.expect(1); var def; var delta = 0; testUtils.patch(Widget, { init: function () { delta += 1; this._super.apply(this, arguments); }, destroy: function () { delta -= 1; this._super.apply(this, arguments); }, }); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'read') { return $.when(def).then(_.constant(result)); } return result; }, }); actionManager.doAction(4); var n = delta; actionManager.doAction(3, {clear_breadcrumbs: true}); // switch to the form view (this request is blocked) def = $.Deferred(); actionManager.$('.o_list_view .o_data_row:first').click(); // execute another action meanwhile (don't block this request) actionManager.doAction(4, {clear_breadcrumbs: true}); // unblock the switch to the form view in action 3 def.resolve(); assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed"); actionManager.destroy(); testUtils.unpatch(Widget); }); QUnit.test('no memory leaks when executing an action while loading views', function (assert) { assert.expect(1); var def; var delta = 0; testUtils.patch(Widget, { init: function () { delta += 1; this._super.apply(this, arguments); }, destroy: function () { delta -= 1; this._super.apply(this, arguments); }, }); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'load_views') { return $.when(def).then(_.constant(result)); } return result; }, }); // execute action 4 to know the number of widgets it instantiates actionManager.doAction(4); var n = delta; // execute a first action (its 'load_views' RPC is blocked) def = $.Deferred(); actionManager.doAction(3, {clear_breadcrumbs: true}); // execute another action meanwhile (and unlock the RPC) actionManager.doAction(4, {clear_breadcrumbs: true}); def.resolve(); assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed"); actionManager.destroy(); testUtils.unpatch(Widget); }); QUnit.test('no memory leaks when executing an action while loading data of default view', function (assert) { assert.expect(1); var def; var delta = 0; testUtils.patch(Widget, { init: function () { delta += 1; this._super.apply(this, arguments); }, destroy: function () { delta -= 1; this._super.apply(this, arguments); }, }); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route) { var result = this._super.apply(this, arguments); if (route === '/web/dataset/search_read') { return $.when(def).then(_.constant(result)); } return result; }, }); // execute action 4 to know the number of widgets it instantiates actionManager.doAction(4); var n = delta; // execute a first action (its 'search_read' RPC is blocked) def = $.Deferred(); actionManager.doAction(3, {clear_breadcrumbs: true}); // execute another action meanwhile (and unlock the RPC) actionManager.doAction(4, {clear_breadcrumbs: true}); def.resolve(); assert.strictEqual(n, delta, "all widgets of action 3 should have been destroyed"); actionManager.destroy(); testUtils.unpatch(Widget); }); QUnit.test('action with "no_breadcrumbs" set to true', function (assert) { assert.expect(2); _.findWhere(this.actions, {id: 4}).context = {no_breadcrumbs: true}; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); actionManager.doAction(3); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, "there should be one controller in the breadcrumbs"); // push another action flagged with 'no_breadcrumbs=true' actionManager.doAction(4); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 0, "the breadcrumbs should be empty"); actionManager.destroy(); }); QUnit.test('on_reverse_breadcrumb handler is correctly called', function (assert) { assert.expect(3); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); // execute action 3 and open a record in form view actionManager.doAction(3); actionManager.$('.o_list_view .o_data_row:first').click(); // execute action 4 without 'on_reverse_breadcrumb' handler, then go back actionManager.doAction(4); $('.o_control_panel .breadcrumb a:first').click(); assert.verifySteps([]); // execute action 4 with an 'on_reverse_breadcrumb' handler, then go back actionManager.doAction(4, { on_reverse_breadcrumb: function () { assert.step('on_reverse_breadcrumb'); } }); $('.o_control_panel .breadcrumb a:first').click(); assert.verifySteps(['on_reverse_breadcrumb']); actionManager.destroy(); }); QUnit.test('handles "history_back" event', function (assert) { assert.expect(2); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, }); actionManager.doAction(4); actionManager.doAction(3); actionManager.trigger_up('history_back'); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, "there should be one controller in the breadcrumbs"); assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4', "breadcrumbs should display the display_name of the action"); actionManager.destroy(); }); QUnit.test('stores and restores scroll position', function (assert) { assert.expect(7); var left; var top; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { getScrollPosition: function (ev) { assert.step('getScrollPosition'); ev.data.callback({left: left, top: top}); }, scrollTo: function (ev) { assert.step('scrollTo left ' + ev.data.left + ', top ' + ev.data.top); }, }, }); // execute a first action and simulate a scroll assert.step('execute action 3'); actionManager.doAction(3); left = 50; top = 100; // execute a second action (in which we don't scroll) assert.step('execute action 4'); actionManager.doAction(4); // go back using the breadcrumbs assert.step('go back to action 3'); $('.o_control_panel .breadcrumb a').click(); assert.verifySteps([ 'execute action 3', 'execute action 4', 'getScrollPosition', // of action 3, before leaving it 'go back to action 3', 'getScrollPosition', // of action 4, before leaving it 'scrollTo left 50, top 100', // restore scroll position of action 3 ]); actionManager.destroy(); }); QUnit.module('Push State'); QUnit.test('properly push state', function (assert) { assert.expect(3); var stateDescriptions = [ {action: 4, model: "partner", title: "Partners Action 4", view_type: "kanban"}, {action: 8, model: "pony", title: "Favorite Ponies", view_type: "list"}, {action: 8, id: 4, model: "pony", title: "Twilight Sparkle", view_type: "form"}, ]; var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { push_state: function (event) { var descr = stateDescriptions.shift(); assert.deepEqual(_.extend({}, event.data.state), descr, "should notify the environment of new state"); }, }, }); actionManager.doAction(4); actionManager.doAction(8); actionManager.$('tr.o_data_row:first').click(); actionManager.destroy(); }); QUnit.test('push state after action is loaded, not before', function (assert) { assert.expect(5); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { push_state: function () { assert.step('push_state'); }, }, mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, }); actionManager.doAction(4); assert.verifySteps([ '/web/action/load', '/web/dataset/call_kw/partner', '/web/dataset/search_read', 'push_state' ]); actionManager.destroy(); }); QUnit.test('do not push state for actions in target=new', function (assert) { assert.expect(3); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { push_state: function () { assert.step('push_state'); }, }, }); actionManager.doAction(4); assert.verifySteps(['push_state']); actionManager.doAction(5); assert.verifySteps(['push_state']); actionManager.destroy(); }); QUnit.test('do not push state when action fails', function (assert) { assert.expect(4); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { push_state: function () { assert.step('push_state'); }, }, mockRPC: function (route, args) { if (args.method === 'read') { // this is the rpc to load form view return $.Deferred().reject(); } return this._super.apply(this, arguments); }, }); actionManager.doAction(8); assert.verifySteps(['push_state']); actionManager.$('tr.o_data_row:first').click(); assert.verifySteps(['push_state']); // we make sure here that the list view is still in the dom assert.strictEqual(actionManager.$('.o_list_view').length, 1, "there should still be a list view in dom"); actionManager.destroy(); }); QUnit.module('Load State'); QUnit.test('should not crash on invalid state', function (assert) { assert.expect(2); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ res_model: 'partner', // the valid key for the model is 'model', not 'res_model' }); assert.strictEqual(actionManager.$el.text(), '', "should display nothing"); assert.verifySteps([]); actionManager.destroy(); }); QUnit.test('properly load client actions', function (assert) { assert.expect(2); var ClientAction = AbstractAction.extend({ className: 'o_client_action_test', start: function () { this.$el.text('Hello World'); }, }); core.action_registry.add('HelloWorldTest', ClientAction); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ action: 'HelloWorldTest', }); assert.strictEqual(actionManager.$('.o_client_action_test').text(), 'Hello World', "should have correctly rendered the client action"); assert.verifySteps([]); actionManager.destroy(); delete core.action_registry.map.HelloWorldTest; }); QUnit.test('properly load act window actions', function (assert) { assert.expect(6); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ action: 1, }); assert.strictEqual($('.o_control_panel').length, 1, "should have rendered a control panel"); assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, "should have rendered a kanban view"); assert.verifySteps([ '/web/action/load', 'load_views', '/web/dataset/search_read', ]); actionManager.destroy(); }); QUnit.test('properly load records', function (assert) { assert.expect(5); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ id: 2, model: 'partner', }); assert.strictEqual(actionManager.$('.o_form_view').length, 1, "should have rendered a form view"); assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Second record', "should have opened the second record"); assert.verifySteps([ 'load_views', 'read', ]); actionManager.destroy(); }); QUnit.test('load requested view for act window actions', function (assert) { assert.expect(6); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ action: 3, view_type: 'kanban', }); assert.strictEqual(actionManager.$('.o_list_view').length, 0, "should not have rendered a list view"); assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, "should have rendered a kanban view"); assert.verifySteps([ '/web/action/load', 'load_views', '/web/dataset/search_read', ]); actionManager.destroy(); }); QUnit.test('lazy load multi record view if mono record one is requested', function (assert) { assert.expect(11); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.loadState({ action: 3, id: 2, view_type: 'form', }); assert.strictEqual(actionManager.$('.o_list_view').length, 0, "should not have rendered a list view"); assert.strictEqual(actionManager.$('.o_form_view').length, 1, "should have rendered a form view"); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, "there should be two controllers in the breadcrumbs"); assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Second record', "breadcrumbs should contain the display_name of the opened record"); // go back to Lst $('.o_control_panel .breadcrumb a').click(); assert.strictEqual(actionManager.$('.o_list_view').length, 1, "should now display the list view"); assert.strictEqual(actionManager.$('.o_form_view').length, 0, "should not display the form view anymore"); assert.verifySteps([ '/web/action/load', 'load_views', 'read', // read the opened record '/web/dataset/search_read', // search read when coming back to List ]); actionManager.destroy(); }); QUnit.test('change the viewType of the current action', function (assert) { assert.expect(13); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); actionManager.doAction(3); assert.strictEqual(actionManager.$('.o_list_view').length, 1, "should have rendered a list view"); // switch to kanban view actionManager.loadState({ action: 3, view_type: 'kanban', }); assert.strictEqual(actionManager.$('.o_list_view').length, 0, "should not display the list view anymore"); assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, "should have switched to the kanban view"); // switch to form view, open record 4 actionManager.loadState({ action: 3, id: 4, view_type: 'form', }); assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, "should not display the kanban view anymore"); assert.strictEqual(actionManager.$('.o_form_view').length, 1, "should have switched to the form view"); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, "there should be two controllers in the breadcrumbs"); assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Fourth record', "should have opened the requested record"); // verify steps to ensure that the whole action hasn't been re-executed // (if it would have been, /web/action/load and load_views would appear // several times) assert.verifySteps([ '/web/action/load', 'load_views', '/web/dataset/search_read', // list view '/web/dataset/search_read', // kanban view 'read', // form view ]); actionManager.destroy(); }); QUnit.test('change the id of the current action', function (assert) { assert.expect(11); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, mockRPC: function (route, args) { assert.step(args.method || route); return this._super.apply(this, arguments); }, }); // execute action 3 and open the first record in a form view actionManager.doAction(3); actionManager.$('.o_list_view .o_data_row:first').click(); assert.strictEqual(actionManager.$('.o_form_view').length, 1, "should have rendered a form view"); assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record', "should have opened the first record"); // switch to record 4 actionManager.loadState({ action: 3, id: 4, view_type: 'form', }); assert.strictEqual(actionManager.$('.o_form_view').length, 1, "should still display the form view"); assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, "there should be two controllers in the breadcrumbs"); assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Fourth record', "should have switched to the requested record"); // verify steps to ensure that the whole action hasn't been re-executed // (if it would have been, /web/action/load and load_views would appear // twice) assert.verifySteps([ '/web/action/load', 'load_views', '/web/dataset/search_read', // list view 'read', // form view, record 1 'read', // form view, record 4 ]); actionManager.destroy(); }); QUnit.test('should not push a loaded state', function (assert) { assert.expect(3); var actionManager = createActionManager({ actions: this.actions, archs: this.archs, data: this.data, intercepts: { push_state: function () { assert.step('push_state'); }, }, }); actionManager.loadState({action: 3}); assert.verifySteps([], "should not push the loaded state"); actionManager.$('tr.o_data_row:first').click(); assert.verifySteps(['push_state'], "should push the state of it changes afterwards"); actionManager.destroy(); }); QUnit.test('should not push a loaded state of a client action', function (assert) { assert.expect(4); var ClientAction = Widget.extend({ init: function (parent, action, options) { this._super.apply(this, arguments); this.controllerID = options.controllerID; }, start: function () { var self = this; var $button = $('