diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml index d239f70c..806a2afa 100644 --- a/.github/workflows/stage-4-acceptance.yaml +++ b/.github/workflows/stage-4-acceptance.yaml @@ -87,6 +87,7 @@ jobs: uses: actions/checkout@v5 - name: "Run UI test" run: | + cp .env.example .env make test-ui - name: "Save result" run: | diff --git a/.gitignore b/.gitignore index d5e07a30..37b09f0a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ lung_cancer_screening/assets/compiled/* .DS_Store .venv .devcontainer/ca.crt +tests/TEST-*.xml diff --git a/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py b/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py deleted file mode 100644 index 5f00b621..00000000 --- a/lung_cancer_screening/core/tests/acceptance/test_cannot_change_answers_after_submission.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from playwright.sync_api import sync_playwright, expect -from datetime import datetime -from dateutil.relativedelta import relativedelta - -from .helpers.user_interaction_helpers import ( - fill_in_and_submit_height_metric, - fill_in_and_submit_weight_metric, - fill_in_and_submit_participant_id, - fill_in_and_submit_smoking_eligibility, - fill_in_and_submit_date_of_birth, - fill_in_and_submit_sex_at_birth, - fill_in_and_submit_gender, - fill_in_and_submit_ethnicity, - fill_in_and_submit_asbestos_exposure, - fill_in_and_submit_respiratory_conditions -) - -class TestQuestionnaire(StaticLiveServerTestCase): - - @classmethod - def setUpClass(cls): - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - super().setUpClass() - cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls.browser.close() - cls.playwright.stop() - - def test_cannot_change_responses_once_checked_and_submitted(self): - participant_id = '123' - smoking_status = 'Yes, I used to smoke regularly' - age = datetime.now() - relativedelta(years=55) - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - fill_in_and_submit_smoking_eligibility(page, smoking_status) - fill_in_and_submit_date_of_birth(page, age) - fill_in_and_submit_height_metric(page, "170") - fill_in_and_submit_weight_metric(page, "25.4") - fill_in_and_submit_sex_at_birth(page, "Male") - fill_in_and_submit_gender(page, "Male") - fill_in_and_submit_ethnicity(page, "White") - page.click("text=Continue") # education - fill_in_and_submit_respiratory_conditions(page, "No, I have not had any of these respiratory conditions") - fill_in_and_submit_asbestos_exposure(page, "No") - page.click("text=Continue") # cancer diagnosis - page.click("text=Continue") # family history - page.click("text=Submit") - - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - - expect(page.locator('#maincontent')).to_contain_text( - "Responses have already been submitted for this participant" - ) diff --git a/lung_cancer_screening/core/tests/acceptance/test_participant_not_smoker.py b/lung_cancer_screening/core/tests/acceptance/test_participant_not_smoker.py deleted file mode 100644 index a352d49e..00000000 --- a/lung_cancer_screening/core/tests/acceptance/test_participant_not_smoker.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from playwright.sync_api import sync_playwright, expect - - -from .helpers.user_interaction_helpers import ( - fill_in_and_submit_participant_id, - fill_in_and_submit_smoking_eligibility -) - - -class TestParticipantNotSmoker(StaticLiveServerTestCase): - - @classmethod - def setUpClass(cls): - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - super().setUpClass() - cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls.browser.close() - cls.playwright.stop() - - def test_participant_not_smoker(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - fill_in_and_submit_smoking_eligibility(page, 'No, I have never smoked') - - expect(page).to_have_url(f"{self.live_server_url}/non-smoker-exit") - - expect(page.locator(".title")).to_have_text( - "You do not need an NHS lung health check") diff --git a/lung_cancer_screening/core/tests/acceptance/test_participant_out_of_age_range.py b/lung_cancer_screening/core/tests/acceptance/test_participant_out_of_age_range.py deleted file mode 100644 index e146d60f..00000000 --- a/lung_cancer_screening/core/tests/acceptance/test_participant_out_of_age_range.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from playwright.sync_api import sync_playwright, expect -from datetime import datetime -from dateutil.relativedelta import relativedelta - -from .helpers.user_interaction_helpers import ( - fill_in_and_submit_participant_id, - fill_in_and_submit_smoking_eligibility, - fill_in_and_submit_date_of_birth -) - - -class TestParticipantOutOfAgeRange(StaticLiveServerTestCase): - - @classmethod - def setUpClass(cls): - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - super().setUpClass() - cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls.browser.close() - cls.playwright.stop() - - def test_participant_out_of_age_range(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - fill_in_and_submit_smoking_eligibility(page, 'Yes, I used to smoke regularly') - - age = datetime.now() - relativedelta(years=20) - fill_in_and_submit_date_of_birth(page, age) - - expect(page).to_have_url(f"{self.live_server_url}/age-range-exit") - - expect(page.locator(".title")).to_have_text( - "You do not need an NHS lung health check") diff --git a/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py b/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py deleted file mode 100644 index f6bbbaf4..00000000 --- a/lung_cancer_screening/core/tests/acceptance/test_questionnaire.py +++ /dev/null @@ -1,162 +0,0 @@ -import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from playwright.sync_api import sync_playwright, expect -from datetime import datetime -from dateutil.relativedelta import relativedelta - -from .helpers.user_interaction_helpers import ( - fill_in_and_submit_height_imperial, - fill_in_and_submit_height_metric, - fill_in_and_submit_participant_id, - fill_in_and_submit_smoking_eligibility, - fill_in_and_submit_date_of_birth, - fill_in_and_submit_weight_metric, - fill_in_and_submit_weight_imperial, - fill_in_and_submit_sex_at_birth, - fill_in_and_submit_gender, - fill_in_and_submit_ethnicity, - fill_in_and_submit_asbestos_exposure, - fill_in_and_submit_respiratory_conditions -) - -from .helpers.assertion_helpers import expect_back_link_to_have_url - -class TestQuestionnaire(StaticLiveServerTestCase): - - @classmethod - def setUpClass(cls): - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - super().setUpClass() - cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls.browser.close() - cls.playwright.stop() - - def test_full_questionnaire_user_journey(self): - participant_id = '123' - smoking_status = 'Yes, I used to smoke regularly' - age = datetime.now() - relativedelta(years=55) - height = "170" - feet = 5 - inches = 7 - weight_metric = 70 - weight_stone = 5 - weight_pound = 10 - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - - expect(page).to_have_url( - f"{self.live_server_url}/have-you-ever-smoked") - expect_back_link_to_have_url(page, "/start") - fill_in_and_submit_smoking_eligibility(page, smoking_status) - - expect(page).to_have_url(f"{self.live_server_url}/date-of-birth") - expect_back_link_to_have_url(page, "/have-you-ever-smoked") - fill_in_and_submit_date_of_birth(page, age) - - expect(page).to_have_url(f"{self.live_server_url}/height") - expect_back_link_to_have_url(page, "/date-of-birth") - fill_in_and_submit_height_metric(page, height) - - page.click("text=Back") - expect(page).to_have_url(f"{self.live_server_url}/height") - page.click("text=Switch to imperial") - fill_in_and_submit_height_imperial(page, feet, inches) - - expect(page).to_have_url(f"{self.live_server_url}/weight") - expect_back_link_to_have_url(page, "/height") - fill_in_and_submit_weight_metric(page, weight_metric) - page.click("text=Back") - - expect(page).to_have_url(f"{self.live_server_url}/weight") - page.get_by_role("link", name="Switch to stone and pounds").click() - fill_in_and_submit_weight_imperial(page, weight_stone, weight_pound) - - expect(page).to_have_url(f"{self.live_server_url}/sex-at-birth") - expect_back_link_to_have_url(page, "/weight") - fill_in_and_submit_sex_at_birth(page, "Male") - - expect(page).to_have_url(f"{self.live_server_url}/gender") - expect_back_link_to_have_url(page, "/sex-at-birth") - fill_in_and_submit_gender(page, "Male") - - expect(page).to_have_url(f"{self.live_server_url}/ethnicity") - expect_back_link_to_have_url(page, "/gender") - fill_in_and_submit_ethnicity(page, "White") - - expect(page).to_have_url(f"{self.live_server_url}/education") - expect_back_link_to_have_url(page, "/ethnicity") - page.click("text=Continue") - - expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions") - expect_back_link_to_have_url(page, "/education") - fill_in_and_submit_respiratory_conditions(page, "No, I have not had any of these respiratory conditions") - - expect(page).to_have_url(f"{self.live_server_url}/asbestos-exposure") - expect_back_link_to_have_url(page, "/respiratory-conditions") - fill_in_and_submit_asbestos_exposure(page, "No") - - expect(page).to_have_url(f"{self.live_server_url}/cancer-diagnosis") - expect_back_link_to_have_url(page, "/asbestos-exposure") - page.click("text=Continue") - - expect(page).to_have_url(f"{self.live_server_url}/family-history-lung-cancer") - expect_back_link_to_have_url(page, "/cancer-diagnosis") - page.click("text=Continue") - - expect(page).to_have_url(f"{self.live_server_url}/responses") - expect_back_link_to_have_url(page, "/family-history-lung-cancer") - - responses = page.locator(".responses") - expect(responses).to_contain_text("Have you ever smoked? Yes, I used to smoke regularly") - expect(responses).to_contain_text(age.strftime("What is your date of birth? %Y-%m-%d")) - expect(responses).to_contain_text(f"What is your height? {feet} feet {inches} inches") - expect(responses).to_contain_text(f"What is your weight? {weight_stone} stone {weight_pound} pound") - expect(responses).to_contain_text("What was your sex at birth? Male") - expect(responses).to_contain_text("Which of these best describes you? Male") - expect(responses).to_contain_text("What is your ethnic background? White") - expect(responses).to_contain_text("Have you ever worked in a job where you might have been exposed to asbestos? No") - expect(responses).to_contain_text("Have you ever been diagnosed with any of the following respiratory conditions? No, I have not had any of these respiratory conditions") - - page.click("text=Submit") - - expect(page).to_have_url(f"{self.live_server_url}/your-results") - - def test_can_select_multiple_respiratory_conditions(self): - """Test that users can select multiple respiratory conditions in the UI""" - participant_id = '456' - smoking_status = 'Yes, I currently smoke' - age = datetime.now() - relativedelta(years=60) - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - - fill_in_and_submit_participant_id(page, participant_id) - fill_in_and_submit_smoking_eligibility(page, smoking_status) - fill_in_and_submit_date_of_birth(page, age) - fill_in_and_submit_height_metric(page, "170") - fill_in_and_submit_weight_metric(page, "70") - fill_in_and_submit_sex_at_birth(page, "Female") - fill_in_and_submit_gender(page, "Female") - fill_in_and_submit_ethnicity(page, "White") - page.click("text=Continue") # education - - # Select multiple respiratory conditions - expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions") - fill_in_and_submit_respiratory_conditions(page, ["Pneumonia", "Emphysema"]) - - fill_in_and_submit_asbestos_exposure(page, "No") - page.click("text=Continue") # cancer diagnosis - page.click("text=Continue") # family history - - # Verify both conditions appear on the responses page - expect(page).to_have_url(f"{self.live_server_url}/responses") - responses = page.locator(".responses") - expect(responses).to_contain_text("Have you ever been diagnosed with any of the following respiratory conditions? Pneumonia, Emphysema") diff --git a/lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py b/lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py deleted file mode 100644 index efc35f90..00000000 --- a/lung_cancer_screening/core/tests/acceptance/test_questionnaire_validation_errors.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from playwright.sync_api import sync_playwright, expect - -from .helpers.user_interaction_helpers import ( - fill_in_and_submit_participant_id -) - -class TestQuestionnaireValidationErrors(StaticLiveServerTestCase): - - @classmethod - def setUpClass(cls): - os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" - super().setUpClass() - cls.playwright = sync_playwright().start() - cls.browser = cls.playwright.chromium.launch() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - cls.browser.close() - cls.playwright.stop() - - def test_date_of_birth_validation_errors(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - fill_in_and_submit_participant_id(page, participant_id) - page.goto(f"{self.live_server_url}/date-of-birth") - - expect(page.locator("legend")).to_have_text( - "What is your date of birth?") - - page.click("text=Continue") - - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Enter your date of birth." - ) - - def test_height_validation_errors(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - fill_in_and_submit_participant_id(page, participant_id) - page.goto(f"{self.live_server_url}/height") - - page.click("text=Continue") - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Enter your height." - ) - - page.get_by_label("Centimetre").fill('139.6') - page.click('text=Continue') - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Height must be between 139.7cm and 243.8 cm" - ) - - page.get_by_label("Centimetre").fill('243.9') - page.click('text=Continue') - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Height must be between 139.7cm and 243.8 cm" - ) - - page.click("text=Switch to imperial") - - page.click("text=Continue") - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Enter your height." - ) - - page.get_by_label("Feet").fill('5.2') - page.get_by_label("Inches").fill('2') - page.click('text=Continue') - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Feet must be in whole numbers" - ) - - page.get_by_label("Feet").fill('5') - page.get_by_label("Inches").fill('2.2') - page.click('text=Continue') - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Inches must be in whole numbers" - ) - - def test_weight_validation_errors(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - fill_in_and_submit_participant_id(page, participant_id) - page.goto(f"{self.live_server_url}/weight") - - page.click("text=Continue") - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Enter your weight." - ) - # Test weight below minimum - page.get_by_label("Kilograms").fill('25.3') - page.click('text=Continue') - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Weight must be between 25.4kg and 317.5kg" - ) - - def test_ethnicity_validation_errors(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - fill_in_and_submit_participant_id(page, participant_id) - page.goto(f"{self.live_server_url}/ethnicity") - - page.click("text=Continue") - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Select your ethnic background." - ) - - def test_respiratory_conditions_validation_errors(self): - participant_id = '123' - - page = self.browser.new_page() - page.goto(f"{self.live_server_url}/start") - fill_in_and_submit_participant_id(page, participant_id) - page.goto(f"{self.live_server_url}/respiratory-conditions") - - page.click("text=Continue") - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Select if you have had any respiratory conditions" - ) - - # Select one respiratory condition - page.get_by_label("Bronchitis").click() - - # Select None option - page.get_by_label("No, I have not had any of these respiratory conditions").click() - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Select if you have had any respiratory conditions" - ) - - # Continue - page.click("text=Continue") - - # Assert error is shown - expect(page.locator(".nhsuk-error-message")).to_contain_text( - "Select if you have had any respiratory conditions, or select 'No, I have not had any of these respiratory conditions'" - ) - - expect(page).to_have_url(f"{self.live_server_url}/respiratory-conditions") - - diff --git a/lung_cancer_screening/settings_test.py b/lung_cancer_screening/settings_test.py index ae9441b3..b7a69ab0 100644 --- a/lung_cancer_screening/settings_test.py +++ b/lung_cancer_screening/settings_test.py @@ -13,4 +13,6 @@ "whitenoise.middleware.WhiteNoiseMiddleware", ) +INSTALLED_APPS += ["behave_django"] + logging.disable(logging.CRITICAL) diff --git a/poetry.lock b/poetry.lock index 1a4b5a5d..6ce8e73b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,7 +6,7 @@ version = "3.10.0" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734"}, {file = "asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e"}, @@ -72,6 +72,72 @@ msal = ">=1.30.0" msal-extensions = ">=1.2.0" typing-extensions = ">=4.0.0" +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"}, + {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "behave" +version = "1.3.3" +description = "behave is behaviour-driven development, Python style" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "behave-1.3.3-py2.py3-none-any.whl", hash = "sha256:89bdb62af8fb9f147ce245736a5de69f025e5edfb66f1fbe16c5007493f842c0"}, + {file = "behave-1.3.3.tar.gz", hash = "sha256:2b8f4b64ed2ea756a5a2a73e23defc1c4631e9e724c499e46661778453ebaf51"}, +] + +[package.dependencies] +colorama = ">=0.3.7" +cucumber-expressions = {version = ">=17.1.0", markers = "python_version >= \"3.8\""} +cucumber-tag-expressions = ">=4.1.0" +parse = ">=1.18.0" +parse-type = ">=0.6.0" +six = ">=1.15.0" + +[package.extras] +develop = ["PyHamcrest (<2.0) ; python_version < \"3.0\"", "PyHamcrest (>=2.0.2) ; python_version >= \"3.0\"", "build (>=0.5.1)", "coverage (>=5.0)", "invoke (>=1.7.0) ; python_version >= \"3.6\"", "invoke (>=1.7.0,<2.0) ; python_version < \"3.6\"", "mock (<4.0) ; python_version < \"3.6\"", "mock (>=4.0) ; python_version >= \"3.6\"", "modernize (>=0.5)", "path (>=13.1.0) ; python_version >= \"3.5\"", "path.py (>=11.5.0) ; python_version < \"3.5\"", "pycmd", "pylint", "pytest (>=4.2,<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-cov", "pytest-html (>=1.19.0,<2.0) ; python_version < \"3.0\"", "pytest-html (>=2.0) ; python_version >= \"3.0\"", "ruff ; python_version >= \"3.7\"", "tox (>=3.28.0,<4.0)", "twine (>=1.13.0)", "virtualenv (<20.22.0) ; python_version < \"3.7\"", "virtualenv (>=20.26.6) ; python_version >= \"3.7\""] +docs = ["furo (>=2024.04.27) ; python_version >= \"3.8\"", "sphinx (>=1.6,<4.4) ; python_version < \"3.7\"", "sphinx (>=7.4.0) ; python_version >= \"3.7\"", "sphinx-copybutton (>=0.5.2) ; python_version >= \"3.7\"", "sphinxcontrib-applehelp (>=1.0.8) ; python_version >= \"3.7\"", "sphinxcontrib-htmlhelp (>=2.0.5) ; python_version >= \"3.7\""] +formatters = ["behave-html-formatter (>=0.9.10) ; python_version >= \"3.6\"", "behave-html-pretty-formatter (>=1.9.1) ; python_version >= \"3.6\""] +testing = ["PyHamcrest (<2.0) ; python_version < \"3.0\"", "PyHamcrest (>=2.0.2) ; python_version >= \"3.0\"", "assertpy (>=1.1)", "chardet", "freezegun (>=1.5.1) ; python_version > \"3.7\"", "mock (<4.0) ; python_version < \"3.6\"", "mock (>=4.0) ; python_version >= \"3.6\"", "path (>=13.1.0) ; python_version >= \"3.5\"", "path.py (>=11.5.0,<13.0) ; python_version < \"3.5\"", "pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-html (>=1.19.0,<2.0) ; python_version < \"3.0\"", "pytest-html (>=2.0) ; python_version >= \"3.0\""] + +[[package]] +name = "behave-django" +version = "1.9.0" +description = "Behave BDD integration for Django" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "behave_django-1.9.0-py3-none-any.whl", hash = "sha256:41c6d95886cd59413493037cc79fbe3346a828c83594431bbd596859ad12399b"}, + {file = "behave_django-1.9.0.tar.gz", hash = "sha256:cbae403b9c9873d4d079f3560d6c8b390eaea9fb8e8caff63439a0b06ea60309"}, +] + +[package.dependencies] +beautifulsoup4 = "*" +behave = ">=1.3.3" +django = ">=4.2" + [[package]] name = "certifi" version = "2025.11.12" @@ -305,6 +371,18 @@ files = [ {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "cryptography" version = "46.0.3" @@ -382,13 +460,37 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "cucumber-expressions" +version = "18.0.1" +description = "Cucumber Expressions - a simpler alternative to Regular Expressions" +optional = false +python-versions = "<4.0,>=3.8" +groups = ["dev"] +files = [ + {file = "cucumber_expressions-18.0.1-py3-none-any.whl", hash = "sha256:86230d503cdda7ef35a1f2072a882d7d57c740aa4c163c82b07f039b6bc60c42"}, + {file = "cucumber_expressions-18.0.1.tar.gz", hash = "sha256:86ce41bf28ee520408416f38022e5a083d815edf04a0bd1dae46d474ca597c60"}, +] + +[[package]] +name = "cucumber-tag-expressions" +version = "8.0.0" +description = "Provides a tag-expression parser and evaluation logic for cucumber/behave" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cucumber_tag_expressions-8.0.0-py3-none-any.whl", hash = "sha256:bfe552226f62a4462ee91c9643582f524af84ac84952643fb09057580cbb110a"}, + {file = "cucumber_tag_expressions-8.0.0.tar.gz", hash = "sha256:4af80282ff0349918c332428176089094019af6e2a381a2fd8f1c62a7a6bb7e8"}, +] + [[package]] name = "django" version = "5.2.8" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "django-5.2.8-py3-none-any.whl", hash = "sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f"}, {file = "django-5.2.8.tar.gz", hash = "sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f"}, @@ -419,8 +521,6 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -430,8 +530,6 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -441,8 +539,6 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -452,8 +548,6 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -461,8 +555,6 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, - {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -472,8 +564,6 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -702,6 +792,39 @@ files = [ {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "parse" +version = "1.20.2" +description = "parse() is the opposite of format()" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, + {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, +] + +[[package]] +name = "parse-type" +version = "0.6.6" +description = "Simplifies to build parse types based on the parse module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,>=2.7" +groups = ["dev"] +files = [ + {file = "parse_type-0.6.6-py2.py3-none-any.whl", hash = "sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c"}, + {file = "parse_type-0.6.6.tar.gz", hash = "sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2"}, +] + +[package.dependencies] +parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""} +six = ">=1.15" + +[package.extras] +develop = ["build (>=0.5.1)", "coverage (>=4.4)", "pylint", "pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-cov", "pytest-html (>=1.19.0)", "ruff ; python_version >= \"3.7\"", "setuptools", "setuptools-scm", "tox (>=2.8,<4.0)", "twine (>=1.13.0)", "virtualenv (<20.22.0) ; python_version <= \"3.6\"", "virtualenv (>=20.0.0) ; python_version > \"3.6\"", "wheel"] +docs = ["Sphinx (>=1.6)", "sphinx_bootstrap_theme (>=0.6.0)"] +testing = ["pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-html (>=1.19.0)"] + [[package]] name = "playwright" version = "1.56.0" @@ -925,19 +1048,31 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] +[[package]] +name = "soupsieve" +version = "2.8" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, + {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, +] + [[package]] name = "sqlparse" version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -965,7 +1100,7 @@ version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] +groups = ["main", "dev"] markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, @@ -1007,5 +1142,5 @@ brotli = ["brotli"] [metadata] lock-version = "2.1" -python-versions = ">=3.13" -content-hash = "562d9f8ae2d248b4f1bda653611b89a61217cd2f39d239f1f25f94a9821e0719" +python-versions = ">=3.13, <4.0" +content-hash = "58d4e82d0b07b8f145858201e771c7050debede65a9617e874d8311c4158f74c" diff --git a/pyproject.toml b/pyproject.toml index bf6c4d8a..250d579b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "" authors = [{name = "Your Name",email = "you@example.com"}] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.13, <4.0" dependencies = [ "azure-identity (>=1.23.0,<2.0.0)", "django (>=5.2.4,<6.0.0)", @@ -23,8 +23,11 @@ package-mode = false ruff = "^0.14.5" playwright = "^1.56.0" axe-playwright-python = "^0.1.6" - +behave-django = "^1.8.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.behave] +paths = ["tests/features"] diff --git a/scripts/tests/ui.sh b/scripts/tests/ui.sh new file mode 100755 index 00000000..a599bcda --- /dev/null +++ b/scripts/tests/ui.sh @@ -0,0 +1 @@ +docker compose run --rm web poetry run python manage.py behave --settings=lung_cancer_screening.settings_test diff --git a/tests/features/cannot_change_answers_after_submission.feature b/tests/features/cannot_change_answers_after_submission.feature new file mode 100644 index 00000000..929fb311 --- /dev/null +++ b/tests/features/cannot_change_answers_after_submission.feature @@ -0,0 +1,7 @@ +Feature: Participants with submitted responses + Scenario: Cannot change responses once submitted + Given I have already submitted my responses + When I go to "/start" + And I submit my participant id + Then I am on "/start" + And I see an error summary "Responses have already been submitted for this participant" diff --git a/tests/features/environment.py b/tests/features/environment.py new file mode 100644 index 00000000..d34a343c --- /dev/null +++ b/tests/features/environment.py @@ -0,0 +1,65 @@ +""" +Behave environment setup for Django tests. +behave-django handles test database setup automatically. +We just need to add live server and Playwright setup. +""" +import os +from django.contrib.staticfiles.testing import StaticLiveServerTestCase # noqa: E402 +from playwright.sync_api import sync_playwright # noqa: E402 + +# behave-django automatically handles: +# - Django setup +# - Test database creation +# - Database transactions per scenario + + +class LiveServer(StaticLiveServerTestCase): + """Live server for Behave tests - extends Django's StaticLiveServerTestCase.""" + + @classmethod + def setUpClass(cls): + """Set up live server - called once for all scenarios.""" + super().setUpClass() + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + + +def before_all(context): + """Set up before all tests run.""" + # Set up live server (behave-django handles test database) + LiveServer.setUpClass() + context.live_server_url = LiveServer.live_server_url + context.live_server_class = LiveServer + + # Set up Playwright browser + os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" + context.playwright = sync_playwright().start() + context.browser = context.playwright.chromium.launch(headless=True) + + +def after_all(context): + """Clean up after all tests run.""" + # Clean up Playwright + if hasattr(context, 'browser'): + context.browser.close() + if hasattr(context, 'playwright'): + context.playwright.stop() + + # Tear down live server (behave-django handles test database teardown) + if hasattr(context, 'live_server_class'): + LiveServer.tearDownClass() + + +def before_scenario(context, _scenario): + """Set up before each scenario.""" + # behave-django automatically handles database transactions per scenario + context.page = context.browser.new_page() + + +def after_scenario(context, _scenario): + """Clean up after each scenario.""" + # Close the page if it exists + if hasattr(context, 'page'): + context.page.close() + del context.page + + # behave-django automatically rolls back database transactions diff --git a/tests/features/not_a_smoker_exit.feature b/tests/features/not_a_smoker_exit.feature new file mode 100644 index 00000000..8a47fde3 --- /dev/null +++ b/tests/features/not_a_smoker_exit.feature @@ -0,0 +1,7 @@ +Feature: Non smokers + Scenario: Non smokers are not elligible + Given I have started the questionnaire + When I go to "/have-you-ever-smoked" + And I fill in and submit my smoking status with "No, I have never smoked" + Then I am on "/non-smoker-exit" + And I see a title "You do not need an NHS lung health check" diff --git a/tests/features/not_in_age_range.feature b/tests/features/not_in_age_range.feature new file mode 100644 index 00000000..5243eb11 --- /dev/null +++ b/tests/features/not_in_age_range.feature @@ -0,0 +1,7 @@ +Feature: Not in age range + Scenario: Participants outside age range are not elligible + Given I have started the questionnaire + When I go to "/date-of-birth" + And I fill in and submit my date of birth with "01-01-1900" + Then I am on "/age-range-exit" + And I see a title "You do not need an NHS lung health check" diff --git a/tests/features/questionnaire.feature b/tests/features/questionnaire.feature new file mode 100644 index 00000000..c4c1bb4a --- /dev/null +++ b/tests/features/questionnaire.feature @@ -0,0 +1,57 @@ +Feature: Questionnaire + Scenario: The user can complete the full questionnaire + When I go to "/start" + And I submit my participant id + Then I am on "/have-you-ever-smoked" + And I see a back link to "/start" + When I fill in and submit my smoking status with "Yes, I used to smoke regularly" + Then I am on "/date-of-birth" + And I see a back link to "/have-you-ever-smoked" + When I fill in and submit my date of birth as 55 years ago + Then I am on "/height" + And I see a back link to "/date-of-birth" + When I fill in and submit my height with "170" + And I click "Back" + And I click "Switch to imperial" + When I fill in and submit my height with "5" feet and "7" inches + Then I am on "/weight" + And I see a back link to "/height" + When I fill in and submit my weight with "70" + And I click "Back" + And I click "Switch to stone and pounds" + When I fill in and submit my weight with "5" stone and "10" pounds + Then I am on "/sex-at-birth" + And I see a back link to "/weight" + When I fill in and submit my sex at birth with "Male" + Then I am on "/gender" + And I see a back link to "/sex-at-birth" + When I fill in and submit my gender with "Female" + Then I am on "/ethnicity" + And I see a back link to "/gender" + When I fill in and submit my ethnicity with "White" + Then I am on "/education" + And I see a back link to "/ethnicity" + When I click "Continue" + Then I am on "/respiratory-conditions" + And I see a back link to "/education" + When I fill in and submit my respiratory conditions with "Pneumonia" and "Emphysema" + Then I am on "/asbestos-exposure" + And I see a back link to "/respiratory-conditions" + When I fill in and submit my asbestos exposure with "No" + Then I am on "/cancer-diagnosis" + And I see a back link to "/asbestos-exposure" + When I click "Continue" + Then I am on "/family-history-lung-cancer" + And I see a back link to "/cancer-diagnosis" + When I click "Continue" + Then I am on "/responses" + And I see a back link to "/family-history-lung-cancer" + And I see responses "Have you ever smoked? Yes, I used to smoke regularly" + And I see responses "What is your date of birth?" with a date 55 years ago + And I see responses "What is your height? 5 feet 7 inches" + And I see responses "What is your weight? 5 stone 10 pounds" + And I see responses "What was your sex at birth? Male" + And I see responses "Which of these best describes you? Female" + And I see responses "What is your ethnic background? White" + And I see responses "Have you ever worked in a job where you might have been exposed to asbestos? No" + And I see responses "Have you ever been diagnosed with any of the following respiratory conditions? Pneumonia, Emphysema" diff --git a/tests/features/steps/error_steps.py b/tests/features/steps/error_steps.py new file mode 100644 index 00000000..6e508f46 --- /dev/null +++ b/tests/features/steps/error_steps.py @@ -0,0 +1,11 @@ +from behave import then +from playwright.sync_api import expect + + +@then(u'I see an error summary "{error_summary}"') +def then_i_see_an_error_summary(context, error_summary): + expect(context.page.locator('.nhsuk-error-summary')).to_contain_text(error_summary) + +@then(u'I see a form error "{error_message}"') +def then_i_see_a_form_error(context, error_message): + expect(context.page.locator('.nhsuk-error-message')).to_contain_text(error_message) diff --git a/tests/features/steps/form_steps.py b/tests/features/steps/form_steps.py new file mode 100644 index 00000000..f9efbc50 --- /dev/null +++ b/tests/features/steps/form_steps.py @@ -0,0 +1,86 @@ +from behave import when +from datetime import datetime +from dateutil.relativedelta import relativedelta + + +@when('I submit my participant id') +def when_i_submit_my_participant_id(context): + if not hasattr(context, 'participant_id'): + context.participant_id = 'abc123' + context.page.fill('input[name="participant_id"]', context.participant_id) + context.page.click('button[type="submit"]') + +@when('I fill in and submit my smoking status with "{smoking_status}"') +def when_i_fill_in_and_submit_my_smoking_status(context, smoking_status): + context.page.get_by_label(smoking_status).check() + when_i_submit_the_form(context) + +@when('I fill in and submit my date of birth with "{date_of_birth}"') +def when_i_fill_in_and_submit_my_date_of_birth(context, date_of_birth): + day, month, year = date_of_birth.split('-') + context.page.get_by_label('Day').fill(day) + context.page.get_by_label('Month').fill(month) + context.page.get_by_label('Year').fill(year) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my date of birth as {years} years ago') +def when_i_fill_in_and_submit_my_date_of_birth_as_x_years_ago(context, years): + date_of_birth = datetime.now() - relativedelta(years=int(years)) + context.page.get_by_label('Day').fill(str(date_of_birth.day)) + context.page.get_by_label('Month').fill(str(date_of_birth.month)) + context.page.get_by_label('Year').fill(str(date_of_birth.year)) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my height with "{height}"') +def when_i_fill_in_and_submit_my_height(context, height): + context.page.get_by_label('Centimetre').fill(height) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my height with "{feet}" feet and "{inches}" inch') +@when(u'I fill in and submit my height with "{feet}" feet and "{inches}" inches') +def when_i_fill_in_and_submit_my_height_with_feet_and_inches(context, feet, inches): + context.page.get_by_label('Feet').fill(feet) + context.page.get_by_label('Inches').fill(inches) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my weight with "{weight}"') +def when_i_fill_in_and_submit_my_weight(context, weight): + context.page.get_by_label('Kilograms').fill(weight) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my weight with "{stone}" stone and "{pounds}" pound') +@when(u'I fill in and submit my weight with "{stone}" stone and "{pounds}" pounds') +def stone_and_pounds(context, stone, pounds): + context.page.get_by_label('Stone').fill(stone) + context.page.get_by_label('Pounds').fill(pounds) + when_i_submit_the_form(context) + +@when(u'I fill in and submit my sex at birth with "{sex_at_birth}"') +def when_i_fill_in_and_submit_my_sex_at_birth(context, sex_at_birth): + context.page.get_by_label(sex_at_birth, exact=True).check() + when_i_submit_the_form(context) + +@when(u'I fill in and submit my gender with "{gender}"') +def when_i_fill_in_and_submit_my_gender(context, gender): + context.page.get_by_label(gender, exact=True).check() + when_i_submit_the_form(context) + +@when(u'I fill in and submit my ethnicity with "{ethnicity}"') +def when_i_fill_in_and_submit_my_ethnicity(context, ethnicity): + context.page.get_by_label(ethnicity, exact=True).check() + when_i_submit_the_form(context) + +@when(u'I fill in and submit my respiratory conditions with "{condition_a}" and "{condition_b}"') +def when_i_fill_in_and_submit_my_respiratory_conditions(context, condition_a, condition_b): + context.page.get_by_label(condition_a).check() + context.page.get_by_label(condition_b).check() + when_i_submit_the_form(context) + +@when(u'I fill in and submit my asbestos exposure with "{asbestos_exposure}"') +def when_i_fill_in_and_submit_my_asbestos_exposure(context, asbestos_exposure): + context.page.get_by_label(asbestos_exposure, exact=True).check() + when_i_submit_the_form(context) + +@when('I submit the form') +def when_i_submit_the_form(context): + context.page.click("text=Continue") diff --git a/tests/features/steps/navigation_steps.py b/tests/features/steps/navigation_steps.py new file mode 100644 index 00000000..d4427706 --- /dev/null +++ b/tests/features/steps/navigation_steps.py @@ -0,0 +1,15 @@ +from behave import when, then +from playwright.sync_api import expect + + +@when('I go to "{path}"') +def given_i_go_to(context, path): + context.page.goto(f"{context.live_server_url}{path}") + +@when('I click "{text}"') +def when_i_click(context, text): + context.page.click(f"text={text}") + +@then('I am on "{path}"') +def then_i_am_on(context, path): + expect(context.page).to_have_url(f"{context.live_server_url}{path}") diff --git a/tests/features/steps/page_assertion_steps.py b/tests/features/steps/page_assertion_steps.py new file mode 100644 index 00000000..709ae8db --- /dev/null +++ b/tests/features/steps/page_assertion_steps.py @@ -0,0 +1,25 @@ +from behave import given, then +from playwright.sync_api import expect +from datetime import datetime +from dateutil.relativedelta import relativedelta + +@then(u'I see a title "{title}"') +def then_i_see_a_title(context, title): + expect(context.page.locator('.title')).to_have_text(title) + +@then(u'I see a back link to "{url}"') +def then_i_see_a_back_link_to(context, url): + back_link = context.page.locator(".nhsuk-back-link") + expect(back_link).to_have_count(1) + expect(back_link).to_have_attribute("href", url) + +@then(u'I see responses "{text}"') +def then_i_see_responses(context, text): + responses = context.page.locator(".responses") + expect(responses).to_contain_text(text) + +@then(u'I see responses "{text}" with a date {years} years ago') +def then_i_see_responses_with_a_date(context, text, years): + date_of_birth = datetime.now() - relativedelta(years=int(years)) + responses = context.page.locator(".responses") + expect(responses).to_contain_text(f"{text} {date_of_birth.strftime('%Y-%m-%d')}") diff --git a/tests/features/steps/participant_steps.py b/tests/features/steps/participant_steps.py new file mode 100644 index 00000000..1db7711d --- /dev/null +++ b/tests/features/steps/participant_steps.py @@ -0,0 +1,25 @@ +from behave import given +from django.utils import timezone +from lung_cancer_screening.questions.models.participant import Participant + + +@given('a participant {participant_id} exists') +@given('the participant {participant_id} exists') +def given_a_participant_exists(context, participant_id): + Participant.objects.create(unique_id=participant_id) + + +@given('I have already submitted my responses') +def given_i_have_already_submitted_my_responses(context): + context.participant_id = 'abc123' + participant = Participant.objects.create(unique_id=context.participant_id) + participant.responseset_set.create( + submitted_at=timezone.now() + ) + +@given('I have started the questionnaire') +def given_i_have_started_the_questionnaire(context): + context.participant_id = 'abc123' + context.page.goto(f'{context.live_server_url}/start') + context.page.fill('input[name="participant_id"]', context.participant_id) + context.page.click('button[type="submit"]') diff --git a/tests/features/validation_errors.feature b/tests/features/validation_errors.feature new file mode 100644 index 00000000..3f7b26ea --- /dev/null +++ b/tests/features/validation_errors.feature @@ -0,0 +1,99 @@ +Feature: Validation errors + Scenario: Date of birth form errors + Given I have started the questionnaire + When I go to "/date-of-birth" + And I submit the form + Then I am on "/date-of-birth" + And I see a form error "Enter your date of birth." + + Scenario: Height form errors + Given I have started the questionnaire + When I go to "/height" + And I submit the form + Then I am on "/height" + And I see a form error "Enter your height." + When I fill in and submit my height with "139.6" + Then I am on "/height" + And I see a form error "Height must be between 139.7cm and 243.8 cm" + When I fill in and submit my height with "243.9" + Then I am on "/height" + And I see a form error "Height must be between 139.7cm and 243.8 cm" + + Scenario: Height imperial form errors + Given I have started the questionnaire + When I go to "/height?unit=imperial" + And I submit the form + Then I am on "/height?unit=imperial" + And I see a form error "Enter your height." + When I fill in and submit my height with "5.2" feet and "2" inches + Then I am on "/height?unit=imperial" + And I see a form error "Feet must be in whole numbers" + When I fill in and submit my height with "5" feet and "2.2" inches + Then I am on "/height?unit=imperial" + And I see a form error "Inches must be in whole numbers" + When I fill in and submit my height with "8" feet and "1" inch + Then I am on "/height?unit=imperial" + And I see a form error "Height must be between 4 feet 7 inches and 8 feet" + + Scenario: Weight form errors + Given I have started the questionnaire + When I go to "/weight" + And I submit the form + Then I am on "/weight" + And I see a form error "Enter your weight." + When I fill in and submit my weight with "25.3" + Then I am on "/weight" + And I see a form error "Weight must be between 25.4kg and 317.5kg" + When I fill in and submit my weight with "317.6" + Then I am on "/weight" + And I see a form error "Weight must be between 25.4kg and 317.5kg" + + Scenario: Weight imperial form errors + Given I have started the questionnaire + When I go to "/weight?unit=imperial" + And I submit the form + Then I am on "/weight?unit=imperial" + And I see a form error "Enter your weight." + When I fill in and submit my weight with "5.2" stone and "2" pounds + Then I am on "/weight?unit=imperial" + And I see a form error "Stone must be in whole numbers" + When I fill in and submit my weight with "5" stone and "2.2" pounds + Then I am on "/weight?unit=imperial" + And I see a form error "Pounds must be in whole numbers" + When I fill in and submit my weight with "3" stone and "12" pounds + Then I am on "/weight?unit=imperial" + And I see a form error "Weight must be between 4 stone and 50 stone" + When I fill in and submit my weight with "50" stone and "1" pound + Then I am on "/weight?unit=imperial" + And I see a form error "Weight must be between 4 stone and 50 stone" + + Scenario: Sex at birth form errors + Given I have started the questionnaire + When I go to "/sex-at-birth" + And I submit the form + Then I am on "/sex-at-birth" + And I see a form error "Select your sex at birth." + + Scenario: Gender form errors + Given I have started the questionnaire + When I go to "/gender" + And I submit the form + Then I am on "/gender" + And I see a form error "Select the option that best describes your gender." + + Scenario: Ethnicity form errors + Given I have started the questionnaire + When I go to "/ethnicity" + And I submit the form + Then I am on "/ethnicity" + And I see a form error "Select your ethnic background." + + Scenario: Respiratory conditions form errors + Given I have started the questionnaire + When I go to "/respiratory-conditions" + And I submit the form + Then I am on "/respiratory-conditions" + And I see a form error "Select if you have had any respiratory conditions" + When I fill in and submit my respiratory conditions with "Bronchitis" and "No, I have not had any of these respiratory conditions" + Then I am on "/respiratory-conditions" + And I see a form error "Select if you have had any respiratory conditions, or select 'No, I have not had any of these respiratory conditions'"