Skip to content

Commit 48dad37

Browse files
Andyg79victor-soares-ibmvidhya-chandra1adrianoaru-nhs
authored
Feature/bcss 20581 selenium to playwright communications production (#106)
<!-- markdownlint-disable-next-line first-line-heading --> ## Description <!-- Describe your changes in detail. --> Part of the selenium to playwright migration - this covers the communications production tests ## Context <!-- Why is this change required? What problem does it solve? --> ## Type of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. --> Includes the migration of selenium tests to the playwright framework and any applicable updates/additions to POMS and Utils. - [x] Refactoring (non-breaking change) - [x] New feature (non-breaking change which adds functionality) - [x] 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: victor-soares-ibm <[email protected]> Co-authored-by: vidhya-chandra1 <[email protected]> Co-authored-by: adrianoaru-nhs <[email protected]>
1 parent 1e7f7bb commit 48dad37

19 files changed

+2090
-59
lines changed

docs/utility-guides/Oracle.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# Utility Guide: Oracle
22

3-
The Oracle Utility provides an easy way to interact with an Oracle database directly from your Playwright test suite.It can be used to run SQL queries or stored procedures on the Oracle database.
3+
The Oracle Utility provides an easy way to interact with an Oracle database directly from your Playwright test suite. It can be used to run SQL queries or stored procedures on the Oracle database.
44

55
## When and Why to Use the Oracle Utility
66

7-
You might need to use this utility in scenarios such as:
7+
You might use this utility for:
88

9-
- To run SQL queries or stored procedures on the Oracle database.
10-
- Verifying that data is correctly written to or updated in the database after a workflow is completed in your application.
9+
- Running SQL queries or stored procedures on the Oracle database
10+
11+
- Validating application workflows against live database records
12+
13+
- Creating test data dynamically via helper functions
14+
15+
- Verifying data integrity after system events
1116

1217
## Table of Contents
1318

@@ -26,6 +31,10 @@ You might need to use this utility in scenarios such as:
2631

2732
To use the Oracle Utility, import the 'OracleDB' class into your test file and then call the OracleDB methods from within your tests, as required.
2833

34+
```python
35+
from utils.oracle.oracle import OracleDB
36+
```
37+
2938
## Required arguments
3039

3140
The functions in this class require different arguments.<br>
@@ -42,6 +51,7 @@ The docstrings also specify when arguments are optional, and what the default va
4251
- **execute_stored_procedure(self, `procedure_name`, params=None)**: Executes a named stored procedure with optional parameters.
4352
- **exec_bcss_timed_events(self, nhs_number_df)**: Runs the `bcss_timed_events` stored procedure for each NHS number provided in a DataFrame.
4453
- **get_subject_id_from_nhs_number(self, nhs_number)**: Retrieves the `subject_screening_id` for a given NHS number.
54+
- **create_subjects_via_sspi(...)**: Creates synthetic subjects using stored procedure `PKG_SSPI.p_process_pi_subject`
4555

4656
For full implementation details, see utils/oracle/oracle.py.
4757

@@ -68,6 +78,16 @@ def test_oracle_query() -> None:
6878

6979
def run_stored_procedure() -> None:
7080
OracleDB().execute_stored_procedure("bcss_timed_events")
81+
82+
def create_subjects_dynamically() -> None:
83+
OracleDB().create_subjects_via_sspi(
84+
count=5,
85+
screening_centre="BCS01",
86+
base_age=60,
87+
start_offset=-2,
88+
end_offset=4,
89+
nhs_start=9200000000
90+
)
7191
```
7292

7393
---

pages/call_and_recall/generate_invitations_page.py

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
from pages.base_page import BasePage
33
import pytest
44
import logging
5+
from utils.oracle.oracle import OracleDB
6+
from utils.oracle.subject_selection_query_builder import SubjectSelectionQueryBuilder
7+
from classes.user import User
8+
from classes.subject import Subject
9+
from utils.table_util import TableUtils
10+
11+
DISPLAY_RS_SELECTOR = "#displayRS"
512

613

714
class GenerateInvitationsPage(BasePage):
@@ -14,10 +21,10 @@ def __init__(self, page: Page):
1421
self.generate_invitations_button = self.page.get_by_role(
1522
"button", name="Generate Invitations"
1623
)
17-
self.display_rs = self.page.locator("#displayRS")
24+
self.display_rs = self.page.locator(DISPLAY_RS_SELECTOR)
1825
self.refresh_button = self.page.get_by_role("button", name="Refresh")
1926
self.planned_invitations_total = self.page.locator("#col8_total")
20-
self.self_referrals_total = self.page.locator("#col9_total")
27+
self.self_referrals_total = self.page.locator("#col5_total")
2128

2229
def click_generate_invitations_button(self) -> None:
2330
"""This function is used to click the Generate Invitations button."""
@@ -45,7 +52,7 @@ def wait_for_invitation_generation_complete(
4552
Every 5 seconds it refreshes the table and checks to see if the invitations have been generated.
4653
It also checks that enough invitations were generated and checks to see if self referrals are present
4754
"""
48-
self.page.wait_for_selector("#displayRS", timeout=5000)
55+
self.page.wait_for_selector(DISPLAY_RS_SELECTOR, timeout=5000)
4956

5057
if self.planned_invitations_total == "0":
5158
pytest.fail("There are no planned invitations to generate")
@@ -103,3 +110,132 @@ def wait_for_invitation_generation_complete(
103110
else:
104111
logging.warning("No S1 Digital Leaflet batch will be generated")
105112
return False
113+
114+
def wait_for_self_referral_invitation_generation_complete(
115+
self, expected_minimum: int = 1
116+
) -> bool:
117+
"""
118+
Waits until the invitations have been generated and checks that 'Self Referrals Generated' meets the expected threshold.
119+
120+
Args:
121+
expected_minimum (int): Minimum number of self-referrals expected to be generated (default is 1)
122+
123+
Returns:
124+
bool: True if threshold is met, False otherwise.
125+
"""
126+
# Reuse the existing table completion logic — consider extracting this into a shared method later
127+
timeout = 120000
128+
wait_interval = 5000
129+
elapsed = 0
130+
logging.info(
131+
"[WAIT] Waiting for self-referral invitation generation to complete"
132+
)
133+
134+
while elapsed < timeout:
135+
table_text = self.display_rs.text_content()
136+
if table_text is None:
137+
pytest.fail("Failed to retrieve table text content")
138+
139+
if "Failed" in table_text:
140+
pytest.fail("Invitation has failed to generate")
141+
elif "Queued" in table_text or "In Progress" in table_text:
142+
self.click_refresh_button()
143+
self.page.wait_for_timeout(wait_interval)
144+
elapsed += wait_interval
145+
else:
146+
break
147+
148+
try:
149+
expect(self.display_rs).to_contain_text("Completed")
150+
logging.info(
151+
f"[STATUS] Generation finished after {elapsed / 1000:.1f} seconds"
152+
)
153+
except Exception as e:
154+
pytest.fail(f"[ERROR] Invitations not generated successfully: {str(e)}")
155+
156+
# Dynamically check 'Self Referrals Generated'
157+
table_utils = TableUtils(self.page, DISPLAY_RS_SELECTOR)
158+
159+
try:
160+
value_text = table_utils.get_footer_value_by_header(
161+
"Self Referrals Generated"
162+
)
163+
value = int(value_text.strip())
164+
logging.info(f"[RESULT] 'Self Referrals Generated' = {value}")
165+
except Exception as e:
166+
pytest.fail(f"[ERROR] Unable to read 'Self Referrals Generated': {str(e)}")
167+
168+
if value >= expected_minimum:
169+
return True
170+
else:
171+
logging.warning(
172+
f"[ASSERTION] Expected at least {expected_minimum}, but got {value}"
173+
)
174+
return False
175+
176+
def check_self_referral_subjects_ready(
177+
self, search_scope: str, volume: str
178+
) -> None:
179+
"""
180+
Asserts whether self-referral subjects are ready to invite.
181+
182+
Args:
183+
search_scope (str): Either "currently" or "now" to determine when to assert
184+
volume (str): Either "some" or "no" — expected number of self-referrals
185+
186+
Raises:
187+
AssertionError if the count doesn't match expectation and search_scope is "now"
188+
"""
189+
assert search_scope in (
190+
"currently",
191+
"now",
192+
), f"Invalid search_scope: '{search_scope}'"
193+
assert volume in ("some", "no"), f"Invalid volume: '{volume}'"
194+
195+
# Confirm we're on the Generate Invitations page
196+
self.verify_generate_invitations_title()
197+
198+
# Extract and clean the count
199+
self_referrals_text = self.self_referrals_total.text_content()
200+
if self_referrals_text is None:
201+
pytest.fail("Failed to read self-referrals total")
202+
else:
203+
self_referrals_text = self_referrals_text.strip()
204+
205+
self_referrals_count = int(self_referrals_text)
206+
207+
# Determine if condition is met
208+
condition_met = (
209+
self_referrals_count > 0 if volume == "some" else self_referrals_count == 0
210+
)
211+
212+
logging.debug(f"[DEBUG] Self-referral subject count = {self_referrals_count}")
213+
214+
if search_scope == "now":
215+
assert (
216+
condition_met
217+
), f"Expected {volume.upper()} self-referral subjects, but got {self_referrals_count}"
218+
logging.info(
219+
f"[SELF-REFERRAL CHECK] scope='{search_scope}' | expected='{volume}' | actual={self_referrals_count}"
220+
)
221+
222+
def get_self_referral_eligible_subject(self, user: User, subject: Subject) -> str:
223+
criteria = {
224+
"screening status": "Inactive",
225+
"subject age": ">= 75",
226+
"has GP practice": "Yes - active",
227+
"subject hub code": "BCS02",
228+
}
229+
230+
builder = SubjectSelectionQueryBuilder()
231+
query, bind_vars = builder.build_subject_selection_query(
232+
criteria, user, subject
233+
)
234+
235+
oracle = OracleDB()
236+
result = oracle.execute_query(query, bind_vars)
237+
238+
if result.empty:
239+
raise RuntimeError("No eligible subject found")
240+
241+
return result.iloc[0]["subject_nhs_number"]

0 commit comments

Comments
 (0)