Skip to content

Commit eb47649

Browse files
Feature/bcss 21310 selenium to playwright fobtregressiontests scenario 7 (#138)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> Adding scenario 7 from fobtregressiontests.feature ## Context <!-- Why is this change required? What problem does it solve? --> Adding scenario 7 from fobtregressiontests.feature ## 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. --------- Co-authored-by: AndyG <[email protected]>
1 parent 493ae29 commit eb47649

File tree

9 files changed

+917
-10
lines changed

9 files changed

+917
-10
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from enum import Enum
2+
from typing import Optional
3+
4+
5+
class DeductionReasonType(Enum):
6+
"""
7+
Enum representing deduction reason types, mapped to valid value IDs, allowed values, and descriptions.
8+
Provides utility methods for lookup by description, allowed value, and valid value ID.
9+
"""
10+
11+
AFL = (307006, "AFL", "AF Enlistment (local)")
12+
AFN = (307007, "AFN", "AF Enlistment (AF)")
13+
CGA = (307008, "CGA", "Gone away")
14+
DEA = (307009, "DEA", "Death")
15+
Death = (307009, "DEA", "Death") # Alias for DEA
16+
DDR = (1, "DDR", "Deducted at Doctor's request")
17+
DIS = (307010, "DIS", "Practice dissolution")
18+
DPR = (2, "DPR", "Deducted at Patient's request")
19+
EMB = (307011, "EMB", "Embarkation")
20+
Embarkation = (307011, "EMB", "Embarkation") # Alias for EMB
21+
FP69 = (17, "FP69", "Deducted as a result of a non-respond to an FP69/")
22+
LDN = (307012, "LDN", "Logical Deletion")
23+
MH = (7, "M/H", "Mental Hospital")
24+
NIT = (307013, "NIT", "Transfer to Northern Ireland")
25+
OR = (10, "O/R", "Other")
26+
OPA = (307014, "OPA", "Address out of practice area")
27+
ORR = (307015, "ORR", "Other Reason")
28+
PAR = (13, "PAR", "Practice advise subject no longer resident")
29+
PSR = (14, "PSR", "Practice advise removal via screening system")
30+
PVR = (15, "PVR", "Practice advise removal via vaccination data")
31+
R = (26, "R", "Deducted as a result of removal to other HA")
32+
RA = (8, "R/A", "Registration/A")
33+
RC = (9, "R/C", "Registration Cancelled")
34+
RU = (27, "R/U", "Deducted as a result of return undelivered")
35+
RDI = (307016, "RDI", "Practice Request - immediate")
36+
RDR = (307017, "RDR", "Practice request")
37+
RFI = (307018, "RFI", "Residential Institute")
38+
RPR = (307019, "RPR", "Patient request")
39+
SD = (6, "S/D", "Service Dependant")
40+
SCT = (307020, "SCT", "Transferred to Scotland")
41+
SDL = (307021, "SDL", "Services Dependant (local)")
42+
SDN = (307022, "SDN", "Services Dependant (SMU)")
43+
SER = (5, "SER", "Services")
44+
TRA = (307023, "TRA", "Temporary resident not returned")
45+
46+
def __init__(
47+
self, valid_value_id: int, allowed_value: str, description: str
48+
) -> None:
49+
self._valid_value_id = valid_value_id
50+
self._allowed_value = allowed_value
51+
self._description = description
52+
53+
@property
54+
def valid_value_id(self) -> int:
55+
"""Returns the valid value ID for the deduction reason type."""
56+
return self._valid_value_id
57+
58+
@property
59+
def allowed_value(self) -> str:
60+
"""Returns the allowed value for the deduction reason type."""
61+
return self._allowed_value
62+
63+
@property
64+
def description(self) -> str:
65+
"""Returns the description for the deduction reason type."""
66+
return self._description
67+
68+
@classmethod
69+
def by_description(cls, description: str) -> Optional["DeductionReasonType"]:
70+
"""
71+
Returns the enum member matching the given description (case-sensitive).
72+
Args:
73+
description (str): The description to match.
74+
Returns:
75+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
76+
"""
77+
for member in cls:
78+
if member.description == description:
79+
return member
80+
return None
81+
82+
@classmethod
83+
def by_description_case_insensitive(
84+
cls, description: str
85+
) -> Optional["DeductionReasonType"]:
86+
"""
87+
Returns the enum member matching the given description (case-insensitive).
88+
Args:
89+
description (str): The description to match.
90+
Returns:
91+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
92+
"""
93+
desc_lower = description.lower()
94+
for member in cls:
95+
if member.description.lower() == desc_lower:
96+
return member
97+
return None
98+
99+
@classmethod
100+
def by_deduction_code(cls, code: str) -> Optional["DeductionReasonType"]:
101+
"""
102+
Returns the enum member matching the given allowed value (deduction code).
103+
Args:
104+
code (str): The code to match.
105+
Returns:
106+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
107+
"""
108+
code_upper = code.upper()
109+
for member in cls:
110+
if member.allowed_value.upper() == code_upper:
111+
return member
112+
return None
113+
114+
@classmethod
115+
def by_valid_value_id(cls, valid_value_id: int) -> Optional["DeductionReasonType"]:
116+
"""
117+
Returns the enum member matching the given valid value ID.
118+
Args:
119+
valid_value_id (str): The valid_value_id to match.
120+
Returns:
121+
Optional[DeductionReasonType]: The matching enum member, or None if not found.
122+
"""
123+
for member in cls:
124+
if member.valid_value_id == valid_value_id:
125+
return member
126+
return None

classes/episode/subject_has_episode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def by_description(cls, description: str) -> Optional["SubjectHasEpisode"]:
3232
Optional[SubjectHasEpisode]: The matching enum member, or None if not found.
3333
"""
3434
for item in cls:
35-
if item.value == description:
35+
if item.value.lower() == description.lower():
3636
return item
3737
return None
3838

classes/repositories/subject_repository.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import logging
22
from typing import Optional
33
from classes.subject.pi_subject import PISubject
4+
from classes.subject.subject import Subject
5+
from classes.user.user import User
46
from utils.oracle.oracle import OracleDB
7+
from utils.oracle.subject_selection_query_builder import SubjectSelectionQueryBuilder
58

69

710
class SubjectRepository:
@@ -193,3 +196,32 @@ def get_latest_gp_practice_for_subject(self, nhs_number: str) -> Optional[str]:
193196
if df.empty:
194197
return None
195198
return df.iloc[0]["gp_code"]
199+
200+
def get_matching_subject(
201+
self, criteria: dict, subject: Subject, user: User
202+
) -> Subject:
203+
"""
204+
Finds a subject in the DB matching the given criteria.
205+
Args:
206+
criteria (dict): The criteria you want the subject to match
207+
subject (Subject): The subject class object. Can be populated or not, depends on the criteria
208+
user (User): The user class object. Can be populated or not, depends on the criteria
209+
Returns:
210+
Subject: A populated Subject object with a subject that matches the provided criteria
211+
Raises:
212+
ValueError: If no subject matching the criteria was found.
213+
"""
214+
builder = SubjectSelectionQueryBuilder()
215+
query, bind_vars = builder.build_subject_selection_query(
216+
criteria=criteria,
217+
user=user,
218+
subject=subject,
219+
subjects_to_retrieve=1,
220+
)
221+
df = OracleDB().execute_query(query, bind_vars)
222+
try:
223+
df_row = df.iloc[0]
224+
subject = Subject().from_dataframe_row(df_row)
225+
except Exception:
226+
raise ValueError("No subject found matching the given criteria")
227+
return subject

pages/screening_practitioner_appointments/appointment_detail_page.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from playwright.sync_api import Page, expect
22
from pages.base_page import BasePage
33
from enum import StrEnum
4+
from datetime import datetime
5+
from utils.calendar_picker import CalendarPicker
46

57

68
class AppointmentDetailPage(BasePage):
@@ -87,6 +89,23 @@ def select_reason_for_cancellation_option(self, option: str) -> None:
8789
self.reason_for_cancellation_dropdown.select_option(value=option)
8890

8991

92+
def mark_appointment_as_attended(self, date: datetime) -> None:
93+
"""
94+
Marks an appointment as attended.
95+
Args:
96+
date (datetime): The date the appointment was attended
97+
"""
98+
self.wait_for_attendance_radio(
99+
600000
100+
) # Max of 10 minute wait as appointments need to be set for future times and they are in 10 minute intervals
101+
self.check_attendance_radio()
102+
self.check_attended_check_box()
103+
self.click_calendar_button()
104+
CalendarPicker(self.page).v1_calender_picker(date)
105+
self.click_save_button()
106+
self.verify_text_visible("Record updated")
107+
108+
90109
class ReasonForCancellationOptions(StrEnum):
91110
"""Enum for cancellation reason options"""
92111

pages/screening_subject_search/advance_fobt_screening_episode_page.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@ def __init__(self, page: Page):
4444
self.record_contact_with_patient_button = self.page.get_by_role(
4545
"button", name="Record Contact with Patient"
4646
)
47+
self.amend_diagnosis_date_button = self.page.get_by_role(
48+
"button", name="Amend Diagnosis Date"
49+
)
50+
self.advance_checkbox_v2 = self.page.get_by_role("checkbox")
51+
self.subsequent_assessment_appointment_required_dropdown = (
52+
self.page.get_by_role("combobox")
53+
)
54+
self.subsequent_assessment_appointment_required_button = self.page.get_by_role(
55+
"button", name="Subsequent Assessment Appointment Required"
56+
)
4757
self.suitable_for_radiological_test_button = self.page.get_by_role(
4858
"button", name="Suitable for Radiological Test"
4959
)
@@ -57,6 +67,9 @@ def __init__(self, page: Page):
5767
"button", name="Waiting Decision to Proceed with Diagnostic Test"
5868
)
5969
)
70+
self.not_suitable_for_diagnostic_tests_button = self.page.get_by_role(
71+
"button", name="Not Suitable for Diagnostic Tests"
72+
)
6073

6174
def click_suitable_for_endoscopic_test_button(self) -> None:
6275
"""Click the 'Suitable for Endoscopic Test' button."""
@@ -126,6 +139,28 @@ def check_advance_checkbox(self) -> None:
126139
"""Selects the 'Advance FOBT' checkbox"""
127140
self.advance_checkbox.check()
128141

142+
def click_amend_diagnosis_date_button(self) -> None:
143+
"""Checks the 'Advance FOBT' checkbox and clicks the 'Amend Diagnosis Date' button."""
144+
self.advance_checkbox_v2.check()
145+
self.click(self.amend_diagnosis_date_button)
146+
147+
def click_and_select_subsequent_assessment_appointment_required(
148+
self, option: str
149+
) -> None:
150+
"""
151+
Click the 'Subsequent Assessment Appointment Required' button and select an option from the dropdown.
152+
Args:
153+
option (str): The option to select from the dropdown.
154+
Must be one of:
155+
- 'Previous attendance, further assessment required'
156+
- 'Interpreter requirement not identified'
157+
- 'SC interpreter DNA'
158+
"""
159+
self.subsequent_assessment_appointment_required_dropdown.select_option(
160+
label=option
161+
)
162+
self.safe_accept_dialog(self.subsequent_assessment_appointment_required_button)
163+
129164
def click_suitable_for_radiological_test_button(self) -> None:
130165
"""Click the 'Suitable for Radiological Test' button."""
131166
self.safe_accept_dialog(self.suitable_for_radiological_test_button)
@@ -141,3 +176,7 @@ def click_waiting_decision_to_proceed_with_diagnostic_test(self) -> None:
141176
self.safe_accept_dialog(
142177
self.waiting_decision_to_proceed_with_diagnostic_test_button
143178
)
179+
180+
def click_not_suitable_for_diagnostic_tests_button(self) -> None:
181+
"""Click the 'Not Suitable for Diagnostic Tests' button."""
182+
self.safe_accept_dialog(self.not_suitable_for_diagnostic_tests_button)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from playwright.sync_api import Page
2+
from pages.base_page import BasePage
3+
from datetime import datetime
4+
from utils.calendar_picker import CalendarPicker
5+
6+
7+
class PatientAdvisedOfDiagnosisPage(BasePage):
8+
"""Advance FOBT Screening Episode Page locators, and methods for interacting with the page."""
9+
10+
def __init__(self, page: Page):
11+
super().__init__(page)
12+
self.page = page
13+
# Patient Advised Of Diagnosis - page locators
14+
self.diagnosis_date_calendar_icon = self.page.locator("#diagnosisAdvice span")
15+
self.reason_dropdown = self.page.locator("#reason")
16+
self.save_button = self.page.get_by_role("button", name="Save")
17+
18+
def select_diagnosis_date_and_reason(self, date: datetime, reason: str) -> None:
19+
"""
20+
Selects the diagnosis date and reason, then saves the form.
21+
Args:
22+
date (datetime): The diagnosis date to select.
23+
reason (str): The reason for the diagnosis.
24+
"""
25+
self.click(self.diagnosis_date_calendar_icon)
26+
CalendarPicker(self.page).v2_calendar_picker(date)
27+
self.reason_dropdown.select_option(label=reason)
28+
self.click(self.save_button)

pages/screening_subject_search/reopen_fobt_screening_episode_page.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ def __init__(self, page: Page):
1212
self.reopen_to_book_an_assessment_button = self.page.get_by_role(
1313
"button", name="Reopen to book an assessment"
1414
)
15+
self.reopen_episode_for_correction_button = self.page.get_by_role(
16+
"button", name="Reopen episode for correction"
17+
)
18+
self.reopen_due_to_subject_or_patient_decision = self.page.get_by_role(
19+
"button", name="Reopen due to subject or patient decision"
20+
)
1521
self.reopen_following_non_response_button = self.page.get_by_role(
1622
"button", name="Reopen following Non-Response"
1723
)
@@ -20,6 +26,14 @@ def click_reopen_to_book_an_assessment_button(self) -> None:
2026
"""Click the 'Reopen to book an assessment' button."""
2127
self.safe_accept_dialog(self.reopen_to_book_an_assessment_button)
2228

29+
def click_reopen_episode_for_correction_button(self) -> None:
30+
"""Click the 'Reopen episode for correction' button."""
31+
self.safe_accept_dialog(self.reopen_episode_for_correction_button)
32+
33+
def click_reopen_due_to_subject_or_patient_decision(self) -> None:
34+
"""Click the 'Reopen due to subject or patient decision' button."""
35+
self.safe_accept_dialog(self.reopen_due_to_subject_or_patient_decision)
36+
2337
def click_reopen_following_non_response_button(self) -> None:
2438
"""Click the 'Reopen following Non-Response' button."""
2539
self.safe_accept_dialog(self.reopen_following_non_response_button)

0 commit comments

Comments
 (0)