odoo.define('web.action_manager_tests', function (require) {
"use strict";
var ActionManager = require('web.ActionManager');
var ReportClientAction = require('report.client_action');
var Notification = require('web.Notification');
var NotificationService = require('web.NotificationService');
var AbstractAction = require('web.AbstractAction');
var AbstractStorageService = require('web.AbstractStorageService');
var BasicFields = require('web.basic_fields');
var core = require('web.core');
var ListController = require('web.ListController');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var RamStorage = require('web.RamStorage');
var ReportService = require('web.ReportService');
var SessionStorageService = require('web.SessionStorageService');
var testUtils = require('web.test_utils');
var WarningDialog = require('web.CrashManager').WarningDialog;
var Widget = require('web.Widget');
var createActionManager = testUtils.createActionManager;
const cpHelpers = testUtils.controlPanel;
const { xml } = owl.tags;
QUnit.module('ActionManager', {
beforeEach: function () {
this.data = {
partner: {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "many2one", relation: 'partner'},
o2m: {string: "One2Many", type: "one2many", relation: 'partner', relation_field: 'bar'},
},
records: [
{id: 1, display_name: "First record", foo: "yop", bar: 2, o2m: [2, 3]},
{id: 2, display_name: "Second record", foo: "blip", bar: 1, o2m: [1, 4, 5]},
{id: 3, display_name: "Third record", foo: "gnap", bar: 1, o2m: []},
{id: 4, display_name: "Fourth record", foo: "plop", bar: 2, o2m: []},
{id: 5, display_name: "Fifth record", foo: "zoup", bar: 2, o2m: []},
],
},
pony: {
fields: {
name: {string: 'Name', type: 'char'},
},
records: [
{id: 4, name: 'Twilight Sparkle'},
{id: 6, name: 'Applejack'},
{id: 9, name: 'Fluttershy'}
],
},
};
this.actions = [{
id: 1,
name: 'Partners Action 1',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[1, 'kanban']],
}, {
id: 2,
type: 'ir.actions.server',
}, {
id: 3,
name: 'Partners',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [1, 'kanban'], [false, 'form']],
}, {
id: 4,
name: 'Partners Action 4',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[1, 'kanban'], [2, 'list'], [false, 'form']],
}, {
id: 5,
name: 'Create a Partner',
res_model: 'partner',
target: 'new',
type: 'ir.actions.act_window',
views: [[false, 'form']],
}, {
id: 6,
name: 'Partner',
res_id: 2,
res_model: 'partner',
target: 'inline',
type: 'ir.actions.act_window',
views: [[false, 'form']],
}, {
id: 7,
name: "Some Report",
report_name: 'some_report',
report_type: 'qweb-pdf',
type: 'ir.actions.report',
}, {
id: 8,
name: 'Favorite Ponies',
res_model: 'pony',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
}, {
id: 9,
name: 'A Client Action',
tag: 'ClientAction',
type: 'ir.actions.client',
}, {
id: 10,
type: 'ir.actions.act_window_close',
}, {
id: 11,
name: "Another Report",
report_name: 'another_report',
report_type: 'qweb-pdf',
type: 'ir.actions.report',
close_on_report_download: true,
}, {
id: 12,
name: "Some HTML Report",
report_name: 'some_report',
report_type: 'qweb-html',
type: 'ir.actions.report',
}];
this.archs = {
// kanban views
'partner,1,kanban': '' +
'
' +
' ',
// list views
'partner,false,list': ' ',
'partner,2,list': ' ',
'pony,false,list': ' ',
// form views
'partner,false,form': '
',
'pony,false,form': '',
// search views
'partner,false,search': ' ',
'partner,1,search': '' +
' ' +
' ',
'pony,false,search': ' ',
};
},
}, function () {
QUnit.module('Misc');
QUnit.test('breadcrumbs and actions with target inline', async function (assert) {
assert.expect(3);
this.actions[3].views = [[false, 'form']];
this.actions[3].target = 'inline';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(4);
assert.ok(!$('.o_control_panel').is(':visible'),
"control panel should not be visible");
await 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', async function (assert) {
assert.expect(1);
var delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
delta--;
this._super.apply(this, arguments);
},
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(8);
var n = delta;
await actionManager.doAction(4);
// kanban view is loaded, switch to list view
await cpHelpers.switchView(actionManager, 'list');
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// go back to action 7 in breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
assert.strictEqual(delta, n,
"should have properly destroyed all other widgets");
actionManager.destroy();
testUtils.mock.unpatch(Widget);
});
QUnit.test('no widget memory leaks when executing actions in dialog', async function (assert) {
assert.expect(1);
var delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
delta--;
}
this._super.apply(this, arguments);
},
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
var n = delta;
await actionManager.doAction(5);
await actionManager.doAction({type: 'ir.actions.act_window_close'});
assert.strictEqual(delta, n,
"should have properly destroyed all widgets");
actionManager.destroy();
testUtils.mock.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while switching view', async function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = await 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 Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
await actionManager.doAction(4);
var n = delta;
await actionManager.doAction(3, {clear_breadcrumbs: true});
// switch to the form view (this request is blocked)
def = testUtils.makeTestPromise();
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// execute another action meanwhile (don't block this request)
await actionManager.doAction(4, {clear_breadcrumbs: true});
// unblock the switch to the form view in action 3
def.resolve();
await testUtils.nextTick();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.mock.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while loading views', async function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'load_views') {
return Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
// execute action 4 to know the number of widgets it instantiates
await actionManager.doAction(4);
var n = delta;
// execute a first action (its 'load_views' RPC is blocked)
def = testUtils.makeTestPromise();
actionManager.doAction(3, {clear_breadcrumbs: true});
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4, {clear_breadcrumbs: true});
def.resolve();
await testUtils.nextTick();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.mock.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while loading data of default view', async function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.mock.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
return Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
// execute action 4 to know the number of widgets it instantiates
await actionManager.doAction(4);
var n = delta;
// execute a first action (its 'search_read' RPC is blocked)
def = testUtils.makeTestPromise();
actionManager.doAction(3, {clear_breadcrumbs: true});
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4, {clear_breadcrumbs: true});
def.resolve();
await testUtils.nextTick();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.mock.unpatch(Widget);
});
QUnit.test('action with "no_breadcrumbs" set to true', async function (assert) {
assert.expect(2);
_.findWhere(this.actions, {id: 4}).context = {no_breadcrumbs: true};
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
// push another action flagged with 'no_breadcrumbs=true'
await actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 0,
"the breadcrumbs should be empty");
actionManager.destroy();
});
QUnit.test('on_reverse_breadcrumb handler is correctly called', async function (assert) {
assert.expect(3);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute action 3 and open a record in form view
await actionManager.doAction(3);
testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// execute action 4 without 'on_reverse_breadcrumb' handler, then go back
await actionManager.doAction(4);
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
assert.verifySteps([]);
// execute action 4 with an 'on_reverse_breadcrumb' handler, then go back
await actionManager.doAction(4, {
on_reverse_breadcrumb: function () {
assert.step('on_reverse_breadcrumb');
}
});
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
assert.verifySteps(['on_reverse_breadcrumb']);
actionManager.destroy();
});
QUnit.test('handles "history_back" event', async function (assert) {
assert.expect(2);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(4);
await actionManager.doAction(3);
actionManager.trigger_up('history_back');
await testUtils.nextTick();
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners Action 4',
"breadcrumbs should display the display_name of the action");
actionManager.destroy();
});
QUnit.test('stores and restores scroll position', async function (assert) {
assert.expect(7);
var left;
var top;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
getScrollPosition: function (ev) {
assert.step('getScrollPosition');
ev.data.callback({left: left, top: top});
},
scrollTo: function (ev) {
assert.step('scrollTo left ' + ev.data.left + ', top ' + ev.data.top);
},
},
});
// execute a first action and simulate a scroll
assert.step('execute action 3');
await actionManager.doAction(3);
left = 50;
top = 100;
// execute a second action (in which we don't scroll)
assert.step('execute action 4');
await actionManager.doAction(4);
// go back using the breadcrumbs
assert.step('go back to action 3');
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.verifySteps([
'execute action 3',
'execute action 4',
'getScrollPosition', // of action 3, before leaving it
'go back to action 3',
'getScrollPosition', // of action 4, before leaving it
'scrollTo left 50, top 100', // restore scroll position of action 3
]);
actionManager.destroy();
});
QUnit.test('executing an action with target != "new" closes all dialogs', async function (assert) {
assert.expect(4);
this.archs['partner,false,form'] = '';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view');
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view');
await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first'));
assert.containsOnce(document.body, '.modal .o_form_view');
await actionManager.doAction(1); // target != 'new'
assert.containsNone(document.body, '.modal');
actionManager.destroy();
});
QUnit.test('executing an action with target "new" does not close dialogs', async function (assert) {
assert.expect(4);
this.archs['partner,false,form'] = '' +
'' +
' ' +
' ' +
' ' +
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view');
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view');
await testUtils.dom.click(actionManager.$('.o_form_view .o_data_row:first'));
assert.containsOnce(document.body, '.modal .o_form_view');
await actionManager.doAction(5); // target 'new'
assert.containsN(document.body, '.modal .o_form_view', 2);
actionManager.destroy();
});
QUnit.test('executing a window action with onchange warning does not hide it', async function (assert) {
assert.expect(2);
this.archs['partner,false,form'] = `
`;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return Promise.resolve({
value: {},
warning: {
title: "Warning",
message: "Everything is alright",
type: 'dialog',
},
});
}
return this._super.apply(this, arguments);
},
intercepts: {
warning: function (event) {
new WarningDialog(actionManager, {
title: event.data.title,
}, event.data).open();
},
},
});
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_button_add'));
assert.containsOnce(
$,
'.modal.o_technical_modal.show',
"Warning modal should be opened");
await testUtils.dom.click($('.modal.o_technical_modal.show button.close'));
assert.containsNone(
$,
'.modal.o_technical_modal.show',
"Warning modal should be closed");
actionManager.destroy();
});
QUnit.module('Push State');
QUnit.test('properly push state', async 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 = await 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");
},
},
});
await actionManager.doAction(4);
await actionManager.doAction(8);
await testUtils.dom.click(actionManager.$('tr.o_data_row:first'));
actionManager.destroy();
});
QUnit.test('push state after action is loaded, not before', async function (assert) {
assert.expect(5);
var actionManager = await 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);
},
});
await 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', async function (assert) {
assert.expect(3);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
});
await actionManager.doAction(4);
assert.verifySteps(['push_state']);
await actionManager.doAction(5);
assert.verifySteps([]);
actionManager.destroy();
});
QUnit.test('do not push state when action fails', async function (assert) {
assert.expect(4);
var actionManager = await 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 Promise.reject();
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(8);
assert.verifySteps(['push_state']);
await testUtils.dom.click(actionManager.$('tr.o_data_row:first'));
assert.verifySteps([]);
// we make sure here that the list view is still in the dom
assert.containsOnce(actionManager, '.o_list_view',
"there should still be a list view in dom");
actionManager.destroy();
});
QUnit.module('Load State');
QUnit.test('should not crash on invalid state', async function (assert) {
assert.expect(2);
var actionManager = await 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);
},
});
await 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', async function (assert) {
assert.expect(2);
var ClientAction = AbstractAction.extend({
start: function () {
this.$el.text('Hello World');
this.$el.addClass('o_client_action_test');
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = await 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);
},
});
await 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', async function (assert) {
assert.expect(6);
var actionManager = await 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);
},
});
await actionManager.loadState({
action: 1,
});
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.containsOnce(actionManager, '.o_kanban_view',
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('properly load records', async function (assert) {
assert.expect(5);
var actionManager = await 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);
},
});
await actionManager.loadState({
id: 2,
model: 'partner',
});
assert.containsOnce(actionManager, '.o_form_view',
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Second record',
"should have opened the second record");
assert.verifySteps([
'load_views',
'read',
]);
actionManager.destroy();
});
QUnit.test('properly load default record', async function (assert) {
assert.expect(5);
var actionManager = await 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);
},
});
await actionManager.loadState({
action: 3,
id: "", // might happen with bbq and id=& in URL
model: 'partner',
view_type: 'form',
});
assert.containsOnce(actionManager, '.o_form_view',
"should have rendered a form view");
assert.verifySteps([
'/web/action/load',
'load_views',
'onchange',
]);
actionManager.destroy();
});
QUnit.test('load requested view for act window actions', async function (assert) {
assert.expect(6);
var actionManager = await 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);
},
});
await actionManager.loadState({
action: 3,
view_type: 'kanban',
});
assert.containsNone(actionManager, '.o_list_view',
"should not have rendered a list view");
assert.containsOnce(actionManager, '.o_kanban_view',
"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', async function (assert) {
assert.expect(11);
var actionManager = await 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);
},
});
await actionManager.loadState({
action: 3,
id: 2,
view_type: 'form',
});
assert.containsNone(actionManager, '.o_list_view',
"should not have rendered a list view");
assert.containsOnce(actionManager, '.o_form_view',
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Second record',
"breadcrumbs should contain the display_name of the opened record");
// go back to Lst
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsOnce(actionManager, '.o_list_view',
"should now display the list view");
assert.containsNone(actionManager, '.o_form_view',
"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('lazy load multi record view with previous action', async function (assert) {
assert.expect(6);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(4);
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 contain the display_name of the opened record");
await actionManager.doAction(3, {
resID: 2,
viewType: 'form',
});
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3,
"there should be three controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4PartnersSecond record',
"the breadcrumb elements should be correctly ordered");
// go back to List
await testUtils.dom.click($('.o_control_panel .breadcrumb a:last'));
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4Partners',
"the breadcrumb elements should be correctly ordered");
actionManager.destroy();
});
QUnit.test('lazy loaded multi record view with failing mono record one', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'read') {
return Promise.reject();
}
return this._super.apply(this, arguments);
},
});
await actionManager.loadState({
action: 3,
id: 2,
view_type: 'form',
}).then(function () {
assert.ok(false, 'should not resolve the deferred');
}).catch(function () {
assert.ok(true, 'should reject the deferred');
});
assert.containsNone(actionManager, '.o_form_view');
assert.containsNone(actionManager, '.o_list_view');
await actionManager.doAction(1);
assert.containsOnce(actionManager, '.o_kanban_view');
actionManager.destroy();
});
QUnit.test('change the viewType of the current action', async function (assert) {
assert.expect(13);
var actionManager = await 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);
},
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view',
"should have rendered a list view");
// switch to kanban view
await actionManager.loadState({
action: 3,
view_type: 'kanban',
});
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view anymore");
assert.containsOnce(actionManager, '.o_kanban_view',
"should have switched to the kanban view");
// switch to form view, open record 4
await actionManager.loadState({
action: 3,
id: 4,
view_type: 'form',
});
assert.containsNone(actionManager, '.o_kanban_view',
"should not display the kanban view anymore");
assert.containsOnce(actionManager, '.o_form_view',
"should have switched to the form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item: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', async function (assert) {
assert.expect(11);
var actionManager = await 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
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view',
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'First record',
"should have opened the first record");
// switch to record 4
await actionManager.loadState({
action: 3,
id: 4,
view_type: 'form',
});
assert.containsOnce(actionManager, '.o_form_view',
"should still display the form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item: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', async function (assert) {
assert.expect(3);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
});
await actionManager.loadState({action: 3});
assert.verifySteps([], "should not push the loaded state");
await testUtils.dom.click(actionManager.$('tr.o_data_row:first'));
assert.verifySteps(['push_state'],
"should push the state of it changes afterwards");
actionManager.destroy();
});
QUnit.test('should not push a loaded state of a client action', async function (assert) {
assert.expect(4);
var ClientAction = AbstractAction.extend({
init: function (parent, action, options) {
this._super.apply(this, arguments);
this.controllerID = options.controllerID;
},
start: function () {
var self = this;
var $button = $('').text('Click Me!');
$button.on('click', function () {
self.trigger_up('push_state', {
controllerID: self.controllerID,
state: {someValue: 'X'},
});
});
this.$el.append($button);
return this._super.apply(this, arguments);
},
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function (ev) {
assert.step('push_state');
assert.deepEqual(ev.data.state, {
action: 9,
someValue: 'X',
title: 'A Client Action',
});
},
},
});
await actionManager.loadState({action: 9});
assert.verifySteps([], "should not push the loaded state");
await testUtils.dom.click(actionManager.$('button'));
assert.verifySteps(['push_state'],
"should push the state of it changes afterwards");
actionManager.destroy();
});
QUnit.test('change a param of an ir.actions.client in the url', async function (assert) {
assert.expect(7);
var ClientAction = AbstractAction.extend({
hasControlPanel: true,
init: function (parent, action) {
this._super.apply(this, arguments);
var context = action.context;
this.a = context.params && context.params.a || 'default value';
},
start: function () {
assert.step('start');
this.$('.o_content').text(this.a);
this.$el.addClass('o_client_action');
this.trigger_up('push_state', {
controllerID: this.controllerID,
state: {a: this.a},
});
return this._super.apply(this, arguments);
},
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute the client action
await actionManager.doAction(9);
assert.strictEqual(actionManager.$('.o_client_action .o_content').text(), 'default value',
"should have rendered the client action");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
// update param 'a' in the url
await actionManager.loadState({
action: 9,
a: 'new value',
});
assert.strictEqual(actionManager.$('.o_client_action .o_content').text(), 'new value',
"should have rerendered the client action with the correct param");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should still be one controller in the breadcrumbs");
// should have executed the client action twice
assert.verifySteps(['start', 'start']);
actionManager.destroy();
delete core.action_registry.map.ClientAction;
});
QUnit.test('load a window action without id (in a multi-record view)', async function (assert) {
assert.expect(14);
var RamStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
session_storage: RamStorageService,
},
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
testUtils.mock.intercept(actionManager, 'call_service', function (ev) {
if (ev.data.service === 'session_storage') {
assert.step(ev.data.method);
}
}, true);
await actionManager.doAction(4);
assert.containsOnce(actionManager, '.o_kanban_view',
"should display a kanban view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners Action 4',
"breadcrumbs should display the display_name of the action");
await actionManager.loadState({
model: 'partner',
view_type: 'list',
});
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners Action 4',
"should still be in the same action");
assert.containsNone(actionManager, '.o_kanban_view',
"should no longer display a kanban view");
assert.containsOnce(actionManager, '.o_list_view',
"should display a list view");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // action 3
'setItem', // action 3
'getItem', // loadState
'load_views', // loaded action
'/web/dataset/search_read', // loaded action
'setItem', // loaded action
]);
actionManager.destroy();
});
QUnit.test('state with integer active_ids should not crash', async function (assert) {
assert.expect(0);
var actionManager = await createActionManager({
actions: this.actions,
mockRPC: function (route, args) {
if (route === '/web/action/run') {
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
});
await actionManager.loadState({
action: 2,
active_ids: 3,
});
actionManager.destroy();
});
QUnit.module('Concurrency management');
QUnit.test('drop previous actions if possible', async function (assert) {
assert.expect(6);
var def = testUtils.makeTestPromise();
var actionManager = await 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();
await testUtils.nextTick();
// action 4 loads a kanban view first, 6 loads a list view. We want a list
assert.containsOnce(actionManager, '.o_list_view',
'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', async function (assert) {
assert.expect(8);
var def = testUtils.makeTestPromise();
var defs = [Promise.resolve(), def, Promise.resolve()];
var actionManager = await 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;
},
});
await actionManager.doAction(4);
// kanban view is loaded, switch to list view
await cpHelpers.switchView(actionManager, 'list');
// here, list view is not ready yet, because def is not resolved
// switch back to kanban view
await cpHelpers.switchView(actionManager, 'kanban');
// 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();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_kanban_view',
"there should be a kanban view in dom");
assert.containsNone(actionManager, '.o_list_view',
"there should not be a list view in dom");
actionManager.destroy();
});
QUnit.test('when an server action takes too much time...', async function (assert) {
assert.expect(1);
var def = testUtils.makeTestPromise();
var actionManager = await 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();
await testUtils.nextTick();
assert.strictEqual($('.o_control_panel .breadcrumb-item.active').text(), 'Partners Action 4',
'action 4 should be loaded');
actionManager.destroy();
});
QUnit.test('clicking quickly on breadcrumbs...', async function (assert) {
assert.expect(1);
var def = Promise.resolve();
var actionManager = await 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
await actionManager.doAction(4);
await testUtils.dom.click(actionManager.$('.o_kanban_record:first'));
actionManager.doAction(8);
// now, the next read operations will be promise (this is the read
// operation for the form view reload)
def = testUtils.makeTestPromise();
await testUtils.nextTick();
// click on the breadcrumbs for the form view, then on the kanban view
// before the form view is fully reloaded
await testUtils.dom.click($('.o_control_panel .breadcrumb-item:eq(1)'));
await testUtils.dom.click($('.o_control_panel .breadcrumb-item:eq(0)'));
// resolve the form view read
def.resolve();
await testUtils.nextTick();
assert.strictEqual($('.o_control_panel .breadcrumb-item.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', async function (assert) {
assert.expect(15);
var def;
var actionManager = await 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 Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
await actionManager.loadState({
action: 4,
id: 2,
view_type: 'form',
});
assert.containsOnce(actionManager, '.o_form_view',
"should display the form view of action 4");
// click to go back to Kanban (this request is blocked)
def = testUtils.makeTestPromise();
await testUtils.nextTick();
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsOnce(actionManager, '.o_form_view',
"should still display the form view of action 4");
// execute another action meanwhile (don't block this request)
await actionManager.doAction(8, {clear_breadcrumbs: true});
assert.containsOnce(actionManager, '.o_list_view',
"should display action 8");
assert.containsNone(actionManager, '.o_form_view',
"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();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_list_view',
"should still display action 8");
assert.containsNone(actionManager, '.o_kanban_view',
"should not display the kanban view of action 4");
assert.verifySteps([]);
actionManager.destroy();
});
QUnit.test('execute a new action while handling a call_button', async function (assert) {
assert.expect(16);
var self = this;
var def = testUtils.makeTestPromise();
var actionManager = await 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
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view',
"should display the form view of action 3");
// click on 'Call method' button (this request is blocked)
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Call method)'));
assert.containsOnce(actionManager, '.o_form_view',
"should still display the form view of action 3");
// execute another action
await actionManager.doAction(8, {clear_breadcrumbs: true});
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view of action 8");
assert.containsNone(actionManager, '.o_form_view',
"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();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_list_view',
"should still display the list view of action 8");
assert.containsNone(actionManager, '.o_kanban_view',
"should not display action 1");
assert.verifySteps([]);
actionManager.destroy();
});
QUnit.test('execute a new action while switching to another controller', async function (assert) {
assert.expect(15);
/*
* This test's bottom line is that a doAction always has priority
* over a switch controller (clicking on a record row to go to form view).
* In general, the last actionManager's operation has priority because we want
* to allow the user to make mistakes, or to rapidly reconsider her next action.
* Here we assert that the actionManager's RPC are in order, but a 'read' operation
* is expected, with the current implementation, to take place when switching to the form view.
* Ultimately the form view's 'read' is superfluous, but can happen at any point of the flow,
* except at the very end, which should always be the final action's list's 'search_read'.
*/
var def;
var actionManager = await 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') {
assert.ok(true, "A 'read' should have been done. Check test's comment though.");
return Promise.resolve(def).then(_.constant(result));
}
assert.step(args.method || route);
return result;
},
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view of action 3");
// switch to the form view (this request is blocked)
def = testUtils.makeTestPromise();
await testUtils.nextTick();
testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_list_view',
"should still display the list view of action 3");
// execute another action meanwhile (don't block this request)
await actionManager.doAction(4, {clear_breadcrumbs: true});
assert.containsOnce(actionManager, '.o_kanban_view',
"should display the kanban view of action 8");
assert.containsNone(actionManager, '.o_list_view',
"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
'/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();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_kanban_view',
"should still display the kanban view of action 8");
assert.containsNone(actionManager, '.o_form_view',
"should not display the form view of action 3");
assert.verifySteps([]);
actionManager.destroy();
});
QUnit.test('execute a new action while loading views', async function (assert) {
assert.expect(10);
var def;
var actionManager = await 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 === 'load_views') {
return Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
// execute a first action (its 'load_views' RPC is blocked)
def = testUtils.makeTestPromise();
actionManager.doAction(3);
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view of action 3");
await testUtils.nextTick();
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4);
def.resolve();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_kanban_view',
"should display the kanban view of action 4");
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view of action 3");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/action/load', // action 4
'load_views', // action 4
'/web/dataset/search_read', // search read action 4
]);
actionManager.destroy();
});
QUnit.test('execute a new action while loading data of default view', async function (assert) {
assert.expect(11);
var def;
var actionManager = await 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') {
return Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
// execute a first action (its 'search_read' RPC is blocked)
def = testUtils.makeTestPromise();
actionManager.doAction(3);
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view of action 3");
await testUtils.nextTick();
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4);
def.resolve();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_kanban_view',
"should display the kanban view of action 4");
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view of action 3");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // search read action 3
'/web/action/load', // action 4
'load_views', // action 4
'/web/dataset/search_read', // search read action 4
]);
actionManager.destroy();
});
QUnit.test('open a record while reloading the list view', async function (assert) {
assert.expect(12);
var def;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
return Promise.resolve(def).then(_.constant(result));
}
return result;
},
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
assert.containsN(actionManager, '.o_list_view .o_data_row', 5,
"list view should contain 5 records");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 1,
"list view buttons should be displayed in control panel");
// reload (the search_read RPC will be blocked)
def = testUtils.makeTestPromise();
await testUtils.nextTick();
await cpHelpers.switchView(actionManager, 'list');
assert.containsN(actionManager, '.o_list_view .o_data_row', 5,
"list view should still contain 5 records");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 1,
"list view buttons should still be displayed in control panel");
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view',
"should display the form view");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 0,
"list view buttons should no longer be displayed in control panel");
assert.strictEqual($('.o_control_panel .o_form_buttons_view').length, 1,
"form view buttons should be displayed instead");
// unblock the search_read RPC
def.resolve();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_form_view',
"should display the form view");
assert.containsNone(actionManager, '.o_list_view',
"should not display the list view");
assert.strictEqual($('.o_control_panel .o_list_buttons').length, 0,
"list view buttons should still not be displayed in control panel");
assert.strictEqual($('.o_control_panel .o_form_buttons_view').length, 1,
"form view buttons should still be displayed instead");
actionManager.destroy();
});
QUnit.module('Client Actions');
QUnit.test('can execute client actions from tag name', async function (assert) {
assert.expect(3);
var ClientAction = AbstractAction.extend({
start: function () {
this.$el.text('Hello World');
this.$el.addClass('o_client_action_test');
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = await createActionManager({
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
}
});
await 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', async function (assert) {
assert.expect(4);
var ClientAction = AbstractAction.extend({
hasControlPanel: true,
start: async function () {
this.$('.o_content').text('Hello World');
this.$el.addClass('o_client_action_test');
this.controlPanelProps.title = 'Hello';
await this._super.apply(this, arguments);
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = await createActionManager();
await actionManager.doAction('HelloWorldTest');
assert.strictEqual($('.o_control_panel:visible').length, 1,
"should have rendered a control panel");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Hello',
"breadcrumbs should still display the title of the controller");
assert.strictEqual(actionManager.$('.o_client_action_test .o_content').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', async function (assert) {
assert.expect(3);
const ClientAction = AbstractAction.extend({
getTitle: function () {
return 'a title';
},
getState: function () {
return {foo: 'baz'};
}
});
const actionManager = await createActionManager({
intercepts: {
push_state: function (ev) {
const expectedState = {action: 'HelloWorldTest', foo: 'baz', title: 'a title'};
assert.deepEqual(ev.data.state, expectedState,
"should include a complete state description, including custom state");
assert.step('push state');
},
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
await actionManager.doAction('HelloWorldTest');
assert.verifySteps(['push state']);
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('action can use a custom control panel', async function (assert) {
assert.expect(1);
class CustomControlPanel extends owl.Component {}
CustomControlPanel.template = xml/* xml */`
My custom control panel
`
const ClientAction = AbstractAction.extend({
hasControlPanel: true,
config: {
ControlPanel: CustomControlPanel
},
});
const actionManager = await createActionManager();
core.action_registry.add('HelloWorldTest', ClientAction);
await actionManager.doAction('HelloWorldTest');
assert.containsOnce(actionManager, '.custom-control-panel',
"should have a custom control panel");
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('breadcrumb is updated on title change', async function (assert) {
assert.expect(2);
var ClientAction = AbstractAction.extend({
hasControlPanel: true,
events: {
click: function () {
this.updateControlPanel({ title: 'new title' });
},
},
start: async function () {
this.$('.o_content').text('Hello World');
this.$el.addClass('o_client_action_test');
this.controlPanelProps.title = 'initial title';
await this._super.apply(this, arguments);
},
});
var actionManager = await createActionManager();
core.action_registry.add('HelloWorldTest', ClientAction);
await actionManager.doAction('HelloWorldTest');
assert.strictEqual($('ol.breadcrumb').text(), "initial title",
"should have initial title as breadcrumb content");
await testUtils.dom.click(actionManager.$('.o_client_action_test'));
assert.strictEqual($('ol.breadcrumb').text(), "new title",
"should have updated title as breadcrumb content");
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('test display_notification client action', async function (assert) {
assert.expect(6);
testUtils.mock.patch(Notification, {
_animation: false,
});
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
notification: NotificationService,
},
});
await actionManager.doAction(1);
assert.containsOnce(actionManager, '.o_kanban_view');
await actionManager.doAction({
type: 'ir.actions.client',
tag: 'display_notification',
params: {
title: 'title',
message: 'message',
sticky: true,
}
});
const notificationSelector = '.o_notification_manager .o_notification';
assert.containsOnce(document.body, notificationSelector,
'a notification should be present');
const notificationElement = document.body.querySelector(notificationSelector);
assert.strictEqual(
notificationElement.querySelector('.o_notification_title').textContent,
'title',
"the notification should have the correct title"
);
assert.strictEqual(
notificationElement.querySelector('.o_notification_content').textContent,
'message',
"the notification should have the correct message"
);
assert.containsOnce(actionManager, '.o_kanban_view');
await testUtils.dom.click(
notificationElement.querySelector('.o_notification_close')
);
assert.containsNone(document.body, notificationSelector,
"the notification should be destroy ");
actionManager.destroy();
testUtils.mock.unpatch(Notification);
});
QUnit.module('Server actions');
QUnit.test('can execute server actions from db ID', async function (assert) {
assert.expect(9);
var actionManager = await 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 Promise.resolve(1); // execute action 1
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(2);
assert.strictEqual($('.o_control_panel:visible').length, 1,
"should have rendered a control panel");
assert.containsOnce(actionManager, '.o_kanban_view',
"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.test('handle server actions returning false', async function (assert) {
assert.expect(9);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/action/run') {
return Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
});
// execute an action in target="new"
await actionManager.doAction(5, {
on_close: assert.step.bind(assert, 'close handler'),
});
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"should have rendered a form view in a modal");
// execute a server action that returns false
await actionManager.doAction(2);
assert.strictEqual($('.o_technical_modal').length, 0,
"should have closed the modal");
assert.verifySteps([
'/web/action/load', // action 5
'load_views',
'onchange',
'/web/action/load', // action 2
'/web/action/run',
'close handler',
]);
actionManager.destroy();
});
QUnit.module('Report actions');
QUnit.test('can execute report actions from db ID', async function (assert) {
assert.expect(5);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
report: ReportService,
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return Promise.resolve('ok');
}
return this._super.apply(this, arguments);
},
session: {
get_file: async function (params) {
assert.step(params.url);
params.success();
params.complete();
return true;
},
},
});
await actionManager.doAction(7, {
on_close: function () {
assert.step('on_close');
},
});
await testUtils.nextTick();
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'/report/download',
'on_close',
]);
actionManager.destroy();
});
QUnit.test('report actions can close modals and reload views', async function (assert) {
assert.expect(8);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
report: ReportService,
},
mockRPC: function (route, args) {
if (route === '/report/check_wkhtmltopdf') {
return Promise.resolve('ok');
}
return this._super.apply(this, arguments);
},
session: {
get_file: async function (params) {
assert.step(params.url);
params.success();
params.complete();
return true;
},
},
});
// load modal
await actionManager.doAction(5, {
on_close: function () {
assert.step('on_close');
},
});
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"should have rendered a form view in a modal");
await actionManager.doAction(7, {
on_close: function () {
assert.step('on_printed');
},
});
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"The modal should still exist");
await actionManager.doAction(11);
assert.strictEqual($('.o_technical_modal .o_form_view').length, 0,
"the modal should have been closed after the action report");
assert.verifySteps([
'/report/download',
'on_printed',
'/report/download',
'on_close',
]);
actionManager.destroy();
});
QUnit.test('should trigger a notification if wkhtmltopdf is to upgrade', async function (assert) {
assert.expect(5);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
report: ReportService,
notification: NotificationService.extend({
notify: function (params) {
assert.step(params.type || 'notification');
}
}),
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return Promise.resolve('upgrade');
}
return this._super.apply(this, arguments);
},
session: {
get_file: async function (params) {
assert.step(params.url);
params.success();
params.complete();
return true;
},
},
});
await actionManager.doAction(7);
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'warning',
'/report/download',
]);
actionManager.destroy();
});
QUnit.test('should open the report client action if wkhtmltopdf is broken', async function (assert) {
assert.expect(7);
// patch the report client action to override its iframe's url so that
// it doesn't trigger an RPC when it is appended to the DOM (for this
// usecase, using removeSRCAttribute doesn't work as the RPC is
// triggered as soon as the iframe is in the DOM, even if its src
// attribute is removed right after)
testUtils.mock.patch(ReportClientAction, {
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self._rpc({route: self.iframe.getAttribute('src')});
self.iframe.setAttribute('src', 'about:blank');
});
}
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
report: ReportService,
notification: NotificationService.extend({
notify: function (params) {
assert.step(params.type || 'notification');
}
})
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return Promise.resolve('broken');
}
if (route.includes('/report/html/some_report')) {
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
session: {
get_file: function (params) {
assert.step(params.url); // should not be called
return true;
},
},
});
await actionManager.doAction(7);
assert.containsOnce(actionManager, '.o_report_iframe',
"should have opened the report client action");
assert.containsOnce(actionManager, '.o_cp_buttons .o_report_buttons .o_report_print');
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'warning',
'/report/html/some_report?context=%7B%7D', // report client action's iframe
]);
actionManager.destroy();
testUtils.mock.unpatch(ReportClientAction);
});
QUnit.test('send context in case of html report', async function (assert) {
assert.expect(4);
// patch the report client action to override its iframe's url so that
// it doesn't trigger an RPC when it is appended to the DOM (for this
// usecase, using removeSRCAttribute doesn't work as the RPC is
// triggered as soon as the iframe is in the DOM, even if its src
// attribute is removed right after)
testUtils.mock.patch(ReportClientAction, {
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self._rpc({route: self.iframe.getAttribute('src')});
self.iframe.setAttribute('src', 'about:blank');
});
}
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
report: ReportService,
notification: NotificationService.extend({
notify: function (params) {
assert.step(params.type || 'notification');
}
})
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route.includes('/report/html/some_report')) {
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
session: {
user_context: {
some_key: 2,
}
},
});
await actionManager.doAction(12);
assert.containsOnce(actionManager, '.o_report_iframe',
"should have opened the report client action");
assert.verifySteps([
'/web/action/load',
'/report/html/some_report?context=%7B%22some_key%22%3A2%7D', // report client action's iframe
]);
actionManager.destroy();
testUtils.mock.unpatch(ReportClientAction);
});
QUnit.test('crashmanager service called on failed report download actions', async function (assert) {
assert.expect(1);
var actionManager = await createActionManager({
data: this.data,
actions: this.actions,
services: {
report: ReportService,
},
mockRPC: function (route) {
if (route === '/report/check_wkhtmltopdf') {
return Promise.resolve('ok');
}
return this._super.apply(this, arguments);
},
session: {
get_file: function (params) {
params.error({
data: {
name: 'error',
arguments: ['could not download file'],
}
});
params.complete();
},
},
});
try {
await actionManager.doAction(11);
} catch (e) {
// e is undefined if we land here because of a rejected promise,
// otherwise, it is an Error, which is not what we expect
assert.strictEqual(e, undefined);
}
actionManager.destroy();
});
QUnit.module('Window Actions');
QUnit.test('can execute act_window actions from db ID', async function (assert) {
assert.expect(6);
var actionManager = await 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);
},
});
await actionManager.doAction(1);
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.containsOnce(actionManager, '.o_kanban_view',
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('sidebar is present in list view', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var res = this._super.apply(this, arguments);
if (args.method === 'load_views') {
assert.strictEqual(args.kwargs.options.toolbar, true,
"should ask for toolbar information");
return res.then(function (fieldsViews) {
fieldsViews.list.toolbar = {
print: [{name: "Print that record"}],
};
return fieldsViews;
});
}
return res;
},
});
await actionManager.doAction(3);
assert.containsNone(actionManager, '.o_cp_action_menus');
await testUtils.dom.clickFirst(actionManager.$('input.custom-control-input'));
assert.isVisible(actionManager.$('.o_cp_action_menus button.o_dropdown_toggler_btn:contains("Print")'));
assert.isVisible(actionManager.$('.o_cp_action_menus button.o_dropdown_toggler_btn:contains("Action")'));
actionManager.destroy();
});
QUnit.test('can switch between views', async function (assert) {
assert.expect(18);
var actionManager = await 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);
},
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
// switch to kanban view
await cpHelpers.switchView(actionManager, 'kanban');
assert.containsNone(actionManager, '.o_list_view',
"should no longer display the list view");
assert.containsOnce(actionManager, '.o_kanban_view',
"should display the kanban view");
// switch back to list view
await cpHelpers.switchView(actionManager, 'list');
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
assert.containsNone(actionManager, '.o_kanban_view',
"should no longer display the kanban view");
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsNone(actionManager, '.o_list_view',
"should no longer display the list view");
assert.containsOnce(actionManager, '.o_form_view',
"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
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
assert.containsNone(actionManager, '.o_form_view',
"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('orderedBy in context is not propagated when executing another action', async function (assert) {
assert.expect(6);
this.data.partner.fields.foo.sortable = true;
this.archs['partner,false,form'] = '';
var searchReadCount = 1;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
if (searchReadCount === 1) {
assert.strictEqual(args.model, 'partner');
assert.notOk(args.sort);
}
if (searchReadCount === 2) {
assert.strictEqual(args.model, 'partner');
assert.strictEqual(args.sort, "foo ASC");
}
if (searchReadCount === 3) {
assert.strictEqual(args.model, 'pony');
assert.notOk(args.sort);
}
searchReadCount += 1;
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(3);
// Simulate the activation of a filter
var searchData = {
domains: [[["foo", "=", "yop"]]],
contexts: [{
orderedBy: [],
}],
};
actionManager.trigger_up('search', searchData);
// Sort records
await testUtils.dom.click(actionManager.$('.o_list_view th.o_column_sortable'));
// get to the form view of the model, on the first record
await testUtils.dom.click(actionManager.$('.o_data_cell:first'));
// Change model by clicking on the button within the form
await testUtils.dom.click(actionManager.$('.o_form_view button'));
actionManager.destroy();
});
QUnit.test('breadcrumbs are updated when switching between views', async function (assert) {
assert.expect(15);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners',
"breadcrumbs should display the display_name of the action");
// switch to kanban view
await cpHelpers.switchView(actionManager, 'kanban');
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should still be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners',
"breadcrumbs should still display the display_name of the action");
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_kanban_view .o_kanban_record:first'));
await testUtils.nextTick();
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// go back to kanban view using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners',
"breadcrumbs should display the display_name of the action");
// switch back to list view
await cpHelpers.switchView(actionManager, 'list');
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should still be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners',
"breadcrumbs should still display the display_name of the action");
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// go back to list view using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsOnce(actionManager, '.o_list_view',
"should be back on list view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item').text(), 'Partners',
"breadcrumbs should display the display_name of the action");
actionManager.destroy();
});
QUnit.test('switch buttons are updated when switching between views', async function (assert) {
assert.expect(13);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsN(actionManager, '.o_control_panel button.o_switch_view', 2,
"should have two switch buttons (list and kanban)");
assert.containsOnce(actionManager, '.o_control_panel button.o_switch_view.active',
"should have only one active button");
assert.hasClass($('.o_control_panel .o_switch_view:first'),'o_list',
"list switch button should be the first one");
assert.hasClass($('.o_control_panel .o_switch_view.o_list'), 'active',
"list should be the active view");
// switch to kanban view
await cpHelpers.switchView(actionManager, 'kanban');
assert.containsN(actionManager, '.o_control_panel .o_switch_view', 2,
"should still have two switch buttons (list and kanban)");
assert.containsOnce(actionManager, '.o_control_panel .o_switch_view.active',
"should still have only one active button");
assert.hasClass($('.o_control_panel .o_switch_view:first'), 'o_list',
"list switch button should still be the first one");
assert.hasClass($('.o_control_panel .o_switch_view.o_kanban'),'active',
"kanban should now be the active view");
// switch back to list view
await cpHelpers.switchView(actionManager, 'list');
assert.containsN(actionManager, '.o_control_panel .o_switch_view', 2,
"should still have two switch buttons (list and kanban)");
assert.hasClass($('.o_control_panel .o_switch_view.o_list'),'active',
"list should now be the active view");
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsNone(actionManager, '.o_control_panel .o_switch_view',
"should not have any switch buttons");
// go back to list view using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsN(actionManager, '.o_control_panel .o_switch_view', 2,
"should have two switch buttons (list and kanban)");
assert.hasClass($('.o_control_panel .o_switch_view.o_list'),'active',
"list should be the active view");
actionManager.destroy();
});
QUnit.test('pager is updated when switching between views', async function (assert) {
assert.expect(10);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await 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
await cpHelpers.switchView(actionManager, 'list');
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
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
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
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
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
await cpHelpers.switchView(actionManager, 'kanban');
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("domain is kept when switching between views", async function (assert) {
assert.expect(5);
this.actions[2].search_view_id = [1, 'a custom search view'];
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsN(actionManager, '.o_data_row', 5);
// activate a domain
await cpHelpers.toggleFilterMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, "Bar");
assert.containsN(actionManager, '.o_data_row', 2);
// switch to kanban
await cpHelpers.switchView(actionManager, 'kanban');
assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 2);
// remove the domain
await testUtils.dom.click(actionManager.$('.o_searchview .o_facet_remove'));
assert.containsN(actionManager, '.o_kanban_record:not(.o_kanban_ghost)', 5);
// switch back to list
await cpHelpers.switchView(actionManager, 'list');
assert.containsN(actionManager, '.o_data_row', 5);
actionManager.destroy();
});
QUnit.test('there is no flickering when switching between views', async function (assert) {
assert.expect(20);
var def;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function () {
var result = this._super.apply(this, arguments);
return Promise.resolve(def).then(_.constant(result));
},
});
await actionManager.doAction(3);
// switch to kanban view
def = testUtils.makeTestPromise();
await cpHelpers.switchView(actionManager, 'kanban');
assert.containsOnce(actionManager, '.o_list_view',
"should still display the list view");
assert.containsNone(actionManager, '.o_kanban_view',
"shouldn't display the kanban view yet");
def.resolve();
await testUtils.nextTick();
assert.containsNone(actionManager, '.o_list_view',
"shouldn't display the list view anymore");
assert.containsOnce(actionManager, '.o_kanban_view',
"should now display the kanban view");
// switch back to list view
def = testUtils.makeTestPromise();
await cpHelpers.switchView(actionManager, 'list');
assert.containsOnce(actionManager, '.o_kanban_view',
"should still display the kanban view");
assert.containsNone(actionManager, '.o_list_view',
"shouldn't display the list view yet");
def.resolve();
await testUtils.nextTick();
assert.containsNone(actionManager, '.o_kanban_view',
"shouldn't display the kanban view anymore");
assert.containsOnce(actionManager, '.o_list_view',
"should now display the list view");
// open a record in form view
def = testUtils.makeTestPromise();
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_list_view',
"should still display the list view");
assert.containsNone(actionManager, '.o_form_view',
"shouldn't display the form view yet");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should still be one controller in the breadcrumbs");
def.resolve();
await testUtils.nextTick();
assert.containsNone(actionManager, '.o_list_view',
"should no longer display the list view");
assert.containsOnce(actionManager, '.o_form_view',
"should display the form view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
// go back to list view using the breadcrumbs
def = testUtils.makeTestPromise();
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
assert.containsOnce(actionManager, '.o_form_view',
"should still display the form view");
assert.containsNone(actionManager, '.o_list_view',
"shouldn't display the list view yet");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should still be two controllers in the breadcrumbs");
def.resolve();
await testUtils.nextTick();
assert.containsNone(actionManager, '.o_form_view',
"should no longer display the form view");
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
actionManager.destroy();
});
QUnit.test('breadcrumbs are updated when display_name changes', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// switch to edit mode and change the display_name
await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
await testUtils.fields.editInput(actionManager.$('.o_field_widget[name=display_name]'), 'New name');
await testUtils.dom.click($('.o_control_panel .o_form_button_save'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should still be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'New name',
"breadcrumbs should contain the display_name of the opened record");
actionManager.destroy();
});
QUnit.test('reverse breadcrumb works on accesskey "b"', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Execute action)'));
assert.containsN(actionManager, '.o_control_panel .breadcrumb li', 3);
var $previousBreadcrumb = actionManager.$('.o_control_panel .breadcrumb li.active').prev();
assert.strictEqual($previousBreadcrumb.attr("accesskey"), "b",
"previous breadcrumb should have accessKey 'b'");
await testUtils.dom.click($previousBreadcrumb);
assert.containsN(actionManager, '.o_control_panel .breadcrumb li', 2);
var $previousBreadcrumb = actionManager.$('.o_control_panel .breadcrumb li.active').prev();
assert.strictEqual($previousBreadcrumb.attr("accesskey"), "b",
"previous breadcrumb should have accessKey 'b'");
actionManager.destroy();
});
QUnit.test('reload previous controller when discarding a new record', async function (assert) {
assert.expect(8);
var actionManager = await 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);
},
});
await actionManager.doAction(3);
// create a new record
await testUtils.dom.click($('.o_control_panel .o_list_button_add'));
assert.containsOnce(actionManager, '.o_form_view.o_form_editable',
"should have opened the form view in edit mode");
// discard
await testUtils.dom.click($('.o_control_panel .o_form_button_cancel'));
assert.containsOnce(actionManager, '.o_list_view',
"should have switched back to the list view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list
'onchange', // form
'/web/dataset/search_read', // list
]);
actionManager.destroy();
});
QUnit.test('requests for execute_action of type object are handled', async function (assert) {
assert.expect(10);
var self = this;
var actionManager = await 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]],
kwargs: {context: {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 Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
session: {user_context: {
some_key: 2,
}},
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
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)
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Call method)'));
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', async function (assert) {
assert.expect(11);
var actionManager = await 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);
},
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// click on 'Execute action' button (should execute an action)
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two parts in the breadcrumbs");
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Execute action)'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 3,
"the returned action should have been stacked over the previous one");
assert.containsOnce(actionManager, '.o_kanban_view',
"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('requests for execute_action of type object: disable buttons', async function (assert) {
assert.expect(2);
var def;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button') {
return Promise.resolve(false);
} else if (args.method === 'read') {
// Block the 'read' call
var result = this._super.apply(this, arguments);
return Promise.resolve(def).then(_.constant(result));
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// click on 'Call method' button (should call an Object method)
def = testUtils.makeTestPromise();
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Call method)'));
// Buttons should be disabled
assert.strictEqual(
actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
'disabled', 'buttons should be disabled');
// Release the 'read' call
def.resolve();
await testUtils.nextTick();
// Buttons should be enabled after the reload
assert.strictEqual(
actionManager.$('.o_form_view button:contains(Call method)').attr('disabled'),
undefined, 'buttons should be disabled')
actionManager.destroy();
});
QUnit.test('can open different records from a multi record view', async function (assert) {
assert.expect(11);
var actionManager = await 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);
},
});
await actionManager.doAction(3);
// open the first record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.strictEqual($('.o_control_panel .breadcrumb-item: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
await testUtils.dom.click($('.o_control_panel .breadcrumb a'));
// open the second record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:nth(1)'));
assert.strictEqual($('.o_control_panel .breadcrumb-item: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('restore previous view state when switching back', async function (assert) {
assert.expect(5);
this.actions[2].views.unshift([false, 'graph']);
this.archs['partner,false,graph'] = ' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.hasClass($('.o_control_panel .fa-bar-chart-o'),'active',
"bar chart button is active");
assert.doesNotHaveClass($('.o_control_panel .fa-area-chart'), 'active',
"line chart button is not active");
// display line chart
await testUtils.dom.click($('.o_control_panel .fa-area-chart'));
assert.hasClass($('.o_control_panel .fa-area-chart'),'active',
"line chart button is now active");
// switch to kanban and back to graph view
await cpHelpers.switchView(actionManager, 'kanban');
assert.strictEqual($('.o_control_panel .fa-area-chart').length, 0,
"graph buttons are no longer in control panel");
await cpHelpers.switchView(actionManager, 'graph');
assert.hasClass($('.o_control_panel .fa-area-chart'),'active',
"line chart button is still active");
actionManager.destroy();
});
QUnit.test('view switcher is properly highlighted in graph view', async 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 = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.hasClass($('.o_control_panel .o_switch_view.o_list'),'active',
"list button in control panel is active");
assert.doesNotHaveClass($('.o_control_panel .o_switch_view.o_graph'), 'active',
"graph button in control panel is not active");
// switch to graph view
await cpHelpers.switchView(actionManager, 'graph');
assert.doesNotHaveClass($('.o_control_panel .o_switch_view.o_list'), 'active',
"list button in control panel is not active");
assert.hasClass($('.o_control_panel .o_switch_view.o_graph'),'active',
"graph button in control panel is active");
actionManager.destroy();
});
QUnit.test('can interact with search view', async function (assert) {
assert.expect(2);
this.archs['partner,false,search'] = ''+
''+
' ' +
' '+
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.doesNotHaveClass(actionManager.$('.o_list_table'), 'o_list_table_grouped',
"list view is not grouped");
// open group by dropdown
await testUtils.dom.click($('.o_control_panel .o_cp_bottom_right button:contains(Group By)'));
// click on first link
await testUtils.dom.click($('.o_control_panel .o_group_by_menu a:first'));
assert.hasClass(actionManager.$('.o_list_table'),'o_list_table_grouped',
'list view is now grouped');
actionManager.destroy();
});
QUnit.test('can open a many2one external window', async 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 = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(route);
if (args.method === "get_formview_id") {
return Promise.resolve(false);
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(3);
// open first record in form view
await testUtils.dom.click(actionManager.$('.o_data_row:first'));
// click on edit
await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
// click on external button for m2o
await testUtils.dom.click(actionManager.$('.o_external_button'));
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', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(4);
// open record in form view
await testUtils.dom.click(actionManager.$('.o_kanban_record:first'));
// edit record
await testUtils.dom.click($('.o_control_panel button.o_form_button_edit'));
await testUtils.fields.editInput(actionManager.$('input[name="foo"]'), 'pinkypie');
// go back to kanban view
await testUtils.dom.click($('.o_control_panel .breadcrumb-item:first a'));
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
await testUtils.dom.click($('.modal .modal-footer button.btn-secondary'));
assert.containsOnce(actionManager, '.o_form_view',
"should still be in form view");
// go back again to kanban view
await testUtils.dom.click($('.o_control_panel .breadcrumb-item:first a'));
// confirm discard
await testUtils.dom.click($('.modal .modal-footer button.btn-primary'));
assert.containsNone(actionManager, '.o_form_view',
"should no longer be in form view");
assert.containsOnce(actionManager, '.o_kanban_view',
"should be in kanban view");
actionManager.destroy();
});
QUnit.test('limit set in action is passed to each created controller', async function (assert) {
assert.expect(2);
_.findWhere(this.actions, {id: 3}).limit = 2;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsN(actionManager, '.o_data_row', 2,
"should only display 2 record");
// switch to kanban view
await cpHelpers.switchView(actionManager, 'kanban');
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', async function (assert) {
assert.expect(10);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item: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
await actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 3,
"there should be three controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Partners Action 4',
"breadcrumbs should contain the name of the current action");
// go back using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a:nth(1)'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item: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
await actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 3,
"there should be three controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Partners Action 4',
"breadcrumbs should contain the name of the current action");
// go back using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Partners',
"breadcrumbs should contain the name of the current action");
actionManager.destroy();
});
QUnit.test('form views are restored in readonly when coming back in breadcrumbs', async function (assert) {
assert.expect(2);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
// open a record in form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// switch to edit mode
await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
assert.hasClass(actionManager.$('.o_form_view'), 'o_form_editable');
// do some other action
await actionManager.doAction(4);
// go back to form view
await testUtils.dom.clickLast($('.o_control_panel .breadcrumb a'));
await testUtils.nextTick();
assert.hasClass(actionManager.$('.o_form_view'), 'o_form_readonly');
actionManager.destroy();
});
QUnit.test('honor group_by specified in actions context', async function (assert) {
assert.expect(5);
_.findWhere(this.actions, {id: 3}).context = "{'group_by': 'bar'}";
this.archs['partner,false,search'] = ''+
''+
' ' +
' '+
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_table_grouped',
"should be grouped");
assert.containsN(actionManager, '.o_group_header', 2,
"should be grouped by 'bar' (two groups) at first load");
// groupby 'bar' using the searchview
await testUtils.dom.click($('.o_control_panel .o_cp_bottom_right button:contains(Group By)'));
await testUtils.dom.click($('.o_control_panel .o_group_by_menu a:first'));
assert.containsN(actionManager, '.o_group_header', 5,
"should be grouped by 'foo' (five groups)");
// remove the groupby in the searchview
await testUtils.dom.click($('.o_control_panel .o_searchview .o_facet_remove'));
assert.containsOnce(actionManager, '.o_list_table_grouped',
"should still be grouped");
assert.containsN(actionManager, '.o_group_header', 2,
"should be grouped by 'bar' (two groups) at reload");
actionManager.destroy();
});
QUnit.test('switch request to unknown view type', async function (assert) {
assert.expect(7);
this.actions.push({
id: 33,
name: 'Partners',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [1, 'kanban']], // no form view
});
var actionManager = await 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);
},
});
await actionManager.doAction(33);
assert.containsOnce(actionManager, '.o_list_view',
"should display the list view");
// try to open a record in a form view
testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_list_view',
"should still display the list view");
assert.containsNone(actionManager, '.o_form_view',
"should not display the form view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('save current search', async function (assert) {
assert.expect(4);
testUtils.mock.patch(ListController, {
getOwnedQueryParams: function () {
return {
context: {
shouldBeInFilterContext: true,
}
};
},
});
this.actions.push({
id: 33,
context: {
shouldNotBeInFilterContext: false,
},
name: 'Partners',
res_model: 'partner',
search_view_id: [1, 'a custom search view'],
type: 'ir.actions.act_window',
views: [[false, 'list']],
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
env: {
dataManager: {
create_filter: function (filter) {
assert.strictEqual(filter.domain, `[("bar", "=", 1)]`,
"should save the correct domain");
const expectedContext = {
group_by: [], // default groupby is an empty list
shouldBeInFilterContext: true,
};
assert.deepEqual(filter.context, expectedContext,
"should save the correct context");
},
}
},
});
await actionManager.doAction(33);
assert.containsN(actionManager, '.o_data_row', 5,
"should contain 5 records");
// filter on bar
await cpHelpers.toggleFilterMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, "Bar");
assert.containsN(actionManager, '.o_data_row', 2);
// save filter
await cpHelpers.toggleFavoriteMenu(actionManager);
await cpHelpers.toggleSaveFavorite(actionManager);
await cpHelpers.editFavoriteName(actionManager, "some name");
await cpHelpers.saveFavorite(actionManager);
testUtils.mock.unpatch(ListController);
actionManager.destroy();
});
QUnit.test('list with default_order and favorite filter with no orderedBy', async function (assert) {
assert.expect(5);
this.archs['partner,1,list'] = ' ';
this.actions.push({
id: 100,
name: 'Partners',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[1, 'list'], [false, 'form']],
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
favoriteFilters: [{
user_id: [2, "Mitchell Admin"],
name: 'favorite filter',
id: 5,
context: {},
sort: '[]',
domain: '[("bar", "=", 1)]'
}],
});
await actionManager.doAction(100);
assert.strictEqual(actionManager.$('.o_list_view tr.o_data_row .o_data_cell').text(), 'zoupyopplopgnapblip',
'record should be in descending order as default_order applies');
await cpHelpers.toggleFavoriteMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, "favorite filter");
assert.strictEqual(actionManager.$('.o_control_panel .o_facet_values').text().trim(),
'favorite filter', 'favorite filter should be applied');
assert.strictEqual(actionManager.$('.o_list_view tr.o_data_row .o_data_cell').text(), 'gnapblip',
'record should still be in descending order after default_order applied');
// go to formview and come back to listview
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
await testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb a:eq(0)'));
assert.strictEqual(actionManager.$('.o_list_view tr.o_data_row .o_data_cell').text(), 'gnapblip',
'order of records should not be changed, while coming back through breadcrumb');
// remove filter
await cpHelpers.removeFacet(actionManager, 0);
assert.strictEqual(actionManager.$('.o_list_view tr.o_data_row .o_data_cell').text(),
'zoupyopplopgnapblip', 'order of records should not be changed, after removing current filter');
actionManager.destroy();
});
QUnit.test("search menus are still available when switching between actions", async function (assert) {
assert.expect(3);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(1);
assert.isVisible(actionManager.el.querySelector('.o_search_options .o_dropdown.o_filter_menu'),
"the search options should be available");
await actionManager.doAction(3);
assert.isVisible(actionManager.el.querySelector('.o_search_options .o_dropdown.o_filter_menu'),
"the search options should be available");
// go back using the breadcrumbs
await testUtils.dom.click($('.o_control_panel .breadcrumb a:first'));
assert.isVisible(actionManager.el.querySelector('.o_search_options .o_dropdown.o_filter_menu'),
"the search options should be available");
actionManager.destroy();
});
QUnit.test("current act_window action is stored in session_storage", async function (assert) {
assert.expect(1);
var expectedAction = _.extend({}, _.findWhere(this.actions, {id: 3}), {
context: {},
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
session_storage: SessionStorageService.extend({
setItem: function (key, value) {
assert.strictEqual(value, JSON.stringify(expectedAction),
"should store the executed action in the sessionStorage");
},
}),
},
});
await actionManager.doAction(3);
actionManager.destroy();
});
QUnit.test("store evaluated context of current action in session_storage", async function (assert) {
// this test ensures that we don't store stringified instances of
// CompoundContext in the session_storage, as they would be meaningless
// once restored
assert.expect(1);
var expectedAction = _.extend({}, _.findWhere(this.actions, {id: 4}), {
context: {
active_model: 'partner',
active_id: 1,
active_ids: [1],
},
});
var checkSessionStorage = false;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: {
session_storage: SessionStorageService.extend({
setItem: function (key, value) {
if (checkSessionStorage) {
assert.strictEqual(value, JSON.stringify(expectedAction),
"should correctly store the executed action in the sessionStorage");
}
},
}),
},
});
// execute an action and open a record in form view
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
// click on 'Execute action' button (it executes an action with a CompoundContext as context)
checkSessionStorage = true;
await testUtils.dom.click(actionManager.$('.o_form_view button:contains(Execute action)'));
actionManager.destroy();
});
QUnit.test("destroy action with lazy loaded controller", async function (assert) {
assert.expect(6);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.loadState({
action: 3,
id: 2,
view_type: 'form',
});
assert.containsNone(actionManager, '.o_list_view');
assert.containsOnce(actionManager, '.o_form_view');
assert.strictEqual($('.o_control_panel .breadcrumb-item').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb-item:last').text(), 'Second record',
"breadcrumbs should contain the display_name of the opened record");
await actionManager.doAction(1, {clear_breadcrumbs: true});
assert.containsNone(actionManager, '.o_form_view');
assert.containsOnce(actionManager, '.o_kanban_view');
actionManager.destroy();
});
QUnit.test('execute action from dirty, new record, and come back', async function (assert) {
assert.expect(17);
this.data.partner.fields.bar.default = 1;
this.archs['partner,false,form'] = '' +
' ' +
' ' +
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'get_formview_action') {
return Promise.resolve({
res_id: 1,
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'form']],
});
}
return this._super.apply(this, arguments);
},
intercepts: {
do_action: function (ev) {
actionManager.doAction(ev.data.action, {});
},
},
});
// execute an action and create a new record
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_button_add'));
assert.containsOnce(actionManager, '.o_form_view.o_form_editable');
assert.containsOnce(actionManager, '.o_form_uri:contains(First record)');
assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(),
"PartnersNew");
// set form view dirty and open m2o record
await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val');
await testUtils.dom.click(actionManager.$('.o_form_uri:contains(First record)'));
assert.containsOnce($('body'), '.modal'); // confirm discard dialog
// confirm discard changes
await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(),
"PartnersNewFirst record");
// go back to New using the breadcrumbs
await testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb-item:nth(1) a'));
assert.containsOnce(actionManager, '.o_form_view.o_form_editable');
assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(),
"PartnersNew");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // views of action 3
'/web/dataset/search_read', // list
'onchange', // form (create)
'get_formview_action', // click on m2o
'load_views', // form view of dynamic action
'read', // form
'onchange', // form (create)
]);
actionManager.destroy();
});
QUnit.test('execute a contextual action from a form view', async function (assert) {
assert.expect(4);
const contextualAction = this.actions.find(action => action.id === 8);
contextualAction.context = "{}"; // need a context to evaluate
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: async function (route, args) {
const res = await this._super(...arguments);
if (args.method === 'load_views' && args.model === 'partner') {
assert.strictEqual(args.kwargs.options.toolbar, true,
"should ask for toolbar information");
res.form.toolbar = {
action: [contextualAction],
print: [],
};
}
return res;
},
intercepts: {
do_action: function (ev) {
actionManager.doAction(ev.data.action, {});
},
},
});
// execute an action and open a record
await actionManager.doAction(3);
assert.containsOnce(actionManager, '.o_list_view');
await testUtils.dom.click(actionManager.$('.o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view');
// execute the custom action from the action menu
await cpHelpers.toggleActionMenu(actionManager);
await cpHelpers.toggleMenuItem(actionManager, "Favorite Ponies");
assert.containsOnce(actionManager, '.o_list_view');
actionManager.destroy();
});
QUnit.module('Actions in target="new"');
QUnit.test('can execute act_window actions in target="new"', async function (assert) {
assert.expect(7);
var actionManager = await 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);
},
});
await actionManager.doAction(5);
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"should have rendered a form view in a modal");
assert.hasClass($('.o_technical_modal .modal-body'),'o_act_window',
"dialog main element should have classname 'o_act_window'");
assert.hasClass($('.o_technical_modal .o_form_view'),'o_form_editable',
"form view should be in edit mode");
assert.verifySteps([
'/web/action/load',
'load_views',
'onchange',
]);
actionManager.destroy();
});
QUnit.test('chained action on_close', async function (assert) {
assert.expect(3);
function on_close() {
assert.step('Close Action');
};
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(5, {on_close: on_close});
// a target=new action shouldn't activate the on_close
await actionManager.doAction(5);
assert.verifySteps([]);
// An act_window_close should trigger the on_close
await actionManager.doAction(10);
assert.verifySteps(['Close Action']);
actionManager.destroy();
});
QUnit.test('footer buttons are moved to the dialog footer', async function (assert) {
assert.expect(3);
this.archs['partner,false,form'] = '' +
' ' +
'' +
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(5);
assert.containsNone($('.o_technical_modal .modal-body'), 'button.infooter',
"the button should not be in the body");
assert.containsOnce($('.o_technical_modal .modal-footer'), 'button.infooter',
"the button should be in the footer");
assert.containsOnce($('.o_technical_modal .modal-footer'), 'button',
"the modal footer should only contain one button");
actionManager.destroy();
});
QUnit.test("Button with `close` attribute closes dialog", async function (assert) {
assert.expect(2);
const actions = [
{
id: 4,
name: "Partners Action 4",
res_model: "partner",
type: "ir.actions.act_window",
views: [[false, "form"]],
},
{
id: 5,
name: "Create a Partner",
res_model: "partner",
target: "new",
type: "ir.actions.act_window",
views: [["view_ref", "form"]],
},
];
const actionManager = await createActionManager({
actions,
archs: {
"partner,false,form": `
`,
"partner,view_ref,form": `
`,
"partner,false,search": " ",
},
data: this.data,
mockRPC: async function (route, args) {
if (
route === "/web/dataset/call_button" &&
args.method === "some_method"
) {
return {
tag: "display_notification",
type: "ir.actions.client"
};
}
return this._super(...arguments);
},
});
await actionManager.doAction(4);
await testUtils.dom.click(`button[name="5"]`);
assert.strictEqual($(".modal").length, 1, "It should display a modal");
await testUtils.dom.click(`button[name="some_method"]`);
assert.strictEqual($(".modal").length, 0, "It should have closed the modal");
actionManager.destroy();
});
QUnit.test('can execute act_window actions in target="new"', async function (assert) {
assert.expect(5);
this.actions.push({
id: 999,
name: 'A window action',
res_model: 'partner',
target: 'new',
type: 'ir.actions.act_window',
views: [[999, 'form']],
});
this.archs['partner,999,form'] = `
`;
this.archs['partner,1000,form'] = `Another action `;
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'method') {
return Promise.resolve({
id: 1000,
name: 'Another window action',
res_model: 'partner',
target: 'new',
type: 'ir.actions.act_window',
views: [[1000, 'form']],
});
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(999);
assert.containsOnce(document.body, '.modal button[name=method]');
await testUtils.dom.click($('.modal button[name=method]'));
assert.containsN(document.body, '.modal', 2);
assert.strictEqual($('.modal:last .modal-body').text(), 'Are you sure?');
await testUtils.dom.click($('.modal:last .modal-footer .btn-primary'));
assert.containsOnce(document.body, '.modal');
assert.strictEqual($('.modal:last .modal-body').text().trim(), 'Another action');
actionManager.destroy();
});
QUnit.test('on_attach_callback is called for actions in target="new"', async function (assert) {
assert.expect(4);
var ClientAction = AbstractAction.extend({
on_attach_callback: function () {
assert.step('on_attach_callback');
assert.ok(actionManager.currentDialogController,
"the currentDialogController should have been set already");
},
start: function () {
this.$el.addClass('o_test');
},
});
core.action_registry.add('test', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction({
tag: 'test',
target: 'new',
type: 'ir.actions.client',
});
assert.strictEqual($('.modal .o_test').length, 1,
"should have rendered the client action in a dialog");
assert.verifySteps(['on_attach_callback']);
actionManager.destroy();
delete core.action_registry.map.test;
});
QUnit.module('Actions in target="inline"');
QUnit.test('form views for actions in target="inline" open in edit mode', async function (assert) {
assert.expect(5);
var actionManager = await 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);
},
});
await actionManager.doAction(6);
assert.containsOnce(actionManager, '.o_form_view.o_form_editable',
"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"', async function (assert) {
assert.expect(7);
this.actions[0].target = 'fullscreen';
var actionManager = await 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');
},
},
});
await actionManager.doAction(1);
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.containsOnce(actionManager, '.o_kanban_view',
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
'toggle_fullscreen',
]);
actionManager.destroy();
});
QUnit.test('fullscreen on action change: back to a "current" action', async function (assert) {
assert.expect(3);
this.actions[0].target = 'fullscreen';
this.archs['partner,false,form'] = '' +
' ' +
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
toggle_fullscreen: function (ev) {
var fullscreen = ev.data.fullscreen;
switch (toggleFullscreenCalls) {
case 0:
assert.strictEqual(fullscreen, false);
break;
case 1:
assert.strictEqual(fullscreen, true);
break;
case 2:
assert.strictEqual(fullscreen, false);
break;
}
},
},
});
var toggleFullscreenCalls = 0;
await actionManager.doAction(6);
toggleFullscreenCalls = 1;
await testUtils.dom.click(actionManager.$('button[name=1]'));
toggleFullscreenCalls = 2;
await testUtils.dom.click(actionManager.$('.breadcrumb li a:first'));
actionManager.destroy();
});
QUnit.test('fullscreen on action change: all "fullscreen" actions', async function (assert) {
assert.expect(3);
this.actions[5].target = 'fullscreen';
this.archs['partner,false,form'] = '' +
' ' +
' ';
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
toggle_fullscreen: function (ev) {
var fullscreen = ev.data.fullscreen;
assert.strictEqual(fullscreen, true);
},
},
});
await actionManager.doAction(6);
await testUtils.dom.click(actionManager.$('button[name=1]'));
await testUtils.dom.click(actionManager.$('.breadcrumb li a:first'));
actionManager.destroy();
});
QUnit.module('"ir.actions.act_window_close" actions');
QUnit.test('close the currently opened dialog', async function (assert) {
assert.expect(2);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute an action in target="new"
await 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
await 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', async function (assert) {
assert.expect(3);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute an action in target="new"
await 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
await 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
await actionManager.doAction({type: 'ir.actions.act_window_close'}, options);
assert.verifySteps(['on_close']);
actionManager.destroy();
});
QUnit.test('doAction resolved with an action', async function (assert) {
assert.expect(4);
this.actions.push({
id: 21,
name: 'A Close Action',
type: 'ir.actions.act_window_close',
});
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction(21).then(function (action) {
assert.ok(action, "doAction should be resolved with an action");
assert.strictEqual(action.id, 21,
"should be resolved with correct action id");
assert.strictEqual(action.name, 'A Close Action',
"should be resolved with correct action name");
assert.strictEqual(action.type, 'ir.actions.act_window_close',
"should be resolved with correct action type");
actionManager.destroy();
});
});
QUnit.test('close action with provided infos', async function (assert) {
assert.expect(1);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
var options = {
on_close: function (infos) {
assert.strictEqual(infos, 'just for testing',
"should have the correct close infos");
}
};
await actionManager.doAction({
type: 'ir.actions.act_window_close',
infos: 'just for testing',
}, options);
actionManager.destroy();
});
QUnit.test('history back calls on_close handler of dialog action', async function (assert) {
assert.expect(2);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// open a new dialog form
await actionManager.doAction(this.actions[4], {
on_close: function () {
assert.step('on_close');
},
});
actionManager.trigger_up('history_back');
assert.verifySteps(['on_close'], "should have called the on_close handler");
actionManager.destroy();
});
QUnit.test('properly drop client actions after new action is initiated', async function (assert) {
assert.expect(1);
var slowWillStartDef = testUtils.makeTestPromise();
var ClientAction = AbstractAction.extend({
willStart: function () {
return slowWillStartDef;
},
});
core.action_registry.add('slowAction', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction('slowAction');
actionManager.doAction(4);
slowWillStartDef.resolve();
await testUtils.nextTick();
assert.containsOnce(actionManager, '.o_kanban_view',
'should have loaded a kanban view');
actionManager.destroy();
delete core.action_registry.map.slowAction;
});
QUnit.test('abstract action does not crash on navigation_moves', async function (assert) {
assert.expect(1);
var ClientAction = AbstractAction.extend({
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction('ClientAction');
actionManager.trigger_up('navigation_move', {direction:'down'});
assert.ok(true); // no error so it's good
actionManager.destroy();
delete core.action_registry.ClientAction;
});
QUnit.test('fields in abstract action does not crash on navigation_moves', async function (assert) {
assert.expect(1);
// create a client action with 2 input field
var inputWidget;
var secondInputWidget;
var ClientAction = AbstractAction.extend(StandaloneFieldManagerMixin, {
init: function () {
this._super.apply(this, arguments);
StandaloneFieldManagerMixin.init.call(this);
},
start: function () {
var _self = this;
return this.model.makeRecord('partner', [{
name: 'display_name',
type: 'char',
}]).then(function (recordID) {
var record = _self.model.get(recordID);
inputWidget = new BasicFields.InputField(_self, 'display_name', record, {mode: 'edit',});
_self._registerWidget(recordID, 'display_name', inputWidget);
secondInputWidget = new BasicFields.InputField(_self, 'display_name', record, {mode: 'edit',});
secondInputWidget.attrs = {className:"secondField"};
_self._registerWidget(recordID, 'display_name', secondInputWidget);
inputWidget.appendTo(_self.$el);
secondInputWidget.appendTo(_self.$el);
});
}
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
await actionManager.doAction('ClientAction');
inputWidget.$el[0].focus();
var event = $.Event('keydown', {
which: $.ui.keyCode.TAB,
keyCode: $.ui.keyCode.TAB,
});
$(inputWidget.$el[0]).trigger(event);
assert.notOk(event.isDefaultPrevented(),
"the keyboard event default should not be prevented"); // no crash is good
actionManager.destroy();
delete core.action_registry.ClientAction;
});
QUnit.test('web client is not deadlocked when a view crashes', async function (assert) {
assert.expect(3);
var readOnFirstRecordDef = testUtils.makeTestPromise();
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'read' && args.args[0][0] === 1) {
return readOnFirstRecordDef;
}
return this._super.apply(this, arguments);
}
});
await actionManager.doAction(3);
// open first record in form view. this will crash and will not
// display a form view
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
readOnFirstRecordDef.reject("not working as intended");
assert.containsOnce(actionManager, '.o_list_view',
"there should still be a list view in dom");
// open another record, the read will not crash
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:eq(2)'));
assert.containsNone(actionManager, '.o_list_view',
"there should not be a list view in dom");
assert.containsOnce(actionManager, '.o_form_view',
"there should be a form view in dom");
actionManager.destroy();
});
QUnit.test('data-mobile attribute on action button, in desktop', async function (assert) {
assert.expect(2);
testUtils.mock.patch(ActionManager, {
doAction(action, options) {
assert.strictEqual(options.plop, undefined);
return this._super(...arguments);
},
});
this.archs['partner,75,kanban'] = `
`;
this.actions.push({
id: 100,
name: 'action 100',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[75, 'kanban']],
});
const actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data
});
await actionManager.doAction(100, {});
await testUtils.dom.click(actionManager.$('button[data-mobile]:first'));
actionManager.destroy();
testUtils.mock.unpatch(ActionManager);
});
QUnit.module('Search View Action');
QUnit.test('search view should keep focus during do_search', async function (assert) {
assert.expect(5);
/* One should be able to type something in the search view, press on enter to
* make the facet and trigger the search, then do this process
* over and over again seamlessly.
* Verifying the input's value is a lot trickier than verifying the search_read
* because of how native events are handled in tests
*/
var searchPromise = testUtils.makeTestPromise();
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
assert.step('search_read ' + args.domain);
if (_.isEqual(args.domain, [['foo', 'ilike', 'm']])) {
return searchPromise.then(this._super.bind(this, route, args));
}
}
return this._super.apply(this, arguments);
},
});
await actionManager.doAction(3);
await cpHelpers.editSearch(actionManager, "m");
await cpHelpers.validateSearch(actionManager);
assert.verifySteps(["search_read ", "search_read foo,ilike,m"]);
// Triggering the do_search above will kill the current searchview Input
await cpHelpers.editSearch(actionManager, "o");
// We have something in the input of the search view. Making the search_read
// return at this point will trigger the redraw of the view.
// However we want to hold on to what we just typed
searchPromise.resolve();
await cpHelpers.validateSearch(actionManager);
assert.verifySteps(["search_read |,foo,ilike,m,foo,ilike,o"]);
actionManager.destroy();
});
QUnit.test('Call twice clearUncommittedChanges in a row does not display twice the discard warning', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
clear_uncommitted_changes: function () {
actionManager.clearUncommittedChanges();
},
},
});
// execute an action and edit existing record
await actionManager.doAction(3);
await testUtils.dom.click(actionManager.$('.o_list_view .o_data_row:first'));
assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
await testUtils.dom.click($('.o_control_panel .o_form_button_edit'));
assert.containsOnce(actionManager, '.o_form_view.o_form_editable');
await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val');
actionManager.trigger_up('clear_uncommitted_changes');
await testUtils.nextTick();
assert.containsOnce($('body'), '.modal'); // confirm discard dialog
// confirm discard changes
await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
actionManager.trigger_up('clear_uncommitted_changes');
await testUtils.nextTick();
assert.containsNone($('body'), '.modal');
actionManager.destroy();
});
});
});