Skip to content

Commit d29416c

Browse files
Adding both scenarios from _Setup.feature
1 parent ee3cd25 commit d29416c

File tree

16 files changed

+850
-101
lines changed

16 files changed

+850
-101
lines changed

.test_last_runs.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"subject/episodes/datasets/investigation/endoscopy/polypcategories/test_setup": "2025-07-09"
3+
}

conftest.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,46 @@ def smokescreen_properties() -> dict:
3535
@pytest.fixture
3636
def general_properties() -> dict:
3737
return PropertiesFile().get_general_properties()
38+
39+
40+
from typing import Any
41+
import pytest
42+
from _pytest.config.argparsing import Parser
43+
from _pytest.fixtures import FixtureRequest
44+
45+
46+
def pytest_addoption(parser: Parser) -> None:
47+
"""
48+
Add custom command-line options to pytest.
49+
50+
Args:
51+
parser (Parser): The pytest parser object used to define CLI options.
52+
53+
Adds:
54+
--subjects-to-run-for (int):
55+
The number of subjects to run the test setup for.
56+
Default is 10.
57+
58+
Example:
59+
pytest tests/test_setup.py::test_setup_subjects_as_a259 --subjects-to-run-for=5
60+
"""
61+
parser.addoption(
62+
"--subjects-to-run-for",
63+
action="store",
64+
default="10",
65+
help="Number of subjects to run the test setup for (default: 10)",
66+
)
67+
68+
69+
@pytest.fixture
70+
def subjects_to_run_for(request: FixtureRequest) -> int:
71+
"""
72+
Fixture to retrieve the value of the '--subjects-to-run-for' CLI argument.
73+
74+
Args:
75+
request (FixtureRequest): Provides access to the requesting test context.
76+
77+
Returns:
78+
int: The number of subjects specified via the CLI or default (10).
79+
"""
80+
return int(request.config.getoption("--subjects-to-run-for")) # type: ignore

docs/utility-guides/LastTestRun.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Utility Guide: last_test_run
2+
3+
The `last_test_run` utility provides a simple way to track when specific tests were last executed. It is designed to help you avoid running setups for tests multiple times and instead does a check to see if the test has been run today.
4+
5+
---
6+
7+
## Table of Contents
8+
9+
- [Utility Guide: last\_test\_run](#utility-guide-last_test_run)
10+
- [Table of Contents](#table-of-contents)
11+
- [Overview](#overview)
12+
- [When and Why to Use last\_test\_run](#when-and-why-to-use-last_test_run)
13+
- [Required Arguments](#required-arguments)
14+
- [last\_test\_run Methods](#last_test_run-methods)
15+
- [Example Usage](#example-usage)
16+
- [How It Works](#how-it-works)
17+
- [Implementation Details](#implementation-details)
18+
19+
---
20+
21+
## Overview
22+
23+
This utility manages a JSON file (`.test_last_runs.json`) that records the last date each test was run. It provides functions to load, save, and check this data, making it easy to implement "run once per day" logic in your test suite.
24+
25+
---
26+
27+
## When and Why to Use last_test_run
28+
29+
You might want to use this utility in scenarios such as:
30+
31+
- Avoiding repeated execution of slow or stateful tests within the same day.
32+
- Ensuring setup or teardown routines only run once per day.
33+
- Tracking test execution dates for reporting or debugging.
34+
35+
---
36+
37+
## Required Arguments
38+
39+
Each function in this utility requires specific arguments:
40+
41+
- `has_test_run_today(test_name: str) -> bool`:
42+
- `test_name` (str): The unique name of the test to check.
43+
44+
See the docstrings in the code for details on each function.
45+
46+
---
47+
48+
## last_test_run Methods
49+
50+
**The main methods provided are:**
51+
52+
- **load_last_run_data() -> Dict[str, Any]**
53+
Loads the last run data from the JSON file. Returns a dictionary mapping test names to their last run date.
54+
55+
- **save_last_run_data(data: Dict[str, Any]) -> None**
56+
Saves the provided dictionary to the JSON file.
57+
58+
- **has_test_run_today(test_name: str) -> bool**
59+
Checks if the given test has already run today. If not, updates the record to mark it as run today.
60+
61+
---
62+
63+
## Example Usage
64+
65+
```python
66+
from utils.last_test_run import has_test_run_today
67+
68+
def test_expensive_setup():
69+
if has_test_run_today("test_expensive_setup"):
70+
print("Setup already run today, skipping.")
71+
return
72+
# ... perform expensive setup ...
73+
print("Setup complete.")
74+
```
75+
76+
---
77+
78+
## How It Works
79+
80+
- The utility stores a mapping of test names to their last run date in `.test_last_runs.json`.
81+
- When `has_test_run_today` is called, it checks if the test has already run today.
82+
- If yes, it returns `True`.
83+
- If no, it updates the file and returns `False`.
84+
85+
---
86+
87+
## Implementation Details
88+
89+
- The JSON file is created in the project root if it does not exist.
90+
- If the file is empty or corrupted, the utility will safely return an empty dictionary and continue.
91+
- All functions are type-annotated and documented with docstrings for clarity.
92+
93+
---
94+
95+
> **Note:**
96+
> The `last_test_run` utility is available under `utils/last_test_run.py`.
97+
> See the source code for more details and to extend its functionality as needed.

pages/base_page.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def __init__(self, page: Page):
5353
"#ntshPageTitle"
5454
)
5555
self.main_menu__header = self.page.locator("#ntshPageTitle")
56+
self.log_in_page = self.page.get_by_role("button", name="Log in")
5657

5758
def click_main_menu_link(self) -> None:
5859
"""Click the Base Page 'Main Menu' link if it is visible."""
@@ -270,3 +271,7 @@ def handle_dialog(dialog: Dialog):
270271
dialog.dismiss() # Dismiss dialog
271272

272273
self.page.once("dialog", handle_dialog)
274+
275+
def go_to_log_in_page(self) -> None:
276+
"""Click on the Log in button to navigate to the login page."""
277+
self.click(self.log_in_page)

pages/datasets/cancer_audit_datasets_page.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from playwright.sync_api import Page
22
from pages.base_page import BasePage
3-
from enum import Enum
3+
from enum import StrEnum
44
from utils.calendar_picker import CalendarPicker
55
from datetime import datetime
66
from pages.datasets.investigation_dataset_page import (
@@ -26,6 +26,9 @@ def __init__(self, page: Page):
2626
self.save_dataset_button = self.page.locator(
2727
"#UI_DIV_BUTTON_SAVE1"
2828
).get_by_role("button", name="Save Dataset")
29+
self.edit_dataset_button = self.page.locator(
30+
"#UI_DIV_BUTTON_EDIT1"
31+
).get_by_role("button", name="Edit Dataset")
2932
# Types of Scan
3033
self.abdominal_ultrasound_checkbox = self.page.get_by_role(
3134
"checkbox", name="Abdominal Ultrasound"
@@ -52,6 +55,10 @@ def __init__(self, page: Page):
5255
self.other_textbox = self.page.get_by_role(
5356
"textbox", name="Please enter the other"
5457
)
58+
# Other Staging and Pre-Treatment Information locators
59+
self.treatment_received_select = self.page.get_by_label(
60+
"Treatment Received", exact=True
61+
)
5562
# Tumour Information
5663
self.date_of_diagnosis_textbox = self.page.get_by_role(
5764
"textbox", name="Date of Diagnosis"
@@ -83,6 +90,10 @@ def click_save_dataset_button(self) -> None:
8390
"""Clicks on the 'Save Dataset' button."""
8491
self.click(self.save_dataset_button)
8592

93+
def click_edit_dataset_button(self) -> None:
94+
"""Clicks on the 'Edit Dataset' button"""
95+
self.click(self.edit_dataset_button)
96+
8697
def check_abdominal_ultrasound_checkbox(self) -> None:
8798
"""Checks the 'Abdominal Ultrasound' checkbox."""
8899
self.abdominal_ultrasound_checkbox.check()
@@ -132,6 +143,10 @@ def fill_other_textbox(self, text: str) -> None:
132143
self.other_textbox.fill(text)
133144
self.other_textbox.press("Tab")
134145

146+
def select_treatment_received_option(self, option: str) -> None:
147+
"""Select and option from the 'Treatment Received' dropdown"""
148+
self.treatment_received_select.select_option(option)
149+
135150
def fill_date_of_diagnosis_textbox(self, date: datetime) -> None:
136151
"""Fills the 'Date of Diagnosis' textbox with the provided date."""
137152
CalendarPicker(self.page).calendar_picker_ddmmyyyy(
@@ -160,7 +175,7 @@ def select_first_definitive_teatment_information_option(self, option: str) -> No
160175
NOT_REPORTED_CODE = "202140~~202188"
161176

162177

163-
class ASAGradeOptions(Enum):
178+
class ASAGradeOptions(StrEnum):
164179
"""Enum for ASA Grade options."""
165180

166181
FIT = "17009"
@@ -171,22 +186,22 @@ class ASAGradeOptions(Enum):
171186
NOT_KNOWN = "17015"
172187

173188

174-
class YesNoOptions(Enum):
189+
class YesNoOptions(StrEnum):
175190
"""Enum for YesNo options."""
176191

177192
YES = "17058"
178193
NO = "17059"
179194

180195

181-
class MetastasesPresentOptions(Enum):
196+
class MetastasesPresentOptions(StrEnum):
182197
"""Enum for Metastases Present options."""
183198

184199
CERTAIN = "17131~~202199"
185200
NONE = "17130"
186201
NOT_REPORTED = NOT_REPORTED_CODE
187202

188203

189-
class FinalPreTreatmentTCategoryOptions(Enum):
204+
class FinalPreTreatmentTCategoryOptions(StrEnum):
190205
"""Enum for Final Pre-Treatment T Category options."""
191206

192207
CTX = "17356"
@@ -198,7 +213,7 @@ class FinalPreTreatmentTCategoryOptions(Enum):
198213
NOT_REPORTED = NOT_REPORTED_CODE
199214

200215

201-
class FinalPreTreatmentNCategoryOptions(Enum):
216+
class FinalPreTreatmentNCategoryOptions(StrEnum):
202217
"""Enum for Final Pre-Treatment N Category options."""
203218

204219
CNX = "202201"
@@ -208,7 +223,7 @@ class FinalPreTreatmentNCategoryOptions(Enum):
208223
NOT_REPORTED = NOT_REPORTED_CODE
209224

210225

211-
class ReasonNoTreatmentRecievedOptions(Enum):
226+
class ReasonNoTreatmentRecievedOptions(StrEnum):
212227
"""Enum for Reason No Treatment Received options."""
213228

214229
ADVANCED_DISEASE = "99016"
@@ -219,7 +234,7 @@ class ReasonNoTreatmentRecievedOptions(Enum):
219234
UNKNOWN = "99018"
220235

221236

222-
class PreviouslyExcisedTumorOptions(Enum):
237+
class PreviouslyExcisedTumorOptions(StrEnum):
223238
"""Enum for Previously Excised Tumor options."""
224239

225240
YES = "17058~~305403"
@@ -228,14 +243,14 @@ class PreviouslyExcisedTumorOptions(Enum):
228243
NOT_REPORTED = "202140"
229244

230245

231-
class TreatmentTypeOptions(Enum):
246+
class TreatmentTypeOptions(StrEnum):
232247
"""Enum for Treatment Type options."""
233248

234249
SURGICAL = "202143"
235250
NON_SURGICAL = "202144"
236251

237252

238-
class TreatmentGivenOptions(Enum):
253+
class TreatmentGivenOptions(StrEnum):
239254
"""Enum for Treatment Given options."""
240255

241256
CHEMOTHERAPY = "202160~~202184,202217,202218,202219,202220,202221,202222,202223,202224,202225,202226,202227,202228,202287,305395,305397"
@@ -245,7 +260,7 @@ class TreatmentGivenOptions(Enum):
245260
SPECIALIST_PALLIATIVE_CARE = "202164~~202184,202217,202218,202219,202220,202221,202222,202223,202224,202225,202226,202227,202228,305395,305397"
246261

247262

248-
class CancerTreatmentIntentOptions(Enum):
263+
class CancerTreatmentIntentOptions(StrEnum):
249264
"""Enum for Cancer Treatment Intent options."""
250265

251266
CURATIVE = "17370"
@@ -254,14 +269,14 @@ class CancerTreatmentIntentOptions(Enum):
254269
NOT_KNOWN = "17373"
255270

256271

257-
class NHSOrPrivateOptions(Enum):
272+
class NHSOrPrivateOptions(StrEnum):
258273
"""Enum for NHS or Private options."""
259274

260275
NHS = "202153~~202177,202178"
261276
PRIVATE = "202154~~202179"
262277

263278

264-
class TreatmentProviderLookupOptions(Enum):
279+
class TreatmentProviderLookupOptions(StrEnum):
265280
"""Enum for Treatment Provider lookup options."""
266281

267282
ADVANCE_NURSE_PRACTITIONER_1 = "51905"
@@ -290,7 +305,7 @@ class TreatmentProviderLookupOptions(Enum):
290305
BUSHBURY_HEALTH_CENTRE = "51801"
291306

292307

293-
class ConsultantLookupOptions(Enum):
308+
class ConsultantLookupOptions(StrEnum):
294309
"""Enum for Consultant lookup options."""
295310

296311
B_FRAME = "201"

pages/screening_practitioner_appointments/appointment_detail_page.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,12 @@ def click_save_button(self) -> None:
3333
def verify_text_visible(self, text: str) -> None:
3434
"""Verifies that the specified text is visible on the page."""
3535
expect(self.page.get_by_text(text)).to_be_visible()
36+
37+
def wait_for_attendance_radio(self, timeout_duration: float = 30000) -> None:
38+
"""
39+
Waits for the attendance radio to be visible. Default timeout is 30 seconds but this can be changed
40+
41+
Args:
42+
timeout_duration (float): This is how long you want to wait for in milliseconds
43+
"""
44+
self.attendance_radio.wait_for(timeout=timeout_duration)

pages/screening_practitioner_appointments/practitioner_availability_page.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def select_practitioner_dropdown_option(self, practitioner: str) -> None:
2727
"""Selects the practitioner from the dropdown list."""
2828
self.screening_practitioner_dropdown.select_option(label=practitioner)
2929

30+
def select_practitioner_dropdown_option_from_index(self, index: int) -> None:
31+
"""Selects the practitioner from the dropdown list."""
32+
self.screening_practitioner_dropdown.select_option(index=index)
33+
3034
def click_calendar_button(self) -> None:
3135
"""Clicks the calendar button to open the calendar picker."""
3236
self.click(self.calendar_button)

pages/screening_subject_search/episode_events_and_notes_page.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ def __init__(self, page: Page):
99
super().__init__(page)
1010
self.page = page
1111
# List of episode events and notes - page locators
12+
self.view_appointment_link = self.page.get_by_role(
13+
"link", name="View Appointment"
14+
)
1215

1316
def expected_episode_event_is_displayed(self, event_description: str) -> None:
1417
"""Check if the expected episode event is displayed on the page."""
1518
expect(
1619
self.page.get_by_role("cell", name=event_description, exact=True)
1720
).to_be_visible()
21+
22+
def click_view_appointment_link(self) -> None:
23+
"""Click the 'View Appointment' link"""
24+
self.click(self.view_appointment_link)

pages/screening_subject_search/subject_screening_summary_page.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def __init__(self, page: Page):
5959
)
6060
self.temporary_address_popup = self.page.locator("#idTempAddress")
6161
self.close_button = self.page.get_by_role("img", name="close")
62+
self.book_practitioner_clinic_button = self.page.get_by_role(
63+
"button", name="Book Practitioner Clinic"
64+
)
6265

6366
def wait_for_page_title(self) -> None:
6467
"""Waits for the page to be the Subject Screening Summary"""
@@ -227,6 +230,10 @@ def click_close_button(self) -> None:
227230
"""Click on the close button in the temporary address popup."""
228231
self.click(self.close_button)
229232

233+
def click_book_practitioner_clinic_button(self) -> None:
234+
"""Click on the 'Book Practitioner Clinic' button"""
235+
self.click(self.book_practitioner_clinic_button)
236+
230237

231238
class ChangeScreeningStatusOptions(Enum):
232239
"""Enum for Change Screening Status options."""

0 commit comments

Comments
 (0)