合并企业版代码(未测试,先提交到测试分支)

This commit is contained in:
qihao.gong@jikimo.com
2023-04-14 17:42:23 +08:00
parent 7a7b3d7126
commit d28525526a
1300 changed files with 513579 additions and 5426 deletions

View File

@@ -0,0 +1,12 @@
/** @odoo-module */
import { createWebClient } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { legacyServiceProvider } from "@web_enterprise/legacy/legacy_service_provider";
import { WebClientEnterprise } from "@web_enterprise/webclient/webclient";
export function createEnterpriseWebClient(params) {
params.WebClientClass = WebClientEnterprise;
registry.category("services").add("enterprise_legacy_service_provider", legacyServiceProvider);
return createWebClient(params);
}

View File

@@ -0,0 +1,128 @@
/** @odoo-module **/
import { createWebClient, doAction, getActionManagerServerData, loadState } from "@web/../tests/webclient/helpers";
import { click, getFixture, legacyExtraNextTick } from "@web/../tests/helpers/utils";
let serverData;
let target;
QUnit.module('ActionManager', {
beforeEach() {
serverData = getActionManagerServerData();
target = getFixture();
Object.assign(serverData, {
actions: {
1: {
id: 1,
name: 'Partners Action 1',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'kanban'], [false, 'form']],
},
2: {
id: 2,
name: 'Partners Action 2',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list'], [false, 'form']],
},
},
views: {
'partner,false,kanban': `
<kanban>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="foo"/>
</div>
</t>
</templates>
</kanban>`,
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,form':
`<form>
<group>
<field name="display_name"/>
</group>
</form>`,
'partner,false,search': '<search><field name="foo" string="Foo"/></search>',
},
models: {
partner: {
fields: {
foo: { string: "Foo", type: "char" },
},
records: [
{ id: 1, display_name: "First record", foo: "yop" },
],
},
},
});
},
});
QUnit.test('uses a mobile-friendly view by default (if possible)', async function (assert) {
const webClient = await createWebClient({ serverData });
// should default on a mobile-friendly view (kanban) for action 1
await doAction(webClient, 1);
assert.containsNone(target, '.o_list_view');
assert.containsOnce(target, '.o_kanban_view');
// there is no mobile-friendly view for action 2, should use the first one (list)
await doAction(webClient, 2);
assert.containsOnce(target, '.o_list_view');
assert.containsNone(target, '.o_kanban_view');
});
QUnit.test('lazy load mobile-friendly view', async function (assert) {
const mockRPC = (route, args) => {
assert.step(args.method || route);
};
const webClient = await createWebClient({ serverData, mockRPC });
await loadState(webClient, {
action: 1,
view_type: 'form',
});
assert.containsNone(target, '.o_list_view');
assert.containsNone(target, '.o_kanban_view');
assert.containsOnce(target, '.o_form_view');
// go back to lazy loaded view
await click(target, '.o_control_panel .breadcrumb .o_back_button');
await legacyExtraNextTick();
assert.containsNone(target, '.o_form_view');
assert.containsNone(target, '.o_list_view');
assert.containsOnce(target, '.o_kanban_view');
assert.verifySteps([
'/web/webclient/load_menus',
'/web/action/load',
'get_views',
'onchange', // default_get/onchange to open form view
'web_search_read', // web search read when coming back to Kanban
]);
});
QUnit.test('view switcher button should be displayed in dropdown on mobile screens', async function (assert) {
// This test will spawn a kanban view (mobile friendly).
// so, the "legacy" code won't be tested here.
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
assert.containsOnce(target.querySelector('.o_control_panel'), '.o_cp_switch_buttons > .o-dropdown > button');
assert.containsNone(target.querySelector('.o_control_panel'), '.o_cp_switch_buttons .o-dropdown--menu .o_switch_view.o_kanban');
assert.containsNone(target.querySelector('.o_control_panel'), '.o_cp_switch_buttons .o-dropdown--menu button.o_switch_view');
assert.hasClass(target.querySelector('.o_control_panel .o_cp_switch_buttons > .o-dropdown > button > i'), 'oi-view-kanban');
await click(target, '.o_control_panel .o_cp_switch_buttons > .o-dropdown > button');
assert.hasClass(target.querySelector('.o_cp_switch_buttons .o-dropdown--menu button.o_switch_view.o_kanban'), 'active');
assert.doesNotHaveClass(target.querySelector('.o_cp_switch_buttons .o-dropdown--menu button.o_switch_view.o_list'), 'active');
assert.hasClass(target.querySelector('.o_cp_switch_buttons .o-dropdown--menu button.o_switch_view.o_kanban'), 'oi-view-kanban');
});

View File

@@ -0,0 +1,160 @@
odoo.define('web_mobile.barcode.tests', function (require) {
"use strict";
const fieldRegistry = require('web.field_registry');
const FormView = require('web.FormView');
const { FieldMany2One } = require('web.relational_fields');
const { createView, dom, mock } = require('web.test_utils');
const FieldMany2OneBarcode = require('web_mobile.barcode_fields');
const BarcodeScanner = require('@web/webclient/barcode/barcode_scanner');
const NAME_SEARCH = "name_search";
const PRODUCT_PRODUCT = 'product.product';
const SALE_ORDER_LINE = 'sale_order_line';
const PRODUCT_FIELD_NAME = 'product_id';
const ARCHS = {
'product.product,false,kanban': `
<kanban><templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click"><field name="display_name"/></div>
</t>
</templates></kanban>`,
'product.product,false,search': '<search></search>',
};
async function mockRPC(route, args) {
const result = await this._super(...arguments);
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
const records = this.data[PRODUCT_PRODUCT].records
.filter((record) => record.barcode === args.kwargs.name)
.map((record) => [record.id, record.name]);
return records.concat(result);
}
return result;
}
QUnit.module('web_mobile', {
beforeEach() {
this.data = {
[PRODUCT_PRODUCT]: {
fields: {
id: {type: 'integer'},
name: {},
barcode: {},
},
records: [{
id: 111,
name: 'product_cable_management_box',
barcode: '601647855631',
}, {
id: 112,
name: 'product_n95_mask',
barcode: '601647855632',
}, {
id: 113,
name: 'product_surgical_mask',
barcode: '601647855633',
}],
},
[SALE_ORDER_LINE]: {
fields: {
id: {type: 'integer'},
[PRODUCT_FIELD_NAME]: {
string: PRODUCT_FIELD_NAME,
type: 'many2one',
relation: PRODUCT_PRODUCT
},
product_uom_qty: {type: 'integer'}
}
},
};
},
}, function () {
QUnit.test("web_mobile: barcode button in a mobile environment with single results", async function (assert) {
assert.expect(2);
// simulate a mobile environment
fieldRegistry.add('many2one_barcode', FieldMany2OneBarcode);
mock.patch(BarcodeScanner, {
isBarcodeScannerSupported: () => true,
scanBarcode: async () => this.data[PRODUCT_PRODUCT].records[0].barcode,
});
const form = await createView({
View: FormView,
arch: `
<form>
<sheet>
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
</sheet>
</form>`,
data: this.data,
model: SALE_ORDER_LINE,
archs: ARCHS,
mockRPC,
});
const $scanButton = form.$('.o_barcode');
assert.equal($scanButton.length, 1, "has scanner button");
await dom.click($scanButton);
const selectedId = form.renderer.state.data[PRODUCT_FIELD_NAME].res_id;
assert.equal(selectedId, this.data[PRODUCT_PRODUCT].records[0].id,
`product found and selected (${this.data[PRODUCT_PRODUCT].records[0].barcode})`);
form.destroy();
fieldRegistry.add('many2one_barcode', FieldMany2One);
mock.unpatch(BarcodeScanner);
});
QUnit.test("web_mobile: barcode button in a mobile environment with multiple results", async function (assert) {
assert.expect(4);
// simulate a mobile environment
fieldRegistry.add('many2one_barcode', FieldMany2OneBarcode);
mock.patch(BarcodeScanner, {
isBarcodeScannerSupported: () => true,
scanBarcode: async () => "mask",
});
const form = await createView({
View: FormView,
arch: `
<form>
<sheet>
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
</sheet>
</form>`,
data: this.data,
model: SALE_ORDER_LINE,
archs: ARCHS,
mockRPC,
});
const $scanButton = form.$('.o_barcode');
assert.equal($scanButton.length, 1, "has scanner button");
await dom.click($scanButton);
const $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
assert.equal($modal.find('.o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 2,
'there should be 2 records displayed');
await dom.click($modal.find('.o_legacy_kanban_view .o_kanban_record:first'));
const selectedId = form.renderer.state.data[PRODUCT_FIELD_NAME].res_id;
assert.equal(selectedId, this.data[PRODUCT_PRODUCT].records[1].id,
`product found and selected (${this.data[PRODUCT_PRODUCT].records[1].barcode})`);
form.destroy();
fieldRegistry.add('many2one_barcode', FieldMany2One);
mock.unpatch(BarcodeScanner);
});
});
});

View File

@@ -0,0 +1,155 @@
odoo.define('web_mobile.barcode.tests', function (require) {
"use strict";
const fieldRegistry = require('web.field_registry');
const FormView = require('web.FormView');
const { FieldMany2One } = require('web.relational_fields');
const { createView, dom, mock } = require('web.test_utils');
const FieldMany2OneBarcode = require('web_mobile.barcode_fields');
const BarcodeScanner = require('@web/webclient/barcode/barcode_scanner');
const NAME_SEARCH = "name_search";
const PRODUCT_PRODUCT = 'product.product';
const SALE_ORDER_LINE = 'sale_order_line';
const PRODUCT_FIELD_NAME = 'product_id';
const ARCHS = {
'product.product,false,list': `<tree>
<field name="display_name"/>
</tree>`,
'product.product,false,search': '<search></search>',
};
async function mockRPC(route, args) {
const result = await this._super(...arguments);
if (args.method === NAME_SEARCH && args.model === PRODUCT_PRODUCT) {
const records = this.data[PRODUCT_PRODUCT].records
.filter((record) => record.barcode === args.kwargs.name)
.map((record) => [record.id, record.name]);
return records.concat(result);
}
return result;
}
QUnit.module('web_mobile', {
beforeEach() {
this.data = {
[PRODUCT_PRODUCT]: {
fields: {
id: {type: 'integer'},
name: {},
barcode: {},
},
records: [{
id: 111,
name: 'product_cable_management_box',
barcode: '601647855631',
}, {
id: 112,
name: 'product_n95_mask',
barcode: '601647855632',
}, {
id: 113,
name: 'product_surgical_mask',
barcode: '601647855633',
}],
},
[SALE_ORDER_LINE]: {
fields: {
id: {type: 'integer'},
[PRODUCT_FIELD_NAME]: {
string: PRODUCT_FIELD_NAME,
type: 'many2one',
relation: PRODUCT_PRODUCT
},
}
},
};
},
}, function () {
QUnit.test("web_mobile: barcode button in a mobile environment with single results", async function (assert) {
assert.expect(2);
// simulate a mobile environment
fieldRegistry.add('many2one_barcode', FieldMany2OneBarcode);
mock.patch(BarcodeScanner, {
isBarcodeScannerSupported: () => true,
scanBarcode: async () => this.data[PRODUCT_PRODUCT].records[0].barcode,
});
const form = await createView({
View: FormView,
arch: `
<form>
<sheet>
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
</sheet>
</form>`,
data: this.data,
model: SALE_ORDER_LINE,
archs: ARCHS,
mockRPC,
});
const $scanButton = form.$('.o_barcode');
assert.containsOnce(form, $scanButton, "has scanner button");
await dom.click($scanButton);
const selectedId = form.renderer.state.data[PRODUCT_FIELD_NAME].res_id;
assert.equal(selectedId, this.data[PRODUCT_PRODUCT].records[0].id,
`product found and selected (${this.data[PRODUCT_PRODUCT].records[0].barcode})`);
form.destroy();
fieldRegistry.add('many2one_barcode', FieldMany2One);
mock.unpatch(BarcodeScanner);
});
QUnit.test("web_mobile: barcode button in a mobile environment with multiple results", async function (assert) {
// simulate a mobile environment
fieldRegistry.add('many2one_barcode', FieldMany2OneBarcode);
mock.patch(BarcodeScanner, {
isBarcodeScannerSupported: () => true,
scanBarcode: async () => "mask"
});
const form = await createView({
View: FormView,
arch: `
<form>
<sheet>
<field name="${PRODUCT_FIELD_NAME}" widget="many2one_barcode"/>
</sheet>
</form>`,
data: this.data,
model: SALE_ORDER_LINE,
archs: ARCHS,
mockRPC,
});
const $scanButton = form.$('.o_barcode');
assert.containsOnce(form, $scanButton, "has scanner button");
await dom.click($scanButton);
const $modal = $('.modal-dialog.modal-lg');
assert.containsOnce($('body'), $modal, 'there should be one modal opened in full screen');
assert.containsN($modal, '.o_legacy_list_view .o_data_row', 2,
'there should be 2 records displayed');
await dom.click($modal.find('.o_legacy_list_view .o_data_row:first'));
const selectedId = form.renderer.state.data[PRODUCT_FIELD_NAME].res_id;
assert.equal(selectedId, this.data[PRODUCT_PRODUCT].records[1].id,
`product found and selected (${this.data[PRODUCT_PRODUCT].records[1].barcode})`);
form.destroy();
fieldRegistry.add('many2one_barcode', FieldMany2One);
mock.unpatch(BarcodeScanner);
});
});
});

View File

@@ -0,0 +1,82 @@
odoo.define('web.action_menus_mobile_tests', function (require) {
"use strict";
const ActionMenus = require('web.ActionMenus');
const Registry = require('web.Registry');
const testUtils = require('web.test_utils');
const { createComponent } = testUtils;
QUnit.module('Components', {
beforeEach() {
this.action = {
res_model: 'hobbit',
};
this.view = {
type: 'form',
};
this.props = {
activeIds: [23],
context: {},
items: {
action: [
{ action: { id: 1 }, name: "What's taters, precious ?", id: 1 },
],
print: [
{ action: { id: 2 }, name: "Po-ta-toes", id: 2 },
],
},
};
// Patch the registry of the action menus
this.actionMenusRegistry = ActionMenus.registry;
ActionMenus.registry = new Registry();
},
afterEach() {
ActionMenus.registry = this.actionMenusRegistry;
},
}, function () {
QUnit.module('ActionMenus');
QUnit.test('Auto close the print dropdown after click inside an item', async function (assert) {
assert.expect(6);
const actionMenus = await createComponent(ActionMenus, {
env: {
device: {
isMobile: true
},
action: this.action,
view: this.view,
},
intercepts: {
'do-action': ev => assert.step('do-action'),
},
props: this.props,
async mockRPC(route, args) {
switch (route) {
case '/web/action/load':
const expectedContext = {
active_id: 23,
active_ids: [23],
active_model: 'hobbit',
};
assert.deepEqual(args.context, expectedContext);
assert.step('load-action');
return {context: {}, flags: {}};
default:
return this._super(...arguments);
}
},
});
await testUtils.controlPanel.toggleActionMenu(actionMenus, "Print");
assert.containsOnce(actionMenus.el, '.dropdown-menu-start',
"should display the dropdown menu");
await testUtils.controlPanel.toggleMenuItem(actionMenus, "Po-ta-toes");
assert.containsNone(actionMenus.el, '.dropdown-menu-start',
"should not display the dropdown menu");
assert.verifySteps(['load-action', 'do-action']);
});
});
});

View File

@@ -0,0 +1,245 @@
odoo.define('web.control_panel_mobile_tests', function (require) {
"use strict";
const FormView = require('web.FormView');
const testUtils = require('web.test_utils');
const cpHelpers = require('@web/../tests/search/helpers');
const { browser } = require("@web/core/browser/browser");
const { patchWithCleanup, getFixture } = require("@web/../tests/helpers/utils");
const { createControlPanel, createView } = testUtils;
const { createWebClient, doAction, getActionManagerServerData } = require('@web/../tests/webclient/helpers');
let serverData;
let target;
QUnit.module('Control Panel', {
beforeEach: function () {
target = getFixture();
this.actions = [{
id: 1,
name: "Yes",
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'list']],
}];
this.archs = {
'partner,false,list': '<tree><field name="foo"/></tree>',
'partner,false,search': `
<search>
<filter string="Active" name="my_projects" domain="[('boolean_field', '=', True)]"/>
<field name="foo" string="Foo"/>
</search>`,
};
this.data = {
partner: {
fields: {
foo: { string: "Foo", type: "char" },
boolean_field: { string: "I am a boolean", type: "boolean" },
},
records: [
{ id: 1, display_name: "First record", foo: "yop" },
],
},
};
const actions = {};
this.actions.forEach((act) => {
actions[act.xml_id || act.id] = act;
});
serverData = getActionManagerServerData();
Object.assign(serverData, { models: this.data, views: this.archs, actions });
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
clearTimeout: () => {},
});
},
}, function () {
QUnit.test('basic rendering', async function (assert) {
assert.expect(2);
const webClient = await createWebClient({serverData});
await doAction(webClient, 1);
assert.containsNone(target, '.o_control_panel .o_mobile_search',
"search options are hidden by default");
assert.containsOnce(target, '.o_control_panel .o_enable_searchview',
"should display a button to toggle the searchview");
});
QUnit.test("control panel appears at top on scroll event", async function (assert) {
assert.expect(12);
const MAX_HEIGHT = 800;
const MIDDLE_HEIGHT = 400;
const DELTA_TEST = 20;
const form = await createView({
View: FormView,
arch:
'<form>' +
'<sheet>' +
`<div style="height: ${2 * MAX_HEIGHT}px"></div>` +
'</sheet>' +
'</form>',
data: this.data,
model: 'partner',
res_id: 1,
});
const controlPanelEl = document.querySelector('.o_control_panel');
const controlPanelHeight = controlPanelEl.offsetHeight;
// Force container to have a scrollbar
controlPanelEl.parentElement.style.maxHeight = `${MAX_HEIGHT}px`;
const scrollAndAssert = async (targetHeight, expectedTopValue, hasStickyClass) => {
if (targetHeight !== null) {
controlPanelEl.parentElement.scrollTo(0, targetHeight);
await testUtils.nextTick();
}
const expectedPixelValue = `${expectedTopValue}px`;
assert.strictEqual(controlPanelEl.style.top, expectedPixelValue,
`Top must be ${expectedPixelValue} (after scroll to ${targetHeight})`);
if (hasStickyClass) {
assert.hasClass(controlPanelEl, 'o_mobile_sticky');
} else {
assert.doesNotHaveClass(controlPanelEl, 'o_mobile_sticky');
}
}
// Initial position (scrollTop: 0)
await scrollAndAssert(null, 0, false);
// Scroll down 800px (scrollTop: 800)
await scrollAndAssert(MAX_HEIGHT, -controlPanelHeight, true);
// Scoll up 20px (scrollTop: 780)
await scrollAndAssert(MAX_HEIGHT - DELTA_TEST, -controlPanelHeight + DELTA_TEST, true);
// Scroll up 380px (scrollTop: 400)
await scrollAndAssert(MIDDLE_HEIGHT, 0, true);
// Scroll down 200px (scrollTop: 800)
await scrollAndAssert(MAX_HEIGHT, -controlPanelHeight, true);
// Scroll up 400px (scrollTop: 0)
await scrollAndAssert(0, -controlPanelHeight, false);
form.destroy();
});
QUnit.test("mobile search: basic display", async function (assert) {
assert.expect(4);
const fields = {
birthday: { string: "Birthday", type: "date", store: true, sortable: true },
};
const searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"];
const params = {
cpModelConfig: {
arch: `
<search>
<filter name="birthday" date="birthday"/>
</search>`,
fields,
searchMenuTypes,
},
cpProps: { fields, searchMenuTypes },
};
const controlPanel = await createControlPanel(params);
// Toggle search bar controls
await testUtils.dom.click(controlPanel.el.querySelector("button.o_enable_searchview"));
// Open search view
await testUtils.dom.click(controlPanel.el.querySelector("button.o_toggle_searchview_full"));
// Toggle filter date
// Note: 'document.body' is used instead of 'controlPanel' because the
// search view is directly in the body.
await cpHelpers.toggleFilterMenu(document);
await cpHelpers.toggleMenuItem(document, "Birthday");
await cpHelpers.toggleMenuItemOption(document, "Birthday", 0);
assert.containsOnce(document.body, ".o_filter_menu");
assert.containsOnce(document.body, ".o_group_by_menu");
assert.containsOnce(document.body, ".o_comparison_menu");
assert.containsOnce(document.body, ".o_favorite_menu");
});
QUnit.test('mobile search: activate a filter through quick search', async function (assert) {
assert.expect(7);
let searchRPCFlag = false;
const mockRPC = (route, args) => {
if (searchRPCFlag && args.method === "web_search_read") {
assert.deepEqual(args.kwargs.domain, [['foo', 'ilike', 'A']],
"domain should have been properly transferred to list view");
}
};
const webClient = await createWebClient({serverData, mockRPC});
await doAction(webClient, 1);
assert.containsOnce(document.body, 'button.o_enable_searchview.oi-search',
"should display a button to open the searchview");
assert.containsNone(document.body, '.o_searchview_input_container',
"Quick search input should be hidden");
// open the search view
await testUtils.dom.click(document.querySelector('button.o_enable_searchview'));
assert.containsOnce(document.body, '.o_toggle_searchview_full',
"should display a button to expand the searchview");
assert.containsOnce(document.body, '.o_searchview_input_container',
"Quick search input should now be visible");
searchRPCFlag = true;
// use quick search input (search view is directly put in the body)
await cpHelpers.editSearch(document.body, "A");
await cpHelpers.validateSearch(document.body);
// close quick search
await testUtils.dom.click(document.querySelector('button.o_enable_searchview.fa-arrow-left'));
assert.containsNone(document.body, '.o_toggle_searchview_full',
"Expand icon shoud be hidden");
assert.containsNone(document.body, '.o_searchview_input_container',
"Quick search input should be hidden");
});
QUnit.test('mobile search: activate a filter in full screen search view', async function (assert) {
assert.expect(3);
const webClient = await createWebClient({ serverData });
await doAction(webClient, 1);
assert.containsNone(document.body, '.o_mobile_search');
// open the search view
await testUtils.dom.click(target.querySelector('button.o_enable_searchview'));
// open it in full screen
await testUtils.dom.click(target.querySelector('.o_toggle_searchview_full'));
assert.containsOnce(document.body, '.o_mobile_search');
await cpHelpers.toggleFilterMenu(document.body);
await cpHelpers.toggleMenuItem(document.body, "Active");
// closing search view
await testUtils.dom.click(
[...document.querySelectorAll('.o_mobile_search_button')].find(
e => e.innerText.trim() === "FILTER"
)
);
assert.containsNone(document.body, '.o_mobile_search');
});
});
});

View File

@@ -0,0 +1,409 @@
odoo.define('web_enterprise.form_tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('web_enterprise', {
beforeEach: function () {
this.data = {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
},
records: [{
id: 1,
display_name: "first record",
trululu: 4,
}, {
id: 2,
display_name: "second record",
trululu: 1,
}, {
id: 4,
display_name: "aaa",
}],
},
};
},
}, function () {
QUnit.module('Mobile FormView');
QUnit.test('statusbar buttons are correctly rendered in mobile', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<header>' +
'<button string="Confirm"/>' +
'<button string="Do it"/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<button name="display_name"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_statusbar_buttons a:contains(Action)').length, 1,
"statusbar should contain a button 'Action'");
assert.containsOnce(form, '.o_statusbar_buttons .dropdown-menu',
"statusbar should contain a dropdown");
assert.containsNone(form, '.o_statusbar_buttons .dropdown-menu:visible',
"dropdown should be hidden");
// open the dropdown
await testUtils.dom.click(form.$('.o_statusbar_buttons a'));
assert.containsOnce(form, '.o_statusbar_buttons .dropdown-menu:visible',
"dropdown should be visible");
assert.containsN(form, '.o_statusbar_buttons .dropdown-menu > button', 2,
"dropdown should contain 2 buttons");
form.destroy();
});
QUnit.test('statusbar "Action" button should be displayed only if there are multiple visible buttons', async function (assert) {
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<header>' +
'<button string="Confirm" attrs=\'{"invisible": [["display_name", "=", "first record"]]}\'/>' +
'<button string="Do it" attrs=\'{"invisible": [["display_name", "=", "first record"]]}\'/>' +
'</header>' +
'<sheet>' +
'<group>' +
'<field name="display_name"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
});
// if all buttons are invisible then there should be no action button
assert.containsNone(form, '.o_statusbar_buttons > btn-group > .dropdown-toggle',
"'Action' dropdown is not displayed as there are no visible buttons");
// there should be two invisible buttons
assert.containsN(form, '.o_statusbar_buttons > button.o_invisible_modifier', 2,
"Status bar should have two buttons with 'o_invisible_modifier' class");
// change display_name to update buttons modifiers and make it visible
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=display_name]'), 'test');
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_statusbar_buttons a:contains(Action)',
"statusbar should contain a button 'Action'");
assert.containsOnce(form, '.o_statusbar_buttons .dropdown-menu',
"statusbar should contain a dropdown");
form.destroy();
});
QUnit.test('statusbar "Action" button not displayed in edit mode with .oe_read_only button', async function (assert) {
assert.expect(2);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<header>
<button string="Share" type="action" class="oe_highlight oe_read_only"/>
<button string="Email" type="action" class="oe_highlight oe_read_only"/>
</header>
<sheet>
<group>
<field name="display_name"/>
</group>
</sheet>
</form>
`,
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
assert.containsNone(form, '.o_statusbar_buttons a:contains(Action)',
"'Action' button should not be there");
await testUtils.form.clickSave(form);
assert.containsOnce(form, '.o_statusbar_buttons a:contains(Action)',
"'Action' button should be there");
form.destroy();
});
QUnit.test(`statusbar "Action" button shouldn't be displayed for only one visible button`, async function (assert) {
assert.expect(3);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `<form>
<header>
<button string="Hola" attrs='{"invisible": [["display_name", "=", "first record"]]}'/>
<button string="Ciao"/>
</header>
<sheet>
<group>
<field name="display_name"/>
</group>
</sheet>
</form>`,
res_id: 1,
viewOptions: {
mode: 'edit',
},
});
// There should be a simple statusbar button and no action dropdown
assert.containsNone(form, '.o_statusbar_buttons a:contains(Action)', "should have no 'Action' dropdown");
assert.containsOnce(form, '.o_statusbar_buttons > button > span:contains(Ciao)', "should have a simple statusbar button 'Ciao'");
// change display_name to update buttons modifiers and make both buttons visible
await testUtils.fields.editInput(form.$('input[name=display_name]'), 'test');
// Now there should an action dropdown, because there are two visible buttons
assert.containsOnce(form, '.o_statusbar_buttons a:contains(Action)', "should have no 'Action' dropdown");
form.destroy();
});
QUnit.test(`statusbar widgets should appear in the statusbar dropdown only if there are multiple items`, async function (assert) {
assert.expect(4);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form string="Partners">
<header>
<widget name="attach_document" string="Attach document"/>
<button string="Ciao" attrs='{"invisible": [["display_name", "=", "first record"]]}'/>
</header>
<sheet>
<group>
<field name="display_name"/>
</group>
</sheet>
</form>
`,
res_id: 2,
viewOptions: {
mode: 'edit',
},
});
const dropdownActionButton = '.o_statusbar_buttons a:contains(Action)';
// Now there should an action dropdown, because there are two visible buttons
assert.containsOnce(form, dropdownActionButton, "should have 'Action' dropdown");
assert.containsN(form, `.o_statusbar_buttons .dropdown-menu > button`,
2, "should have 2 buttons in the dropdown");
// change display_name to update buttons modifiers and make one button visible
await testUtils.fields.editInput(form.$('input[name=display_name]'), 'first record');
// There should be a simple statusbar button and no action dropdown
assert.containsNone(form, dropdownActionButton, "shouldn't have 'Action' dropdown");
assert.containsOnce(form, `.o_statusbar_buttons > button:visible`,
"should have 1 button visible in the statusbar");
form.destroy();
});
QUnit.test(`Quick Edition: quick edit many2one`, async function (assert) {
assert.expect(1);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<sheet>
<group>
<field name="trululu" />
</group>
</sheet>
</form>
`,
archs: {
'partner,false,kanban': `
<kanban>
<templates><t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="display_name"/>
</div>
</t></templates>
</kanban>
`,
'partner,false,search': '<search></search>',
},
res_id: 2,
});
await testUtils.dom.click(form.$('.o_form_label'));
await testUtils.nextTick(); // wait for quick edit
const $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
form.destroy();
});
QUnit.test('statusbar "Action" dropdown should keep its open/close state', async function (assert) {
assert.expect(5);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<header>
<button string="Just more than one"/>
<button string="Confirm" attrs='{"invisible": [["display_name", "=", ""]]}'/>
<button string="Do it" attrs='{"invisible": [["display_name", "!=", ""]]}'/>
</header>
<sheet>
<field name="display_name"/>
</sheet>
</form>
`,
});
const dropdownMenuSelector = '.o_statusbar_buttons .dropdown-menu';
assert.containsOnce(form, dropdownMenuSelector,
"statusbar should contain a dropdown");
assert.doesNotHaveClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should be closed");
// open the dropdown
await testUtils.dom.click(form.el.querySelector('.o_statusbar_buttons .dropdown-toggle'));
assert.hasClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should be opened");
// change display_name to update buttons' modifiers
await testUtils.fields.editInput(form.el.querySelector('input[name="display_name"]'), 'test');
assert.containsOnce(form, dropdownMenuSelector,
"statusbar should contain a dropdown");
assert.hasClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should still be opened");
form.destroy();
});
QUnit.test(`statusbar "Action" dropdown's open/close state shouldn't be modified after 'onchange'`, async function (assert) {
assert.expect(5);
let resolveOnchange;
const onchangePromise = new Promise(resolve => {
resolveOnchange = resolve;
});
this.data.partner.onchanges = {
display_name: async () => {},
};
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<header>
<button name="create" string="Create Invoice" type="action"/>
<button name="send" string="Send by Email" type="action"/>
</header>
<sheet>
<field name="display_name" />
</sheet>
</form>
`,
async mockRPC(route, { method, args: [, , changedField] }) {
return method === 'onchange' && changedField === 'display_name' ? onchangePromise : this._super(...arguments);
},
});
const dropdownMenuSelector = '.o_statusbar_buttons .dropdown-menu';
assert.containsOnce(form, dropdownMenuSelector,
"statusbar should contain a dropdown");
assert.doesNotHaveClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should be closed");
await testUtils.fields.editInput(form.el.querySelector('input[name="display_name"]'), 'before onchange');
await testUtils.dom.click(form.el.querySelector('.o_statusbar_buttons .dropdown-toggle'));
assert.hasClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should be opened");
resolveOnchange({ value: { display_name: 'after onchange' } });
await testUtils.nextTick();
assert.strictEqual(form.el.querySelector('input[name="display_name"]').value, 'after onchange');
assert.hasClass(form.el.querySelector(dropdownMenuSelector), 'show',
"dropdown should still be opened");
form.destroy();
});
QUnit.test("preserve current scroll position on form view while closing dialog", async function (assert) {
assert.expect(6);
const form = await createView({
View: FormView,
arch: `<form>
<sheet>
<p style='height:500px'></p>
<field name="trululu"/>
<p style='height:500px'></p>
</sheet>
</form>`,
archs: {
"partner,false,kanban": `<kanban>
<templates><t t-name="kanban-box">
<div class="oe_kanban_global_click"><field name="display_name"/></div>
</t></templates>
</kanban>`,
"partner,false,search": "<search></search>",
},
data: this.data,
model: "partner",
res_id: 2,
debug: true, // need to be in the viewport to check scrollPosition
viewOptions: { mode: "edit" },
});
const scrollPosition = { top: 265, left: 0 };
window.scrollTo(scrollPosition);
assert.strictEqual(window.scrollY, scrollPosition.top, "Should have scrolled 265 px vertically");
assert.strictEqual(window.scrollX, scrollPosition.left, "Should be 0 px from left as it is");
// click on m2o field
await testUtils.dom.click(form.$(".o_field_many2one input"));
assert.strictEqual(window.scrollY, 0, "Should have scrolled to top (0) px");
assert.containsOnce($("body"), ".modal.o_modal_full", "there should be a many2one modal opened in full screen");
// click on back button
await testUtils.dom.click($(".modal").find(".modal-header .fa-arrow-left"));
assert.strictEqual(window.scrollY, scrollPosition.top, "Should have scrolled back to 265 px vertically");
assert.strictEqual(window.scrollX, scrollPosition.left, "Should be 0 px from left as it is");
form.destroy();
});
});
});

View File

@@ -0,0 +1,498 @@
odoo.define('web_enterprise.relational_fields_mobile_tests', function (require) {
"use strict";
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
QUnit.module('web_enterprise', {}, function () {
QUnit.module('relational_fields', {
beforeEach: function () {
this.data = {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
sibling_ids: {string: "Sibling", type: "many2many", relation: 'partner'},
p: { string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu' },
},
records: [{
id: 1,
display_name: "first record",
trululu: 4,
}, {
id: 2,
display_name: "second record",
trululu: 1,
}, {
id: 4,
display_name: "aaa",
}],
},
};
}
}, function () {
QUnit.module('FieldStatus');
QUnit.test('statusbar is rendered correclty on small devices', async function (assert) {
assert.expect(7);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:
'<form string="Partners">' +
'<header><field name="trululu" widget="statusbar"/></header>' +
'<field name="display_name"/>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('.o_statusbar_status > button:contains(aaa)').length, 1,
"should have only one visible status in mobile, the active one");
assert.containsOnce(form, '.o_statusbar_status .dropdown-menu',
"should have a dropdown containing all status");
assert.containsNone(form, '.o_statusbar_status .dropdown-menu:visible',
"dropdown should be hidden");
// open the dropdown
testUtils.dom.click(form.$('.o_statusbar_status > button'));
assert.containsOnce(form, '.o_statusbar_status .dropdown-menu:visible',
"dropdown should be visible");
assert.containsN(form, '.o_statusbar_status .dropdown-menu button', 3,
"should have 3 status");
assert.containsN(form, '.o_statusbar_status button:disabled', 3,
"all status should be disabled");
var $activeStatus = form.$('.o_statusbar_status .dropdown-menu button[data-value=4]');
assert.hasClass($activeStatus,'btn-primary', "active status should be btn-primary");
form.destroy();
});
QUnit.test('statusbar with no status on extra small screens', async function (assert) {
assert.expect(9);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<header><field name="trululu" widget="statusbar"/></header>' +
'</form>',
res_id: 4,
});
assert.hasClass(form.$('.o_statusbar_status'),'o_field_empty',
'statusbar widget should have class o_field_empty');
assert.strictEqual(form.$('.o_statusbar_status').children().length, 2,
'statusbar widget should have two children');
assert.containsOnce(form, '.o_statusbar_status button.dropdown-toggle',
'statusbar widget should have a button');
assert.strictEqual(form.$('.o_statusbar_status button.dropdown-toggle').text().trim(), '',
'statusbar button has no text'); // Behavior as of saas-15, might be improved
assert.containsOnce(form, '.o_statusbar_status .dropdown-menu',
'statusbar widget should have a dropdown menu');
assert.containsN(form, '.o_statusbar_status .dropdown-menu button', 3,
'statusbar widget dropdown menu should have 3 buttons');
assert.strictEqual(form.$('.o_statusbar_status .dropdown-menu button').eq(0).text().trim(), 'first record',
'statusbar widget dropdown first button should display the first record display_name');
assert.strictEqual(form.$('.o_statusbar_status .dropdown-menu button').eq(1).text().trim(), 'second record',
'statusbar widget dropdown second button should display the second record display_name');
assert.strictEqual(form.$('.o_statusbar_status .dropdown-menu button').eq(2).text().trim(), 'aaa',
'statusbar widget dropdown third button should display the third record display_name');
form.destroy();
});
QUnit.test('clickable statusbar widget on mobile view', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<header><field name="trululu" widget="statusbar" options=\'{"clickable": "1"}\'/></header>' +
'</form>',
res_id: 1,
});
var $selectedStatus = form.$('.o_statusbar_status button[data-value="4"]');
assert.hasClass($selectedStatus, 'btn-primary');
assert.hasClass($selectedStatus,'disabled');
var selector = '.o_statusbar_status button.btn-secondary:not(.dropdown-toggle):not(:disabled)';
assert.containsN(form, selector, 2, "other status should be btn-secondary and not disabled");
await testUtils.dom.click(form.$('.o_statusbar_status .dropdown-toggle'));
await testUtils.dom.clickFirst(form.$(selector));
var $status = form.$('.o_statusbar_status button[data-value="1"]');
assert.hasClass($status, 'btn-primary');
assert.hasClass($status, 'disabled');
form.destroy();
});
QUnit.module('FieldMany2One');
QUnit.test("many2one in a enterprise environment", async function (assert) {
assert.expect(7);
var form = await createView({
View: FormView,
arch:
'<form>' +
'<sheet>' +
'<field name="trululu"/>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,kanban': '<kanban>' +
'<templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click"><field name="display_name"/></div>' +
'</t></templates>' +
'</kanban>',
'partner,false,search': '<search></search>',
},
data: this.data,
model: 'partner',
res_id: 2,
viewOptions: {mode: 'edit'},
});
var $input = form.$('.o_field_many2one input');
assert.doesNotHaveClass($input, 'ui-autocomplete-input',
"autocomplete should not be visible in a mobile environment");
await testUtils.dom.click($input);
var $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
assert.containsOnce($modal, '.o_legacy_kanban_view',
'kanban view should be open in SelectCreateDialog');
assert.containsOnce($modal, '.o_cp_searchview',
'should have Search view inside SelectCreateDialog');
assert.containsNone($modal.find(".o_control_panel .o_cp_buttons"), '.o-kanban-button-new',
"kanban view in SelectCreateDialog should not have Create button");
assert.strictEqual($modal.find(".o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)").length, 3,
"popup should load 3 records in kanban");
await testUtils.dom.click($modal.find('.o_legacy_kanban_view .o_kanban_record:first'));
assert.strictEqual($input.val(), 'first record',
'clicking kanban card should select record for many2one field');
form.destroy();
});
QUnit.test("hide/show element using selection_mode in kanban view in a enterprise environment", async function (assert) {
assert.expect(5);
this.data.partner.fields.foo = {string: "Foo", type: "char", default: "My little Foo Value"};
var form = await createView({
View: FormView,
arch:
'<form>' +
'<sheet>' +
'<field name="trululu"/>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,kanban': '<kanban>' +
'<templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<field name="display_name"/>' +
'</div>' +
'<div class="o_sibling_tags" t-if="!selection_mode">' +
'<field name="sibling_ids"/>' +
'</div>' +
'<div class="o_foo" t-if="selection_mode">' +
'<field name="foo"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
'partner,false,search': '<search></search>',
},
data: this.data,
model: 'partner',
res_id: 2,
viewOptions: {mode: 'edit'},
});
var $input = form.$('.o_field_many2one input');
assert.doesNotHaveClass($input, 'ui-autocomplete-input',
"autocomplete should not be visible in a mobile environment");
await testUtils.dom.click($input);
var $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
assert.containsOnce($modal, '.o_legacy_kanban_view',
'kanban view should be open in SelectCreateDialog');
assert.containsNone($modal, '.o_legacy_kanban_view .o_sibling_tags',
'o_sibling_tags div should not be available as div have condition on selection_mode');
assert.containsN($modal, '.o_legacy_kanban_view .o_foo', 3,
'o_foo div should be available as div have condition on selection_mode');
form.destroy();
});
QUnit.test("kanban_view_ref attribute opens specific kanban view given as a reference in a mobile environment", async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
arch:
'<form>' +
'<sheet>' +
'<field name="trululu" kanban_view_ref="2"/>' +
'</sheet>' +
'</form>',
archs: {
'partner,1,kanban': '<kanban class="kanban1">' +
'<templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<field name="display_name"/>' +
'</div>' +
'</t></templates>' +
'</kanban>',
'partner,2,kanban': '<kanban class="kanban2">' +
'<templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click">' +
'<div>' +
'<field name="display_name"/>' +
'</div>' +
'<div>' +
'<field name="trululu"/>' +
'</div>' +
'</div>' +
'</t></templates>' +
'</kanban>',
'partner,false,search': '<search></search>',
},
data: this.data,
model: 'partner',
res_id: 2,
viewOptions: {mode: 'edit'},
});
var $input = form.$('.o_field_many2one input');
assert.doesNotHaveClass($input, 'ui-autocomplete-input',
"autocomplete should not be visible in a mobile environment");
await testUtils.dom.click($input);
var $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
assert.containsOnce($modal, '.o_legacy_kanban_view',
'kanban view should be open in SelectCreateDialog');
assert.hasClass($modal.find('.o_legacy_kanban_view'), 'kanban2',
'kanban view with id 2 should be opened as it is given as kanban_view_ref');
assert.strictEqual($modal.find('.o_legacy_kanban_view .o_kanban_record:first').text(),
'first recordaaa',
'kanban with two fields should be opened');
form.destroy();
});
QUnit.test("many2one dialog on mobile: clear button header", async function (assert) {
assert.expect(7);
const form = await createView({
View: FormView,
arch: `
<form>
<sheet>
<field name="trululu"/>
</sheet>
</form>
`,
archs: {
'partner,false,kanban': `
<kanban>
<templates><t t-name="kanban-box">
<div class="oe_kanban_global_click"><field name="display_name"/></div>
</t></templates>
</kanban>
`,
'partner,false,search': '<search></search>',
},
data: this.data,
model: 'partner',
res_id: 2,
viewOptions: {mode: 'edit'},
});
let $input = form.$('.o_field_many2one input');
assert.doesNotHaveClass($input, 'ui-autocomplete-input',
"autocomplete should not be visible in a mobile environment");
await testUtils.dom.click($input);
assert.containsOnce($('body'), '.modal.o_modal_full',
"there should be a modal opened in full screen");
assert.containsN($('.modal'), '.o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 3,
"popup should load 3 records in kanban");
await testUtils.dom.click($('.modal').find('.o_legacy_kanban_view .o_kanban_record:first'));
assert.strictEqual($input.val(), 'first record',
'clicking kanban card should select record for many2one field');
await testUtils.dom.click($input);
// clear button.
assert.containsOnce($('.modal').find('.modal-header'), '.o_clear_button',
"there should be a Clear button in the modal header");
await testUtils.dom.click($('.modal').find('.modal-header .o_clear_button'));
assert.containsNone($('body'), '.modal', "there should be no more modal");
$input = form.$('.o_field_many2one input');
assert.strictEqual($input.val(), "", "many2one should be cleared");
form.destroy();
});
QUnit.module('FieldMany2Many');
QUnit.test("many2many_tags in a mobile environment", async function (assert) {
assert.expect(10);
var rpcReadCount = 0;
var form = await createView({
View: FormView,
arch:
'<form>' +
'<sheet>' +
'<field name="sibling_ids" widget="many2many_tags"/>' +
'</sheet>' +
'</form>',
archs: {
'partner,false,kanban': '<kanban>' +
'<templates><t t-name="kanban-box">' +
'<div class="oe_kanban_global_click"><field name="display_name"/></div>' +
'</t></templates>' +
'</kanban>',
'partner,false,search': '<search></search>',
},
data: this.data,
model: 'partner',
res_id: 2,
viewOptions: {mode: 'edit'},
mockRPC: function (route, args) {
if (args.method === "read" && args.model === "partner") {
if (rpcReadCount === 0) {
assert.deepEqual(args.args[0], [2], "form should initially show partner 2");
} else if (rpcReadCount === 1) {
assert.deepEqual(args.args[0], [1], "partner with id 1 should be selected");
}
rpcReadCount++;
}
return this._super.apply(this, arguments);
},
});
var $input = form.$(".o_field_widget .o_input");
assert.strictEqual($input.find(".badge").length, 0,
"many2many_tags should have no tags");
await testUtils.dom.click($input);
var $modal = $('.o_modal_full .modal-lg');
assert.equal($modal.length, 1, 'there should be one modal opened in full screen');
assert.containsOnce($modal, '.o_legacy_kanban_view',
'kanban view should be open in SelectCreateDialog');
assert.containsOnce($modal, '.o_cp_searchview',
'should have Search view inside SelectCreateDialog');
assert.containsNone($modal.find(".o_control_panel .o_cp_buttons"), '.o-kanban-button-new',
"kanban view in SelectCreateDialog should not have Create button");
assert.strictEqual($modal.find(".o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)").length, 3,
"popup should load 3 records in kanban");
await testUtils.dom.click($modal.find('.o_legacy_kanban_view .o_kanban_record:first'));
assert.strictEqual(rpcReadCount, 2, "there should be a read for current form record and selected sibling");
assert.strictEqual(form.$(".o_field_widget.o_input .badge").length, 1,
"many2many_tags should have partner coucou3");
form.destroy();
});
QUnit.module('FieldOne2Many');
QUnit.test('one2many on mobile: remove header button', async function (assert) {
assert.expect(9);
this.data.partner.records[0].p = [1, 2, 4];
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form string="Partners">
<field name="p"/>
</form>
`,
archs: {
'partner,false,form': `
<form string="Partner">
<field name="display_name"/>
</form>
`,
'partner,false,kanban': `
<kanban>
<templates><t t-name="kanban-box">
<div class="oe_kanban_global_click">
<field name="display_name"/>
</div>
</t></templates>
</kanban>
`,
},
res_id: 1,
mockRPC(route, args) {
if (route === '/web/dataset/call_kw/partner/write') {
const commands = args.args[1].p;
assert.strictEqual(commands.length, 3,
'should have generated three commands');
assert.ok(commands[0][0] === 4 && commands[0][1] === 2,
'should have generated the command 4 (LINK_TO) with id 2');
assert.ok(commands[1][0] === 4 && commands[1][1] === 4,
'should have generated the command 2 (LINK_TO) with id 1');
assert.ok(commands[2][0] === 2 && commands[2][1] === 1,
'should have generated the command 2 (DELETE) with id 2');
}
return this._super.apply(this, arguments);
},
});
await testUtils.form.clickEdit(form);
assert.containsN(form, '.o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 3,
"should have 3 records in kanban");
await testUtils.dom.click(form.$('.o_legacy_kanban_view .o_kanban_record:first'));
assert.containsOnce($('body'), '.modal.o_modal_full',
"there should be a modal opened in full screen");
// remove button.
assert.containsOnce($('.modal').find('.modal-header'), '.o_btn_remove',
"there should be a 'Remove' button in the modal header");
await testUtils.dom.click($('.modal').find('.modal-header .o_btn_remove'));
assert.containsNone($('body'), '.modal', "there should be no more modal");
assert.containsN(form, '.o_legacy_kanban_view .o_kanban_record:not(.o_kanban_ghost)', 2,
"should have 2 records in kanban");
// save and check that the correct command has been generated
await testUtils.form.clickSave(form);
form.destroy();
});
});
});
});

View File

@@ -0,0 +1,73 @@
odoo.define('web_enterprise.BasicRenderMobileTests', function (require) {
"use strict";
const BasicRenderer = require('web.BasicRenderer');
const FormView = require('web.FormView');
const testUtils = require('web.test_utils');
const createView = testUtils.createView;
QUnit.module('web_enterprise > basic > basic_render_mobile', {
beforeEach: function () {
this.data = {
partner: {
fields: {
display_name: {string: "Displayed name", type: "char", help: 'The name displayed'},
},
records: [
{
id: 1,
display_name: "first record",
},
],
},
};
}
}, function () {
QUnit.module('Basic Render Mobile');
QUnit.test(`field tooltip shouldn't remain displayed in mobile`, async function (assert) {
assert.expect(2);
testUtils.mock.patch(BasicRenderer, {
SHOW_AFTER_DELAY: 0,
_getTooltipOptions: function () {
return Object.assign({}, this._super(...arguments), {
animation: false,
});
},
});
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: `
<form>
<sheet>
<group>
<field name="display_name"/>
</group>
</sheet>
</form>
`,
});
const label = form.el.querySelector('label.o_form_label');
await testUtils.dom.triggerEvent(label, 'touchstart');
assert.strictEqual(
document.querySelectorAll('.tooltip .oe_tooltip_string').length,
1, "should have a tooltip displayed"
);
await testUtils.dom.triggerEvent(label, 'touchend');
assert.strictEqual(
document.querySelectorAll('.tooltip .oe_tooltip_string').length,
0, "shouldn't have a tooltip displayed"
);
form.destroy();
testUtils.mock.unpatch(BasicRenderer);
});
});
});

View File

@@ -0,0 +1,216 @@
odoo.define('web_enterprise.kanban_mobile_tests', function (require) {
"use strict";
const KanbanView = require('web.KanbanView');
const { createView, dom} = require('web.test_utils');
QUnit.module('LegacyViews', {
beforeEach() {
this.data = {
partner: {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "boolean"},
int_field: {string: "int_field", type: "integer", sortable: true},
qux: {string: "my float", type: "float"},
product_id: {string: "something_id", type: "many2one", relation: "product"},
category_ids: { string: "categories", type: "many2many", relation: 'category'},
state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]},
date: {string: "Date Field", type: 'date'},
datetime: {string: "Datetime Field", type: 'datetime'},
},
records: [
{id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: []},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
{id: 5, bar: false, foo: "Hello \"World\"! #peace_n'_love", int_field: -9, qux: 10, state: "jkl", category_ids: []},
]
},
product: {
fields: {
id: {string: "ID", type: "integer"},
name: {string: "Display Name", type: "char"},
},
records: [
{id: 3, name: "hello"},
{id: 5, name: "xmo"},
]
},
category: {
fields: {
name: {string: "Category Name", type: "char"},
color: {string: "Color index", type: "integer"},
},
records: [
{id: 6, name: "gold", color: 2},
{id: 7, name: "silver", color: 5},
]
},
};
},
}, function () {
QUnit.module("KanbanView (legacy) - Mobile")
QUnit.test('kanban with searchpanel: rendering in mobile', async function (assert) {
assert.expect(34);
const kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: `
<kanban>
<templates><t t-name="kanban-box">
<div>
<field name="foo"/>
</div>
</t></templates>
</kanban>
`,
archs: {
'partner,false,search': `
<search>
<searchpanel>
<field name="product_id" expand="1" enable_counters="1"/>
<field name="state" expand="1" select="multi" enable_counters="1"/>
</searchpanel>
</search>
`,
},
mockRPC(route, {method}) {
if (method && method.includes('search_panel_')) {
assert.step(method);
}
return this._super.apply(this, arguments);
},
});
let $sp = kanban.$(".o_search_panel");
assert.containsOnce(kanban, ".o_search_panel.o_search_panel_summary");
assert.containsNone(document.body, "div.o_search_panel.o_searchview.o_mobile_search");
assert.verifySteps([
"search_panel_select_range",
"search_panel_select_multi_range",
]);
assert.containsOnce($sp, ".fa.fa-filter");
assert.containsOnce($sp, ".o_search_panel_current_selection:contains(All)");
// open the search panel
await dom.click($sp);
$sp = $(".o_search_panel");
assert.containsNone(kanban, ".o_search_panel.o_search_panel_summary");
assert.containsOnce(document.body, "div.o_search_panel.o_searchview.o_mobile_search");
assert.containsOnce($sp, ".o_mobile_search_header > button:contains(FILTER)");
assert.containsOnce($sp, "button.o_mobile_search_footer:contains(SEE RESULT)");
assert.containsN($sp, ".o_search_panel_section", 2);
assert.containsOnce($sp, ".o_search_panel_section.o_search_panel_category");
assert.containsOnce($sp, ".o_search_panel_section.o_search_panel_filter");
assert.containsN($sp, ".o_search_panel_category_value", 3);
assert.containsOnce($sp, ".o_search_panel_category_value > header.active", 3);
assert.containsN($sp, ".o_search_panel_filter_value", 3);
// select category
await dom.click($sp.find(".o_search_panel_category_value:contains(hello) header"));
assert.verifySteps([
"search_panel_select_range",
"search_panel_select_multi_range",
]);
// select filter
await dom.click($sp.find(".o_search_panel_filter_value:contains(DEF) input"));
assert.verifySteps([
"search_panel_select_range",
"search_panel_select_multi_range",
]);
// close with back button
await dom.click($sp.find(".o_mobile_search_header button"));
$sp = $(".o_search_panel");
assert.containsOnce(kanban, ".o_search_panel.o_search_panel_summary");
assert.containsNone(document.body, "div.o_search_panel.o_searchview.o_mobile_search");
// selection is kept when closed
assert.containsOnce($sp, ".o_search_panel_current_selection");
assert.containsOnce($sp, ".o_search_panel_category:contains(hello)");
assert.containsOnce($sp, ".o_search_panel_filter:contains(DEF)");
// open the search panel
await dom.click($sp);
$sp = $(".o_search_panel");
assert.containsOnce($sp, ".o_search_panel_category_value > header.active:contains(hello)");
assert.containsOnce($sp, ".o_search_panel_filter_value:contains(DEF) input:checked");
assert.containsNone(kanban, ".o_search_panel.o_search_panel_summary");
assert.containsOnce(document.body, "div.o_search_panel.o_searchview.o_mobile_search");
// close with bottom button
await dom.click($sp.find("button.o_mobile_search_footer"));
assert.containsOnce(kanban, ".o_search_panel.o_search_panel_summary");
assert.containsNone(document.body, "div.o_search_panel.o_searchview.o_mobile_search");
kanban.destroy();
});
QUnit.module('KanbanView Mobile');
QUnit.test('mobile no quick create column when grouping on non m2o field', async function (assert) {
assert.expect(2);
var kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test o_kanban_small_column" on_create="quick_create">' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'<div><field name="int_field"/></div>' +
'</t></templates>' +
'</kanban>',
groupBy: ['int_field'],
});
assert.containsNone(kanban, '.o_kanban_mobile_add_column', "should not have the add column button");
assert.containsNone(kanban.$('.o_column_quick_create'),
"should not have column quick create tab as we grouped records on integer field");
kanban.destroy();
});
QUnit.test("autofocus quick create form", async function (assert) {
assert.expect(2);
const kanban = await createView({
View: KanbanView,
model: "partner",
data: this.data,
arch: `<kanban on_create="quick_create">
<templates>
<t t-name="kanban-box">
<div><field name="foo"/></div>
</t>
</templates>
</kanban>`,
groupBy: ["product_id"],
});
// quick create in first column
await dom.click(kanban.$buttons.find(".o-kanban-button-new"));
assert.ok(kanban.$(".o_kanban_group:nth(0) > div:nth(1)").hasClass("o_kanban_quick_create"),
"clicking on create should open the quick_create in the first column");
assert.strictEqual(document.activeElement, kanban.$(".o_kanban_quick_create .o_input:first")[0],
"the first input field should get the focus when the quick_create is opened");
kanban.destroy();
});
});
});

View File

@@ -0,0 +1,229 @@
odoo.define('web_enterprise.list_mobile_tests', function (require) {
"use strict";
const ListRenderer = require('web.ListRenderer');
const ListView = require('web.ListView');
const testUtils = require('web.test_utils');
const { createView, dom, mock } = testUtils;
QUnit.module("LegacyViews", {
beforeEach() {
this.data = {
foo: {
fields: {
foo: { string: "Foo", type: "char" },
bar: { string: "Bar", type: "boolean" },
},
records: [
{ id: 1, bar: true, foo: "yop" },
{ id: 2, bar: true, foo: "blip" },
{ id: 3, bar: true, foo: "gnap" },
{ id: 4, bar: false, foo: "blip" },
],
},
};
mock.patch(ListRenderer, {
init() {
this._super(...arguments);
this.LONG_TOUCH_THRESHOLD = 0;
}
});
},
afterEach() {
mock.unpatch(ListRenderer);
},
}, function () {
QUnit.module("ListView (legacy) - Mobile");
QUnit.test("selection is properly displayed (single page)", async function (assert) {
assert.expect(10);
const list = await createView({
touchScreen: true,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
data: this.data,
model: 'foo',
viewOptions: { hasActionMenus: true },
View: ListView,
});
assert.containsN(list, '.o_data_row', 4);
assert.containsNone(list, '.o_list_selection_box');
// select a record
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchend');
assert.containsOnce(list, '.o_list_selection_box');
assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain');
assert.ok(list.$('.o_list_selection_box').text().includes("1 selected"))
// unselect a record
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchend');
assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain');
// select 2 records
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchend');
await dom.triggerEvent(list.$('.o_data_row:eq(1)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(1)'), 'touchend');
assert.ok(list.$('.o_list_selection_box').text().includes("2 selected"))
assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus');
await testUtils.controlPanel.toggleActionMenu(list);
assert.deepEqual(testUtils.controlPanel.getMenuItemTexts(list), ['Delete'],
'action menu should contain the Delete action');
// unselect all
await dom.click(list.$('.o_discard_selection'));
await testUtils.nextTick();
assert.containsNone(list, '.o_list_selection_box');
list.destroy();
});
QUnit.test("selection box is properly displayed (multi pages)", async function (assert) {
assert.expect(13);
const list = await createView({
touchScreen: true,
arch: `
<tree limit="3">
<field name="foo"/>
<field name="bar"/>
</tree>`,
data: this.data,
model: 'foo',
View: ListView,
viewOptions: { hasActionMenus: true },
});
assert.containsN(list, '.o_data_row', 3);
assert.containsNone(list, '.o_list_selection_box');
// select a record
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchend');
assert.containsOnce(list, '.o_list_selection_box');
assert.containsNone(list.$('.o_list_selection_box'), '.o_list_select_domain');
assert.strictEqual(list.$('.o_list_selection_box').text().replace(/\s+/g, ' '),
" × 1 selected ");
assert.containsOnce(list, '.o_list_selection_box');
assert.containsOnce(list.el, 'div.o_control_panel .o_cp_action_menus');
await testUtils.controlPanel.toggleActionMenu(list);
assert.deepEqual(testUtils.controlPanel.getMenuItemTexts(list), ['Delete'],
'action menu should contain the Delete action');
// select all records of first page
await dom.triggerEvent(list.$('.o_data_row:eq(1)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(1)'), 'touchend');
await dom.triggerEvent(list.$('.o_data_row:eq(2)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(2)'), 'touchend');
assert.containsOnce(list, '.o_list_selection_box');
assert.containsOnce(list.$('.o_list_selection_box'), '.o_list_select_domain');
assert.strictEqual(list.$('.o_list_selection_box').text().replace(/\s+/g, ' ').trim(),
"× 3 selected Select all 4");
// select all domain
await dom.click(list.$('.o_list_selection_box .o_list_select_domain'));
assert.containsOnce(list, '.o_list_selection_box');
assert.strictEqual(list.$('.o_list_selection_box').text().replace(/\s+/g, ' ').trim(),
"× All 4 selected");
list.destroy();
});
QUnit.test("export button is properly hidden", async function (assert) {
assert.expect(2);
const list = await createView({
touchScreen: true,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
data: this.data,
model: 'foo',
View: ListView,
session: {
async user_has_group(group) {
if (group === 'base.group_allow_export') {
return true;
}
return this._super(...arguments);
},
},
});
assert.containsN(list, '.o_data_row', 4);
assert.isNotVisible(list.$buttons.find('.o_list_export_xlsx'));
list.destroy();
});
QUnit.test('editable readonly list view is disabled', async function (assert) {
assert.expect(1);
const list = await createView({
touchScreen: true,
arch: `
<tree>
<field name="foo"/>
</tree>`,
data: this.data,
model: 'foo',
View: ListView,
});
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchstart');
await dom.triggerEvent(list.$('.o_data_row:eq(0)'), 'touchend');
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(0)'));
assert.containsNone(list, '.o_selected_row .o_field_widget[name=foo]',
"The listview should not contains an edit field");
list.destroy();
});
QUnit.test("add custom field button not shown in mobile (with opt. col.)", async function (assert) {
assert.expect(3);
const list = await testUtils.createView({
arch: `
<tree>
<field name="foo"/>
<field name="bar" optional="hide"/>
</tree>`,
data: this.data,
model: 'foo',
touchScreen: true,
View: ListView,
});
assert.containsOnce(list.$('table'), '.o_optional_columns_dropdown_toggle');
await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle'));
const $dropdown = list.$('div.o_optional_columns');
assert.containsOnce($dropdown, 'div.dropdown-item');
assert.containsNone($dropdown, 'button.dropdown-item-studio');
list.destroy();
});
QUnit.test("add custom field button not shown to non-system users (wo opt. col.)", async function (assert) {
assert.expect(1);
const list = await testUtils.createView({
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
data: this.data,
model: 'foo',
touchScreen: true,
View: ListView,
});
assert.containsNone(list.$('table'), '.o_optional_columns_dropdown_toggle');
list.destroy();
});
});
});

View File

@@ -0,0 +1,259 @@
odoo.define('web_enterprise.list_tests', function (require) {
"use strict";
const AbstractStorageService = require('web.AbstractStorageService');
const ListRenderer = require('web.ListRenderer');
const ListView = require('web.ListView');
const RamStorage = require('web.RamStorage');
const testUtils = require('web.test_utils');
const { unblockUI } = require('web.framework');
const { patch, unpatch, UnknownPatchError } = require('web.utils');
const PromoteStudioDialog = require('web_enterprise.PromoteStudioDialog');
QUnit.module('web_enterprise', {
beforeEach: function () {
this.data = {
foo: {
fields: {
foo: {string: "Foo", type: "char"},
bar: {string: "Bar", type: "boolean"},
},
records: [
{id: 1, bar: true, foo: "yop"},
{id: 2, bar: true, foo: "blip"},
{id: 3, bar: true, foo: "gnap"},
{id: 4, bar: false, foo: "blip"},
]
}
};
this.RamStorageService = AbstractStorageService.extend({
storage: new RamStorage(),
});
}
}, function () {
QUnit.module('ListView (Legacy)');
QUnit.test("add custom field button with other optional columns - studio not installed", async function (assert) {
assert.expect(11);
let listPatch;
try {
listPatch = unpatch(ListRenderer.prototype, 'web_studio.ListRenderer');
} catch (e) {
if (!(e instanceof UnknownPatchError)) {
throw e;
}
}
testUtils.mock.patch(PromoteStudioDialog, {
_reloadPage: function () {
assert.step('window_reload');
unblockUI(); // the UI is normally unblocked by the reload
}
});
const list = await testUtils.createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar" optional="hide"/>
</tree>`,
session: {
is_system: true
},
action: {
xml_id: "action_43",
},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.module.module') {
assert.step('studio_module_id');
return Promise.resolve([{id: 42}]);
}
if (args.method === 'button_immediate_install' && args.model === 'ir.module.module') {
assert.deepEqual(args.args[0], [42], "Should be the id of studio module returned by the search read");
assert.step('studio_module_install');
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
services: {
local_storage: this.RamStorageService,
},
});
assert.ok(list.$('.o_data_row').length > 0);
assert.containsOnce(list.$('table'), '.o_optional_columns_dropdown_toggle');
await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle'));
const $dropdown = list.$('div.o_optional_columns');
assert.containsOnce($dropdown, 'div.dropdown-item');
assert.containsOnce($dropdown, 'button.dropdown-item-studio');
await testUtils.dom.click(list.$('div.o_optional_columns button.dropdown-item-studio'));
await testUtils.nextTick();
assert.containsOnce(document.body, '.modal-studio');
await testUtils.dom.click($('.modal-studio .o_install_studio'));
assert.equal(window.localStorage.openStudioOnReload, 'main');
assert.verifySteps(['studio_module_id', 'studio_module_install', 'window_reload']);
// wash localStorage
window.localStorage.openStudioOnReload = false;
testUtils.mock.unpatch(PromoteStudioDialog);
if (listPatch) {
patch(ListRenderer.prototype, 'web_studio.ListRenderer', listPatch);
}
list.destroy();
});
QUnit.test("add custom field button without other optional columns - studio not installed", async function (assert) {
assert.expect(11);
let listPatch;
try {
listPatch = unpatch(ListRenderer.prototype, 'web_studio.ListRenderer');
} catch (e) {
if (!(e instanceof UnknownPatchError)) {
throw e;
}
}
testUtils.mock.patch(PromoteStudioDialog, {
_reloadPage: function () {
assert.step('window_reload');
unblockUI(); // the UI is normally unblocked by the reload
}
});
const list = await testUtils.createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
session: {
is_system: true
},
action: {
xml_id: "action_43",
},
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'ir.module.module') {
assert.step('studio_module_id');
return Promise.resolve([{id: 42}]);
}
if (args.method === 'button_immediate_install' && args.model === 'ir.module.module') {
assert.deepEqual(args.args[0], [42], "Should be the id of studio module returned by the search read");
assert.step('studio_module_install');
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
services: {
local_storage: this.RamStorageService,
},
});
assert.ok(list.$('.o_data_row').length > 0);
assert.containsOnce(list.$('table'), '.o_optional_columns_dropdown_toggle');
await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle'));
const $dropdown = list.$('div.o_optional_columns');
assert.containsNone($dropdown, 'div.dropdown-item');
assert.containsOnce($dropdown, 'button.dropdown-item-studio');
await testUtils.dom.click(list.$('div.o_optional_columns button.dropdown-item-studio'));
await testUtils.nextTick();
assert.containsOnce(document.body, '.modal-studio');
await testUtils.dom.click($('.modal-studio .o_install_studio'));
assert.equal(window.localStorage.openStudioOnReload, 'main');
assert.verifySteps(['studio_module_id', 'studio_module_install', 'window_reload']);
// wash localStorage
window.localStorage.openStudioOnReload = false;
if (listPatch) {
patch(ListRenderer.prototype, 'web_studio.ListRenderer', listPatch);
}
testUtils.mock.unpatch(PromoteStudioDialog);
list.destroy();
});
QUnit.test("add custom field button not shown to non-system users (with opt. col.)", async function (assert) {
assert.expect(3);
const list = await testUtils.createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar" optional="hide"/>
</tree>`,
session: {
is_system: false
},
action: {
xml_id: "action_43",
},
});
assert.containsOnce(list.$('table'), '.o_optional_columns_dropdown_toggle');
await testUtils.dom.click(list.$('table .o_optional_columns_dropdown_toggle'));
const $dropdown = list.$('div.o_optional_columns');
assert.containsOnce($dropdown, 'div.dropdown-item');
assert.containsNone($dropdown, 'button.dropdown-item-studio');
list.destroy();
});
QUnit.test("add custom field button not shown to non-system users (wo opt. col.)", async function (assert) {
assert.expect(1);
const list = await testUtils.createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
session: {
is_system: false
},
action: {
xml_id: "action_43",
},
});
assert.containsNone(list.$('table'), '.o_optional_columns_dropdown_toggle');
list.destroy();
});
QUnit.test("add custom field button not shown with invalid action", async function (assert) {
assert.expect(1);
const list = await testUtils.createView({
View: ListView,
model: 'foo',
data: this.data,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
session: {
is_system: true
},
action: {
xml_id: null,
},
});
assert.containsNone(list.$('div.o_optional_columns'), 'button.dropdown-item-studio');
list.destroy();
});
});
});

View File

@@ -0,0 +1,84 @@
/** @odoo-module **/
import { click, legacyExtraNextTick } from "@web/../tests/helpers/utils";
import { getActionManagerServerData } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { createEnterpriseWebClient } from "@web_enterprise/../tests/helpers";
import { EnterpriseBurgerMenu } from "@web_enterprise/webclient/burger_menu/burger_menu";
import { homeMenuService } from "@web_enterprise/webclient/home_menu/home_menu_service";
import { companyService } from "@web/webclient/company_service";
import { ormService } from "@web/core/orm_service";
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
/**
* Note: The asserts are all based on document.body (instead of getFixture() by example) because
* the burger menu is porteled into the dom and is not part of the qunit fixture.
*/
let serverData;
const serviceRegistry = registry.category("services");
QUnit.module("Burger Menu Enterprise", {
beforeEach() {
serverData = getActionManagerServerData();
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
serviceRegistry.add("orm", ormService);
serviceRegistry.add("company", companyService);
serviceRegistry.add("home_menu", homeMenuService);
registry.category("systray").add("burger_menu", {
Component: EnterpriseBurgerMenu,
});
},
});
QUnit.test("Burger Menu on home menu", async (assert) => {
assert.expect(5);
await createEnterpriseWebClient({ serverData });
assert.containsNone(document.body, ".o_burger_menu");
assert.isVisible(document.body.querySelector(".o_home_menu"));
await click(document.body, ".o_mobile_menu_toggle");
assert.containsOnce(document.body, ".o_burger_menu");
assert.containsOnce(document.body, ".o_user_menu_mobile");
await click(document.body, ".o_burger_menu_close");
assert.containsNone(document.body, ".o_burger_menu");
});
QUnit.test("Burger Menu on home menu over an App", async (assert) => {
assert.expect(5);
serverData.menus[1].children = [99];
serverData.menus[99] = {
id: 99,
children: [],
name: "SubMenu",
appID: 1,
actionID: 1002,
xmlid: "",
webIconData: undefined,
webIcon: false,
};
await createEnterpriseWebClient({ serverData });
await click(document.body, ".o_app:first-of-type");
await legacyExtraNextTick();
await click(document.body, ".o_menu_toggle");
await legacyExtraNextTick();
assert.containsNone(document.body, ".o_burger_menu");
assert.isVisible(document.body.querySelector(".o_home_menu"));
await click(document.body, ".o_mobile_menu_toggle");
assert.containsOnce(document.body, ".o_burger_menu");
assert.containsNone(
document.body,
".o_burger_menu nav.o_burger_menu_content li"
);
assert.doesNotHaveClass(
document.body.querySelector(".o_burger_menu_content"),
"o_burger_menu_dark"
);
});

View File

@@ -0,0 +1,185 @@
/** @odoo-module **/
import { dialogService } from "@web/core/dialog/dialog_service";
import { registry } from "@web/core/registry";
import {
makeFakeLocalizationService,
makeFakeUserService,
} from "@web/../tests/helpers/mock_services";
import { getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
import {
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
} from "@web/../tests/search/helpers";
import { makeView } from "@web/../tests/views/helpers";
import { browser } from "@web/core/browser/browser";
const serviceRegistry = registry.category("services");
let serverData;
let target;
QUnit.module("Views", (hooks) => {
hooks.beforeEach(() => {
target = getFixture();
serverData = {
models: {
partner: {
fields: {
foo: {
string: "Foo",
type: "integer",
searchable: true,
group_operator: "sum",
},
bar: { string: "bar", type: "boolean", store: true, sortable: true },
date: { string: "Date", type: "date", store: true, sortable: true },
product_id: {
string: "Product",
type: "many2one",
relation: "product",
store: true,
sortable: true,
},
other_product_id: {
string: "Other Product",
type: "many2one",
relation: "product",
store: true,
sortable: true,
},
non_stored_m2o: {
string: "Non Stored M2O",
type: "many2one",
relation: "product",
},
customer: {
string: "Customer",
type: "many2one",
relation: "customer",
store: true,
sortable: true,
},
computed_field: {
string: "Computed and not stored",
type: "integer",
compute: true,
group_operator: "sum",
},
company_type: {
string: "Company Type",
type: "selection",
selection: [
["company", "Company"],
["individual", "individual"],
],
searchable: true,
sortable: true,
store: true,
},
},
records: [
{
id: 1,
foo: 12,
bar: true,
date: "2016-12-14",
product_id: 37,
customer: 1,
computed_field: 19,
company_type: "company",
},
{
id: 2,
foo: 1,
bar: true,
date: "2016-10-26",
product_id: 41,
customer: 2,
computed_field: 23,
company_type: "individual",
},
{
id: 3,
foo: 17,
bar: true,
date: "2016-12-15",
product_id: 41,
customer: 2,
computed_field: 26,
company_type: "company",
},
{
id: 4,
foo: 2,
bar: false,
date: "2016-04-11",
product_id: 41,
customer: 1,
computed_field: 19,
company_type: "individual",
},
],
},
product: {
fields: {
name: { string: "Product Name", type: "char" },
},
records: [
{
id: 37,
display_name: "xphone",
},
{
id: 41,
display_name: "xpad",
},
],
},
customer: {
fields: {
name: { string: "Customer Name", type: "char" },
},
records: [
{
id: 1,
display_name: "First",
},
{
id: 2,
display_name: "Second",
},
],
},
},
};
setupControlPanelFavoriteMenuRegistry();
setupControlPanelServiceRegistry();
serviceRegistry.add("dialog", dialogService);
serviceRegistry.add("localization", makeFakeLocalizationService());
serviceRegistry.add("user", makeFakeUserService());
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
});
QUnit.module("PivotView");
QUnit.test("simple pivot rendering", async function (assert) {
assert.expect(2);
await makeView({
type: "pivot",
resModel: "partner",
serverData,
arch: `
<pivot string="Partners">
<field name="foo" type="measure"/>
</pivot>`,
});
assert.hasClass(target.querySelector(".o_pivot_view"), "o_view_controller");
assert.containsOnce(
target,
"td.o_pivot_cell_value:contains(32)",
"should contain a pivot cell with the sum of all records"
);
});
});

View File

@@ -0,0 +1,53 @@
/** @odoo-module **/
import { getActionManagerServerData, doAction } from "@web/../tests/webclient/helpers";
import { homeMenuService } from "@web_enterprise/webclient/home_menu/home_menu_service";
import { ormService } from "@web/core/orm_service";
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
import { registry } from "@web/core/registry";
import { createEnterpriseWebClient } from "../helpers";
import { click, getFixture } from "@web/../tests/helpers/utils";
const serviceRegistry = registry.category("services");
QUnit.module("WebClient Mobile", (hooks) => {
let serverData;
hooks.beforeEach(() => {
serverData = getActionManagerServerData();
serviceRegistry.add("home_menu", homeMenuService);
serviceRegistry.add("orm", ormService);
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
});
QUnit.test("scroll position is kept", async (assert) => {
// This test relies on the fact that the scrollable element in mobile
// is view's root node.
const record = serverData.models.partner.records[0];
serverData.models.partner.records = [];
for (let i = 0; i < 80; i++) {
const rec = Object.assign({}, record);
rec.id = i + 1;
rec.display_name = `Record ${rec.id}`;
serverData.models.partner.records.push(rec);
}
// force the html node to be scrollable element
const target = getFixture();
const webClient = await createEnterpriseWebClient({ serverData });
await doAction(webClient, 3); // partners in list/kanban
assert.containsOnce(target, ".o_kanban_view");
target.querySelector(".o_kanban_view").scrollTo(0, 123);
await click(target.querySelectorAll(".o_kanban_record")[20]);
assert.containsOnce(target, ".o_form_view");
assert.containsNone(target, ".o_kanban_view");
await click(target.querySelector(".o_control_panel .o_back_button"));
assert.containsNone(target, ".o_form_view");
assert.containsOnce(target, ".o_kanban_view");
assert.strictEqual(target.querySelector(".o_kanban_view").scrollTop, 123);
});
});

View File

@@ -0,0 +1,6 @@
/** @odoo-module */
import { unpatch } from "@web/core/utils/patch";
import { ListRenderer } from "@web/views/list/list_renderer";
unpatch(ListRenderer.prototype, "web_enterprise.ListRendererDesktop");

View File

@@ -0,0 +1,247 @@
/** @odoo-module */
import { getFixture, patchWithCleanup, click, nextTick } from "@web/../tests/helpers/utils";
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
import { patch, unpatch } from "@web/core/utils/patch";
import { session } from "@web/session";
import { ListRenderer } from "@web/views/list/list_renderer";
import { browser } from "@web/core/browser/browser";
import { patchListRendererDesktop } from "@web_enterprise/views/list/list_renderer_desktop";
let config;
let serverData;
let target;
QUnit.module(
"web_enterprise",
{
beforeEach() {
target = getFixture();
config = {
actionId: 1,
actionType: "ir.actions.act_window",
};
serverData = {
models: {
foo: {
fields: {
foo: { string: "Foo", type: "char" },
bar: { string: "Bar", type: "boolean" },
},
records: [
{ id: 1, bar: true, foo: "yop" },
{ id: 2, bar: true, foo: "blip" },
{ id: 3, bar: true, foo: "gnap" },
{ id: 4, bar: false, foo: "blip" },
],
},
},
};
patch(
ListRenderer.prototype,
"web_enterprise.ListRendererDesktop",
patchListRendererDesktop
);
setupViewRegistries();
},
afterEach() {
unpatch(ListRenderer.prototype, "web_enterprise.ListRendererDesktop");
},
},
function () {
QUnit.module("ListView");
QUnit.test(
"add custom field button with other optional columns - studio not installed",
async function (assert) {
assert.expect(11);
patchWithCleanup(session, { is_system: true });
await makeView({
serverData,
type: "list",
resModel: "foo",
arch: `
<tree>
<field name="foo"/>
<field name="bar" optional="hide"/>
</tree>`,
mockRPC(route, args) {
if (args.method === "search_read" && args.model === "ir.module.module") {
assert.step("studio_module_id");
return Promise.resolve([{ id: 42 }]);
}
if (
args.method === "button_immediate_install" &&
args.model === "ir.module.module"
) {
assert.deepEqual(
args.args[0],
[42],
"Should be the id of studio module returned by the search read"
);
assert.step("studio_module_install");
return true;
}
},
config,
});
patchWithCleanup(browser.location, {
reload: function () {
assert.step("window_reload");
},
});
assert.containsN(target, ".o_data_row", 4);
assert.containsOnce(target, ".o_optional_columns_dropdown_toggle");
await click(target, ".o_optional_columns_dropdown_toggle");
const dropdown = target.querySelector(".o_optional_columns_dropdown");
assert.containsN(dropdown, ".dropdown-item", 2);
assert.containsOnce(dropdown, ".dropdown-item-studio");
await click(target, ".o_optional_columns_dropdown .dropdown-item-studio");
await nextTick();
assert.containsOnce(target, ".modal-studio");
await click(target, ".modal .o_install_studio");
assert.equal(browser.localStorage.getItem("openStudioOnReload"), "main");
assert.verifySteps(["studio_module_id", "studio_module_install", "window_reload"]);
}
);
QUnit.test(
"add custom field button without other optional columns - studio not installed",
async function (assert) {
assert.expect(11);
patchWithCleanup(session, { is_system: true });
await makeView({
serverData,
type: "list",
resModel: "foo",
config,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
mockRPC: function (route, args) {
if (args.method === "search_read" && args.model === "ir.module.module") {
assert.step("studio_module_id");
return Promise.resolve([{ id: 42 }]);
}
if (
args.method === "button_immediate_install" &&
args.model === "ir.module.module"
) {
assert.deepEqual(
args.args[0],
[42],
"Should be the id of studio module returned by the search read"
);
assert.step("studio_module_install");
return true;
}
},
});
patchWithCleanup(browser.location, {
reload: function () {
assert.step("window_reload");
},
});
assert.containsN(target, ".o_data_row", 4);
assert.containsOnce(target, ".o_optional_columns_dropdown_toggle");
await click(target, ".o_optional_columns_dropdown_toggle");
const dropdown = target.querySelector(".o_optional_columns_dropdown");
assert.containsOnce(dropdown, ".dropdown-item");
assert.containsOnce(dropdown, ".dropdown-item-studio");
await click(target, ".o_optional_columns_dropdown .dropdown-item-studio");
await nextTick();
assert.containsOnce(target, ".modal-studio");
await click(target, ".modal .o_install_studio");
assert.equal(browser.localStorage.getItem("openStudioOnReload"), "main");
assert.verifySteps(["studio_module_id", "studio_module_install", "window_reload"]);
}
);
QUnit.test(
"add custom field button not shown to non-system users (with opt. col.)",
async function (assert) {
assert.expect(3);
patchWithCleanup(session, { is_system: false });
await makeView({
serverData,
type: "list",
resModel: "foo",
config,
arch: `
<tree>
<field name="foo"/>
<field name="bar" optional="hide"/>
</tree>`,
});
assert.containsOnce(target, ".o_optional_columns_dropdown_toggle");
await click(target, ".o_optional_columns_dropdown_toggle");
const dropdown = target.querySelector(".o_optional_columns_dropdown");
assert.containsOnce(dropdown, ".dropdown-item");
assert.containsNone(dropdown, ".dropdown-item-studio");
}
);
QUnit.test(
"add custom field button not shown to non-system users (wo opt. col.)",
async function (assert) {
assert.expect(1);
patchWithCleanup(session, { is_system: false });
await makeView({
serverData,
type: "list",
resModel: "foo",
config,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
});
assert.containsNone(target, ".o_optional_columns_dropdown_toggle");
}
);
QUnit.test(
"add custom field button not shown with invalid action",
async function (assert) {
assert.expect(1);
patchWithCleanup(session, { is_system: false });
config.actionId = null;
await makeView({
serverData,
type: "list",
resModel: "foo",
config,
arch: `
<tree>
<field name="foo"/>
<field name="bar"/>
</tree>`,
});
assert.containsNone(target, ".o_optional_columns_dropdown_toggle");
}
);
}
);

View File

@@ -0,0 +1,784 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { click, getFixture, patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
import { makeFakeNotificationService } from "@web/../tests/helpers/mock_services";
import { browser } from "@web/core/browser/browser";
import { ormService } from "@web/core/orm_service";
import testUtils from "web.test_utils";
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
import { homeMenuService } from "@web_enterprise/webclient/home_menu/home_menu_service";
import { session } from "@web/session";
const serviceRegistry = registry.category("services");
let target;
async function createExpirationPanel(params = {}) {
const mockedCookieService = {
name: "cookie",
start() {
return Object.assign(
{
current: {},
setCookie() {},
deleteCookie() {},
},
params.cookie
);
},
};
serviceRegistry.add(mockedCookieService.name, mockedCookieService);
serviceRegistry.add("orm", ormService);
serviceRegistry.add("home_menu", homeMenuService);
serviceRegistry.add(
"notification",
makeFakeNotificationService(params.mockNotification || (() => {}))
);
patchWithCleanup(session, { ...params.session });
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
patchWithCleanup(browser, params.browser);
const webclient = await createWebClient({
mockRPC: params.mockRPC,
});
await doAction(webclient, "menu");
return webclient;
}
QUnit.module("web_enterprise", function ({ beforeEach }) {
beforeEach(() => {
target = getFixture();
});
QUnit.module("Expiration Panel");
QUnit.test("Expiration Panel one app installed", async function (assert) {
assert.expect(3);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-11-09 12:00:00",
expiration_reason: "",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 1 month."
);
// Color should be grey
assert.hasClass(target.querySelector(".database_expiration_panel"), "alert-info");
// Close the expiration panel
await click(target.querySelector(".oe_instance_hide_panel"));
assert.containsNone(target, ".database_expiration_panel");
});
QUnit.test("Expiration Panel one app installed, buy subscription", async function (assert) {
assert.expect(6);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-24 12:00:00",
expiration_reason: "demo",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route) {
if (route === "/web/dataset/call_kw/res.users/search_count") {
return 7;
}
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This demo database will expire in 14 days. Register your subscription or buy a subscription."
);
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-warning",
"Color should be orange"
);
assert.containsOnce(
target,
".oe_instance_register_show",
"Part 'Register your subscription'"
);
assert.containsOnce(target, ".oe_instance_buy", "Part 'buy a subscription'");
assert.containsNone(
target,
".oe_instance_register_form",
"There should be no registration form"
);
// Click on 'buy subscription'
await click(target.querySelector(".oe_instance_buy"));
});
QUnit.test(
"Expiration Panel one app installed, try several times to register subscription",
async function (assert) {
assert.expect(44);
patchDate(2019, 9, 10, 12, 0, 0);
let callToGetParamCount = 0;
await createExpirationPanel({
session: {
expiration_date: "2019-10-15 12:00:00",
expiration_reason: "trial",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockNotification(message, options) {
assert.step(JSON.stringify({ message, options }));
},
mockRPC(route, { args }) {
if (route === "/web/dataset/call_kw/ir.config_parameter/get_param") {
assert.step("get_param");
if (args[0] === "database.already_linked_subscription_url") {
return false;
}
if (args[0] === "database.already_linked_email") {
return "super_company_admin@gmail.com";
}
assert.strictEqual(args[0], "database.expiration_date");
callToGetParamCount++;
if (callToGetParamCount <= 3) {
return "2019-10-15 12:00:00";
} else {
return "2019-11-15 12:00:00";
}
}
if (route === "/web/dataset/call_kw/ir.config_parameter/set_param") {
assert.step("set_param");
assert.strictEqual(args[0], "database.enterprise_code");
if (callToGetParamCount === 1) {
assert.strictEqual(args[1], "ABCDEF");
} else {
assert.strictEqual(args[1], "ABC");
}
return true;
}
if (
route ===
"/web/dataset/call_kw/publisher_warranty.contract/update_notification"
) {
assert.step("update_notification");
assert.ok(args[0] instanceof Array && args[0].length === 0);
return true;
}
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 5 days. Register your subscription or buy a subscription."
);
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-danger",
"Color should be red"
);
assert.containsOnce(
target,
".oe_instance_register_show",
"Part 'Register your subscription'"
);
assert.containsOnce(target, ".oe_instance_buy", "Part 'buy a subscription'");
assert.containsNone(
target,
".oe_instance_register_form",
"There should be no registration form"
);
// Click on 'register your subscription'
await click(target.querySelector(".oe_instance_register_show"));
assert.containsOnce(
target,
".oe_instance_register_form",
"there should be a registration form"
);
assert.containsOnce(
target,
'.oe_instance_register_form input[placeholder="Paste code here"]',
"with an input with place holder 'Paste code here'"
);
assert.containsOnce(
target,
".oe_instance_register_form button",
"and a button 'REGISTER'"
);
assert.strictEqual(
target.querySelector(".oe_instance_register_form button").innerText,
"REGISTER"
);
await click(target.querySelector(".oe_instance_register_form button"));
assert.containsOnce(
target,
".oe_instance_register_form",
"there should be a registration form"
);
assert.containsOnce(
target,
'.oe_instance_register_form input[placeholder="Paste code here"]',
"with an input with place holder 'Paste code here'"
);
assert.containsOnce(
target,
".oe_instance_register_form button",
"and a button 'REGISTER'"
);
await testUtils.fields.editInput(
target.querySelector(".oe_instance_register_form input"),
"ABCDEF"
);
await click(target.querySelector(".oe_instance_register_form button"));
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"Something went wrong while registering your database. You can try again or contact Odoo Support."
);
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-danger",
"Color should be red"
);
assert.containsOnce(target, "span.oe_instance_error");
assert.containsOnce(
target,
".oe_instance_register_form",
"there should be a registration form"
);
assert.containsOnce(
target,
'.oe_instance_register_form input[placeholder="Paste code here"]',
"with an input with place holder 'Paste code here'"
);
assert.containsOnce(
target,
".oe_instance_register_form button",
"and a button 'REGISTER'"
);
assert.strictEqual(
target.querySelector(".oe_instance_register_form button").innerText,
"RETRY"
);
await testUtils.fields.editInput(
target.querySelector(".oe_instance_register_form input"),
"ABC"
);
await click(target.querySelector(".oe_instance_register_form button"));
assert.containsNone(
target,
".database_expiration_panel",
"expiration panel should be gone"
);
assert.verifySteps([
// second try to submit
"get_param",
"set_param",
"get_param",
"get_param",
"update_notification",
"get_param",
// third try
"get_param",
"set_param",
"get_param",
"get_param",
"update_notification",
"get_param",
`{"message":"Thank you, your registration was successful! Your database is valid until November 15, 2019.","options":{"type":"success"}}`,
]);
}
);
QUnit.test(
"Expiration Panel one app installed, subscription already linked",
async function (assert) {
assert.expect(12);
patchDate(2019, 9, 10, 12, 0, 0);
// There are some line breaks mismatches between local and runbot test instances.
// Since they don't affect the layout and we're only interested in the text itself,
// we normalize whitespaces and line breaks from both the expected and end result
const formatWhiteSpaces = (text) =>
text
.split(/[\n\s]/)
.filter((w) => w !== "")
.join(" ");
let getExpirationDateCount = 0;
await createExpirationPanel({
session: {
expiration_date: "2019-10-15 12:00:00",
expiration_reason: "trial",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { method, args }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (route === "/already/linked/send/mail/url") {
return {
result: false,
reason: "By design",
};
}
assert.step(method);
if (args[0] === "database.expiration_date") {
getExpirationDateCount++;
if (getExpirationDateCount === 1) {
return "2019-10-15 12:00:00";
} else {
return "2019-11-17 12:00:00";
}
}
if (args[0] === "database.already_linked_subscription_url") {
return "www.super_company.com";
}
if (args[0] === "database.already_linked_send_mail_url") {
return "/already/linked/send/mail/url";
}
if (args[0] === "database.already_linked_email") {
return "super_company_admin@gmail.com";
}
return true;
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 5 days. Register your subscription or buy a subscription."
);
// Click on 'register your subscription'
await click(target.querySelector(".oe_instance_register_show"));
await testUtils.fields.editInput(
target.querySelector(".oe_instance_register_form input"),
"ABC"
);
await click(target.querySelector(".oe_instance_register_form button"));
assert.strictEqual(
formatWhiteSpaces(
target.querySelector(".oe_instance_register.oe_database_already_linked")
.innerText
),
formatWhiteSpaces(
`Your subscription is already linked to a database.
To unlink it you can either:
Login to your Odoo.com dashboard then unlink your previous database: www.super_company.com
Click here to send an email to the subscription owner (email: super_company_admin@gmail.com) with the instructions to follow`
)
);
await click(target.querySelector("a.oe_contract_send_mail"));
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-danger",
"Color should be red"
);
assert.strictEqual(
formatWhiteSpaces(
target.querySelector(".oe_instance_register.oe_database_already_linked")
.innerText
),
formatWhiteSpaces(
`Your subscription is already linked to a database.
To unlink it you can either:
Login to your Odoo.com dashboard then unlink your previous database: www.super_company.com
Click here to send an email to the subscription owner (email: super_company_admin@gmail.com) with the instructions to follow
Unable to send the instructions by email, please contact the Odoo Support
Error reason: By design`
)
);
assert.verifySteps([
"get_param",
"set_param",
"get_param",
"get_param",
"update_notification",
"get_param",
"get_param",
]);
}
);
QUnit.test("One app installed, database expired", async function (assert) {
assert.expect(8);
patchDate(2019, 9, 10, 12, 0, 0);
let callToGetParamCount = 0;
await createExpirationPanel({
session: {
expiration_date: "2019-10-08 12:00:00",
expiration_reason: "trial",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { args, method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
if (args[0] === "database.already_linked_subscription_url") {
return false;
}
callToGetParamCount++;
if (callToGetParamCount === 1) {
return "2019-10-09 12:00:00";
} else {
return "2019-11-09 12:00:00";
}
}
return true;
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database has expired. Register your subscription or buy a subscription."
);
assert.containsOnce(target, ".o_blockUI", "UI should be blocked");
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-danger",
"Color should be red"
);
assert.containsOnce(
target,
".oe_instance_register_show",
"Part 'Register your subscription'"
);
assert.containsOnce(target, ".oe_instance_buy", "Part 'buy a subscription'");
assert.containsNone(target, ".oe_instance_register_form");
// Click on 'Register your subscription'
await click(target.querySelector(".oe_instance_register_show"));
await testUtils.fields.editInput(
target.querySelector(".oe_instance_register_form input"),
"ABC"
);
await click(target.querySelector(".oe_instance_register_form button"));
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"Thank you, your registration was successful! Your database is valid until November 9, 2019."
);
assert.containsNone(target, ".o_blockUI", "UI should no longer be blocked");
});
QUnit.test("One app installed, renew", async function (assert) {
assert.expect(8);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { args, method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
assert.step("get_param");
assert.strictEqual(args[0], "database.enterprise_code");
return "ABC";
}
return true;
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 10 days. Renew your subscription "
);
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-warning",
"Color should be red"
);
assert.containsOnce(target, ".oe_instance_renew", "Part 'Register your subscription'");
assert.containsOnce(
target,
"a.check_enterprise_status",
"there should be a button for status checking"
);
assert.containsNone(target, ".oe_instance_register_form");
// Click on 'Renew your subscription'
await click(target.querySelector(".oe_instance_renew"));
assert.verifySteps(["get_param"]);
});
QUnit.test("One app installed, check status and get success", async function (assert) {
assert.expect(6);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { args, method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
assert.step("get_param");
assert.strictEqual(args[0], "database.expiration_date");
return "2019-10-24 12:00:00";
}
if (method === "update_notification") {
assert.step("update_notification");
}
return true;
},
});
// click on "Refresh subscription status"
const refreshButton = target.querySelector("a.check_enterprise_status");
assert.strictEqual(refreshButton.getAttribute("aria-label"), "Refresh subscription status");
await click(refreshButton);
assert.strictEqual(
target.querySelector(".oe_instance_register.oe_subscription_updated").innerText,
"Your subscription was updated and is valid until October 24, 2019."
);
assert.verifySteps(["update_notification", "get_param"]);
});
// Why would we want to reload the page when we check the status and it hasn't changed?
QUnit.skip("One app installed, check status and get page reload", async function (assert) {
assert.expect(4);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
assert.step("get_param");
return "2019-10-20 12:00:00";
}
if (method === "update_notification") {
assert.step("update_notification");
}
return true;
},
});
// click on "Refresh subscription status"
await click(target.querySelector("a.check_enterprise_status"));
assert.verifySteps(["update_notification", "get_param", "reloadPage"]);
});
QUnit.test("One app installed, upgrade database", async function (assert) {
assert.expect(6);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "upsell",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { args, method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
assert.step("get_param");
assert.strictEqual(args[0], "database.enterprise_code");
return "ABC";
}
if (method === "search_count") {
assert.step("search_count");
return 13;
}
return true;
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 10 days. You have more users or more apps installed than your subscription allows.\n" +
"Upgrade your subscription "
);
// click on "Upgrade your subscription"
await click(target.querySelector("a.oe_instance_upsell"));
assert.verifySteps(["get_param", "search_count"]);
});
QUnit.test("One app installed, message for non admin user", async function (assert) {
assert.expect(2);
patchDate(2019, 9, 10, 12, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-11-08 12:00:00",
expiration_reason: "",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "user",
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database will expire in 29 days. Log in as an administrator to correct the issue."
);
assert.hasClass(
target.querySelector(".database_expiration_panel"),
"alert-info",
"Color should be grey"
);
});
QUnit.test("One app installed, navigation to renewal page", async function (assert) {
assert.expect(9);
patchDate(2019, 11, 10, 0, 0, 0);
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
mockRPC(route, { args, method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
assert.step("get_param");
assert.strictEqual(args[0], "database.enterprise_code");
return "ABC";
}
if (method === "update_notification") {
assert.step("update_notification");
}
return true;
},
});
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"This database has expired. Renew your subscription "
);
assert.hasClass(target.querySelector(".database_expiration_panel"), "alert-danger");
assert.containsOnce(target, ".oe_instance_renew", "Part 'Register your subscription'");
assert.containsOnce(
target,
"a.check_enterprise_status",
"there should be a button for status checking"
);
assert.containsNone(target, ".oe_instance_register_form");
// Click on 'Renew your subscription'
await click(target.querySelector(".oe_instance_renew"));
assert.verifySteps(["get_param"]);
});
QUnit.test("One app installed, different locale (arabic)", async function (assert) {
assert.expect(1);
patchDate(2019, 9, 25, 12, 0, 0);
patchWithCleanup(luxon.Settings, {
defaultLocale: "ar-001",
defaultNumberingSystem: "arab",
});
await createExpirationPanel({
session: {
expiration_date: "2019-10-20 12:00:00",
expiration_reason: "renewal",
notification_type: true, // used by subscription service to know whether mail is installed
warning: "admin",
},
async mockRPC(route, { method }) {
if (route === "/web/webclient/load_menus") {
return;
}
if (method === "get_param") {
return "2019-11-09 12:00:00";
}
return true;
},
});
await click(target, ".check_enterprise_status");
assert.strictEqual(
target.querySelector(".oe_instance_register").innerText,
"Your subscription was updated and is valid until ٩ نوفمبر ٢٠١٩."
);
});
});

View File

@@ -0,0 +1,370 @@
/** @odoo-module **/
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { getFixture, nextTick, triggerHotkey, patchWithCleanup } from "@web/../tests/helpers/utils";
import { commandService } from "@web/core/commands/command_service";
import { hotkeyService } from "@web/core/hotkeys/hotkey_service";
import { ormService } from "@web/core/orm_service";
import { registry } from "@web/core/registry";
import { uiService } from "@web/core/ui/ui_service";
import { HomeMenu } from "@web_enterprise/webclient/home_menu/home_menu";
import testUtils from "web.test_utils";
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
import { session } from "@web/session";
import { templates } from "@web/core/assets";
const { App, EventBus } = owl;
const patchDate = testUtils.mock.patchDate;
const serviceRegistry = registry.category("services");
let target;
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
async function createHomeMenu(homeMenuProps) {
const env = await makeTestEnv();
const app = new App(HomeMenu, {
env,
props: homeMenuProps,
templates,
test: true,
});
const homeMenu = await app.mount(target);
registerCleanup(() => app.destroy());
return homeMenu;
}
async function walkOn(assert, path) {
for (const step of path) {
triggerHotkey(`${step.shiftKey ? "shift+" : ""}${step.key}`);
await nextTick();
assert.hasClass(
target.querySelectorAll(".o_menuitem")[step.index],
"o_focused",
`step ${step.number}`
);
}
}
// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------
let homeMenuProps;
let bus;
QUnit.module(
"web_enterprise",
{
beforeEach: function () {
homeMenuProps = {
apps: [
{
actionID: 121,
appID: 1,
id: 1,
label: "Discuss",
parents: "",
webIcon: false,
xmlid: "app.1",
},
{
actionID: 122,
appID: 2,
id: 2,
label: "Calendar",
parents: "",
webIcon: false,
xmlid: "app.2",
},
{
actionID: 123,
appID: 3,
id: 3,
label: "Contacts",
parents: "",
webIcon: false,
xmlid: "app.3",
},
],
};
bus = new EventBus();
const fakeHomeMenuService = {
name: "home_menu",
start() {
return {
toggle(show) {
bus.trigger("toggle", show);
},
};
},
};
const fakeMenuService = {
name: "menu",
start() {
return {
selectMenu(menu) {
bus.trigger("selectMenu", menu.id);
},
getMenu() {
return {};
},
};
},
};
serviceRegistry.add("ui", uiService);
serviceRegistry.add("hotkey", hotkeyService);
serviceRegistry.add("command", commandService);
serviceRegistry.add("localization", makeFakeLocalizationService());
serviceRegistry.add("orm", ormService);
serviceRegistry.add(enterpriseSubscriptionService.name, enterpriseSubscriptionService);
serviceRegistry.add(fakeHomeMenuService.name, fakeHomeMenuService);
serviceRegistry.add(fakeMenuService.name, fakeMenuService);
target = getFixture();
},
},
function () {
QUnit.module("HomeMenu");
QUnit.test("ESC Support", async function (assert) {
bus.on("toggle", null, (show) => {
assert.step(`toggle ${show}`);
});
await createHomeMenu(homeMenuProps);
await testUtils.dom.triggerEvent(window, "keydown", { key: "Escape" });
assert.verifySteps(["toggle false"]);
});
QUnit.test("Click on an app", async function (assert) {
bus.on("selectMenu", null, (menuId) => {
assert.step(`selectMenu ${menuId}`);
});
await createHomeMenu(homeMenuProps);
await testUtils.dom.click(target.querySelectorAll(".o_menuitem")[0]);
assert.verifySteps(["selectMenu 1"]);
});
QUnit.test("Display Expiration Panel (no module installed)", async function (assert) {
const unpatchDate = patchDate(2019, 9, 10, 0, 0, 0);
registerCleanup(unpatchDate);
patchWithCleanup(session, {
expiration_date: "2019-11-01 12:00:00",
expiration_reason: "",
isMailInstalled: false,
warning: "admin",
});
let cookie = false;
const mockedCookieService = {
name: "cookie",
start() {
return {
get current() {
return cookie;
},
setCookie() {
cookie = true;
},
};
},
};
serviceRegistry.add(mockedCookieService.name, mockedCookieService);
await createHomeMenu(homeMenuProps);
assert.containsOnce(target, ".database_expiration_panel");
assert.strictEqual(
target.querySelector(".database_expiration_panel .oe_instance_register").innerText,
"You will be able to register your database once you have installed your first app.",
"There should be an expiration panel displayed"
);
// Close the expiration panel
await testUtils.dom.click(
target.querySelector(".database_expiration_panel .oe_instance_hide_panel")
);
assert.containsNone(target, ".database_expiration_panel");
});
QUnit.test("Navigation (only apps, only one line)", async function (assert) {
assert.expect(8);
homeMenuProps = {
apps: new Array(3).fill().map((x, i) => {
return {
actionID: 120 + i,
appID: i + 1,
id: i + 1,
label: `0${i}`,
parents: "",
webIcon: false,
xmlid: `app.${i}`,
};
}),
};
await createHomeMenu(homeMenuProps);
const path = [
{ number: 0, key: "ArrowDown", index: 0 },
{ number: 1, key: "ArrowRight", index: 1 },
{ number: 2, key: "Tab", index: 2 },
{ number: 3, key: "ArrowRight", index: 0 },
{ number: 4, key: "Tab", shiftKey: true, index: 2 },
{ number: 5, key: "ArrowLeft", index: 1 },
{ number: 6, key: "ArrowDown", index: 1 },
{ number: 7, key: "ArrowUp", index: 1 },
];
await walkOn(assert, path);
});
QUnit.test("Navigation (only apps, two lines, one incomplete)", async function (assert) {
assert.expect(19);
homeMenuProps = {
apps: new Array(8).fill().map((x, i) => {
return {
actionID: 121,
appID: i + 1,
id: i + 1,
label: `0${i}`,
parents: "",
webIcon: false,
xmlid: `app.${i}`,
};
}),
};
await createHomeMenu(homeMenuProps);
const path = [
{ number: 1, key: "ArrowRight", index: 0 },
{ number: 2, key: "ArrowUp", index: 6 },
{ number: 3, key: "ArrowUp", index: 0 },
{ number: 4, key: "ArrowDown", index: 6 },
{ number: 5, key: "ArrowDown", index: 0 },
{ number: 6, key: "ArrowRight", index: 1 },
{ number: 7, key: "ArrowRight", index: 2 },
{ number: 8, key: "ArrowUp", index: 7 },
{ number: 9, key: "ArrowUp", index: 1 },
{ number: 10, key: "ArrowRight", index: 2 },
{ number: 11, key: "ArrowDown", index: 7 },
{ number: 12, key: "ArrowDown", index: 1 },
{ number: 13, key: "ArrowUp", index: 7 },
{ number: 14, key: "ArrowRight", index: 6 },
{ number: 15, key: "ArrowLeft", index: 7 },
{ number: 16, key: "ArrowUp", index: 1 },
{ number: 17, key: "ArrowLeft", index: 0 },
{ number: 18, key: "ArrowLeft", index: 5 },
{ number: 19, key: "ArrowRight", index: 0 },
];
await walkOn(assert, path);
});
QUnit.test("Navigation and open an app in the home menu", async function (assert) {
assert.expect(7);
bus.on("selectMenu", null, (menuId) => {
assert.step(`selectMenu ${menuId}`);
});
await createHomeMenu(homeMenuProps);
// No app selected so nothing to open
await testUtils.dom.triggerEvent(window, "keydown", { key: "Enter" });
assert.verifySteps([]);
const path = [
{ number: 0, key: "ArrowDown", index: 0 },
{ number: 1, key: "ArrowRight", index: 1 },
{ number: 2, key: "Tab", index: 2 },
{ number: 3, key: "shift+Tab", index: 1 },
];
await walkOn(assert, path);
// open first app (Calendar)
await testUtils.dom.triggerEvent(window, "keydown", { key: "Enter" });
assert.verifySteps(["selectMenu 2"]);
});
QUnit.test(
"The HomeMenu input takes the focus when you press a key only if no other element is the activeElement",
async function (assert) {
const target = getFixture();
const homeMenu = await createHomeMenu(homeMenuProps);
const input = target.querySelector(".o_search_hidden");
assert.strictEqual(document.activeElement, input);
const activeElement = document.createElement("div");
homeMenu.env.services.ui.activateElement(activeElement);
// remove the focus from the input
const otherInput = document.createElement("input");
target.querySelector(".o_home_menu").appendChild(otherInput);
otherInput.focus();
otherInput.blur();
assert.notEqual(document.activeElement, input);
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.notEqual(document.activeElement, input);
homeMenu.env.services.ui.deactivateElement(activeElement);
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.strictEqual(document.activeElement, input);
}
);
QUnit.test(
"The HomeMenu input does not take the focus if it is already on another input",
async function (assert) {
const target = getFixture();
await createHomeMenu(homeMenuProps);
const homeMenuInput = target.querySelector(".o_search_hidden");
assert.strictEqual(document.activeElement, homeMenuInput);
const otherInput = document.createElement("input");
target.querySelector(".o_home_menu").appendChild(otherInput);
otherInput.focus();
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.notEqual(document.activeElement, homeMenuInput);
otherInput.remove();
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.strictEqual(document.activeElement, homeMenuInput);
}
);
QUnit.test(
"The HomeMenu input does not take the focus if it is already on a textarea",
async function (assert) {
const target = getFixture();
await createHomeMenu(homeMenuProps);
const homeMenuInput = target.querySelector(".o_search_hidden");
assert.strictEqual(document.activeElement, homeMenuInput);
const textarea = document.createElement("textarea");
target.querySelector(".o_home_menu").appendChild(textarea);
textarea.focus();
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.notEqual(document.activeElement, homeMenuInput);
textarea.remove();
await testUtils.dom.triggerEvent(window, "keydown", { key: "a" });
await nextTick();
assert.strictEqual(document.activeElement, homeMenuInput);
}
);
}
);

View File

@@ -0,0 +1,528 @@
/** @odoo-module **/
import {
click,
editInput,
getFixture,
nextTick,
patchWithCleanup,
} from "@web/../tests/helpers/utils";
import { doAction, getActionManagerServerData, loadState } from "@web/../tests/webclient/helpers";
import { registry } from "@web/core/registry";
import { editView } from "@web/views/debug_items";
import { createEnterpriseWebClient } from "@web_enterprise/../tests/helpers";
import { homeMenuService } from "@web_enterprise/webclient/home_menu/home_menu_service";
import { ormService } from "@web/core/orm_service";
import { enterpriseSubscriptionService } from "@web_enterprise/webclient/home_menu/enterprise_subscription_service";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { errorService } from "@web/core/errors/error_service";
import { browser } from "@web/core/browser/browser";
const { Component, xml } = owl;
let serverData;
let fixture;
const serviceRegistry = registry.category("services");
// Should test ONLY the webClient and features present in Enterprise
// Those tests rely on hidden view to be in CSS: display: none
QUnit.module("WebClient Enterprise", (hooks) => {
hooks.beforeEach(() => {
serverData = getActionManagerServerData();
fixture = getFixture();
serviceRegistry.add("home_menu", homeMenuService);
serviceRegistry.add("orm", ormService);
serviceRegistry.add("enterprise_subscription", enterpriseSubscriptionService);
});
QUnit.module("basic flow with home menu", (hooks) => {
let mockRPC;
hooks.beforeEach((assert) => {
serverData.menus[1].actionID = 4;
serverData.menus.root.children = [1];
serverData.views["partner,false,form"] = `
<form>
<field name="display_name"/>
<field name="m2o" open_target="current"/>
</form>`;
mockRPC = async (route) => {
assert.step(route);
if (route === "/web/dataset/call_kw/partner/get_formview_action") {
return {
type: "ir.actions.act_window",
res_model: "partner",
view_type: "form",
view_mode: "form",
views: [[false, "form"]],
target: "current",
res_id: 2,
};
}
};
});
QUnit.test("1 -- start up", async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
assert.ok(document.body.classList.contains("o_home_menu_background"));
assert.containsOnce(fixture, ".o_home_menu");
assert.isNotVisible(fixture.querySelector(".o_menu_toggle"));
assert.containsOnce(fixture, ".o_app.o_menuitem");
});
QUnit.test("2 -- navbar updates on displaying an action", async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await click(fixture.querySelector(".o_app.o_menuitem"));
assert.verifySteps([
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
]);
assert.notOk(document.body.classList.contains("o_home_menu_background"));
assert.containsNone(fixture, ".o_home_menu");
assert.containsOnce(fixture, ".o_kanban_view");
const menuToggle = fixture.querySelector(".o_menu_toggle");
assert.isVisible(menuToggle);
assert.notOk(menuToggle.classList.contains("o_menu_toggle_back"));
});
QUnit.test("3 -- push another action in the breadcrumb", async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await click(fixture.querySelector(".o_app.o_menuitem"));
assert.verifySteps([
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
]);
await click(fixture.querySelector(".o_kanban_record"));
await nextTick(); // there is another tick to update navbar and destroy HomeMenu
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
assert.isVisible(fixture.querySelector(".o_menu_toggle"));
assert.containsOnce(fixture, ".o_form_view");
assert.strictEqual(
fixture.querySelector(".breadcrumb-item.active").textContent,
"First record"
);
});
QUnit.test("4 -- push a third action in the breadcrumb", async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await click(fixture.querySelector(".o_app.o_menuitem"));
assert.verifySteps([
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
]);
await click(fixture.querySelector(".o_kanban_record"));
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
await click(fixture, '.o_field_widget[name="m2o"] .o_external_button', true);
assert.verifySteps([
"/web/dataset/call_kw/partner/get_formview_action",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
]);
assert.containsOnce(fixture, ".o_form_view");
assert.strictEqual(
fixture.querySelector(".breadcrumb-item.active").textContent,
"Second record"
);
assert.containsN(fixture, ".breadcrumb-item", 3);
});
QUnit.test(
"5 -- switch to HomeMenu from an action with 2 breadcrumbs",
async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await click(fixture.querySelector(".o_app.o_menuitem"));
assert.verifySteps([
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
]);
await click(fixture.querySelector(".o_kanban_record"));
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
await click(fixture, '.o_field_widget[name="m2o"] .o_external_button', true);
assert.verifySteps([
"/web/dataset/call_kw/partner/get_formview_action",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
]);
const menuToggle = fixture.querySelector(".o_menu_toggle");
await click(menuToggle);
assert.verifySteps([]);
assert.ok(menuToggle.classList.contains("o_menu_toggle_back"));
assert.containsOnce(fixture, ".o_home_menu");
assert.isNotVisible(fixture.querySelector(".o_form_view"));
}
);
QUnit.test("6 -- back to underlying action with many breadcrumbs", async function (assert) {
await createEnterpriseWebClient({ fixture, serverData, mockRPC });
assert.verifySteps(["/web/webclient/load_menus"]);
await click(fixture.querySelector(".o_app.o_menuitem"));
assert.verifySteps([
"/web/action/load",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/web_search_read",
]);
await click(fixture.querySelector(".o_kanban_record"));
assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
await click(fixture, '.o_field_widget[name="m2o"] .o_external_button', true);
assert.verifySteps([
"/web/dataset/call_kw/partner/get_formview_action",
"/web/dataset/call_kw/partner/get_views",
"/web/dataset/call_kw/partner/read",
]);
const menuToggle = fixture.querySelector(".o_menu_toggle");
await click(menuToggle);
await click(menuToggle);
// if we don't reload on going back to underlying action
// assert.verifySteps(
// [],
// "the underlying view should not reload when toggling the HomeMenu to off"
// );
// endif
// if we reload on going back to underlying action
assert.verifySteps(
["/web/dataset/call_kw/partner/read"],
"the underlying view should reload when toggling the HomeMenu to off"
);
// endif
assert.containsNone(fixture, ".o_home_menu");
assert.containsOnce(fixture, ".o_form_view");
assert.notOk(menuToggle.classList.contains("o_menu_toggle_back"));
assert.strictEqual(
fixture.querySelector(".breadcrumb-item.active").textContent,
"Second record"
);
assert.containsN(fixture, ".breadcrumb-item", 3);
});
QUnit.test("restore the newly created record in form view (legacy)", async (assert) => {
const action = serverData.actions[6];
delete action.res_id;
action.target = "current";
const webClient = await createEnterpriseWebClient({ fixture, serverData });
await doAction(webClient, 6);
assert.containsOnce(fixture, ".o_form_view");
assert.containsOnce(fixture, ".o_form_view .o_form_editable");
await editInput(fixture, ".o_field_widget[name=display_name] input", "red right hand");
await click(fixture.querySelector(".o_form_button_save"));
assert.strictEqual(
fixture.querySelector(".breadcrumb-item.active").textContent,
"red right hand"
);
await click(fixture.querySelector(".o_menu_toggle"));
assert.isNotVisible(fixture.querySelector(".o_form_view"));
await click(fixture.querySelector(".o_menu_toggle"));
assert.containsOnce(fixture, ".o_form_view");
assert.containsOnce(fixture, ".o_form_view .o_form_saved");
assert.strictEqual(
fixture.querySelector(".breadcrumb-item.active").textContent,
"red right hand"
);
});
QUnit.skip("fast clicking on restore (implementation detail)", async (assert) => {
assert.expect(6);
let doVeryFastClick = false;
class DelayedClientAction extends Component {
setup() {
owl.onMounted(() => {
if (doVeryFastClick) {
doVeryFastClick = false;
click(fixture.querySelector(".o_menu_toggle"));
}
});
}
}
DelayedClientAction.template = xml`<div class='delayed_client_action'>
<button t-on-click="resolve">RESOLVE</button>
</div>`;
registry.category("actions").add("DelayedClientAction", DelayedClientAction);
const webClient = await createEnterpriseWebClient({ fixture, serverData });
await doAction(webClient, "DelayedClientAction");
await nextTick();
await click(fixture.querySelector(".o_menu_toggle"));
assert.isVisible(fixture.querySelector(".o_home_menu"));
assert.isNotVisible(fixture.querySelector(".delayed_client_action"));
doVeryFastClick = true;
await click(fixture.querySelector(".o_menu_toggle"));
await nextTick();
// off homemenu
assert.isVisible(fixture.querySelector(".o_home_menu"));
assert.isNotVisible(fixture.querySelector(".delayed_client_action"));
await click(fixture.querySelector(".o_menu_toggle"));
await nextTick();
assert.containsNone(fixture, ".o_home_menu");
assert.containsOnce(fixture, ".delayed_client_action");
});
});
QUnit.test("clear unCommittedChanges when toggling home menu", async function (assert) {
assert.expect(6);
// Edit a form view, don't save, toggle home menu
// the autosave feature of the Form view is activated
// and relied upon by this test
const mockRPC = (route, args) => {
if (args.method === "create") {
assert.strictEqual(args.model, "partner");
assert.deepEqual(args.args, [
{
display_name: "red right hand",
foo: false,
},
]);
}
};
const webClient = await createEnterpriseWebClient({ fixture, serverData, mockRPC });
await doAction(webClient, 3, { viewType: "form" });
assert.containsOnce(fixture, ".o_form_view .o_form_editable");
await editInput(fixture, ".o_field_widget[name=display_name] input", "red right hand");
await click(fixture.querySelector(".o_menu_toggle"));
assert.containsNone(fixture, ".o_form_view");
assert.containsNone(fixture, ".modal");
assert.containsOnce(fixture, ".o_home_menu");
});
QUnit.test("can have HomeMenu and dialog action", async function (assert) {
const webClient = await createEnterpriseWebClient({ fixture, serverData });
assert.containsOnce(fixture, ".o_home_menu");
assert.containsNone(fixture, ".modal .o_form_view");
await doAction(webClient, 5);
assert.containsOnce(fixture, ".modal .o_form_view");
assert.isVisible(fixture.querySelector(".modal .o_form_view"));
assert.containsOnce(fixture, ".o_home_menu");
});
QUnit.test("supports attachments of apps deleted", async function (assert) {
// When doing a pg_restore without the filestore
// LPE fixme: may not be necessary anymore since menus are not HomeMenu props anymore
serverData.menus = {
root: { id: "root", children: [1], name: "root", appID: "root" },
1: {
id: 1,
appID: 1,
actionID: 1,
xmlid: "",
name: "Partners",
children: [],
webIconData: "",
webIcon: "bloop,bloop",
},
};
patchWithCleanup(odoo, { debug: "1" });
await createEnterpriseWebClient({ fixture, serverData });
assert.containsOnce(fixture, ".o_home_menu");
});
QUnit.test(
"debug manager resets to global items when home menu is displayed",
async function (assert) {
const debugRegistry = registry.category("debug");
debugRegistry.category("view").add("editView", editView);
debugRegistry.category("default").add("item_1", () => {
return {
type: "item",
description: "globalItem",
callback: () => {},
sequence: 10,
};
});
const mockRPC = async (route) => {
if (route.includes("check_access_rights")) {
return true;
}
};
patchWithCleanup(odoo, { debug: "1" });
const webClient = await createEnterpriseWebClient({ fixture, serverData, mockRPC });
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
assert.containsOnce(fixture, ".o_debug_manager .dropdown-item:contains('globalItem')");
assert.containsNone(
fixture,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
await doAction(webClient, 1);
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
assert.containsOnce(fixture, ".o_debug_manager .dropdown-item:contains('globalItem')");
assert.containsOnce(
fixture,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
await click(fixture.querySelector(".o_menu_toggle"));
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
assert.containsOnce(fixture, ".o_debug_manager .dropdown-item:contains('globalItem')");
assert.containsNone(
fixture,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
await doAction(webClient, 3);
await click(fixture.querySelector(".o_debug_manager .dropdown-toggle"));
assert.containsOnce(fixture, ".o_debug_manager .dropdown-item:contains('globalItem')");
assert.containsOnce(
fixture,
".o_debug_manager .dropdown-item:contains('Edit View: List')"
);
assert.containsNone(
fixture,
".o_debug_manager .dropdown-item:contains('Edit View: Kanban')"
);
}
);
QUnit.test(
"url state is well handled when going in and out of the HomeMenu",
async function (assert) {
const webClient = await createEnterpriseWebClient({ fixture, serverData });
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, { action: "menu" });
await click(fixture.querySelector(".o_app.o_menuitem:nth-child(2)"));
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 1002,
menu_id: 2,
});
await click(fixture.querySelector(".o_menu_toggle"));
await nextTick();
assert.deepEqual(webClient.env.services.router.current.hash, { action: "menu" });
await click(fixture.querySelector(".o_menu_toggle"));
await nextTick();
// if we reload on going back to underlying action
// end if
assert.deepEqual(webClient.env.services.router.current.hash, {
action: 1002,
menu_id: 2,
});
}
);
QUnit.test(
"underlying action's menu items are invisible when HomeMenu is displayed",
async function (assert) {
serverData.menus[1].children = [99];
serverData.menus[99] = {
id: 99,
children: [],
name: "SubMenu",
appID: 1,
actionID: 1002,
xmlid: "",
webIconData: undefined,
webIcon: false,
};
await createEnterpriseWebClient({ fixture, serverData });
assert.containsNone(fixture, "nav .o_menu_sections");
assert.containsNone(fixture, "nav .o_menu_brand");
await click(fixture.querySelector(".o_app.o_menuitem:nth-child(1)"));
await nextTick();
assert.containsOnce(fixture, "nav .o_menu_sections");
assert.containsOnce(fixture, "nav .o_menu_brand");
assert.isVisible(fixture.querySelector(".o_menu_sections"));
assert.isVisible(fixture.querySelector(".o_menu_brand"));
await click(fixture.querySelector(".o_menu_toggle"));
assert.containsOnce(fixture, "nav .o_menu_sections");
assert.containsOnce(fixture, "nav .o_menu_brand");
assert.isNotVisible(fixture.querySelector(".o_menu_sections"));
assert.isNotVisible(fixture.querySelector(".o_menu_brand"));
}
);
QUnit.test("loadState back and forth keeps relevant keys in state", async function (assert) {
const webClient = await createEnterpriseWebClient({ fixture, serverData });
await click(fixture.querySelector(".o_app.o_menuitem:nth-child(2)"));
await nextTick();
assert.containsOnce(fixture, ".test_client_action");
assert.containsNone(fixture, ".o_home_menu");
const state = webClient.env.services.router.current.hash;
assert.deepEqual(state, {
action: 1002,
menu_id: 2,
});
await loadState(webClient, {});
assert.containsNone(fixture, ".test_client_action");
assert.containsOnce(fixture, ".o_home_menu");
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "menu",
});
await loadState(webClient, state);
assert.containsOnce(fixture, ".test_client_action");
assert.containsNone(fixture, ".o_home_menu");
assert.deepEqual(webClient.env.services.router.current.hash, state);
});
QUnit.test(
"go back to home menu using browser back button (i.e. loadState)",
async function (assert) {
const webClient = await createEnterpriseWebClient({ fixture, serverData });
assert.containsOnce(fixture, ".o_home_menu");
assert.isNotVisible(fixture.querySelector(".o_main_navbar .o_menu_toggle"));
await click(fixture.querySelector(".o_app.o_menuitem:nth-child(2)"));
assert.containsOnce(fixture, ".test_client_action");
assert.containsNone(fixture, ".o_home_menu");
await loadState(webClient, { action: "menu" }); // FIXME: this might need to be changed
assert.containsNone(fixture, ".test_client_action");
assert.containsOnce(fixture, ".o_home_menu");
assert.isNotVisible(fixture.querySelector(".o_main_navbar .o_menu_toggle"));
}
);
QUnit.test("initial action crashes", async (assert) => {
const handler = (ev) => {
// need to preventDefault to remove error from console (so python test pass)
ev.preventDefault();
};
window.addEventListener("unhandledrejection", handler);
registerCleanup(() => window.removeEventListener("unhandledrejection", handler));
patchWithCleanup(QUnit, {
onUnhandledRejection: () => {},
});
browser.location.hash = "#action=__test__client__action__&menu_id=1";
const ClientAction = registry.category("actions").get("__test__client__action__");
class Override extends ClientAction {
setup() {
super.setup();
assert.step("clientAction setup");
throw new Error("my error");
}
}
registry.category("actions").add("__test__client__action__", Override, { force: true });
registry.category("services").add("error", errorService);
const webClient = await createEnterpriseWebClient({ fixture, serverData });
assert.verifySteps(["clientAction setup"]);
assert.containsOnce(fixture, "nav .o_menu_toggle");
assert.isVisible(fixture.querySelector("nav .o_menu_toggle"));
assert.strictEqual(fixture.querySelector(".o_action_manager").innerHTML, "");
assert.deepEqual(webClient.env.services.router.current.hash, {
action: "__test__client__action__",
menu_id: 1,
});
});
});