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