diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 8efc341c968..445cd19c128 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -15,6 +15,7 @@ QUnit.module('ActionManager', {
partner: {
fields: {
foo: {string: "Foo", type: "char"},
+ bar: {string: "Bar", type: "many2one", relation: 'partner'},
},
records: [
{id: 1, display_name: "First record", foo: "yop"},
@@ -24,6 +25,16 @@ QUnit.module('ActionManager', {
{id: 5, display_name: "Fifth record", foo: "zoup"},
],
},
+ pony: {
+ fields: {
+ name: {string: 'Name', type: 'char'},
+ },
+ records: [
+ {id: 4, name: 'Twilight Sparkle'},
+ {id: 6, name: 'Applejack'},
+ {id: 9, name: 'Fluttershy'}
+ ],
+ },
};
this.actions = [{
@@ -70,17 +81,24 @@ QUnit.module('ActionManager', {
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']],
}];
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': '',
+ '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';
+ this.actions[3].view_mode = 'form';
+
+ 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);
@@ -259,6 +344,116 @@ QUnit.module('ActionManager', {
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) {
@@ -581,6 +776,160 @@ QUnit.module('ActionManager', {
QUnit.module('Concurrency management');
+ QUnit.test('drop previous actions if possible', function (assert) {
+ assert.expect(6);
+
+ var def = $.Deferred();
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route) {
+ var result = this._super.apply(this, arguments);
+ assert.step(route);
+ if (route === '/web/action/load') {
+ return def.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+ actionManager.doAction(4);
+ actionManager.doAction(8);
+
+ def.resolve();
+
+ // action 4 loads a kanban view first, 6 loads a list view. We want a list
+ assert.strictEqual(actionManager.$('.o_list_view').length, 1,
+ 'there should be a list view in DOM');
+
+ assert.verifySteps([
+ '/web/action/load', // load action 4
+ '/web/action/load', // load action 6
+ '/web/dataset/call_kw/pony', // load views for action 6
+ '/web/dataset/search_read', // search read for list view action 6
+ ]);
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('handle switching view and switching back on slow network', function (assert) {
+ assert.expect(8);
+
+ var def = $.Deferred();
+ var defs = [$.when(), def, $.when()];
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route) {
+ assert.step(route);
+ var result = this._super.apply(this, arguments);
+ if (route === '/web/dataset/search_read') {
+ var def = defs.shift();
+ return def.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+ actionManager.doAction(4);
+
+ // kanban view is loaded, switch to list view
+ $('.o_control_panel .o_cp_switch_list').click();
+
+ // here, list view is not ready yet, because def is not resolved
+ // switch back to kanban view
+ $('.o_control_panel .o_cp_switch_kanban').click();
+
+ // here, we want the kanban view to reload itself, regardless of list view
+ assert.verifySteps([
+ "/web/action/load", // initial load action
+ "/web/dataset/call_kw/partner", // load views
+ "/web/dataset/search_read", // search_read for kanban view
+ "/web/dataset/search_read", // search_read for list view (not resolved yet)
+ "/web/dataset/search_read" // search_read for kanban view reload (not resolved yet)
+ ]);
+
+ // we resolve def => list view is now ready (but we want to ignore it)
+ def.resolve();
+
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
+ "there should be a kanban view in dom");
+ assert.strictEqual(actionManager.$('.o_list_view').length, 0,
+ "there should not be a list view in dom");
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('when an server action takes too much time...', function (assert) {
+ assert.expect(1);
+
+ var def = $.Deferred();
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route) {
+ if (route === '/web/action/run') {
+ return def.then(_.constant(1));
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ actionManager.doAction(2);
+ actionManager.doAction(4);
+
+ def.resolve();
+
+ assert.strictEqual($('.o_control_panel .breadcrumb li.active').text(), 'Partners Action 4',
+ 'action 4 should be loaded');
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('clicking quickly on breadcrumbs...', function (assert) {
+ assert.expect(1);
+
+ var def = $.when();
+
+ 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 def.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // create a situation with 3 breadcrumbs: kanban/form/list
+ actionManager.doAction(4);
+ actionManager.$('.o_kanban_record:first').click();
+ actionManager.doAction(8);
+
+ // now, the next read operations will be deferred (this is the read
+ // operation for the form view reload)
+ def = $.Deferred();
+
+ // click on the breadcrumbs for the form view, then on the kanban view
+ // before the form view is fully reloaded
+ $('.o_control_panel .breadcrumb li:eq(1)').click();
+ $('.o_control_panel .breadcrumb li:eq(0)').click();
+
+ // resolve the form view read
+ def.resolve();
+
+ assert.strictEqual($('.o_control_panel .breadcrumb li.active').text(), 'Partners Action 4',
+ 'action 4 should be loaded and visible');
+
+ actionManager.destroy();
+ });
+
QUnit.test('execute a new action while loading a lazy-loaded controller', function (assert) {
assert.expect(15);
@@ -1558,6 +1907,195 @@ QUnit.module('ActionManager', {
actionManager.destroy();
});
+ QUnit.test('restore previous view state when switching back', function (assert) {
+ assert.expect(5);
+
+ this.actions[2].views.unshift([false, 'graph']);
+ this.archs['partner,false,graph'] = '';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+ actionManager.doAction(3);
+
+ assert.ok($('.o_control_panel .fa-bar-chart-o').hasClass('active'),
+ "bar chart button is active");
+ assert.ok(!$('.o_control_panel .fa-line-chart').hasClass('active'),
+ "line chart button is not active");
+
+ // display line chart
+ $('.o_control_panel .fa-line-chart').click();
+ assert.ok($('.o_control_panel .fa-line-chart').hasClass('active'),
+ "line chart button is now active");
+
+ // switch to kanban and back to graph view
+ $('.o_control_panel .o_cp_switch_kanban').click();
+ assert.strictEqual($('.o_control_panel .fa-line-chart').length, 0,
+ "graph buttons are no longer in control panel");
+
+ $('.o_control_panel .o_cp_switch_graph').click();
+ assert.ok($('.o_control_panel .fa-line-chart').hasClass('active'),
+ "line chart button is still active");
+ actionManager.destroy();
+ });
+
+ QUnit.test('view switcher is properly highlighted in graph view', function (assert) {
+ assert.expect(4);
+
+ // note: this test should be moved to graph tests ?
+
+ this.actions[2].views.splice(1, 1, [false, 'graph']);
+ this.archs['partner,false,graph'] = '';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+ actionManager.doAction(3);
+
+ assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'),
+ "list button in control panel is active");
+ assert.ok(!$('.o_control_panel .o_cp_switch_graph').hasClass('active'),
+ "graph button in control panel is not active");
+
+ // switch to graph view
+ $('.o_control_panel .o_cp_switch_graph').click();
+ assert.ok(!$('.o_control_panel .o_cp_switch_list').hasClass('active'),
+ "list button in control panel is not active");
+ assert.ok($('.o_control_panel .o_cp_switch_graph').hasClass('active'),
+ "graph button in control panel is active");
+ actionManager.destroy();
+ });
+
+ QUnit.test('can interact with search view', function (assert) {
+ assert.expect(2);
+
+ this.archs['partner,false,search'] = ''+
+ ''+
+ '' +
+ ''+
+ '';
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+ actionManager.doAction(3);
+
+ assert.ok(!actionManager.$('.o_list_view').hasClass('o_list_view_grouped'),
+ "list view is not grouped");
+ // open search view sub menus
+ $('.o_control_panel .o_searchview_more').click();
+
+ // open group by dropdown
+ $('.o_control_panel .o_cp_right button:contains(Group By)').click();
+
+ // click on first link
+ $('.o_control_panel .o_group_by_menu a:first').click();
+
+ assert.ok(actionManager.$('.o_list_view').hasClass('o_list_view_grouped'),
+ 'list view is now grouped');
+
+ actionManager.destroy();
+ });
+
+ QUnit.test('can open a many2one external window', function (assert) {
+ // AAB: this test could be merged with 'many2ones in form views' in relational_fields_tests.js
+ assert.expect(8);
+
+ this.data.partner.records[0].bar = 2;
+ this.archs['partner,false,search'] = ''+
+ ''+
+ '' +
+ ''+
+ '';
+ this.archs['partner,false,form'] = '';
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ if (args.method === "get_formview_id") {
+ return $.when(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ actionManager.doAction(3);
+
+ // open first record in form view
+ actionManager.$('.o_data_row:first').click();
+ // click on edit
+ $('.o_control_panel .o_form_button_edit').click();
+
+ // click on external button for m2o
+ actionManager.$('.o_external_button').click();
+ assert.verifySteps([
+ '/web/action/load', // initial load action
+ '/web/dataset/call_kw/partner', // load views
+ '/web/dataset/search_read', // read list view data
+ '/web/dataset/call_kw/partner/read', // read form view data
+ '/web/dataset/call_kw/partner/get_formview_id', // get form view id
+ '/web/dataset/call_kw/partner', // load form view for modal
+ '/web/dataset/call_kw/partner/read' // read data for m2o record
+ ]);
+ actionManager.destroy();
+ });
+
+ QUnit.test('ask for confirmation when leaving a "dirty" view', function (assert) {
+ assert.expect(4);
+
+ var actionManager = createActionManager({
+ actions: this.actions,
+ archs: this.archs,
+ data: this.data,
+ });
+ actionManager.doAction(4);
+
+ // open record in form view
+ actionManager.$('.o_kanban_record:first').click();
+
+ // edit record
+ $('.o_control_panel button.o_form_button_edit').click();
+ actionManager.$('input[name="foo"]').val('pinkypie').trigger('input');
+
+ // go back to kanban view
+ $('.o_control_panel .breadcrumb li:first a').click();
+
+ assert.strictEqual($('.modal .modal-body').text(),
+ "The record has been modified, your changes will be discarded. Do you want to proceed?",
+ "should display a modal dialog to confirm discard action");
+
+ // cancel
+ $('.modal .modal-footer button.btn-default').click();
+
+ assert.strictEqual(actionManager.$('.o_form_view').length, 1,
+ "should still be in form view");
+
+ // go back again to kanban view
+ $('.o_control_panel .breadcrumb li:first a').click();
+
+ // confirm discard
+ $('.modal .modal-footer button.btn-primary').click();
+
+ assert.strictEqual(actionManager.$('.o_form_view').length, 0,
+ "should no longer be in form view");
+ assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
+ "should be in kanban view");
+
+ actionManager.destroy();
+ });
+
QUnit.test('limit set in action is passed to each created controller', function (assert) {
assert.expect(2);