Skip to content

Commit 4a8dd28

Browse files
committed
Feature/bcss 20580 selenium to playwright call and recall (create a plan) (#93)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> Migration of the call and recall (create a plan) tests from Selenium to Playwright. As part of this PR I have also refactored the Table Util to be able to extract values from tables with 2 header rows. I have also added 2 functions to the create_a_plan_page.py. I will create new branches for the remaining call and recall scenarios (non-invitation days and generate fobt invitations) ## Context <!-- Why is this change required? What problem does it solve? --> This is part of the Selenium to Playwright migration work. ## Type of changes <!-- 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 - [x] I have added tests to cover my changes (where appropriate) - [x] 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 9ba1c04 commit 4a8dd28

File tree

10 files changed

+488
-0
lines changed

10 files changed

+488
-0
lines changed

pages/base_page.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,30 @@ 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)

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/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.get_by_role("textbox", name="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("#displayRS")
1222

1323
def verify_non_invitation_days_tile(self) -> None:
1424
"""Verifies the page title of the Non Invitation Days page"""
1525
self.bowel_cancer_screening_page_title_contains_text("Non-Invitation Days")
26+
27+
def enter_date(self, date: str) -> None:
28+
"""Enters a date in the date input field
29+
Args:
30+
date (str): The date to enter in the field, formatted as 'dd/mm/yyyy'.
31+
"""
32+
self.enter_date_field.fill(date)
33+
34+
def enter_note(self, note: str) -> None:
35+
"""Enters a note in the note input field
36+
Args:
37+
note (str): The note to enter in the field.
38+
"""
39+
self.enter_note_field.fill(note)
40+
41+
def click_add_non_invitation_day_button(self) -> None:
42+
"""Clicks the Add Non-Invitation Day button"""
43+
self.click(self.add_non_invitation_day_button)
44+
45+
def click_delete_button(self) -> None:
46+
"""Clicks the Delete button for a non-invitation day"""
47+
self.click(self.non_invitation_day_delete_button)
48+
49+
def verify_date_is_visible(self) -> None:
50+
"""Verifies that the specified date is visible on the page
51+
Args:
52+
date (str): The date to verify, formatted as 'dd/mm/yyyy'.
53+
"""
54+
date = DateTimeUtils.current_datetime("dd/mm/yyyy")
55+
expect(self.created_on_date_locator).to_have_text(date)
56+
57+
def verify_date_is_not_visible(self) -> None:
58+
"""Verifies that the specified date is not visible on the page
59+
Args:
60+
date (str): The date to verify, formatted as 'dd/mm/yyyy'.
61+
"""
62+
date = DateTimeUtils.current_datetime("dd/mm/yyyy")
63+
expect(self.created_on_date_locator).not_to_have_text(date)

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=28
29+
weekly_invitation_rate=130
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
from playwright.sync_api import Page
3+
from pages.base_page import BasePage
4+
from pages.call_and_recall.call_and_recall_page import CallAndRecallPage
5+
from pages.call_and_recall.invitations_monitoring_page import InvitationsMonitoringPage
6+
from pages.call_and_recall.invitations_plans_page import InvitationsPlansPage
7+
from pages.call_and_recall.create_a_plan_page import CreateAPlanPage
8+
from utils.user_tools import UserTools
9+
10+
11+
@pytest.fixture(scope="function", autouse=True)
12+
def before_each(page: Page):
13+
"""
14+
Before every test is executed, this fixture logs in to BCSS as a test user and navigates to the call and recall page
15+
"""
16+
# Log in to BCSS
17+
UserTools.user_login(page, "Hub Manager State Registered at BCS01")
18+
19+
# Go to call and recall page
20+
BasePage(page).go_to_call_and_recall_page()
21+
22+
23+
@pytest.mark.regression
24+
@pytest.mark.call_and_recall
25+
def test_create_a_plan_set_daily_rate(page: Page, general_properties: dict) -> None:
26+
"""
27+
Verifies that a user is able to click on the Set all button and enter a daily rate.
28+
"""
29+
# When I go to "Invitations Monitoring - Screening Centre"
30+
CallAndRecallPage(page).go_to_planning_and_monitoring_page()
31+
32+
# And I click the link text "BCS001"
33+
InvitationsMonitoringPage(page).go_to_invitation_plan_page(
34+
general_properties["screening_centre_code"]
35+
)
36+
# And I click the "Create a Plan" button
37+
InvitationsPlansPage(page).go_to_create_a_plan_page()
38+
39+
# And I click the set all button
40+
CreateAPlanPage(page).click_set_all_button()
41+
42+
# And I enter "28" in the input box with id "dailyRate"
43+
CreateAPlanPage(page).fill_daily_invitation_rate_field(
44+
general_properties["daily_invitation_rate"]
45+
)
46+
47+
# And I click the "Update" button
48+
CreateAPlanPage(page).click_update_button()
49+
50+
# Then the Weekly Invitation Rate for weeks 1 to 50 is set correctly
51+
# based on a set all daily rate of 28
52+
CreateAPlanPage(page).verify_weekly_invitation_rate_for_weeks(1, 50, "140")
53+
54+
55+
@pytest.mark.regression
56+
@pytest.mark.call_and_recall
57+
def test_create_a_plan_weekly_rate(page: Page, general_properties: dict) -> None:
58+
"""
59+
Verifies that a user can set a weekly invitation rate in Create a Plan.
60+
"""
61+
62+
# When I go to "Invitations Monitoring - Screening Centre"
63+
CallAndRecallPage(page).go_to_planning_and_monitoring_page()
64+
65+
# And I click the link text "BCS001"
66+
InvitationsMonitoringPage(page).go_to_invitation_plan_page(
67+
general_properties["screening_centre_code"]
68+
)
69+
# And I click the "Create a Plan" button
70+
InvitationsPlansPage(page).go_to_create_a_plan_page()
71+
72+
# And I click the set all button
73+
CreateAPlanPage(page).click_set_all_button()
74+
75+
# And I enter "130" in the input box with id "weeklyRate"
76+
CreateAPlanPage(page).fill_weekly_invitation_rate_field(
77+
general_properties["weekly_invitation_rate"]
78+
)
79+
80+
# And I click the "Update" button
81+
CreateAPlanPage(page).click_update_button()
82+
83+
# And the Weekly Invitation Rate for weeks 1 to 50 is set to the set all weekly rate of 130
84+
CreateAPlanPage(page).verify_weekly_invitation_rate_for_weeks(1, 50, "130")
85+
86+
87+
@pytest.mark.regression
88+
@pytest.mark.call_and_recall
89+
def test_update_invitation_rate_weekly(page: Page, general_properties: dict) -> None:
90+
"""
91+
Verifies that a Hub Manager State Registered is able to update a weekly Invitation rate
92+
and the Cumulative 'Invitations sent' and 'Resulting Position' values are updated.
93+
"""
94+
95+
# When I go to "Invitations Monitoring - Screening Centre"
96+
CallAndRecallPage(page).go_to_planning_and_monitoring_page()
97+
98+
# And I click the link text "BCS001"
99+
InvitationsMonitoringPage(page).go_to_invitation_plan_page(
100+
general_properties["screening_centre_code"]
101+
)
102+
103+
# And I click the "Create a Plan" button
104+
InvitationsPlansPage(page).go_to_create_a_plan_page()
105+
106+
# When I increase the Weekly Invitation Rate for week 1 by 1 and tab out of the cell
107+
# Then the Cumulative Invitations Sent is incremented by 1 for week 1
108+
# And the Cumulative Resulting Position is incremented by 1 for week 1
109+
CreateAPlanPage(page).increment_invitation_rate_and_verify_changes()

0 commit comments

Comments
 (0)