Skip to content

Commit 821aabd

Browse files
committed
test: cover CEL ace editor mode and hide-menu upgrade re-hide
Add a guardrail asserting the CEL Expressions view's ace fields use a CodeEditor-valid mode (regression test for the 'text' mode crash that broke the tab), plus unit and integration coverage for spp_hide_menus_base._reapply_hide() and the hide_menus() re-hide branch that runs after a module upgrade resets a menu's group_ids.
1 parent e840b86 commit 821aabd

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

spp_approval/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from . import test_approval_review
55
from . import test_approval_security
66
from . import test_cel_evaluator_security
7+
from . import test_cel_view
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Guardrail for the CEL Expressions view's ace editor configuration.
3+
4+
Odoo 19's ``CodeEditor`` OWL component validates its ``mode`` prop against
5+
a fixed allow-list:
6+
7+
addons/web/static/src/core/code_editor/code_editor.js
8+
static MODES = ["javascript", "xml", "qweb", "scss", "python"];
9+
10+
A field declaring ``widget="ace" options="{'mode': '<invalid>'}"`` crashes
11+
the client the moment the editor mounts (the field is only rendered when
12+
its ``use_cel_*`` toggle is enabled). The original view used
13+
``'mode': 'text'`` — not in MODES — which is exactly that crash. This test
14+
parses the view arch and fails if any ace field declares a mode the
15+
component would reject, so the regression can't silently come back.
16+
"""
17+
18+
import ast
19+
20+
from lxml import etree
21+
22+
from odoo.tests import TransactionCase, tagged
23+
24+
# Mirror of CodeEditor.MODES (see module docstring). Kept here as a literal
25+
# because the allow-list lives in JS and isn't importable from Python.
26+
VALID_ACE_MODES = {"javascript", "xml", "qweb", "scss", "python"}
27+
28+
29+
@tagged("post_install", "-at_install")
30+
class TestCelViewAceMode(TransactionCase):
31+
"""The CEL Expressions form must only use ace modes CodeEditor accepts."""
32+
33+
def test_cel_ace_fields_use_valid_mode(self):
34+
view = self.env.ref("spp_approval.view_spp_approval_definition_form_cel")
35+
arch = etree.fromstring(view.arch)
36+
37+
ace_fields = arch.xpath("//field[@widget='ace']")
38+
self.assertTrue(
39+
ace_fields,
40+
"Expected at least one widget='ace' field in the CEL view — did the view change?",
41+
)
42+
43+
for field in ace_fields:
44+
options = field.get("options")
45+
self.assertTrue(
46+
options,
47+
f"ace field {field.get('name')!r} must declare options with a mode",
48+
)
49+
mode = ast.literal_eval(options).get("mode")
50+
self.assertIn(
51+
mode,
52+
VALID_ACE_MODES,
53+
f"ace field {field.get('name')!r} uses mode {mode!r}, which "
54+
f"CodeEditor rejects (valid: {sorted(VALID_ACE_MODES)}). "
55+
"This crashes the client when the field is rendered.",
56+
)

spp_hide_menus_base/tests/test_hide_menu.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
flow.
1010
"""
1111

12+
from odoo import Command
1213
from odoo.tests import TransactionCase, tagged
1314

1415

@@ -149,6 +150,101 @@ def test_hide_menus_is_idempotent(self):
149150
for record in after_second:
150151
self.assertEqual(record.state, "hide")
151152

153+
def test_reapply_hide_after_simulated_upgrade_reset(self):
154+
"""_reapply_hide() re-applies the hide group after a module upgrade
155+
reset the menu's group_ids via XML (noupdate="0").
156+
157+
A real module upgrade can't run inside a test transaction, so we
158+
simulate its effect: hide the menu, then overwrite group_ids the way
159+
an XML data reload would, dropping the hide group. _reapply_hide()
160+
must detect the stale state and restore the hidden configuration.
161+
"""
162+
hide_group = self.env.ref("spp_hide_menus_base.group_hide_menus_user")
163+
record = self.env["spp.hide.menu"].create({"menu_id": self.menu.id, "xml_id": "test.hide_menu_target"})
164+
record.hide_menu()
165+
self.assertIn(hide_group, self.menu.group_ids, "precondition: menu should be hidden")
166+
167+
# Simulate the upgrade resetting group_ids to the module's XML default
168+
# (some real group, without the hide group).
169+
reset_groups = self.env.ref("base.group_user")
170+
self.menu.write({"group_ids": [Command.set([reset_groups.id])]})
171+
self.assertNotIn(hide_group, self.menu.group_ids, "precondition: reset must drop the hide group")
172+
173+
record._reapply_hide()
174+
175+
self.assertIn(
176+
hide_group,
177+
self.menu.group_ids,
178+
"_reapply_hide() should restore the hide group after an upgrade reset",
179+
)
180+
# The reset groups become the new restore snapshot so show_menu()
181+
# returns the menu to its real post-upgrade default.
182+
self.assertEqual(record.default_group_ids, reset_groups)
183+
184+
def test_reapply_hide_noop_when_already_hidden(self):
185+
"""_reapply_hide() is a no-op when the hide group is still present.
186+
187+
If no upgrade reset happened, the guard (hide_group not in
188+
group_ids) is False, so neither group_ids nor the saved snapshot
189+
should change.
190+
"""
191+
hide_group = self.env.ref("spp_hide_menus_base.group_hide_menus_user")
192+
record = self.env["spp.hide.menu"].create({"menu_id": self.menu.id, "xml_id": "test.hide_menu_target"})
193+
record.hide_menu()
194+
groups_before = self.menu.group_ids
195+
snapshot_before = record.default_group_ids
196+
self.assertIn(hide_group, groups_before, "precondition: menu should be hidden")
197+
198+
record._reapply_hide()
199+
200+
self.assertEqual(self.menu.group_ids, groups_before)
201+
self.assertEqual(record.default_group_ids, snapshot_before)
202+
203+
def test_hide_menus_reapplies_after_reset(self):
204+
"""``hide_menus()`` re-hides an already-hidden menu whose group_ids
205+
were reset, exercising the ``elif ... state == "hide"`` branch.
206+
207+
This is the end-to-end shape of the upgrade bug: a menu is hidden,
208+
a later module upgrade resets its group_ids, and the next
209+
install/upgrade pass through hide_menus() must put the hide group
210+
back.
211+
"""
212+
IrModuleModule = self.env["ir.module.module"]
213+
HideMenu = self.env["spp.hide.menu"]
214+
hide_group = self.env.ref("spp_hide_menus_base.group_hide_menus_user")
215+
216+
# Find one resolvable MENU_APP entry to drive the real entry point.
217+
target = None
218+
for module_name, info in IrModuleModule.MENU_APP.items():
219+
menu = self.env.ref(info["menu_xml_id"], raise_if_not_found=False)
220+
module = IrModuleModule.search([("name", "=", module_name)], limit=1)
221+
if menu and module:
222+
target = menu
223+
break
224+
if target is None:
225+
self.skipTest("No MENU_APP entries are resolvable in this test DB")
226+
227+
# First pass creates the record and hides the menu.
228+
HideMenu.search([]).unlink()
229+
IrModuleModule.hide_menus()
230+
record = HideMenu.search([("menu_id", "=", target.id)], limit=1)
231+
self.assertTrue(record, "hide_menus() should have created a hide record")
232+
self.assertEqual(record.state, "hide")
233+
self.assertIn(hide_group, target.group_ids)
234+
235+
# Simulate an upgrade resetting the menu's group_ids via XML.
236+
reset_groups = self.env.ref("base.group_user")
237+
target.write({"group_ids": [Command.set([reset_groups.id])]})
238+
self.assertNotIn(hide_group, target.group_ids)
239+
240+
# Second pass must re-hide via the state == "hide" branch.
241+
IrModuleModule.hide_menus()
242+
self.assertIn(
243+
hide_group,
244+
target.group_ids,
245+
"hide_menus() should re-hide a menu whose group_ids were reset on upgrade",
246+
)
247+
152248
def test_hide_menus_skips_unknown_modules(self):
153249
"""An ir.module.module record whose name isn't in MENU_APP must be
154250
ignored by hide_menus() — no spp.hide.menu record is created for it.

0 commit comments

Comments
 (0)