diff options
| author | Aaron Bohy <aab@odoo.com> | 2017-11-17 13:14:02 +0100 |
|---|---|---|
| committer | Aaron Bohy <aab@odoo.com> | 2018-01-16 20:49:19 +0100 |
| commit | 32b8cec5361174c92c45a4975d4e02f1fb448aa2 (patch) | |
| tree | 110bd6986c2da1ed5047002b7ea40c91ea9fa5ed /addons/web/static/tests | |
| parent | 9bee25a9df65f657fef0f2f32a7ad823fdc7d4e1 (diff) | |
[REF] web,*: ActionManager/ViewManager refactoring
This rev. is the first part (out of 3) of the refactoring of the
ActionManager and ViewManager layer of the webclient.
The main changes are:
- there is no ViewManager anymore, its work is now handled by the
ActionManager itself, but isolated in a specific file ; this
eases a lot of things, as the breadcrumbs handling for example.
- the ActionManager code is converted to the new coding principles
and guidelines ; mainly, children widgets communicate with it by
triggering events up, and not by function calls anymore.
- the ActionManager layer is now testable, and a lot of tests have
already been written.
- the code in other addons has been adapted consequently.
What's coming next:
- introduce the 'AbstractAction' Widget, and make client actions
and view controllers inherit from it ; this widget will
implement a common API that could be used uniformly by the
ActionManager (e.g. restore(), canBeLeft(), renderButtons(),
getTitle()...).
- move the ControlPanel handling from the ActionManager to the
AbstractAction.
Diffstat (limited to 'addons/web/static/tests')
| -rw-r--r-- | addons/web/static/tests/chrome/action_manager_mobile_tests.js | 85 | ||||
| -rw-r--r-- | addons/web/static/tests/chrome/action_manager_tests.js | 1667 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/mock_server.js | 21 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/test_utils.js | 89 | ||||
| -rw-r--r-- | addons/web/static/tests/views/form_tests.js | 41 | ||||
| -rw-r--r-- | addons/web/static/tests/views/kanban_tests.js | 14 | ||||
| -rw-r--r-- | addons/web/static/tests/views/list_tests.js | 31 | ||||
| -rw-r--r-- | addons/web/static/tests/views/view_dialogs_tests.js | 2 | ||||
| -rw-r--r-- | addons/web/static/tests/widgets/data_export_tests.js | 4 |
9 files changed, 1889 insertions, 65 deletions
diff --git a/addons/web/static/tests/chrome/action_manager_mobile_tests.js b/addons/web/static/tests/chrome/action_manager_mobile_tests.js new file mode 100644 index 00000000000..c23d7be19f8 --- /dev/null +++ b/addons/web/static/tests/chrome/action_manager_mobile_tests.js @@ -0,0 +1,85 @@ +odoo.define('web.action_manager_mobile_tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); + +var createActionManager = testUtils.createActionManager; + +QUnit.module('ActionManager', { + beforeEach: function () { + this.data = { + partner: { + fields: { + foo: {string: "Foo", type: "char"}, + }, + records: [ + {id: 1, display_name: "First record", foo: "yop"}, + ], + }, + }; + + this.actions = [{ + id: 1, + name: 'Partners Action 1', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'kanban'], [false, 'form']], + }, { + id: 2, + name: 'Partners Action 2', + res_model: 'partner', + type: 'ir.actions.act_window', + views: [[false, 'list'], [false, 'form']], + }]; + + this.archs = { + // kanban views + 'partner,false,kanban': '<kanban><templates><t t-name="kanban-box">' + + '<div class="oe_kanban_global_click"><field name="foo"/></div>' + + '</t></templates></kanban>', + + // list views + 'partner,false,list': '<tree><field name="foo"/></tree>', + + // form views + 'partner,false,form': '<form>' + + '<group>' + + '<field name="display_name"/>' + + '</group>' + + '</form>', + + // search views + 'partner,false,search': '<search><field name="foo" string="Foo"/></search>', + }; + }, +}, function () { + QUnit.test('uses a mobile-friendly view by default (if possible)', function (assert) { + assert.expect(4); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + // should default on a mobile-friendly view (kanban) for action 1 + actionManager.doAction(1); + + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "should not have rendered the list view"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should have rendered the kanban view"); + + // there is no mobile-friendly view for action 2, should use the first one (list) + actionManager.doAction(2); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should have rendered the list view"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "there should be no kanban view in the DOM"); + + actionManager.destroy(); + }); +}); + +}); diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js new file mode 100644 index 00000000000..848d7eb1570 --- /dev/null +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -0,0 +1,1667 @@ +odoo.define('web.action_manager_tests', function (require) { +"use strict"; + +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); +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"}, + }, + records: [ + {id: 1, display_name: "First record", foo: "yop"}, + {id: 2, display_name: "Second record", foo: "blip"}, + {id: 3, display_name: "Third record", foo: "gnap"}, + {id: 4, display_name: "Fourth record", foo: "plop"}, + {id: 5, display_name: "Fifth record", foo: "zoup"}, + ], + }, + }; + + 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', + view_mode: 'form', + views: [[false, 'form']], + }, { + id: 6, + name: 'Partner', + res_id: 2, + res_model: 'partner', + target: 'inline', + type: 'ir.actions.act_window', + view_mode: 'form', + views: [[false, 'form']], + }]; + this.archs = { + // kanban views + 'partner,1,kanban': '<kanban><templates><t t-name="kanban-box">' + + '<div><field name="foo"/></div>' + + '</t></templates></kanban>', + + // list views + 'partner,false,list': '<tree><field name="foo"/></tree>', + 'partner,2,list': '<tree limit="3"><field name="foo"/></tree>', + + // form views + 'partner,false,form': '<form>' + + '<header>' + + '<button name="object" string="Call method" type="object"/>' + + '<button name="4" string="Execute action" type="action"/>' + + '</header>' + + '<group>' + + '<field name="display_name"/>' + + '<field name="foo"/>' + + '</group>' + + '</form>', + + // search views + 'partner,false,search': '<search><field name="foo" string="Foo"/></search>', + }; + }, +}, function () { + QUnit.module('Misc'); + + 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('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.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 = Widget.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(1); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + intercepts: { + push_state: function () { + assert.step('push_state'); + }, + }, + }); + actionManager.loadState({action: 1}); + + assert.verifySteps([]); + + actionManager.destroy(); + }); + + QUnit.module('Concurrency management'); + + QUnit.test('execute a new action while loading a lazy-loaded controller', function (assert) { + assert.expect(15); + + var def; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + assert.step(args.method || route); + if (route === '/web/dataset/search_read' && args.model === 'partner') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + actionManager.loadState({ + action: 4, + id: 2, + view_type: 'form', + }); + + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should display the form view of action 4"); + + // click to go back to Kanban (this request is blocked) + def = $.Deferred(); + $('.o_control_panel .breadcrumb a').click(); + + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should still display the form view of action 4"); + + // execute another action meanwhile (don't block this request) + actionManager.doAction(8, {clear_breadcrumbs: true}); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display action 8"); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "should no longer display the form view"); + + assert.verifySteps([ + '/web/action/load', // load state action 4 + 'load_views', // load state action 4 + 'read', // read the opened record (action 4) + '/web/dataset/search_read', // blocked search read when coming back to Kanban (action 4) + '/web/action/load', // action 8 + 'load_views', // action 8 + '/web/dataset/search_read', // search read action 8 + ]); + + // unblock the switch to Kanban in action 4 + def.resolve(); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should still display action 8"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "should not display the kanban view of action 4"); + + assert.verifySteps([ + '/web/action/load', // load state action 4 + 'load_views', // load state action 4 + 'read', // read the opened record (action 4) + '/web/dataset/search_read', // blocked search read when coming back to Kanban (action 4) + '/web/action/load', // action 8 + 'load_views', // action 8 + '/web/dataset/search_read', // search read action 8 + ]); + + actionManager.destroy(); + }); + + QUnit.test('execute a new action while handling a call_button', function (assert) { + assert.expect(16); + + var self = this; + var def = $.Deferred(); + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + if (route === '/web/dataset/call_button') { + return def.then(_.constant(self.actions[0])); + } + return this._super.apply(this, arguments); + }, + }); + + // execute action 3 and open a record in form view + actionManager.doAction(3); + actionManager.$('.o_list_view .o_data_row:first').click(); + + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should display the form view of action 3"); + + // click on 'Call method' button (this request is blocked) + actionManager.$('.o_form_view button:contains(Call method)').click(); + + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should still display the form view of action 3"); + + // execute another action + actionManager.doAction(8, {clear_breadcrumbs: true}); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display the list view of action 8"); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "should no longer display the form view"); + + assert.verifySteps([ + '/web/action/load', // action 3 + 'load_views', // action 3 + '/web/dataset/search_read', // list for action 3 + 'read', // form for action 3 + 'object', // click on 'Call method' button (this request is blocked) + '/web/action/load', // action 8 + 'load_views', // action 8 + '/web/dataset/search_read', // list for action 8 + ]); + + // unblock the call_button request + def.resolve(); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should still display the list view of action 8"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "should not display action 1"); + + assert.verifySteps([ + '/web/action/load', // action 3 + 'load_views', // action 3 + '/web/dataset/search_read', // list for action 3 + 'read', // form for action 3 + 'object', // click on 'Call method' button (this request is blocked) + '/web/action/load', // action 8 + 'load_views', // action 8 + '/web/dataset/search_read', // list for action 8 + ]); + + actionManager.destroy(); + }); + + QUnit.test('execute a new action while switching to another controller', function (assert) { + assert.expect(15); + + var def; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + assert.step(args.method || route); + if (args.method === 'read') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + }); + + actionManager.doAction(3); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display the list view of action 3"); + + // switch to the form view (this request is blocked) + def = $.Deferred(); + actionManager.$('.o_list_view .o_data_row:first').click(); + + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should still display the list view of action 3"); + + // execute another action meanwhile (don't block this request) + actionManager.doAction(4, {clear_breadcrumbs: true}); + + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should display the kanban view of action 8"); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "should no longer display the list view"); + + assert.verifySteps([ + '/web/action/load', // action 3 + 'load_views', // action 3 + '/web/dataset/search_read', // search read of list view of action 3 + 'read', // read the opened record of action 3 (this request is blocked) + '/web/action/load', // action 4 + 'load_views', // action 4 + '/web/dataset/search_read', // search read action 4 + ]); + + // unblock the switch to the form view in action 3 + def.resolve(); + + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should still display the kanban view of action 8"); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "should not display the form view of action 3"); + + assert.verifySteps([ + '/web/action/load', // action 3 + 'load_views', // action 3 + '/web/dataset/search_read', // search read of list view of action 3 + 'read', // read the opened record of action 3 (this request is blocked) + '/web/action/load', // action 4 + 'load_views', // action 4 + '/web/dataset/search_read', // search read action 4 + ]); + + actionManager.destroy(); + }); + + QUnit.module('Client Actions'); + + QUnit.test('can execute client actions from tag name', function (assert) { + assert.expect(3); + + var ClientAction = Widget.extend({ + className: 'o_client_action_test', + start: function () { + this.$el.text('Hello World'); + }, + }); + core.action_registry.add('HelloWorldTest', ClientAction); + + var actionManager = createActionManager({ + mockRPC: function (route, args) { + assert.step(args.method || route); + return this._super.apply(this, arguments); + } + }); + actionManager.doAction('HelloWorldTest'); + + assert.strictEqual($('.o_control_panel:visible').length, 0, // AAB: global selector until the ControlPanel is moved from ActionManager to the Views + "shouldn't have rendered a control panel"); + 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('client action with control panel', function (assert) { + assert.expect(4); + + var ClientAction = Widget.extend(ControlPanelMixin, { + className: 'o_client_action_test', + start: function () { + this.$el.text('Hello World'); + this.set('title', 'Hello'); // AAB: drop this and replace by getTitle() + }, + }); + core.action_registry.add('HelloWorldTest', ClientAction); + + var actionManager = createActionManager(); + actionManager.doAction('HelloWorldTest'); + + assert.strictEqual($('.o_control_panel:visible').length, 1, + "should have rendered a control panel"); + 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(), 'Hello', + "breadcrumbs should still display the title of the controller"); + assert.strictEqual(actionManager.$('.o_client_action_test').text(), + 'Hello World', "should have correctly rendered the client action"); + + actionManager.destroy(); + delete core.action_registry.map.HelloWorldTest; + }); + + QUnit.test('state is pushed for client actions', function (assert) { + assert.expect(2); + + var ClientAction = Widget.extend(ControlPanelMixin, { + className: 'o_client_action_test', + start: function () { + this.$el.text('Hello World'); + }, + }); + var actionManager = createActionManager({ + intercepts: { + push_state: function () { + assert.step('push state'); + }, + }, + }); + core.action_registry.add('HelloWorldTest', ClientAction); + + actionManager.doAction('HelloWorldTest'); + + assert.verifySteps(['push state']); + + actionManager.destroy(); + delete core.action_registry.map.HelloWorldTest; + }); + + QUnit.module('Server actions'); + + QUnit.test('can execute server actions from db ID', function (assert) { + assert.expect(9); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + if (route === '/web/action/run') { + assert.strictEqual(args.action_id, 2, + "should call the correct server action"); + return $.when(1); // execute action 1 + } + return this._super.apply(this, arguments); + }, + }); + actionManager.doAction(2); + + assert.strictEqual($('.o_control_panel:visible').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', + '/web/action/run', + '/web/action/load', + 'load_views', + '/web/dataset/search_read', + ]); + + actionManager.destroy(); + }); + + QUnit.module('Window Actions'); + + QUnit.test('can execute act_window actions from db ID', 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.doAction(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('can switch between views', function (assert) { + assert.expect(18); + + 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 display the list view"); + + // switch to kanban view + $('.o_control_panel .o_cp_switch_kanban').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "should no longer display the list view"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should display the kanban view"); + + // switch back to list view + $('.o_control_panel .o_cp_switch_list').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display the list view"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "should no longer display the kanban view"); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "should no longer display the list view"); + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should display the form view"); + assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop', + "should have opened the correct record"); + + // go back to list view using the breadcrumbs + $('.o_control_panel .breadcrumb a').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display the list view"); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "should no longer display the form view"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list + '/web/dataset/search_read', // kanban + '/web/dataset/search_read', // list + 'read', // form + '/web/dataset/search_read', // list + ]); + + actionManager.destroy(); + }); + + QUnit.test('breadcrumbs are updated when switching between views', function (assert) { + assert.expect(10); + + 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"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners', + "breadcrumbs should display the display_name of the action"); + + // switch to kanban view + $('.o_control_panel .o_cp_switch_kanban').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should still be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners', + "breadcrumbs should still display the display_name of the action"); + + // switch back to list view + $('.o_control_panel .o_cp_switch_list').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should still be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners', + "breadcrumbs should still display the display_name of the action"); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + 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(), 'First record', + "breadcrumbs should contain the display_name of the opened record"); + + // go back to list view using the breadcrumbs + $('.o_control_panel .breadcrumb a').click(); + 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', + "breadcrumbs should display the display_name of the action"); + + actionManager.destroy(); + }); + + QUnit.test('switch buttons are updated when switching between views', function (assert) { + assert.expect(13); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(3); + + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2, + "should have two switch buttons (list and kanban)"); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button.active').length, 1, + "should have only one active button"); + assert.ok($('.o_control_panel .o_cp_switch_buttons button:first').hasClass('o_cp_switch_list'), + "list switch button should be the first one"); + assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'), + "list should be the active view"); + + // switch to kanban view + $('.o_control_panel .o_cp_switch_kanban').click(); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2, + "should still have two switch buttons (list and kanban)"); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button.active').length, 1, + "should still have only one active button"); + assert.ok($('.o_control_panel .o_cp_switch_buttons button:first').hasClass('o_cp_switch_list'), + "list switch button should still be the first one"); + assert.ok($('.o_control_panel .o_cp_switch_kanban').hasClass('active'), + "kanban should now be the active view"); + + // switch back to list view + $('.o_control_panel .o_cp_switch_list').click(); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2, + "should still have two switch buttons (list and kanban)"); + assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'), + "list should now be the active view"); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 0, + "should not have any switch buttons"); + + // go back to list view using the breadcrumbs + $('.o_control_panel .breadcrumb a').click(); + assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2, + "should have two switch buttons (list and kanban)"); + assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'), + "list should be the active view"); + + actionManager.destroy(); + }); + + QUnit.test('pager is updated when switching between views', function (assert) { + assert.expect(10); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(4); + + assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-5', + "value should be correct for kanban"); + assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5', + "limit should be correct for kanban"); + + // switch to list view + $('.o_control_panel .o_cp_switch_list').click(); + assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-3', + "value should be correct for list"); + assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5', + "limit should be correct for list"); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1', + "value should be correct for form"); + assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '3', + "limit should be correct for form"); + + // go back to list view using the breadcrumbs + $('.o_control_panel .breadcrumb a').click(); + assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-3', + "value should be correct for list"); + assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5', + "limit should be correct for list"); + + // switch back to kanban view + $('.o_control_panel .o_cp_switch_kanban').click(); + assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-5', + "value should be correct for kanban"); + assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5', + "limit should be correct for kanban"); + + actionManager.destroy(); + }); + + QUnit.test('there is no flickering when switching between views', function (assert) { + assert.expect(20); + + var def; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function () { + var result = this._super.apply(this, arguments); + return $.when(def).then(_.constant(result)); + }, + }); + actionManager.doAction(3); + + // switch to kanban view + def = $.Deferred(); + $('.o_control_panel .o_cp_switch_kanban').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should still display the list view"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "shouldn't display the kanban view yet"); + def.resolve(); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "shouldn't display the list view anymore"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should now display the kanban view"); + + // switch back to list view + def = $.Deferred(); + $('.o_control_panel .o_cp_switch_list').click(); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should still display the kanban view"); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "shouldn't display the list view yet"); + def.resolve(); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 0, + "shouldn't display the kanban view anymore"); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should now display the list view"); + + // open a record in form view + def = $.Deferred(); + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should still display the list view"); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "shouldn't display the form view yet"); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should still be one controller in the breadcrumbs"); + def.resolve(); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "should no longer display the list view"); + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should display the form view"); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should be two controllers in the breadcrumbs"); + + // go back to list view using the breadcrumbs + def = $.Deferred(); + $('.o_control_panel .breadcrumb a').click(); + assert.strictEqual(actionManager.$('.o_form_view').length, 1, + "should still display the form view"); + assert.strictEqual(actionManager.$('.o_list_view').length, 0, + "shouldn't display the list view yet"); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should still be two controllers in the breadcrumbs"); + def.resolve(); + assert.strictEqual(actionManager.$('.o_form_view').length, 0, + "should no longer display the form view"); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should display the list view"); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should be one controller in the breadcrumbs"); + + actionManager.destroy(); + }); + + QUnit.test('breadcrumbs are updated when display_name changes', function (assert) { + assert.expect(4); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(3); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + 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(), 'First record', + "breadcrumbs should contain the display_name of the opened record"); + + // switch to edit mode and change the display_name + $('.o_control_panel .o_form_button_edit').click(); + actionManager.$('.o_field_widget[name=display_name]').val('New name').trigger('input'); + $('.o_control_panel .o_form_button_save').click(); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should still be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'New name', + "breadcrumbs should contain the display_name of the opened record"); + + actionManager.destroy(); + }); + + QUnit.test('reload previous controller when discarding a new record', function (assert) { + assert.expect(8); + + 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); + + // create a new record + $('.o_control_panel .o_list_button_add').click(); + assert.strictEqual(actionManager.$('.o_form_view.o_form_editable').length, 1, + "should have opened the form view in edit mode"); + + // discard + $('.o_control_panel .o_form_button_cancel').click(); + assert.strictEqual(actionManager.$('.o_list_view').length, 1, + "should have switched back to the list view"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list + 'default_get', // form + '/web/dataset/search_read', // list + ]); + + actionManager.destroy(); + }); + + QUnit.test('requests for execute_action of type object are handled', function (assert) { + assert.expect(10); + + var self = this; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + assert.step(args.method || route); + if (route === '/web/dataset/call_button') { + assert.deepEqual(args, { + args: [[1], {some_key: 2}], + method: 'object', + model: 'partner', + }, "should call route with correct arguments"); + var record = _.findWhere(self.data.partner.records, {id: args.args[0][0]}); + record.foo = 'value changed'; + return $.when(false); + } + return this._super.apply(this, arguments); + }, + session: {user_context: { + some_key: 2, + }}, + }); + actionManager.doAction(3); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop', + "check initial value of 'yop' field"); + + // click on 'Call method' button (should call an Object method) + actionManager.$('.o_form_view button:contains(Call method)').click(); + assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'value changed', + "'yop' has been changed by the server, and should be updated in the UI"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list for action 3 + 'read', // form for action 3 + 'object', // click on 'Call method' button + 'read', // re-read form view + ]); + + actionManager.destroy(); + }); + + QUnit.test('requests for execute_action of type action are handled', 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.doAction(3); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + + // click on 'Execute action' button (should execute an action) + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should be two parts in the breadcrumbs"); + actionManager.$('.o_form_view button:contains(Execute action)').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3, + "the returned action should have been stacked over the previous one"); + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "the returned action should have been executed"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list for action 3 + 'read', // form for action 3 + '/web/action/load', // click on 'Execute action' button + 'load_views', + '/web/dataset/search_read', // kanban for action 4 + ]); + + actionManager.destroy(); + }); + + QUnit.test('can open different records from a multi record view', 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.doAction(3); + + // open the first record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record', + "breadcrumbs should contain the display_name of the opened record"); + assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop', + "should have opened the correct record"); + + // go back to list view using the breadcrumbs + $('.o_control_panel .breadcrumb a').click(); + + // open the second record in form view + actionManager.$('.o_list_view .o_data_row:nth(1)').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Second record', + "breadcrumbs should contain the display_name of the opened record"); + assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'blip', + "should have opened the correct record"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + '/web/dataset/search_read', // list + 'read', // form + '/web/dataset/search_read', // list + 'read', // form + ]); + + actionManager.destroy(); + }); + + QUnit.test('limit set in action is passed to each created controller', function (assert) { + assert.expect(2); + + _.findWhere(this.actions, {id: 3}).limit = 2; + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(3); + + assert.strictEqual(actionManager.$('.o_data_row').length, 2, + "should only display 2 record"); + + // switch to kanban view + $('.o_control_panel .o_cp_switch_kanban').click(); + + assert.strictEqual(actionManager.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, + "should only display 2 record"); + + actionManager.destroy(); + }); + + QUnit.test('go back to a previous action using the breadcrumbs', function (assert) { + assert.expect(10); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(3); + + // open a record in form view + actionManager.$('.o_list_view .o_data_row:first').click(); + 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(), 'First record', + "breadcrumbs should contain the display_name of the opened record"); + + // push another action on top of the first one, and come back to the form view + actionManager.doAction(4); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3, + "there should be three controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners Action 4', + "breadcrumbs should contain the name of the current action"); + // go back using the breadcrumbs + $('.o_control_panel .breadcrumb a:nth(1)').click(); + 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(), 'First record', + "breadcrumbs should contain the display_name of the opened record"); + + // push again the other action on top of the first one, and come back to the list view + actionManager.doAction(4); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3, + "there should be three controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners Action 4', + "breadcrumbs should contain the name of the current action"); + // go back using the breadcrumbs + $('.o_control_panel .breadcrumb a:first').click(); + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners', + "breadcrumbs should contain the name of the current action"); + + actionManager.destroy(); + }); + + QUnit.module('Actions in target="new"'); + + QUnit.test('can execute act_window actions in target="new"', function (assert) { + assert.expect(7); + + 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(5); + + assert.strictEqual($('.o_technical_modal .o_form_view').length, 1, + "should have rendered a form view in a modal"); + assert.ok($('.o_technical_modal .modal-body').hasClass('o_act_window'), + "modal-body element should have classname 'o_act_window'"); + assert.ok($('.o_technical_modal .o_form_view').hasClass('o_form_editable'), + "form view should be in edit mode"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + 'default_get', + ]); + + actionManager.destroy(); + }); + + QUnit.test('footer buttons are moved to the dialog footer', function (assert) { + assert.expect(3); + + this.archs['partner,false,form'] = '<form>' + + '<field name="display_name"/>' + + '<footer>' + + '<button string="Create" type="object" class="infooter"/>' + + '</footer>' + + '</form>'; + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + actionManager.doAction(5); + + assert.strictEqual($('.o_technical_modal .modal-body button.infooter').length, 0, + "the button should not be in the body"); + assert.strictEqual($('.o_technical_modal .modal-footer button.infooter').length, 1, + "the button should be in the footer"); + assert.strictEqual($('.o_technical_modal .modal-footer button').length, 1, + "the modal footer should only contain one button"); + + actionManager.destroy(); + }); + + QUnit.module('Actions in target="inline"'); + + QUnit.test('form views for actions in target="inline" open in edit mode', 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.doAction(6); + + assert.strictEqual(actionManager.$('.o_form_view.o_form_editable').length, 1, + "should have rendered a form view in edit mode"); + + assert.verifySteps([ + '/web/action/load', + 'load_views', + 'read', + ]); + + actionManager.destroy(); + }); + + QUnit.module('Actions in target="fullscreen"'); + + QUnit.test('correctly execute act_window actions in target="fullscreen"', function (assert) { + assert.expect(7); + + this.actions[0].target = 'fullscreen'; + 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); + }, + intercepts: { + toggle_fullscreen: function () { + assert.step('toggle_fullscreen'); + }, + }, + }); + actionManager.doAction(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', + 'toggle_fullscreen', + ]); + + actionManager.destroy(); + }); + + QUnit.module('"ir.actions.act_window_close" actions'); + + QUnit.test('close the currently opened dialog', function (assert) { + assert.expect(2); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + // execute an action in target="new" + actionManager.doAction(5); + assert.strictEqual($('.o_technical_modal .o_form_view').length, 1, + "should have rendered a form view in a modal"); + + // execute an 'ir.actions.act_window_close' action + actionManager.doAction({ + type: 'ir.actions.act_window_close', + }); + assert.strictEqual($('.o_technical_modal').length, 0, + "should have closed the modal"); + + actionManager.destroy(); + }); + + QUnit.test('execute "on_close" only if there is no dialog to close', function (assert) { + assert.expect(3); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + }); + + // execute an action in target="new" + actionManager.doAction(5); + + var options = { + on_close: assert.step.bind(assert, 'on_close'), + }; + // execute an 'ir.actions.act_window_close' action + // should not call 'on_close' as there is a dialog to close + actionManager.doAction({type: 'ir.actions.act_window_close'}, options); + + assert.verifySteps([]); + + // execute again an 'ir.actions.act_window_close' action + // should call 'on_close' as there is no dialog to close + actionManager.doAction({type: 'ir.actions.act_window_close'}, options); + + assert.verifySteps(['on_close']); + + actionManager.destroy(); + }); +}); + +}); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 4bc58dd097d..dbe1a2c49e0 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -12,6 +12,7 @@ var MockServer = Class.extend({ * @constructor * @param {Object} data * @param {Object} options + * @param {Object[]} [options.actions=[]] * @param {Object} [options.archs={}] dict of archs with keys being strings like * 'model,id,viewType' * @param {boolean} [options.debug=false] logs RPCs if set to true @@ -47,6 +48,7 @@ var MockServer = Class.extend({ this.currentDate = options.currentDate || moment().format("YYYY-MM-DD"); + this.actions = options.actions || []; this.archs = options.archs || {}; }, @@ -432,6 +434,23 @@ var MockServer = Class.extend({ return modelFields; }, /** + * Simulate a call to the '/web/action/load' route + * + * @private + * @param {Object} kwargs + * @param {integer} kwargs.action_id + * @returns {Object} + */ + _mockLoadAction: function (kwargs) { + var action = _.findWhere(this.actions, {id: parseInt(kwargs.action_id)}); + if (!action) { + // when the action doesn't exist, the real server doesn't crash, it + // simply returns false + console.warn("No action found for ID " + event.data.actionID); + } + return action || false; + }, + /** * Simulate a 'load_views' operation * * @param {string} model @@ -943,7 +962,7 @@ var MockServer = Class.extend({ _performRpc: function (route, args) { switch (route) { case '/web/action/load': - return $.when(this._mockLoadAction(args)); + return $.when(this._mockLoadAction(args.kwargs)); case '/web/dataset/search_read': return $.when(this._mockSearchReadController(args)); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index e049ba968b2..afeb579eb02 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -10,9 +10,11 @@ odoo.define('web.test_utils', function (require) { * instance of a view, appended in the dom, ready to be tested. */ +var ActionManager = require('web.ActionManager'); var ajax = require('web.ajax'); var basic_fields = require('web.basic_fields'); var config = require('web.config'); +var ControlPanel = require('web.ControlPanel'); var core = require('web.core'); var dom = require('web.dom'); var session = require('web.session'); @@ -59,6 +61,58 @@ function observe(widget) { } /** + * create and return an instance of ActionManager with all rpcs going through a + * mock method using the data, actions and archs objects as sources. + * + * @param {Object} [params] + * @param {Object} [params.actions] the actions given to the mock server + * @param {Object} [params.archs] this archs given to the mock server + * @param {Object} [params.data] the business data given to the mock server + * @param {boolean} [params.debug] + * @param {function} [params.mockRPC] + * @returns {ActionManager} + */ +var createActionManager = function (params) { + params = params || {}; + var $target = $('#qunit-fixture'); + if (params.debug) { + $target = $('body'); + } + + var widget = new Widget(); + // when 'document' addon is installed, the sidebar does a 'search_read' on + // model 'ir_attachment' each time a record is open, so we monkey-patch + // 'mockRPC' to mute those RPCs, so that the tests can be written uniformly, + // whether or not 'document' is installed + var mockRPC = params.mockRPC; + _.extend(params, { + mockRPC: function (route, args) { + if (args.model === 'ir.attachment') { + return $.when([]); + } + if (mockRPC) { + return mockRPC.apply(this, arguments); + } + return this._super.apply(this, arguments); + }, + }); + addMockEnvironment(widget, _.defaults(params, {debounce: false})); + widget.appendTo($target); + widget.$el.addClass('o_web_client'); + + var userContext = params.context && params.context.user_context || {}; + var actionManager = new ActionManager(widget, userContext); + var originalDestroy = ActionManager.prototype.destroy; + actionManager.destroy = function () { + actionManager.destroy = originalDestroy; + widget.destroy(); + }; + actionManager.appendTo(widget.$el); + + return actionManager; +}; + +/** * create a view synchronously. This method uses the createAsyncView method. * Most views are synchronous, so the deferred can be resolved immediately and * this method will work. @@ -87,8 +141,7 @@ function createView(params) { * * It returns the instance of the view, properly created, with all rpcs going * through a mock method using the data object as source, and already loaded/ - * started (with a do_search). The buttons/pager should also be created, if - * appropriate. + * started. * * Most views can be tested synchronously (@see createView), but some view have * external dependencies (like lazy loaded libraries). In that case, it is @@ -145,9 +198,9 @@ function createAsyncView(params) { // reproduce the DOM environment of views var $web_client = $('<div>').addClass('o_web_client').prependTo($target); - var $control_panel = $('<div>').addClass('o_control_panel').appendTo($web_client); + var controlPanel = new ControlPanel(widget); + controlPanel.appendTo($web_client); var $content = $('<div>').addClass('o_content').appendTo($web_client); - var $view_manager = $('<div>').addClass('o_view_manager_content').appendTo($content); return view.getController(widget).then(function (view) { // override the view's 'destroy' so that it calls 'destroy' on the widget @@ -159,29 +212,21 @@ function createAsyncView(params) { widget.destroy(); $('#qunit-fixture').off('DOMNodeInserted.removeSRC'); }; + + // link the view to the control panel + view.set_cp_bus(controlPanel.get_bus()); + // render the view in a fragment as they must be able to render correctly // without being in the DOM var fragment = document.createDocumentFragment(); return view.appendTo(fragment).then(function () { - dom.append($view_manager, fragment, { + dom.append($content, fragment, { callbacks: [{widget: view}], in_DOM: true, }); view.$el.on('click', 'a', function (ev) { ev.preventDefault(); }); - }).then(function () { - var $buttons = $('<div>'); - view.renderButtons($buttons); - $buttons.contents().appendTo($control_panel); - - var $sidebar = $('<div>'); - view.renderSidebar($sidebar); - $sidebar.contents().appendTo($control_panel); - - var $pager = $('<div>'); - view.renderPager($pager); - $pager.contents().appendTo($control_panel); return view; }); @@ -250,6 +295,7 @@ function addMockEnvironment(widget, params) { console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url); } var mockServer = new Server(params.data, { + actions: params.actions, archs: params.archs, currentDate: params.currentDate, debug: params.debug, @@ -336,6 +382,14 @@ function addMockEnvironment(widget, params) { } }); + intercept(widget, 'load_action', function (event) { + mockServer.performRpc('/web/action/load', { + kwargs: {action_id: event.data.actionID}, + }).then(function (action) { + event.data.on_success(action); + }); + }); + intercept(widget, "load_views", function (event) { mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, { args: [], @@ -617,6 +671,7 @@ return $.when( }, 0); return { addMockEnvironment: addMockEnvironment, + createActionManager: createActionManager, createAsyncView: createAsyncView, createModel: createModel, createParent: createParent, diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 9f2a9b68698..06cc046df83 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -1451,7 +1451,7 @@ QUnit.module('Views', { '<field name="foo"/>' + '</sheet>' + '</form>', - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, res_id: 1, mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'ir.attachment') { @@ -1597,7 +1597,7 @@ QUnit.module('Views', { '<field name="foo"/>' + '</form>', res_id: 1, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'ir.attachment') { return $.when([]); @@ -1628,7 +1628,7 @@ QUnit.module('Views', { '<field name="foo"/>' + '</form>', res_id: 1, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'ir.attachment') { return $.when([]); @@ -1659,7 +1659,7 @@ QUnit.module('Views', { '</footer>' + '</form>', res_id: 1, - viewOptions: {footer_to_buttons: true}, + viewOptions: {footerToButtons: true}, }); assert.ok(form.$buttons.find('button.infooter').length, "footer button should be in footer"); @@ -2050,7 +2050,7 @@ QUnit.module('Views', { data: this.data, arch: '<form string="Partners"><field name="date"></field></form>', intercepts: { - switch_to_previous_view: function (event) { + history_back: function () { form.update({}, {reload: false}); } }, @@ -2086,9 +2086,9 @@ QUnit.module('Views', { return this._super.apply(this, arguments); }, intercepts: { - switch_to_previous_view: function (event) { + history_back: function () { assert.ok(true, "should have sent correct event"); - // simulate the response from the view manager, in the case + // simulate the response from the action manager, in the case // where we have only one active view (the form). If there // was another view, we would have switched to that view // instead @@ -2126,7 +2126,7 @@ QUnit.module('Views', { data: this.data, arch: '<form string="Partners"><field name="foo"></field></form>', intercepts: { - switch_to_previous_view: function () { + history_back: function () { assert.ok(true, "should have sent correct event"); } } @@ -2155,9 +2155,9 @@ QUnit.module('Views', { data: this.data, arch: '<form string="Partners"><field name="foo"></field></form>', intercepts: { - switch_to_previous_view: function (event) { + history_back: function () { assert.ok(true, "should have sent correct event"); - // simulate the response from the view manager, in the case + // simulate the response from the action manager, in the case // where we have only one active view (the form). If there // was another view, we would have switched to that view // instead @@ -2197,7 +2197,7 @@ QUnit.module('Views', { data: this.data, arch: '<form string="Partners"><field name="foo"></field></form>', res_id: 1, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'ir.attachment') { return $.when([]); @@ -2542,7 +2542,7 @@ QUnit.module('Views', { viewOptions: { ids: [1, 2, 4], index: 0, - sidebar: true, + hasSidebar: true, }, res_id: 1, mockRPC: function (route, args) { @@ -2586,7 +2586,7 @@ QUnit.module('Views', { viewOptions: { ids: [1], index: 0, - sidebar: true, + hasSidebar: true, }, res_id: 1, mockRPC: function (route, args) { @@ -2602,15 +2602,14 @@ QUnit.module('Views', { form.sidebar.$('button.o_dropdown_toggler_btn').click(); form.sidebar.$('a:contains(Delete)').click(); - testUtils.intercept(form, 'do_action', function (event) { - assert.strictEqual(event.data.action, 'history_back', - "should trigger an history back action"); + testUtils.intercept(form, 'history_back', function () { + assert.step('history_back'); }); assert.strictEqual($('.modal').length, 1, 'a confirm modal should be displayed'); $('.modal .modal-footer button.btn-primary').click(); assert.strictEqual($('.modal').length, 0, 'no confirm modal should be displayed'); - assert.verifySteps(['read', 'unlink']); + assert.verifySteps(['read', 'unlink', 'history_back']); form.destroy(); }); @@ -4287,7 +4286,7 @@ QUnit.module('Views', { '</form>', res_id: 1, viewOptions: { - footer_to_buttons: true, + footerToButtons: true, mode: 'edit', }, }); @@ -4326,7 +4325,7 @@ QUnit.module('Views', { '</form>', res_id: 1, viewOptions: { - footer_to_buttons: true, + footerToButtons: true, }, }); @@ -4684,7 +4683,7 @@ QUnit.module('Views', { print: [], }, viewOptions: { - sidebar: true, + hasSidebar: true, }, mockRPC: function (route, args) { if (route === '/web/action/load') { @@ -5943,7 +5942,7 @@ QUnit.module('Views', { '<field name="display_name"/>' + '</form>', res_id: 1, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, mockRPC: function (route, args) { if (args.method === 'search_read' && args.model === 'ir.attachment') { // rpcs done by the sidebar diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index eed80410e7f..5b34ed413c5 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -164,7 +164,7 @@ QUnit.module('Views', { groupBy: ['bar'], intercepts: { env_updated: function (event) { - assert.deepEqual(event.data.ids, envIDs, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, @@ -384,7 +384,7 @@ QUnit.module('Views', { groupBy: ['bar'], intercepts: { env_updated: function (event) { - assert.strictEqual(event.data.ids.length, nbRecords, + assert.strictEqual(event.data.env.ids.length, nbRecords, "should update the env with the records ids"); }, } @@ -666,7 +666,7 @@ QUnit.module('Views', { }, intercepts: { switch_view: function (event) { - assert.deepEqual(event.data, { + assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), { mode: 'readonly', model: 'partner', res_id: 1, @@ -733,7 +733,7 @@ QUnit.module('Views', { }, intercepts: { env_updated: function (event) { - assert.deepEqual(event.data.ids, envIDs, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, @@ -977,7 +977,7 @@ QUnit.module('Views', { groupBy: ['product_id'], intercepts: { env_updated: function (event) { - assert.deepEqual(envIDs, event.data.ids, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, @@ -1659,7 +1659,7 @@ QUnit.module('Views', { }, intercepts: { env_updated: function (event) { - assert.deepEqual(event.data.ids, envIDs, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, @@ -1823,7 +1823,7 @@ QUnit.module('Views', { }, intercepts:{ env_updated: function (event) { - assert.deepEqual(event.data.ids, envIDs, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 33b44948fca..b86a820196e 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -150,7 +150,7 @@ QUnit.module('Views', { View: ListView, model: 'foo', data: this.data, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, arch: '<tree delete="0"><field name="foo"/></tree>', }); @@ -261,7 +261,7 @@ QUnit.module('Views', { '<field name="foo"/>' + '<field name="m2o" invisible="1"/>' + '</tree>', - mockRPC: function (route, args) { + mockRPC: function (route) { assert.step(_.last(route.split('/'))); return this._super.apply(this, arguments); }, @@ -765,7 +765,7 @@ QUnit.module('Views', { View: ListView, model: 'foo', data: this.data, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, arch: '<tree><field name="foo"/></tree>', }); @@ -795,7 +795,7 @@ QUnit.module('Views', { View: ListView, model: 'foo', data: this.data, - viewOptions: {sidebar: true}, + viewOptions: {hasSidebar: true}, arch: '<tree><field name="foo"/></tree>', mockRPC: function (route) { if (route === '/web/dataset/call_kw/ir.attachment/search_read') { @@ -1686,7 +1686,7 @@ QUnit.module('Views', { "'switch_view' event has been triggered"); }, env_updated: function (event) { - assert.deepEqual(event.data.ids, envIDs, + assert.deepEqual(event.data.env.ids, envIDs, "should notify the environment with the correct ids"); }, }, @@ -2031,7 +2031,7 @@ QUnit.module('Views', { '<field name="bar"/>' + '</tree>', intercepts: { - warning: function (ev) { + warning: function () { warnings++; }, }, @@ -2070,14 +2070,13 @@ QUnit.module('Views', { arch: '<tree><field name="name"/></tree>', }); - testUtils.intercept(list, "switch_view", function (event) { - assert.deepEqual(event.data, { - 'model': 'event', - 'res_id': '2-20170808020000', - 'view_type': 'form', - 'mode': 'readonly' - }, - "should trigger switch to the form view for the record virtual id"); + testUtils.intercept(list, 'switch_view', function (event) { + assert.deepEqual(_.pick(event.data, 'mode', 'model', 'res_id', 'view_type'), { + mode: 'readonly', + model: 'event', + res_id: '2-20170808020000', + view_type: 'form', + }, "should trigger a switch_view event to the form view for the record virtual id"); }); list.$('td:contains(virtual)').click(); @@ -2092,7 +2091,7 @@ QUnit.module('Views', { model: 'foo', data: this.data, arch: '<tree editable="bottom"><field name="foo"/></tree>', - mockRPC: function (route, args) { + mockRPC: function (route) { assert.step(route); return this._super.apply(this, arguments); }, @@ -2220,7 +2219,7 @@ QUnit.module('Views', { print: [], }, viewOptions: { - sidebar: true, + hasSidebar: true, }, }); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index b2dc9c5ab0e..08ddd2c91c2 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -51,7 +51,7 @@ QUnit.module('Views', { }); testUtils.intercept(parent, 'env_updated', function () { - throw new Error("The environment should not be propagated to the view manager"); + throw new Error("The environment should not be propagated to the action manager"); }); diff --git a/addons/web/static/tests/widgets/data_export_tests.js b/addons/web/static/tests/widgets/data_export_tests.js index d251e3dea15..192944ce3b5 100644 --- a/addons/web/static/tests/widgets/data_export_tests.js +++ b/addons/web/static/tests/widgets/data_export_tests.js @@ -53,7 +53,7 @@ QUnit.module('widgets', { data: this.data, arch: '<tree><field name="foo"/></tree>', viewOptions: { - sidebar: true, + hasSidebar: true, }, mockRPC: function (route) { if (route === '/web/export/formats') { @@ -144,7 +144,7 @@ QUnit.module('widgets', { data: this.data, arch: '<tree><field name="foo"/></tree>', viewOptions: { - sidebar: true, + hasSidebar: true, }, mockRPC: function (route) { if (route === '/web/export/formats') { |
