Skip to content

Commit b925a16

Browse files
mepr1Andyg79
authored andcommitted
Feature/bcss 20022 add a table utility (#23)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> ## Context This PR is adding a new table util to be able to use for all tables on BCSS. Currently test_report page using the first two methods of table util to click on first link in the table and Reports.page is the place where the locators of the table are stored <!-- Why is this change required? What problem does it solve? --> ## Type of changes This was added so that we could have a general utility to call whenever we need to use a table during our automated tests. <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. --> - [ x] Refactoring (non-breaking change) - [x ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would change existing functionality) - [ ] Bug fix (non-breaking change which fixes an issue) ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x ] I am familiar with the [contributing guidelines](https://github.com/nhs-england-tools/playwright-python-blueprint/blob/main/CONTRIBUTING.md) - [x ] I have followed the code style of the project - [ ] I have added tests to cover my changes (where appropriate) - [ ] I have updated the documentation accordingly - [ ] This PR is a result of pair or mob programming --- ## Sensitive Information Declaration To ensure the utmost confidentiality and protect your and others privacy, we kindly ask you to NOT including [PII (Personal Identifiable Information) / PID (Personal Identifiable Data)](https://digital.nhs.uk/data-and-information/keeping-data-safe-and-benefitting-the-public) or any other sensitive data in this PR (Pull Request) and the codebase changes. We will remove any PR that do contain any sensitive information. We really appreciate your cooperation in this matter. - [ x] I confirm that neither PII/PID nor sensitive data are included in this PR and the codebase changes.
1 parent 6d2aceb commit b925a16

File tree

3 files changed

+201
-35
lines changed

3 files changed

+201
-35
lines changed

pages/reports/reports_page.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from playwright.sync_api import Page
22
import pytest
33
from pages.base_page import BasePage
4+
from utils.table_util import TableUtils
45

56

67
class ReportsPage(BasePage):
78
def __init__(self, page):
89
super().__init__(page)
910
self.page = page
1011

12+
# Initialize TableUtils for different tables
13+
self.failsafe_reports_sub_links_table = TableUtils(page, "#listReportDataTable")
14+
self.fail_safe_reports_screening_subjects_with_inactive_open_episodes_table = TableUtils(page, "#subjInactiveOpenEpisodes")
15+
1116
# Reports page main menu links
1217
self.bureau_reports_link = self.page.get_by_text("Bureau Reports")
1318
self.failsafe_reports_link = self.page.get_by_role(
@@ -180,32 +185,22 @@ def go_to_screening_practitioner_6_weeks_availability_not_set_up_report_page(
180185
)
181186

182187
def go_to_screening_practitioner_appointments_page(self) -> None:
183-
self.click(self.screening_practitioner_appointments_link)
188+
self.click(self.screening_practitioner_appointments_page)
189+
190+
def click_failsafe_reports_sub_links(self):
191+
"""
192+
Clicks the first NHS number link from the primary report table.
193+
"""
194+
self.failsafe_reports_sub_links_table.click_first_link_in_column("NHS Number")
195+
196+
def click_fail_safe_reports_screening_subjects_with_inactive_open_episodes_link(self):
197+
"""
198+
Clicks the first NHS number link from the primary report table.
199+
"""
200+
self.fail_safe_reports_screening_subjects_with_inactive_open_episodes_table.click_first_link_in_column("NHS Number")
184201

185-
def click_nhs_number_link(self, page: Page) -> None:
202+
def click_fail_safe_reports_identify_and_link_new_gp_practices_link(self):
186203
"""
187-
Clicks the first NHS number link present on the screen if any are found.
204+
Clicks the first Practice Code link from the primary report table.
188205
"""
189-
# List of locators to check for NHS number links.
190-
# This implementation is a workaround for the fact that the NHS number
191-
# links are not using the same locators accross bcss
192-
# This is a temporary solution until
193-
# we have a table utility that will allow us to interact with tables across bcss.
194-
locators = [
195-
"#listReportDataTable > tbody > tr:nth-child(3) > td:nth-child(1) > a",
196-
"//*[@id='listReportDataTable']/tbody/tr[3]/td[1]",
197-
"//*[@id='listReportDataTable']/tbody/tr[3]/td[2]",
198-
"#listReportDataTable > tbody > tr:nth-child(3) > td:nth-child(1) > a",
199-
"#subjInactiveOpenEpisodes > tbody > tr:nth-child(1) > td.NHS_NUMBER.dt-type-numeric > a",
200-
]
201-
202-
for locator_string in locators:
203-
try:
204-
# Use page.locator to get a locator object
205-
locator = page.locator(locator_string)
206-
# Check if the locator is visible
207-
if locator.is_visible():
208-
# Click the locator
209-
locator.click()
210-
except Exception:
211-
print("No NHS number links found on the page")
206+
self.failsafe_reports_sub_links_table.click_first_link_in_column("Practice Code")

tests/test_reports_page.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def before_each(page: Page):
2525
BasePage(page).go_to_reports_page()
2626

2727

28-
@pytest.mark.smoke
28+
2929
def test_reports_page_navigation(page: Page) -> None:
3030
"""
3131
Confirms all menu items are displayed on the reports page, and that the relevant pages
@@ -131,14 +131,13 @@ def test_failsafe_reports_screening_subjects_with_inactive_open_episode(
131131
ReportsPage(page).click_generate_report_button()
132132

133133
# Open a screening subject record
134-
ReportsPage(page).click_nhs_number_link(page)
134+
ReportsPage(page).click_fail_safe_reports_screening_subjects_with_inactive_open_episodes_link()
135135

136136
# Verify the page title is "Subject Screening Summary"
137137
BasePage(page).bowel_cancer_screening_page_title_contains_text(
138138
"Subject Screening Summary"
139139
)
140140

141-
142141
def test_failsafe_reports_subjects_ceased_due_to_date_of_birth_changes(
143142
page: Page,
144143
) -> None:
@@ -170,14 +169,14 @@ def test_failsafe_reports_subjects_ceased_due_to_date_of_birth_changes(
170169
)
171170

172171
# Open a screening subject record from the search results
173-
ReportsPage(page).click_nhs_number_link(page)
172+
173+
ReportsPage(page).click_failsafe_reports_sub_links()
174174

175175
# Verify page title is "Subject Demographic"
176176
BasePage(page).bowel_cancer_screening_page_title_contains_text(
177177
"Subject Demographic"
178178
)
179179

180-
181180
def test_failsafe_reports_allocate_sc_for_patient_movements_within_hub_boundaries(
182181
page: Page, general_properties: dict
183182
) -> None:
@@ -211,7 +210,8 @@ def test_failsafe_reports_allocate_sc_for_patient_movements_within_hub_boundarie
211210
)
212211

213212
# Open a screening subject record from the first row/first cell of the table
214-
ReportsPage(page).click_nhs_number_link(page)
213+
# nhs_number_link.click()
214+
ReportsPage(page).click_failsafe_reports_sub_links()
215215

216216
# Verify page title is "Set Patient's Screening Centre"
217217
BasePage(page).bowel_cancer_screening_page_title_contains_text(
@@ -309,8 +309,8 @@ def test_failsafe_reports_identify_and_link_new_gp(page: Page) -> None:
309309
report_timestamp
310310
)
311311

312-
# Open a screening subject record from the first row/second cell of the table
313-
ReportsPage(page).click_nhs_number_link(page)
312+
# Open a practice code from the first row/second cell of the table
313+
ReportsPage(page).click_fail_safe_reports_identify_and_link_new_gp_practices_link()
314314

315315
# Verify page title is "Link GP practice to Screening Centre"
316316
BasePage(page).bowel_cancer_screening_page_title_contains_text(
@@ -319,6 +319,7 @@ def test_failsafe_reports_identify_and_link_new_gp(page: Page) -> None:
319319

320320

321321
# Operational Reports
322+
322323
def test_operational_reports_appointment_attendance_not_updated(
323324
page: Page, general_properties: dict
324325
) -> None:
@@ -358,7 +359,7 @@ def test_operational_reports_appointment_attendance_not_updated(
358359
)
359360

360361
# Open an appointment record from the report
361-
ReportsPage(page).click_nhs_number_link(page)
362+
ReportsPage(page).click_failsafe_reports_sub_links()
362363

363364
# Verify the page title is "Appointment Detail"
364365
BasePage(page).bowel_cancer_screening_page_title_contains_text("Appointment Detail")

utils/table_util.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
from playwright.sync_api import Page, Locator, expect
2+
from pages.base_page import BasePage
3+
import logging
4+
import secrets
5+
6+
7+
class TableUtils:
8+
"""
9+
A utility class providing functionality around tables in BCSS.
10+
"""
11+
12+
def __init__(self, page: Page, table_locator: str) -> None:
13+
"""
14+
Initializer for TableUtils.
15+
16+
Args:
17+
page (playwright.sync_api.Page): The page the table is on.
18+
table_locator (str): The locator value to use to find the table.
19+
20+
Returns:
21+
A TableUtils object ready to use.
22+
"""
23+
self.page = page
24+
self.table_id = table_locator # Store the table locator as a string
25+
self.table = page.locator(
26+
table_locator
27+
) # Create a locator object for the table
28+
29+
def get_column_index(self, column_name: str) -> int:
30+
"""
31+
Finds the column index dynamically based on column name.
32+
Works even if <thead> is missing and header is inside <tbody>.
33+
34+
:param column_name: Name of the column (e.g., 'NHS Number')
35+
:return: 1-based column index or -1 if not found
36+
"""
37+
header_row = (
38+
self.table.locator("tbody tr").filter(has=self.page.locator("th")).first
39+
)
40+
41+
headers = header_row.locator("th")
42+
header_texts = headers.evaluate_all("ths => ths.map(th => th.innerText.trim())")
43+
44+
for index, header in enumerate(header_texts):
45+
if column_name.lower() in header.lower():
46+
return index + 1 # Convert to 1-based index
47+
return -1 # Column not found
48+
49+
def click_first_link_in_column(self, column_name: str):
50+
"""
51+
Clicks the first link found in the given column.
52+
:param column_name: Name of the column containing links
53+
"""
54+
column_index = self.get_column_index(column_name)
55+
if column_index == -1:
56+
raise ValueError(f"Column '{column_name}' not found in table")
57+
58+
# Create a dynamic locator for the esired column
59+
link_locator = f"{self.table_id} tbody tr td:nth-child({column_index}) a"
60+
links = self.page.locator(link_locator)
61+
62+
if links.count() > 0:
63+
links.first.click()
64+
else:
65+
logging.info(f"No links found in column '{column_name}'")
66+
67+
def _format_inner_text(self, data: str) -> dict:
68+
"""
69+
This formats the inner text of a row to make it easier to manage
70+
71+
Args:
72+
data (str): The .inner_text() of a table row.
73+
74+
Returns:
75+
A dict with each column item from the row identified with its position.
76+
"""
77+
dict_to_return = {}
78+
split_rows = data.split("\t")
79+
pos = 1
80+
for item in split_rows:
81+
dict_to_return[pos] = item
82+
pos += 1
83+
return dict_to_return
84+
85+
def get_table_headers(self) -> dict:
86+
"""
87+
This retrieves the headers from the table.
88+
89+
Returns:
90+
A dict with each column item from the header row identified with its position.
91+
"""
92+
headers = self.page.locator(f"{self.table_id} > thead tr").nth(0).inner_text()
93+
return self._format_inner_text(headers)
94+
95+
def get_row_count(self) -> int:
96+
"""
97+
This returns the total rows visible on the table (on the screen currently)
98+
99+
Returns:
100+
An int with the total row count.
101+
"""
102+
return self.page.locator(f"{self.table_id} > tbody tr").count()
103+
104+
def pick_row(self, row_number: int) -> Locator:
105+
"""
106+
This picks a selected row from table
107+
108+
Args:
109+
row_id (str): The row number of the row to select.
110+
111+
Returns:
112+
A playwright.sync_api.Locator with the row object.
113+
"""
114+
return self.page.locator(f"{self.table_id} > tbody tr").nth(row_number)
115+
116+
def pick_random_row(self) -> Locator:
117+
"""
118+
This picks a random row from the visible rows in the table (full row)
119+
120+
Returns:
121+
A playwright.sync_api.Locator with the row object.
122+
"""
123+
return self.page.locator(f"{self.table_id} > tbody tr").nth(
124+
secrets.randbelow(self.get_row_count())
125+
)
126+
127+
def pick_random_row_number(self) -> int:
128+
"""
129+
This picks a random row from the table in BCSS and returns its position
130+
131+
Returns:
132+
An int representing a random row on the table.
133+
"""
134+
return secrets.randbelow(self.get_row_count())
135+
136+
def get_row_data_with_headers(self, row_number: int) -> dict:
137+
"""
138+
This picks a selected row from table
139+
140+
Args:
141+
row_number (str): The row number of the row to select.
142+
143+
Returns:
144+
A dict object with keys representing the headers, and values representing the row contents.
145+
"""
146+
headers = self.get_table_headers()
147+
row_data = self._format_inner_text(
148+
self.page.locator(f"{self.table_id} > tbody tr")
149+
.nth(row_number)
150+
.inner_text()
151+
)
152+
results = {}
153+
154+
for key in headers:
155+
results[headers[key]] = row_data[key]
156+
157+
return results
158+
159+
def get_full_table_with_headers(self) -> dict:
160+
"""
161+
This returns the full table as a dict of rows, with each entry having a header key / value pair.
162+
NOTE: The row count starts from 1 to represent the first row, not 0.
163+
164+
Returns:
165+
A dict object with keys representing the rows, with values being a dict representing a header key / column value pair.
166+
"""
167+
full_results = {}
168+
for row in range(self.get_row_count()):
169+
full_results[row + 1] = self.get_row_data_with_headers(row)
170+
return full_results

0 commit comments

Comments
 (0)