Skip to content

Commit eeafd72

Browse files
authored
Feature/bcss 20605 regression tests subject notes (#100)
<!-- markdownlint-disable-next-line first-line-heading --> Converting the subject notes tests from the selenium framework to playwright python <!-- Describe your changes in detail. --> Converting the subject notes test from selenium to playwright <!-- Why is this change required? What problem does it solve? --> This activity is part of tests migration to playwright to make sure tests are running without any issues. <!-- 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 d29325e commit eeafd72

15 files changed

+1842
-1
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Utility Guide: Note Test Utilities
2+
3+
The **Note Test Utility** module provides reusable helper functions for verifying and comparing note data during test automation of screening subjects in BCSS.
4+
It includes helpers for:
5+
6+
1. Fetching supporting notes from the database.
7+
2. Verifying that a note matches expected values from the DB or UI.
8+
3. Confirming that a removed note is archived properly as obsolete.
9+
10+
## Table of Contents
11+
12+
- [Utility Guide: Note Test Utilities](#utility-guide-note-test-utilities)
13+
- [Table of Contents](#table-of-contents)
14+
- [Using These Utilities](#using-these-utilities)
15+
16+
---
17+
18+
## Using These Utilities
19+
20+
You can import functions into your test files like so:
21+
22+
```python
23+
from utils.note_test_util import (
24+
fetch_supporting_notes_from_db,
25+
verify_note_content_matches_expected,
26+
verify_note_content_ui_vs_db,
27+
verify_note_removal_and_obsolete_transition,
28+
)

pages/base_page.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,18 @@ def handle_dialog(dialog: Dialog):
270270
dialog.dismiss() # Dismiss dialog
271271

272272
self.page.once("dialog", handle_dialog)
273+
274+
def safe_accept_dialog_select_option(self, locator: Locator, option: str) -> None:
275+
"""
276+
Safely accepts a dialog triggered by selecting a dropdown, avoiding the error:
277+
playwright._impl._errors.Error: Dialog.accept: Cannot accept dialog which is already handled!
278+
If no dialog appears, continues without error.
279+
Args:
280+
locator (Locator): The locator that triggers the dialog when clicked.
281+
example: If clicking a save button opens a dialog, pass that save button's locator.
282+
"""
283+
self.page.once("dialog", self._accept_dialog)
284+
try:
285+
locator.select_option(option)
286+
except Exception as e:
287+
logging.error(f"Option selection failed: {e}")
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from playwright.sync_api import Page, Locator
2+
from pages.base_page import BasePage
3+
from enum import StrEnum
4+
import logging
5+
import pytest
6+
from utils.table_util import TableUtils
7+
8+
9+
class SubjectEventsNotes(BasePage):
10+
"""Subject Events Notes Page locators, and methods for interacting with the page."""
11+
12+
def __init__(self, page: Page):
13+
super().__init__(page)
14+
self.page = page
15+
self.table_utils = TableUtils(
16+
page, "#displayRS"
17+
) # Initialize TableUtils for the table with id="displayRS"
18+
# Subject Events Notes - page filters
19+
self.additional_care_note_checkbox = self.page.get_by_label(
20+
"Additional Care Needs Note"
21+
)
22+
self.subject_note_checkbox = self.page.get_by_label("Subject Note")
23+
self.kit_note_checkbox = self.page.get_by_label("Kit Note")
24+
self.note_title = self.page.get_by_label("Note Title")
25+
self.additional_care_note_type = self.page.locator("#UI_ADDITIONAL_CARE_NEED")
26+
self.notes_upto_500_char = self.page.get_by_label("Notes (up to 500 char)")
27+
self.update_notes_button = self.page.get_by_role("button", name="Update Notes")
28+
self.note_type = self.page.locator("#UI_ADDITIONAL_CARE_NEED_FILTER")
29+
self.note_status = self.page.locator(
30+
"//table[@id='displayRS']/tbody/tr[2]/td[3]/select"
31+
)
32+
self.episode_note_status = self.page.locator(
33+
"//table[@id='displayRS']/tbody/tr[2]/td[4]/select"
34+
)
35+
36+
def select_additional_care_note(self) -> None:
37+
"""Selects the 'Additional Care Needs Note' checkbox."""
38+
self.additional_care_note_checkbox.check()
39+
40+
def select_subject_note(self) -> None:
41+
"""Selects the 'subject note' checkbox."""
42+
self.subject_note_checkbox.check()
43+
44+
def select_kit_note(self) -> None:
45+
"""Selects the 'kit note' checkbox."""
46+
self.kit_note_checkbox.check()
47+
48+
def select_additional_care_note_type(self, option: str) -> None:
49+
"""Selects an option from the 'Additional Care Note Type' dropdown.
50+
51+
Args:
52+
option (AdditionalCareNoteTypeOptions): The option to select from the dropdown.
53+
Use one of the predefined values from the
54+
AdditionalCareNoteTypeOptions enum, such as:
55+
- AdditionalCareNoteTypeOptions.LEARNING_DISABILITY
56+
- AdditionalCareNoteTypeOptions.SIGHT_DISABILITY
57+
- AdditionalCareNoteTypeOptions.HEARING_DISABILITY
58+
- AdditionalCareNoteTypeOptions.MOBILITY_DISABILITY
59+
- AdditionalCareNoteTypeOptions.MANUAL_DEXTERITY
60+
- AdditionalCareNoteTypeOptions.SPEECH_DISABILITY
61+
- AdditionalCareNoteTypeOptions.CONTINENCE_DISABILITY
62+
- AdditionalCareNoteTypeOptions.LANGUAGE
63+
- AdditionalCareNoteTypeOptions.OTHER
64+
"""
65+
self.additional_care_note_type.select_option(option)
66+
67+
def select_note_type(self, option: str) -> None:
68+
"""
69+
Selects a note type from the dropdown menu.
70+
71+
Args:
72+
option (str): The value of the option to select from the dropdown.
73+
"""
74+
self.note_type.select_option(option)
75+
76+
def select_note_status(self, option: str) -> None:
77+
"""
78+
Selects a note status from the dropdown menu.
79+
80+
Args:
81+
option (str): The value of the option to select from the dropdown.
82+
"""
83+
self.note_status.select_option(option)
84+
85+
def fill_note_title(self, title: str) -> None:
86+
"""Fills the title field with the provided text."""
87+
self.note_title.fill(title)
88+
89+
def fill_notes(self, notes: str) -> None:
90+
"""Fills the notes field with the provided text."""
91+
self.notes_upto_500_char.fill(notes)
92+
93+
def accept_dialog_and_update_notes(self) -> None:
94+
"""Clicks the 'Update Notes' button and handles the dialog by clicking 'OK'."""
95+
self.page.once("dialog", lambda dialog: dialog.accept())
96+
self.update_notes_button.click()
97+
98+
def accept_dialog_and_add_replacement_note(self) -> None:
99+
"""
100+
Dismisses the dialog and clicks the 'Add Replacement Note' button.
101+
"""
102+
self.page.once("dialog", lambda dialog: dialog.accept())
103+
self.page.get_by_role("button", name="Add Replacement Note").click()
104+
105+
def get_title_and_note_from_row(self, row_number: int = 0) -> dict:
106+
"""
107+
Extracts title and note from a specific row's 'Notes' column using dynamic column index.
108+
"""
109+
cell_text = self.table_utils.get_cell_value("Notes", row_number)
110+
lines = cell_text.split("\n\n")
111+
title = lines[0].strip() if len(lines) > 0 else ""
112+
note = lines[1].strip() if len(lines) > 1 else ""
113+
logging.info(
114+
f"Extracted title: '{title}' and note: '{note}' from row {row_number}"
115+
)
116+
return {"title": title, "note": note}
117+
118+
119+
class AdditionalCareNoteTypeOptions(StrEnum):
120+
"""Enum for AdditionalCareNoteTypeOptions."""
121+
122+
LEARNING_DISABILITY = "4120"
123+
SIGHT_DISABILITY = "4121"
124+
HEARING_DISABILITY = "4122"
125+
MOBILITY_DISABILITY = "4123"
126+
MANUAL_DEXTERITY = "4124"
127+
SPEECH_DISABILITY = "4125"
128+
CONTINENCE_DISABILITY = "4126"
129+
LANGUAGE = "4128"
130+
OTHER = "4127"
131+
132+
133+
class NotesOptions(StrEnum):
134+
"""Enum for NoteTypeOptions."""
135+
136+
SUBJECT_NOTE = "4111"
137+
KIT_NOTE = "308015"
138+
ADDITIONAL_CARE_NOTE = "4112"
139+
EPISODE_NOTE = "4110"
140+
141+
142+
class NotesStatusOptions(StrEnum):
143+
"""Enum for NoteStatusOptions."""
144+
145+
ACTIVE = "4100"
146+
OBSOLETE = "4101"
147+
INVALID = "4102"

pages/screening_subject_search/subject_screening_search_page.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def __init__(self, page: Page):
1717
self.demographics_filter = self.page.get_by_role("radio", name="Demographics")
1818
self.datasets_filter = self.page.get_by_role("radio", name="Datasets")
1919
self.nhs_number_filter = self.page.get_by_role("textbox", name="NHS Number")
20+
self.nhs_number_input = self.page.get_by_label("NHS Number")
2021
self.surname_filter = self.page.locator("#A_C_Surname")
2122
self.soundex_filter = self.page.get_by_role("checkbox", name="Use soundex")
2223
self.forename_filter = self.page.get_by_role("textbox", name="Forename")

pages/screening_subject_search/subject_screening_summary_page.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(self, page: Page):
5454
self.advance_fobt_screening_episode_button = self.page.get_by_role(
5555
"button", name="Advance FOBT Screening Episode"
5656
)
57+
self.additional_care_note_link = self.page.get_by_role("link", name="(AN)")
5758
self.temporary_address_icon = self.page.get_by_role(
5859
"link", name="The person has a current"
5960
)
@@ -195,6 +196,47 @@ def click_advance_fobt_screening_episode_button(self) -> None:
195196
except Exception as e:
196197
pytest.fail(f"Unable to advance the episode: {e}")
197198

199+
def verify_additional_care_note_visible(self) -> None:
200+
"""Verifies that the '(AN)' link is visible."""
201+
expect(self.additional_care_note_link).to_be_visible()
202+
203+
def verify_note_link_present(self, note_type_name: str) -> None:
204+
"""
205+
Verifies that the link for the specified note type is visible on the page.
206+
207+
Args:
208+
note_type_name (str): The name of the note type to check (e.g., 'Additional Care Note', 'Episode Note').
209+
210+
Raises:
211+
AssertionError: If the link is not visible on the page.
212+
"""
213+
logging.info(f"Checking if the '{note_type_name}' link is visible.")
214+
note_link_locator = self.page.get_by_role(
215+
"link", name=f"({note_type_name})"
216+
) # Dynamic locator for the note type link
217+
assert (
218+
note_link_locator.is_visible()
219+
), f"'{note_type_name}' link is not visible, but it should be."
220+
logging.info(f"Verified: '{note_type_name}' link is visible.")
221+
222+
def verify_note_link_not_present(self, note_type_name: str) -> None:
223+
"""
224+
Verifies that the link for the specified note type is not visible on the page.
225+
226+
Args:
227+
note_type_name (str): The name of the note type to check (e.g., 'Additional Care Note', 'Episode Note').
228+
229+
Raises:
230+
AssertionError: If the link is visible on the page.
231+
"""
232+
logging.info(f"Checking if the '{note_type_name}' link is not visible.")
233+
note_link_locator = self.page.get_by_role(
234+
"link", name=f"({note_type_name})"
235+
) # Dynamic locator for the note type link
236+
assert (
237+
not note_link_locator.is_visible()
238+
), f"'{note_type_name}' link is visible, but it should not be."
239+
logging.info(f"Verified: '{note_type_name}' link is not visible.")
198240
def verify_temporary_address_popup_visible(self) -> None:
199241
"""Verify that the temporary address popup is visible."""
200242
try:

pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ markers =
3939
vpn_required: for tests that require a VPN connection
4040
regression: tests that are part of the regression test suite
4141
call_and_recall: tests that are part of the call and recall test suite
42+
note_tests: tests that are part of the notes test suite
4243
subject_tests: tests that are part of the subject tests suite
4344
subject_search: tests that are part of the subject search test suite

tests/bcss_tests.properties

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,17 @@ episode_closed_date=22/09/2020
2828
daily_invitation_rate=28
2929
fobt_daily_invitation_rate=6
3030
weekly_invitation_rate=130
31+
32+
# ----------------------------------
33+
# Subject Notes Test DATA
34+
# ----------------------------------
35+
additional_care_note_name=AN
36+
additional_care_note_type_value=4112
37+
episode_note_name=EN
38+
episode_note_type_value=4110
39+
subject_note_name=SN
40+
subject_note_type_value=4111
41+
kit_note_name=KN
42+
kit_note_type_value=308015
43+
note_status_active=4100
44+
note_status_obsolete=4101

0 commit comments

Comments
 (0)