odoo.define('web.form_tests', function (require) { "use strict"; const AbstractField = require("web.AbstractField"); var AbstractStorageService = require('web.AbstractStorageService'); var basicFields = require('web.basic_fields'); var BasicModel = require('web.BasicModel'); var concurrency = require('web.concurrency'); var core = require('web.core'); var fieldRegistry = require('web.field_registry'); const fieldRegistryOwl = require('web.field_registry_owl'); const { FieldBoolean } = require("web.basic_fields_owl"); const FormRenderer = require('web.FormRenderer'); var FormView = require('web.FormView'); var ListView = require('web.ListView'); var KanbanView = require('web.KanbanView'); var mixins = require('web.mixins'); var pyUtils = require('web.py_utils'); var RamStorage = require('web.RamStorage'); var testUtils = require('web.test_utils'); var ViewDialogs = require('web.view_dialogs'); var widgetRegistry = require('web.widget_registry'); const widgetRegistryOwl = require('web.widgetRegistry'); var Widget = require('web.Widget'); const { registry } = require('@web/core/registry'); const legacyViewRegistry = require('web.view_registry'); const { registerCleanup } = require("@web/../tests/helpers/cleanup"); var _t = core._t; var createView = testUtils.createView; const { getFixture, legacyExtraNextTick, patchWithCleanup } = require("@web/../tests/helpers/utils"); const { createWebClient, doAction } = require('@web/../tests/webclient/helpers'); const { makeTestEnv } = require("@web/../tests/helpers/mock_env"); const makeTestEnvironment = require("web.test_env"); const { mapLegacyEnvToWowlEnv } = require("@web/legacy/utils"); const { scrollerService } = require("@web/core/scroller_service"); const { LegacyComponent } = require("@web/legacy/legacy_component"); const { onMounted, onWillUnmount, xml } = require("@odoo/owl"); let serverData; let target; QUnit.module('LegacyViews', { beforeEach: function () { // Avoid animation to not have to wait until the tooltip is removed this.initialTooltipDefaultAnimation = Tooltip.Default.animation; Tooltip.Default.animation = false; target = getFixture(); registry.category("services").add("scroller", scrollerService); registry.category("views").remove("list"); // remove new list from registry registry.category("views").remove("kanban"); // remove new kanban from registry registry.category("views").remove("form"); // remove new form from registry legacyViewRegistry.add("list", ListView); // add legacy list -> will be wrapped and added to new registry legacyViewRegistry.add("kanban", KanbanView); // add legacy kanban -> will be wrapped and added to new registry legacyViewRegistry.add("form", FormView); // add legacy form -> will be wrapped and added to new registry this.data = { partner: { fields: { display_name: { string: "Displayed name", type: "char" }, foo: {string: "Foo", type: "char", default: "My little Foo Value"}, bar: {string: "Bar", type: "boolean"}, int_field: {string: "int_field", type: "integer", sortable: true}, qux: {string: "Qux", type: "float", digits: [16,1] }, p: {string: "one2many field", type: "one2many", relation: 'partner'}, trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, product_id: {string: "Product", type: "many2one", relation: 'product'}, priority: { string: "Priority", type: "selection", selection: [[1, "Low"], [2, "Medium"], [3, "High"]], default: 1, }, state: {string: "State", type: "selection", selection: [["ab", "AB"], ["cd", "CD"], ["ef", "EF"]]}, date: {string: "Some Date", type: "date"}, datetime: {string: "Datetime Field", type: 'datetime'}, product_ids: {string: "one2many product", type: "one2many", relation: "product"}, reference: {string: "Reference Field", type: 'reference', selection: [["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]}, }, records: [{ id: 1, display_name: "first record", bar: true, foo: "yop", int_field: 10, qux: 0.44, p: [], timmy: [], trululu: 4, state: "ab", date: "2017-01-25", datetime: "2016-12-12 10:55:05", }, { id: 2, display_name: "second record", bar: true, foo: "blip", int_field: 9, qux: 13, p: [], timmy: [], trululu: 1, state: "cd", }, { id: 4, display_name: "aaa", state: "ef", }, { id: 5, display_name: "aaa", foo:'', bar:false, state: "ef", }], onchanges: {}, }, product: { fields: { display_name: {string: "Product Name", type: "char"}, name: {string: "Product Name", type: "char"}, partner_type_id: {string: "Partner type", type: "many2one", relation: "partner_type"}, }, records: [{ id: 37, display_name: "xphone", }, { id: 41, display_name: "xpad", }] }, partner_type: { fields: { name: {string: "Partner Type", type: "char"}, color: {string: "Color index", type: "integer"}, }, records: [ {id: 12, display_name: "gold", color: 2}, {id: 14, display_name: "silver", color: 5}, ] }, user: { fields: { name: {string: "Name", type: "char"}, partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'}, }, records: [{ id: 17, name: "Aline", partner_ids: [1], }, { id: 19, name: "Christine", }] }, "res.company": { fields: { name: { string: "Name", type: "char" }, }, }, }; this.actions = [{ id: 1, name: 'Partners Action 1', res_model: 'partner', type: 'ir.actions.act_window', views: [[false, 'kanban'], [false, 'form']], }]; // map legacy test data const actions = {}; this.actions.forEach((act) => { actions[act.xmlId || act.id] = act; }); serverData = { actions, models: this.data, }; }, afterEach: async function () { Tooltip.Default.animation = this.initialTooltipDefaultAnimation; }, }, function () { QUnit.module('FormView (legacy)'); QUnit.test('simple form rendering', async function (assert) { assert.expect(12); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
some htmlaa
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.containsOnce(form, 'div.test'); assert.strictEqual(form.$('div.test').css('opacity'), '0.5', "should keep the inline style on html elements"); assert.containsOnce(form, 'label:contains(Foo)'); assert.containsOnce(form, 'span:contains(blip)'); assert.hasAttrValue(form.$('.o_group .o_group:first'), 'style', 'background-color: red', "should apply style attribute on groups"); assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue', "should apply style attribute on fields"); assert.containsNone(form, 'label:contains(something_id)'); assert.containsOnce(form, 'label:contains(f3_description)'); assert.containsOnce(form, 'div.o_field_one2many table'); assert.containsOnce(form, 'tbody td:not(.o_list_record_selector) .form-check input:checked'); assert.containsOnce(form, '.o_control_panel .breadcrumb:contains(second record)'); assert.containsNone(form, 'label.o_form_label_empty:contains(timmy)'); form.destroy(); }); QUnit.test('duplicate fields rendered properly', async function (assert) { assert.expect(6); this.data.partner.records.push({ id: 6, bar: true, foo: "blip", int_field: 9, }); var form = await createView({ View: FormView, viewOptions: { mode: 'edit' }, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 6, }); assert.hasClass(form.$('div.o_group input[name="foo"]:eq(0)'), 'o_invisible_modifier', 'first foo widget should be invisible'); assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(1):not(.o_invisible_modifier)', "second foo widget should be visible"); assert.containsOnce(form, 'div.o_group input[name="foo"]:eq(2):not(.o_invisible_modifier)', "third foo widget should be visible"); await testUtils.fields.editInput(form.$('div.o_group input[name="foo"]:eq(2)'), "hello"); assert.strictEqual(form.$('div.o_group input[name="foo"]:eq(1)').val(), "hello", "second foo widget should be 'hello'"); assert.containsOnce(form, 'div.o_group input[name="int_field"]:eq(0):not(.o_readonly_modifier)', "first int_field widget should not be readonly"); assert.hasClass(form.$('div.o_group span[name="int_field"]:eq(0)'),'o_readonly_modifier', "second int_field widget should be readonly"); form.destroy(); }); QUnit.test('duplicate fields rendered properly (one2many)', async function (assert) { assert.expect(7); this.data.partner.records.push({ id: 6, p: [1], }); var form = await createView({ View: FormView, viewOptions: { mode: 'edit' }, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 6, }); assert.containsOnce(form, 'div.o_field_one2many:eq(0):not(.o_readonly_modifier)', "first one2many widget should not be readonly"); assert.hasClass(form.$('div.o_field_one2many:eq(1)'),'o_readonly_modifier', "second one2many widget should be readonly"); await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) tr.o_data_row td.o_data_cell:eq(0)')); assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "yop", "first line in one2many of first tab contains yop"); assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(), "yop", "first line in one2many of second tab contains yop"); await testUtils.fields.editInput(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]'), "hello"); assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row td.o_data_cell:eq(0)').text(), "hello", "first line in one2many of second tab contains hello"); await testUtils.dom.click(form.$('div.tab-content table.o_list_table:eq(0) a:contains(Add a line)')); assert.strictEqual(form.$('div.tab-content table.o_list_table tr.o_selected_row input[name="foo"]').val(), "My little Foo Value", "second line in one2many of first tab contains 'My little Foo Value'"); assert.strictEqual(form.$('div.tab-content table.o_list_table:eq(1) tr.o_data_row:eq(1) td.o_data_cell:eq(0)').text(), "My little Foo Value", "first line in one2many of second tab contains hello"); form.destroy(); }); QUnit.test('attributes are transferred on async widgets', async function (assert) { assert.expect(1); var done = assert.async(); var def = testUtils.makeTestPromise(); var FieldChar = fieldRegistry.get('char'); fieldRegistry.add('asyncwidget', FieldChar.extend({ willStart: function () { return def; }, })); createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 2, }).then(function (form) { assert.hasAttrValue(form.$('.o_field_widget[name=foo]'), 'style', 'color: blue', "should apply style attribute on fields"); form.destroy(); delete fieldRegistry.map.asyncwidget; done(); }); def.resolve(); await testUtils.nextTick(); }); QUnit.test('placeholder attribute on input', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '
', res_id: 2, }); assert.containsOnce(form, 'input[placeholder="chimay"]'); form.destroy(); }); QUnit.test('decoration works on widgets', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 2, }); assert.doesNotHaveClass(form.$('span[name="display_name"]'), 'text-danger'); assert.hasClass(form.$('span[name="foo"]'), 'text-danger'); form.destroy(); }); QUnit.test('decoration on widgets are reevaluated if necessary', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '', res_id: 2, viewOptions: {mode: 'edit'}, }); assert.doesNotHaveClass(form.$('input[name="display_name"]'), 'text-danger'); await testUtils.fields.editInput(form.$('input[name=int_field]'), 3); assert.hasClass(form.$('input[name="display_name"]'), 'text-danger'); form.destroy(); }); QUnit.test('decoration on widgets works on same widget', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 2, viewOptions: {mode: 'edit'}, }); assert.doesNotHaveClass(form.$('input[name="int_field"]'), 'text-danger'); await testUtils.fields.editInput(form.$('input[name=int_field]'), 3); assert.hasClass(form.$('input[name="int_field"]'), 'text-danger'); form.destroy(); }); QUnit.test('only necessary fields are fetched with correct context', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, mockRPC: function (route, args) { // NOTE: actually, the current web client always request the __last_update // field, not sure why. Maybe this test should be modified. assert.deepEqual(args.args[1], ["foo", "display_name"], "should only fetch requested fields"); assert.deepEqual(args.kwargs.context, {bin_size: true}, "bin_size should always be in the context"); return this._super(route, args); } }); form.destroy(); }); QUnit.test('group rendering', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'table.o_inner_group'); form.destroy(); }); QUnit.test('group containing both a field and a group', async function (assert) { // The purpose of this test is to check that classnames defined in a // field widget and those added by the form renderer are correctly // combined. For instance, the renderer adds className 'o_group_col_x' // on outer group's children (an outer group being a group that contains // at least a group). assert.expect(4); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, '.o_group .o_field_widget[name=foo]'); assert.containsOnce(form, '.o_group .o_inner_group .o_field_widget[name=int_field]'); assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_field_char'); assert.hasClass(form.$('.o_field_widget[name=foo]'), 'o_group_col_6'); form.destroy(); }); QUnit.test('Form and subview with _view_ref contexts', async function (assert) { assert.expect(3); serverData.models.product.fields.partner_type_ids = {string: "one2many field", type: "one2many", relation: "partner_type"}, serverData.models.product.records = [{id: 1, name: 'Tromblon', partner_type_ids: [12,14]}]; serverData.models.partner.records[0].product_id = 1; // This is an old test, written before "get_views" (formerly "load_views") automatically // inlines x2many subviews. As the purpose of this test is to assert that the js fetches // the correct sub view when it is not inline (which can still happen in nested form views), // we bypass the inline mecanism of "get_views" by setting widget="one2many" on the field. serverData.views = { 'product,false,form': '
'+ ''+ '' + '', 'partner_type,false,list': ''+ ''+ '', 'product,false,search': '', 'partner,false,form': '
' + '' + '' + '', 'partner,false,search': '', }; const mockRPC = (route, args) => { if (args.method === 'get_views') { var context = args.kwargs.context; if (args.model === 'product') { assert.strictEqual(context.tree_view_ref, 'some_tree_view', 'The correct _view_ref should have been sent to the server, first time'); } if (args.model === 'partner_type') { assert.strictEqual(context.base_model_name, 'product', 'The correct base_model_name should have been sent to the server for the subview'); assert.strictEqual(context.tree_view_ref, 'some_other_tree_view', 'The correct _view_ref should have been sent to the server for the subview'); } } if (args.method === 'get_formview_action') { return Promise.resolve({ res_id: 1, type: 'ir.actions.act_window', target: 'current', res_model: args.model, context: args.kwargs.context, 'view_mode': 'form', 'views': [[false, 'form']], }); } }; const webClient = await createWebClient({ serverData, mockRPC}); await doAction(webClient, { res_id: 1, type: 'ir.actions.act_window', target: 'current', res_model: 'partner', 'view_mode': 'form', 'views': [[false, 'form']], }); await testUtils.dom.click(target.querySelector('.o_field_widget[name="product_id"]')); }); QUnit.test('invisible fields are properly hidden', async function (assert) { assert.expect(4); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + // x2many field without inline view: as it is always invisible, the view // should not be fetched. we don't specify any view in this test, so if it // ever tries to fetch it, it will crash, indicating that this is wrong. '' + '' + '
', res_id: 1, }); assert.containsNone(form, 'label:contains(Foo)'); assert.containsNone(form, '.o_field_widget[name=foo]'); assert.containsNone(form, '.o_field_widget[name=qux]'); assert.containsNone(form, '.o_field_widget[name=p]'); form.destroy(); }); QUnit.test('invisible elements are properly hidden', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, '.o_form_statusbar.o_invisible_modifier button:contains(coucou)'); assert.containsOnce(form, '.o_notebook li.o_invisible_modifier a:contains(invisible)'); assert.containsOnce(form, 'table.o_inner_group.o_invisible_modifier td:contains(invgroup)'); form.destroy(); }); QUnit.test('invisible attrs on fields are re-evaluated on field change', async function (assert) { assert.expect(3); // we set the value bar to simulate a falsy boolean value. this.data.partner.records[0].bar = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode:'edit' }, }); assert.hasClass(form.$('.foo_field'), 'o_invisible_modifier'); assert.hasClass(form.$('.bar_field'), 'o_invisible_modifier'); // set a value on the m2o await testUtils.fields.many2one.searchAndClickItem('product_id'); assert.doesNotHaveClass(form.$('.foo_field'), 'o_invisible_modifier'); form.destroy(); }); QUnit.test('asynchronous fields can be set invisible', async function (assert) { assert.expect(1); var done = assert.async(); var def = testUtils.makeTestPromise(); // we choose this widget because it is a quite simple widget with a non // empty qweb template var PercentPieWidget = fieldRegistry.get('percentpie'); fieldRegistry.add('asyncwidget', PercentPieWidget.extend({ willStart: function () { return def; }, })); createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', res_id: 1, }).then(function (form) { assert.containsNone(form, '.o_field_widget[name="int_field"]'); form.destroy(); delete fieldRegistry.map.asyncwidget; done(); }); def.resolve(); }); QUnit.test('properly handle modifiers and attributes on notebook tags', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.hasClass(form.$('.o_notebook'), 'o_invisible_modifier'); assert.hasClass(form.$('.o_notebook'), 'new_class'); form.destroy(); }); QUnit.test('empty notebook', async function (assert) { assert.expect(2); const form = await createView({ arch: `
`, data: this.data, model: 'partner', res_id: 1, View: FormView, }); // Does not change when switching state await testUtils.form.clickEdit(form); assert.containsNone(form, ':scope .o_notebook .nav'); // Does not change when coming back to initial state await testUtils.form.clickSave(form); assert.containsNone(form, ':scope .o_notebook .nav'); form.destroy(); }); QUnit.test('no visible page', async function (assert) { assert.expect(4); const form = await createView({ arch: `
`, data: this.data, model: 'partner', res_id: 1, View: FormView, }); // Does not change when switching state await testUtils.form.clickEdit(form); for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) { assert.containsNone(nav, '.nav-link.active'); assert.containsN(nav, '.nav-item.o_invisible_modifier', 2); } // Does not change when coming back to initial state await testUtils.form.clickSave(form); for (const nav of form.el.querySelectorAll(':scope .o_notebook .nav')) { assert.containsNone(nav, '.nav-link.active'); assert.containsN(nav, '.nav-item.o_invisible_modifier', 2); } form.destroy(); }); QUnit.test('notebook: pages with invisible modifiers', async function (assert) { assert.expect(10); const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, }); await testUtils.form.clickEdit(form); assert.containsOnce(form, ".o_notebook .nav .nav-link.active", "There should be only one active tab" ); assert.isVisible(form.$(".o_notebook .nav .nav-item:first")); assert.hasClass(form.$(".o_notebook .nav .nav-link:first"), "active"); assert.isNotVisible(form.$(".o_notebook .nav .nav-item:eq(1)")); assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active"); await testUtils.dom.click(form.$(".o_field_widget[name=bar] input")); assert.containsOnce(form, ".o_notebook .nav .nav-link.active", "There should be only one active tab" ); assert.isNotVisible(form.$(".o_notebook .nav .nav-item:first")); assert.doesNotHaveClass(form.$(".o_notebook .nav .nav-link:first"), "active"); assert.isVisible(form.$(".o_notebook .nav .nav-item:eq(1)")); assert.hasClass(form.$(".o_notebook .nav .nav-link:eq(1)"), "active"); form.destroy(); }); QUnit.test('invisible attrs on first notebook page', async function (assert) { assert.expect(6); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.hasClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier'); // set a value on the m2o await testUtils.fields.many2one.searchAndClickItem('product_id'); assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); assert.hasClass(form.$('.o_notebook .nav .nav-item:first()'), 'o_invisible_modifier'); assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); assert.hasClass(form.$('.o_notebook .tab-content .tab-pane:nth(1)'), 'active'); form.destroy(); }); QUnit.test('invisible attrs on notebook page which has only one page', async function (assert) { assert.expect(4); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.notOk(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'), 'first tab should not be active'); assert.ok(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'), 'first tab should be invisible'); // enable checkbox await testUtils.dom.click(form.$('.o_field_boolean input')); assert.ok(form.$('.o_notebook .nav .nav-link:first()').hasClass('active'), 'first tab should be active'); assert.notOk(form.$('.o_notebook .nav .nav-item:first()').hasClass('o_invisible_modifier'), 'first tab should be visible'); form.destroy(); }); QUnit.test('first notebook page invisible', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.notOk(form.$('.o_notebook .nav .nav-item:first()').is(':visible'), 'first tab should be invisible'); assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); form.destroy(); }); QUnit.test('hide notebook element if all pages hidden', async function (assert) { assert.expect(4); const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, }); assert.ok(form.$('.o_notebook .nav li:not(.o_invisible_modifier)').length, "there should be visible page"); assert.notOk(form.$('.o_notebook .nav').hasClass('o_invisible_modifier'), 'the notebook headers should not be hidden if one of the page is visible'); await testUtils.dom.click(form.$('.o_field_boolean input')); assert.notOk(form.$('.o_notebook .nav li:not(.o_invisible_modifier)').length, "there should not be any visible page"); assert.ok(form.$('.o_notebook .nav').hasClass('o_invisible_modifier'), 'the notebook headers should be hidden if none of the page is visible'); form.destroy(); }); QUnit.test('autofocus on second notebook page', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.doesNotHaveClass(form.$('.o_notebook .nav .nav-link:first()'), 'active'); assert.hasClass(form.$('.o_notebook .nav .nav-link:nth(1)'), 'active'); form.destroy(); }); QUnit.test("notebook page is changing when an anchor is clicked from another page", async (assert) => { assert.expect(6); // This should be removed as soon as the view is moved to owl const wowlEnv = await makeTestEnv(); const legacyEnv = makeTestEnvironment({ bus: core.bus }); mapLegacyEnvToWowlEnv(legacyEnv, wowlEnv); const scrollableParent = document.createElement("div"); scrollableParent.style.overflow = "auto"; const target = getFixture(); target.append(scrollableParent); var form = await createView({ View: FormView, model: "partner", data: { partner: { fields: {}, records: [ { id: 1, }, ], }, }, arch: `
No scrollbar!
TO ANCHOR 2

Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue.

Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue.

There is a scroll bar

TO ANCHOR 1

Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue.

`, res_id: 1, }); scrollableParent.append(form.el); // We set the height of the parent to the height of the second pane // We are then sure there will be no scrollable on this pane but a // only for the first pane scrollableParent.style.maxHeight = scrollableParent.querySelector(".o_action").getBoundingClientRect().height + "px"; // The element must be contained in the scrollable parent (top and bottom) const isVisible = (el) => { return ( el.getBoundingClientRect().bottom <= scrollableParent.getBoundingClientRect().bottom && el.getBoundingClientRect().top >= scrollableParent.getBoundingClientRect().top ); }; assert.ok( scrollableParent .querySelector(".tab-pane.active") .contains(scrollableParent.querySelector("#anchor1")), "the first pane is visible" ); assert.ok( !isVisible(scrollableParent.querySelector("#anchor2")), "the second anchor is not visible" ); scrollableParent.querySelector(".link2").click(); assert.ok( scrollableParent .querySelector(".tab-pane.active") .contains(scrollableParent.querySelector("#anchor2")), "the second pane is visible" ); assert.ok( isVisible(scrollableParent.querySelector("#anchor2")), "the second anchor is visible" ); scrollableParent.querySelector(".link1").click(); assert.ok( scrollableParent .querySelector(".tab-pane.active") .contains(scrollableParent.querySelector("#anchor1")), "the first pane is visible" ); assert.ok(isVisible(scrollableParent.querySelector("#anchor1")), "the first anchor is visible"); form.destroy(); }); QUnit.test('notebook name transferred to DOM', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.hasClass(form.$(".o_notebook .nav .nav-link[name='choucroute']"), 'active'); form.destroy(); }); QUnit.test('invisible attrs on group are re-evaluated on field change', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit' }, }); assert.containsOnce(form, 'div.o_group:visible'); await testUtils.dom.click('.o_field_boolean input', form); assert.containsOnce(form, 'div.o_group:hidden'); form.destroy(); }); QUnit.test('invisible attrs with zero value in domain and unset value in data', async function (assert) { assert.expect(1); this.data.partner.fields.int_field.type = 'monetary'; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + 'this should be invisible' + '' + '' + '' + '
', }); assert.isNotVisible(form.$('div.hello')); form.destroy(); }); QUnit.test('reset local state when switching to another view', async function (assert) { assert.expect(3); serverData.views = { 'partner,false,form': `
`, 'partner,false,list': '', 'partner,false,search': '', }; serverData.actions = { 1: { id: 1, name: 'Partner', res_model: 'partner', type: 'ir.actions.act_window', views: [[false, 'list'], [false, 'form']], } }; const webClient = await createWebClient({ serverData }); await doAction(webClient, 1); await testUtils.dom.click(target.querySelector('.o_list_button_add')); await legacyExtraNextTick(); assert.containsOnce(target, '.o_legacy_form_view'); // click on second page tab await testUtils.dom.click($(target).find('.o_notebook .nav-link:eq(1)')); await testUtils.dom.click('.o_control_panel .o_form_button_cancel'); await legacyExtraNextTick(); assert.containsNone(target, '.o_legacy_form_view'); await testUtils.dom.click(target.querySelector('.o_list_button_add')); await legacyExtraNextTick(); // check notebook active page is 0th page assert.hasClass($(target).find('.o_notebook .nav-link:eq(0)'), 'active'); }); QUnit.test('rendering stat buttons with action', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
' + '' + '' + '
' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.containsN(form, 'button.oe_stat_button', 2); assert.containsOnce(form, 'button.oe_stat_button.o_invisible_modifier'); assert.containsOnce(form, 'button.oe_stat_button:disabled'); form.destroy(); }); QUnit.test('rendering stat buttons without action', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
' + '' + '' + '
' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.containsN(form, 'button.oe_stat_button', 2); assert.containsOnce(form, 'button.oe_stat_button.o_invisible_modifier'); assert.containsN(form, 'button.oe_stat_button:disabled', 2); form.destroy(); }); QUnit.test('readonly stat buttons stays disabled', async function (assert) { assert.expect(4); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
' + '' + '' + '
' + '' + '' + '' + '
' + '
', res_id: 2, }); await testUtils.mock.intercept(form, "execute_action", function (event) { if (event.data.action_data.name == "action_to_perform") { assert.containsN(form, 'button.oe_stat_button[disabled]', 2, "While performing the action, both buttons should be disabled."); event.data.on_success(); } }); assert.containsN(form, 'button.oe_stat_button', 2); assert.containsN(form, 'button.oe_stat_button[disabled]', 1); await testUtils.dom.click('button[name=action_to_perform]'); assert.containsN(form, 'button.oe_stat_button[disabled]', 1, "After performing the action, only one button should be disabled."); form.destroy(); }); QUnit.test('label uses the string attribute', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '
', res_id: 2, }); assert.containsOnce(form, 'label.o_form_label:contains(customstring)'); form.destroy(); }); QUnit.test("label ignores the content of the label when present", async function (assert) { await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 2, }); assert.containsOnce(target, "label.o_form_label"); assert.strictEqual(target.querySelector("label.o_form_label").textContent, "Bar"); }); QUnit.test('input ids for multiple occurrences of fields in form view', async function (assert) { // A same field can occur several times in the view, but its id must be // unique by occurrence, otherwise there is a warning in the console (in // edit mode) as we get several inputs with the same "id" attribute, and // several labels the same "for" attribute. assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, }); const fieldIdAttrs = [...form.$('.o_field_widget')].map(n => n.getAttribute('id')); const labelForAttrs = [...form.$('.o_form_label')].map(n => n.getAttribute('for')); assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, "should have generated a unique id for each field occurrence"); assert.deepEqual(fieldIdAttrs, labelForAttrs, "the for attribute of labels must coincide with field ids"); form.destroy(); }); QUnit.test('input ids for multiple occurrences of fields in sub form view (inline)', async function (assert) { // A same field can occur several times in the view, but its id must be // unique by occurrence, otherwise there is a warning in the console (in // edit mode) as we get several inputs with the same "id" attribute, and // several labels the same "for" attribute. assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, }); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(document.body, '.modal .o_legacy_form_view'); const fieldIdAttrs = [...$('.modal .o_legacy_form_view .o_field_widget')].map(n => n.getAttribute('id')); const labelForAttrs = [...$('.modal .o_legacy_form_view .o_form_label')].map(n => n.getAttribute('for')); assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, "should have generated a unique id for each field occurrence"); assert.deepEqual(fieldIdAttrs, labelForAttrs, "the for attribute of labels must coincide with field ids"); form.destroy(); }); QUnit.test('input ids for multiple occurrences of fields in sub form view (not inline)', async function (assert) { // A same field can occur several times in the view, but its id must be // unique by occurrence, otherwise there is a warning in the console (in // edit mode) as we get several inputs with the same "id" attribute, and // several labels the same "for" attribute. assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
', archs: { 'partner,false,list': '', 'partner,false,form': `
` }, }); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(document.body, '.modal .o_legacy_form_view'); const fieldIdAttrs = [...$('.modal .o_legacy_form_view .o_field_widget')].map(n => n.getAttribute('id')); const labelForAttrs = [...$('.modal .o_legacy_form_view .o_form_label')].map(n => n.getAttribute('for')); assert.strictEqual([...new Set(fieldIdAttrs)].length, 4, "should have generated a unique id for each field occurrence"); assert.deepEqual(fieldIdAttrs, labelForAttrs, "the for attribute of labels must coincide with field ids"); form.destroy(); }); QUnit.test('two occurrences of invalid field in form view', async function (assert) { assert.expect(2); this.data.partner.fields.trululu.required = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, }); await testUtils.form.clickSave(form); assert.containsN(form, '.o_form_label.o_field_invalid', 2); assert.containsN(form, '.o_field_many2one.o_field_invalid', 2); form.destroy(); }); QUnit.test('tooltips on multiple occurrences of fields and labels', async function (assert) { assert.expect(4); const initialDebugMode = odoo.debug; odoo.debug = false; this.data.partner.fields.foo.help = 'foo tooltip'; this.data.partner.fields.bar.help = 'bar tooltip'; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, }); const $fooLabel1 = form.$('.o_form_label:nth(0)'); $fooLabel1.tooltip('show', false); $fooLabel1[0].dispatchEvent(new Event('mouseover')); assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip"); $fooLabel1[0].dispatchEvent(new Event('mouseout')); const $fooLabel2 = form.$('.o_form_label:nth(2)'); $fooLabel2.tooltip('show', false); $fooLabel2[0].dispatchEvent(new Event('mouseover')); assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "foo tooltip"); $fooLabel2[0].dispatchEvent(new Event('mouseout')); const $barLabel1 = form.$('.o_form_label:nth(1)'); $barLabel1.tooltip('show', false); $barLabel1[0].dispatchEvent(new Event('mouseover')); assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip"); $barLabel1[0].dispatchEvent(new Event('mouseout')); const $barLabel2 = form.$('.o_form_label:nth(3)'); $barLabel2.tooltip('show', false); $barLabel2[0].dispatchEvent(new Event('mouseover')); assert.strictEqual($('.tooltip .oe_tooltip_help').text().trim(), "bar tooltip"); $barLabel2[0].dispatchEvent(new Event('mouseout')); odoo.debug = initialDebugMode; form.destroy(); }); QUnit.test('readonly attrs on fields are re-evaluated on field change', async function (assert) { assert.expect(4); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.containsOnce(form, 'span[name="foo"]', "the foo field widget should be readonly"); await testUtils.dom.click(form.$('.o_field_boolean input')); assert.containsOnce(form, 'input[name="foo"]', "the foo field widget should have been rerendered to now be editable"); await testUtils.dom.click(form.$('.o_field_boolean input')); assert.containsOnce(form, 'span[name="foo"]', "the foo field widget should have been rerendered to now be readonly again"); await testUtils.dom.click(form.$('.o_field_boolean input')); assert.containsOnce(form, 'input[name="foo"]', "the foo field widget should have been rerendered to now be editable again"); form.destroy(); }); QUnit.test('readonly attrs on lines are re-evaluated on field change 2', async function (assert) { assert.expect(4); this.data.partner.records[0].product_ids = [37]; this.data.partner.records[0].trululu = false; this.data.partner.onchanges = { trululu(record) { // when trululu changes, push another record in product_ids. // only push a second record once. if (record.product_ids.map(command => command[1]).includes(41)) { return; } // copy the list to force it as different from the original record.product_ids = record.product_ids.slice(); record.product_ids.push([4,41,false]); } }; this.data.product.records[0].name = 'test'; // This one is necessary to have a valid, rendered widget this.data.product.fields.int_field = { type:"integer", string: "intField" }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, viewOptions: { mode: 'edit', }, }); for (let value of [true, false, true, false]) { if (value) { await testUtils.fields.many2one.clickOpenDropdown('trululu') await testUtils.fields.many2one.clickHighlightedItem('trululu') assert.notOk($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should not be readonly') } else { await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="trululu"] input'), '', ['keyup']) assert.ok($('.o_field_one2many[name="product_ids"]').hasClass("o_readonly_modifier"), 'lines should be readonly') } } form.destroy(); }); QUnit.test('empty fields have o_form_empty class in readonly mode', async function (assert) { assert.expect(8); this.data.partner.fields.foo.default = false; // no default value for this test this.data.partner.records[1].foo = false; // 1 is record with id=2 this.data.partner.records[1].trululu = false; // 1 is record with id=2 this.data.partner.fields.int_field.readonly = true; this.data.partner.onchanges.foo = function (obj) { if (obj.foo === "hello") { obj.int_field = false; } }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); assert.containsN(form, '.o_field_widget.o_field_empty', 2, "should have 2 empty fields with correct class"); assert.containsN(form, '.o_form_label_empty', 2, "should have 2 muted labels (for the empty fieds) in readonly"); await testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_field_empty', "in edit mode, only empty readonly fields should have the o_field_empty class"); assert.containsOnce(form, '.o_form_label_empty', "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'test'); assert.containsNone(form, '.o_field_empty', "after readonly modifier change, the o_field_empty class should have been removed"); assert.containsNone(form, '.o_form_label_empty', "after readonly modifier change, the o_form_label_empty class should have been removed"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'hello'); assert.containsOnce(form, '.o_field_empty', "after value changed to false for a readonly field, the o_field_empty class should have been added"); assert.containsOnce(form, '.o_form_label_empty', "after value changed to false for a readonly field, the o_form_label_empty class should have been added"); form.destroy(); }); QUnit.test('empty fields\' labels still get the empty class after widget rerender', async function (assert) { assert.expect(6); this.data.partner.fields.foo.default = false; // no default value for this test this.data.partner.records[1].foo = false; // 1 is record with id=2 this.data.partner.records[1].display_name = false; // 1 is record with id=2 var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', res_id: 2, }); assert.containsN(form, '.o_field_widget.o_field_empty', 2); assert.containsN(form, '.o_form_label_empty', 2, "should have 1 muted label (for the empty fied) in readonly"); await testUtils.form.clickEdit(form); assert.containsNone(form, '.o_field_empty', "in edit mode, only empty readonly fields should have the o_field_empty class"); assert.containsNone(form, '.o_form_label_empty', "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly'); await testUtils.fields.editInput(form.$('input[name=foo]'), 'edit'); await testUtils.fields.editInput(form.$('input[name=display_name]'), 'some name'); await testUtils.fields.editInput(form.$('input[name=foo]'), 'readonly'); assert.containsNone(form, '.o_field_empty', "there still should not be any empty class on fields as the readonly one is now set"); assert.containsNone(form, '.o_form_label_empty', "there still should not be any empty class on labels as the associated readonly field is now set"); form.destroy(); }); QUnit.test('empty inner readonly fields don\'t have o_form_empty class in "create" mode', async function (assert) { assert.expect(2); this.data.partner.fields.product_id.readonly = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', }); assert.containsNone(form, '.o_form_label_empty', "no empty class on label"); assert.containsNone(form, '.o_field_empty', "no empty class on field"); form.destroy(); }); QUnit.test('label tag added for fields have o_form_empty class in readonly mode if field is empty', async function (assert) { assert.expect(8); this.data.partner.fields.foo.default = false; // no default value for this test this.data.partner.records[1].foo = false; // 1 is record with id=2 this.data.partner.records[1].trululu = false; // 1 is record with id=2 this.data.partner.fields.int_field.readonly = true; this.data.partner.onchanges.foo = function (obj) { if (obj.foo === "hello") { obj.int_field = false; } }; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 2, }); assert.containsN(form, '.o_field_widget.o_field_empty', 2, "should have 2 empty fields with correct class"); assert.containsN(form, '.o_form_label_empty', 2, "should have 2 muted labels (for the empty fieds) in readonly"); await testUtils.form.clickEdit(form); assert.containsOnce(form, '.o_field_empty', "in edit mode, only empty readonly fields should have the o_field_empty class"); assert.containsOnce(form, '.o_form_label_empty', "in edit mode, only labels associated to empty readonly fields should have the o_form_label_empty class"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'test'); assert.containsNone(form, '.o_field_empty', "after readonly modifier change, the o_field_empty class should have been removed"); assert.containsNone(form, '.o_form_label_empty', "after readonly modifier change, the o_form_label_empty class should have been removed"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'hello'); assert.containsOnce(form, '.o_field_empty', "after value changed to false for a readonly field, the o_field_empty class should have been added"); assert.containsOnce(form, '.o_form_label_empty', "after value changed to false for a readonly field, the o_form_label_empty class should have been added"); form.destroy(); }); QUnit.test('form view can switch to edit mode', async function (assert) { assert.expect(9); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.strictEqual(form.mode, 'readonly', 'form view should be in readonly mode'); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly'); assert.isVisible(form.$buttons.find('.o_form_buttons_view')); assert.isNotVisible(form.$buttons.find('.o_form_buttons_edit')); await testUtils.form.clickEdit(form); assert.strictEqual(form.mode, 'edit', 'form view should be in edit mode'); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable'); assert.doesNotHaveClass(form.$('.o_legacy_form_view'), 'o_form_readonly'); assert.isNotVisible(form.$buttons.find('.o_form_buttons_view')); assert.isVisible(form.$buttons.find('.o_form_buttons_edit')); form.destroy(); }); QUnit.test('required attrs on fields are re-evaluated on field change', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); assert.containsOnce(form, 'input[name="foo"].o_required_modifier', "the foo field widget should be required"); await testUtils.dom.click('.o_field_boolean input'); assert.containsOnce(form, 'input[name="foo"]:not(.o_required_modifier)', "the foo field widget should now have been marked as non-required"); await testUtils.dom.click('.o_field_boolean input'); assert.containsOnce(form, 'input[name="foo"].o_required_modifier', "the foo field widget should now have been marked as required again"); form.destroy(); }); QUnit.test('required fields should have o_required_modifier in readonly mode', async function (assert) { assert.expect(2); this.data.partner.fields.foo.required = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'span.o_required_modifier'); await testUtils.form.clickEdit(form); assert.containsOnce(form, 'input.o_required_modifier', "in edit mode, should have 1 input with o_required_modifier"); form.destroy(); }); QUnit.test('required float fields works as expected', async function (assert) { assert.expect(10); this.data.partner.fields.qux.required = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); assert.hasClass(form.$('input[name="qux"]'), 'o_required_modifier'); assert.strictEqual(form.$('input[name="qux"]').val(), "0.0", "qux input is 0 by default (float field)"); await testUtils.form.clickSave(form); assert.containsNone(form.$('input[name="qux"]'), "should have switched to readonly"); await testUtils.form.clickEdit(form); await testUtils.fields.editInput(form.$('input[name=qux]'), '1'); await testUtils.form.clickSave(form); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('input[name="qux"]').val(), "1.0", "qux input is properly formatted"); assert.verifySteps(['onchange', 'create', 'read', 'write', 'read']); form.destroy(); }); QUnit.test('separators', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'div.o_horizontal_separator'); form.destroy(); }); QUnit.test('invisible attrs on separators', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.hasClass(form.$('div.o_horizontal_separator'), 'o_invisible_modifier'); form.destroy(); }); QUnit.test('buttons in form view', async function (assert) { assert.expect(8); var rpcCount = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 2, mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.args[1].foo, "tralala", "should have saved the changes"); } assert.step(args.method); return this._super(route, args); }, }); await testUtils.form.clickEdit(form); var count = 0; await testUtils.mock.intercept(form, "execute_action", function (event) { event.stopPropagation(); count++; }); await testUtils.dom.click('.oe_stat_button'); assert.strictEqual(count, 1, "should have triggered a execute action"); assert.strictEqual(form.mode, "edit", "form view should be in edit mode"); await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); await testUtils.dom.click('.oe_stat_button:first'); assert.strictEqual(form.mode, "edit", "form view should be in edit mode"); assert.strictEqual(count, 2, "should have triggered a execute action"); assert.verifySteps(['read', 'write', 'read']); form.destroy(); }); QUnit.test('clicking on stat buttons save and reload in edit mode', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '
' + '' + '
' + '' + '' + '' + '
' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method === 'write') { // simulate an override of the model... args.args[1].display_name = "GOLDORAK"; args.args[1].name = "GOLDORAK"; } return this._super.apply(this, arguments); }, }); assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'second record', "should have correct display_name"); await testUtils.form.clickEdit(form); await testUtils.fields.editInput(form.$('input[name=name]'), 'some other name'); await testUtils.dom.click('.oe_stat_button'); assert.strictEqual(form.$('.o_control_panel .breadcrumb').text(), 'GOLDORAK', "should have correct display_name"); form.destroy(); }); QUnit.test('buttons with attr "special" do not trigger a save', async function (assert) { assert.expect(4); var executeActionCount = 0; var writeCount = 0; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 2, }); // readonly mode assert.containsOnce(form, '.oe_stat_button', "button box should be displayed in readonly"); // edit mode await testUtils.form.clickEdit(form); assert.containsOnce(form, '.oe_stat_button', "button box should be displayed in edit on an existing record"); // create mode (leave edition first!) await testUtils.form.clickDiscard(form); await testUtils.form.clickCreate(form); assert.containsOnce(form, '.oe_stat_button', "button box should be displayed when creating a new record as well"); form.destroy(); }); QUnit.test('properly apply onchange on one2many fields', async function (assert) { assert.expect(5); this.data.partner.records[0].p = [4]; this.data.partner.onchanges = { foo: function (obj) { obj.p = [ [5], [1, 4, {display_name: "updated record"}], [0, null, {display_name: "created record"}], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, '.o_field_one2many .o_data_row', "there should be one one2many record linked at first"); assert.strictEqual(form.$('.o_field_one2many .o_data_row td:first').text(), 'aaa', "the 'display_name' of the one2many record should be correct"); // switch to edit mode await testUtils.form.clickEdit(form); await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange'); var $o2m = form.$('.o_field_one2many'); assert.strictEqual($o2m.find('.o_data_row').length, 2, "there should be two linked record"); assert.strictEqual($o2m.find('.o_data_row:first td:first').text(), 'updated record', "the 'display_name' of the first one2many record should have been updated"); assert.strictEqual($o2m.find('.o_data_row:nth(1) td:first').text(), 'created record', "the 'display_name' of the second one2many record should be correct"); form.destroy(); }); QUnit.test('properly apply onchange on one2many fields direct click', async function (assert) { assert.expect(3); var def = testUtils.makeTestPromise(); this.data.partner.records[0].p = [2, 4]; this.data.partner.onchanges = { int_field: function (obj) { obj.p = [ [5], [1, 2, {display_name: "updated record 1", int_field: obj.int_field}], [1, 4, {display_name: "updated record 2", int_field: obj.int_field * 2}], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { var self = this; var my_args = arguments; var my_super = this._super; return def.then(() => { return my_super.apply(self, my_args) }); } return this._super.apply(this, arguments); }, archs: { 'partner,false,form': '
' }, viewOptions: { mode: 'edit', }, }); // Trigger the onchange await testUtils.fields.editInput(form.$('input[name=int_field]'), '2'); // Open first record in one2many await testUtils.dom.click(form.$('.o_data_row:first')); assert.containsNone(document.body, '.modal'); def.resolve(); await testUtils.nextTick(); assert.containsOnce(document.body, '.modal'); assert.strictEqual($('.modal').find('input[name=int_field]').val(), '2'); form.destroy(); }); QUnit.test('update many2many value in one2many after onchange', async function (assert) { assert.expect(2); this.data.partner.records[1].p = [4]; this.data.partner.onchanges = { foo: function (obj) { obj.p = [ [5], [1, 4, { display_name: "gold", timmy: [[5]], }], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 2, }); assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "aaaNo records", "should have proper initial content"); await testUtils.form.clickEdit(form); await testUtils.fields.editInput(form.$('input[name=foo]'), 'tralala'); assert.strictEqual($('div[name="p"] .o_data_row td').text().trim(), "goldNo records", "should have proper initial content"); form.destroy(); }); QUnit.test('delete a line in a one2many while editing another line', async function (assert) { assert.expect(2); this.data.partner.records[0].p = [1, 2]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_data_cell').first()); await testUtils.fields.editInput(form.$('input[name=display_name]'), ''); await testUtils.dom.click(form.$('.fa-trash-o').eq(1)); // use of owlCompatibilityExtraNextTick because there are two sequential updates of the // control panel (which is written in owl): each of them waits for the next animation frame // to complete await testUtils.owlCompatibilityExtraNextTick(); assert.hasClass(form.$('.o_data_cell').first(), "o_invalid_cell", "Cell should be invalidated."); assert.containsN(form, '.o_data_row', 2, "The other line should not have been deleted."); form.destroy(); }); QUnit.test('properly apply onchange on many2many fields', async function (assert) { assert.expect(14); this.data.partner.onchanges = { foo: function (obj) { obj.timmy = [ [5], [4, 12], [4, 14], ]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { assert.step(args.method); if (args.method === 'read' && args.model === 'partner_type') { assert.deepEqual(args.args[0], [12, 14], "should read both m2m with one RPC"); } if (args.method === 'write') { assert.deepEqual(args.args[1].timmy, [[6, false, [12, 14]]], "should correctly save the changed m2m values"); } return this._super.apply(this, arguments); }, res_id: 2, }); assert.containsNone(form, '.o_field_many2many .o_data_row', "there should be no many2many record linked at first"); // switch to edit mode await testUtils.form.clickEdit(form); await testUtils.fields.editInput(form.$('input[name=foo]'), 'let us trigger an onchange'); var $m2m = form.$('.o_field_many2many'); assert.strictEqual($m2m.find('.o_data_row').length, 2, "there should be two linked records"); assert.strictEqual($m2m.find('.o_data_row:first td:first').text(), 'gold', "the 'display_name' of the first m2m record should be correctly displayed"); assert.strictEqual($m2m.find('.o_data_row:nth(1) td:first').text(), 'silver', "the 'display_name' of the second m2m record should be correctly displayed"); await testUtils.form.clickSave(form); assert.verifySteps(['read', 'onchange', 'read', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('form with domain widget: opening a many2many form and save should not crash', async function (assert) { assert.expect(0); // We just test that there is no crash in this situation this.data.partner.records[0].timmy = [12]; const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, }); // switch to edit mode await testUtils.form.clickEdit(form); // open a form view and save many2many record await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first')); await testUtils.dom.click($('.modal-dialog footer button:first-child')); form.destroy(); }); QUnit.test('display_name not sent for onchanges if not in view', async function (assert) { assert.expect(7); this.data.partner.records[0].timmy = [12]; this.data.partner.onchanges = { foo: function () {}, }; this.data.partner_type.onchanges = { name: function () {}, }; var readInModal = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', mockRPC: function (route, args) { if (args.method === 'read' && args.model === 'partner') { assert.deepEqual(args.args[1], ['foo', 'timmy', 'display_name'], "should read display_name even if not in the view"); } if (args.method === 'read' && args.model === 'partner_type') { if (!readInModal) { assert.deepEqual(args.args[1], ['name'], "should not read display_name for records in the list"); } else { assert.deepEqual(args.args[1], ['name', 'color', 'display_name'], "should read display_name when opening the subrecord"); } } if (args.method === 'onchange' && args.model === 'partner') { assert.deepEqual(args.args[1], { id: 1, foo: 'coucou', timmy: [[6, false, [12]]], }, "should only send the value of fields in the view (+ id)"); assert.deepEqual(args.args[3], { foo: '1', timmy: '', 'timmy.name': '1', 'timmy.color': '', }, "only the fields in the view should be in the onchange spec"); } if (args.method === 'onchange' && args.model === 'partner_type') { assert.deepEqual(args.args[1], { id: 12, name: 'new name', color: 2, }, "should only send the value of fields in the view (+ id)"); assert.deepEqual(args.args[3], { name: '1', color: '', }, "only the fields in the view should be in the onchange spec"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); // trigger the onchange await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), "coucou"); // open a subrecord and trigger an onchange readInModal = true; await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first')); await testUtils.fields.editInput($('.modal .o_field_widget[name=name]'), "new name"); form.destroy(); }); QUnit.test('onchanges on date(time) fields', async function (assert) { assert.expect(6); this.data.partner.onchanges = { foo: function (obj) { obj.date = '2021-12-12'; obj.datetime = '2021-12-12 10:55:05'; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, session: { getTZOffset: function () { return 120; }, }, }); assert.strictEqual(form.$('.o_field_widget[name=date]').text(), '01/25/2017', "the initial date should be correct"); assert.strictEqual(form.$('.o_field_widget[name=datetime]').text(), '12/12/2016 12:55:05', "the initial datetime should be correct"); await testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_widget[name=date] input').val(), '01/25/2017', "the initial date should be correct in edit"); assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(), '12/12/2016 12:55:05', "the initial datetime should be correct in edit"); // trigger the onchange await testUtils.fields.editInput(form.$('.o_field_widget[name="foo"]'), "coucou"); assert.strictEqual(form.$('.o_field_widget[name=date] input').val(), '12/12/2021', "the initial date should be correct in edit"); assert.strictEqual(form.$('.o_field_widget[name=datetime] input').val(), '12/12/2021 12:55:05', "the initial datetime should be correct in edit"); form.destroy(); }); QUnit.test('onchanges are not sent for each keystrokes', async function (assert) { var done = assert.async(); assert.expect(5); var onchangeNbr = 0; this.data.partner.onchanges = { foo: function (obj) { obj.int_field = obj.foo.length + 1000; }, }; var def = testUtils.makeTestPromise(); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '
', res_id: 2, fieldDebounce: 3, mockRPC: function (route, args) { var result = this._super.apply(this, arguments); if (args.method === 'onchange') { onchangeNbr++; return concurrency.delay(3).then(function () { def.resolve(); return result; }); } return result; }, }); await testUtils.form.clickEdit(form); testUtils.fields.editInput(form.$('input[name=foo]'), '1'); assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); testUtils.fields.editInput(form.$('input[name=foo]'), '12'); assert.strictEqual(onchangeNbr, 0, "no onchange has been called yet"); return waitForFinishedOnChange().then(async function () { assert.strictEqual(onchangeNbr, 1, "one onchange has been called"); // add something in the input, then focus another input await testUtils.fields.editAndTrigger(form.$('input[name=foo]'), '123', ['change']); assert.strictEqual(onchangeNbr, 2, "one onchange has been called immediately"); return waitForFinishedOnChange(); }).then(function () { assert.strictEqual(onchangeNbr, 2, "no extra onchange should have been called"); form.destroy(); done(); }); function waitForFinishedOnChange() { return def.then(function () { def = testUtils.makeTestPromise(); return concurrency.delay(0); }); } }); QUnit.test('onchanges are not sent for invalid values', async function (assert) { assert.expect(6); this.data.partner.onchanges = { int_field: function (obj) { obj.foo = String(obj.int_field); }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); await testUtils.form.clickEdit(form); // edit int_field, and check that an onchange has been applied await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123"); assert.strictEqual(form.$('input[name="foo"]').val(), "123", "the onchange has been applied"); // enter an invalid value in a float, and check that no onchange has // been applied await testUtils.fields.editInput(form.$('input[name="int_field"]'), "123a"); assert.strictEqual(form.$('input[name="foo"]').val(), "123", "the onchange has not been applied"); // save, and check that the int_field input is marked as invalid await testUtils.form.clickSave(form); assert.hasClass(form.$('input[name="int_field"]'),'o_field_invalid', "input int_field is marked as invalid"); assert.verifySteps(['read', 'onchange']); form.destroy(); }); QUnit.test('rpc complete after destroying parent', async function (assert) { // We just test that there is no crash in this situation assert.expect(0); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '' + '' + '', res_id: 2, viewOptions: { context: {some_context: true}, }, intercepts: { execute_action: function (e) { assert.deepEqual(e.data.action_data.context, { 'test': 2 }, "button context should have been evaluated and given to the action, with magicc without previous context"); }, }, }); await testUtils.dom.click(form.$('.oe_stat_button')); form.destroy(); }); QUnit.test('clicking on a stat button with no context', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '
' + '' + '
' + '
' + '
', res_id: 2, viewOptions: { context: {some_context: true}, }, intercepts: { execute_action: function (e) { assert.deepEqual(e.data.action_data.context, { }, "button context should have been evaluated and given to the action, with magic keys but without previous context"); }, }, }); await testUtils.dom.click(form.$('.oe_stat_button')); form.destroy(); }); QUnit.test('diplay a stat button outside a buttonbox', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 2, }); assert.containsOnce(form, 'button .o_field_widget', "a field widget should be display inside the button"); assert.strictEqual(form.$('button .o_field_widget').children().length, 2, "the field widget should have 2 children, the text and the value"); assert.strictEqual(parseInt(form.$('button .o_field_widget .o_stat_value').text()), 9, "the value rendered should be the same as the field value"); form.destroy(); }); QUnit.test('diplay something else than a button in a buttonbox', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '' + '
' + '
', res_id: 2, }); assert.strictEqual(form.$('.oe_button_box').children().length, 2, "button box should contain two children"); assert.containsOnce(form, '.oe_button_box > .oe_stat_button', "button box should only contain one button"); assert.containsOnce(form, '.oe_button_box > label', "button box should only contain one label"); form.destroy(); }); QUnit.test('invisible fields are not considered as visible in a buttonbox', async function (assert) { assert.expect(2); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
', res_id: 2, }); assert.strictEqual(form.$('.oe_button_box').children().length, 1, "button box should contain only one child"); assert.hasClass(form.$('.oe_button_box'), 'o_not_full', "the buttonbox should not be full"); form.destroy(); }); QUnit.test('display correctly buttonbox, in large size class', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '
' + '' + '' + '
' + '
', res_id: 2, config: { device: {size_class: 5}, }, }); assert.strictEqual(form.$('.oe_button_box').children().length, 2, "button box should contain two children"); form.destroy(); }); QUnit.test('one2many default value creation', async function (assert) { assert.expect(1); this.data.partner.records[0].product_ids = [37]; this.data.partner.fields.product_ids.default = [ [0, 0, { name: 'xdroid', partner_type_id: 12 }] ]; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'create') { var command = args.args[0].product_ids[0]; assert.strictEqual(command[2].partner_type_id, 12, "the default partner_type_id should be equal to 12"); } return this._super.apply(this, arguments); }, }); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('many2manys inside one2manys are saved correctly', async function (assert) { assert.expect(1); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'create') { var command = args.args[0].p; assert.deepEqual(command, [[0, command[0][1], { timmy: [[6, false, [12]]], }]], "the default partner_type_id should be equal to 12"); } return this._super.apply(this, arguments); }, }); // add a o2m subrecord with a m2m tag await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.fields.many2one.clickOpenDropdown('timmy'); await testUtils.fields.many2one.clickHighlightedItem('timmy'); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2manys (list editable) inside one2manys are saved correctly', async function (assert) { assert.expect(3); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', archs: { "partner,false,form": '
' + '' + '' + '' + '' + '' + '
' }, mockRPC: function (route, args) { if (args.method === 'create') { assert.deepEqual(args.args[0].p, [[0, args.args[0].p[0][1], { p: [[0, args.args[0].p[0][2].p[0][1], {display_name: "xtv"}]], }]], "create should be called with the correct arguments"); } return this._super.apply(this, arguments); }, }); // add a o2m subrecord await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.dom.click($('.modal-body .o_field_one2many .o_field_x2many_list_row_add a')); await testUtils.fields.editInput($('.modal-body input'), 'xtv'); await testUtils.dom.click($('.modal-footer button:first')); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); var row = form.$('.o_field_one2many .o_legacy_list_view .o_data_row'); assert.strictEqual(row.children()[0].textContent, '1 record', "the cell should contains the number of record: 1"); await testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('oe_read_only and oe_edit_only classNames on fields inside groups', async function (assert) { assert.expect(10); const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: `
`, res_id: 1, }); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly', 'form should be in readonly mode'); assert.isVisible(form.$('.o_field_widget[name=foo]')); assert.isVisible(form.$('label:contains(Foo)')); assert.isNotVisible(form.$('.o_field_widget[name=bar]')); assert.isNotVisible(form.$('label:contains(Bar)')); await testUtils.form.clickEdit(form); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable', 'form should be in readonly mode'); assert.isNotVisible(form.$('.o_field_widget[name=foo]')); assert.isNotVisible(form.$('label:contains(Foo)')); assert.isVisible(form.$('.o_field_widget[name=bar]')); assert.isVisible(form.$('label:contains(Bar)')); form.destroy(); }); QUnit.test('oe_read_only className is handled in list views', async function (assert) { assert.expect(7); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly', 'form should be in readonly mode'); assert.isVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'), 'display_name cell should be visible in readonly mode'); await testUtils.form.clickEdit(form); assert.strictEqual(form.el.querySelector('th[data-name="foo"]').style.width, '100%', 'As the only visible char field, "foo" should take 100% of the remaining space'); assert.strictEqual(form.el.querySelector('th.oe_read_only').style.width, '0px', '"oe_read_only" in edit mode should have a 0px width'); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable', 'form should be in edit mode'); assert.isNotVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'), 'display_name cell should not be visible in edit mode'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.owlCompatibilityExtraNextTick(); assert.hasClass(form.$('.o_legacy_form_view .o_legacy_list_view tbody tr:first input[name="display_name"]'), 'oe_read_only', 'display_name input should have oe_read_only class'); form.destroy(); }); QUnit.test('oe_edit_only className is handled in list views', async function (assert) { assert.expect(5); var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_readonly', 'form should be in readonly mode'); assert.isNotVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'), 'display_name cell should not be visible in readonly mode'); await testUtils.form.clickEdit(form); assert.hasClass(form.$('.o_legacy_form_view'), 'o_form_editable', 'form should be in edit mode'); assert.isVisible(form.$('.o_field_one2many .o_legacy_list_view thead th[data-name="display_name"]'), 'display_name cell should be visible in edit mode'); await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); await testUtils.owlCompatibilityExtraNextTick(); assert.hasClass(form.$('.o_legacy_form_view .o_legacy_list_view tbody tr:first input[name="display_name"]'), 'oe_edit_only', 'display_name input should have oe_edit_only class'); form.destroy(); }); QUnit.test('*_view_ref in context are passed correctly', async function (assert) { var done = assert.async(); assert.expect(3); createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', res_id: 1, intercepts: { load_views: function (event) { var context = event.data.context; assert.strictEqual(context.tree_view_ref, 'module.tree_view_ref', "context should contain tree_view_ref"); event.data.on_success(); } }, viewOptions: { context: {some_context: false}, }, mockRPC: function (route, args) { if (args.method === 'read') { assert.strictEqual('some_context' in args.kwargs.context && !args.kwargs.context.some_context, true, "the context should have been set"); } return this._super.apply(this, arguments); }, }).then(async function (form) { // reload to check that the record's context hasn't been modified await form.reload(); form.destroy(); done(); }); }); QUnit.test('non inline subview and create=0 in action context', async function (assert) { // the create=0 should apply on the main view (form), but not on subviews assert.expect(2); const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
', archs: { "product,false,kanban": `
`, }, res_id: 1, viewOptions: { context: {create: false}, mode: 'edit', }, }); assert.containsNone(form, '.o_form_button_create'); assert.containsOnce(form, '.o-kanban-button-new'); form.destroy(); }); QUnit.test('readonly fields with modifiers may be saved', async function (assert) { // the readonly property on the field description only applies on view, // this is not a DB constraint. It should be seen as a default value, // that may be overridden in views, for example with modifiers. So // basically, a field defined as readonly may be edited. assert.expect(3); this.data.partner.fields.foo.readonly = true; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1], {foo: 'New foo value'}, "the new value should be saved"); } return this._super.apply(this, arguments); }, }); // bar being set to true, foo shouldn't be readonly and thus its value // could be saved, even if in its field description it is readonly await testUtils.form.clickEdit(form); assert.containsOnce(form, 'input[name="foo"]', "foo field should be editable"); await testUtils.fields.editInput(form.$('input[name="foo"]'), 'New foo value'); await testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_field_widget[name=foo]').text(), 'New foo value', "new value for foo field should have been saved"); form.destroy(); }); QUnit.test('readonly set by modifier do not break many2many_tags', async function (assert) { assert.expect(0); this.data.partner.onchanges = { bar: function (obj) { obj.timmy = [[6, false, [12]]]; }, }; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
', res_id: 5, }); await testUtils.form.clickEdit(form); await testUtils.dom.click(form.$('.o_field_widget[name=bar] input')); form.destroy(); }); QUnit.test('check if id and active_id are defined', async function (assert) { assert.expect(2); let checkOnchange = false; var form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', archs: { "partner,false,form": '
' }, mockRPC: function (route, args) { if (args.method === 'onchange' && checkOnchange) { assert.strictEqual(args.kwargs.context.current_id, false, "current_id should be false"); assert.strictEqual(args.kwargs.context.default_trululu, false, "default_trululu should be false"); } return this._super.apply(this, arguments); }, }); checkOnchange = true; await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('modifiers are considered on multiple