vanilla 19.0

This commit is contained in:
Ernad Husremovic 2025-10-08 10:49:46 +02:00
parent 991d2234ca
commit d1963a3c3a
3066 changed files with 1651266 additions and 922560 deletions

View file

@ -1,172 +0,0 @@
odoo.define("base.abstract_controller_tests", function (require) {
"use strict";
var testUtils = require("web.test_utils");
var createView = testUtils.createView;
var BasicView = require("web.BasicView");
var BasicRenderer = require("web.BasicRenderer");
const AbstractRenderer = require('web.AbstractRendererOwl');
const RendererWrapper = require('web.RendererWrapper');
const { xml, onMounted, onWillUnmount, onWillDestroy } = require("@odoo/owl");
function getHtmlRenderer(html) {
return BasicRenderer.extend({
start: function () {
this.$el.html(html);
return this._super.apply(this, arguments);
}
});
}
function getOwlView(owlRenderer, viewType) {
viewType = viewType || "test";
return BasicView.extend({
viewType: viewType,
config: _.extend({}, BasicView.prototype.config, {
Renderer: owlRenderer,
}),
getRenderer() {
return new RendererWrapper(null, this.config.Renderer, {});
}
});
}
function getHtmlView(html, viewType) {
viewType = viewType || "test";
return BasicView.extend({
viewType: viewType,
config: _.extend({}, BasicView.prototype.config, {
Renderer: getHtmlRenderer(html)
})
});
}
QUnit.module("LegacyViews", {
beforeEach: function () {
this.data = {
test_model: {
fields: {},
records: []
}
};
}
}, function () {
QUnit.module('AbstractController');
QUnit.test('click on a a[type="action"] child triggers the correct action', async function (assert) {
assert.expect(7);
var html =
"<div>" +
'<a name="a1" type="action" class="simple">simple</a>' +
'<a name="a2" type="action" class="with-child">' +
"<span>child</input>" +
"</a>" +
'<a type="action" data-model="foo" data-method="bar" class="method">method</a>' +
'<a type="action" data-model="foo" data-res-id="42" class="descr">descr</a>' +
'<a type="action" data-model="foo" class="descr2">descr2</a>' +
"</div>";
var view = await createView({
View: getHtmlView(html, "test"),
data: this.data,
model: "test_model",
arch: "<test/>",
intercepts: {
do_action: function (event) {
assert.step(event.data.action.name || event.data.action);
}
},
mockRPC: function (route, args) {
if (args.model === 'foo' && args.method === 'bar') {
assert.step("method");
return Promise.resolve({name: 'method'});
}
return this._super.apply(this, arguments);
}
});
await testUtils.dom.click(view.$(".simple"));
await testUtils.dom.click(view.$(".with-child span"));
await testUtils.dom.click(view.$(".method"));
await testUtils.dom.click(view.$(".descr"));
await testUtils.dom.click(view.$(".descr2"));
assert.verifySteps(["a1", "a2", "method", "method", "descr", "descr2"]);
view.destroy();
});
QUnit.test('OWL Renderer correctly destroyed', async function (assert) {
assert.expect(2);
class Renderer extends AbstractRenderer {
setup() {
onWillDestroy(() => {
assert.step("destroy");
});
}
}
Renderer.template = xml`<div>Test</div>`;
var view = await createView({
View: getOwlView(Renderer, "test"),
data: this.data,
model: "test_model",
arch: "<test/>",
});
view.destroy();
assert.verifySteps(["destroy"]);
});
QUnit.test('Correctly set focus to search panel with Owl Renderer', async function (assert) {
assert.expect(1);
class Renderer extends AbstractRenderer { }
Renderer.template = xml`<div>Test</div>`;
var view = await createView({
View: getOwlView(Renderer, "test"),
data: this.data,
model: "test_model",
arch: "<test/>",
});
assert.hasClass(document.activeElement, "o_searchview_input");
view.destroy();
});
QUnit.test('Owl Renderer mounted/willUnmount hooks are properly called', async function (assert) {
// This test could be removed as soon as controllers and renderers will
// both be converted in Owl.
assert.expect(3);
class Renderer extends AbstractRenderer {
setup() {
onMounted(() => {
assert.step("mounted");
});
onWillUnmount(() => {
assert.step("unmounted");
});
}
}
Renderer.template = xml`<div>Test</div>`;
const view = await createView({
View: getOwlView(Renderer, "test"),
data: this.data,
model: "test_model",
arch: "<test/>",
});
view.destroy();
assert.verifySteps([
"mounted",
"unmounted",
]);
});
});
});

View file

@ -1,130 +0,0 @@
odoo.define('web.abstract_model_tests', function (require) {
"use strict";
const AbstractModel = require('web.AbstractModel');
const Domain = require('web.Domain');
QUnit.module('LegacyViews', {}, function () {
QUnit.module('AbstractModel');
QUnit.test('leave sample mode when unknown route is called on sample server', async function (assert) {
assert.expect(4);
const Model = AbstractModel.extend({
_isEmpty() {
return true;
},
async __load() {
if (this.isSampleModel) {
await this._rpc({ model: 'partner', method: 'unknown' });
}
},
});
const model = new Model(null, {
modelName: 'partner',
fields: {},
useSampleModel: true,
SampleModel: Model,
});
assert.ok(model.useSampleModel);
assert.notOk(model._isInSampleMode);
await model.load({});
assert.notOk(model.useSampleModel);
assert.notOk(model._isInSampleMode);
model.destroy();
});
QUnit.test("don't cath general error on sample server in sample mode", async function (assert) {
assert.expect(5);
const error = new Error();
const Model = AbstractModel.extend({
_isEmpty() {
return true;
},
async __reload() {
if (this.isSampleModel) {
await this._rpc({ model: 'partner', method: 'read_group' });
}
},
async _rpc() {
throw error;
},
});
const model = new Model(null, {
modelName: 'partner',
fields: {},
useSampleModel: true,
SampleModel: Model,
});
assert.ok(model.useSampleModel);
assert.notOk(model._isInSampleMode);
await model.load({});
assert.ok(model.useSampleModel);
assert.ok(model._isInSampleMode);
async function reloadModel() {
try {
await model.reload();
} catch (e) {
assert.strictEqual(e, error);
}
}
await reloadModel();
model.destroy();
});
QUnit.test('fetch sample data: concurrency', async function (assert) {
assert.expect(3);
const Model = AbstractModel.extend({
_isEmpty() {
return true;
},
__get() {
return { isSample: !!this.isSampleModel };
},
});
const model = new Model(null, {
modelName: 'partner',
fields: {},
useSampleModel: true,
SampleModel: Model,
});
await model.load({ domain: Domain.FALSE_DOMAIN, });
const beforeReload = model.get(null, { withSampleData: true });
const reloaded = model.reload(null, { domain: Domain.TRUE_DOMAIN });
const duringReload = model.get(null, { withSampleData: true });
await reloaded;
const afterReload = model.get(null, { withSampleData: true });
assert.strictEqual(beforeReload.isSample, true,
"Sample data flag must be true before reload"
);
assert.strictEqual(duringReload.isSample, true,
"Sample data flag must be true during reload"
);
assert.strictEqual(afterReload.isSample, false,
"Sample data flag must be true after reload"
);
});
});
});

View file

@ -1,109 +0,0 @@
odoo.define('web.abstract_view_banner_tests', function (require) {
"use strict";
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractView = require('web.AbstractView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var TestRenderer = AbstractRenderer.extend({
_renderView: function () {
this.$el.addClass('test_content');
return this._super();
},
});
var TestView = AbstractView.extend({
type: 'test',
config: _.extend({}, AbstractView.prototype.config, {
Renderer: TestRenderer
}),
});
var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css';
QUnit.module('LegacyViews', {
beforeEach: function () {
this.data = {
test_model: {
fields: {},
records: [],
},
};
},
afterEach: function () {
$('head link[href$="' + test_css_url + '"]').remove();
}
}, function () {
QUnit.module('BasicRenderer');
QUnit.test("The banner should be fetched from the route", function (assert) {
var done = assert.async();
assert.expect(6);
var banner_html =`
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog"
data-bs-backdrop="false">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-footer">
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
data-o-hide-banner="true">
Remove
</a>
</div>
</div>
</div>
</div>
<div class="o_onboarding_container collapse show">
<div class="o_onboarding_wrap">
<a href="#" data-bs-toggle="modal" data-bs-target=".o_onboarding_modal"
class="float-end o_onboarding_btn_close">
<i class="fa fa-times" title="Close the onboarding panel" />
</a>
</div>
<div>
<link type="text/css" href="` + test_css_url + `" rel="stylesheet">
<div class="hello_banner">Here is the banner</div>
</div>
</div>`;
createView({
View: TestView,
model: 'test_model',
data: this.data,
arch: '<test banner_route="/module/hello_banner"/>',
mockRPC: function (route, args) {
if (route === '/module/hello_banner') {
assert.step(route);
return Promise.resolve({html: banner_html});
}
return this._super(route, args);
},
}).then(async function (view) {
var $banner = view.$('.hello_banner');
assert.strictEqual($banner.length, 1,
"The view should contain the response from the controller.");
assert.verifySteps(['/module/hello_banner'], "The banner should be fetched.");
var $head_link = $('head link[href$="' + test_css_url + '"]');
assert.strictEqual($head_link.length, 1,
"The stylesheet should have been added to head.");
var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
assert.strictEqual($banner_link.length, 0,
"The stylesheet should have been removed from the banner.");
await testUtils.dom.click(view.$('.o_onboarding_btn_close')); // click on close to remove banner
await testUtils.dom.click(view.$('.o_technical_modal .btn-primary:contains("Remove")')); // click on button remove from techinal modal
assert.strictEqual(view.$('.o_onboarding_container.show').length, 0,
"Banner should be removed from the view");
view.destroy();
done();
});
});
}
);
});

View file

@ -1,69 +0,0 @@
odoo.define('web.abstract_view_tests', function (require) {
"use strict";
const { registry } = require('@web/core/registry');
const legacyViewRegistry = require('web.view_registry');
var ListView = require('web.ListView');
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
QUnit.module('LegacyViews', {
beforeEach: function () {
this.data = {
fake_model: {
fields: {},
record: [],
},
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"},
]
},
};
},
}, function () {
QUnit.module('AbstractView');
QUnit.test('group_by from context can be a string, instead of a list of strings', async function (assert) {
assert.expect(1);
registry.category("views").remove("list"); // remove new list from registry
legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry
const serverData = {
actions: {
1: {
id: 1,
name: 'Foo',
res_model: 'foo',
type: 'ir.actions.act_window',
views: [[false, 'list']],
context: {
group_by: 'bar',
},
}
},
views: {
'foo,false,list': '<tree><field name="foo"/><field name="bar"/></tree>',
'foo,false,search': '<search></search>',
},
models: this.data
};
const mockRPC = (route, args) => {
if (args.method === 'web_read_group') {
assert.deepEqual(args.kwargs.groupby, ['bar']);
}
};
const webClient = await createWebClient({ serverData, mockRPC });
await doAction(webClient, 1);
});
});
});

View file

@ -1,80 +0,0 @@
odoo.define('web.basic_view_tests', function (require) {
"use strict";
const BasicView = require('web.BasicView');
const BasicRenderer = require("web.BasicRenderer");
const testUtils = require('web.test_utils');
const widgetRegistryOwl = require('web.widgetRegistry');
const { LegacyComponent } = require("@web/legacy/legacy_component");
const { xml } = owl;
const createView = testUtils.createView;
QUnit.module('LegacyViews', {
beforeEach: function () {
this.data = {
fake_model: {
fields: {},
record: [],
},
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" },
]
},
};
},
}, function () {
QUnit.module('BasicView');
QUnit.test('fields given in fieldDependencies of custom widget are loaded', async function (assert) {
assert.expect(1);
const basicView = BasicView.extend({
viewType: "test",
config: Object.assign({}, BasicView.prototype.config, {
Renderer: BasicRenderer,
})
});
class MyWidget extends LegacyComponent {}
MyWidget.fieldDependencies = {
foo: { type: 'char' },
bar: { type: 'boolean' },
};
MyWidget.template = xml/* xml */`
<div class="custom-widget">Hello World!</div>
`;
widgetRegistryOwl.add('testWidget', MyWidget);
const view = await createView({
View: basicView,
data: this.data,
model: "foo",
arch:
`<test>
<widget name="testWidget"/>
</test>`,
mockRPC: function (route, args) {
if (route === "/web/dataset/search_read") {
assert.deepEqual(args.fields, ["foo", "bar"],
"search_read should be called with dependent fields");
return Promise.resolve();
}
return this._super.apply(this, arguments);
}
});
view.destroy();
delete widgetRegistryOwl.map.testWidget;
});
});
});

View file

@ -0,0 +1,555 @@
/** @odoo-module alias=@web/../tests/views/calendar/helpers default=false */
import { uiService } from "@web/core/ui/ui_service";
import { createElement } from "@web/core/utils/xml";
import { registry } from "@web/core/registry";
import { Field } from "@web/views/fields/field";
import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env";
import { click, getFixture, mount, nextTick, triggerEvent } from "../../helpers/utils";
import { setupViewRegistries } from "@web/../tests/views/helpers";
export function makeEnv(services = {}) {
clearRegistryWithCleanup(registry.category("main_components"));
setupViewRegistries();
services = Object.assign(
{
ui: uiService,
},
services
);
for (const [key, service] of Object.entries(services)) {
registry.category("services").add(key, service, { force: true });
}
return makeTestEnv({
config: {
setDisplayName: () => {},
},
});
}
//------------------------------------------------------------------------------
export async function mountComponent(C, env, props) {
const target = getFixture();
return await mount(C, target, { env, props });
}
//------------------------------------------------------------------------------
export function makeFakeDate() {
return luxon.DateTime.local(2021, 7, 16, 8, 0, 0, 0);
}
export function makeFakeRecords() {
return {
1: {
id: 1,
title: "1 day, all day in July",
start: makeFakeDate(),
isAllDay: true,
end: makeFakeDate(),
},
2: {
id: 2,
title: "3 days, all day in July",
start: makeFakeDate().plus({ days: 2 }),
isAllDay: true,
end: makeFakeDate().plus({ days: 4 }),
},
3: {
id: 3,
title: "1 day, all day in June",
start: makeFakeDate().plus({ months: -1 }),
isAllDay: true,
end: makeFakeDate().plus({ months: -1 }),
},
4: {
id: 4,
title: "3 days, all day in June",
start: makeFakeDate().plus({ months: -1, days: 2 }),
isAllDay: true,
end: makeFakeDate().plus({ months: -1, days: 4 }),
},
5: {
id: 5,
title: "Over June and July",
start: makeFakeDate().startOf("month").plus({ days: -2 }),
isAllDay: true,
end: makeFakeDate().startOf("month").plus({ days: 2 }),
},
};
}
export const FAKE_FILTER_SECTIONS = [
{
label: "Attendees",
fieldName: "partner_ids",
avatar: {
model: "res.partner",
field: "avatar_128",
},
hasAvatar: true,
write: {
model: "filter_partner",
field: "partner_id",
},
canAddFilter: true,
filters: [
{
type: "user",
label: "Mitchell Admin",
active: true,
value: 3,
colorIndex: 3,
recordId: null,
canRemove: false,
hasAvatar: true,
},
{
type: "all",
label: "Everybody's calendar",
active: false,
value: "all",
colorIndex: null,
recordId: null,
canRemove: false,
hasAvatar: false,
},
{
type: "record",
label: "Brandon Freeman",
active: true,
value: 4,
colorIndex: 4,
recordId: 1,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 6,
colorIndex: 6,
recordId: 2,
canRemove: true,
hasAvatar: true,
},
],
},
{
label: "Users",
fieldName: "user_id",
avatar: {
model: null,
field: null,
},
hasAvatar: false,
write: {
model: null,
field: null,
},
canAddFilter: false,
filters: [
{
type: "record",
label: "Brandon Freeman",
active: false,
value: 1,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
{
type: "record",
label: "Marc Demo",
active: false,
value: 2,
colorIndex: false,
recordId: null,
canRemove: true,
hasAvatar: true,
},
],
},
];
export const FAKE_FIELDS = {
id: { string: "Id", type: "integer" },
user_id: { string: "User", type: "many2one", relation: "user", default: -1 },
partner_id: {
string: "Partner",
type: "many2one",
relation: "partner",
related: "user_id.partner_id",
default: 1,
},
name: { string: "Name", type: "char" },
start_date: { string: "Start Date", type: "date" },
stop_date: { string: "Stop Date", type: "date" },
start: { string: "Start Datetime", type: "datetime" },
stop: { string: "Stop Datetime", type: "datetime" },
delay: { string: "Delay", type: "float" },
allday: { string: "Is All Day", type: "boolean" },
partner_ids: {
string: "Attendees",
type: "one2many",
relation: "partner",
default: [[6, 0, [1]]],
},
type: { string: "Type", type: "integer" },
event_type_id: { string: "Event Type", type: "many2one", relation: "event_type" },
color: { string: "Color", type: "integer", related: "event_type_id.color" },
};
function makeFakeModelState() {
const fakeFieldNode = createElement("field", { name: "name" });
const fakeModels = { event: { fields: FAKE_FIELDS } };
return {
canCreate: true,
canDelete: true,
canEdit: true,
date: makeFakeDate(),
fieldMapping: {
date_start: "start_date",
date_stop: "stop_date",
date_delay: "delay",
all_day: "allday",
color: "color",
},
fieldNames: ["start_date", "stop_date", "color", "delay", "allday", "user_id"],
fields: FAKE_FIELDS,
filterSections: FAKE_FILTER_SECTIONS,
firstDayOfWeek: 0,
isDateHidden: false,
isTimeHidden: false,
hasAllDaySlot: true,
hasEditDialog: false,
quickCreate: false,
popoverFieldNodes: {
name: Field.parseFieldNode(fakeFieldNode, fakeModels, "event", "calendar"),
},
activeFields: {
name: {
context: "{}",
invisible: false,
readonly: false,
required: false,
onChange: false,
},
},
rangeEnd: makeFakeDate().endOf("month"),
rangeStart: makeFakeDate().startOf("month"),
records: makeFakeRecords(),
resModel: "event",
scale: "month",
scales: ["day", "week", "month", "year"],
unusualDays: [],
};
}
export function makeFakeModel(state = {}) {
return {
...makeFakeModelState(),
load() {},
createFilter() {},
createRecord() {},
unlinkFilter() {},
unlinkRecord() {},
updateFilter() {},
updateRecord() {},
...state,
};
}
// DOM Utils
//------------------------------------------------------------------------------
async function scrollTo(el, scrollParam) {
el.scrollIntoView(scrollParam);
await new Promise(window.requestAnimationFrame);
}
export function findPickedDate(target) {
return target.querySelector(".o_datetime_picker .o_selected");
}
export async function pickDate(target, date) {
const day = date.split("-")[2];
const iDay = parseInt(day, 10) - 1;
const el = target.querySelectorAll(`.o_datetime_picker .o_date_item_cell:not(.o_out_of_range)`)[
iDay
];
el.scrollIntoView();
await click(el);
}
export function expandCalendarView(target) {
// Expends Calendar view and FC too
let tmpElement = target.querySelector(".fc");
do {
tmpElement = tmpElement.parentElement;
tmpElement.classList.add("h-100");
} while (!tmpElement.classList.contains("o_view_controller"));
}
export function findAllDaySlot(target, date) {
return target.querySelector(`.fc-daygrid-body .fc-day[data-date="${date}"]`);
}
export function findDateCell(target, date) {
return target.querySelector(`.fc-day[data-date="${date}"]`);
}
export function findEvent(target, eventId) {
return target.querySelector(`.o_event[data-event-id="${eventId}"]`);
}
export function findDateCol(target, date) {
return target.querySelector(`.fc-col-header-cell.fc-day[data-date="${date}"]`);
}
export function findTimeRow(target, time) {
return target.querySelector(`.fc-timegrid-slot[data-time="${time}"]`);
}
export async function triggerEventForCalendar(el, type, position = {}) {
const rect = el.getBoundingClientRect();
const x = position.x || rect.x + rect.width / 2;
const y = position.y || rect.y + rect.height / 2;
const attrs = {
which: 1,
clientX: x,
clientY: y,
};
await triggerEvent(el, null, type, attrs);
}
export async function clickAllDaySlot(target, date) {
const el = findAllDaySlot(target, date);
await scrollTo(el);
await triggerEventForCalendar(el, "mousedown");
await triggerEventForCalendar(el, "mouseup");
await nextTick();
}
export async function clickDate(target, date) {
const el = findDateCell(target, date);
await scrollTo(el);
await triggerEventForCalendar(el, "mousedown");
await triggerEventForCalendar(el, "mouseup");
await nextTick();
}
export async function clickEvent(target, eventId) {
const el = findEvent(target, eventId);
await scrollTo(el);
await click(el);
await nextTick();
}
export async function selectTimeRange(target, startDateTime, endDateTime) {
const [startDate, startTime] = startDateTime.split(" ");
const [endDate, endTime] = endDateTime.split(" ");
const startCol = findDateCol(target, startDate);
const endCol = findDateCol(target, endDate);
const startRow = findTimeRow(target, startTime);
const endRow = findTimeRow(target, endTime);
await scrollTo(startRow);
const startColRect = startCol.getBoundingClientRect();
const startRowRect = startRow.getBoundingClientRect();
await triggerEventForCalendar(startRow, "mousedown", {
x: startColRect.x + startColRect.width / 2,
y: startRowRect.y + 2,
});
await scrollTo(endRow, false);
const endColRect = endCol.getBoundingClientRect();
const endRowRect = endRow.getBoundingClientRect();
await triggerEventForCalendar(endRow, "mousemove", {
x: endColRect.x + endColRect.width / 2,
y: endRowRect.y - 2,
});
await triggerEventForCalendar(endRow, "mouseup", {
x: endColRect.x + endColRect.width / 2,
y: endRowRect.y - 2,
});
await nextTick();
}
export async function selectDateRange(target, startDate, endDate) {
const start = findDateCell(target, startDate);
const end = findDateCell(target, endDate);
await scrollTo(start);
await triggerEventForCalendar(start, "mousedown");
await scrollTo(end);
await triggerEventForCalendar(end, "mousemove");
await triggerEventForCalendar(end, "mouseup");
await nextTick();
}
export async function selectAllDayRange(target, startDate, endDate) {
const start = findAllDaySlot(target, startDate);
const end = findAllDaySlot(target, endDate);
await scrollTo(start);
await triggerEventForCalendar(start, "mousedown");
await scrollTo(end);
await triggerEventForCalendar(end, "mousemove");
await triggerEventForCalendar(end, "mouseup");
await nextTick();
}
export async function moveEventToDate(target, eventId, date, options = {}) {
const event = findEvent(target, eventId);
const cell = findDateCell(target, date);
await scrollTo(event);
await triggerEventForCalendar(event, "mousedown");
await scrollTo(cell);
await triggerEventForCalendar(cell, "mousemove");
if (!options.disableDrop) {
await triggerEventForCalendar(cell, "mouseup");
}
await nextTick();
}
export async function moveEventToTime(target, eventId, dateTime) {
const event = findEvent(target, eventId);
const [date, time] = dateTime.split(" ");
const col = findDateCol(target, date);
const row = findTimeRow(target, time);
// Find event position
await scrollTo(event);
const eventRect = event.getBoundingClientRect();
const eventPos = {
x: eventRect.x + eventRect.width / 2,
y: eventRect.y,
};
await triggerEventForCalendar(event, "mousedown", eventPos);
// Find target position
await scrollTo(row, false);
const colRect = col.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
const toPos = {
x: colRect.x + colRect.width / 2,
y: rowRect.y - 1,
};
await triggerEventForCalendar(row, "mousemove", toPos);
await triggerEventForCalendar(row, "mouseup", toPos);
await nextTick();
}
export async function moveEventToAllDaySlot(target, eventId, date) {
const event = findEvent(target, eventId);
const slot = findAllDaySlot(target, date);
// Find event position
await scrollTo(event);
const eventRect = event.getBoundingClientRect();
const eventPos = {
x: eventRect.x + eventRect.width / 2,
y: eventRect.y,
};
await triggerEventForCalendar(event, "mousedown", eventPos);
// Find target position
await scrollTo(slot);
const slotRect = slot.getBoundingClientRect();
const toPos = {
x: slotRect.x + slotRect.width / 2,
y: slotRect.y - 1,
};
await triggerEventForCalendar(slot, "mousemove", toPos);
await triggerEventForCalendar(slot, "mouseup", toPos);
await nextTick();
}
export async function resizeEventToTime(target, eventId, dateTime) {
const event = findEvent(target, eventId);
const [date, time] = dateTime.split(" ");
const col = findDateCol(target, date);
const row = findTimeRow(target, time);
// Find event position
await scrollTo(event);
await triggerEventForCalendar(event, "mouseover");
// Find event resizer
const resizer = event.querySelector(".fc-event-resizer-end");
resizer.style.display = "block";
resizer.style.width = "100%";
resizer.style.height = "1em";
resizer.style.bottom = "0";
const resizerRect = resizer.getBoundingClientRect();
const resizerPos = {
x: resizerRect.x + resizerRect.width / 2,
y: resizerRect.y + resizerRect.height / 2,
};
await triggerEventForCalendar(resizer, "mousedown", resizerPos);
// Find target position
await scrollTo(row, false);
const colRect = col.getBoundingClientRect();
const rowRect = row.getBoundingClientRect();
const toPos = {
x: colRect.x + colRect.width / 2,
y: rowRect.y - 1,
};
await triggerEventForCalendar(row, "mousemove", toPos);
await triggerEventForCalendar(row, "mouseup", toPos);
await nextTick();
}
export async function changeScale(target, scale) {
await click(target, `.o_view_scale_selector .scale_button_selection`);
await click(target, `.o-dropdown--menu .o_scale_button_${scale}`);
await nextTick();
}
export async function navigate(target, direction) {
await click(target, `.o_calendar_navigation_buttons .o_calendar_button_${direction}`);
}
export function findFilterPanelSection(target, sectionName) {
return target.querySelector(`.o_calendar_filter[data-name="${sectionName}"]`);
}
export function findFilterPanelFilter(target, sectionName, filterValue) {
return findFilterPanelSection(target, sectionName).querySelector(
`.o_calendar_filter_item[data-value="${filterValue}"]`
);
}
export function findFilterPanelSectionFilter(target, sectionName) {
return findFilterPanelSection(target, sectionName).querySelector(
`.o_calendar_filter_items_checkall`
);
}
export async function toggleFilter(target, sectionName, filterValue) {
const el = findFilterPanelFilter(target, sectionName, filterValue).querySelector(`input`);
await scrollTo(el);
await click(el);
}
export async function toggleSectionFilter(target, sectionName) {
const el = findFilterPanelSectionFilter(target, sectionName).querySelector(`input`);
await scrollTo(el);
await click(el);
}

View file

@ -1,109 +0,0 @@
/* global Benchmark */
odoo.define('web.form_benchmarks', function (require) {
"use strict";
const FormView = require('web.FormView');
const testUtils = require('web.test_utils');
const { createView } = testUtils;
QUnit.module('Form View', {
beforeEach: function () {
this.data = {
foo: {
fields: {
foo: {string: "Foo", type: "char"},
many2many: { string: "bar", type: "many2many", relation: 'bar'},
},
records: [
{ id: 1, foo: "bar", many2many: []},
],
onchanges: {}
},
bar: {
fields: {
char: {string: "char", type: "char"},
many2many: { string: "pokemon", type: "many2many", relation: 'pokemon'},
},
records: [],
onchanges: {}
},
pokemon: {
fields: {
name: {string: "Name", type: "char"},
},
records: [],
onchanges: {}
},
};
this.arch = null;
this.run = function (assert, viewParams, cb) {
const data = this.data;
const arch = this.arch;
return new Promise(resolve => {
new Benchmark.Suite({})
.add('form', {
defer: true,
fn: async (deferred) => {
const form = await createView(Object.assign({
View: FormView,
model: 'foo',
data,
arch,
}, viewParams));
if (cb) {
await cb(form);
}
form.destroy();
deferred.resolve();
},
})
.on('cycle', event => {
assert.ok(true, String(event.target));
})
.on('complete', resolve)
.run({ async: true });
});
};
}
}, function () {
QUnit.test('x2many with 250 rows, 2 fields (with many2many_tags, and modifiers), onchanges, and edition', function (assert) {
assert.expect(1);
this.data.foo.onchanges.many2many = function (obj) {
obj.many2many = [5].concat(obj.many2many);
};
for (let i = 2; i < 500; i++) {
this.data.bar.records.push({
id: i,
char: "automated data",
});
this.data.foo.records[0].many2many.push(i);
}
this.arch = `
<form>
<field name="many2many">
<tree editable="top" limit="250">
<field name="char"/>
<field name="many2many" widget="many2many_tags" attrs="{'readonly': [('char', '==', 'toto')]}"/>
</tree>
</field>
</form>`;
return this.run(assert, { res_id: 1 }, async form => {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_data_cell:first'));
await testUtils.fields.editInput(form.$('input:first'), "tralala");
});
});
QUnit.test('form view with 100 fields, half of them being invisible', function (assert) {
assert.expect(1);
this.arch = `
<form>
${[...Array(100)].map((_, i) => '<field name="foo"' + (i % 2 ? ' invisible="1"' : '') + '/>').join('')}
</form>`;
return this.run(assert);
});
});
});

View file

@ -0,0 +1,44 @@
/** @odoo-module alias=@web/../tests/views/graph_view_tests default=false */
import { click, findChildren, triggerEvent } from "@web/../tests/helpers/utils";
import { ensureArray } from "@web/core/utils/arrays";
// TODO: remove when dependant modules are converted
export function checkLabels(assert, graph, expectedLabels) {
const labels = getGraphRenderer(graph).chart.data.labels.map((l) => l.toString());
assert.deepEqual(labels, expectedLabels);
}
export function checkLegend(assert, graph, expectedLegendLabels) {
expectedLegendLabels = ensureArray(expectedLegendLabels);
const { chart } = getGraphRenderer(graph);
const actualLegendLabels = chart.config.options.plugins.legend.labels
.generateLabels(chart)
.map((o) => o.text);
assert.deepEqual(actualLegendLabels, expectedLegendLabels);
}
export function clickOnDataset(graph) {
const { chart } = getGraphRenderer(graph);
const meta = chart.getDatasetMeta(0);
const rectangle = chart.canvas.getBoundingClientRect();
const point = meta.data[0].getCenterPoint();
return triggerEvent(chart.canvas, null, "click", {
pageX: rectangle.left + point.x,
pageY: rectangle.top + point.y,
});
}
export function getGraphRenderer(graph) {
for (const { component } of Object.values(findChildren(graph).children)) {
if (component.chart) {
return component;
}
}
return null;
}
export function selectMode(el, mode) {
return click(el, `.o_graph_button[data-mode="${mode}"`);
}

View file

@ -0,0 +1,128 @@
/** @odoo-module alias=@web/../tests/views/helpers default=false */
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import { getFixture, mount, nextTick } from "@web/../tests/helpers/utils";
import { createDebugContext } from "@web/core/debug/debug_context";
import { Dialog } from "@web/core/dialog/dialog";
import { MainComponentsContainer } from "@web/core/main_components_container";
import { registry } from "@web/core/registry";
import { View, getDefaultConfig } from "@web/views/view";
import {
makeFakeLocalizationService,
patchUserWithCleanup,
} from "../helpers/mock_services";
import {
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
} from "../search/helpers";
import { Component, useSubEnv, xml } from "@odoo/owl";
const serviceRegistry = registry.category("services");
const rootDialogTemplate = xml`<Dialog><View t-props="props.viewProps"/></Dialog>`;
/**
* @typedef {{
* serverData: Object,
* mockRPC?: Function,
* type: string,
* resModel: string,
* [prop:string]: any
* }} MakeViewParams
*/
/**
* @template {Component} T
* @param {MakeViewParams} params
* @param {boolean} [inDialog=false]
* @returns {Promise<T>}
*/
async function _makeView(params, inDialog = false) {
const props = { resId: false, ...params };
const serverData = props.serverData;
const mockRPC = props.mockRPC;
const config = {
...getDefaultConfig(),
...props.config,
};
delete props.serverData;
delete props.mockRPC;
delete props.config;
if (props.arch) {
serverData.views = serverData.views || {};
props.viewId = params.viewId || 100000001; // hopefully will not conflict with an id already in views
serverData.views[`${props.resModel},${props.viewId},${props.type}`] = props.arch;
delete props.arch;
props.searchViewId = 100000002; // hopefully will not conflict with an id already in views
const searchViewArch = props.searchViewArch || "<search/>";
serverData.views[`${props.resModel},${props.searchViewId},search`] = searchViewArch;
delete props.searchViewArch;
}
const env = await makeTestEnv({ serverData, mockRPC });
Object.assign(env, createDebugContext(env)); // This is needed if the views are in debug mode
const target = getFixture();
const viewEnv = Object.assign(Object.create(env), { config });
await mount(MainComponentsContainer, target, { env });
let viewNode;
if (inDialog) {
let root;
class RootDialog extends Component {
static components = { Dialog, View };
static template = rootDialogTemplate;
static props = ["*"];
setup() {
root = this;
useSubEnv(viewEnv);
}
}
env.services.dialog.add(RootDialog, { viewProps: props });
await nextTick();
const rootNode = root.__owl__;
const dialogNode = Object.values(rootNode.children)[0];
viewNode = Object.values(dialogNode.children)[0];
} else {
const view = await mount(View, target, { env: viewEnv, props });
await nextTick();
viewNode = view.__owl__;
}
const withSearchNode = Object.values(viewNode.children)[0];
const concreteViewNode = Object.values(withSearchNode.children)[0];
const concreteView = concreteViewNode.component;
return concreteView;
}
/**
* @param {MakeViewParams} params
*/
export function makeView(params) {
return _makeView(params);
}
/**
* @param {MakeViewParams} params
*/
export function makeViewInDialog(params) {
return _makeView(params, true);
}
export function setupViewRegistries() {
setupControlPanelFavoriteMenuRegistry();
setupControlPanelServiceRegistry();
patchUserWithCleanup({
hasGroup: async (group) => {
return [
"base.group_allow_export",
"base.group_user",
].includes(group);
},
isInternalUser: true,
});
serviceRegistry.add("localization", makeFakeLocalizationService());
}

View file

@ -0,0 +1,111 @@
/** @odoo-module alias=@web/../tests/views/kanban/helpers default=false */
import { makeFakeDialogService } from "@web/../tests/helpers/mock_services";
import { click, editInput, getDropdownMenu, nextTick } from "@web/../tests/helpers/utils";
import { registry } from "@web/core/registry";
export function patchDialog(addDialog) {
registry.category("services").add("dialog", makeFakeDialogService(addDialog), { force: true });
}
// Kanban
// WOWL remove this helper and use the control panel instead
export async function reload(kanban, params = {}) {
kanban.env.searchModel.reload(params);
kanban.env.searchModel.search();
await nextTick();
}
export function getCard(target, cardIndex = 0) {
return target.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")[cardIndex];
}
export function getColumn(target, groupIndex = 0, ignoreFolded = false) {
let selector = ".o_kanban_group";
if (ignoreFolded) {
selector += ":not(.o_column_folded)";
}
return target.querySelectorAll(selector)[groupIndex];
}
export function getColumnDropdownMenu(target, groupIndex = 0, ignoreFolded = false) {
let selector = ".o_kanban_group";
if (ignoreFolded) {
selector += ":not(.o_column_folded)";
}
const column = target.querySelectorAll(selector)[groupIndex];
return getDropdownMenu(target, column);
}
export function getCardTexts(target, groupIndex) {
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
return [...root.querySelectorAll(".o_kanban_record:not(.o_kanban_ghost)")]
.map((card) => card.innerText.trim())
.filter(Boolean);
}
export function getCounters(target) {
return [...target.querySelectorAll(".o_animated_number")].map((counter) => counter.innerText);
}
export function getProgressBars(target, columnIndex) {
const column = getColumn(target, columnIndex);
return [...column.querySelectorAll(".o_column_progress .progress-bar")];
}
export function getTooltips(target, groupIndex) {
const root = groupIndex >= 0 ? getColumn(target, groupIndex) : target;
return [...root.querySelectorAll(".o_column_progress .progress-bar")]
.map((card) => card.dataset.tooltip)
.filter(Boolean);
}
// Record
export async function createRecord(target) {
await click(target, ".o_control_panel_main_buttons button.o-kanban-button-new");
await nextTick();
}
export async function quickCreateRecord(target, groupIndex) {
await click(getColumn(target, groupIndex), ".o_kanban_quick_add");
await nextTick();
}
export async function editQuickCreateInput(target, field, value) {
await editInput(target, `.o_kanban_quick_create .o_field_widget[name=${field}] input`, value);
}
export async function validateRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_add");
}
export async function editRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_edit");
}
export async function discardRecord(target) {
await click(target, ".o_kanban_quick_create .o_kanban_cancel");
}
export async function toggleRecordDropdown(target, recordIndex) {
const group = target.querySelectorAll(`.o_kanban_record`)[recordIndex];
await click(group, ".o_dropdown_kanban .dropdown-toggle");
}
// Column
export async function createColumn(target) {
await click(target, ".o_column_quick_create > .o_quick_create_folded");
}
export async function editColumnName(target, value) {
await editInput(target, ".o_column_quick_create input", value);
}
export async function validateColumn(target) {
await click(target, ".o_column_quick_create .o_kanban_add");
}
export async function loadMore(target, columnIndex) {
await click(getColumn(target, columnIndex), ".o_kanban_load_more button");
}

View file

@ -1,93 +0,0 @@
/* global Benchmark */
odoo.define('web.kanban_benchmarks', function (require) {
"use strict";
const KanbanView = require('web.KanbanView');
const { createView } = require('web.test_utils');
QUnit.module('Kanban View', {
beforeEach: function () {
this.data = {
foo: {
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"},
},
records: [
{ id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13},
]
},
};
this.arch = null;
this.run = function (assert) {
const data = this.data;
const arch = this.arch;
return new Promise(resolve => {
new Benchmark.Suite({})
.add('kanban', {
defer: true,
fn: async (deferred) => {
const kanban = await createView({
View: KanbanView,
model: 'foo',
data,
arch,
});
kanban.destroy();
deferred.resolve();
},
})
.on('cycle', event => {
assert.ok(true, String(event.target));
})
.on('complete', resolve)
.run({ async: true });
});
};
}
}, function () {
QUnit.test('simple kanban view with 2 records', function (assert) {
assert.expect(1);
this.arch = `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<t t-esc="record.foo.value"/>
<field name="foo"/>
</div>
</t>
</templates>
</kanban>`;
return this.run(assert);
});
QUnit.test('simple kanban view with 200 records', function (assert) {
assert.expect(1);
for (let i = 2; i < 200; i++) {
this.data.foo.records.push({
id: i,
foo: `automated data ${i}`,
});
}
this.arch = `
<kanban>
<templates>
<t t-name="kanban-box">
<div>
<t t-esc="record.foo.value"/>
<field name="foo"/>
</div>
</t>
</templates>
</kanban>`;
return this.run(assert);
});
});
});

View file

@ -1,380 +0,0 @@
odoo.define('web.kanban_model_tests', function (require) {
"use strict";
var KanbanModel = require('web.KanbanModel');
var testUtils = require('web.test_utils');
var createModel = testUtils.createModel;
QUnit.module('LegacyViews', {
beforeEach: function () {
this.data = {
partner: {
fields: {
active: {string: "Active", type: "boolean", default: true},
display_name: {string: "STRING", type: 'char'},
foo: {string: "Foo", type: 'char'},
bar: {string: "Bar", type: 'integer'},
qux: {string: "Qux", type: 'many2one', relation: 'partner'},
product_id: {string: "Favorite product", type: 'many2one', relation: 'product'},
product_ids: {string: "Favorite products", type: 'one2many', relation: 'product'},
category: {string: "Category M2M", type: 'many2many', relation: 'partner_type'},
},
records: [
{id: 1, foo: 'blip', bar: 1, product_id: 37, category: [12], display_name: "first partner"},
{id: 2, foo: 'gnap', bar: 2, product_id: 41, display_name: "second partner"},
],
onchanges: {},
},
product: {
fields: {
name: {string: "Product Name", type: "char"}
},
records: [
{id: 37, display_name: "xphone"},
{id: 41, display_name: "xpad"}
]
},
partner_type: {
fields: {
display_name: {string: "Partner Type", type: "char"}
},
records: [
{id: 12, display_name: "gold"},
{id: 14, display_name: "silver"},
{id: 15, display_name: "bronze"}
]
},
};
// add related fields to category.
this.data.partner.fields.category.relatedFields =
$.extend(true, {}, this.data.partner_type.fields);
this.params = {
fields: this.data.partner.fields,
limit: 40,
modelName: 'partner',
openGroupByDefault: true,
viewType: 'kanban',
};
},
}, function () {
QUnit.module('KanbanModel (legacy)');
QUnit.test('load grouped + add a new group', async function (assert) {
var done = assert.async();
assert.expect(22);
var calledRoutes = {};
var model = await createModel({
Model: KanbanModel,
data: this.data,
mockRPC: function (route) {
if (!(route in calledRoutes)) {
calledRoutes[route] = 1;
} else {
calledRoutes[route]++;
}
return this._super.apply(this, arguments);
},
});
var params = _.extend(this.params, {
groupedBy: ['product_id'],
fieldNames: ['foo'],
});
model.load(params).then(async function (resultID) {
// various checks on the load result
var state = model.get(resultID);
assert.ok(_.isEqual(state.groupedBy, ['product_id']), 'should be grouped by "product_id"');
assert.strictEqual(state.data.length, 2, 'should have found 2 groups');
assert.strictEqual(state.count, 2, 'both groups contain one record');
var xphoneGroup = _.findWhere(state.data, {res_id: 37});
assert.strictEqual(xphoneGroup.model, 'partner', 'group should have correct model');
assert.ok(xphoneGroup, 'should have a group for res_id 37');
assert.ok(xphoneGroup.isOpen, '"xphone" group should be open');
assert.strictEqual(xphoneGroup.value, 'xphone', 'group 37 should be "xphone"');
assert.strictEqual(xphoneGroup.count, 1, '"xphone" group should have one record');
assert.strictEqual(xphoneGroup.data.length, 1, 'should have fetched the records in the group');
assert.ok(_.isEqual(xphoneGroup.domain[0], ['product_id', '=', 37]),
'domain should be correct');
assert.strictEqual(xphoneGroup.limit, 40, 'limit in a group should be 40');
// add a new group
await model.createGroup('xpod', resultID);
state = model.get(resultID);
assert.strictEqual(state.data.length, 3, 'should now have 3 groups');
assert.strictEqual(state.count, 2, 'there are still 2 records');
var xpodGroup = _.findWhere(state.data, {value: 'xpod'});
assert.strictEqual(xpodGroup.model, 'partner', 'new group should have correct model');
assert.ok(xpodGroup, 'should have an "xpod" group');
assert.ok(xpodGroup.isOpen, 'new group should be open');
assert.strictEqual(xpodGroup.count, 0, 'new group should contain no record');
assert.ok(_.isEqual(xpodGroup.domain[0], ['product_id', '=', xpodGroup.res_id]),
'new group should have correct domain');
// check the rpcs done
assert.strictEqual(Object.keys(calledRoutes).length, 3, 'three different routes have been called');
var nbReadGroups = calledRoutes['/web/dataset/call_kw/partner/web_read_group'];
var nbSearchRead = calledRoutes['/web/dataset/search_read'];
var nbNameCreate = calledRoutes['/web/dataset/call_kw/product/name_create'];
assert.strictEqual(nbReadGroups, 1, 'should have done 1 read_group');
assert.strictEqual(nbSearchRead, 2, 'should have done 2 search_read');
assert.strictEqual(nbNameCreate, 1, 'should have done 1 name_create');
model.destroy();
done();
});
});
QUnit.test('archive/restore a column', async function (assert) {
var done = assert.async();
assert.expect(4);
var model = await createModel({
Model: KanbanModel,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/partner/action_archive') {
this.data.partner.records[0].active = false;
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
});
var params = _.extend(this.params, {
groupedBy: ['product_id'],
fieldNames: ['foo'],
});
model.load(params).then(async function (resultID) {
var state = model.get(resultID);
var xphoneGroup = _.findWhere(state.data, {res_id: 37});
var xpadGroup = _.findWhere(state.data, {res_id: 41});
assert.strictEqual(xphoneGroup.count, 1, 'xphone group has one record');
assert.strictEqual(xpadGroup.count, 1, 'xpad group has one record');
// archive the column 'xphone'
var recordIDs = xphoneGroup.data.map(record => record.res_id);
await model.actionArchive(recordIDs, xphoneGroup.id);
state = model.get(resultID);
xphoneGroup = _.findWhere(state.data, {res_id: 37});
assert.strictEqual(xphoneGroup.count, 0, 'xphone group has no record anymore');
xpadGroup = _.findWhere(state.data, {res_id: 41});
assert.strictEqual(xpadGroup.count, 1, 'xpad group still has one record');
model.destroy();
done();
});
});
QUnit.test('kanban model does not allow nested groups', async function (assert) {
var done = assert.async();
assert.expect(2);
var model = await createModel({
Model: KanbanModel,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'web_read_group') {
assert.deepEqual(args.kwargs.groupby, ['product_id'],
"the second level of groupBy should have been removed");
}
return this._super.apply(this, arguments);
},
});
var params = _.extend(this.params, {
groupedBy: ['product_id', 'qux'],
fieldNames: ['foo'],
});
model.load(params).then(function (resultID) {
var state = model.get(resultID);
assert.deepEqual(state.groupedBy, ['product_id'],
"the second level of groupBy should have been removed");
model.destroy();
done();
});
});
QUnit.test('resequence columns and records', async function (assert) {
var done = assert.async();
assert.expect(8);
this.data.product.fields.sequence = {string: "Sequence", type: "integer"};
this.data.partner.fields.sequence = {string: "Sequence", type: "integer"};
this.data.partner.records.push({id: 3, foo: 'aaa', product_id: 37});
var nbReseq = 0;
var model = await createModel({
Model: KanbanModel,
data: this.data,
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
nbReseq++;
if (nbReseq === 1) { // resequencing columns
assert.deepEqual(args.ids, [41, 37],
"ids should be correct");
assert.strictEqual(args.model, 'product',
"model should be correct");
} else if (nbReseq === 2) { // resequencing records
assert.deepEqual(args.ids, [3, 1],
"ids should be correct");
assert.strictEqual(args.model, 'partner',
"model should be correct");
}
}
return this._super.apply(this, arguments);
},
});
var params = _.extend(this.params, {
groupedBy: ['product_id'],
fieldNames: ['foo'],
});
model.load(params)
.then(function (stateID) {
var state = model.get(stateID);
assert.strictEqual(state.data[0].res_id, 37,
"first group should be res_id 37");
// resequence columns
return model.resequence('product', [41, 37], stateID);
})
.then(function (stateID) {
var state = model.get(stateID);
assert.strictEqual(state.data[0].res_id, 41,
"first group should be res_id 41 after resequencing");
assert.strictEqual(state.data[1].data[0].res_id, 1,
"first record should be res_id 1");
// resequence records
return model.resequence('partner', [3, 1], state.data[1].id);
})
.then(function (groupID) {
var group = model.get(groupID);
assert.strictEqual(group.data[0].res_id, 3,
"first record should be res_id 3 after resequencing");
model.destroy();
done();
});
});
QUnit.test('add record to group', async function (assert) {
var done = assert.async();
assert.expect(8);
var self = this;
var model = await createModel({
Model: KanbanModel,
data: this.data,
});
var params = _.extend(this.params, {
groupedBy: ['product_id'],
fieldNames: ['foo'],
});
model.load(params).then(function (stateID) {
self.data.partner.records.push({id: 3, foo: 'new record', product_id: 37});
var state = model.get(stateID);
assert.deepEqual(state.res_ids, [1, 2],
"state should have the correct res_ids");
assert.strictEqual(state.count, 2,
"state should have the correct count");
assert.strictEqual(state.data[0].count, 1,
"first group should contain one record");
return model.addRecordToGroup(state.data[0].id, 3).then(function () {
var state = model.get(stateID);
assert.deepEqual(state.res_ids, [3, 1, 2],
"state should have the correct res_ids");
assert.strictEqual(state.count, 3,
"state should have the correct count");
assert.deepEqual(state.data[0].res_ids, [3, 1],
"new record's id should have been added to the res_ids");
assert.strictEqual(state.data[0].count, 2,
"first group should now contain two records");
assert.strictEqual(state.data[0].data[0].data.foo, 'new record',
"new record should have been fetched");
});
}).then(function() {
model.destroy();
done();
})
});
QUnit.test('call get (raw: true) before loading x2many data', async function (assert) {
// Sometimes, get can be called on a datapoint that is currently being
// reloaded, and thus in a partially updated state (e.g. in a kanban
// view, the user interacts with the searchview, and before the view is
// fully reloaded, it clicks on CREATE). Ideally, this shouldn't happen,
// but with the sync API of get, we can't change that easily. So at most,
// we can ensure that it doesn't crash. Moreover, sensitive functions
// requesting the state for more precise information that, e.g., the
// count, can do that in the mutex to ensure that the state isn't
// currently being reloaded.
// In this test, we have a grouped kanban view with a one2many, whose
// relational data is loaded in batch, once for all groups. We call get
// when the search_read for the first group has returned, but not the
// second (and thus, the read of the one2many hasn't started yet).
// Note: this test can be removed as soon as search_reads are performed
// alongside read_group.
var done = assert.async();
assert.expect(2);
this.data.partner.records[1].product_ids = [37, 41];
this.params.fieldsInfo = {
kanban: {
product_ids: {
fieldsInfo: {
default: { display_name: {}, color: {} },
},
relatedFields: this.data.product.fields,
viewType: 'default',
},
},
};
this.params.viewType = 'kanban';
this.params.groupedBy = ['foo'];
var block;
var def = testUtils.makeTestPromise();
var model = await createModel({
Model: KanbanModel,
data: this.data,
mockRPC: function (route) {
var result = this._super.apply(this, arguments);
if (route === '/web/dataset/search_read' && block) {
block = false;
return Promise.all([def]).then(_.constant(result));
}
return result;
},
});
model.load(this.params).then(function (handle) {
block = true;
model.reload(handle, {});
var state = model.get(handle, {raw: true});
assert.strictEqual(state.count, 2);
def.resolve();
state = model.get(handle, {raw: true});
assert.strictEqual(state.count, 2);
}).then(function() {
model.destroy();
done();
});
});
});
});

View file

@ -1,114 +0,0 @@
/* global Benchmark */
odoo.define('web.list_benchmarks', function (require) {
"use strict";
const ListView = require('web.ListView');
const { createView } = require('web.test_utils');
QUnit.module('List View', {
beforeEach: function () {
this.data = {
foo: {
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"},
},
records: [
{id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4},
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13},
]
},
};
this.arch = null;
this.run = function (assert, cb) {
const data = this.data;
const arch = this.arch;
return new Promise(resolve => {
new Benchmark.Suite({})
.add('list', {
defer: true,
fn: async (deferred) => {
const list = await createView({
View: ListView,
model: 'foo',
data,
arch,
});
if (cb) {
cb(list);
}
list.destroy();
deferred.resolve();
},
})
.on('cycle', event => {
assert.ok(true, String(event.target));
})
.on('complete', resolve)
.run({ async: true });
});
};
}
}, function () {
QUnit.test('simple readonly list with 2 rows and 2 fields', function (assert) {
assert.expect(1);
this.arch = '<tree><field name="foo"/><field name="int_field"/></tree>';
return this.run(assert);
});
QUnit.test('simple readonly list with 200 rows and 2 fields', function (assert) {
assert.expect(1);
for (let i = 2; i < 200; i++) {
this.data.foo.records.push({
id: i,
foo: "automated data",
int_field: 10 * i,
});
}
this.arch = '<tree><field name="foo"/><field name="int_field"/></tree>';
return this.run(assert);
});
QUnit.test('simple readonly list with 200 rows and 2 fields (with widgets)', function (assert) {
assert.expect(1);
for (let i = 2; i < 200; i++) {
this.data.foo.records.push({
id: i,
foo: "automated data",
int_field: 10 * i,
});
}
this.arch = '<tree><field name="foo" widget="char"/><field name="int_field" widget="integer"/></tree>';
return this.run(assert);
});
QUnit.test('editable list with 200 rows 4 fields', function (assert) {
assert.expect(1);
for (let i = 2; i < 200; i++) {
this.data.foo.records.push({
id: i,
foo: "automated data",
int_field: 10 * i,
bar: i % 2 === 0,
});
}
this.arch = `
<tree editable="bottom">
<field name="foo" attrs="{'readonly': [['bar', '=', True]]}"/>
<field name="int_field"/>
<field name="bar"/>
<field name="qux"/>
</tree>`;
return this.run(assert, list => {
list.$('.o_list_button_add').click();
list.$('.o_list_button_discard').click();
});
});
});
});

View file

@ -1,75 +0,0 @@
odoo.define('web.qweb_view_tests', function (require) {
"use strict";
const utils = require('web.test_utils');
const { createWebClient, doAction } = require('@web/../tests/webclient/helpers');
const { getFixture } = require("@web/../tests/helpers/utils");
QUnit.module("Views", {
}, function () {
QUnit.module("QWeb");
QUnit.test("basic", async function (assert) {
assert.expect(14);
const serverData = {
models: {
test: {
fields: {},
records: [],
}
},
views: {
'test,5,qweb': '<div id="xxx"><t t-esc="ok"/></div>',
'test,false,search': '<search/>'
},
};
const mockRPC = (route, args) => {
if (/^\/web\/dataset\/call_kw/.test(route)) {
switch (_.str.sprintf('%(model)s.%(method)s', args)) {
case 'test.qweb_render_view':
assert.step('fetch');
assert.equal(args.kwargs.view_id, 5);
return Promise.resolve(
'<div>foo' +
'<div data-model="test" data-method="wheee" data-id="42" data-other="5">' +
'<a type="toggle" class="fa fa-caret-right">Unfold</a>' +
'</div>' +
'</div>'
);
case 'test.wheee':
assert.step('unfold');
assert.deepEqual(args.args, [42]);
assert.deepEqual(args.kwargs, { other: 5, context: {} });
return Promise.resolve('<div id="sub">ok</div>');
}
}
};
const target = getFixture();
const webClient = await createWebClient({ serverData, mockRPC});
let resolved = false;
const doActionProm = doAction(webClient, {
type: 'ir.actions.act_window',
views: [[false, 'qweb']],
res_model: 'test',
}).then(function () { resolved = true; });
assert.ok(!resolved, "Action cannot be resolved synchronously");
await doActionProm;
assert.ok(resolved, "Action is resolved asynchronously");
const content = target.querySelector('.o_content');
assert.ok(/^\s*foo/.test(content.textContent));
await utils.dom.click(content.querySelector('[type=toggle]'));
assert.equal(content.querySelector('div#sub').textContent, 'ok', 'should have unfolded the sub-item');
await utils.dom.click(content.querySelector('[type=toggle]'));
assert.containsNone(content, "div#sub");
await utils.dom.click(content.querySelector('[type=toggle]'));
assert.verifySteps(['fetch', 'unfold', 'unfold']);
});
});
});

View file

@ -1,486 +0,0 @@
odoo.define('web.sample_server_tests', function (require) {
"use strict";
const SampleServer = require('web.SampleServer');
const session = require('web.session');
const { mock } = require('web.test_utils');
const {
MAIN_RECORDSET_SIZE, SEARCH_READ_LIMIT, // Limits
SAMPLE_COUNTRIES, SAMPLE_PEOPLE, SAMPLE_TEXTS, // Text values
MAX_COLOR_INT, MAX_FLOAT, MAX_INTEGER, MAX_MONETARY, // Number values
SUB_RECORDSET_SIZE, // Records sise
} = SampleServer;
/**
* Transforms random results into deterministic ones.
*/
class DeterministicSampleServer extends SampleServer {
constructor() {
super(...arguments);
this.arrayElCpt = 0;
this.boolCpt = 0;
this.subRecordIdCpt = 0;
}
_getRandomArrayEl(array) {
return array[this.arrayElCpt++ % array.length];
}
_getRandomBool() {
return Boolean(this.boolCpt++ % 2);
}
_getRandomSubRecordId() {
return (this.subRecordIdCpt++ % SUB_RECORDSET_SIZE) + 1;
}
}
QUnit.module("Sample Server (legacy)", {
beforeEach() {
this.fields = {
'res.users': {
display_name: { string: "Name", type: 'char' },
name: { string: "Reference", type: 'char' },
email: { string: "Email", type: 'char' },
phone_number: { string: "Phone number", type: 'char' },
brol_machin_url_truc: { string: "URL", type: 'char' },
urlemailphone: { string: "Whatever", type: 'char' },
active: { string: "Active", type: 'boolean' },
is_alive: { string: "Is alive", type: 'boolean' },
description: { string: "Description", type: 'text' },
birthday: { string: "Birthday", type: 'date' },
arrival_date: { string: "Date of arrival", type: 'datetime' },
height: { string: "Height", type: 'float' },
color: { string: "Color", type: 'integer' },
age: { string: "Age", type: 'integer' },
salary: { string: "Salary", type: 'monetary' },
currency: { string: "Currency", type: 'many2one', relation: 'res.currency' },
manager_id: { string: "Manager", type: 'many2one', relation: 'res.users' },
cover_image_id: { string: "Cover Image", type: 'many2one', relation: 'ir.attachment' },
managed_ids: { string: "Managing", type: 'one2many', relation: 'res.users' },
tag_ids: { string: "Tags", type: 'many2many', relation: 'tag' },
type: { string: "Type", type: 'selection', selection: [
['client', "Client"], ['partner', "Partner"], ['employee', "Employee"]
] },
},
'res.country': {
display_name: { string: "Name", type: 'char' },
},
'hobbit': {
display_name: { string: "Name", type: 'char' },
profession: { string: "Profession", type: 'selection', selection: [
['gardener', "Gardener"], ['brewer', "Brewer"], ['adventurer', "Adventurer"]
] },
age: { string: "Age", type: 'integer' },
},
'ir.attachment': {
display_name: { string: "Name", type: 'char' },
}
};
},
}, function () {
QUnit.module("Basic behaviour");
QUnit.test("Sample data: people type + all field names", async function (assert) {
assert.expect(26);
mock.patch(session, {
company_currency_id: 4,
});
const allFieldNames = Object.keys(this.fields['res.users']);
const server = new DeterministicSampleServer('res.users', this.fields['res.users']);
const { records } = await server.mockRpc({
method: '/web/dataset/search_read',
model: 'res.users',
fields: allFieldNames,
});
const rec = records[0];
function assertFormat(fieldName, regex) {
if (regex instanceof RegExp) {
assert.ok(
regex.test(rec[fieldName].toString()),
`Field "${fieldName}" has the correct format`
);
} else {
assert.strictEqual(
typeof rec[fieldName], regex,
`Field "${fieldName}" is of type ${regex}`
);
}
}
function assertBetween(fieldName, min, max) {
const val = rec[fieldName];
assert.ok(
min <= val && val < max && parseInt(val, 10) === val,
`Field "${fieldName}" should be an integer between ${min} and ${max}: ${val}`
);
}
// Basic fields
assert.ok(SAMPLE_PEOPLE.includes(rec.display_name));
assert.ok(SAMPLE_PEOPLE.includes(rec.name));
assert.strictEqual(rec.email,
`${rec.display_name.replace(/ /, ".").toLowerCase()}@sample.demo`
);
assertFormat('phone_number', /\+1 555 754 000\d/);
assertFormat('brol_machin_url_truc', /http:\/\/sample\d\.com/);
assert.strictEqual(rec.urlemailphone, false);
assert.strictEqual(rec.active, true);
assertFormat('is_alive', 'boolean');
assert.ok(SAMPLE_TEXTS.includes(rec.description));
assertFormat('birthday', /\d{4}-\d{2}-\d{2}/);
assertFormat('arrival_date', /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
assert.ok(rec.height >= 0 && rec.height <= MAX_FLOAT, "Field height should be between 0 and 100");
assertBetween('color', 0, MAX_COLOR_INT);
assertBetween('age', 0, MAX_INTEGER);
assertBetween('salary', 0, MAX_MONETARY);
// check float field have 2 decimal rounding
assert.strictEqual(rec.height, parseFloat(parseFloat(rec.height).toFixed(2)));
const selectionValues = this.fields['res.users'].type.selection.map(
(sel) => sel[0]
);
assert.ok(selectionValues.includes(rec.type));
// Relational fields
assert.strictEqual(rec.currency[0], 4);
// Currently we expect the currency name to be a latin string, which
// is not important; in most case we only need the ID. The following
// assertion can be removed if needed.
assert.ok(SAMPLE_TEXTS.includes(rec.currency[1]));
assert.strictEqual(typeof rec.manager_id[0], 'number');
assert.ok(SAMPLE_PEOPLE.includes(rec.manager_id[1]));
assert.strictEqual(rec.cover_image_id, false);
assert.strictEqual(rec.managed_ids.length, 2);
assert.ok(rec.managed_ids.every(
(id) => typeof id === 'number')
);
assert.strictEqual(rec.tag_ids.length, 2);
assert.ok(rec.tag_ids.every(
(id) => typeof id === 'number')
);
mock.unpatch(session);
});
QUnit.test("Sample data: country type", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('res.country', this.fields['res.country']);
const { records } = await server.mockRpc({
method: '/web/dataset/search_read',
model: 'res.country',
fields: ['display_name'],
});
assert.ok(SAMPLE_COUNTRIES.includes(records[0].display_name));
});
QUnit.test("Sample data: any type", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const { records } = await server.mockRpc({
method: '/web/dataset/search_read',
model: 'hobbit',
fields: ['display_name'],
});
assert.ok(SAMPLE_TEXTS.includes(records[0].display_name));
});
QUnit.module("RPC calls");
QUnit.test("Send 'search_read' RPC: valid field names", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: '/web/dataset/search_read',
model: 'hobbit',
fields: ['display_name'],
});
assert.deepEqual(
Object.keys(result.records[0]),
['id', 'display_name']
);
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
assert.ok(/\w+/.test(result.records[0].display_name),
"Display name has been mocked"
);
});
QUnit.test("Send 'search_read' RPC: invalid field names", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: '/web/dataset/search_read',
model: 'hobbit',
fields: ['name'],
});
assert.deepEqual(
Object.keys(result.records[0]),
['id', 'name']
);
assert.strictEqual(result.length, SEARCH_READ_LIMIT);
assert.strictEqual(result.records[0].name, false,
`Field "name" doesn't exist => returns false`
);
});
QUnit.test("Send 'web_read_group' RPC: no group", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
server.setExistingGroups([]);
const result = await server.mockRpc({
method: 'web_read_group',
model: 'hobbit',
groupBy: ['profession'],
});
assert.deepEqual(result, { groups: [], length: 0 });
});
QUnit.test("Send 'web_read_group' RPC: 2 groups", async function (assert) {
assert.expect(5);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const existingGroups = [
{ profession: 'gardener', profession_count: 0 },
{ profession: 'adventurer', profession_count: 0 },
];
server.setExistingGroups(existingGroups);
const result = await server.mockRpc({
method: 'web_read_group',
model: 'hobbit',
groupBy: ['profession'],
fields: [],
});
assert.strictEqual(result.length, 2);
assert.strictEqual(result.groups.length, 2);
assert.deepEqual(
result.groups.map((g) => g.profession),
["gardener", "adventurer"]
);
assert.strictEqual(
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
assert.ok(
result.groups.every((g) => g.profession_count === g.__data.length)
);
});
QUnit.test("Send 'web_read_group' RPC: all groups", async function (assert) {
assert.expect(5);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const existingGroups = [
{ profession: 'gardener', profession_count: 0 },
{ profession: 'brewer', profession_count: 0 },
{ profession: 'adventurer', profession_count: 0 },
];
server.setExistingGroups(existingGroups);
const result = await server.mockRpc({
method: 'web_read_group',
model: 'hobbit',
groupBy: ['profession'],
fields: [],
});
assert.strictEqual(result.length, 3);
assert.strictEqual(result.groups.length, 3);
assert.deepEqual(
result.groups.map((g) => g.profession),
["gardener", "brewer", "adventurer"]
);
assert.strictEqual(
result.groups.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE
);
assert.ok(
result.groups.every((g) => g.profession_count === g.__data.length)
);
});
QUnit.test("Send 'read_group' RPC: no group", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read_group',
model: 'hobbit',
fields: [],
groupBy: [],
});
assert.deepEqual(result, [{
__count: MAIN_RECORDSET_SIZE,
__domain: [],
}]);
});
QUnit.test("Send 'read_group' RPC: groupBy", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read_group',
model: 'hobbit',
fields: [],
groupBy: ['profession'],
});
assert.strictEqual(result.length, 3);
assert.deepEqual(
result.map((g) => g.profession),
["adventurer", "brewer", "gardener"]
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE,
);
});
QUnit.test("Send 'read_group' RPC: groupBy and field", async function (assert) {
assert.expect(4);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read_group',
model: 'hobbit',
fields: ['age'],
groupBy: ['profession'],
});
assert.strictEqual(result.length, 3);
assert.deepEqual(
result.map((g) => g.profession),
["adventurer", "brewer", "gardener"]
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.profession_count, 0),
MAIN_RECORDSET_SIZE,
);
assert.strictEqual(
result.reduce((acc, g) => acc + g.age, 0),
server.data.hobbit.records.reduce((acc, g) => acc + g.age, 0)
);
});
QUnit.test("Send 'read_group' RPC: multiple groupBys and lazy", async function (assert) {
assert.expect(2);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read_group',
model: 'hobbit',
fields: [],
groupBy: ['profession', 'age'],
});
assert.ok('profession' in result[0]);
assert.notOk('age' in result[0]);
});
QUnit.test("Send 'read_group' RPC: multiple groupBys and not lazy", async function (assert) {
assert.expect(2);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read_group',
model: 'hobbit',
fields: [],
groupBy: ['profession', 'age'],
lazy: false,
});
assert.ok('profession' in result[0]);
assert.ok('age' in result[0]);
});
QUnit.test("Send 'read' RPC: no id", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read',
model: 'hobbit',
args: [
[], ['display_name']
],
});
assert.deepEqual(result, []);
});
QUnit.test("Send 'read' RPC: one id", async function (assert) {
assert.expect(3);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const result = await server.mockRpc({
method: 'read',
model: 'hobbit',
args: [
[1], ['display_name']
],
});
assert.strictEqual(result.length, 1);
assert.ok(
/\w+/.test(result[0].display_name),
"Display name has been mocked"
);
assert.strictEqual(result[0].id, 1);
});
QUnit.test("Send 'read' RPC: more than all available ids", async function (assert) {
assert.expect(1);
const server = new DeterministicSampleServer('hobbit', this.fields.hobbit);
const amount = MAIN_RECORDSET_SIZE + 3;
const ids = new Array(amount).fill().map((_, i) => i + 1);
const result = await server.mockRpc({
method: 'read',
model: 'hobbit',
args: [
ids, ['display_name']
],
});
assert.strictEqual(result.length, MAIN_RECORDSET_SIZE);
});
// To be implemented if needed
// QUnit.test("Send 'read_progress_bar' RPC", async function (assert) { ... });
});
});

View file

@ -1,404 +0,0 @@
/** @odoo-module **/
import { getFixture, legacyExtraNextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
import {
getFacetTexts,
removeFacet,
saveFavorite,
setupControlPanelFavoriteMenuRegistry,
setupControlPanelServiceRegistry,
switchView,
toggleFavoriteMenu,
toggleMenu,
toggleMenuItem,
toggleSaveFavorite,
} from "@web/../tests/search/helpers";
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { ControlPanel } from "@web/search/control_panel/control_panel";
import { SearchModel } from "@web/search/search_model";
import AbstractView from "web.AbstractView";
import ActionModel from "web.ActionModel";
import { mock } from "web.test_utils";
import legacyViewRegistry from "web.view_registry";
import { browser } from "@web/core/browser/browser";
import { LegacyComponent } from "@web/legacy/legacy_component";
import { xml } from "@odoo/owl";
const viewRegistry = registry.category("views");
let serverData;
let target;
QUnit.module("LegacyViews", (hooks) => {
hooks.beforeEach(async () => {
target = getFixture();
serverData = {
models: {
foo: {
fields: {
display_name: { string: "Displayed name", type: "char" },
foo: {
string: "Foo",
type: "char",
default: "My little Foo Value",
store: true,
sortable: true,
},
date_field: { string: "Date", type: "date", store: true, sortable: true },
float_field: { string: "Float", type: "float" },
bar: {
string: "Bar",
type: "many2one",
relation: "partner",
store: true,
sortable: true,
},
},
records: [],
},
},
views: {
"foo,false,legacy_toy": `<legacy_toy/>`,
"foo,false,toy": `<toy/>`,
"foo,false,search": `
<search>
<field name="foo" operator="="/>
<filter name="true_domain" string="True Domain" domain="[(1, '=', 1)]"/>
<filter name="date_domain" string="Date Filter" date="date_field" domain="[]"/>
<filter name="group_by_bar" string="Bar" context="{ 'group_by': 'bar' }"/>
<filter name="group_by_date_field" string="Date GroupBy" context="{ 'group_by': 'date_field' }"/>
</search>
`,
},
};
setupControlPanelFavoriteMenuRegistry();
setupControlPanelServiceRegistry();
class ToyController extends LegacyComponent {}
ToyController.template = xml`<div class="o_toy_view"><ControlPanel /></div>`;
ToyController.components = { ControlPanel};
viewRegistry.add("toy", {
type: "toy",
display_name: _lt("Toy view"),
multiRecord: true,
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
Controller: ToyController,
});
const LegacyToyView = AbstractView.extend({
display_name: _lt("Legacy toy view"),
icon: "fa fa-bars",
multiRecord: true,
viewType: "legacy_toy",
searchMenuTypes: ["filter", "groupBy", "comparison", "favorite"],
});
legacyViewRegistry.add("legacy_toy", LegacyToyView);
});
QUnit.module("State Mappings");
QUnit.test(
"legacy and new views can share search model state (no favorite)",
async function (assert) {
assert.expect(10);
const unpatchDate = mock.patchDate(2021, 6, 1, 10, 0, 0);
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Action name",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "toy"],
[false, "legacy_toy"],
],
context: {
search_default_foo: "ABC",
search_default_true_domain: 1,
search_default_date_domain: 1,
search_default_group_by_bar: 50,
search_default_group_by_date_field: 1,
},
});
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
"TrueDomainorDate Filter: July 2021",
"DateGroupBy: Month>Bar",
]
);
await toggleMenu(target, "Comparison");
await toggleMenuItem(target, "Date Filter: Previous Period");
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
"TrueDomainorDate Filter: July 2021",
"DateGroupBy: Month>Bar",
"DateFilter: Previous Period",
]
);
await switchView(target, "legacy_toy");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
"TrueDomainorDate Filter: July 2021",
"DateGroupBy: Month>Bar",
"DateFilter: Previous Period",
]
);
await removeFacet(target, 1);
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
"DateGroupBy: Month>Bar",
]
);
await switchView(target, "toy");
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
"DateGroupBy: Month>Bar",
]
);
// Check if update works
await removeFacet(target, 1);
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
]
);
await switchView(target, "legacy_toy");
assert.deepEqual(
getFacetTexts(target).map((s) => s.replace(/\s/, "")),
[
"FooABC",
]
);
unpatchDate();
}
);
QUnit.test(
"legacy and new views can share search model state (favorite)",
async function (assert) {
assert.expect(6);
serverData.models.foo.filters = [
{
context: "{}",
domain: "[['foo', '=', 'qsdf']]",
id: 7,
is_default: true,
name: "My favorite",
sort: "[]",
user_id: [2, "Mitchell Admin"],
},
];
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Action name",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "toy"],
[false, "legacy_toy"],
],
});
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
await switchView(target, "legacy_toy");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
await switchView(target, "toy");
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.deepEqual(getFacetTexts(target), ["My favorite"]);
}
);
QUnit.test(
"newly created favorite in a new view can be used in a legacy view",
async function (assert) {
assert.expect(5);
patchWithCleanup(browser, { setTimeout: (fn) => fn() });
const webClient = await createWebClient({
serverData,
mockRPC(_, args) {
if (args.method === "create_or_replace") {
assert.ok(typeof args.args[0].domain === "string");
return 7; // fake serverId to simulate the creation of
// the favorite in db.
}
},
});
await doAction(webClient, {
name: "Action name",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "toy"],
[false, "legacy_toy"],
],
});
assert.containsOnce(target, ".o_switch_view.o_toy.active");
await toggleFavoriteMenu(target);
await toggleSaveFavorite(target);
await saveFavorite(target);
assert.deepEqual(getFacetTexts(target), ["Action name"]);
await switchView(target, "legacy_toy");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.deepEqual(getFacetTexts(target), ["Action name"]);
}
);
QUnit.test(
"legacy and new views with search model extensions can share search model state",
async function (assert) {
assert.expect(16);
serverData.views[
"foo,1,legacy_toy"
] = `<legacy_toy js_class="legacy_toy_with_extension"/>`;
class SearchModelExtension extends SearchModel {
setup() {
super.setup(...arguments);
this.toyExtension = { locationId: "Grand-Rosière" };
}
exportState() {
const exportedState = super.exportState(...arguments);
exportedState.toyExtension = this.toyExtension;
return exportedState;
}
_importState(state) {
super._importState(state);
this.toyExtension = state.toyExtension;
assert.step(JSON.stringify(state.toyExtension));
}
}
const ToyView = viewRegistry.get("toy");
ToyView.SearchModel = SearchModelExtension;
class ToyExtension extends ActionModel.Extension {
importState(state) {
super.importState(state); // done even if state is undefined in legacy code
assert.step(JSON.stringify(state) || "no state");
}
prepareState() {
Object.assign(this.state, {
locationId: "The place to be",
});
}
}
ActionModel.registry.add("toyExtension", ToyExtension);
const LegacyToyView = legacyViewRegistry.get("legacy_toy");
const LegacyToyViewWithExtension = LegacyToyView.extend({
_createSearchModel(params, extraExtensions = {}) {
Object.assign(extraExtensions, { toyExtension: {} });
return this._super(params, extraExtensions);
},
});
legacyViewRegistry.add("legacy_toy_with_extension", LegacyToyViewWithExtension);
const webClient = await createWebClient({ serverData });
await doAction(webClient, {
name: "Action name",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[false, "toy"],
[1, "legacy_toy"],
],
});
assert.containsOnce(target, ".o_switch_view.o_toy.active");
await switchView(target, "legacy_toy");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.verifySteps([`{"locationId":"Grand-Rosière"}`]);
await switchView(target, "toy");
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.verifySteps([`{"locationId":"Grand-Rosière"}`]);
await doAction(webClient, {
name: "Action name",
res_model: "foo",
type: "ir.actions.act_window",
views: [
[1, "legacy_toy"],
[false, "toy"],
],
});
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.verifySteps([`no state`]);
await switchView(target, "toy");
assert.containsOnce(target, ".o_switch_view.o_toy.active");
assert.verifySteps([`{"locationId":"The place to be"}`]);
await switchView(target, "legacy_toy");
await legacyExtraNextTick();
assert.containsOnce(target, ".o_switch_view.o_legacy_toy.active");
assert.verifySteps([`{"locationId":"The place to be"}`]);
}
);
});

View file

@ -1,772 +0,0 @@
odoo.define('web.view_dialogs_tests', function (require) {
"use strict";
var dialogs = require('web.view_dialogs');
var ListController = require('web.ListController');
var testUtils = require('web.test_utils');
var Widget = require('web.Widget');
var FormView = require('web.FormView');
const { browser } = require('@web/core/browser/browser');
const { patchWithCleanup } = require('@web/../tests/helpers/utils');
const cpHelpers = require('@web/../tests/search/helpers');
var createView = testUtils.createView;
async function createParent(params) {
var widget = new Widget();
params.server = await testUtils.mock.addMockEnvironment(widget, params);
return widget;
}
QUnit.module('LegacyViews', {
beforeEach: function () {
this.data = {
partner: {
fields: {
display_name: { string: "Displayed name", type: "char" },
foo: {string: "Foo", type: 'char'},
bar: {string: "Bar", type: "boolean"},
instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'},
},
records: [
{id: 1, foo: 'blip', display_name: 'blipblip', bar: true},
{id: 2, foo: 'ta tata ta ta', display_name: 'macgyver', bar: false},
{id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true},
],
},
instrument: {
fields: {
name: {string: "name", type: "char"},
badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]},
},
},
badassery: {
fields: {
level: {string: 'level', type: "char"},
},
records: [
{id: 1, level: 'Awsome'},
],
},
product: {
fields : {
name: {string: "name", type: "char" },
partner : {string: 'Doors', type: 'one2many', relation: 'partner'},
},
records: [
{id: 1, name: 'The end'},
],
},
};
},
}, function () {
QUnit.module('ViewDialog (legacy)');
QUnit.test('formviewdialog buttons in footer are positioned properly', async function (assert) {
assert.expect(2);
var parent = await createParent({
data: this.data,
archs: {
'partner,false,form':
'<form string="Partner">' +
'<sheet>' +
'<group><field name="foo"/></group>' +
'<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
'</sheet>' +
'</form>',
},
});
new dialogs.FormViewDialog(parent, {
res_model: 'partner',
res_id: 1,
}).open();
await testUtils.nextTick();
assert.notOk($('.modal-body button').length,
"should not have any button in body");
assert.strictEqual($('.modal-footer button').length, 1,
"should have only one button in footer");
parent.destroy();
});
QUnit.test('formviewdialog buttons in footer are not duplicated', async function (assert) {
assert.expect(2);
this.data.partner.fields.poney_ids = {string: "Poneys", type: "one2many", relation: 'partner'};
this.data.partner.records[0].poney_ids = [];
var parent = await createParent({
data: this.data,
archs: {
'partner,false,form':
'<form string="Partner">' +
'<field name="poney_ids"><tree editable="top"><field name="display_name"/></tree></field>' +
'<footer><button string="Custom Button" type="object" class="btn-primary"/></footer>' +
'</form>',
},
});
new dialogs.FormViewDialog(parent, {
res_model: 'partner',
res_id: 1,
}).open();
await testUtils.nextTick();
assert.strictEqual($('.modal button.btn-primary').length, 1,
"should have 1 buttons in modal");
await testUtils.dom.click($('.o_field_x2many_list_row_add a'));
await testUtils.fields.triggerKeydown($('input.o_input'), 'escape');
assert.strictEqual($('.modal button.btn-primary').length, 1,
"should still have 1 buttons in modal");
parent.destroy();
});
QUnit.test('SelectCreateDialog use domain, group_by and search default', async function (assert) {
assert.expect(3);
var search = 0;
var parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree string="Partner">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<field name="foo" filter_domain="[(\'display_name\',\'ilike\',self), (\'foo\',\'ilike\',self)]"/>' +
'<group expand="0" string="Group By">' +
'<filter name="groupby_bar" context="{\'group_by\' : \'bar\'}"/>' +
'</group>' +
'</search>',
},
mockRPC: function (route, args) {
if (args.method === 'web_read_group') {
assert.deepEqual(args.kwargs, {
context: {
search_default_foo: "piou",
search_default_groupby_bar: true,
},
domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
fields: ["display_name", "foo", "bar"],
groupby: ["bar"],
orderby: '',
lazy: true,
limit: 80,
}, "should search with the complete domain (domain + search), and group by 'bar'");
}
if (search === 0 && route === '/web/dataset/search_read') {
search++;
assert.deepEqual(args, {
context: {
search_default_foo: "piou",
search_default_groupby_bar: true,
bin_size: true
}, // not part of the test, may change
domain: ["&", ["display_name", "like", "a"], "&", ["display_name", "ilike", "piou"], ["foo", "ilike", "piou"]],
fields: ["display_name", "foo"],
model: "partner",
limit: 80,
sort: ""
}, "should search with the complete domain (domain + search)");
} else if (search === 1 && route === '/web/dataset/search_read') {
assert.deepEqual(args, {
context: {
search_default_foo: "piou",
search_default_groupby_bar: true,
bin_size: true
}, // not part of the test, may change
domain: [["display_name", "like", "a"]],
fields: ["display_name", "foo"],
model: "partner",
limit: 80,
sort: ""
}, "should search with the domain");
}
return this._super.apply(this, arguments);
},
});
new dialogs.SelectCreateDialog(parent, {
no_create: true,
readonly: true,
res_model: 'partner',
domain: [['display_name', 'like', 'a']],
context: {
search_default_groupby_bar: true,
search_default_foo: 'piou',
},
}).open();
await testUtils.nextTick();
const modal = document.body.querySelector(".modal");
await cpHelpers.removeFacet(modal, "Bar");
await cpHelpers.removeFacet(modal);
parent.destroy();
});
QUnit.test('SelectCreateDialog correctly evaluates domains', async function (assert) {
assert.expect(1);
var parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree string="Partner">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<field name="foo"/>' +
'</search>',
},
mockRPC: function (route, args) {
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [['id', '=', 2]],
"should have correctly evaluated the domain");
}
return this._super.apply(this, arguments);
},
session: {
user_context: {uid: 2},
},
});
new dialogs.SelectCreateDialog(parent, {
no_create: true,
readonly: true,
res_model: 'partner',
domain: "[['id', '=', uid]]",
}).open();
await testUtils.nextTick();
parent.destroy();
});
QUnit.test('SelectCreateDialog list view in readonly', async function (assert) {
assert.expect(1);
var parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree string="Partner" editable="bottom">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search/>'
},
});
var dialog;
new dialogs.SelectCreateDialog(parent, {
res_model: 'partner',
}).open().then(function (result) {
dialog = result;
});
await testUtils.nextTick();
// click on the first row to see if the list is editable
await testUtils.dom.click(dialog.$('.o_legacy_list_view tbody tr:first td:not(.o_list_record_selector):first'));
assert.equal(dialog.$('.o_legacy_list_view tbody tr:first td:not(.o_list_record_selector):first input').length, 0,
"list view should not be editable in a SelectCreateDialog");
parent.destroy();
});
QUnit.test('SelectCreateDialog cascade x2many in create mode', async function (assert) {
assert.expect(5);
var form = await createView({
View: FormView,
model: 'product',
data: this.data,
arch: '<form>' +
'<field name="name"/>' +
'<field name="partner" widget="one2many" >' +
'<tree editable="top">' +
'<field name="display_name"/>' +
'<field name="instrument"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
archs: {
'partner,false,form': '<form>' +
'<field name="name"/>' +
'<field name="instrument" widget="one2many" mode="tree"/>' +
'</form>',
'instrument,false,form': '<form>'+
'<field name="name"/>'+
'<field name="badassery">' +
'<tree>'+
'<field name="level"/>'+
'</tree>' +
'</field>' +
'</form>',
'badassery,false,list': '<tree>'+
'<field name="level"/>'+
'</tree>',
'badassery,false,search': '<search>'+
'<field name="level"/>'+
'</search>',
},
mockRPC: function(route, args) {
if (route === '/web/dataset/call_kw/partner/get_formview_id') {
return Promise.resolve(false);
}
if (route === '/web/dataset/call_kw/instrument/get_formview_id') {
return Promise.resolve(false);
}
if (route === '/web/dataset/call_kw/instrument/create') {
assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: "ABC"}],
'The method create should have been called with the right arguments');
return Promise.resolve(false);
}
return this._super(route, args);
},
});
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.fields.many2one.createAndEdit("instrument");
var $modal = $('.modal-lg');
assert.equal($modal.length, 1,
'There should be one modal');
await testUtils.dom.click($modal.find('.o_field_x2many_list_row_add a'));
var $modals = $('.modal-lg');
assert.equal($modals.length, 2,
'There should be two modals');
var $second_modal = $modals.not($modal);
await testUtils.dom.click($second_modal.find('.o_list_table.table.table-sm.table-striped.o_list_table_ungrouped .o_data_row input[type=checkbox]'));
await testUtils.dom.click($second_modal.find('.o_select_button'));
$modal = $('.modal-lg');
assert.equal($modal.length, 1,
'There should be one modal');
assert.equal($modal.find('.o_data_cell').text(), 'Awsome',
'There should be one item in the list of the modal');
await testUtils.dom.click($modal.find('.btn.btn-primary'));
form.destroy();
});
QUnit.test('Form dialog and subview with _view_ref contexts', async function (assert) {
assert.expect(2);
this.data.instrument.records = [{id: 1, name: 'Tromblon', badassery: [1]}];
this.data.partner.records[0].instrument = 1;
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="name"/>' +
'<field name="instrument" context="{\'tree_view_ref\': \'some_tree_view\'}"/>' +
'</form>',
res_id: 1,
archs: {
'instrument,false,form': '<form>'+
'<field name="name"/>'+
'<field name="badassery" context="{\'tree_view_ref\': \'some_other_tree_view\'}"/>' +
'</form>',
'badassery,false,list': '<tree>'+
'<field name="level"/>'+
'</tree>',
},
viewOptions: {
mode: 'edit',
},
mockRPC: function(route, args) {
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super(route, args);
},
interceptsPropagate: {
load_views: function (ev) {
var evaluatedContext = ev.data.context;
if (ev.data.modelName === 'instrument') {
assert.deepEqual(evaluatedContext, {tree_view_ref: 'some_tree_view'},
'The correct _view_ref should have been sent to the server, first time');
}
if (ev.data.modelName === 'badassery') {
assert.deepEqual(evaluatedContext, {
base_model_name: 'instrument',
tree_view_ref: 'some_other_tree_view',
}, 'The correct _view_ref should have been sent to the server for the subview');
}
},
},
});
await testUtils.dom.click(form.$('.o_field_widget[name="instrument"] button.o_external_button'));
form.destroy();
});
QUnit.test("Form dialog replaces the context with _createContext method when specified", async function (assert) {
assert.expect(5);
const parent = await createParent({
data: this.data,
archs: {
"partner,false,form":
`<form string="Partner">
<sheet>
<group><field name="foo"/></group>
</sheet>
</form>`,
},
mockRPC: function (route, args) {
if (args.method === "create") {
assert.step(JSON.stringify(args.kwargs.context));
}
return this._super(route, args);
},
});
new dialogs.FormViewDialog(parent, {
res_model: "partner",
context: { answer: 42 },
_createContext: () => ({ dolphin: 64 }),
}).open();
await testUtils.nextTick();
assert.notOk($(".modal-body button").length,
"should not have any button in body");
assert.strictEqual($(".modal-footer button").length, 3,
"should have 3 buttons in footer");
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
assert.verifySteps(['{"answer":42}', '{"dolphin":64}']);
parent.destroy();
});
QUnit.test("Form dialog keeps full context when no _createContext is specified", async function (assert) {
assert.expect(5);
const parent = await createParent({
data: this.data,
archs: {
"partner,false,form":
`<form string="Partner">
<sheet>
<group><field name="foo"/></group>
</sheet>
</form>`,
},
mockRPC: function (route, args) {
if (args.method === "create") {
assert.step(JSON.stringify(args.kwargs.context));
}
return this._super(route, args);
},
});
new dialogs.FormViewDialog(parent, {
res_model: "partner",
context: { answer: 42 }
}).open();
await testUtils.nextTick();
assert.notOk($(".modal-body button").length,
"should not have any button in body");
assert.strictEqual($(".modal-footer button").length, 3,
"should have 3 buttons in footer");
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
await testUtils.dom.click($(".modal-footer button:contains(Save & New)"));
assert.verifySteps(['{"answer":42}', '{"answer":42}']);
parent.destroy();
});
QUnit.test('SelectCreateDialog: save current search', async function (assert) {
assert.expect(4);
testUtils.mock.patch(ListController, {
getOwnedQueryParams: function () {
return {
context: {
shouldBeInFilterContext: true,
},
};
},
});
// save favorite needs this
patchWithCleanup(browser, {
setTimeout: (fn) => fn(),
});
var parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree>' +
'<field name="display_name"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<filter name="bar" help="Bar" domain="[(\'bar\', \'=\', True)]"/>' +
'</search>',
},
env: {
dataManager: {
create_filter: function (filter) {
assert.strictEqual(filter.domain, `[("bar", "=", True)]`,
"should save the correct domain");
const expectedContext = {
group_by: [], // default groupby is an empty list
shouldBeInFilterContext: true,
};
assert.deepEqual(filter.context, expectedContext,
"should save the correct context");
},
}
},
});
var dialog;
new dialogs.SelectCreateDialog(parent, {
context: {shouldNotBeInFilterContext: false},
res_model: 'partner',
}).open().then(function (result) {
dialog = result;
});
await testUtils.nextTick();
assert.containsN(dialog, '.o_data_row', 3, "should contain 3 records");
// filter on bar
const modal = document.body.querySelector(".modal");
await cpHelpers.toggleFilterMenu(modal);
await cpHelpers.toggleMenuItem(modal, "Bar");
assert.containsN(dialog, '.o_data_row', 2, "should contain 2 records");
// save filter
await cpHelpers.toggleFavoriteMenu(modal);
await cpHelpers.toggleSaveFavorite(modal);
await cpHelpers.editFavoriteName(modal, "some name");
await cpHelpers.saveFavorite(modal);
testUtils.mock.unpatch(ListController);
parent.destroy();
});
QUnit.test('SelectCreateDialog calls on_selected with every record matching the domain', async function (assert) {
assert.expect(3);
const parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree limit="2" string="Partner">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<field name="foo"/>' +
'</search>',
},
session: {},
});
new dialogs.SelectCreateDialog(parent, {
res_model: 'partner',
on_selected: function(records) {
assert.equal(records.length, 3);
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver,Jack O'Neill");
assert.strictEqual(records.map((r) => r.id).toString(), "1,2,3");
}
}).open();
await testUtils.nextTick();
await testUtils.dom.click($('thead .o_list_record_selector input'));
await testUtils.dom.click($('.o_list_selection_box .o_list_select_domain'));
await testUtils.dom.click($('.modal .o_select_button'));
parent.destroy();
});
QUnit.test('SelectCreateDialog calls on_selected with every record matching without selecting a domain', async function (assert) {
assert.expect(3);
const parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree limit="2" string="Partner">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<field name="foo"/>' +
'</search>',
},
session: {},
});
new dialogs.SelectCreateDialog(parent, {
res_model: 'partner',
on_selected: function(records) {
assert.equal(records.length, 2);
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver");
assert.strictEqual(records.map((r) => r.id).toString(), "1,2");
}
}).open();
await testUtils.nextTick();
await testUtils.dom.click($('thead .o_list_record_selector input'));
await testUtils.dom.click($('.o_list_selection_box '));
await testUtils.dom.click($('.modal .o_select_button'));
parent.destroy();
});
QUnit.test('propagate can_create onto the search popup o2m', async function (assert) {
assert.expect(4);
this.data.instrument.records = [
{id: 1, name: 'Tromblon1'},
{id: 2, name: 'Tromblon2'},
{id: 3, name: 'Tromblon3'},
{id: 4, name: 'Tromblon4'},
{id: 5, name: 'Tromblon5'},
{id: 6, name: 'Tromblon6'},
{id: 7, name: 'Tromblon7'},
{id: 8, name: 'Tromblon8'},
];
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="name"/>' +
'<field name="instrument" can_create="false"/>' +
'</form>',
res_id: 1,
archs: {
'instrument,false,list': '<tree>'+
'<field name="name"/>'+
'</tree>',
'instrument,false,search': '<search>'+
'<field name="name"/>'+
'</search>',
},
viewOptions: {
mode: 'edit',
},
mockRPC: function(route, args) {
if (args.method === 'get_formview_id') {
return Promise.resolve(false);
}
return this._super(route, args);
},
});
await testUtils.fields.many2one.clickOpenDropdown('instrument');
assert.containsNone(form, '.ui-autocomplete a:contains(Start typing...)');
await testUtils.fields.editInput(form.el.querySelector(".o_field_many2one[name=instrument] input"), "a");
assert.containsNone(form, '.ui-autocomplete a:contains(Create and Edit)');
await testUtils.fields.editInput(form.el.querySelector(".o_field_many2one[name=instrument] input"), "");
await testUtils.fields.many2one.clickItem('instrument', 'Search More...');
var $modal = $('.modal-dialog.modal-lg');
assert.strictEqual($modal.length, 1, 'Modal present');
assert.strictEqual($modal.find('.modal-footer button').text(), "Cancel",
'Only the cancel button is present in modal');
form.destroy();
});
QUnit.test('formviewdialog is not closed when button handlers return a rejected promise', async function (assert) {
assert.expect(3);
this.data.partner.fields.poney_ids = { string: "Poneys", type: "one2many", relation: 'partner' };
this.data.partner.records[0].poney_ids = [];
var reject = true;
var parent = await createParent({
data: this.data,
archs: {
'partner,false,form':
'<form string="Partner">' +
'<field name="poney_ids"><tree><field name="display_name"/></tree></field>' +
'</form>',
},
});
new dialogs.FormViewDialog(parent, {
res_model: 'partner',
res_id: 1,
buttons: [{
text: 'Click me !',
classes: "btn-secondary o_form_button_magic",
close: true,
click: function () {
return reject ? Promise.reject() : Promise.resolve();
},
}],
}).open();
await testUtils.nextTick();
assert.strictEqual($('.modal').length, 1, "should have a modal displayed");
await testUtils.dom.click($('.modal .o_form_button_magic'));
assert.strictEqual($('.modal').length, 1, "modal should still be opened");
reject = false;
await testUtils.dom.click($('.modal .o_form_button_magic'));
assert.strictEqual($('.modal').length, 0, "modal should be closed");
parent.destroy();
});
});
});