Skip to content

Commit e41fb01

Browse files
authored
Selenium to Playwright non invitation days (#94)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> This includes the migration of the call and recall (non invitation days) tests from selenium to playwright and an additional date time utils function to select a random date (weekday) in the future. ## Context <!-- Why is this change required? What problem does it solve? --> This is part of the selenium to playwright migration ## 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 9f7531f commit e41fb01

File tree

4 files changed

+91
-64
lines changed

4 files changed

+91
-64
lines changed

pages/call_and_recall/non_invitations_days_page.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ def __init__(self, page: Page):
1010
super().__init__(page)
1111
self.page = page
1212
# Non Invitation Days - page locators, methods
13-
self.enter_note_field = self.page.get_by_role("textbox", name="note")
13+
self.enter_note_field = self.page.locator("#note")
1414
self.enter_date_field = self.page.get_by_role("textbox", name="date")
1515
self.add_non_invitation_day_button = self.page.get_by_role(
1616
"button", name="Add Non-Invitation Day"
1717
)
1818
self.non_invitation_day_delete_button = self.page.get_by_role(
1919
"button", name="Delete"
2020
)
21-
self.created_on_date_locator = self.page.locator("#displayRS")
21+
self.created_on_date_locator = self.page.locator(
22+
"tr.oddTableRow td:nth-child(4)"
23+
)
2224

2325
def verify_non_invitation_days_tile(self) -> None:
2426
"""Verifies the page title of the Non Invitation Days page"""
@@ -46,18 +48,16 @@ def click_delete_button(self) -> None:
4648
"""Clicks the Delete button for a non-invitation day"""
4749
self.click(self.non_invitation_day_delete_button)
4850

49-
def verify_date_is_visible(self) -> None:
51+
def verify_created_on_date_is_visible(self) -> None:
5052
"""Verifies that the specified date is visible on the page
5153
Args:
5254
date (str): The date to verify, formatted as 'dd/mm/yyyy'.
5355
"""
54-
date = DateTimeUtils.current_datetime("dd/mm/yyyy")
55-
expect(self.created_on_date_locator).to_have_text(date)
56+
today = DateTimeUtils.current_datetime("%d/%m/%Y")
57+
expect(self.created_on_date_locator).to_have_text(today)
5658

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'.
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.
6162
"""
62-
date = DateTimeUtils.current_datetime("dd/mm/yyyy")
63-
expect(self.created_on_date_locator).not_to_have_text(date)
63+
expect(self.created_on_date_locator).not_to_be_visible

tests/regression/call_and_recall/test_non_invitation_days_regression.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from pages.call_and_recall.call_and_recall_page import CallAndRecallPage
55
from pages.call_and_recall.non_invitations_days_page import NonInvitationDaysPage
66
from utils.user_tools import UserTools
7+
from utils.date_time_utils import DateTimeUtils
78

89

910
@pytest.fixture(scope="function", autouse=True)
@@ -24,29 +25,32 @@ def test_add_then_delete_non_invitation_day(page: Page) -> None:
2425
"""
2526
Verifies that a user can add and delete a non-invitation day.
2627
"""
27-
# Scenario: Add then delete a non invitation day
28-
# And I go to "Non-Invitation Days"
28+
test_date = DateTimeUtils().generate_unique_weekday_date()
29+
30+
# When I go to "Non-Invitation Days"
2931
CallAndRecallPage(page).go_to_non_invitation_days_page()
3032

31-
# # The date entered should be a week day, otherwise a warning message will pop up
32-
# When I enter "14/11/2030" in the input box with id "date"
33-
NonInvitationDaysPage(page).enter_date("14/11/2030")
34-
# # Add a new non invitation day
33+
# And I enter a date in the input box with id "date"
34+
# (The date entered should be a week day, otherwise a warning message will pop up)
35+
NonInvitationDaysPage(page).enter_date(test_date)
36+
3537
# And I enter "Add a non invitation day for automated test" in the input box with id "note"
3638
NonInvitationDaysPage(page).enter_note(
3739
"Add a non-invitation day for automated test"
3840
)
3941

4042
# And I click the "Add Non-Invitation Day" button
4143
NonInvitationDaysPage(page).click_add_non_invitation_day_button()
44+
4245
# Then todays date is visible in the non-invitation days table
43-
NonInvitationDaysPage(page).verify_date_is_visible()
46+
NonInvitationDaysPage(page).verify_created_on_date_is_visible()
47+
4448
# When I click the delete button for the non-invitation day
45-
# NonInvitationDaysPage(page).click_delete_button() TODO: this step should be executed as part of the next step (delete this step once confirmed working)
4649
# And I press OK on my confirmation prompt
4750
BasePage(page).safe_accept_dialog(page.get_by_role("button", name="Delete"))
48-
# Then todays date is not visible in the non-invitation days table
49-
NonInvitationDaysPage(page).verify_date_is_not_visible()
51+
52+
# Then the non-invitation days has been successfully deleted
53+
NonInvitationDaysPage(page).verify_created_on_date_is_not_visible()
5054

5155

5256
@pytest.mark.regression

tests_utils/test_date_time_utils.py

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,26 @@ def test_add_days():
3232
assert new_date == date + timedelta(days=5)
3333

3434

35-
def test_get_day_of_week_for_today():
35+
# Valid weekdays for testing get_day_of_week
36+
VALID_WEEKDAYS = [
37+
"Monday",
38+
"Tuesday",
39+
"Wednesday",
40+
"Thursday",
41+
"Friday",
42+
"Saturday",
43+
"Sunday",
44+
]
45+
46+
47+
def test_get_day_of_week_with_specific_date():
3648
dtu = utils.date_time_utils.DateTimeUtils()
37-
date = datetime.now()
38-
day_of_week = dtu.get_a_day_of_week(date)
39-
assert day_of_week in [
40-
"Monday",
41-
"Tuesday",
42-
"Wednesday",
43-
"Thursday",
44-
"Friday",
45-
"Saturday",
46-
"Sunday",
47-
]
48-
49-
50-
def test_get_a_day_of_week():
49+
date = datetime(2023, 11, 8) # Known Wednesday
50+
day_of_week = dtu.get_day_of_week(date)
51+
assert day_of_week in VALID_WEEKDAYS
52+
53+
54+
def test_get_day_of_week_with_default_today():
5155
dtu = utils.date_time_utils.DateTimeUtils()
52-
date = datetime(2023, 11, 8)
53-
day_of_week = dtu.get_a_day_of_week(date)
54-
assert day_of_week in [
55-
"Monday",
56-
"Tuesday",
57-
"Wednesday",
58-
"Thursday",
59-
"Friday",
60-
"Saturday",
61-
"Sunday",
62-
]
56+
day_of_week = dtu.get_day_of_week()
57+
assert day_of_week in VALID_WEEKDAYS

utils/date_time_utils.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from datetime import datetime, timedelta
2+
from typing import Optional
3+
import random
24

35

46
class DateTimeUtils:
@@ -45,27 +47,18 @@ def add_days(date: datetime, days: float) -> datetime:
4547
return date + timedelta(days=days)
4648

4749
@staticmethod
48-
def get_day_of_week_for_today(date: datetime) -> str:
49-
"""Gets the day of the week (e.g., Monday, Tuesday) from the specified date.
50-
51-
Args:
52-
date (datetime): The current date using the now function
53-
54-
Returns:
55-
str: The day of the week relating to the specified date.
50+
def get_day_of_week(date: Optional[datetime] = None) -> str:
5651
"""
57-
return date.strftime("%A")
58-
59-
@staticmethod
60-
def get_a_day_of_week(date: datetime) -> str:
61-
"""Gets the day of the week (e.g., Monday, Tuesday) from the specified date.
52+
Returns the day of the week (e.g., Monday, Tuesday) for the given date.
53+
If no date is provided, uses today’s date.
6254
6355
Args:
64-
date (datetime): The date for which the day of the week will be returned.
56+
date (Optional[datetime]): The date to inspect. Defaults to now.
6557
6658
Returns:
67-
str: The day of the week relating to the specified date.
59+
str: Day of week corresponding to the date.
6860
"""
61+
date = date or datetime.now()
6962
return date.strftime("%A")
7063

7164
@staticmethod
@@ -98,7 +91,8 @@ def screening_practitioner_appointments_report_timestamp_date_format() -> str:
9891

9992
return DateTimeUtils.format_date(datetime.now(), "%d.%m.%Y at %H:%M:%S")
10093

101-
def month_string_to_number(self, string: str) -> int:
94+
@staticmethod
95+
def month_string_to_number(string: str) -> int:
10296
"""
10397
This is used to convert a month from a string to an integer.
10498
It accepts the full month or the short version and is not case sensitive
@@ -123,4 +117,38 @@ def month_string_to_number(self, string: str) -> int:
123117
out = months[month_short]
124118
return out
125119
except Exception:
126-
raise ValueError("Not a month")
120+
raise ValueError(
121+
f"'{string}' is not a valid month name. Accepted values are: {', '.join(months.keys())}"
122+
)
123+
124+
@staticmethod
125+
def generate_unique_weekday_date(start_year: int = 2025) -> str:
126+
"""
127+
Returns a random future weekday (Mon–Fri) date from the given year onward.
128+
129+
The result is in 'dd/mm/yyyy' format and useful for automated tests needing
130+
unique, non-weekend dates. Uses non-cryptographic randomness for variability between runs.
131+
132+
Args:
133+
start_year (int): The minimum year from which the date may be generated. Defaults to 2025.
134+
135+
Returns:
136+
str: A future weekday date in the format 'dd/mm/yyyy'.
137+
"""
138+
# Start from tomorrow to ensure future date
139+
base_date = datetime.now() + timedelta(days=1)
140+
141+
# Keep moving forward until we find a weekday in 2025 or later
142+
while True:
143+
if base_date.weekday() < 5 and base_date.year >= start_year:
144+
break
145+
base_date += timedelta(days=1)
146+
147+
# Add randomness to avoid repeated values across runs
148+
base_date += timedelta(days=random.randint(0, 10))
149+
150+
# Re-check for weekday after shift
151+
while base_date.weekday() >= 5:
152+
base_date += timedelta(days=1)
153+
154+
return base_date.strftime("%d/%m/%Y")

0 commit comments

Comments
 (0)