From e7807fbad6112fd66e6e927b9bf25d1f72243db2 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 10:51:34 +0800 Subject: [PATCH 01/11] test(spp_cel_widget): check every JS import resolves on the backend test page Reproduces #551: cel_widget_tour.js imports the Odoo 17 module @web_tour/tour_service/tour_utils, which no Odoo 19 file defines, so the module loader logs a console error and every backend tour fails. Tours never run in CI (no Chrome in the test image), so the check resolves imports statically with Odoo's own transpiler. --- spp_cel_widget/tests/__init__.py | 1 + spp_cel_widget/tests/test_assets.py | 122 ++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 spp_cel_widget/tests/test_assets.py diff --git a/spp_cel_widget/tests/__init__.py b/spp_cel_widget/tests/__init__.py index 16443049a..67b4a734e 100644 --- a/spp_cel_widget/tests/__init__.py +++ b/spp_cel_widget/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_symbol_provider from . import test_controller +from . import test_assets diff --git a/spp_cel_widget/tests/test_assets.py b/spp_cel_widget/tests/test_assets.py new file mode 100644 index 000000000..9415a8006 --- /dev/null +++ b/spp_cel_widget/tests/test_assets.py @@ -0,0 +1,122 @@ +"""Tests that the module's JavaScript imports resolve in the bundles that load them. + +Odoo's module loader logs a console error for every import that no loaded file +defines, and the browser test runner fails any tour on a console error. The +test image has no Chrome, so tours never run in CI; these tests check import +resolution statically, using Odoo's own transpiler to read each file's +dependencies. + +Limitation: a module defined only in a lazy-loaded bundle is not in the +defined set, so an import of one would be reported as unresolved. +""" + +import ast +import re + +from odoo.tests import tagged +from odoo.tests.common import BaseCase, TransactionCase +from odoo.tools.js_transpiler import ( + ODOO_MODULE_RE, + is_odoo_module, + transpile_javascript, + url_to_module_path, +) +from odoo.tools.misc import file_open + +# web.webclient_bootstrap loads web.assets_web, then web.assets_tests in test mode. +BACKEND_TEST_BUNDLES = ("web.assets_web", "web.assets_tests") + +TOUR_FILE = "/spp_cel_widget/static/tests/tours/cel_widget_tour.js" + +ODOO_DEFINE_RE = re.compile(r"""odoo\.define\((['"])(?P.+?)\1,\s*(?P\[.*?\])""") + + +def module_dependencies(url, content): + """Return the module names a JS file imports, as Odoo's transpiler resolves them.""" + match = ODOO_DEFINE_RE.search(transpile_javascript(url, content)) + return ast.literal_eval(match["deps"]) + + +def defined_module_names(url, content): + """Return the module names a JS file defines when it is loaded.""" + names = set() + if is_odoo_module(url, content): + names.add(url_to_module_path(url)) + header = ODOO_MODULE_RE.match(content) + if header and header["alias"]: + names.add(header["alias"]) + # Files that call odoo.define by hand, e.g. the "@odoo/owl" wrapper in web/static/lib. + names.update(match["name"] for match in ODOO_DEFINE_RE.finditer(content)) + return names + + +class TestAssetImportHelpers(BaseCase): + """Unit tests for the helpers, so the bundle test cannot pass by matching nothing.""" + + def test_module_dependencies_resolves_named_bare_and_relative_imports(self): + content = ( + 'import {registry} from "@web/core/registry";\n' + 'import "@web_tour/tour_utils";\n' + 'import {helper} from "./sibling";\n' + ) + self.assertCountEqual( + module_dependencies("/spp_cel_widget/static/src/js/example.js", content), + ["@web/core/registry", "@web_tour/tour_utils", "@spp_cel_widget/js/sibling"], + ) + + def test_module_dependencies_reads_the_tour_file_web_tour_import(self): + with file_open(TOUR_FILE.lstrip("/")) as tour_file: + dependencies = module_dependencies(TOUR_FILE, tour_file.read()) + self.assertTrue( + any(name.startswith("@web_tour/") for name in dependencies), + f"Expected a @web_tour/ import in {TOUR_FILE}, got {dependencies}", + ) + + def test_defined_module_names_uses_the_path_and_alias(self): + content = "/** @odoo-module alias=spp_cel_widget.Example **/\nexport const x = 1;\n" + self.assertEqual( + defined_module_names("/spp_cel_widget/static/src/js/example.js", content), + {"@spp_cel_widget/js/example", "spp_cel_widget.Example"}, + ) + + def test_defined_module_names_reads_a_hand_written_define(self): + content = 'odoo.define("@odoo/owl", [], function () {\n return owl;\n});\n' + self.assertEqual( + defined_module_names("/web/static/lib/owl/odoo_module.js", content), + {"@odoo/owl"}, + ) + + +@tagged("post_install", "-at_install") +class TestCelWidgetAssetImports(TransactionCase): + """Integration test: every import in this module's JS resolves on the backend test page.""" + + def _backend_test_page_scripts(self): + scripts = {} + for bundle in BACKEND_TEST_BUNDLES: + for path, full_path, _bundle, _last_modified in self.env["ir.asset"]._get_asset_paths(bundle, {}): + if path.endswith(".js") and path not in scripts: + with file_open(full_path) as script: + scripts[path] = script.read() + return scripts + + def test_imports_resolve_on_the_backend_test_page(self): + scripts = self._backend_test_page_scripts() + defined = set().union(*(defined_module_names(url, content) for url, content in scripts.items())) + own_modules = { + url: content + for url, content in scripts.items() + if url.startswith("/spp_cel_widget/") and is_odoo_module(url, content) + } + self.assertIn(TOUR_FILE, own_modules) + + unresolved = {} + for url, content in own_modules.items(): + missing = sorted(set(module_dependencies(url, content)) - defined) + if missing: + unresolved[url] = missing + self.assertFalse( + unresolved, + f"Imports that no file on the backend test page defines (the module loader " + f"logs a console error for each, failing every tour): {unresolved}", + ) From 2763d4cad07112248af67957f848dc3bb1aa15ce Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 11:14:07 +0800 Subject: [PATCH 02/11] test(e2e): assert the backend test assets load without module loader errors One unresolvable import in any installed module's web.assets_tests makes Odoo's module loader log a console error, which fails every backend tour (#551). Test 24 opens /odoo?debug=tests on the SP-MIS stack and asserts the loader reports no errors and injects no error banner. --- e2e/tests/01-spp-starter-spmis.spec.ts | 59 +++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/e2e/tests/01-spp-starter-spmis.spec.ts b/e2e/tests/01-spp-starter-spmis.spec.ts index 02aa98328..18fe22c3c 100644 --- a/e2e/tests/01-spp-starter-spmis.spec.ts +++ b/e2e/tests/01-spp-starter-spmis.spec.ts @@ -30,11 +30,14 @@ // (200_individuals.xlsx) and confirms the pager reflects the new count // 23 - Imports 200 group registrants in bulk via the Import records wizard // (200_groups.xlsx) and confirms the pager reflects the new count +// 24 - Opens the backend with the test assets loaded (?debug=tests) and confirms Odoo's +// JavaScript module loader reports no errors, since one bad import in any installed +// module's web.assets_tests fails every backend tour // // All tests run in order and share a single browser session (test.describe.serial). // A fresh Docker stack is spun up in beforeAll so every run starts from a clean database. -import {test, expect, Page, Browser} from "@playwright/test"; +import {test, expect, Page, Browser, ConsoleMessage} from "@playwright/test"; import {resetStack} from "./helpers"; import * as path from "path"; @@ -1374,4 +1377,58 @@ test.describe.serial("OpenSPP Starter SP-MIS", () => { await logout(page); console.log("✅ Admin logged out"); }); + + test("24 - backend test assets load without JavaScript module errors", async () => { + // Odoo's module loader (web/static/src/module_loader.js) logs these console + // errors when a module imports something no loaded file defines, e.g. an + // import path from an older Odoo version. The tour runner fails on any + // console error, so one bad import in web.assets_tests breaks every tour. + const moduleLoaderError = + /needed by other modules but have not been defined|failed to load because of an error|form a dependency cycle|have unmet dependencies/; + const loaderErrors: Promise[] = []; + const onConsole = (msg: ConsoleMessage) => { + if (msg.type() === "error" && moduleLoaderError.test(msg.text())) { + // The module names are in the array argument, not in msg.text(). + loaderErrors.push( + Promise.all(msg.args().map((arg) => arg.jsonValue())).then((args) => + JSON.stringify(args) + ) + ); + } + }; + + await login(page); + console.log("✅ Logged in as admin"); + + page.on("console", onConsole); + try { + await page.goto("/odoo?debug=tests"); + await expect(page.locator(".o_main_navbar")).toBeVisible({timeout: 30_000}); + // Not "networkidle": the bus websocket worker request never finishes. + // Wait until the loader has defined web.assets_tests' first module and + // run its error check, which logs any errors synchronously. + await page.waitForFunction( + () => { + const loader = (window as any).odoo.loader; + return ( + loader.factories.has("@web/../tests/legacy/utils") && + loader.checkErrorProm === null + ); + }, + null, + {timeout: 30_000} + ); + await page.waitForLoadState("load"); + + expect(await Promise.all(loaderErrors)).toEqual([]); + // In debug mode the loader also injects this style as a red page banner. + await expect(page.locator("style.o_module_error_banner")).toHaveCount(0); + console.log("✅ Test assets loaded with no module loader errors"); + } finally { + page.off("console", onConsole); + } + + await logout(page); + console.log("✅ Admin logged out"); + }); }); From 6bd83bdc12ae397b2e8b3a7d82a76600e9ca4f50 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 11:15:33 +0800 Subject: [PATCH 03/11] fix(spp_cel_widget): import stepUtils from the Odoo 19 path @web_tour/tour_utils Odoo 19 has no @web_tour/tour_service/tour_utils module, so the tours file failed to load in web.assets_tests and the module loader's console error failed every backend tour on databases with spp_cel_widget installed (auto_install). Fixes #551. --- spp_cel_widget/__manifest__.py | 2 +- spp_cel_widget/readme/HISTORY.md | 5 +++++ spp_cel_widget/static/tests/tours/cel_widget_tour.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/spp_cel_widget/__manifest__.py b/spp_cel_widget/__manifest__.py index 63cca8f6b..942a97853 100644 --- a/spp_cel_widget/__manifest__.py +++ b/spp_cel_widget/__manifest__.py @@ -1,6 +1,6 @@ { "name": "OpenSPP CEL Expression Widget", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "category": "OpenSPP", "summary": "Reusable CEL expression editor with syntax highlighting and autocomplete", "author": "OpenSPP.org, OpenSPP", diff --git a/spp_cel_widget/readme/HISTORY.md b/spp_cel_widget/readme/HISTORY.md index 4aaf9afef..2d9530e6b 100644 --- a/spp_cel_widget/readme/HISTORY.md +++ b/spp_cel_widget/readme/HISTORY.md @@ -1,3 +1,8 @@ +### 19.0.2.0.1 + +- fix: import the tours' stepUtils from the Odoo 19 path @web_tour/tour_utils, so web.assets_tests + loads without module loader errors and no longer fails every backend tour (#551) + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_cel_widget/static/tests/tours/cel_widget_tour.js b/spp_cel_widget/static/tests/tours/cel_widget_tour.js index 885cb12f2..573f515f8 100644 --- a/spp_cel_widget/static/tests/tours/cel_widget_tour.js +++ b/spp_cel_widget/static/tests/tours/cel_widget_tour.js @@ -8,7 +8,7 @@ */ import {registry} from "@web/core/registry"; -import {stepUtils} from "@web_tour/tour_service/tour_utils"; +import {stepUtils} from "@web_tour/tour_utils"; /** * Tour 1: Basic Widget Rendering and Initialization From 0dec46df5cb5967d5e98429842c62b446d674403 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 11:32:29 +0800 Subject: [PATCH 04/11] fix(spp_cel_widget): drop the pre-18 test key from the tours Odoo 19 validates web_tour.tours entries against {name, steps, url, wait_for}, so "test: true" made the tours module throw on load with "unknown key 'test'", a module loader console error that still failed every backend tour after the import fix. Every tour is a test tour in Odoo 19, so the key has no replacement. Refs #551. --- spp_cel_widget/readme/HISTORY.md | 5 +++-- spp_cel_widget/static/tests/tours/cel_widget_tour.js | 10 ---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/spp_cel_widget/readme/HISTORY.md b/spp_cel_widget/readme/HISTORY.md index 2d9530e6b..1acfa6e32 100644 --- a/spp_cel_widget/readme/HISTORY.md +++ b/spp_cel_widget/readme/HISTORY.md @@ -1,7 +1,8 @@ ### 19.0.2.0.1 -- fix: import the tours' stepUtils from the Odoo 19 path @web_tour/tour_utils, so web.assets_tests - loads without module loader errors and no longer fails every backend tour (#551) +- fix: import the tours' stepUtils from the Odoo 19 path @web_tour/tour_utils and drop the pre-18 + `test` tour key that Odoo 19 rejects, so web.assets_tests loads without module loader errors and + no longer fails every backend tour (#551) ### 19.0.2.0.0 diff --git a/spp_cel_widget/static/tests/tours/cel_widget_tour.js b/spp_cel_widget/static/tests/tours/cel_widget_tour.js index 573f515f8..57cf6294b 100644 --- a/spp_cel_widget/static/tests/tours/cel_widget_tour.js +++ b/spp_cel_widget/static/tests/tours/cel_widget_tour.js @@ -17,7 +17,6 @@ import {stepUtils} from "@web_tour/tour_utils"; * Verifies all UI components are present. */ registry.category("web_tour.tours").add("cel_widget_basic_rendering", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -78,7 +77,6 @@ registry.category("web_tour.tours").add("cel_widget_basic_rendering", { * Tests the autocomplete functionality by typing 'me.' and selecting a field. */ registry.category("web_tour.tours").add("cel_widget_autocomplete", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -142,7 +140,6 @@ registry.category("web_tour.tours").add("cel_widget_autocomplete", { * Tests that entering a valid expression shows success validation feedback. */ registry.category("web_tour.tours").add("cel_widget_validation_success", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -209,7 +206,6 @@ registry.category("web_tour.tours").add("cel_widget_validation_success", { * Tests that entering an invalid expression shows error feedback. */ registry.category("web_tour.tours").add("cel_widget_validation_error", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -272,7 +268,6 @@ registry.category("web_tour.tours").add("cel_widget_validation_error", { * Tests opening the symbol browser, navigating tabs, searching, and inserting symbols. */ registry.category("web_tour.tours").add("cel_widget_symbol_browser", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -395,7 +390,6 @@ registry.category("web_tour.tours").add("cel_widget_symbol_browser", { * Tests the search/filter functionality within the symbol browser. */ registry.category("web_tour.tours").add("cel_widget_symbol_search", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -478,7 +472,6 @@ registry.category("web_tour.tours").add("cel_widget_symbol_search", { * Tests that clicking the autocomplete button (Ctrl+Space alternative) works. */ registry.category("web_tour.tours").add("cel_widget_manual_autocomplete", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -538,7 +531,6 @@ registry.category("web_tour.tours").add("cel_widget_manual_autocomplete", { * Tests that the widget properly disables editing in readonly mode. */ registry.category("web_tour.tours").add("cel_widget_readonly", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -582,7 +574,6 @@ registry.category("web_tour.tours").add("cel_widget_readonly", { * Tests that empty expressions don't show validation errors initially. */ registry.category("web_tour.tours").add("cel_widget_empty_validation", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), @@ -656,7 +647,6 @@ registry.category("web_tour.tours").add("cel_widget_empty_validation", { * Tests entering a complex expression with operators, functions, and field access. */ registry.category("web_tour.tours").add("cel_widget_complex_expression", { - test: true, url: "/web", steps: () => [ stepUtils.showAppsMenuItem(), From f6f1a8eb0834202d44742aa6ca4e3988f4bc5e57 Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 11:44:39 +0800 Subject: [PATCH 05/11] test: address review of #552 (multi-line odoo.define, e2e wait comment) The defined-modules scan missed hand-written odoo.define calls whose arguments start on the next line (e.g. spreadsheet's @odoo/o-spreadsheet wrapper), so importing one would have been reported as unresolved. The e2e comment called the sentinel module the bundle's first; it is not, and any module from web.assets_tests works. --- e2e/tests/01-spp-starter-spmis.spec.ts | 2 +- spp_cel_widget/tests/test_assets.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/e2e/tests/01-spp-starter-spmis.spec.ts b/e2e/tests/01-spp-starter-spmis.spec.ts index 18fe22c3c..cb8c2f10d 100644 --- a/e2e/tests/01-spp-starter-spmis.spec.ts +++ b/e2e/tests/01-spp-starter-spmis.spec.ts @@ -1405,7 +1405,7 @@ test.describe.serial("OpenSPP Starter SP-MIS", () => { await page.goto("/odoo?debug=tests"); await expect(page.locator(".o_main_navbar")).toBeVisible({timeout: 30_000}); // Not "networkidle": the bus websocket worker request never finishes. - // Wait until the loader has defined web.assets_tests' first module and + // Wait until the loader has defined a module from web.assets_tests and // run its error check, which logs any errors synchronously. await page.waitForFunction( () => { diff --git a/spp_cel_widget/tests/test_assets.py b/spp_cel_widget/tests/test_assets.py index 9415a8006..f405cff4a 100644 --- a/spp_cel_widget/tests/test_assets.py +++ b/spp_cel_widget/tests/test_assets.py @@ -28,7 +28,7 @@ TOUR_FILE = "/spp_cel_widget/static/tests/tours/cel_widget_tour.js" -ODOO_DEFINE_RE = re.compile(r"""odoo\.define\((['"])(?P.+?)\1,\s*(?P\[.*?\])""") +ODOO_DEFINE_RE = re.compile(r"""odoo\.define\(\s*(['"])(?P.+?)\1,\s*(?P\[.*?\])""", re.DOTALL) def module_dependencies(url, content): @@ -86,6 +86,19 @@ def test_defined_module_names_reads_a_hand_written_define(self): {"@odoo/owl"}, ) + def test_defined_module_names_reads_a_multi_line_hand_written_define(self): + # Mirrors spreadsheet/static/src/o_spreadsheet/odoo_module.js. + content = ( + "// @odoo-module ignore\n\n" + 'odoo.define(\n "@odoo/o-spreadsheet",\n' + ' ["@web/core/l10n/translation", "@spreadsheet/o_spreadsheet/o_spreadsheet"],\n' + " function (require) {\n return {};\n }\n);\n" + ) + self.assertEqual( + defined_module_names("/spreadsheet/static/src/o_spreadsheet/odoo_module.js", content), + {"@odoo/o-spreadsheet"}, + ) + @tagged("post_install", "-at_install") class TestCelWidgetAssetImports(TransactionCase): From c02748089b84b7d6dffc91e8b8854903ddff67fc Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Thu, 24 Sep 2026 11:45:03 +0800 Subject: [PATCH 06/11] docs(spp_cel_widget): regenerate README for 19.0.2.0.1 (CI's pinned generator output) --- spp_cel_widget/README.rst | 8 ++++++++ spp_cel_widget/static/description/index.html | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/spp_cel_widget/README.rst b/spp_cel_widget/README.rst index 4c2faad66..807a9ad0c 100644 --- a/spp_cel_widget/README.rst +++ b/spp_cel_widget/README.rst @@ -154,6 +154,14 @@ Dependencies Changelog ========= +19.0.2.0.1 +~~~~~~~~~~ + +- fix: import the tours' stepUtils from the Odoo 19 path + @web_tour/tour_utils and drop the pre-18 ``test`` tour key that Odoo + 19 rejects, so web.assets_tests loads without module loader errors and + no longer fails every backend tour (#551) + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_cel_widget/static/description/index.html b/spp_cel_widget/static/description/index.html index e7a4f20bd..27fd31aaa 100644 --- a/spp_cel_widget/static/description/index.html +++ b/spp_cel_widget/static/description/index.html @@ -532,6 +532,15 @@

Changelog

+

19.0.2.0.1

+
    +
  • fix: import the tours’ stepUtils from the Odoo 19 path +@web_tour/tour_utils and drop the pre-18 test tour key that Odoo +19 rejects, so web.assets_tests loads without module loader errors and +no longer fails every backend tour (#551)
  • +
+
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • From 3d121269032996615aff992a0362546dc24e896d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 29 Sep 2026 17:34:37 +0800 Subject: [PATCH 07/11] test(spp_cel_widget): pin the Odoo 19 @web_tour/tour_utils import in the tour-file unit test The old assertion accepted any @web_tour/ import, so it passed with the pre-19 @web_tour/tour_service/tour_utils path too (PR #552 review). --- spp_cel_widget/tests/test_assets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spp_cel_widget/tests/test_assets.py b/spp_cel_widget/tests/test_assets.py index f405cff4a..f54d0a2b8 100644 --- a/spp_cel_widget/tests/test_assets.py +++ b/spp_cel_widget/tests/test_assets.py @@ -67,9 +67,11 @@ def test_module_dependencies_resolves_named_bare_and_relative_imports(self): def test_module_dependencies_reads_the_tour_file_web_tour_import(self): with file_open(TOUR_FILE.lstrip("/")) as tour_file: dependencies = module_dependencies(TOUR_FILE, tour_file.read()) - self.assertTrue( - any(name.startswith("@web_tour/") for name in dependencies), - f"Expected a @web_tour/ import in {TOUR_FILE}, got {dependencies}", + # Odoo 19's path; the pre-19 "@web_tour/tour_service/tour_utils" must fail here. + self.assertIn( + "@web_tour/tour_utils", + dependencies, + f"Expected the Odoo 19 @web_tour/tour_utils import in {TOUR_FILE}, got {dependencies}", ) def test_defined_module_names_uses_the_path_and_alias(self): From 169a3acf8096688ed3c6267f6f27cd947e88e88d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 29 Sep 2026 17:34:43 +0800 Subject: [PATCH 08/11] test(spp_cel_widget): scan only non-transpiled files for hand-written odoo.define calls A transpiled module never contains a hand-written define, so an odoo.define("x", [ in its comments, JSDoc or strings added a module nothing defines and could hide a real missing import (PR #552 review). --- spp_cel_widget/tests/test_assets.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/spp_cel_widget/tests/test_assets.py b/spp_cel_widget/tests/test_assets.py index f54d0a2b8..da118f18c 100644 --- a/spp_cel_widget/tests/test_assets.py +++ b/spp_cel_widget/tests/test_assets.py @@ -45,8 +45,10 @@ def defined_module_names(url, content): header = ODOO_MODULE_RE.match(content) if header and header["alias"]: names.add(header["alias"]) - # Files that call odoo.define by hand, e.g. the "@odoo/owl" wrapper in web/static/lib. - names.update(match["name"] for match in ODOO_DEFINE_RE.finditer(content)) + else: + # Files that call odoo.define by hand, e.g. the "@odoo/owl" wrapper in web/static/lib. + # A transpiled module never does, so its comments and strings are not scanned. + names.update(match["name"] for match in ODOO_DEFINE_RE.finditer(content)) return names @@ -88,6 +90,19 @@ def test_defined_module_names_reads_a_hand_written_define(self): {"@odoo/owl"}, ) + def test_defined_module_names_ignores_define_text_in_a_transpiled_module(self): + # A transpiled module never contains a hand-written define, so a match in its + # comments or strings is not a module; counting it could hide a missing import. + content = ( + "/** @odoo-module **/\n" + '/** Example: odoo.define("@example/not_a_module", [], function () {}); */\n' + "export const x = 1;\n" + ) + self.assertEqual( + defined_module_names("/spp_cel_widget/static/src/js/example.js", content), + {"@spp_cel_widget/js/example"}, + ) + def test_defined_module_names_reads_a_multi_line_hand_written_define(self): # Mirrors spreadsheet/static/src/o_spreadsheet/odoo_module.js. content = ( From 946a3daa38a1e9476c579430f85ee8acea9c361c Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 29 Sep 2026 17:36:27 +0800 Subject: [PATCH 09/11] test(spp_cel_widget): read the backend test page scripts from Odoo's asset bundles Use ir.qweb._get_asset_bundle(...).javascripts instead of rebuilding the pipeline from ir.asset paths and file_open: Odoo's JavascriptAsset decides what is transpiled and serves the transpiled content, and it also reads ir.asset entries stored as attachments (PR #552 review). --- spp_cel_widget/tests/test_assets.py | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/spp_cel_widget/tests/test_assets.py b/spp_cel_widget/tests/test_assets.py index da118f18c..bddd5df1e 100644 --- a/spp_cel_widget/tests/test_assets.py +++ b/spp_cel_widget/tests/test_assets.py @@ -23,6 +23,8 @@ ) from odoo.tools.misc import file_open +from odoo.addons.base.models.assetsbundle import WebAsset + # web.webclient_bootstrap loads web.assets_web, then web.assets_tests in test mode. BACKEND_TEST_BUNDLES = ("web.assets_web", "web.assets_tests") @@ -31,10 +33,15 @@ ODOO_DEFINE_RE = re.compile(r"""odoo\.define\(\s*(['"])(?P.+?)\1,\s*(?P\[.*?\])""", re.DOTALL) +def transpiled_dependencies(transpiled_content): + """Return the module names a transpiled JS module imports, read from its odoo.define call.""" + match = ODOO_DEFINE_RE.search(transpiled_content) + return ast.literal_eval(match["deps"]) + + def module_dependencies(url, content): """Return the module names a JS file imports, as Odoo's transpiler resolves them.""" - match = ODOO_DEFINE_RE.search(transpile_javascript(url, content)) - return ast.literal_eval(match["deps"]) + return transpiled_dependencies(transpile_javascript(url, content)) def defined_module_names(url, content): @@ -122,27 +129,33 @@ class TestCelWidgetAssetImports(TransactionCase): """Integration test: every import in this module's JS resolves on the backend test page.""" def _backend_test_page_scripts(self): + """Return ``{url: JavascriptAsset}`` for the scripts the backend test page loads. + + Odoo's own bundle objects decide what is transpiled and serve the transpiled + content, and they also read ``ir.asset`` entries stored as attachments. + """ scripts = {} for bundle in BACKEND_TEST_BUNDLES: - for path, full_path, _bundle, _last_modified in self.env["ir.asset"]._get_asset_paths(bundle, {}): - if path.endswith(".js") and path not in scripts: - with file_open(full_path) as script: - scripts[path] = script.read() + for asset in self.env["ir.qweb"]._get_asset_bundle(bundle, css=False).javascripts: + scripts.setdefault(asset.url, asset) return scripts def test_imports_resolve_on_the_backend_test_page(self): scripts = self._backend_test_page_scripts() - defined = set().union(*(defined_module_names(url, content) for url, content in scripts.items())) + # The source as written: the @odoo-module header (with its alias) and any + # hand-written odoo.define live there, not in the transpiled output. + # JavascriptAsset reads it through this same base-class property. + defined = set().union( + *(defined_module_names(url, WebAsset.content.fget(asset)) for url, asset in scripts.items()) + ) own_modules = { - url: content - for url, content in scripts.items() - if url.startswith("/spp_cel_widget/") and is_odoo_module(url, content) + url: asset for url, asset in scripts.items() if url.startswith("/spp_cel_widget/") and asset.is_transpiled } self.assertIn(TOUR_FILE, own_modules) unresolved = {} - for url, content in own_modules.items(): - missing = sorted(set(module_dependencies(url, content)) - defined) + for url, asset in own_modules.items(): + missing = sorted(set(transpiled_dependencies(asset.content)) - defined) if missing: unresolved[url] = missing self.assertFalse( From 73329dfb2ef970b5f091a29bbff46761cd95898a Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 29 Sep 2026 17:36:54 +0800 Subject: [PATCH 10/11] test(e2e): drop the redundant load wait in test 24 The sentinel is defined by the deferred web.assets_tests script, and the loader has reported its errors by the time checkErrorProm is null, so the extra waitForLoadState("load") only suggested a timing dependency that does not exist (PR #552 review). --- e2e/tests/01-spp-starter-spmis.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/tests/01-spp-starter-spmis.spec.ts b/e2e/tests/01-spp-starter-spmis.spec.ts index cb8c2f10d..de26e11f1 100644 --- a/e2e/tests/01-spp-starter-spmis.spec.ts +++ b/e2e/tests/01-spp-starter-spmis.spec.ts @@ -1418,7 +1418,6 @@ test.describe.serial("OpenSPP Starter SP-MIS", () => { null, {timeout: 30_000} ); - await page.waitForLoadState("load"); expect(await Promise.all(loaderErrors)).toEqual([]); // In debug mode the loader also injects this style as a red page banner. From e6beb9a32ca2340e8edde8dd2ede3621e167742d Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Tue, 29 Sep 2026 17:46:23 +0800 Subject: [PATCH 11/11] test(e2e): fail test 24 on uncaught page errors too An uncaught exception from a test asset, such as a script that throws outside odoo.define, also fails the tour runner without a module-loader console message. Collect pageerror events and assert there are none (PR #552 review). --- e2e/tests/01-spp-starter-spmis.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/e2e/tests/01-spp-starter-spmis.spec.ts b/e2e/tests/01-spp-starter-spmis.spec.ts index de26e11f1..0c946ba5f 100644 --- a/e2e/tests/01-spp-starter-spmis.spec.ts +++ b/e2e/tests/01-spp-starter-spmis.spec.ts @@ -1396,11 +1396,18 @@ test.describe.serial("OpenSPP Starter SP-MIS", () => { ); } }; + // An uncaught exception from a test asset (a script that throws outside + // odoo.define) also fails the tour runner, without a loader message. + const pageErrors: string[] = []; + const onPageError = (error: Error) => { + pageErrors.push(error.message); + }; await login(page); console.log("✅ Logged in as admin"); page.on("console", onConsole); + page.on("pageerror", onPageError); try { await page.goto("/odoo?debug=tests"); await expect(page.locator(".o_main_navbar")).toBeVisible({timeout: 30_000}); @@ -1420,11 +1427,13 @@ test.describe.serial("OpenSPP Starter SP-MIS", () => { ); expect(await Promise.all(loaderErrors)).toEqual([]); + expect(pageErrors).toEqual([]); // In debug mode the loader also injects this style as a red page banner. await expect(page.locator("style.o_module_error_banner")).toHaveCount(0); console.log("✅ Test assets loaded with no module loader errors"); } finally { page.off("console", onConsole); + page.off("pageerror", onPageError); } await logout(page);