addons-cm/website_sale_lazy_loading/tests/test_lazy_loading.py
GitHub Copilot fbd43bb459
Some checks failed
CI / lint (pull_request) Successful in 33s
CI / test (pull_request) Failing after 9m29s
CI / review (pull_request) Has been skipped
[REF] all: pass pre-commit on the whole repo
Needed for the lint job of the CI to go green. Mostly automatic hook
fixes (black, isort, prettier, pyupgrade, autoflake, whitespace). By hand:

- flake8: two unused imports, an ambiguous variable name `l`.
- pylint-odoo mandatory: manifest keys equal to their default value
  (installable, application, data, external_dependencies).
- end-of-file-fixer skips *.patch too, like trailing-whitespace: the
  upstream patches stay as git format-patch wrote them.

No behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 14:34:58 +02:00

144 lines
6.1 KiB
Python

# 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(f"{key}={value}" for key, value in params.items())
url = "/shop/lazy_products?page=%s" % page
if query:
url = f"{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")