Skip to content

Commit 856ecf1

Browse files
committed
Merge branch 'main' of github.com:NHSDigital/bcss-playwright into feature/BCSS-20605-regression-tests-subject-notes
# Conflicts: # users.json
2 parents 0ff7b0e + 7842110 commit 856ecf1

20 files changed

+1044
-237
lines changed

pages/base_page.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,46 @@ def safe_accept_dialog(self, locator: Locator) -> None:
239239
Safely accepts a dialog triggered by a click, avoiding the error:
240240
playwright._impl._errors.Error: Dialog.accept: Cannot accept dialog which is already handled!
241241
If no dialog appears, continues without error.
242+
Args:
243+
locator (Locator): The locator that triggers the dialog when clicked.
244+
example: If clicking a save button opens a dialog, pass that save button's locator.
242245
"""
243246
self.page.once("dialog", self._accept_dialog)
244247
try:
245248
self.click(locator)
246249
except Exception as e:
247250
logging.error(f"Click failed: {e}")
251+
252+
253+
def assert_dialog_text(self, expected_text: str) -> None:
254+
"""
255+
Asserts that a dialog appears and contains the expected text.
256+
If no dialog appears, logs an error.
257+
Args:
258+
expected_text (str): The text that should be present in the dialog.
259+
"""
260+
261+
def handle_dialog(dialog):
262+
actual_text = dialog.message
263+
assert (
264+
actual_text == expected_text
265+
), f"Expected '{expected_text}', but got '{actual_text}'"
266+
dialog.dismiss() # Dismiss dialog
267+
268+
self.page.once("dialog", handle_dialog)
269+
270+
def safe_accept_dialog_select_option(self, locator: Locator, option: str) -> None:
271+
"""
272+
Safely accepts a dialog triggered by selecting a dropdown, avoiding the error:
273+
playwright._impl._errors.Error: Dialog.accept: Cannot accept dialog which is already handled!
274+
If no dialog appears, continues without error.
275+
Args:
276+
locator (Locator): The locator that triggers the dialog when clicked.
277+
example: If clicking a save button opens a dialog, pass that save button's locator.
278+
"""
279+
self.page.once("dialog", self._accept_dialog)
280+
try:
281+
locator.select_option(option)
282+
except Exception as e:
283+
logging.error(f"Option selection failed: {e}")
284+

pages/call_and_recall/create_a_plan_page.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from playwright.sync_api import Page, expect
22
from pages.base_page import BasePage
3+
from utils.table_util import TableUtils
34

45

56
class CreateAPlanPage(BasePage):
@@ -23,6 +24,20 @@ def __init__(self, page: Page):
2324
self.save_note_button = self.page.locator("#saveNote").get_by_role(
2425
"button", name="Save"
2526
)
27+
# Create A Plan Table Locators
28+
self.weekly_invitation_rate_field_on_table = self.page.locator(
29+
"#invitationPlan > tbody > tr:nth-child(1) > td.input.border-right.dt-type-numeric > input"
30+
)
31+
self.invitations_sent_value = self.page.locator(
32+
"tbody tr:nth-child(1) td:nth-child(8)"
33+
)
34+
35+
self.resulting_position_value = self.page.locator(
36+
"#invitationPlan > tbody > tr:nth-child(1) > td:nth-child(9)"
37+
)
38+
39+
# Initialize TableUtils for different tables
40+
self.create_a_plan_table = TableUtils(page, "#invitationPlan")
2641

2742
def click_set_all_button(self) -> None:
2843
"""Clicks the Set all button to set all values"""
@@ -59,3 +74,106 @@ def click_save_note_button(self) -> None:
5974
def verify_create_a_plan_title(self) -> None:
6075
"""Verifies the Create a Plan page title"""
6176
self.bowel_cancer_screening_page_title_contains_text("View a plan")
77+
78+
def verify_weekly_invitation_rate_for_weeks(
79+
self, start_week: int, end_week: int, expected_weekly_rate: str
80+
) -> None:
81+
"""
82+
Verifies that the weekly invitation rate is correctly calculated and displayed for the specified range of weeks.
83+
84+
Args:
85+
start_week (int): The starting week of the range.
86+
end_week (int): The ending week of the range.
87+
expected_weekly_rate (str): The expected weekly invitation rate.
88+
"""
89+
90+
# Verify the rate for the starting week
91+
weekly_invitation_rate_selector = "#invitationPlan > tbody > tr:nth-child(2) > td.input.border-right.dt-type-numeric > input"
92+
self.page.wait_for_selector(weekly_invitation_rate_selector)
93+
weekly_invitation_rate = self.page.locator(
94+
weekly_invitation_rate_selector
95+
).input_value()
96+
97+
assert (
98+
weekly_invitation_rate == expected_weekly_rate
99+
), f"Expected weekly invitation rate '{expected_weekly_rate}' for week {start_week} but got '{weekly_invitation_rate}'"
100+
# Verify the rate for the specified range of weeks
101+
for week in range(start_week + 1, end_week + 1):
102+
weekly_rate_locator = f"#invitationPlan > tbody > tr:nth-child({week + 2}) > td.input.border-right.dt-type-numeric > input"
103+
104+
# Wait for the element to be available
105+
self.page.wait_for_selector(weekly_rate_locator)
106+
107+
# Get the input value safely
108+
weekly_rate_element = self.page.locator(weekly_rate_locator)
109+
assert (
110+
weekly_rate_element.is_visible()
111+
), f"Week {week} rate element not visible"
112+
113+
# Verify the value
114+
actual_weekly_rate = weekly_rate_element.input_value()
115+
assert (
116+
actual_weekly_rate == expected_weekly_rate
117+
), f"Week {week} invitation rate should be '{expected_weekly_rate}', but found '{actual_weekly_rate}'"
118+
119+
# Get the text safely
120+
# Get the frame first
121+
frame = self.page.frame(
122+
url="https://bcss-bcss-18680-ddc-bcss.k8s-nonprod.texasplatform.uk/invitation/plan/23159/23162/create"
123+
)
124+
125+
# Ensure the frame is found before proceeding
126+
assert frame, "Frame not found!"
127+
128+
# Now locate the input field inside the frame and get its value
129+
weekly_invitation_rate_selector = "#invitationPlan > tbody > tr:nth-child(2) > td.input.border-right.dt-type-numeric > input"
130+
weekly_invitation_rate = frame.locator(
131+
weekly_invitation_rate_selector
132+
).input_value()
133+
134+
# Assert the expected value
135+
assert (
136+
weekly_invitation_rate == expected_weekly_rate
137+
), f"Week 2 invitation rate should be '{expected_weekly_rate}', but found '{weekly_invitation_rate}'"
138+
139+
def increment_invitation_rate_and_verify_changes(self) -> None:
140+
"""
141+
Increments the invitation rate by 1, then verifies that both the
142+
'Invitations Sent' has increased by 1 and 'Resulting Position' has decreased by 1.
143+
"""
144+
# Capture initial values before any changes
145+
initial_invitations_sent = int(self.invitations_sent_value.inner_text().strip())
146+
initial_resulting_position = int(
147+
self.resulting_position_value.inner_text().strip()
148+
)
149+
150+
# Increment the invitation rate
151+
current_rate = int(
152+
self.create_a_plan_table.get_cell_value("Invitation Rate", 1)
153+
)
154+
new_rate = str(current_rate + 1)
155+
self.weekly_invitation_rate_field_on_table.fill(new_rate)
156+
self.page.keyboard.press("Tab")
157+
158+
# Wait dynamically for updates
159+
expect(self.invitations_sent_value).to_have_text(
160+
str(initial_invitations_sent + 1)
161+
)
162+
expect(self.resulting_position_value).to_have_text(
163+
str(initial_resulting_position + 1)
164+
)
165+
166+
# Capture updated values
167+
updated_invitations_sent = int(self.invitations_sent_value.inner_text().strip())
168+
updated_resulting_position = int(
169+
self.resulting_position_value.inner_text().strip()
170+
)
171+
172+
# Assert changes
173+
assert (
174+
updated_invitations_sent == initial_invitations_sent + 1
175+
), f"Expected Invitations Sent to increase by 1. Was {initial_invitations_sent}, now {updated_invitations_sent}."
176+
177+
assert (
178+
updated_resulting_position == initial_resulting_position + 1
179+
), f"Expected Resulting Position to increase by 1. Was {initial_resulting_position}, now {updated_resulting_position}."

pages/call_and_recall/generate_invitations_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def wait_for_invitation_generation_complete(
9191
value = value.strip() # Get text and remove extra spaces
9292
if int(value) < number_of_invitations:
9393
pytest.fail(
94-
f"There are less than {number_of_invitations} invitations generated"
94+
f"Expected {number_of_invitations} invitations generated but got {value}"
9595
)
9696

9797
self_referrals_total_text = self.self_referrals_total.text_content()

pages/call_and_recall/non_invitations_days_page.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from playwright.sync_api import Page, expect
22
from pages.base_page import BasePage
3+
from utils.date_time_utils import DateTimeUtils
34

45

56
class NonInvitationDaysPage(BasePage):
@@ -9,7 +10,54 @@ def __init__(self, page: Page):
910
super().__init__(page)
1011
self.page = page
1112
# Non Invitation Days - page locators, methods
13+
self.enter_note_field = self.page.locator("#note")
14+
self.enter_date_field = self.page.get_by_role("textbox", name="date")
15+
self.add_non_invitation_day_button = self.page.get_by_role(
16+
"button", name="Add Non-Invitation Day"
17+
)
18+
self.non_invitation_day_delete_button = self.page.get_by_role(
19+
"button", name="Delete"
20+
)
21+
self.created_on_date_locator = self.page.locator(
22+
"tr.oddTableRow td:nth-child(4)"
23+
)
1224

1325
def verify_non_invitation_days_tile(self) -> None:
1426
"""Verifies the page title of the Non Invitation Days page"""
1527
self.bowel_cancer_screening_page_title_contains_text("Non-Invitation Days")
28+
29+
def enter_date(self, date: str) -> None:
30+
"""Enters a date in the date input field
31+
Args:
32+
date (str): The date to enter in the field, formatted as 'dd/mm/yyyy'.
33+
"""
34+
self.enter_date_field.fill(date)
35+
36+
def enter_note(self, note: str) -> None:
37+
"""Enters a note in the note input field
38+
Args:
39+
note (str): The note to enter in the field.
40+
"""
41+
self.enter_note_field.fill(note)
42+
43+
def click_add_non_invitation_day_button(self) -> None:
44+
"""Clicks the Add Non-Invitation Day button"""
45+
self.click(self.add_non_invitation_day_button)
46+
47+
def click_delete_button(self) -> None:
48+
"""Clicks the Delete button for a non-invitation day"""
49+
self.click(self.non_invitation_day_delete_button)
50+
51+
def verify_created_on_date_is_visible(self) -> None:
52+
"""Verifies that the specified date is visible on the page
53+
Args:
54+
date (str): The date to verify, formatted as 'dd/mm/yyyy'.
55+
"""
56+
today = DateTimeUtils.current_datetime("%d/%m/%Y")
57+
expect(self.created_on_date_locator).to_have_text(today)
58+
59+
def verify_created_on_date_is_not_visible(self) -> None:
60+
"""Verifies that the 'created on' date element is not visible on the page.
61+
This is used to confirm that the non-invitation day has been successfully deleted.
62+
"""
63+
expect(self.created_on_date_locator).not_to_be_visible

pages/communication_production/batch_list_page.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ def clear_deadline_filter_date(self) -> None:
9393
def verify_deadline_date_filter_input(self, expected_text: str) -> None:
9494
expect(self.deadline_date_filter_with_input).to_have_value(expected_text)
9595

96+
def open_letter_batch(
97+
self, batch_type: str, status: str, level: str, description: str
98+
) -> None:
99+
"""
100+
Finds and opens the batch row based on type, status, level, and description.
101+
Args:
102+
batch_type (str): The type of the batch (e.g., "Original").
103+
status (str): The status of the batch (e.g., "Open").
104+
level (str): The level of the batch (e.g., "S1").
105+
description (str): The description of the batch (e.g., "Pre-invitation (FIT)").
106+
"""
107+
# Step 1: Match the row using nested filters, one per column value
108+
row = (
109+
self.page.locator("table tbody tr")
110+
.filter(has=self.page.locator("td", has_text=batch_type))
111+
.filter(has=self.page.locator("td", has_text=status))
112+
.filter(has=self.page.locator("td", has_text=level))
113+
.filter(has=self.page.locator("td", has_text=description))
114+
)
115+
116+
# Step 2: Click the "View" link in the matched row
117+
view_link = row.locator(
118+
"a"
119+
) # Click the first link in the row identified in step 1
120+
expect(view_link).to_be_visible()
121+
view_link.click()
122+
96123

97124
class ActiveBatchListPage(BatchListPage):
98125
"""Active Batch List Page locators, and methods for interacting with the Active Batch List page"""

pages/screening_subject_search/subject_events_notes.py

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def __init__(self, page: Page):
2727
self.update_notes_button = self.page.get_by_role("button", name="Update Notes")
2828
self.note_type = self.page.locator("#UI_ADDITIONAL_CARE_NEED_FILTER")
2929
self.note_status = self.page.locator(
30-
"select[name^='UI_SUPPORTING_NOTE_STATUS_']"
30+
"//table[@id='displayRS']/tbody/tr[2]/td[3]/select"
3131
)
3232

3333
def select_additional_care_note(self) -> None:
@@ -87,47 +87,27 @@ def fill_notes(self, notes: str) -> None:
8787
"""Fills the notes field with the provided text."""
8888
self.notes_upto_500_char.fill(notes)
8989

90-
def dismiss_dialog_and_update_notes(self) -> None:
90+
def accept_dialog_and_update_notes(self) -> None:
9191
"""Clicks the 'Update Notes' button and handles the dialog by clicking 'OK'."""
9292
self.page.once("dialog", lambda dialog: dialog.accept())
9393
self.update_notes_button.click()
9494

95-
def dismiss_dialog_and_add_replacement_note(self) -> None:
95+
def accept_dialog_and_add_replacement_note(self) -> None:
9696
"""
9797
Dismisses the dialog and clicks the 'Add Replacement Note' button.
9898
"""
99-
self.page.once("dialog", lambda dialog: dialog.dismiss())
99+
self.page.once("dialog", lambda dialog: dialog.accept())
100100
self.page.get_by_role("button", name="Add Replacement Note").click()
101101

102-
def dismiss_dialog(self) -> None:
103-
"""
104-
Dismisses a dialog when it appears.
105-
"""
106-
self.page.once("dialog", lambda dialog: dialog.dismiss())
107-
108102
def get_title_and_note_from_row(self, row_number: int = 0) -> dict:
109103
"""
110104
Extracts title and note from a specific row's 'Notes' column using dynamic column index.
111105
"""
112-
row_count = self.table_utils.get_row_count()
113-
if row_count == 0:
114-
raise ValueError("The table is empty.")
115-
116-
# Find the 'Notes' column index (1-based)
117-
notes_col_index = self.table_utils.get_column_index("Notes")
118-
if notes_col_index == -1:
119-
raise ValueError("The 'Notes' column was not found in the table.")
120-
121-
# Locate the cell at the specific row and column
122-
cell = self.page.locator(
123-
f"{self.table_utils} > tbody > tr:nth-child({row_number + 1}) > td:nth-child({notes_col_index})"
124-
)
125-
cell_text = cell.inner_text().strip()
126-
127-
lines = cell_text.split("\n")
106+
cell_text= self.table_utils.get_cell_value("Notes", row_number)
107+
lines = cell_text.split("\n\n")
128108
title = lines[0].strip() if len(lines) > 0 else ""
129109
note = lines[1].strip() if len(lines) > 1 else ""
130-
110+
logging.info(f"Extracted title: '{title}' and note: '{note}' from row {row_number}")
131111
return {"title": title, "note": note}
132112

133113

pages/screening_subject_search/subject_screening_search_page.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class SubjectScreeningPage(BasePage):
1010
def __init__(self, page: Page):
1111
super().__init__(page)
1212
self.page = page
13+
self.results_table_locator = "table#subject-search-results"
14+
1315
# Subject Search Criteria - page filters
1416
self.episodes_filter = self.page.get_by_role("radio", name="Episodes")
1517
self.demographics_filter = self.page.get_by_role("radio", name="Demographics")
@@ -159,6 +161,7 @@ def select_dob_using_calendar_picker(self, date) -> None:
159161
CalendarPicker(self.page).v1_calender_picker(date)
160162

161163
def verify_date_of_birth_filter_input(self, expected_text: str) -> None:
164+
"""Verifies that the Date of Birth filter input field has the expected value."""
162165
expect(self.date_of_birth_filter).to_have_value(expected_text)
163166

164167

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ markers =
3737
compartment6: only for compartment 6
3838
compartment1_plan_creation: to run the plan creation for compartment 1
3939
vpn_required: for tests that require a VPN connection
40+
regression: tests that are part of the regression test suite
41+
call_and_recall: tests that are part of the call and recall test suite

tests/bcss_tests.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,9 @@ forename=Pentagram
2121
surname=Absurd
2222
subject_dob=11/01/1934
2323
episode_closed_date=22/09/2020
24+
25+
# ----------------------------------
26+
# CALL AND RECALL TEST DATA
27+
# ----------------------------------
28+
daily_invitation_rate=6
29+
weekly_invitation_rate=130

0 commit comments

Comments
 (0)