summaryrefslogtreecommitdiff
path: root/addons/web/static/tests
diff options
context:
space:
mode:
authorAaron Bohy <aab@odoo.com>2020-11-10 14:52:14 +0000
committerAaron Bohy <aab@odoo.com>2020-11-18 08:39:40 +0000
commite8e64f75606f1501a8be7b0bb418a296cfd8be61 (patch)
treed80b4e190ccb9f0ba2a1cc9ce0d15293b7567021 /addons/web/static/tests
parent64ec312778a2552ebb3db75394c558e51a0c3f63 (diff)
[FIX] web: race condition with x2many control panel
The ControlPanel has been converted in Owl [1], and the code using it has been adapted accordingly. However, in the FieldX2Many, we didn't properly wait for the ControlPanel to be updated (an update of the ControlPanel was synchronous before owl, and is now async, like every Owl renderings, as it waits for the nextAnimationFrame). As a consequence, we might have tricky issues because the mounted hook of the control panel might be called multiple times for a single call to willUnmount later on. In mobile, we bind a global event handler (on scroll) in mounted, and unbind it in willUnmount, so we had leftover event handlers, that crashed when called after the ControlPanel was destroyed. Note that even if the issue popped in mobile, calling mounted on already mounted Components isn't a good idea, and this should be fixed anyway. The issue could be reproduced for instance in FieldService (with collaborative pads activated in Project), in mobile, by opening a task in Edit mode. Then, you might get a traceback by scrolling after having discarded the edition Here is a description of what technically happened: - when clicking on Edit, all widgets (including the FieldX2Many are destroyed and re-instantiated in 'edit' mode). - the pad widget directly triggers a field_changed event which causes a reset of the FieldX2Many (i.e. 'render' is called again) - the FieldX2Many detects that it already has a renderer (and a ControlPanel) so it updates them - it first updates the renderer, and when it's done, it updates the ControlPanel BUT doesn't wait for its promise, so the promise returned by that call to 'render' in FieldX2Many is resolved before the ControlPanel is actually updated - note that at this point, all thoses new widgets are not in the DOM yet - when all widgets are ready, the renderer patches the view (i.e. the former content is removed from the DOM, and the new one is attached into the DOM). As soon as this is done, the renderer calls 'on_attach_callback' on its children, including the FieldX2Many, which leads to a call to 'mounted' on the CP. - then, just before the nextAnimationFrame, Owl complete the rendering of the CP, and detects that it is now in the DOM (it wasn't at the beginning), so 'mounted' is called a second time, will cause the issue described above. This commit fixes the issue by properly waiting for the CP to be rendered in the FieldX2Many. However, this required on cascade changes: - Form view renderings with a FieldX2Many are now *really* async (+- 16ms), meaning that the user can easily trigger concurrent renderings by, e.g. clicking quickly several times on 'Edit', 'Save' or 'Discard'. Concurrent renderings are properly handled so to prevent this from happening, we disable the buttons and re-enable them when the rendering is done (like already done in [2]) - in the FieldX2Many, '_updateControlPanel' was called at several placed, but we never waited for it. As this method was originally sync, its calls have probably been naively adapted, whereas their should have been deeply rethought (for instance, as it is async, and we need to wait for it, we don't want it to be called multiple times sequentially when something happens). This commit does that work, i.e. we clean the places where this function is called such that it is (hopefully) never called sequentially twice. To do so, we changed a bit the spec of the pager in multi page, and we also fixed a paging-related bug. In a few words, here is what we did/do when adding a new row in the bottom of a full page: - before: tweak the count in the data to fool the pager and make it think that no new record has been added (so basically, let it display something wrong) - now: temporarily increase the pager limit so that the new record is displayed on the current pager, and the pager values are correct w.r.t. the displayed records. Some tests needed to be adapted accordingly. - By waiting for the ControlPanel when updating the FieldX2Many, a bunch of QUnit tests failed. Those tests have something in common: they spawn an X2Many (list or kanban theoretically, but always list in practice) containing a FieldBoolean (only field widget of /web converted in owl). When the FieldX2Many is updated, we update the renderer (i.e. re-renderer the FieldBoolean, so we have to wait for the nextAnimationFrame), and when this is done, we update the page (again, we have to wait for the nextAnimationFrame). So basically, we have to wait for two nextAnimationFrames to see the result in the DOM. For this, we added a new test util which basically does a nextTick ('owlCompatibilityNextTick'), and called it everywhere it was necessary. When everything will be written in Owl, we could get rid of this util and its calls. [1] https://github.com/odoo/odoo/commit/fbf347498f1cc7b74ef373179b7bcae201715c24 [2] https://github.com/odoo/odoo/commit/39f08950d6e20460e3a20a5b9c33e4ddf66dce78 closes odoo/odoo#61593 Signed-off-by: Adrien Dieudonné (adr) <adr@odoo.com>
Diffstat (limited to 'addons/web/static/tests')
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2many_tests.js6
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2one_tests.js6
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_one2many_tests.js135
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js17
-rw-r--r--addons/web/static/tests/helpers/test_utils.js15
-rw-r--r--addons/web/static/tests/views/form_tests.js9
-rw-r--r--addons/web/static/tests/views/list_tests.js2
7 files changed, 172 insertions, 18 deletions
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
index d7d18dfb86b..ed5fcd7f985 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
@@ -1383,8 +1383,7 @@ QUnit.module('fields', {}, function () {
await testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
- var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)');
- assert.equal(m2m_records.length, 51,
+ assert.strictEqual(form.$('.o_data_row').length, 51,
'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold'
await testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_save'));
@@ -1411,8 +1410,7 @@ QUnit.module('fields', {}, function () {
await testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
- pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
- assert.equal(pager_limit.text(), '51',
+ assert.strictEqual(form.$('.o_data_row').length, 51,
'We should have 51 records in the m2m field');
form.destroy();
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
index 50e7ae2e6cb..e94797ff5f6 100644
--- a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
@@ -3293,13 +3293,16 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
@@ -3386,13 +3389,16 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
index 4d0055eb3b1..75346009aaf 100644
--- a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
+++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
@@ -3,6 +3,8 @@ odoo.define('web.field_one_to_many_tests', function (require) {
var AbstractField = require('web.AbstractField');
var AbstractStorageService = require('web.AbstractStorageService');
+const ControlPanel = require('web.ControlPanel');
+const fieldRegistry = require('web.field_registry');
var FormView = require('web.FormView');
var KanbanRecord = require('web.KanbanRecord');
var ListRenderer = require('web.ListRenderer');
@@ -2557,7 +2559,7 @@ QUnit.module('fields', {}, function () {
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.click(form.$el);
- assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 5',
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
"pager should display the correct total");
form.destroy();
});
@@ -2869,7 +2871,7 @@ QUnit.module('fields', {}, function () {
});
QUnit.test('editable one2many list, adding line when only one page', async function (assert) {
- assert.expect(1);
+ assert.expect(5);
this.data.partner.records[0].turtles = [1, 2, 3];
var form = await createView({
@@ -2892,6 +2894,15 @@ QUnit.module('fields', {}, function () {
// the record currently being added should not count in the pager
assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+ // unselect the row
+ await testUtils.dom.click(form.$el);
+ assert.containsNone(form, '.o_selected_row');
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_widget[name=turtles] .o_pager');
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text(), "1-3 / 4");
+
form.destroy();
});
@@ -2993,13 +3004,13 @@ QUnit.module('fields', {}, function () {
// see a confirm dialog
await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
- assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 4',
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
"pager should still display the correct total");
// click on cancel
await testUtils.dom.click($('.modal .modal-footer .btn-secondary'));
- assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 4',
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
"pager should again display the correct total");
assert.containsOnce(form, '.o_field_one2many input.o_field_invalid',
"there should be an invalid input in the one2many");
@@ -3007,7 +3018,7 @@ QUnit.module('fields', {}, function () {
});
QUnit.test('editable one2many list, adding, discarding, and pager', async function (assert) {
- assert.expect(2);
+ assert.expect(4);
this.data.partner.records[0].turtles = [1];
@@ -3025,15 +3036,15 @@ QUnit.module('fields', {}, function () {
res_id: 1,
});
- // add a 4 records record (to make the pager appear)
+ // add 4 records (to have more records than the limit)
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
- // go on next page
- await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+ assert.containsN(form, 'tr.o_data_row', 5);
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
// discard
await testUtils.form.clickDiscard(form);
@@ -4541,6 +4552,12 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have an x2many field with a boolean field
+ // (written in owl), so when we add a line, we sequentially render the list itself
+ // (including the boolean field), so we have to wait for the next animation frame, and
+ // then we render the control panel (also in owl), so we have to wait again for the
+ // next animation frame
+ await testUtils.owlCompatibilityNextTick();
form.destroy();
});
@@ -4571,6 +4588,12 @@ QUnit.module('fields', {}, function () {
},
});
await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have an x2many field with a boolean field
+ // (written in owl), so when we add a line, we sequentially render the list itself
+ // (including the boolean field), so we have to wait for the next animation frame, and
+ // then we render the control panel (also in owl), so we have to wait again for the
+ // next animation frame
+ await testUtils.owlCompatibilityNextTick();
assert.verifySteps(['onchange', 'onchange']);
form.destroy();
});
@@ -6071,6 +6094,7 @@ QUnit.module('fields', {}, function () {
// add a new row (which is invalid at first)
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
"int_field should still be 0 (no onchange should have been done yet)");
assert.verifySteps(['load_views', 'read', 'onchange']);
@@ -6693,13 +6717,14 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
+ // add a new record page 1 (this increases the limit to 4)
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
await testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
- await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next'));
- await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next'));
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); // page 2: 4 records
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); // page 3: 2 records
- assert.containsN(form, 'tr.o_data_row', 3,
- "should have 3 data rows on the current page");
+ assert.containsN(form, 'tr.o_data_row', 2,
+ "should have 2 data rows on the current page");
form.destroy();
});
@@ -6885,7 +6910,11 @@ QUnit.module('fields', {}, function () {
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
-
+ // use of owlCompatibilityNextTick because we have a boolean field (owl) inside the
+ // x2many, so an update of the x2many requires to wait for 2 animation frames: one
+ // for the list to be re-rendered (with the boolean field) and one for the control
+ // panel.
+ await testUtils.owlCompatibilityNextTick();
await testUtils.dom.click(form.$('.o_field_widget[name=bar] input'));
assert.notOk(form.$('.o_field_widget[name=bar] input').prop('checked'),
"the checkbox should be unticked");
@@ -8430,13 +8459,16 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
@@ -8611,13 +8643,16 @@ QUnit.module('fields', {}, function () {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
@@ -9110,6 +9145,12 @@ QUnit.module('fields', {}, function () {
// resolve the name_create to trigger the onchange, and the reset of p
prom.resolve();
await testUtils.nextTick();
+ // use of owlCompatibilityNextTick because we have two sequential updates of the
+ // fieldX2Many: one because of the onchange, and one because of the click on add a line.
+ // As an update requires an update of the ControlPanel, which is an Owl Component, and
+ // waits for it, we need to wait for two animation frames before seeing the new line in
+ // the DOM
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, '.o_data_row');
assert.hasClass(form.$('.o_data_row'), 'o_selected_row');
@@ -9331,6 +9372,74 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
+
+ QUnit.test('mounted is called only once for x2many control panel', async function (assert) {
+ // This test could be removed as soon as the field widgets will be converted in owl.
+ // It comes with a fix for a bug that occurred because in some circonstances, 'mounted'
+ // is called twice for the x2many control panel.
+ // Specifically, this occurs when there is 'pad' widget in the form view, because this
+ // widget does a 'setValue' in its 'start', which thus resets the field x2many.
+ assert.expect(5);
+
+ const PadLikeWidget = fieldRegistry.get('char').extend({
+ start() {
+ this._setValue("some value");
+ }
+ });
+ fieldRegistry.add('pad_like', PadLikeWidget);
+
+ let resolveCP;
+ const prom = new Promise(r => {
+ resolveCP = r;
+ });
+ ControlPanel.patch('cp_patch_mock', T =>
+ class extends T {
+ constructor() {
+ super(...arguments);
+ owl.hooks.onMounted(() => {
+ assert.step('mounted');
+ });
+ owl.hooks.onWillUnmount(() => {
+ assert.step('willUnmount');
+ });
+ }
+ async update() {
+ // the issue is a race condition, so we manually delay the update to turn it deterministic
+ await prom;
+ super.update(...arguments);
+ }
+ }
+ );
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="foo" widget="pad_like"/>
+ <field name="p">
+ <tree><field name="display_name"/></tree>
+ </field>
+ </form>`,
+ viewOptions: {
+ withControlPanel: false, // s.t. there is only one CP: the one of the x2many
+ },
+ });
+
+ assert.verifySteps(['mounted']);
+
+ resolveCP();
+ await testUtils.nextTick();
+
+ assert.verifySteps([]);
+
+ ControlPanel.unpatch('cp_patch_mock');
+ delete fieldRegistry.map.pad_like;
+ form.destroy();
+
+ assert.verifySteps(["willUnmount"]);
+ });
});
});
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 6ef0e402562..f93e7c35ace 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -474,9 +474,11 @@ QUnit.module('relational_fields', {
await testUtils.form.clickEdit(form);
// add a line (virtual record)
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
await testUtils.fields.editInput(form.$('.o_input'), 'pi');
// delete the line above it
await testUtils.dom.click(form.$('.o_list_record_remove').first());
+ await testUtils.owlCompatibilityNextTick();
// the next line should be displayed below the newly added one
assert.strictEqual(form.$('.o_data_row').length, 2, "should have 2 records");
assert.strictEqual(form.$('.o_data_row .o_data_cell:first-child').text(), 'pikawa',
@@ -2535,6 +2537,7 @@ QUnit.module('relational_fields', {
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
await testUtils.fields.many2one.searchAndClickItem('product_id',
{selector: '.modal', search: 'new record'});
@@ -3070,13 +3073,16 @@ QUnit.module('relational_fields', {
await testUtils.form.clickEdit(form);
await testUtils.fields.many2one.clickOpenDropdown("product_id");
await testUtils.fields.many2one.clickHighlightedItem("product_id");
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
@@ -3164,13 +3170,16 @@ QUnit.module('relational_fields', {
await testUtils.form.clickEdit(form);
await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
await testUtils.fields.many2one.clickHighlightedItem("product_id");
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column when the product_id is set");
await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
'', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
"should be 2 columns in the one2many when product_id is not set");
await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
"should be 1 column after the value change");
form.destroy();
@@ -3230,6 +3239,13 @@ QUnit.module('relational_fields', {
assert.containsN(form.$('.o_field_one2many'), 'div.o_optional_columns div.dropdown-item:visible', 2,
"dropdown is still open");
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because the x2many field is reset, meaning that
+ // 1) its list renderer is updated (updateState is called): this is async and as it
+ // contains a FieldBoolean, which is written in Owl, it completes in the nextAnimationFrame
+ // 2) when this is done, the control panel is updated: as it is written in owl, this is
+ // done in the nextAnimationFrame
+ // -> we need to wait for 2 nextAnimationFrame to ensure that everything is fine
+ await testUtils.owlCompatibilityNextTick();
assert.containsN(form.$('.o_field_one2many'), 'div.o_optional_columns div.dropdown-item:visible', 0,
"dropdown is closed");
var $selectedRow = form.$('.o_field_one2many tr.o_selected_row');
@@ -3329,6 +3345,7 @@ QUnit.module('relational_fields', {
which: $.ui.keyCode.TAB,
keyCode: $.ui.keyCode.TAB,
}));
+ await testUtils.owlCompatibilityNextTick();
await testUtils.dom.click(document.activeElement);
assert.strictEqual(assert.strictEqual(form.$el.find('input[name="turtle_foo"]')[0],
document.activeElement,
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index fb9654b52cf..e998ab14de8 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -97,6 +97,20 @@ odoo.define('web.test_utils', async function (require) {
return testUtilsDom.returnAfterNextAnimationFrame();
}
+ /**
+ * Calls nextTick. While we have a hybrid implemetation (Owl + legacy), we may
+ * have situations where waiting for a single nextTick isn't enough. For instance,
+ * having a layer of Owl components, above a layer of legacy widgets, above a
+ * layer of Owl components requires two nextTick for the whole hierarchy to be
+ * rendered into the DOM. In those situation, one should use this helper, which
+ * will be removed (alongside all its calls) in the future.
+ *
+ * @returns {Promise}
+ */
+ async function owlCompatibilityNextTick() {
+ return nextTick();
+ }
+
// Loading static files cannot be properly simulated when their real content is
// really needed. This is the case for static XML files so we load them here,
// before starting the qunit test suite.
@@ -247,6 +261,7 @@ odoo.define('web.test_utils', async function (require) {
makeTestPromiseWithAssert: makeTestPromiseWithAssert,
nextMicrotaskTick: nextMicrotaskTick,
nextTick: nextTick,
+ owlCompatibilityNextTick: owlCompatibilityNextTick,
prepareTarget: testUtilsCreate.prepareTarget,
returnAfterNextAnimationFrame: testUtilsDom.returnAfterNextAnimationFrame,
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 3ad4eaec88c..510293f2bef 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -3960,7 +3960,10 @@ QUnit.module('Views', {
"Clicking out of a dirty line while editing should trigger a warning modal.");
await testUtils.dom.click($('.modal').find('.btn-primary'));
-
+ // use of owlCompatibilityNextTick 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.owlCompatibilityNextTick();
assert.strictEqual(form.$('.o_data_cell').first().text(), "first record",
"Value should have been reset to what it was before editing began.");
assert.containsOnce(form, '.o_data_row',
@@ -4494,6 +4497,7 @@ QUnit.module('Views', {
await testUtils.form.clickEdit(form);
await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
+ await testUtils.owlCompatibilityNextTick();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
@@ -4554,6 +4558,7 @@ QUnit.module('Views', {
"the initial value should be the default one");
await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger an onchange');
+ await testUtils.owlCompatibilityNextTick();
assert.strictEqual(form.$('.o_data_row td:first').text(), 'foo changed',
"onchange should have been correctly applied on field in o2m list");
@@ -5258,6 +5263,7 @@ QUnit.module('Views', {
'display_name cell should not be visible in edit mode');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
assert.hasClass(form.$('.o_form_view .o_list_view tbody tr:first input[name="display_name"]'),
'oe_read_only', 'display_name input should have oe_read_only class');
@@ -5296,6 +5302,7 @@ QUnit.module('Views', {
'display_name cell should be visible in edit mode');
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
assert.hasClass(form.$('.o_form_view .o_list_view tbody tr:first input[name="display_name"]'),
'oe_edit_only', 'display_name input should have oe_edit_only class');
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 256a998028e..29a3bcddbf4 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -6161,6 +6161,8 @@ QUnit.module('Views', {
// Press 'Tab' -> should get out of the one to many and go to the next field of the form
await testUtils.fields.triggerKeydown(form.$('.o_field_widget[name=o2m] .o_selected_row input'), 'tab');
+ // use of owlCompatibilityNextTick because the x2many control panel is updated twice
+ await testUtils.owlCompatibilityNextTick();
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"the next field should be selected");