# Copyright 2026 - Today Criptomart # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) from odoo.tests import tagged from odoo.tests.common import HttpCase PER_PAGE = 3 PRODUCT_COUNT = 7 PAGE_COUNT = 3 # ceil(7 / 3) @tagged("post_install", "-at_install") class TestLazyLoading(HttpCase): @classmethod def setUpClass(cls): super().setUpClass() cls.website = cls.env["website"].browse(1) cls.website.shop_ppg = PER_PAGE # The listing has to be predictable: only the products created here may # show up in the shop. cls.env["product.template"].search([("is_published", "=", True)]).write( {"is_published": False} ) cls.category = cls.env["product.public.category"].create({"name": "Lazy Cat"}) cls.other_category = cls.env["product.public.category"].create( {"name": "Other Cat"} ) cls.products = cls.env["product.template"].create( [ { "name": "Lazy Product %02d" % index, "list_price": 10.0 + index, "is_published": True, "public_categ_ids": [(6, 0, cls.category.ids)], } for index in range(PRODUCT_COUNT) ] ) cls.other_product = cls.env["product.template"].create( { "name": "Other Product", "list_price": 5.0, "is_published": True, "public_categ_ids": [(6, 0, cls.other_category.ids)], } ) def _get_lazy_page(self, page, **params): """Call the AJAX endpoint and return its decoded payload.""" query = "&".join("%s=%s" % (key, value) for key, value in params.items()) url = "/shop/lazy_products?page=%s" % page if query: url = "%s&%s" % (url, query) response = self.url_open(url) self.assertEqual(response.status_code, 200) return response.json() # ------------------------------------------------------------------ # /shop rendering # ------------------------------------------------------------------ def test_shop_renders_lazy_loading_block(self): """The listing carries the block driving the lazy loading.""" self.website.shop_lazy_loading = "scroll" body = self.url_open("/shop").text self.assertIn("o_wsale_lazy_loading", body) self.assertIn('data-mode="scroll"', body) self.assertIn('data-page-count="%s"' % PAGE_COUNT, body) # The pager is still served: it is the fallback without JavaScript. self.assertIn("products_pager", body) def test_shop_without_lazy_loading(self): """The standard pager is left alone when the mode is off.""" self.website.shop_lazy_loading = "off" body = self.url_open("/shop").text self.assertNotIn("o_wsale_lazy_loading", body) self.assertIn("products_pager", body) def test_shop_last_page_has_no_block(self): """Nothing can be lazy loaded from the last page.""" self.website.shop_lazy_loading = "scroll" body = self.url_open("/shop/page/%s" % PAGE_COUNT).text self.assertNotIn("o_wsale_lazy_loading", body) def test_lazy_loading_url_keeps_the_filters(self): """The AJAX URL carries the filters of the listing on screen.""" self.website.shop_lazy_loading = "scroll" body = self.url_open("/shop?search=Lazy&order=name+desc").text self.assertIn("/shop/lazy_products?", body) self.assertIn("search=Lazy", body) self.assertIn("order=name+desc", body) # ------------------------------------------------------------------ # /shop/lazy_products # ------------------------------------------------------------------ def test_lazy_page_returns_the_next_products(self): """A page holds the products of that page, and only those.""" result = self._get_lazy_page(2) self.assertEqual(result["page"], 2) self.assertEqual(result["page_count"], PAGE_COUNT) self.assertTrue(result["has_next"]) self.assertIn("oe_product", result["html"]) for product in self.products[PER_PAGE : PER_PAGE * 2]: self.assertIn(product.name, result["html"]) for product in self.products[:PER_PAGE]: self.assertNotIn(product.name, result["html"]) def test_lazy_page_last_page(self): """The last page reports that there is nothing left to load.""" result = self._get_lazy_page(PAGE_COUNT) self.assertFalse(result["has_next"]) self.assertIn(self.products[-1].name, result["html"]) def test_lazy_page_out_of_range(self): """An out of range page answers empty instead of repeating the last.""" result = self._get_lazy_page(PAGE_COUNT + 5) self.assertEqual(result["html"], "") self.assertFalse(result["has_next"]) def test_lazy_page_invalid_page(self): """A broken page number falls back to the first page.""" result = self._get_lazy_page("not-a-page") self.assertEqual(result["page"], 1) self.assertIn(self.products[0].name, result["html"]) def test_lazy_page_keeps_the_category(self): """Filtering by category is kept across the appended pages.""" result = self._get_lazy_page(2, category=self.category.id) self.assertIn(self.products[PER_PAGE].name, result["html"]) self.assertNotIn(self.other_product.name, result["html"]) def test_lazy_page_ignores_a_broken_category(self): """A category that is not an id is dropped instead of crashing.""" result = self._get_lazy_page(1, category="pwned") self.assertEqual(result["page"], 1) self.assertIn(self.products[0].name, result["html"]) def test_lazy_page_unknown_category(self): """An unknown category answers a clean 404, not a traceback.""" unknown_id = self.other_category.id + 1000 response = self.url_open("/shop/lazy_products?page=1&category=%s" % unknown_id) self.assertEqual(response.status_code, 404) self.assertEqual(response.json()["error"], "not_found")