@@ -83,3 +83,93 @@ def test_menu_app_catalog_is_well_formed(self):
8383 info ["menu_xml_id" ],
8484 f"MENU_APP[{ module_name !r} ].menu_xml_id is empty" ,
8585 )
86+
87+ def test_hide_menus_processes_catalog (self ):
88+ """``ir.module.module.hide_menus()`` walks MENU_APP and creates a
89+ ``spp.hide.menu`` record (state=hide) for every entry whose menu
90+ xml_id resolves in the current DB.
91+ """
92+ IrModuleModule = self .env ["ir.module.module" ]
93+ HideMenu = self .env ["spp.hide.menu" ]
94+
95+ # Figure out which catalog entries are actually resolvable here —
96+ # most stock Odoo modules in MENU_APP (mail, contacts, ...) are
97+ # present in any test DB, but a few (mass_mailing, survey, ...)
98+ # may not be installed.
99+ resolvable = []
100+ for module_name , info in IrModuleModule .MENU_APP .items ():
101+ menu = self .env .ref (info ["menu_xml_id" ], raise_if_not_found = False )
102+ module = IrModuleModule .search ([("name" , "=" , module_name )], limit = 1 )
103+ if menu and module :
104+ resolvable .append ((module_name , menu .id ))
105+
106+ if not resolvable :
107+ self .skipTest ("No MENU_APP entries are resolvable in this test DB" )
108+
109+ # Wipe any pre-existing spp.hide.menu so the test's assertions are
110+ # clearly about hide_menus()'s effect, not the install hook.
111+ HideMenu .search ([]).unlink ()
112+
113+ IrModuleModule .hide_menus ()
114+
115+ for module_name , menu_id in resolvable :
116+ record = HideMenu .search ([("menu_id" , "=" , menu_id )], limit = 1 )
117+ self .assertTrue (
118+ record ,
119+ f"hide_menus() didn't create a spp.hide.menu for { module_name !r} " ,
120+ )
121+ self .assertEqual (
122+ record .state ,
123+ "hide" ,
124+ f"spp.hide.menu for { module_name !r} expected state=hide, got { record .state } " ,
125+ )
126+
127+ def test_hide_menus_is_idempotent (self ):
128+ """Calling hide_menus() twice doesn't double-hide already-hidden menus.
129+
130+ After the first pass every resolvable entry is in state=hide. A
131+ second pass must leave them in state=hide (the inner guard
132+ ``elif hidden_menus.state == "show"`` skips them).
133+ """
134+ IrModuleModule = self .env ["ir.module.module" ]
135+ HideMenu = self .env ["spp.hide.menu" ]
136+
137+ HideMenu .search ([]).unlink ()
138+ IrModuleModule .hide_menus ()
139+ after_first = HideMenu .search ([])
140+ self .assertTrue (
141+ after_first ,
142+ "hide_menus() didn't create any records — nothing to check idempotency against" ,
143+ )
144+
145+ IrModuleModule .hide_menus ()
146+ after_second = HideMenu .search ([])
147+ # No duplicates created and every record stayed in state=hide.
148+ self .assertEqual (set (after_first .ids ), set (after_second .ids ))
149+ for record in after_second :
150+ self .assertEqual (record .state , "hide" )
151+
152+ def test_hide_menus_skips_unknown_modules (self ):
153+ """An ir.module.module record whose name isn't in MENU_APP must be
154+ ignored by hide_menus() — no spp.hide.menu record is created for it.
155+ """
156+ IrModuleModule = self .env ["ir.module.module" ]
157+ HideMenu = self .env ["spp.hide.menu" ]
158+
159+ # ``base`` is always installed and is NOT in MENU_APP.
160+ self .assertNotIn ("base" , IrModuleModule .MENU_APP )
161+
162+ before = HideMenu .search ([]).ids
163+ IrModuleModule .hide_menus ()
164+ after = HideMenu .search ([]).ids
165+
166+ # Whatever new records appeared, none should belong to the ``base`` menu.
167+ new_ids = set (after ) - set (before )
168+ for record in HideMenu .browse (list (new_ids )):
169+ self .assertNotEqual (
170+ record .menu_id .id ,
171+ self .env .ref ("base.menu_administration" ).id
172+ if self .env .ref ("base.menu_administration" , raise_if_not_found = False )
173+ else 0 ,
174+ "hide_menus() shouldn't touch base.menu_administration" ,
175+ )
0 commit comments