Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,172 @@
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

@ -0,0 +1,130 @@
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

@ -0,0 +1,109 @@
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

@ -0,0 +1,69 @@
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);
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,80 @@
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,109 @@
/* 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);
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
/* 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

@ -0,0 +1,380 @@
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();
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,114 @@
/* 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();
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
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

@ -0,0 +1,486 @@
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) { ... });
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,404 @@
/** @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

@ -0,0 +1,772 @@
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();
});
});
});