From 09b4668c6bbd845e08b8f84cc9538a012c1bdf74 Mon Sep 17 00:00:00 2001 From: Jorge Pinna Puissant Date: Fri, 25 Feb 2022 08:46:16 +0000 Subject: [PATCH] [REF] *: adapt code to owl2 with fine-grained reactivity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On most of the cases, the fine-grained reactivity is bypassed by: - Setting the optional `deep` argument as `true` on the renderer function. This will force a render to update all child components. - Changing some objects from reactive to non-reactive using `toRaw` function. Part-of: odoo/odoo#84314 Co-authored-by: Géry Debongnie --- .../tests/base_automation_error_dialog.js | 8 ++++++-- .../widgets/form_renderer/form_renderer.js | 7 +++++++ .../static/src/entry/chrome_adapter.js | 2 +- addons/point_of_sale/static/src/js/Chrome.js | 8 +++++--- .../ClientListScreen/ClientDetailsEdit.js | 2 +- .../ClientListScreen/ClientListScreen.js | 14 +++++++------- .../js/Screens/PaymentScreen/PaymentScreen.js | 10 +++++----- .../Screens/ProductScreen/ProductsWidget.js | 2 +- .../legacy/js/control_panel/control_panel.js | 15 ++++++++++++--- .../control_panel_model_extension.js | 3 ++- .../src/legacy/js/control_panel/search_bar.js | 4 ++-- .../src/legacy/js/fields/field_wrapper.js | 4 ++-- .../static/src/legacy/js/owl_compatibility.js | 10 +++++++--- .../src/search/control_panel/control_panel.js | 5 ++++- .../search/control_panel/control_panel.xml | 5 +---- addons/web/static/src/search/pager_hook.js | 8 ++++++-- addons/web/static/src/search/search_model.js | 4 ++-- .../src/search/with_search/with_search.js | 4 ++-- addons/web/static/src/views/view.js | 4 ++-- .../web/static/tests/search/control_panel.js | 8 +++----- .../static/tests/search/pager_hook_tests.js | 19 ++++++++++++++++++- 21 files changed, 96 insertions(+), 50 deletions(-) diff --git a/addons/base_automation/static/tests/base_automation_error_dialog.js b/addons/base_automation/static/tests/base_automation_error_dialog.js index be88ab2071d..5dfb772a0a3 100644 --- a/addons/base_automation/static/tests/base_automation_error_dialog.js +++ b/addons/base_automation/static/tests/base_automation_error_dialog.js @@ -16,7 +16,7 @@ import { BaseAutomationErrorDialog } from "../src/js/base_automation_error_dialo import { patchWithCleanup,getFixture, mount, nextTick } from "@web/../tests/helpers/utils"; import { DialogContainer } from "@web/core/dialog/dialog_container"; -const { onMounted } = owl; +const { onMounted, toRaw } = owl; const serviceRegistry = registry.category("services"); @@ -72,7 +72,11 @@ QUnit.module("base_automation", {}, function () { patchWithCleanup(BaseAutomationErrorDialog.prototype, { setup() { - assert.equal(this.props.data.context, errorContext, "Received the correct error context"); + assert.equal( + toRaw(this.props.data.context), + errorContext, + "Received the correct error context" + ); this._super(); }, }); diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer.js b/addons/mail/static/src/widgets/form_renderer/form_renderer.js index 89868f8ee16..993640de1f5 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer.js @@ -155,6 +155,13 @@ FormRenderer.include({ * @override */ _updateView() { + /** + * The chatter is detached before it's removed on the super._updateView function, + * this is done to avoid losing the event handlers. + */ + if (this._hasChatter()) { + this._chatterContainerTarget.remove(); + } this._super(...arguments); if (this._hasChatter()) { this.$chatterContainerHook.replaceWith(this._chatterContainerTarget); diff --git a/addons/point_of_sale/static/src/entry/chrome_adapter.js b/addons/point_of_sale/static/src/entry/chrome_adapter.js index bc5853144e9..f31874fbeae 100644 --- a/addons/point_of_sale/static/src/entry/chrome_adapter.js +++ b/addons/point_of_sale/static/src/entry/chrome_adapter.js @@ -49,7 +49,7 @@ export class ChromeAdapter extends Component { const updateUI = debounce(() => { if (this.env.isMobile !== currentIsMobile) { currentIsMobile = this.env.isMobile; - this.render(); + this.render(true); } }, 15); useExternalListener(window, "resize", updateUI); diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js index 9eaa93f02fc..f86cc2baa1e 100644 --- a/addons/point_of_sale/static/src/js/Chrome.js +++ b/addons/point_of_sale/static/src/js/Chrome.js @@ -10,7 +10,7 @@ odoo.define('point_of_sale.Chrome', function(require) { const PopupControllerMixin = require('point_of_sale.PopupControllerMixin'); const Registries = require('point_of_sale.Registries'); const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen'); - const { identifyError } = require('point_of_sale.utils'); + const { identifyError, batched } = require('point_of_sale.utils'); const { odooExceptionTitleMap } = require("@web/core/errors/error_dialogs"); const { ConnectionLostError, ConnectionAbortedError, RPCError } = require('@web/core/network/rpc_service'); const { useBus } = require("@web/core/utils/hooks"); @@ -26,6 +26,7 @@ odoo.define('point_of_sale.Chrome', function(require) { useRef, useState, useSubEnv, + reactive, } = owl; /** @@ -74,7 +75,8 @@ odoo.define('point_of_sale.Chrome', function(require) { this.previous_touch_y_coordinate = -1; - useSubEnv({ pos: useState(this.env.pos) }); + const pos = reactive(this.env.pos, batched(() => this.render(true))) + useSubEnv({ pos }); onMounted(() => { // remove default webclient handlers that induce click delay @@ -200,7 +202,7 @@ odoo.define('point_of_sale.Chrome', function(require) { this.env.pos.loadPartnersBackground(); } if (this.env.pos.config.limited_products_loading && this.env.pos.config.product_load_background) { - this.env.pos.loadProductsBackground().then(() => this.render()); + this.env.pos.loadProductsBackground().then(() => this.render(true)); } } diff --git a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js index 89afc6be087..1eacd4404fc 100644 --- a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js +++ b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js @@ -78,7 +78,7 @@ odoo.define('point_of_sale.ClientDetailsEdit', function(require) { const resizedImage = await this._resizeImage(loadedImage, 800, 600); this.changes.image_1920 = resizedImage.toDataURL(); // Rerender to reflect the changes in the screen - this.render(); + this.render(true); } } } diff --git a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js index ae21fc4b1f7..0f64aac56ed 100644 --- a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js @@ -56,7 +56,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { back() { if(this.state.detailIsShown) { this.state.detailIsShown = false; - this.render(); + this.render(true); } else { this.props.resolve({ confirmed: false, payload: false }); this.trigger('close-temp-screen'); @@ -110,7 +110,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { this.state.selectedClient = clients[0]; this.clickNext(); } else { - this.render(); + this.render(true); } } clickClient(partner) { @@ -119,14 +119,14 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { } else { this.state.selectedClient = partner; } - this.render(); + this.render(true); } editClient() { this.state.editModeProps = { partner: this.state.selectedClient, }; this.state.detailIsShown = true; - this.render(); + this.render(true); } clickNext() { this.state.selectedClient = this.nextButton.command === 'set' ? this.state.selectedClient : null; @@ -142,7 +142,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { partner: this.state.selectedClient, }; } - this.render(); + this.render(true); } async saveChanges(event) { try { @@ -154,7 +154,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { await this.env.pos.load_new_partners(); this.state.selectedClient = this.env.pos.db.get_partner_by_id(partnerId); this.state.detailIsShown = false; - this.render(); + this.render(true); } catch (error) { if (isConnectionError(error)) { await this.showPopup('OfflineErrorPopup', { @@ -175,7 +175,7 @@ odoo.define('point_of_sale.ClientListScreen', function(require) { body: this.env._t('No customer found'), }); } - this.render(); + this.render(true); } async getNewClient() { var domain = []; diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index a33ef5d66a6..6f06c953e47 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -96,7 +96,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { toggleIsToInvoice() { // click_invoice this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice()); - this.render(); + this.render(true); } openCashbox() { this.env.proxy.printer.open_cashbox(); @@ -120,7 +120,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { toggleIsToShip() { // click_ship this.currentOrder.set_to_ship(!this.currentOrder.is_to_ship()); - this.render(); + this.render(true); } deletePaymentLine(event) { var self = this; @@ -135,13 +135,13 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid).then(function() { self.currentOrder.remove_paymentline(line); NumberBuffer.reset(); - self.render(); + self.render(true); }) } else if (line.get_payment_status() !== 'waitingCancel') { this.currentOrder.remove_paymentline(line); NumberBuffer.reset(); - this.render(); + this.render(true); } } selectPaymentLine(event) { @@ -149,7 +149,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { const line = this.paymentLines.find((line) => line.cid === cid); this.currentOrder.select_paymentline(line); NumberBuffer.reset(); - this.render(); + this.render(true); } async validateOrder(isForceValidate) { if(this.env.pos.config.cash_rounding) { diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js index 10f2cdb859d..bff9350d8d0 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js @@ -82,7 +82,7 @@ odoo.define('point_of_sale.ProductsWidget', function(require) { this.state.searchWord = ''; } _updateProductList(event) { - this.render(); + this.render(true); this.trigger('switch-category', 0); } } diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel.js b/addons/web/static/src/legacy/js/control_panel/control_panel.js index 5b66c659d18..119f6074c63 100644 --- a/addons/web/static/src/legacy/js/control_panel/control_panel.js +++ b/addons/web/static/src/legacy/js/control_panel/control_panel.js @@ -11,7 +11,16 @@ odoo.define('web.ControlPanel', function (require) { const SearchBar = require('web.SearchBar'); const { useModel } = require('web.Model'); - const { Component, onMounted, onPatched, onWillDestroy, onWillUpdateProps, useRef, useSubEnv } = owl; + const { + Component, + onMounted, + onPatched, + onWillDestroy, + onWillUpdateProps, + toRaw, + useRef, + useSubEnv, + } = owl; /** * TODO: remove this whole mechanism as soon as `cp_content` is completely removed. @@ -122,7 +131,7 @@ odoo.define('web.ControlPanel', function (require) { searchViewButtons: useRef('searchViewButtons'), }; - this.fields = this._formatFields(this.props.fields); + this.fields = this._formatFields(toRaw(this.props.fields || {})); this.sprintf = _.str.sprintf; @@ -155,7 +164,7 @@ odoo.define('web.ControlPanel', function (require) { subEnvView = nextProps.view; } if ("fields" in nextProps) { - this.fields = this._formatFields(nextProps.fields); + this.fields = this._formatFields(toRaw(nextProps.fields)); } this.additionalContent = getAdditionalContent(nextProps); }); diff --git a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js index b97b6f2fe65..7deedae7996 100644 --- a/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js +++ b/addons/web/static/src/legacy/js/control_panel/control_panel_model_extension.js @@ -13,6 +13,7 @@ odoo.define("web/static/src/js/control_panel/control_panel_model_extension.js", const FAVORITE_SHARED_GROUP = 2; const DISABLE_FAVORITE = "search_disable_custom_filters"; + const { toRaw } = owl; /** * Control panel model * @@ -158,7 +159,7 @@ odoo.define("web/static/src/js/control_panel/control_panel_model_extension.js", this.actionContext = Object.assign({}, this.config.context); this.searchMenuTypes = this.config.searchMenuTypes || []; this.favoriteFilters = this.config.favoriteFilters || []; - this.fields = this.config.fields || {}; + this.fields = toRaw(this.config.fields || {}); this.searchDefaults = {}; for (const key in this.actionContext) { const match = /^search_default_(.*)$/.exec(key); diff --git a/addons/web/static/src/legacy/js/control_panel/search_bar.js b/addons/web/static/src/legacy/js/control_panel/search_bar.js index ac06f92852b..2f43b34b190 100644 --- a/addons/web/static/src/legacy/js/control_panel/search_bar.js +++ b/addons/web/static/src/legacy/js/control_panel/search_bar.js @@ -7,7 +7,7 @@ odoo.define('web.SearchBar', function (require) { const { useModel } = require('web.Model'); const { fuzzyTest } = require('@web/core/utils/search'); - const { Component, onMounted, onWillUnmount, useExternalListener, useState } = owl; + const { Component, onMounted, onWillUnmount, toRaw, useExternalListener, useState } = owl; const CHAR_FIELDS = ['char', 'html', 'many2many', 'many2one', 'one2many', 'text']; let sourceId = 0; @@ -91,7 +91,7 @@ odoo.define('web.SearchBar', function (require) { * @returns {Object} */ _createSource(filter) { - const field = this.props.fields[filter.fieldName]; + const field = toRaw(this.props.fields)[filter.fieldName]; const type = field.type === "reference" ? "char" : field.type; const source = { active: true, diff --git a/addons/web/static/src/legacy/js/fields/field_wrapper.js b/addons/web/static/src/legacy/js/fields/field_wrapper.js index 4f750ac516f..797bd8821b1 100644 --- a/addons/web/static/src/legacy/js/fields/field_wrapper.js +++ b/addons/web/static/src/legacy/js/fields/field_wrapper.js @@ -4,7 +4,7 @@ odoo.define('web.FieldWrapper', function (require) { const { ComponentWrapper } = require('web.OwlCompatibility'); const field_utils = require('web.field_utils'); - const { App } = owl; + const { App, reactive } = owl; /** * This file defines the FieldWrapper component, an extension of ComponentWrapper, @@ -178,7 +178,7 @@ odoo.define('web.FieldWrapper', function (require) { const fieldsInfo = this.props.record.fieldsInfo[viewType]; fieldsInfo[this.props.fieldName].modifiersValue = modifiers || {}; } - this.componentRef.comp.props = this.props; + this.componentRef.comp.props = reactive(this.props); } } diff --git a/addons/web/static/src/legacy/js/owl_compatibility.js b/addons/web/static/src/legacy/js/owl_compatibility.js index 4e4cefb411e..5288b691d70 100644 --- a/addons/web/static/src/legacy/js/owl_compatibility.js +++ b/addons/web/static/src/legacy/js/owl_compatibility.js @@ -596,11 +596,15 @@ odoo.define('web.OwlCompatibility', function (require) { if (options) { this.mountOptions = options; } - if (this.status === "unmounted") { + const remount = this.status === "unmounted"; + if (remount) { prepareForRemount(this.node); } this.status = "willMount"; const prom = this.app.mountNode(this.node, this.target, this.mountOptions); + if (remount) { + this.node.fiber.deep = true; + } prepareForFinish(this.node); await prom; if (document.contains(this.target)) { @@ -621,7 +625,7 @@ odoo.define('web.OwlCompatibility', function (require) { render() { if (this.renderProm) { - this.node.render(); + this.node.render(true); return this.renderProm; } this.renderProm = new Promise((resolve, reject) => { @@ -632,7 +636,7 @@ odoo.define('web.OwlCompatibility', function (require) { this.renderResolve = null; this.renderReject = null; }); - this.node.render(); + this.node.render(true); return this.renderProm; } diff --git a/addons/web/static/src/search/control_panel/control_panel.js b/addons/web/static/src/search/control_panel/control_panel.js index 552146c5ba3..65ea3c45a4c 100644 --- a/addons/web/static/src/search/control_panel/control_panel.js +++ b/addons/web/static/src/search/control_panel/control_panel.js @@ -8,7 +8,7 @@ import { FilterMenu } from "../filter_menu/filter_menu"; import { GroupByMenu } from "../group_by_menu/group_by_menu"; import { SearchBar } from "../search_bar/search_bar"; -const { Component } = owl; +const { Component, useState } = owl; const MAPPING = { filter: FilterMenu, @@ -20,6 +20,9 @@ const MAPPING = { export class ControlPanel extends Component { setup() { this.actionService = useService("action"); + this.pagerProps = this.env.config.pagerProps + ? useState(this.env.config.pagerProps) + : undefined; } /** diff --git a/addons/web/static/src/search/control_panel/control_panel.xml b/addons/web/static/src/search/control_panel/control_panel.xml index ac8b1382c45..2882a20c2ce 100644 --- a/addons/web/static/src/search/control_panel/control_panel.xml +++ b/addons/web/static/src/search/control_panel/control_panel.xml @@ -29,10 +29,7 @@ diff --git a/addons/web/static/src/search/pager_hook.js b/addons/web/static/src/search/pager_hook.js index 8756d5aa2af..b0f25d8ff36 100644 --- a/addons/web/static/src/search/pager_hook.js +++ b/addons/web/static/src/search/pager_hook.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -const { useEnv, useChildSubEnv } = owl; +const { useEnv, useChildSubEnv, useState, onWillRender } = owl; /** * @typedef PagerUpdateParams @@ -23,11 +23,15 @@ const { useEnv, useChildSubEnv } = owl; */ export function usePager(getProps) { const env = useEnv(); + const pagerState = useState(getProps()); useChildSubEnv({ config: { ...env.config, - getPagerProps: getProps, + pagerProps: pagerState, }, }); + onWillRender(() => { + Object.assign(pagerState, getProps()); + }); } diff --git a/addons/web/static/src/search/search_model.js b/addons/web/static/src/search/search_model.js index 968043e1633..3d58a856eb4 100644 --- a/addons/web/static/src/search/search_model.js +++ b/addons/web/static/src/search/search_model.js @@ -17,7 +17,7 @@ import { } from "./utils/dates"; import { FACET_ICONS } from "./utils/misc"; -const { EventBus } = owl; +const { EventBus, toRaw } = owl; const { DateTime } = luxon; /** @@ -215,7 +215,7 @@ export class SearchModel extends EventBus { const { comparison, context, domain, groupBy, orderBy } = config; this.globalComparison = comparison; - this.globalContext = context || {}; + this.globalContext = toRaw(context || {}); this.globalDomain = domain || []; this.globalGroupBy = groupBy || []; this.globalOrderBy = orderBy || []; diff --git a/addons/web/static/src/search/with_search/with_search.js b/addons/web/static/src/search/with_search/with_search.js index 66a275dcfa1..60fb360eb54 100644 --- a/addons/web/static/src/search/with_search/with_search.js +++ b/addons/web/static/src/search/with_search/with_search.js @@ -4,7 +4,7 @@ import { useBus, useService } from "@web/core/utils/hooks"; import { SearchModel } from "@web/search/search_model"; import { CallbackRecorder, useSetupAction } from "@web/webclient/actions/action_hook"; -const { Component, onWillStart, onWillUpdateProps, useSubEnv, useChildSubEnv } = owl; +const { Component, onWillStart, onWillUpdateProps, toRaw, useSubEnv } = owl; export const SEARCH_KEYS = ["comparison", "context", "domain", "groupBy", "orderBy"]; const OTHER_SEARCH_KEYS = ["irFilters", "searchViewArch", "searchViewFields", "searchViewId"]; @@ -36,7 +36,7 @@ export class WithSearch extends Component { }); onWillStart(async () => { - const config = { ...this.props }; + const config = { ...toRaw(this.props) }; if (config.globalState && config.globalState.searchModel) { config.state = JSON.parse(config.globalState.searchModel); delete config.globalState; diff --git a/addons/web/static/src/views/view.js b/addons/web/static/src/views/view.js index 782a3b735f0..fd4efbd80c8 100644 --- a/addons/web/static/src/views/view.js +++ b/addons/web/static/src/views/view.js @@ -9,7 +9,7 @@ import { WithSearch } from "@web/search/with_search/with_search"; import { useActionLinks } from "@web/views/helpers/view_hook"; import { extractLayoutComponents } from "@web/views/layout"; -const { Component, onWillUpdateProps, onWillStart, useSubEnv } = owl; +const { Component, onWillUpdateProps, onWillStart, toRaw, useSubEnv } = owl; const viewRegistry = registry.category("views"); /** @typedef {Object} Config @@ -282,7 +282,7 @@ export class View extends Component { // prepare the WithSearch component props this.withSearchProps = { - ...this.props, + ...toRaw(this.props), Component: ViewClass, componentProps: viewProps, }; diff --git a/addons/web/static/tests/search/control_panel.js b/addons/web/static/tests/search/control_panel.js index 13a980646d7..2861d2bfeb8 100644 --- a/addons/web/static/tests/search/control_panel.js +++ b/addons/web/static/tests/search/control_panel.js @@ -107,7 +107,7 @@ QUnit.module("Search", (hooks) => { }); QUnit.test("pager", async (assert) => { - let pagerInfo = { + let pagerProps = { offset: 0, limit: 10, total: 50, @@ -119,15 +119,13 @@ QUnit.module("Search", (hooks) => { resModel: "foo", Component: ControlPanel, config: { - getPagerProps() { - return pagerInfo; - }, + pagerProps: pagerProps, }, searchMenuTypes: [], }); assert.containsOnce(controlPanel.el, ".o_pager"); - pagerInfo.total = 0; + pagerProps.total = 0; controlPanel.render(); await nextTick(); assert.containsNone(controlPanel.el, ".o_pager"); diff --git a/addons/web/static/tests/search/pager_hook_tests.js b/addons/web/static/tests/search/pager_hook_tests.js index 773852e0ba4..478fad93573 100644 --- a/addons/web/static/tests/search/pager_hook_tests.js +++ b/addons/web/static/tests/search/pager_hook_tests.js @@ -2,7 +2,7 @@ import { ControlPanel } from "@web/search/control_panel/control_panel"; import { usePager } from "@web/search/pager_hook"; -import { click } from "../helpers/utils"; +import { click, nextTick } from "../helpers/utils"; import { makeWithSearch, setupControlPanelServiceRegistry } from "./helpers"; const { Component, useState, xml } = owl; @@ -110,5 +110,22 @@ QUnit.module("Search", (hooks) => { offset: 10, limit: 10, }); + + comp.state.offset = 20; + await nextTick(); + + assert.containsOnce(comp, ".o_pager"); + assert.strictEqual( + comp.el.querySelector(`.o_pager_counter .o_pager_value`).textContent.trim(), + "21-30" + ); + assert.strictEqual( + comp.el.querySelector(`.o_pager_counter span.o_pager_limit`).textContent.trim(), + "50" + ); + assert.deepEqual(comp.state, { + offset: 20, + limit: 10, + }); }); });