99from typing import Literal , override
1010from urllib .parse import quote_plus , urljoin
1111
12- from playwright .sync_api import expect , Locator , Page , Response
12+ from playwright .sync_api import expect , FrameLocator , Locator , Page , Response
1313from playwright .sync_api import TimeoutError as PWTimeoutError
1414
1515from tests .system .gui .testlib .playwright .helpers import DropdownListNameToID , Keys , LocatorHelper
@@ -238,18 +238,75 @@ def _sub_menu(
238238 _loc = self .locator ().get_by_role (role = "link" , name = menu )
239239 if sub_menu :
240240 _loc .click ()
241-
242- if show_more :
243- show_more_locator = self .locator ().get_by_role (
244- role = "link" , name = "show more" , exact = True
245- )
246- # only try to press if it hasn't already been pressed before
247- if show_more_locator .count ():
248- show_more_locator .click ()
249- _loc = self .locator ().get_by_role (role = "link" , name = sub_menu , exact = exact )
241+ # After the click, the popup renders either the sub menu entry directly, or --
242+ # if the menu has too many entries -- a "show more" link instead. Guards the
243+ # one-shot .count() calls below against racing the popup's render.
244+ sub_menu_entry = self .locator ().get_by_role (role = "link" , name = sub_menu , exact = exact )
245+ show_more_locator = self .locator ().get_by_role (
246+ role = "link" , name = "show more" , exact = True
247+ )
248+ expect (
249+ sub_menu_entry .or_ (show_more_locator ),
250+ message = f"The '{ menu } ' menu popup did not open" ,
251+ ).not_to_have_count (0 )
252+
253+ # only try to press if it hasn't already been pressed before
254+ if show_more and show_more_locator .count ():
255+ show_more_locator .click ()
256+ # Clicking "show more" either reveals the sub menu entry directly, or --
257+ # if it's still hidden inside an individual topic -- that topic's own
258+ # "show all" overflow link. Guards the one-shot .count() call below
259+ # against racing the expansion's render.
260+ topic_overflow_locator = self .locator ().locator (".mm-nav-item-topic__show-all" )
261+ expect (
262+ sub_menu_entry .or_ (topic_overflow_locator ),
263+ message = "Clicking 'show more' did not expand the categories" ,
264+ ).not_to_have_count (0 )
265+ _loc = sub_menu_entry
266+ if not _loc .count ():
267+ _loc = self ._reveal_overflowing_entry (sub_menu , exact )
250268 self ._unique_web_element (_loc )
251269 return _loc
252270
271+ def _reveal_overflowing_entry (self , sub_menu : str , exact : bool ) -> Locator :
272+ """Locate a sub menu entry hidden behind a topic's "Show all" overflow.
273+
274+ Each topic renders only a limited number of entries; any surplus is
275+ collapsed behind a "Show all" link and is therefore absent from the DOM.
276+ Which entries are displayed depends on their configured sort order, so
277+ adding a new entry can push an existing one out of the rendered set.
278+ To stay independent of ordering and entry count, expand each
279+ overflowing topic in turn until the requested entry shows up.
280+ """
281+ back_button = (
282+ self .locator ().locator (".mm-nav-item-topic__show-all-back" ).get_by_role ("button" )
283+ )
284+ show_all_links = self .locator ().locator (".mm-nav-item-topic__show-all" )
285+ entry = self .locator ().get_by_role (role = "link" , name = sub_menu , exact = exact )
286+ # The requested entry is either already rendered, or hidden behind at least one
287+ # topic's "show all" overflow link. Guards the one-shot .count() calls below
288+ # against racing the render.
289+ expect (
290+ show_all_links .first .or_ (entry ),
291+ message = f"Neither '{ sub_menu } ' nor any overflowing topic is shown" ,
292+ ).not_to_have_count (0 )
293+
294+ for index in range (show_all_links .count ()):
295+ show_all_links .nth (index ).click ()
296+ expect (
297+ back_button , message = f"'Show all' for topic { index } did not expand"
298+ ).to_be_visible ()
299+ # Safe one-shot count: the back button and this topic's full entry
300+ # list are driven by the same reactive flag and render in the same
301+ # Vue update, so waiting on the button also covers the entries.
302+ if entry .count ():
303+ return entry
304+ back_button .click ()
305+ expect (
306+ back_button , message = f"Collapsing topic { index } via its 'back' button did not work"
307+ ).not_to_be_visible ()
308+ return entry
309+
253310 @property
254311 def main_page (self ) -> Locator :
255312 return self ._sub_menu ("Go to main page" , sub_menu = None )
@@ -476,7 +533,18 @@ def locator(
476533 ) -> Locator :
477534 if not selector :
478535 selector = ":scope"
479- _loc = self ._iframe_locator .locator (selector )
536+
537+ # Mixed-version tests drive older remote sites where the page content
538+ # still renders inside the main iframe. Current sites render it in the
539+ # shared document; popups, dialogs and slide-ins teleport to <body>,
540+ # so no narrower container than the document itself covers them all.
541+ _base : FrameLocator | Page
542+ self .page .wait_for_load_state ("load" )
543+ if self .page .locator ("iframe[name='main']" ).count ():
544+ _base = self .page .frame_locator ("iframe[name='main']" )
545+ else :
546+ _base = self .page
547+ _loc = _base .locator (selector )
480548 kwargs = self ._build_locator_kwargs (
481549 has_text = has_text ,
482550 has_not_text = has_not_text ,
@@ -710,14 +778,20 @@ def filter_combobox(self, filter_name: str, check: bool = True) -> Locator:
710778 "combobox"
711779 )
712780
781+ def _add_filter (self , filter_name : str , exact : bool = False ) -> None :
782+ """Open `filter_name` in the sidebar's filter list, unless it's already applied."""
783+ self .add_filter_button .click ()
784+ filter_link = self .filter_button (filter_name , exact = exact )
785+ expect (
786+ filter_link , message = f"The '{ filter_name } ' filter link did not render"
787+ ).to_be_visible ()
788+ if "disabled" not in (filter_link .get_attribute ("class" ) or "" ):
789+ filter_link .click ()
790+
713791 def apply_last_service_state_change_filter (
714792 self , from_units : str , from_value : str , until_units : str , until_value : str
715793 ) -> None :
716- try :
717- expect (self .last_service_state_change_filter ).to_be_visible (timeout = 3000 )
718- except AssertionError :
719- self .add_filter_button .click ()
720- self .filter_button ("Last service state change" ).click ()
794+ self ._add_filter ("Last service state change" )
721795
722796 self .last_service_state_change_from_dropdown .click ()
723797 self .dropdown_option (from_units ).click ()
@@ -743,12 +817,9 @@ def apply_host_filter(self, host_filter: str) -> None:
743817 def apply_filter_by_name (
744818 self , filter_name : str , filter_value : str , exact : bool = False
745819 ) -> None :
746- filter_combobox = self .filter_combobox (filter_name , check = False )
747-
748- if filter_combobox .count () == 0 :
749- self .add_filter_button .click ()
750- self .filter_button (filter_name , exact = exact ).click ()
820+ self ._add_filter (filter_name , exact = exact )
751821
822+ filter_combobox = self .filter_combobox (filter_name )
752823 filter_combobox .click ()
753824 self .search_text_field .fill (filter_value )
754825 # TODO: remove 'first' after fixing CMK-19975
0 commit comments