Files
odoo_source/addons/web/static/tests/chrome/action_manager_tests.js
T
Martin GeubelleandAaron Bohy f4b2060af5 [FIX] web: handle lazy load with previous actions
Before this rev. the lazy loading of views was correctly handled if there was
only one action in the action stack (which is usually the case when performing a
F5, the main use case of the view lazy loading).

Nevertheless, it might happen that the lazy-loading is used in top of other
actions (when performing a `doAction` with a form view for an action with
another view as first view, i.e. when leaving Studio while editing the form view).
The controller was `unshift` in the case (put in the first position of the
controller stack), which is not correct.

This causes the breadcrumb to be misordered and a traceback occured when trying
to access a breadcrumb element.

Co-authored-by: Aaron Bohy <aab@odoo.com>
2018-09-27 12:34:04 +02:00

3530 lines
128 KiB
JavaScript

odoo.define('web.action_manager_tests', function (require) {
"use strict";
var ReportClientAction = require('report.client_action');
var AbstractAction = require('web.AbstractAction');
var AbstractStorageService = require('web.AbstractStorageService');
var BasicFields = require('web.basic_fields');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var ListController = require('web.ListController');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
var RamStorage = require('web.RamStorage');
var ReportService = require('web.ReportService');
var SessionStorageService = require('web.SessionStorageService');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var createActionManager = testUtils.createActionManager;
QUnit.module('ActionManager', {
beforeEach: function () {
this.data = {
partner: {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "many2one", relation: 'partner'},
},
records: [
{id: 1, display_name: "First record", foo: "yop", bar: 2},
{id: 2, display_name: "Second record", foo: "blip", bar: 1},
{id: 3, display_name: "Third record", foo: "gnap", bar: 1},
{id: 4, display_name: "Fourth record", foo: "plop", bar: 2},
{id: 5, display_name: "Fifth record", foo: "zoup", bar: 2},
],
},
pony: {
fields: {
name: {string: 'Name', type: 'char'},
},
records: [
{id: 4, name: 'Twilight Sparkle'},
{id: 6, name: 'Applejack'},
{id: 9, name: 'Fluttershy'}
],
},
};
this.actions = [{
id: 1,
name: 'Partners Action 1',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[1, 'kanban']],
}, {
id: 2,
type: 'ir.actions.server',
}, {
id: 3,
name: 'Partners',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [1, 'kanban'], [false, 'form']],
}, {
id: 4,
name: 'Partners Action 4',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[1, 'kanban'], [2, 'list'], [false, 'form']],
}, {
id: 5,
name: 'Create a Partner',
res_model: 'partner',
target: 'new',
type: 'ir.actions.act_window',
views: [[false, 'form']],
}, {
id: 6,
name: 'Partner',
res_id: 2,
res_model: 'partner',
target: 'inline',
type: 'ir.actions.act_window',
views: [[false, 'form']],
}, {
id: 7,
name: "Some Report",
report_name: 'some_report',
report_type: 'qweb-pdf',
type: 'ir.actions.report',
}, {
id: 8,
name: 'Favorite Ponies',
res_model: 'pony',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
}, {
id: 9,
name: 'A Client Action',
tag: 'ClientAction',
type: 'ir.actions.client',
}, {
id: 10,
type: 'ir.actions.act_window_close',
}];
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', function (assert) {
assert.expect(3);
this.actions[3].views = [[false, 'form']];
this.actions[3].target = 'inline';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(4);
assert.ok(!$('.o_control_panel').is(':visible'),
"control panel should not be visible");
actionManager.doAction(1, {clear_breadcrumbs: true});
assert.ok($('.o_control_panel').is(':visible'),
"control panel should now be visible");
assert.strictEqual($('.o_control_panel .breadcrumb').text(), "Partners Action 1",
"should have only one current action visible in breadcrumbs");
actionManager.destroy();
});
QUnit.test('no widget memory leaks when doing some action stuff', function (assert) {
assert.expect(1);
var delta = 0;
testUtils.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
delta--;
this._super.apply(this, arguments);
},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(8);
var n = delta;
actionManager.doAction(4);
// kanban view is loaded, switch to list view
$('.o_control_panel .o_cp_switch_list').click();
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
// go back to action 7 in breadcrumbs
$('.o_control_panel .breadcrumb a:first').click();
assert.strictEqual(delta, n,
"should have properly destroyed all other widgets");
actionManager.destroy();
testUtils.unpatch(Widget);
});
QUnit.test('no widget memory leaks when executing actions in dialog', function (assert) {
assert.expect(1);
var delta = 0;
testUtils.patch(Widget, {
init: function () {
delta++;
this._super.apply(this, arguments);
},
destroy: function () {
if (!this.isDestroyed()) {
delta--;
}
this._super.apply(this, arguments);
},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
var n = delta;
actionManager.doAction(5);
actionManager.doAction({type: 'ir.actions.act_window_close'});
assert.strictEqual(delta, n,
"should have properly destroyed all widgets");
actionManager.destroy();
testUtils.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while switching view', function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
actionManager.doAction(4);
var n = delta;
actionManager.doAction(3, {clear_breadcrumbs: true});
// switch to the form view (this request is blocked)
def = $.Deferred();
actionManager.$('.o_list_view .o_data_row:first').click();
// execute another action meanwhile (don't block this request)
actionManager.doAction(4, {clear_breadcrumbs: true});
// unblock the switch to the form view in action 3
def.resolve();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while loading views', function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'load_views') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
// execute action 4 to know the number of widgets it instantiates
actionManager.doAction(4);
var n = delta;
// execute a first action (its 'load_views' RPC is blocked)
def = $.Deferred();
actionManager.doAction(3, {clear_breadcrumbs: true});
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4, {clear_breadcrumbs: true});
def.resolve();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.unpatch(Widget);
});
QUnit.test('no memory leaks when executing an action while loading data of default view', function (assert) {
assert.expect(1);
var def;
var delta = 0;
testUtils.patch(Widget, {
init: function () {
delta += 1;
this._super.apply(this, arguments);
},
destroy: function () {
delta -= 1;
this._super.apply(this, arguments);
},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
// execute action 4 to know the number of widgets it instantiates
actionManager.doAction(4);
var n = delta;
// execute a first action (its 'search_read' RPC is blocked)
def = $.Deferred();
actionManager.doAction(3, {clear_breadcrumbs: true});
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4, {clear_breadcrumbs: true});
def.resolve();
assert.strictEqual(n, delta,
"all widgets of action 3 should have been destroyed");
actionManager.destroy();
testUtils.unpatch(Widget);
});
QUnit.test('action with "no_breadcrumbs" set to true', function (assert) {
assert.expect(2);
_.findWhere(this.actions, {id: 4}).context = {no_breadcrumbs: true};
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
// push another action flagged with 'no_breadcrumbs=true'
actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 0,
"the breadcrumbs should be empty");
actionManager.destroy();
});
QUnit.test('on_reverse_breadcrumb handler is correctly called', function (assert) {
assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute action 3 and open a record in form view
actionManager.doAction(3);
actionManager.$('.o_list_view .o_data_row:first').click();
// execute action 4 without 'on_reverse_breadcrumb' handler, then go back
actionManager.doAction(4);
$('.o_control_panel .breadcrumb a:first').click();
assert.verifySteps([]);
// execute action 4 with an 'on_reverse_breadcrumb' handler, then go back
actionManager.doAction(4, {
on_reverse_breadcrumb: function () {
assert.step('on_reverse_breadcrumb');
}
});
$('.o_control_panel .breadcrumb a:first').click();
assert.verifySteps(['on_reverse_breadcrumb']);
actionManager.destroy();
});
QUnit.test('handles "history_back" event', function (assert) {
assert.expect(2);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(4);
actionManager.doAction(3);
actionManager.trigger_up('history_back');
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4',
"breadcrumbs should display the display_name of the action");
actionManager.destroy();
});
QUnit.test('stores and restores scroll position', function (assert) {
assert.expect(7);
var left;
var top;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
getScrollPosition: function (ev) {
assert.step('getScrollPosition');
ev.data.callback({left: left, top: top});
},
scrollTo: function (ev) {
assert.step('scrollTo left ' + ev.data.left + ', top ' + ev.data.top);
},
},
});
// execute a first action and simulate a scroll
assert.step('execute action 3');
actionManager.doAction(3);
left = 50;
top = 100;
// execute a second action (in which we don't scroll)
assert.step('execute action 4');
actionManager.doAction(4);
// go back using the breadcrumbs
assert.step('go back to action 3');
$('.o_control_panel .breadcrumb a').click();
assert.verifySteps([
'execute action 3',
'execute action 4',
'getScrollPosition', // of action 3, before leaving it
'go back to action 3',
'getScrollPosition', // of action 4, before leaving it
'scrollTo left 50, top 100', // restore scroll position of action 3
]);
actionManager.destroy();
});
QUnit.module('Push State');
QUnit.test('properly push state', function (assert) {
assert.expect(3);
var stateDescriptions = [
{action: 4, model: "partner", title: "Partners Action 4", view_type: "kanban"},
{action: 8, model: "pony", title: "Favorite Ponies", view_type: "list"},
{action: 8, id: 4, model: "pony", title: "Twilight Sparkle", view_type: "form"},
];
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function (event) {
var descr = stateDescriptions.shift();
assert.deepEqual(_.extend({}, event.data.state), descr,
"should notify the environment of new state");
},
},
});
actionManager.doAction(4);
actionManager.doAction(8);
actionManager.$('tr.o_data_row:first').click();
actionManager.destroy();
});
QUnit.test('push state after action is loaded, not before', function (assert) {
assert.expect(5);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
mockRPC: function (route) {
assert.step(route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(4);
assert.verifySteps([
'/web/action/load',
'/web/dataset/call_kw/partner',
'/web/dataset/search_read',
'push_state'
]);
actionManager.destroy();
});
QUnit.test('do not push state for actions in target=new', function (assert) {
assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
});
actionManager.doAction(4);
assert.verifySteps(['push_state']);
actionManager.doAction(5);
assert.verifySteps(['push_state']);
actionManager.destroy();
});
QUnit.test('do not push state when action fails', function (assert) {
assert.expect(4);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
mockRPC: function (route, args) {
if (args.method === 'read') {
// this is the rpc to load form view
return $.Deferred().reject();
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(8);
assert.verifySteps(['push_state']);
actionManager.$('tr.o_data_row:first').click();
assert.verifySteps(['push_state']);
// we make sure here that the list view is still in the dom
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"there should still be a list view in dom");
actionManager.destroy();
});
QUnit.module('Load State');
QUnit.test('should not crash on invalid state', function (assert) {
assert.expect(2);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
res_model: 'partner', // the valid key for the model is 'model', not 'res_model'
});
assert.strictEqual(actionManager.$el.text(), '', "should display nothing");
assert.verifySteps([]);
actionManager.destroy();
});
QUnit.test('properly load client actions', function (assert) {
assert.expect(2);
var ClientAction = AbstractAction.extend({
className: 'o_client_action_test',
start: function () {
this.$el.text('Hello World');
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
action: 'HelloWorldTest',
});
assert.strictEqual(actionManager.$('.o_client_action_test').text(),
'Hello World', "should have correctly rendered the client action");
assert.verifySteps([]);
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('properly load act window actions', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
action: 1,
});
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('properly load records', function (assert) {
assert.expect(5);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
id: 2,
model: 'partner',
});
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Second record',
"should have opened the second record");
assert.verifySteps([
'load_views',
'read',
]);
actionManager.destroy();
});
QUnit.test('load requested view for act window actions', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
action: 3,
view_type: 'kanban',
});
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not have rendered a list view");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('lazy load multi record view if mono record one is requested', function (assert) {
assert.expect(11);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.loadState({
action: 3,
id: 2,
view_type: 'form',
});
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not have rendered a list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Second record',
"breadcrumbs should contain the display_name of the opened record");
// go back to Lst
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should now display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should not display the form view anymore");
assert.verifySteps([
'/web/action/load',
'load_views',
'read', // read the opened record
'/web/dataset/search_read', // search read when coming back to List
]);
actionManager.destroy();
});
QUnit.test('lazy load multi record view with previous action', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
debug: 1,
});
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");
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
$('.o_control_panel .breadcrumb a:last').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4Partners',
"the breadcrumb elements should be correctly ordered");
actionManager.destroy();
});
QUnit.test('change the viewType of the current action', function (assert) {
assert.expect(13);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should have rendered a list view");
// switch to kanban view
actionManager.loadState({
action: 3,
view_type: 'kanban',
});
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view anymore");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have switched to the kanban view");
// switch to form view, open record 4
actionManager.loadState({
action: 3,
id: 4,
view_type: 'form',
});
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"should not display the kanban view anymore");
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should have switched to the form view");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Fourth record',
"should have opened the requested record");
// verify steps to ensure that the whole action hasn't been re-executed
// (if it would have been, /web/action/load and load_views would appear
// several times)
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list view
'/web/dataset/search_read', // kanban view
'read', // form view
]);
actionManager.destroy();
});
QUnit.test('change the id of the current action', function (assert) {
assert.expect(11);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
// execute action 3 and open the first record in a form view
actionManager.doAction(3);
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should have rendered a form view");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"should have opened the first record");
// switch to record 4
actionManager.loadState({
action: 3,
id: 4,
view_type: 'form',
});
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should still display the form view");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Fourth record',
"should have switched to the requested record");
// verify steps to ensure that the whole action hasn't been re-executed
// (if it would have been, /web/action/load and load_views would appear
// twice)
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list view
'read', // form view, record 1
'read', // form view, record 4
]);
actionManager.destroy();
});
QUnit.test('should not push a loaded state', function (assert) {
assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
push_state: function () {
assert.step('push_state');
},
},
});
actionManager.loadState({action: 3});
assert.verifySteps([], "should not push the loaded state");
actionManager.$('tr.o_data_row:first').click();
assert.verifySteps(['push_state'],
"should push the state of it changes afterwards");
actionManager.destroy();
});
QUnit.test('should not push a loaded state of a client action', function (assert) {
assert.expect(4);
var ClientAction = Widget.extend({
init: function (parent, action, options) {
this._super.apply(this, arguments);
this.controllerID = options.controllerID;
},
start: function () {
var self = this;
var $button = $('<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 = 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',
});
},
},
});
actionManager.loadState({action: 9});
assert.verifySteps([], "should not push the loaded state");
actionManager.$('button').click();
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', function (assert) {
assert.expect(7);
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
className: 'o_client_action',
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.$el.text(this.a);
this.trigger_up('push_state', {
controllerID: this.controllerID,
state: {a: this.a},
});
},
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute the client action
actionManager.doAction(9);
assert.strictEqual(actionManager.$('.o_client_action').text(), 'default value',
"should have rendered the client action");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
// update param 'a' in the url
actionManager.loadState({
action: 9,
a: 'new value',
});
assert.strictEqual(actionManager.$('.o_client_action').text(), 'new value',
"should have rerendered the client action with the correct param");
assert.strictEqual($('.o_control_panel .breadcrumb li').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)', function (assert) {
assert.expect(14);
var RamStorageService = AbstractStorageService.extend({
name: 'session_storage',
storage: new RamStorage(),
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [RamStorageService],
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
testUtils.intercept(actionManager, 'call_service', function (ev) {
if (ev.data.service === 'session_storage') {
assert.step(ev.data.method);
}
}, true);
actionManager.doAction(4);
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should display a kanban view");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4',
"breadcrumbs should display the display_name of the action");
actionManager.loadState({
model: 'partner',
view_type: 'list',
});
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4',
"should still be in the same action");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"should no longer display a kanban view");
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"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', function (assert) {
assert.expect(6);
var def = $.Deferred();
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
assert.step(route);
if (route === '/web/action/load') {
return def.then(_.constant(result));
}
return result;
},
});
actionManager.doAction(4);
actionManager.doAction(8);
def.resolve();
// action 4 loads a kanban view first, 6 loads a list view. We want a list
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
'there should be a list view in DOM');
assert.verifySteps([
'/web/action/load', // load action 4
'/web/action/load', // load action 6
'/web/dataset/call_kw/pony', // load views for action 6
'/web/dataset/search_read', // search read for list view action 6
]);
actionManager.destroy();
});
QUnit.test('handle switching view and switching back on slow network', function (assert) {
assert.expect(8);
var def = $.Deferred();
var defs = [$.when(), def, $.when()];
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
assert.step(route);
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
var def = defs.shift();
return def.then(_.constant(result));
}
return result;
},
});
actionManager.doAction(4);
// kanban view is loaded, switch to list view
$('.o_control_panel .o_cp_switch_list').click();
// here, list view is not ready yet, because def is not resolved
// switch back to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
// here, we want the kanban view to reload itself, regardless of list view
assert.verifySteps([
"/web/action/load", // initial load action
"/web/dataset/call_kw/partner", // load views
"/web/dataset/search_read", // search_read for kanban view
"/web/dataset/search_read", // search_read for list view (not resolved yet)
"/web/dataset/search_read" // search_read for kanban view reload (not resolved yet)
]);
// we resolve def => list view is now ready (but we want to ignore it)
def.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"there should be a kanban view in dom");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"there should not be a list view in dom");
actionManager.destroy();
});
QUnit.test('when an server action takes too much time...', function (assert) {
assert.expect(1);
var def = $.Deferred();
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
if (route === '/web/action/run') {
return def.then(_.constant(1));
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(2);
actionManager.doAction(4);
def.resolve();
assert.strictEqual($('.o_control_panel .breadcrumb li.active').text(), 'Partners Action 4',
'action 4 should be loaded');
actionManager.destroy();
});
QUnit.test('clicking quickly on breadcrumbs...', function (assert) {
assert.expect(1);
var def = $.when();
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'read') {
return def.then(_.constant(result));
}
return result;
},
});
// create a situation with 3 breadcrumbs: kanban/form/list
actionManager.doAction(4);
actionManager.$('.o_kanban_record:first').click();
actionManager.doAction(8);
// now, the next read operations will be deferred (this is the read
// operation for the form view reload)
def = $.Deferred();
// click on the breadcrumbs for the form view, then on the kanban view
// before the form view is fully reloaded
$('.o_control_panel .breadcrumb li:eq(1)').click();
$('.o_control_panel .breadcrumb li:eq(0)').click();
// resolve the form view read
def.resolve();
assert.strictEqual($('.o_control_panel .breadcrumb li.active').text(), 'Partners Action 4',
'action 4 should be loaded and visible');
actionManager.destroy();
});
QUnit.test('execute a new action while loading a lazy-loaded controller', function (assert) {
assert.expect(15);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method || route);
if (route === '/web/dataset/search_read' && args.model === 'partner') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
actionManager.loadState({
action: 4,
id: 2,
view_type: 'form',
});
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view of action 4");
// click to go back to Kanban (this request is blocked)
def = $.Deferred();
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should still display the form view of action 4");
// execute another action meanwhile (don't block this request)
actionManager.doAction(8, {clear_breadcrumbs: true});
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display action 8");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should no longer display the form view");
assert.verifySteps([
'/web/action/load', // load state action 4
'load_views', // load state action 4
'read', // read the opened record (action 4)
'/web/dataset/search_read', // blocked search read when coming back to Kanban (action 4)
'/web/action/load', // action 8
'load_views', // action 8
'/web/dataset/search_read', // search read action 8
]);
// unblock the switch to Kanban in action 4
def.resolve();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display action 8");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"should not display the kanban view of action 4");
assert.verifySteps([
'/web/action/load', // load state action 4
'load_views', // load state action 4
'read', // read the opened record (action 4)
'/web/dataset/search_read', // blocked search read when coming back to Kanban (action 4)
'/web/action/load', // action 8
'load_views', // action 8
'/web/dataset/search_read', // search read action 8
]);
actionManager.destroy();
});
QUnit.test('execute a new action while handling a call_button', function (assert) {
assert.expect(16);
var self = this;
var def = $.Deferred();
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/dataset/call_button') {
return def.then(_.constant(self.actions[0]));
}
return this._super.apply(this, arguments);
},
});
// execute action 3 and open a record in form view
actionManager.doAction(3);
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view of action 3");
// click on 'Call method' button (this request is blocked)
actionManager.$('.o_form_view button:contains(Call method)').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should still display the form view of action 3");
// execute another action
actionManager.doAction(8, {clear_breadcrumbs: true});
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view of action 8");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should no longer display the form view");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // list for action 3
'read', // form for action 3
'object', // click on 'Call method' button (this request is blocked)
'/web/action/load', // action 8
'load_views', // action 8
'/web/dataset/search_read', // list for action 8
]);
// unblock the call_button request
def.resolve();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display the list view of action 8");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"should not display action 1");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // list for action 3
'read', // form for action 3
'object', // click on 'Call method' button (this request is blocked)
'/web/action/load', // action 8
'load_views', // action 8
'/web/dataset/search_read', // list for action 8
]);
actionManager.destroy();
});
QUnit.test('execute a new action while switching to another controller', function (assert) {
assert.expect(15);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method || route);
if (args.method === 'read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view of action 3");
// switch to the form view (this request is blocked)
def = $.Deferred();
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display the list view of action 3");
// execute another action meanwhile (don't block this request)
actionManager.doAction(4, {clear_breadcrumbs: true});
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should display the kanban view of action 8");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should no longer display the list view");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // search read of list view of action 3
'read', // read the opened record of action 3 (this request is blocked)
'/web/action/load', // action 4
'load_views', // action 4
'/web/dataset/search_read', // search read action 4
]);
// unblock the switch to the form view in action 3
def.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should still display the kanban view of action 8");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should not display the form view of action 3");
assert.verifySteps([
'/web/action/load', // action 3
'load_views', // action 3
'/web/dataset/search_read', // search read of list view of action 3
'read', // read the opened record of action 3 (this request is blocked)
'/web/action/load', // action 4
'load_views', // action 4
'/web/dataset/search_read', // search read action 4
]);
actionManager.destroy();
});
QUnit.test('execute a new action while loading views', function (assert) {
assert.expect(10);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method || route);
if (args.method === 'load_views') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
// execute a first action (its 'load_views' RPC is blocked)
def = $.Deferred();
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view of action 3");
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4);
def.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should display the kanban view of action 4");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view of action 3");
assert.strictEqual($('.o_control_panel .breadcrumb li').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', function (assert) {
assert.expect(11);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
assert.step(args.method || route);
if (route === '/web/dataset/search_read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
// execute a first action (its 'search_read' RPC is blocked)
def = $.Deferred();
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view of action 3");
// execute another action meanwhile (and unlock the RPC)
actionManager.doAction(4);
def.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should display the kanban view of action 4");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should not display the list view of action 3");
assert.strictEqual($('.o_control_panel .breadcrumb li').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', function (assert) {
assert.expect(12);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read') {
return $.when(def).then(_.constant(result));
}
return result;
},
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
assert.strictEqual(actionManager.$('.o_list_view .o_data_row').length, 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 = $.Deferred();
$('.o_control_panel .o_cp_switch_list').click(); // click on the switch button
assert.strictEqual(actionManager.$('.o_list_view .o_data_row').length, 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
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"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();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"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', function (assert) {
assert.expect(3);
var ClientAction = AbstractAction.extend({
className: 'o_client_action_test',
start: function () {
this.$el.text('Hello World');
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = createActionManager({
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
}
});
actionManager.doAction('HelloWorldTest');
assert.strictEqual($('.o_control_panel:visible').length, 0, // AAB: global selector until the ControlPanel is moved from ActionManager to the Views
"shouldn't have rendered a control panel");
assert.strictEqual(actionManager.$('.o_client_action_test').text(),
'Hello World', "should have correctly rendered the client action");
assert.verifySteps([]);
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('client action with control panel', function (assert) {
assert.expect(4);
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
className: 'o_client_action_test',
start: function () {
this.$el.text('Hello World');
this.set('title', 'Hello'); // AAB: drop this and replace by getTitle()
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
var actionManager = createActionManager();
actionManager.doAction('HelloWorldTest');
assert.strictEqual($('.o_control_panel:visible').length, 1,
"should have rendered a control panel");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Hello',
"breadcrumbs should still display the title of the controller");
assert.strictEqual(actionManager.$('.o_client_action_test').text(),
'Hello World', "should have correctly rendered the client action");
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('state is pushed for client actions', function (assert) {
assert.expect(2);
var ClientAction = AbstractAction.extend(ControlPanelMixin, {
className: 'o_client_action_test',
start: function () {
this.$el.text('Hello World');
},
});
var actionManager = createActionManager({
intercepts: {
push_state: function () {
assert.step('push state');
},
},
});
core.action_registry.add('HelloWorldTest', ClientAction);
actionManager.doAction('HelloWorldTest');
assert.verifySteps(['push state']);
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.test('breadcrumb is updated on title change', function (assert) {
assert.expect(2);
var ClientAction = Widget.extend(ControlPanelMixin, {
className: 'o_client_action_test',
events: {
click: function () {
this.set("title", 'new title');
},
},
start: function () {
this.set("title", 'initial title');
this.$el.text('Hello World');
},
});
var actionManager = createActionManager();
core.action_registry.add('HelloWorldTest', ClientAction);
actionManager.doAction('HelloWorldTest');
assert.strictEqual($('ol.breadcrumb').text(), "initial title",
"should have initial title as breadcrumb content");
actionManager.$('.o_client_action_test').click();
assert.strictEqual($('ol.breadcrumb').text(), "new title",
"should have updated title as breadcrumb content");
actionManager.destroy();
delete core.action_registry.map.HelloWorldTest;
});
QUnit.module('Server actions');
QUnit.test('can execute server actions from db ID', function (assert) {
assert.expect(9);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/action/run') {
assert.strictEqual(args.action_id, 2,
"should call the correct server action");
return $.when(1); // execute action 1
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(2);
assert.strictEqual($('.o_control_panel:visible').length, 1,
"should have rendered a control panel");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'/web/action/run',
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('handle server actions returning false', function (assert) {
assert.expect(9);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/action/run') {
return $.when(false);
}
return this._super.apply(this, arguments);
},
});
// execute an action in target="new"
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
actionManager.doAction(2);
assert.strictEqual($('.o_technical_modal').length, 0,
"should have closed the modal");
assert.verifySteps([
'/web/action/load', // action 5
'load_views',
'default_get',
'/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', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [ReportService],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return $.when('ok');
}
return this._super.apply(this, arguments);
},
session: {
get_file: function (params) {
assert.step(params.url);
params.success();
params.complete();
},
},
});
actionManager.doAction(7, {
on_close: function () {
assert.step('on_close');
},
});
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'/web/static/src/img/spin.png', // block UI image
'/report/download',
'on_close',
]);
actionManager.destroy();
});
QUnit.test('should trigger a notification if wkhtmltopdf is to upgrade', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [ReportService],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return $.when('upgrade');
}
return this._super.apply(this, arguments);
},
session: {
get_file: function (params) {
assert.step(params.url);
params.success();
params.complete();
},
},
intercepts: {
notification: function () {
assert.step('notification');
},
},
});
actionManager.doAction(7);
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'notification',
'/web/static/src/img/spin.png', // block UI image
'/report/download',
]);
actionManager.destroy();
});
QUnit.test('should open the report client action if wkhtmltopdf is broken', function (assert) {
assert.expect(6);
// 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.patch(ReportClientAction, {
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.iframe.src = 'test ' + self.iframe.getAttribute('src');
});
}
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [ReportService],
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/report/check_wkhtmltopdf') {
return $.when('broken');
}
if (route === 'test /report/html/some_report') {
return $.when();
}
return this._super.apply(this, arguments);
},
session: {
get_file: function (params) {
assert.step(params.url); // should not be called
},
},
intercepts: {
notification: function () {
assert.step('notification');
},
},
});
actionManager.doAction(7);
assert.strictEqual(actionManager.$('.o_report_iframe').length, 1,
"should have opened the report client action");
assert.verifySteps([
'/web/action/load',
'/report/check_wkhtmltopdf',
'notification',
'test /report/html/some_report', // report client action's iframe
]);
actionManager.destroy();
testUtils.unpatch(ReportClientAction);
});
QUnit.module('Window Actions');
QUnit.test('can execute act_window actions from db ID', function (assert) {
assert.expect(6);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(1);
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('can switch between views', function (assert) {
assert.expect(18);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
// switch to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should no longer display the list view");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should display the kanban view");
// switch back to list view
$('.o_control_panel .o_cp_switch_list').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"should no longer display the kanban view");
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should no longer display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view");
assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop',
"should have opened the correct record");
// go back to list view using the breadcrumbs
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should no longer display the form view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list
'/web/dataset/search_read', // kanban
'/web/dataset/search_read', // list
'read', // form
'/web/dataset/search_read', // list
]);
actionManager.destroy();
});
QUnit.test('breadcrumbs are updated when switching between views', function (assert) {
assert.expect(10);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners',
"breadcrumbs should display the display_name of the action");
// switch to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should still be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners',
"breadcrumbs should still display the display_name of the action");
// switch back to list view
$('.o_control_panel .o_cp_switch_list').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should still be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners',
"breadcrumbs should still display the display_name of the action");
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// go back to list view using the breadcrumbs
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners',
"breadcrumbs should display the display_name of the action");
actionManager.destroy();
});
QUnit.test('switch buttons are updated when switching between views', function (assert) {
assert.expect(13);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2,
"should have two switch buttons (list and kanban)");
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button.active').length, 1,
"should have only one active button");
assert.ok($('.o_control_panel .o_cp_switch_buttons button:first').hasClass('o_cp_switch_list'),
"list switch button should be the first one");
assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'),
"list should be the active view");
// switch to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2,
"should still have two switch buttons (list and kanban)");
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button.active').length, 1,
"should still have only one active button");
assert.ok($('.o_control_panel .o_cp_switch_buttons button:first').hasClass('o_cp_switch_list'),
"list switch button should still be the first one");
assert.ok($('.o_control_panel .o_cp_switch_kanban').hasClass('active'),
"kanban should now be the active view");
// switch back to list view
$('.o_control_panel .o_cp_switch_list').click();
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2,
"should still have two switch buttons (list and kanban)");
assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'),
"list should now be the active view");
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 0,
"should not have any switch buttons");
// go back to list view using the breadcrumbs
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual($('.o_control_panel .o_cp_switch_buttons button').length, 2,
"should have two switch buttons (list and kanban)");
assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'),
"list should be the active view");
actionManager.destroy();
});
QUnit.test('pager is updated when switching between views', function (assert) {
assert.expect(10);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-5',
"value should be correct for kanban");
assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5',
"limit should be correct for kanban");
// switch to list view
$('.o_control_panel .o_cp_switch_list').click();
assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-3',
"value should be correct for list");
assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5',
"limit should be correct for list");
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1',
"value should be correct for form");
assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '3',
"limit should be correct for form");
// go back to list view using the breadcrumbs
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-3',
"value should be correct for list");
assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5',
"limit should be correct for list");
// switch back to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual($('.o_control_panel .o_pager_value').text(), '1-5',
"value should be correct for kanban");
assert.strictEqual($('.o_control_panel .o_pager_limit').text(), '5',
"limit should be correct for kanban");
actionManager.destroy();
});
QUnit.test('there is no flickering when switching between views', function (assert) {
assert.expect(20);
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function () {
var result = this._super.apply(this, arguments);
return $.when(def).then(_.constant(result));
},
});
actionManager.doAction(3);
// switch to kanban view
def = $.Deferred();
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display the list view");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"shouldn't display the kanban view yet");
def.resolve();
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"shouldn't display the list view anymore");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should now display the kanban view");
// switch back to list view
def = $.Deferred();
$('.o_control_panel .o_cp_switch_list').click();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should still display the kanban view");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"shouldn't display the list view yet");
def.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 0,
"shouldn't display the kanban view anymore");
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should now display the list view");
// open a record in form view
def = $.Deferred();
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"shouldn't display the form view yet");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should still be one controller in the breadcrumbs");
def.resolve();
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"should no longer display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should display the form view");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
// go back to list view using the breadcrumbs
def = $.Deferred();
$('.o_control_panel .breadcrumb a').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should still display the form view");
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"shouldn't display the list view yet");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should still be two controllers in the breadcrumbs");
def.resolve();
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should no longer display the form view");
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
actionManager.destroy();
});
QUnit.test('breadcrumbs are updated when display_name changes', function (assert) {
assert.expect(4);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// switch to edit mode and change the display_name
$('.o_control_panel .o_form_button_edit').click();
actionManager.$('.o_field_widget[name=display_name]').val('New name').trigger('input');
$('.o_control_panel .o_form_button_save').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should still be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'New name',
"breadcrumbs should contain the display_name of the opened record");
actionManager.destroy();
});
QUnit.test('reload previous controller when discarding a new record', function (assert) {
assert.expect(8);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
// create a new record
$('.o_control_panel .o_list_button_add').click();
assert.strictEqual(actionManager.$('.o_form_view.o_form_editable').length, 1,
"should have opened the form view in edit mode");
// discard
$('.o_control_panel .o_form_button_cancel').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should have switched back to the list view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list
'default_get', // form
'/web/dataset/search_read', // list
]);
actionManager.destroy();
});
QUnit.test('requests for execute_action of type object are handled', function (assert) {
assert.expect(10);
var self = this;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/dataset/call_button') {
assert.deepEqual(args, {
args: [[1], {some_key: 2}],
method: 'object',
model: 'partner',
}, "should call route with correct arguments");
var record = _.findWhere(self.data.partner.records, {id: args.args[0][0]});
record.foo = 'value changed';
return $.when(false);
}
return this._super.apply(this, arguments);
},
session: {user_context: {
some_key: 2,
}},
});
actionManager.doAction(3);
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop',
"check initial value of 'yop' field");
// click on 'Call method' button (should call an Object method)
actionManager.$('.o_form_view button:contains(Call method)').click();
assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'value changed',
"'yop' has been changed by the server, and should be updated in the UI");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list for action 3
'read', // form for action 3
'object', // click on 'Call method' button
'read', // re-read form view
]);
actionManager.destroy();
});
QUnit.test('requests for execute_action of type action are handled', function (assert) {
assert.expect(11);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
// click on 'Execute action' button (should execute an action)
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two parts in the breadcrumbs");
actionManager.$('.o_form_view button:contains(Execute action)').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3,
"the returned action should have been stacked over the previous one");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"the returned action should have been executed");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list for action 3
'read', // form for action 3
'/web/action/load', // click on 'Execute action' button
'load_views',
'/web/dataset/search_read', // kanban for action 4
]);
actionManager.destroy();
});
QUnit.test('requests for execute_action of type object: disable buttons', function (assert) {
assert.expect(2);
var self = this;
var def;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_button') {
return $.when(false);
} else if (args.method === 'read') {
// Block the 'read' call
var result = this._super.apply(this, arguments);
return $.when(def).then(_.constant(result));
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
// click on 'Call method' button (should call an Object method)
def = $.Deferred();
actionManager.$('.o_form_view button:contains(Call method)').click();
// 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();
// 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', function (assert) {
assert.expect(11);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
// open the first record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'yop',
"should have opened the correct record");
// go back to list view using the breadcrumbs
$('.o_control_panel .breadcrumb a').click();
// open the second record in form view
actionManager.$('.o_list_view .o_data_row:nth(1)').click();
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Second record',
"breadcrumbs should contain the display_name of the opened record");
assert.strictEqual(actionManager.$('.o_field_widget[name=foo]').text(), 'blip',
"should have opened the correct record");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read', // list
'read', // form
'/web/dataset/search_read', // list
'read', // form
]);
actionManager.destroy();
});
QUnit.test('restore previous view state when switching back', function (assert) {
assert.expect(5);
this.actions[2].views.unshift([false, 'graph']);
this.archs['partner,false,graph'] = '<graph></graph>';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.ok($('.o_control_panel .fa-bar-chart-o').hasClass('active'),
"bar chart button is active");
assert.ok(!$('.o_control_panel .fa-line-chart').hasClass('active'),
"line chart button is not active");
// display line chart
$('.o_control_panel .fa-line-chart').click();
assert.ok($('.o_control_panel .fa-line-chart').hasClass('active'),
"line chart button is now active");
// switch to kanban and back to graph view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual($('.o_control_panel .fa-line-chart').length, 0,
"graph buttons are no longer in control panel");
$('.o_control_panel .o_cp_switch_graph').click();
assert.ok($('.o_control_panel .fa-line-chart').hasClass('active'),
"line chart button is still active");
actionManager.destroy();
});
QUnit.test('view switcher is properly highlighted in graph view', function (assert) {
assert.expect(4);
// note: this test should be moved to graph tests ?
this.actions[2].views.splice(1, 1, [false, 'graph']);
this.archs['partner,false,graph'] = '<graph></graph>';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.ok($('.o_control_panel .o_cp_switch_list').hasClass('active'),
"list button in control panel is active");
assert.ok(!$('.o_control_panel .o_cp_switch_graph').hasClass('active'),
"graph button in control panel is not active");
// switch to graph view
$('.o_control_panel .o_cp_switch_graph').click();
assert.ok(!$('.o_control_panel .o_cp_switch_list').hasClass('active'),
"list button in control panel is not active");
assert.ok($('.o_control_panel .o_cp_switch_graph').hasClass('active'),
"graph button in control panel is active");
actionManager.destroy();
});
QUnit.test('can interact with search view', function (assert) {
assert.expect(2);
this.archs['partner,false,search'] = '<search>'+
'<group>'+
'<filter name="foo" string="foo" context="{\'group_by\': \'foo\'}"/>' +
'</group>'+
'</search>';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.ok(!actionManager.$('.o_list_view').hasClass('o_list_view_grouped'),
"list view is not grouped");
// open search view sub menus
$('.o_control_panel .o_searchview_more').click();
// open group by dropdown
$('.o_control_panel .o_cp_right button:contains(Group By)').click();
// click on first link
$('.o_control_panel .o_group_by_menu a:first').click();
assert.ok(actionManager.$('.o_list_view').hasClass('o_list_view_grouped'),
'list view is now grouped');
actionManager.destroy();
});
QUnit.test('can open a many2one external window', function (assert) {
// AAB: this test could be merged with 'many2ones in form views' in relational_fields_tests.js
assert.expect(8);
this.data.partner.records[0].bar = 2;
this.archs['partner,false,search'] = '<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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(route);
if (args.method === "get_formview_id") {
return $.when(false);
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(3);
// open first record in form view
actionManager.$('.o_data_row:first').click();
// click on edit
$('.o_control_panel .o_form_button_edit').click();
// click on external button for m2o
actionManager.$('.o_external_button').click();
assert.verifySteps([
'/web/action/load', // initial load action
'/web/dataset/call_kw/partner', // load views
'/web/dataset/search_read', // read list view data
'/web/dataset/call_kw/partner/read', // read form view data
'/web/dataset/call_kw/partner/get_formview_id', // get form view id
'/web/dataset/call_kw/partner', // load form view for modal
'/web/dataset/call_kw/partner/read' // read data for m2o record
]);
actionManager.destroy();
});
QUnit.test('ask for confirmation when leaving a "dirty" view', function (assert) {
assert.expect(4);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(4);
// open record in form view
actionManager.$('.o_kanban_record:first').click();
// edit record
$('.o_control_panel button.o_form_button_edit').click();
actionManager.$('input[name="foo"]').val('pinkypie').trigger('input');
// go back to kanban view
$('.o_control_panel .breadcrumb li:first a').click();
assert.strictEqual($('.modal .modal-body').text(),
"The record has been modified, your changes will be discarded. Do you want to proceed?",
"should display a modal dialog to confirm discard action");
// cancel
$('.modal .modal-footer button.btn-default').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"should still be in form view");
// go back again to kanban view
$('.o_control_panel .breadcrumb li:first a').click();
// confirm discard
$('.modal .modal-footer button.btn-primary').click();
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should no longer be in form view");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should be in kanban view");
actionManager.destroy();
});
QUnit.test('limit set in action is passed to each created controller', function (assert) {
assert.expect(2);
_.findWhere(this.actions, {id: 3}).limit = 2;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_data_row').length, 2,
"should only display 2 record");
// switch to kanban view
$('.o_control_panel .o_cp_switch_kanban').click();
assert.strictEqual(actionManager.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
"should only display 2 record");
actionManager.destroy();
});
QUnit.test('go back to a previous action using the breadcrumbs', function (assert) {
assert.expect(10);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
// open a record in form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// push another action on top of the first one, and come back to the form view
actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3,
"there should be three controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners Action 4',
"breadcrumbs should contain the name of the current action");
// go back using the breadcrumbs
$('.o_control_panel .breadcrumb a:nth(1)').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2,
"there should be two controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'First record',
"breadcrumbs should contain the display_name of the opened record");
// push again the other action on top of the first one, and come back to the list view
actionManager.doAction(4);
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3,
"there should be three controllers in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners Action 4',
"breadcrumbs should contain the name of the current action");
// go back using the breadcrumbs
$('.o_control_panel .breadcrumb a:first').click();
assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1,
"there should be one controller in the breadcrumbs");
assert.strictEqual($('.o_control_panel .breadcrumb li:last').text(), 'Partners',
"breadcrumbs should contain the name of the current action");
actionManager.destroy();
});
QUnit.test('honor group_by specified in actions context', 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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(3);
assert.strictEqual(actionManager.$('.o_list_view_grouped').length, 1,
"should be grouped");
assert.strictEqual(actionManager.$('.o_group_header').length, 2,
"should be grouped by 'bar' (two groups) at first load");
// groupby 'bar' using the searchview
$('.o_control_panel .o_searchview_more').click(); // open search view sub menus
$('.o_control_panel .o_cp_right button:contains(Group By)').click(); // open groupby dropdown
$('.o_control_panel .o_group_by_menu a:first').click(); // click on 'Foo'
assert.strictEqual(actionManager.$('.o_group_header').length, 5,
"should be grouped by 'foo' (five groups)");
// remove the groupby in the searchview
$('.o_control_panel .o_searchview .o_facet_remove').click();
assert.strictEqual(actionManager.$('.o_list_view_grouped').length, 1,
"should still be grouped");
assert.strictEqual(actionManager.$('.o_group_header').length, 2,
"should be grouped by 'bar' (two groups) at reload");
actionManager.destroy();
});
QUnit.test('switch request to unknown view type', 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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(33);
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should display the list view");
// try to open a record in a form view
actionManager.$('.o_list_view .o_data_row:first').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"should still display the list view");
assert.strictEqual(actionManager.$('.o_form_view').length, 0,
"should not display the form view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
]);
actionManager.destroy();
});
QUnit.test('save current search', function (assert) {
assert.expect(4);
testUtils.patch(ListController, {
getContext: function () {
return {
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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
intercepts: {
create_filter: function (event) {
var filter = event.data.filter;
assert.deepEqual(filter.domain, [['bar', '=', 1]],
"should save the correct domain");
assert.deepEqual(filter.context, {shouldBeInFilterContext: true},
"should save the correct context");
},
},
});
actionManager.doAction(33);
assert.strictEqual(actionManager.$('.o_data_row').length, 5,
"should contain 5 records");
// filter on bar
$('.o_control_panel .o_filters_menu a:contains(Bar)').click();
assert.strictEqual(actionManager.$('.o_data_row').length, 2,
"should contain 2 records");
// save filter
$('.o_control_panel .o_save_search a').click(); // toggle 'Save current search'
$('.o_control_panel .o_save_name input[type=text]').val('some name'); // name the filter
$('.o_control_panel .o_save_name button').click(); // click on 'Save'
testUtils.unpatch(ListController);
actionManager.destroy();
});
QUnit.test("search menus are still available when switching between actions", function (assert) {
assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(1);
assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
"the search options should be available");
actionManager.doAction(3);
assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
"the search options should be available");
// go back using the breadcrumbs
$('.o_control_panel .breadcrumb a:first').click();
assert.strictEqual($('.o_search_options .o_dropdown:visible .o_filters_menu').length, 1,
"the search options should be available");
actionManager.destroy();
});
QUnit.test("doAction with option 'keepSearchView'", function (assert) {
assert.expect(4);
this.actions.push({
id: 33,
name: 'Partners',
res_model: 'partner',
search_view_id: [1, 'a specific search view'],
type: 'ir.actions.act_window',
views: [[false, 'list']],
});
var checkRPC = false;
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
if (checkRPC && route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [['bar', '=', 1]],
"should search with the correct domain");
}
return this._super.apply(this, arguments);
},
});
actionManager.doAction(33);
checkRPC = true;
$('.o_control_panel .o_filters_menu a:contains(Bar)').click(); // filter on bar
assert.strictEqual($('.o_control_panel .o_facet_values').text().trim(), 'Bar',
"the filter on Bar should appear in the search view");
actionManager.doAction(3, {keepSearchView: true});
assert.strictEqual($('.o_control_panel .o_facet_values').text().trim(), 'Bar',
"the filter on Bar should still be in the search view");
actionManager.destroy();
});
QUnit.test("current act_window action is stored in session_storage", function (assert) {
assert.expect(1);
var expectedAction = _.extend({}, _.findWhere(this.actions, {id: 3}), {
context: {},
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [SessionStorageService.extend({
setItem: function (key, value) {
assert.strictEqual(value, JSON.stringify(expectedAction),
"should store the executed action in the sessionStorage");
},
})],
});
actionManager.doAction(3);
actionManager.destroy();
});
QUnit.test("store evaluated context of current action in session_storage", 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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
services: [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
actionManager.doAction(3);
actionManager.$('.o_list_view .o_data_row:first').click();
// click on 'Execute action' button (it executes an action with a CompoundContext as context)
checkSessionStorage = true;
actionManager.$('.o_form_view button:contains(Execute action)').click();
actionManager.destroy();
});
QUnit.module('Actions in target="new"');
QUnit.test('can execute act_window actions in target="new"', function (assert) {
assert.expect(7);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(5);
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"should have rendered a form view in a modal");
assert.ok($('.o_technical_modal .modal-body').hasClass('o_act_window'),
"modal-body element should have classname 'o_act_window'");
assert.ok($('.o_technical_modal .o_form_view').hasClass('o_form_editable'),
"form view should be in edit mode");
assert.verifySteps([
'/web/action/load',
'load_views',
'default_get',
]);
actionManager.destroy();
});
QUnit.test('chained action on_close', function (assert) {
assert.expect(3);
function on_close() {
assert.step('Close Action');
};
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(5, {on_close: on_close});
// a target=new action shouldn't activate the on_close
actionManager.doAction(5);
assert.verifySteps([]);
// An act_window_close should trigger the on_close
actionManager.doAction(10);
assert.verifySteps(['Close Action']);
actionManager.destroy();
});
QUnit.test('footer buttons are moved to the dialog footer', function (assert) {
assert.expect(3);
this.archs['partner,false,form'] = '<form>' +
'<field name="display_name"/>' +
'<footer>' +
'<button string="Create" type="object" class="infooter"/>' +
'</footer>' +
'</form>';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction(5);
assert.strictEqual($('.o_technical_modal .modal-body button.infooter').length, 0,
"the button should not be in the body");
assert.strictEqual($('.o_technical_modal .modal-footer button.infooter').length, 1,
"the button should be in the footer");
assert.strictEqual($('.o_technical_modal .modal-footer button').length, 1,
"the modal footer should only contain one button");
actionManager.destroy();
});
QUnit.test('on_attach_callback is called for actions in target="new"', function (assert) {
assert.expect(4);
var ClientAction = AbstractAction.extend({
className: 'o_test',
on_attach_callback: function () {
assert.step('on_attach_callback');
assert.ok(actionManager.currentDialogController,
"the currentDialogController should have been set already");
},
});
core.action_registry.add('test', ClientAction);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
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', function (assert) {
assert.expect(5);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
});
actionManager.doAction(6);
assert.strictEqual(actionManager.$('.o_form_view.o_form_editable').length, 1,
"should have rendered a form view in edit mode");
assert.verifySteps([
'/web/action/load',
'load_views',
'read',
]);
actionManager.destroy();
});
QUnit.module('Actions in target="fullscreen"');
QUnit.test('correctly execute act_window actions in target="fullscreen"', function (assert) {
assert.expect(7);
this.actions[0].target = 'fullscreen';
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method || route);
return this._super.apply(this, arguments);
},
intercepts: {
toggle_fullscreen: function () {
assert.step('toggle_fullscreen');
},
},
});
actionManager.doAction(1);
assert.strictEqual($('.o_control_panel').length, 1,
"should have rendered a control panel");
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
"should have rendered a kanban view");
assert.verifySteps([
'/web/action/load',
'load_views',
'/web/dataset/search_read',
'toggle_fullscreen',
]);
actionManager.destroy();
});
QUnit.module('"ir.actions.act_window_close" actions');
QUnit.test('close the currently opened dialog', function (assert) {
assert.expect(2);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute an action in target="new"
actionManager.doAction(5);
assert.strictEqual($('.o_technical_modal .o_form_view').length, 1,
"should have rendered a form view in a modal");
// execute an 'ir.actions.act_window_close' action
actionManager.doAction({
type: 'ir.actions.act_window_close',
});
assert.strictEqual($('.o_technical_modal').length, 0,
"should have closed the modal");
actionManager.destroy();
});
QUnit.test('execute "on_close" only if there is no dialog to close', function (assert) {
assert.expect(3);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
// execute an action in target="new"
actionManager.doAction(5);
var options = {
on_close: assert.step.bind(assert, 'on_close'),
};
// execute an 'ir.actions.act_window_close' action
// should not call 'on_close' as there is a dialog to close
actionManager.doAction({type: 'ir.actions.act_window_close'}, options);
assert.verifySteps([]);
// execute again an 'ir.actions.act_window_close' action
// should call 'on_close' as there is no dialog to close
actionManager.doAction({type: 'ir.actions.act_window_close'}, options);
assert.verifySteps(['on_close']);
actionManager.destroy();
});
QUnit.test('doAction resolved with an action', function (assert) {
assert.expect(4);
this.actions.push({
id: 21,
name: 'A Close Action',
type: 'ir.actions.act_window_close',
});
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
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('properly drop client actions after new action is initiated', function (assert) {
assert.expect(1);
var slowWillStartDef = $.Deferred();
var ClientAction = AbstractAction.extend({
willStart: function () {
return slowWillStartDef;
},
});
core.action_registry.add('slowAction', ClientAction);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
actionManager.doAction('slowAction');
actionManager.doAction(4);
slowWillStartDef.resolve();
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
'should have loaded a kanban view');
actionManager.destroy();
delete core.action_registry.map.slowAction;
});
QUnit.test('abstract action does not crash on navigation_moves', function (assert) {
assert.expect(1);
var ClientAction = AbstractAction.extend({
});
core.action_registry.add('ClientAction', ClientAction);
var actionManager = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
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', function (assert) {
assert.expect(1);
var self = this;
// 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 = createActionManager({
actions: this.actions,
archs: this.archs,
data: this.data,
});
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', function (assert) {
assert.expect(3);
var readOnFirstRecordDef = $.Deferred();
var actionManager = 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);
}
});
actionManager.doAction(3);
// open first record in form view. this will crash and will not
// display a form view
actionManager.$('.o_list_view .o_data_row:first').click();
readOnFirstRecordDef.reject("not working as intended");
assert.strictEqual(actionManager.$('.o_list_view').length, 1,
"there should still be a list view in dom");
// open another record, the read will not crash
actionManager.$('.o_list_view .o_data_row:eq(2)').click();
assert.strictEqual(actionManager.$('.o_list_view').length, 0,
"there should not be a list view in dom");
assert.strictEqual(actionManager.$('.o_form_view').length, 1,
"there should be a form view in dom");
actionManager.destroy();
});
QUnit.module('Search View Action');
QUnit.test('search view should keep focus during do_search', 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 searchDeferred = $.Deferred();
var actionManager = 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 searchDeferred.then(this._super.bind(this, route, args));
}
}
return this._super.apply(this, arguments);
}
});
actionManager.doAction(3);
var $searchInput = $('.o_searchview input');
$searchInput.trigger($.Event('keypress', {key: 'm', which: 109, keyCode: 109}));
$searchInput.trigger($.Event('keydown', {key: 'Enter', which: 13, keyCode: 13}));
assert.verifySteps(["search_read ",
"search_read foo,ilike,m"]);
// Triggering the do_search above will kill the current searchview Input
$searchInput = $('.o_searchview input');
$searchInput.trigger($.Event('keypress', {key: 'o', which: 111, keyCode: 111}));
// 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
searchDeferred.resolve();
$searchInput.trigger($.Event('keydown', {key: 'Enter', which: 13, keyCode: 13}));
assert.verifySteps(["search_read ",
"search_read foo,ilike,m",
"search_read |,foo,ilike,m,foo,ilike,o"]);
actionManager.destroy();
});
});
});