Files
odoo_source/addons/web/static/tests/chrome/action_manager_tests.js
T
Aaron Bohy c14622fcd3 [FIX] web: correctly close confirm dialog
Let's assume the following scenario:
 - have an action in target new (e.g. a form view)
 - in the dialog, have an action/object button with confirm
   attribute
 - when clicking on that button, a confirm dialog opens
 - if validated, the following action returned by the server
   is again an action in target new

Before this commit, the confirm dialog remained in the DOM.

This issue occurred because it's parent wasn't correctly set (wrong
use of `this`), so when the first dialog was destroyed, the confirm
dialog wasn't automatically destroyed in turn.

OPW~2440712

closes odoo/odoo#66597

X-original-commit: 98f9cb4f692a15494cd8ef298673fe1c949c423c
Signed-off-by: Jorge Pinna Puissant (jpp) <jpp@odoo.com>
2021-02-22 12:13:22 +00:00

4669 lines
172 KiB
JavaScript

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': '<kanban><templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click"><field name="foo"/></div>' +
'</t></templates></kanban>',
// list views
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,2,list': '<tree limit="3"><field name="foo"/></tree>',
'pony,false,list': '<tree><field name="name"/></tree>',
// form views
'partner,false,form': '<form>' +
'<header>' +
'<button name="object" string="Call method" type="object"/>' +
'<button name="4" string="Execute action" type="action"/>' +
'</header>' +
'<group>' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</group>' +
'</form>',
'pony,false,form': '<form>' +
'<field name="name"/>' +
'</form>',
// search views
'partner,false,search': '<search><field name="foo" string="Foo"/></search>',
'partner,1,search': '<search>' +
'<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', 1)]"/>' +
'</search>',
'pony,false,search': '<search></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'));
await testUtils.nextTick(); // wait for clearUncommittedChanges
// 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'] = '<form>' +
'<field name="o2m">' +
'<tree><field name="foo"/></tree>' +
'<form><field name="foo"/></form>' +
'</field>' +
'</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'));
await testUtils.nextTick(); // wait for quick edit
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'] = '<form>' +
'<field name="o2m">' +
'<tree><field name="foo"/></tree>' +
'<form><field name="foo"/></form>' +
'</field>' +
'</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'));
await testUtils.nextTick(); // wait for quick edit
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'] = `
<form>
<field name="foo"/>
</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 = $('<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.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'));
await testUtils.nextTick(); // wait for clearUncommittedChanges
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 */`
<div class="custom-control-panel">My custom control panel</div>
`
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'] = '<header>' +
'<button name="8" string="Execute action" type="action"/>' +
'</header>';
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'] = '<graph></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'] = '<graph></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'] = '<search>'+
'<group>'+
'<filter name="foo" string="foo" context="{\'group_by\': \'foo\'}"/>' +
'</group>'+
'</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'] = '<search>'+
'<group>'+
'<filter name="foo" string="foo" context="{\'group_by\': \'foo\'}"/>' +
'</group>'+
'</search>';
this.archs['partner,false,form'] = '<form>' +
'<group>' +
'<field name="foo"/>' +
'<field name="bar"/>' +
'</group>' +
'</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('save when leaving a "dirty" view', async function (assert) {
assert.expect(4);
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC(route, { args, method, model }) {
if (model === 'partner' && method === 'write') {
assert.deepEqual(args, [
[1],
{ foo: 'pinkypie', },
]);
}
return this._super(...arguments);
}
});
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.containsNone(document.body, '.modal', "should not display a modal dialog");
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'] = '<search>'+
'<group>'+
'<filter name="foo" string="foo" context="{\'group_by\': \'foo\'}"/>' +
'</group>'+
'</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'] = '<tree default_order="foo desc"><field name="foo"/></tree>';
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(18);
this.data.partner.fields.bar.default = 1;
this.archs['partner,false,form'] = '<form>' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'<field name="bar" readonly="1"/>' +
'</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="display_name"]'), 'test');
await testUtils.fields.editInput(actionManager.$('input[name=foo]'), 'val');
await testUtils.dom.click(actionManager.$('.o_form_uri:contains(First record)'));
assert.containsNone(document.body, '.modal', 'should not open modal');
assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(),
"PartnerstestFirst record");
// go back to test using the breadcrumbs
await testUtils.dom.click(actionManager.$('.o_control_panel .breadcrumb-item:nth(1) a'));
// should be readonly and so saved
assert.containsOnce(actionManager, '.o_form_view.o_form_readonly');
assert.strictEqual(actionManager.$('.o_control_panel .breadcrumb-item').text(),
"Partnerstest");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // views of action 3
'/web/dataset/search_read', // list
'onchange', // edit input
'get_formview_action', // click on m2o
'load_views', // form view of dynamic action
'read', // form
'create', // form (create)
'read', // click breadcrumb
]);
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'] = '<form>' +
'<field name="display_name"/>' +
'<footer>' +
'<button string="Create" type="object" class="infooter"/>' +
'</footer>' +
'</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": `
<form>
<header>
<button string="Open dialog" name="5" type="action"/>
</header>
</form>
`,
"partner,view_ref,form": `
<form>
<footer>
<button string="I close the dialog" name="some_method" type="object" close="1"/>
</footer>
</form>
`,
"partner,false,search": "<search></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'] = `
<form>
<button name="method" string="Call method" type="object" confirm="Are you sure?"/>
</form>`;
this.archs['partner,1000,form'] = `<form>Another action</form>`;
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'] = '<form>' +
'<button name="1" type="action" class="oe_stat_button" />' +
'</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'] = '<form>' +
'<button name="1" type="action" class="oe_stat_button" />' +
'</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'] = `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="display_name"/>
<button
name="1"
string="Execute action"
type="action"
data-mobile='{"plop": 28}'/>
</div>
</t>
</templates>
</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 save twice', async function (assert) {
assert.expect(5);
let writeCalls = 0;
var actionManager = await createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
clear_uncommitted_changes: function () {
actionManager.clearUncommittedChanges();
},
},
mockRPC(route, { method }) {
if (method === 'write') {
writeCalls += 1;
}
return this._super(...arguments);
},
});
// 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.containsNone($('body'), '.modal');
actionManager.trigger_up('clear_uncommitted_changes');
await testUtils.nextTick();
assert.containsNone($('body'), '.modal');
assert.strictEqual(writeCalls, 1);
actionManager.destroy();
});
});
});