Skip to content

Commit 8ad8d60

Browse files
committed
gui_e2e: wait for rendering before reading element snapshots
count(), inner_text() and all_inner_texts() return an immediate snapshot without waiting. Replace or supplement their usage with waited checks. CMK-34806 Change-Id: I8831870bac22b2699a0a6541b55adc4994a44248
1 parent ebce41d commit 8ad8d60

9 files changed

Lines changed: 134 additions & 58 deletions

File tree

tests/system/gui/testlib/playwright/pom/email.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import logging
99
from pathlib import Path
1010

11-
from playwright.sync_api import Locator, Page
11+
from playwright.sync_api import expect, Locator, Page
1212

1313
logger = logging.getLogger(__name__)
1414

@@ -32,8 +32,9 @@ def _get_row(self, row_name: str) -> Locator:
3232

3333
def get_field_value(self, field_name: str) -> str:
3434
"""Get the value of the specified field from the rendered HTML email."""
35-
value = self._get_row(field_name).inner_text()
36-
value = value.replace("\n", "").replace("\t", "")
35+
row = self._get_row(field_name)
36+
expect(row, f"Field '{field_name}' is not shown in the email").to_be_visible()
37+
value = row.inner_text().replace("\n", "").replace("\t", "")
3738
return value.split(field_name, 1)[1]
3839

3940
def check_table_content(self, expected_content: dict) -> None:

tests/system/gui/testlib/playwright/pom/monitor/dashboard.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,11 @@ def get_chart_widget_hexagon(
220220

221221
def get_chart_widget_total_value(self, widget_title: str) -> int:
222222
widget = self.get_widget(widget_title)
223-
return int(widget.get_by_role("row", name="Total").locator("a.count").inner_text())
223+
total_count = widget.get_by_role("row", name="Total").locator("a.count")
224+
expect(
225+
total_count, message=f"Total count of chart widget '{widget_title}' is not shown"
226+
).to_have_text(re.compile(r"\d+"))
227+
return int(total_count.inner_text())
224228

225229

226230
class MainDashboard(BaseDashboard):

tests/system/gui/testlib/playwright/pom/monitor/host_search.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,12 @@ def check_label_filter_applied(
7878
Check that label name and logical operator are correct at the expected position.
7979
"""
8080
# example of returned result - ['is', 'test_label:foo', 'and', '(Select label)']
81-
labels_text = self._labels_table.get_by_role("textbox").all_inner_texts()
82-
83-
assert labels_text[expected_position * 2] == logical_operator, (
84-
"Logical operator used to filter hosts using labels is incorrect."
85-
)
86-
assert labels_text[expected_position * 2 + 1] == label, (
87-
"Label used to filter hosts is incorrect."
88-
)
81+
label_textboxes = self._labels_table.get_by_role("textbox")
82+
expect(
83+
label_textboxes.nth(expected_position * 2),
84+
"Logical operator used to filter hosts using labels is incorrect.",
85+
).to_have_text(logical_operator)
86+
expect(
87+
label_textboxes.nth(expected_position * 2 + 1),
88+
"Label used to filter hosts is incorrect.",
89+
).to_have_accessible_name(label)

tests/system/gui/testlib/playwright/pom/page.py

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Literal, override
1010
from 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
1313
from playwright.sync_api import TimeoutError as PWTimeoutError
1414

1515
from 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

tests/system/gui/testlib/playwright/pom/setup/analyze_configuration.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,35 +49,26 @@ def _dropdown_list_name_to_id(self) -> DropdownListNameToID:
4949
def analyse_config_table(self) -> Locator:
5050
return self.main_area.locator("table[class*='analyze_config']")
5151

52-
@property
53-
def status_column_values(self) -> Locator:
54-
return self.main_area.locator("span[class*='state']")
55-
5652
@property
5753
def title_column_values(self) -> Locator:
5854
return self.main_area.locator("td[class*='buttons'] + td")
5955

6056
def verify_all_expected_checks_are_present(self, expected_checks: list[str]) -> None:
6157
"""Verify that all expected checks are present in the analyze configuration table."""
58+
expect(
59+
self.title_column_values,
60+
f"Expected {len(expected_checks)} checks in the analyze configuration table.",
61+
).to_have_count(len(expected_checks))
6262
titles = self.title_column_values.all_inner_texts()
63-
assert len(expected_checks) == len(titles), (
64-
f"Expected {len(expected_checks)} checks, but got {len(titles)} checks."
65-
)
6663
for expected_check in expected_checks:
6764
assert expected_check in titles, (
6865
f"Expected check '{expected_check}' not found in the analyze configuration table."
6966
)
7067

7168
def verify_checks_statuses(self, expected_statues: dict[str, str]) -> None:
72-
titles = self.title_column_values.all_inner_texts()
73-
statuses = self.status_column_values.all_inner_texts()
74-
actual_statuses = dict(zip(titles, statuses))
75-
assert len(titles) == len(actual_statuses), (
76-
f"Expected {len(expected_statues)} checks, but got {len(actual_statuses)} checks."
77-
)
7869
for title, expected_status in expected_statues.items():
79-
actual_status = actual_statuses.get(title)
80-
assert actual_status == expected_status, (
81-
f"Expected status '{expected_status}' for check '{title}', "
82-
f"but got '{actual_status}'."
83-
)
70+
check_row = self.main_area.locator(f"tr:has(td:text-is('{title}'))")
71+
expect(
72+
check_row.locator("span[class*='state']"),
73+
f"Unexpected status of the check '{title}'.",
74+
).to_have_text(expected_status)

tests/system/gui/testlib/playwright/pom/setup/background_jobs.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,6 @@ def result_retrieve_created_dump_file_icon(self) -> Locator:
5252
def progress_info_retrieve_created_dump_icon(self) -> Locator:
5353
return self._detail_row("Progress info").get_by_role("link", name="Download")
5454

55-
@property
56-
def job_state(self) -> str:
57-
"""Return the state of the job as text."""
58-
return self.job_state_locator.inner_text()
59-
6055
@property
6156
def job_state_locator(self) -> Locator:
6257
return self._detail_row("State", exact=True).locator("td")

tests/system/gui/testlib/playwright/pom/setup/distributed_monitoring.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ def clean_all_site_connections(self) -> int:
127127
"""
128128
logger.info("Delete all site connections")
129129

130+
expect(self.data_table, message="Site connections table is not shown").to_be_visible()
130131
delete_buttons = self.data_table.get_by_role("link", name="Delete")
131132

132133
number_of_deleted_sites = delete_buttons.count()

tests/system/gui/testlib/playwright/pom/setup/hosts.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ def delete_folder(self, folder_id: str) -> None:
130130
buttons.hover()
131131
buttons.get_by_role("link", name="Delete this folder").click()
132132
self.main_area.locator().get_by_role("button", name="Delete").click()
133+
expect(
134+
self.folder_icon(folder_id),
135+
message=f"Folder '{folder_id}' is still present after deletion",
136+
).to_have_count(0)
133137

134138
def check_host_not_present(self, host_name: str) -> None:
135139
"""Check that a host is not present in the list of hosts."""

tests/system/gui/testlib/playwright/pom/setup/notification_configuration.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,22 @@ def _get_notification_stat_count(self, title: str) -> Locator:
138138
)
139139

140140
def get_total_sent_notifications_count(self) -> int:
141-
return int(self._get_notification_stat_count("Total sent notifications").inner_text())
141+
stat_count = self._get_notification_stat_count("Total sent notifications")
142+
expect(stat_count, message="'Total sent notifications' count is not shown").to_have_text(
143+
re.compile(r"\d+")
144+
)
145+
return int(stat_count.inner_text())
142146

143147
def check_total_sent_notifications_has_changed(self, previous_count: int) -> None:
144148
locator = self._get_notification_stat_count("Total sent notifications")
145149
expect(locator).not_to_have_text(re.compile(rf"^{previous_count}$"))
146150

147151
def get_failed_notifications_count(self) -> int:
148-
return int(self._get_notification_stat_count("Failed notifications").inner_text())
152+
stat_count = self._get_notification_stat_count("Failed notifications")
153+
expect(stat_count, message="'Failed notifications' count is not shown").to_have_text(
154+
re.compile(r"\d+")
155+
)
156+
return int(stat_count.inner_text())
149157

150158
def check_failed_notifications_has_not_changed(self, previous_count: int) -> None:
151159
locator = self._get_notification_stat_count("Failed notifications")

0 commit comments

Comments
 (0)